From 2a7f395e785b29de3414f4195f7814d5e57e98af Mon Sep 17 00:00:00 2001 From: Gabriel Ramos Date: Mon, 18 May 2026 14:28:43 -0300 Subject: [PATCH 01/15] remove need to have an initialized player to show a portal extension --- .../PortalExtension/PortalExtensionDiscoveryService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/cli/Services/PortalExtension/PortalExtensionDiscoveryService.cs b/cli/cli/Services/PortalExtension/PortalExtensionDiscoveryService.cs index e13eb22c51..a63027a9c0 100644 --- a/cli/cli/Services/PortalExtension/PortalExtensionDiscoveryService.cs +++ b/cli/cli/Services/PortalExtension/PortalExtensionDiscoveryService.cs @@ -21,7 +21,8 @@ public class ExtensionBuildMetaData public class PortalExtensionDiscoveryService : Microservice { - [ClientCallable] + // Note: this creates source code leaking, even tho the service has an ever changing Guid + [Callable] public ExtensionBuildData RequestPortalExtensionData(string currentHash = "") { var observer = Provider.GetService(); From d5ac5e6f1e86f382393207be0a8365372d208563 Mon Sep 17 00:00:00 2001 From: Gabriel Ramos Date: Mon, 18 May 2026 14:29:50 -0300 Subject: [PATCH 02/15] fix dev web script for newer pnpm version --- dev-web.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dev-web.sh b/dev-web.sh index 51ee41fc86..5b77fa5852 100755 --- a/dev-web.sh +++ b/dev-web.sh @@ -77,8 +77,8 @@ echo " [cmd] pnpm version $VERSION --no-git-tag-version" pnpm version "$VERSION" --no-git-tag-version echo " [cmd] pnpm build" pnpm build -echo " [cmd] pnpm publish --registry $REGISTRY --no-git-checks" -pnpm publish --registry "$REGISTRY" --no-git-checks +echo " [cmd] pnpm publish --registry $REGISTRY --no-git-checks --tag local" +pnpm publish --registry "$REGISTRY" --no-git-checks --tag local cp package.json.devbak package.json && rm package.json.devbak SDK_BACKUP=false @@ -119,8 +119,8 @@ pnpm version "$VERSION" --no-git-tag-version echo " [cmd] pnpm build" pnpm build -echo " [cmd] pnpm publish --registry $REGISTRY --no-git-checks" -pnpm publish --registry "$REGISTRY" --no-git-checks +echo " [cmd] pnpm publish --registry $REGISTRY --no-git-checks --tag local" +pnpm publish --registry "$REGISTRY" --no-git-checks --tag local cp package.json.devbak package.json && rm package.json.devbak TOOLKIT_BACKUP=false From 440786340da89c85c6df516daf9913f5a38d9733 Mon Sep 17 00:00:00 2001 From: Gabriel Ramos Date: Mon, 18 May 2026 16:25:28 -0300 Subject: [PATCH 03/15] Fix error when trying to add dep to PE and generating client --- .../PortalExtensionConstants.cs | 1 + .../PortalExtensionAddDependencyCommand.cs | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/cli/beamable.common/Runtime/Constants/Implementations/PortalExtensionConstants.cs b/cli/beamable.common/Runtime/Constants/Implementations/PortalExtensionConstants.cs index ab36c64110..e5a87d5714 100644 --- a/cli/beamable.common/Runtime/Constants/Implementations/PortalExtensionConstants.cs +++ b/cli/beamable.common/Runtime/Constants/Implementations/PortalExtensionConstants.cs @@ -7,6 +7,7 @@ public static partial class Features public static partial class PortalExtension { public const string EXTENSION_DEPENDENCIES_PROPERTY_NAME = "microserviceDependencies"; + public const string EXTENSION_BEAMABLE_PROPERTY_NAME = "beamable"; public const string EXTENSION_NAME_PROPERTY_NAME = "name"; } } diff --git a/cli/cli/Commands/Portal/PortalExtensionAddDependencyCommand.cs b/cli/cli/Commands/Portal/PortalExtensionAddDependencyCommand.cs index d59e9b6084..2b849171a9 100644 --- a/cli/cli/Commands/Portal/PortalExtensionAddDependencyCommand.cs +++ b/cli/cli/Commands/Portal/PortalExtensionAddDependencyCommand.cs @@ -1,7 +1,9 @@ +using Beamable.Server; using cli.Services; using cli.Services.Web; using Newtonsoft.Json.Linq; using System.CommandLine; +using static Beamable.Common.Constants.Features.PortalExtension; namespace cli.Portal; @@ -62,7 +64,13 @@ public override Task Handle(PortalExtensionAddDependencyCommandArgs args) extension.MicroserviceDependencies.Add(microservice.BeamoId); - root[Beamable.Common.Constants.Features.PortalExtension.EXTENSION_DEPENDENCIES_PROPERTY_NAME] = + if (root[EXTENSION_BEAMABLE_PROPERTY_NAME] == null) + { + throw new CliException( + $"Field {EXTENSION_BEAMABLE_PROPERTY_NAME} expected in extension pakage.json file"); + } + + root[EXTENSION_BEAMABLE_PROPERTY_NAME][EXTENSION_DEPENDENCIES_PROPERTY_NAME] = JToken.FromObject(extension.MicroserviceDependencies); File.WriteAllText(packagePath, root.ToString(Newtonsoft.Json.Formatting.Indented)); @@ -86,8 +94,14 @@ public static void GenerateDependenciesClients(string extensionPath, BeamoLocalM foreach ((string beamId, HttpMicroserviceLocalProtocol localProtocol) in manifest.HttpMicroserviceLocalProtocols) { - if (!dependencies.Contains(beamId) || localProtocol.OpenApiDoc == null) + if (!dependencies.Contains(beamId)) + { + continue; + } + + if (localProtocol.OpenApiDoc == null) { + Log.Warning($"Client generation for {beamId} is being skipped because there is no API doc. Try running {beamId} once to make it available and try again."); continue; } From b100cdab033c5934dd6b457fbde695865e096dfa Mon Sep 17 00:00:00 2001 From: Gabriel Ramos Date: Mon, 18 May 2026 16:55:49 -0300 Subject: [PATCH 04/15] websdk fix, content manifest 404 should be considered just empty manifest --- web/src/services/ContentService.ts | 81 ++++++++++++++++++------------ 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/web/src/services/ContentService.ts b/web/src/services/ContentService.ts index e579f419ee..2861591547 100644 --- a/web/src/services/ContentService.ts +++ b/web/src/services/ContentService.ts @@ -398,49 +398,66 @@ export class ContentService const contentStorage = await this.contentStoragePromise; const checksumKey = this.getManifestChecksumKey(manifestId); - // Fetch the locally stored checksum and the latest checksum from the API in parallel. - const [cachedChecksum, { body: latestChecksum }] = await Promise.all([ - contentStorage.get(checksumKey), - contentGetManifestChecksumBasic( - this.requester, - manifestId, - undefined, - this.accountId, - ), - ]); - - // Compare the checksums to see if an update is needed. - if (cachedChecksum?.checksum === latestChecksum.checksum) { - // Checksums match. The local version is up-to-date. - const entriesKey = this.getManifestEntriesKey(manifestId); - const manifestEntries = - await contentStorage.get(entriesKey); - - if (!manifestEntries) { - // Manifest entries not found in storage. Fetch them from the API. + let latestChecksum; + try { + // Fetch the locally stored checksum and the latest checksum from the API in parallel. + const [cachedChecksum, checksumResponse] = await Promise.all([ + contentStorage.get(checksumKey), + contentGetManifestChecksumBasic( + this.requester, + manifestId, + undefined, + this.accountId, + ), + ]); + latestChecksum = checksumResponse.body; + + // Compare the checksums to see if an update is needed. + if (cachedChecksum?.checksum === latestChecksum.checksum) { + // Checksums match. The local version is up-to-date. + const entriesKey = this.getManifestEntriesKey(manifestId); + const manifestEntries = + await contentStorage.get(entriesKey); + + if (!manifestEntries) { + // Manifest entries not found in storage. Fetch them from the API. + await this.fetchAndCacheManifestEntries(manifestId, { + id: latestChecksum.id, + checksum: latestChecksum.checksum, + created: latestChecksum.createdAt, + uid: latestChecksum.uid, + }); + return; + } + + // Manifest entries found in storage. Load them into the in-memory cache. + ContentService._manifestChecksumsCache[manifestId] = cachedChecksum; + ContentService._manifestEntriesCache[manifestId] = manifestEntries; + } else { + // Checksums differ. Fetch the manifest entries from the API and cache it. await this.fetchAndCacheManifestEntries(manifestId, { id: latestChecksum.id, checksum: latestChecksum.checksum, created: latestChecksum.createdAt, uid: latestChecksum.uid, }); + } + } catch (err) { + if (ContentService.isManifestNotFound(err)) { + // Realm has no published content — treat as empty manifest. + ContentService._manifestEntriesCache[manifestId] = []; return; } - - // Manifest entries found in storage. Load them into the in-memory cache. - ContentService._manifestChecksumsCache[manifestId] = cachedChecksum; - ContentService._manifestEntriesCache[manifestId] = manifestEntries; - } else { - // Checksums differ. Fetch the manifest entries from the API and cache it. - await this.fetchAndCacheManifestEntries(manifestId, { - id: latestChecksum.id, - checksum: latestChecksum.checksum, - created: latestChecksum.createdAt, - uid: latestChecksum.uid, - }); + throw err; } } + private static isManifestNotFound(err: unknown): boolean { + if (!BeamError.is(err)) return false; + const status = (err.context?.response as { status?: number })?.status; + return status === 404; + } + /** Fetches a content manifest entries from the API and caches it locally. */ private async fetchAndCacheManifestEntries( manifestId: string, From f0d3e6269b47156e7a302f0988d070cdf88cad41 Mon Sep 17 00:00:00 2001 From: Gabriel Ramos Date: Wed, 20 May 2026 13:49:31 -0300 Subject: [PATCH 05/15] fix portal extension client gen --- .../Commands/Portal/PortalExtensionAddDependencyCommand.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/cli/Commands/Portal/PortalExtensionAddDependencyCommand.cs b/cli/cli/Commands/Portal/PortalExtensionAddDependencyCommand.cs index 2b849171a9..12a214bb90 100644 --- a/cli/cli/Commands/Portal/PortalExtensionAddDependencyCommand.cs +++ b/cli/cli/Commands/Portal/PortalExtensionAddDependencyCommand.cs @@ -67,7 +67,7 @@ public override Task Handle(PortalExtensionAddDependencyCommandArgs args) if (root[EXTENSION_BEAMABLE_PROPERTY_NAME] == null) { throw new CliException( - $"Field {EXTENSION_BEAMABLE_PROPERTY_NAME} expected in extension pakage.json file"); + $"Field {EXTENSION_BEAMABLE_PROPERTY_NAME} expected in extension package.json file"); } root[EXTENSION_BEAMABLE_PROPERTY_NAME][EXTENSION_DEPENDENCIES_PROPERTY_NAME] = @@ -118,7 +118,7 @@ public static List GetDependenciesFromPath(string extensionPath) JObject root = JObject.Parse(jsonContent); - JToken deps = root[Beamable.Common.Constants.Features.PortalExtension.EXTENSION_DEPENDENCIES_PROPERTY_NAME]; + JToken deps = root[EXTENSION_BEAMABLE_PROPERTY_NAME][EXTENSION_DEPENDENCIES_PROPERTY_NAME]; if (deps is { Type: JTokenType.Array }) { From 9230fdc474d2d26e67f3d3bda40437eda4b543ee Mon Sep 17 00:00:00 2001 From: Gabriel Ramos Date: Wed, 20 May 2026 16:53:45 -0300 Subject: [PATCH 06/15] generate typescript client for pe and fix client generation --- .../Commands/Portal/PortalExtensionAddDependencyCommand.cs | 3 +-- cli/cli/Services/Web/WebClientCodeGenerator.cs | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cli/cli/Commands/Portal/PortalExtensionAddDependencyCommand.cs b/cli/cli/Commands/Portal/PortalExtensionAddDependencyCommand.cs index 12a214bb90..4b5e2cc983 100644 --- a/cli/cli/Commands/Portal/PortalExtensionAddDependencyCommand.cs +++ b/cli/cli/Commands/Portal/PortalExtensionAddDependencyCommand.cs @@ -105,7 +105,7 @@ public static void GenerateDependenciesClients(string extensionPath, BeamoLocalM continue; } - var generator = new WebClientCodeGenerator(localProtocol.OpenApiDoc, "js"); + var generator = new WebClientCodeGenerator(localProtocol.OpenApiDoc, "ts"); var clientsOutputDirectory = Path.Combine(extensionPath, "beamable/clients"); generator.GenerateClientCode(clientsOutputDirectory); } @@ -122,7 +122,6 @@ public static List GetDependenciesFromPath(string extensionPath) if (deps is { Type: JTokenType.Array }) { - // Convert the JArray directly to a List return deps.ToObject>(); } diff --git a/cli/cli/Services/Web/WebClientCodeGenerator.cs b/cli/cli/Services/Web/WebClientCodeGenerator.cs index d2baa4aaf7..bdded5e11c 100644 --- a/cli/cli/Services/Web/WebClientCodeGenerator.cs +++ b/cli/cli/Services/Web/WebClientCodeGenerator.cs @@ -1,4 +1,4 @@ -using Beamable.Server; +using Beamable.Server; using cli.Services.Web.CodeGen; using cli.Services.Web.Helpers; using Microsoft.OpenApi.Any; @@ -74,7 +74,7 @@ public WebClientCodeGenerator(OpenApiDocument document, string langType) if (_schemas.Count > 0) { var tsBeamSchemaImport = - new TsImport("beamable-sdk/schema", defaultImport: "* as Schemas", typeImportOnly: true); + new TsImport("@beamable/sdk/schema", defaultImport: "* as Schemas", typeImportOnly: true); _clientFile.AddImport(tsBeamSchemaImport); } @@ -283,7 +283,7 @@ private static void GenerateBeamSDKModuleDeclaration(TsFile clientFile, TsClass var className = tsClass.Name; var camelCaseClassName = StringHelper.ToCamelCaseIdentifier(className); var serviceName = tsClass.Name.Replace("Client", string.Empty); - var tsModule = new TsModule("beamable-sdk"); + var tsModule = new TsModule("@beamable/sdk"); var tsInterface = new TsInterface("BeamBase"); var tsProperty = new TsProperty(camelCaseClassName, TsType.Of(className)); var tsComment = new TsComment( From 89a4720dfae708d5f5931cf5013e82368572172e Mon Sep 17 00:00:00 2001 From: Gabriel Ramos Date: Wed, 20 May 2026 17:02:47 -0300 Subject: [PATCH 07/15] update react template --- .../templates/PortalExtensionReactApp/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/beamable.templates/templates/PortalExtensionReactApp/tsconfig.json b/cli/beamable.templates/templates/PortalExtensionReactApp/tsconfig.json index cd9de40850..9472ad83f1 100644 --- a/cli/beamable.templates/templates/PortalExtensionReactApp/tsconfig.json +++ b/cli/beamable.templates/templates/PortalExtensionReactApp/tsconfig.json @@ -14,5 +14,5 @@ "noEmit": true, "types": ["vite/client"] }, - "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts"] + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "beamable/**/*.ts"] } From 22bc8bee2081fde5d3d200d59cae91a19abe7f47 Mon Sep 17 00:00:00 2001 From: Gabriel Ramos Date: Fri, 22 May 2026 17:15:54 -0300 Subject: [PATCH 08/15] fix template updater --- template_updater/update-template.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/template_updater/update-template.cs b/template_updater/update-template.cs index facadd084e..b2e3527713 100644 --- a/template_updater/update-template.cs +++ b/template_updater/update-template.cs @@ -5,7 +5,7 @@ using System.Text.RegularExpressions; // ── Args ─────────────────────────────────────────────────────────────────────── -var validTemplates = new[] { "BeamService", "BeamStorage", "PortalExtensionApp" }; +var validTemplates = new[] { "BeamService", "BeamStorage", "PortalExtensionApp", "PortalExtensionReactApp" }; if (args.Length < 1) { Usage(); return; } @@ -85,6 +85,7 @@ break; } case "PortalExtensionApp": + case "PortalExtensionReactApp": { foreach (string f in EnumerateFiles(searchPath, "package.json", ignorePaths, [templatesDir])) { From 92097eff45dc80e5f8fe22b91a7295f9c3c3e688 Mon Sep 17 00:00:00 2001 From: Chris Hanna Date: Tue, 26 May 2026 15:39:19 -0400 Subject: [PATCH 09/15] debug install log --- client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs b/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs index d93f72e198..28f06b0509 100644 --- a/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs +++ b/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs @@ -180,6 +180,7 @@ static bool InstallTool() message.AppendLine($"Error: {error}"); message.Append(errorGuide); + Debug.Log("Failed Install Command: " + installCommand); Debug.LogError(message.ToString()); var tryAgainButtonInfo = new OptionDialogWindow.ButtonInfo() { From e273d00beed93e458295e33a0ad783f1cfd9863a Mon Sep 17 00:00:00 2001 From: Chris Hanna Date: Tue, 26 May 2026 16:30:37 -0400 Subject: [PATCH 10/15] attempt install --- .../Editor/BeamCli/BeamCliUtil.cs | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs b/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs index 28f06b0509..af7cc5f6a1 100644 --- a/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs +++ b/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs @@ -163,6 +163,8 @@ static bool InstallTool() RedirectStandardError = true }; proc.StartInfo.Environment.Add("DOTNET_CLI_UI_LANGUAGE", "en"); + Debug.Log($"[BeamCLI] Installing Beam CLI version [{BeamableEnvironment.NugetPackageVersion}]. " + + $"Command: [dotnet {installCommand}]. A first-time install with a cold NuGet cache can be slow while packages download."); TryRunWithTimeout(1); const string errorGuide = @@ -221,14 +223,34 @@ static bool InstallTool() bool TryRunWithTimeout(int currentTry) { + var timeoutMs = 30 * 1000 * currentTry; + Debug.Log($"[BeamCLI] Install attempt {currentTry} starting (timeout {timeoutMs / 1000}s)..."); + var stopwatch = Stopwatch.StartNew(); proc.Start(); - if (proc.WaitForExit(10 * 1000 * currentTry)) + if (proc.WaitForExit(timeoutMs)) { + Debug.Log($"[BeamCLI] Install attempt {currentTry} exited with code {proc.ExitCode} after {stopwatch.Elapsed.TotalSeconds:0.#}s."); return true; } + // Kill the still-running process before retrying; a concurrent retry corrupts the NuGet cache (a version folder left without its .nupkg), which then fails every later restore. Debug.LogError( - "dotnet tool install command did not finish fast enough; timed out. Trying again with longer timeout"); + $"[BeamCLI] Install attempt {currentTry} did not finish within {timeoutMs / 1000}s. " + + "Killing the dotnet process before retrying to avoid corrupting the NuGet cache."); + try + { + if (!proc.HasExited) + { + proc.Kill(); + } + proc.WaitForExit(); + Debug.Log($"[BeamCLI] Killed the timed-out dotnet process from attempt {currentTry}."); + } + catch (Exception killEx) + { + Debug.LogWarning($"[BeamCLI] Failed to kill the timed-out dotnet install process: {killEx.Message}"); + } + const int maxRetries = 5; if (currentTry > maxRetries) { @@ -246,6 +268,7 @@ bool TryRunWithTimeout(int currentTry) return false; } + Debug.Log($"[BeamCLI] Retrying install with a longer timeout (attempt {currentTry + 1})."); return TryRunWithTimeout(++currentTry); } From 510b6b16206aa3dcfab07b42dcb403faa01b442a Mon Sep 17 00:00:00 2001 From: Chris Hanna Date: Tue, 26 May 2026 17:21:47 -0400 Subject: [PATCH 11/15] gate source --- client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs b/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs index af7cc5f6a1..a21a246842 100644 --- a/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs +++ b/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs @@ -138,7 +138,10 @@ static bool InstallTool() var proc = new Process(); var installCommand = $"tool install Beamable.Tools --create-manifest-if-needed --allow-downgrade"; - if (Application.isBatchMode) + // Only add the local folder feed for the local dev version (0.0.123.*). For a published + // version, installing from a local folder feed extracts into the global packages folder + // without copying the .nupkg, which then crashes finalization; let it resolve from nuget.org. + if (Application.isBatchMode && BeamableEnvironment.NugetPackageVersion.ToString().StartsWith("0.0.123")) { installCommand += " --add-source BeamableNugetSource "; } From a10c06192d5a1f02931f38b6a7fda008deda34bb Mon Sep 17 00:00:00 2001 From: Chris Hanna Date: Wed, 27 May 2026 06:33:42 -0400 Subject: [PATCH 12/15] cli issues --- .../Editor/BeamCli/BeamCliUtil.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs b/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs index a21a246842..3477931ccf 100644 --- a/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs +++ b/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs @@ -166,6 +166,8 @@ static bool InstallTool() RedirectStandardError = true }; proc.StartInfo.Environment.Add("DOTNET_CLI_UI_LANGUAGE", "en"); + // Remove a corrupt global-packages entry (extracted, missing its .nupkg) so the install re-downloads cleanly instead of short-circuiting on it. + HealCorruptGlobalPackagesEntry(); Debug.Log($"[BeamCLI] Installing Beam CLI version [{BeamableEnvironment.NugetPackageVersion}]. " + $"Command: [dotnet {installCommand}]. A first-time install with a cold NuGet cache can be slow while packages download."); TryRunWithTimeout(1); @@ -275,6 +277,45 @@ bool TryRunWithTimeout(int currentTry) return TryRunWithTimeout(++currentTry); } + + void HealCorruptGlobalPackagesEntry() + { + try + { + var packageId = "beamable.tools"; + var version = BeamableEnvironment.NugetPackageVersion.ToString().ToLowerInvariant(); + var globalPackages = Environment.GetEnvironmentVariable("NUGET_PACKAGES"); + if (string.IsNullOrEmpty(globalPackages)) + { + var home = Environment.GetEnvironmentVariable("HOME"); + if (string.IsNullOrEmpty(home)) + { + home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + } + globalPackages = Path.Combine(home, ".nuget", "packages"); + } + + var packageDir = Path.Combine(globalPackages, packageId, version); + if (!Directory.Exists(packageDir)) + { + Debug.Log($"[BeamCLI] No pre-existing global-packages entry at [{packageDir}]."); + return; + } + + var nupkgCount = Directory.GetFiles(packageDir, "*.nupkg").Length; + var totalFiles = Directory.GetFiles(packageDir, "*", SearchOption.AllDirectories).Length; + Debug.Log($"[BeamCLI] Found global-packages entry [{packageDir}]: {nupkgCount} .nupkg, {totalFiles} total files."); + if (nupkgCount == 0) + { + Debug.LogWarning($"[BeamCLI] Entry [{packageDir}] is missing its .nupkg (corrupt). Deleting it so the install can re-download a complete copy."); + Directory.Delete(packageDir, true); + } + } + catch (Exception healEx) + { + Debug.LogWarning($"[BeamCLI] Auto-heal of the global-packages cache failed: {healEx.Message}"); + } + } } } } From 8b8db2192ee1e304934eceb4bd8cfadb59ce0bf5 Mon Sep 17 00:00:00 2001 From: Chris Hanna Date: Wed, 27 May 2026 09:05:11 -0400 Subject: [PATCH 13/15] namespace --- client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs b/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs index 3477931ccf..046107bc48 100644 --- a/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs +++ b/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs @@ -284,13 +284,13 @@ void HealCorruptGlobalPackagesEntry() { var packageId = "beamable.tools"; var version = BeamableEnvironment.NugetPackageVersion.ToString().ToLowerInvariant(); - var globalPackages = Environment.GetEnvironmentVariable("NUGET_PACKAGES"); + var globalPackages = System.Environment.GetEnvironmentVariable("NUGET_PACKAGES"); if (string.IsNullOrEmpty(globalPackages)) { - var home = Environment.GetEnvironmentVariable("HOME"); + var home = System.Environment.GetEnvironmentVariable("HOME"); if (string.IsNullOrEmpty(home)) { - home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile); } globalPackages = Path.Combine(home, ".nuget", "packages"); } From 0d584b2fa705518531775f8ef30671a6fa256496 Mon Sep 17 00:00:00 2001 From: Chris Hanna Date: Wed, 27 May 2026 10:22:02 -0400 Subject: [PATCH 14/15] test --- .../Editor/BeamCli/BeamCliUtil.cs | 105 +++++++++++++++--- 1 file changed, 91 insertions(+), 14 deletions(-) diff --git a/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs b/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs index 046107bc48..023b77c92b 100644 --- a/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs +++ b/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs @@ -135,7 +135,6 @@ static bool InstallTool() return true; } - var proc = new Process(); var installCommand = $"tool install Beamable.Tools --create-manifest-if-needed --allow-downgrade"; // Only add the local folder feed for the local dev version (0.0.123.*). For a published @@ -155,19 +154,7 @@ static bool InstallTool() installCommand += $" --version {BeamableEnvironment.NugetPackageVersion}.*"; } - proc.StartInfo = new ProcessStartInfo - { - FileName = "dotnet", - WorkingDirectory = Path.GetFullPath("."), - Arguments = installCommand, - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true - }; - proc.StartInfo.Environment.Add("DOTNET_CLI_UI_LANGUAGE", "en"); - // Remove a corrupt global-packages entry (extracted, missing its .nupkg) so the install re-downloads cleanly instead of short-circuiting on it. - HealCorruptGlobalPackagesEntry(); + var proc = NewInstallProcess(); Debug.Log($"[BeamCLI] Installing Beam CLI version [{BeamableEnvironment.NugetPackageVersion}]. " + $"Command: [dotnet {installCommand}]. A first-time install with a cold NuGet cache can be slow while packages download."); TryRunWithTimeout(1); @@ -177,6 +164,22 @@ static bool InstallTool() var output = proc.StandardOutput.ReadToEnd(); var error = proc.StandardError.ReadToEnd(); + + // On some SDKs the local tool installer caches the package without its .nupkg and then crashes finalizing it. Only when we see that exact failure, pre-download the package via a normal restore (which keeps the .nupkg) and retry once, so unaffected SDKs never pay the cost. + var installLog = (output ?? string.Empty) + (error ?? string.Empty); + if (proc.ExitCode != 0 && installLog.Contains(".nupkg") && + (installLog.Contains("Could not find file") || installLog.Contains("FileNotFoundException"))) + { + Debug.LogWarning("[BeamCLI] Install failed with a missing .nupkg in the global packages cache (known local tool-install issue). Healing the cache and pre-downloading the package, then retrying once."); + HealCorruptGlobalPackagesEntry(); + WarmGlobalPackagesCache(); + + proc = NewInstallProcess(); + TryRunWithTimeout(1); + output = proc.StandardOutput.ReadToEnd(); + error = proc.StandardError.ReadToEnd(); + } + if (!string.IsNullOrWhiteSpace(error) || proc.ExitCode != 0) { StringBuilder message = new StringBuilder("Unable to install BeamCLI"); @@ -226,6 +229,25 @@ static bool InstallTool() return proc.ExitCode == 0; + Process NewInstallProcess() + { + var p = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "dotnet", + WorkingDirectory = Path.GetFullPath("."), + Arguments = installCommand, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + } + }; + p.StartInfo.Environment.Add("DOTNET_CLI_UI_LANGUAGE", "en"); + return p; + } + bool TryRunWithTimeout(int currentTry) { var timeoutMs = 30 * 1000 * currentTry; @@ -278,6 +300,61 @@ bool TryRunWithTimeout(int currentTry) } + void WarmGlobalPackagesCache() + { + var version = BeamableEnvironment.NugetPackageVersion.ToString(); + // The local dev flow installs 0.0.123.* from the local folder feed, which already + // lands the .nupkg in the cache, so there is nothing to warm (and it is not on nuget.org). + if (version.StartsWith("0.0.123")) + { + return; + } + + try + { + var warmDir = Path.Combine(Path.GetFullPath("Library/BeamableEditor"), "cli-cache-warm"); + Directory.CreateDirectory(warmDir); + var csprojPath = Path.Combine(warmDir, "warm.csproj"); + File.WriteAllText(csprojPath, + "\n" + + " net8.0\n" + + $" \n" + + "\n"); + + var warm = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "dotnet", + WorkingDirectory = warmDir, + Arguments = $"restore \"{csprojPath}\"", + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + } + }; + warm.StartInfo.Environment.Add("DOTNET_CLI_UI_LANGUAGE", "en"); + Debug.Log($"[BeamCLI] Warming the NuGet cache with Beamable.Tools [{version}] via PackageDownload restore..."); + warm.Start(); + if (!warm.WaitForExit(120 * 1000)) + { + try { if (!warm.HasExited) warm.Kill(); } + catch { /* ignore */ } + Debug.LogWarning("[BeamCLI] Cache-warm restore timed out; continuing to the install anyway."); + return; + } + + var output = warm.StandardOutput.ReadToEnd(); + var error = warm.StandardError.ReadToEnd(); + Debug.Log($"[BeamCLI] Cache-warm restore exited with code {warm.ExitCode}. {output} {error}"); + } + catch (Exception warmEx) + { + Debug.LogWarning($"[BeamCLI] Cache-warm failed (continuing to the install anyway): {warmEx.Message}"); + } + } + void HealCorruptGlobalPackagesEntry() { try From ad1675cfabc8b096d1f2bd41abc6b9688f922d9d Mon Sep 17 00:00:00 2001 From: Chris Hanna Date: Wed, 27 May 2026 11:04:52 -0400 Subject: [PATCH 15/15] lower case --- .../Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs b/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs index 023b77c92b..547a3b8b71 100644 --- a/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs +++ b/client/Packages/com.beamable/Editor/BeamCli/BeamCliUtil.cs @@ -145,13 +145,15 @@ static bool InstallTool() installCommand += " --add-source BeamableNugetSource "; } - if (!BeamableEnvironment.NugetPackageVersion.ToString().Equals("0.0.123")) + // Lowercase the version: dotnet tool install builds the global-packages path from the raw --version string, so an uppercase pre-release label (7.2.0-PREVIEW.RC1) mismatches NuGet's lowercased cache folder on case-sensitive (Linux) filesystems. + var versionArg = BeamableEnvironment.NugetPackageVersion.ToString().ToLowerInvariant(); + if (!versionArg.Equals("0.0.123")) { - installCommand += $" --version {BeamableEnvironment.NugetPackageVersion}"; + installCommand += $" --version {versionArg}"; } else { - installCommand += $" --version {BeamableEnvironment.NugetPackageVersion}.*"; + installCommand += $" --version {versionArg}.*"; } var proc = NewInstallProcess();