Skip to content

Commit 1b6a0a3

Browse files
dooly123claude
andcommitted
Add "Basis Recommended" one-click pack; suggest it after cloning
Replace the demo "Basis Starter Pack" bundle with "Basis Recommended" — every curated registry package, added on top of a fresh Basis clone in one press. The static-site generator refills the bundle from the full package list so it never drifts behind newly-added packages. After a successful clone the app now offers to install the whole set (sourced from the loaded catalog, so always current). Also convert PackageRowTests to [AvaloniaFact] so it can't initialise the localization singleton before Avalonia's asset loader is ready (which poisoned later tests), and update the bundle tests for the new bundle id. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bd092f6 commit 1b6a0a3

8 files changed

Lines changed: 174 additions & 19 deletions

File tree

src/BasisPM.App/Localization/Languages/en.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,14 @@
290290
"key": "installs.dialog.nameThisInstall",
291291
"value": "Name this install"
292292
},
293+
{
294+
"key": "installs.dialog.recommendedBody",
295+
"value": "Add all {0} recommended Basis packages to this project now? You can manage them anytime on the Packages tab."
296+
},
297+
{
298+
"key": "installs.dialog.recommendedTitle",
299+
"value": "Install the Basis Recommended pack?"
300+
},
293301
{
294302
"key": "installs.empty.body",
295303
"value": "Use Clone Basis above to pull down BasisVR/Basis, or Add existing to register a clone you already have."
@@ -594,6 +602,10 @@
594602
"key": "packages.label.project",
595603
"value": "Project"
596604
},
605+
{
606+
"key": "packages.recommended.name",
607+
"value": "Basis Recommended"
608+
},
597609
{
598610
"key": "packages.status.added",
599611
"value": "Added"
@@ -694,6 +706,10 @@
694706
"key": "packages.status.pasteGitHub",
695707
"value": "Paste a GitHub URL or owner/repo."
696708
},
709+
{
710+
"key": "packages.status.recommendedEmpty",
711+
"value": "No packages available to install — check your connection, then try Refresh on the Packages tab."
712+
},
697713
{
698714
"key": "packages.status.refusedUnsafeUrl",
699715
"value": "Refused to add {0}: the install link's git URL uses an unsupported or unsafe transport."

src/BasisPM.App/ViewModels/InstallsViewModel.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,9 @@ private async Task CloneAsync()
305305
? L.Tr("installs.status.requiresUnityNote", install.UnityVersion)
306306
: "";
307307
_shell.SetStatus(L.Tr("installs.status.clonedInto", dest, versionNote), StatusKind.Success);
308+
309+
// Fresh clone: offer the one-click "Basis Recommended" pack.
310+
await OfferRecommendedAsync(install);
308311
}
309312
catch (Exception ex)
310313
{
@@ -313,6 +316,20 @@ private async Task CloneAsync()
313316
finally { IsCloning = false; }
314317
}
315318

319+
/// <summary>After a fresh clone, offers to add the full "Basis Recommended" package set in one press.</summary>
320+
private async Task OfferRecommendedAsync(BasisInstall install)
321+
{
322+
if (!install.HasUnityProject) return; // no Unity project to add packages to
323+
var count = _shell.PackagesVM.RecommendedCount;
324+
if (count == 0) return; // catalog unavailable (e.g. offline)
325+
var yes = await BasisPM.App.Services.Dialogs.ConfirmAsync(
326+
L.Tr("installs.dialog.recommendedTitle"),
327+
L.Tr("installs.dialog.recommendedBody", count));
328+
if (!yes) return;
329+
_shell.NavigateTo("packages");
330+
await _shell.PackagesVM.InstallRecommendedAsync(install);
331+
}
332+
316333
private async Task UpdateCoreAsync(InstallRow? row)
317334
{
318335
if (row is null) return;

src/BasisPM.App/ViewModels/PackagesViewModel.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,30 @@ public async Task AddBundleAsync(Bundle bundle, BasisInstall target)
572572
finally { IsBusy = false; }
573573
}
574574

575+
/// <summary>How many packages the "Basis Recommended" pack would add — the whole catalog.</summary>
576+
public int RecommendedCount => _catalogService.AllLatest(_catalog).Count();
577+
578+
/// <summary>Installs the full "Basis Recommended" set (every catalog package) into the target project.</summary>
579+
public async Task InstallRecommendedAsync(BasisInstall target)
580+
{
581+
var packages = _catalogService.AllLatest(_catalog)
582+
.Where(v => !string.IsNullOrWhiteSpace(v.Url))
583+
.Select(v => new BundlePackage { Id = v.Name, Name = v.DisplayName, GitUrl = v.Url })
584+
.ToList();
585+
if (packages.Count == 0)
586+
{
587+
_shell.SetStatus(L.Tr("packages.status.recommendedEmpty"), StatusKind.Info);
588+
return;
589+
}
590+
var bundle = new Bundle
591+
{
592+
Id = "basis-recommended",
593+
Name = L.Tr("packages.recommended.name"),
594+
Packages = packages,
595+
};
596+
await AddBundleAsync(bundle, target);
597+
}
598+
575599
private static string Slugify(string s)
576600
{
577601
var slug = new string(s.ToLowerInvariant().Select(c => char.IsLetterOrDigit(c) ? c : '-').ToArray());

src/BasisPM.Server/Program.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,17 @@ static async Task GenerateStaticSiteAsync(string outDir)
154154
File.WriteAllText(Path.Combine(outDir, "packages.json"), JsonSerializer.Serialize(packages, opts));
155155
File.WriteAllText(Path.Combine(outDir, "catalog.json"), JsonSerializer.Serialize(PackageStore.BuildCatalog(packages), opts));
156156

157-
// Curated bundles — copied through as-is for the website's Bundles view + install-back.
157+
// Curated bundles for the website's Bundles view + install-back.
158158
var bundleSeed = ResolveUp(baseDir, Path.Combine("seed", "bundles.json"));
159159
var bundles = BundleStore.LoadSeed(bundleSeed);
160+
// "Basis Recommended" always holds every registry package — refill it from the current list so
161+
// it can never drift behind newly-added packages.
162+
var recommended = bundles.FirstOrDefault(b => b.Id == "basis-recommended");
163+
if (recommended is not null)
164+
recommended.Packages = packages
165+
.Where(p => !string.IsNullOrWhiteSpace(p.GitUrl))
166+
.Select(p => new BundlePackage { Id = p.Id, Name = p.Name, GitUrl = p.GitUrl })
167+
.ToList();
160168
File.WriteAllText(Path.Combine(outDir, "bundles.json"), JsonSerializer.Serialize(bundles, opts));
161169

162170
File.Copy(indexPath, Path.Combine(outDir, "index.html"), overwrite: true);
Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,109 @@
11
[
22
{
3-
"id": "basis-starter",
4-
"name": "Basis Starter Pack",
5-
"description": "A minimal starting pointBasis plus the community Pool Table world asset. A demo bundle.",
3+
"id": "basis-recommended",
4+
"name": "Basis Recommended",
5+
"description": "Every curated Basis packagethe full recommended set, added on top of a fresh Basis clone in one click.",
66
"author": "BasisVR",
77
"authorUrl": "https://github.com/BasisVR",
88
"basisBranch": "main",
99
"unity": "6000.0",
10-
"icon": "🧩",
11-
"tags": [ "starter", "example", "worlds" ],
10+
"icon": "",
11+
"tags": [
12+
"recommended",
13+
"everything",
14+
"all"
15+
],
1216
"packages": [
1317
{
1418
"id": "com.basis.pooltable",
1519
"name": "Basis Ported Pooltable",
16-
"gitUrl": "https://github.com/dooly123/MS-BASISSA-Billiards.git"
20+
"gitUrl": "https://github.com/BasisVR/BasisPoolTable.git"
21+
},
22+
{
23+
"id": "com.basis.addon.snapcontrols",
24+
"name": "Basis Snap Controls",
25+
"gitUrl": "https://github.com/BasisVR/BasisSnapControls.git"
26+
},
27+
{
28+
"id": "com.basis.developer.recorder",
29+
"name": "Basis Avatar Recorder",
30+
"gitUrl": "https://github.com/BasisVR/BasisAvatarRecorder.git"
31+
},
32+
{
33+
"id": "com.basis.developer.exceptions",
34+
"name": "Basis Exception Reporting",
35+
"gitUrl": "https://github.com/BasisVR/BasisExceptionReporting.git"
36+
},
37+
{
38+
"id": "com.basis.provider.servers",
39+
"name": "Basis Servers Panel",
40+
"gitUrl": "https://github.com/BasisVR/BasisServersProvider.git"
41+
},
42+
{
43+
"id": "com.basis.imagepickup",
44+
"name": "Basis Image Pickup",
45+
"gitUrl": "https://github.com/BasisVR/BasisImagePickup.git"
46+
},
47+
{
48+
"id": "com.basis.vehicles",
49+
"name": "Basis Vehicles",
50+
"gitUrl": "https://github.com/BasisVR/BasisVehicles.git"
51+
},
52+
{
53+
"id": "com.basis.examples",
54+
"name": "Basis Examples",
55+
"gitUrl": "https://github.com/BasisVR/BasisExamples.git"
56+
},
57+
{
58+
"id": "com.basis.mediaplayer",
59+
"name": "Basis Media Player",
60+
"gitUrl": "https://github.com/BasisVR/BasisMediaPlayer.git"
61+
},
62+
{
63+
"id": "com.basis.integration.ytdlp",
64+
"name": "Basis yt-dlp Integration",
65+
"gitUrl": "https://github.com/BasisVR/BasisYtDlpIntegration.git"
66+
},
67+
{
68+
"id": "com.basis.integration.audiolink",
69+
"name": "Basis AudioLink Integration",
70+
"gitUrl": "https://github.com/BasisVR/BasisAudioLinkIntegration.git"
71+
},
72+
{
73+
"id": "com.basis.openxr",
74+
"name": "Basis OpenXR Integration",
75+
"gitUrl": "https://github.com/BasisVR/BasisOpenXR.git"
76+
},
77+
{
78+
"id": "com.basis.visualtrackers",
79+
"name": "Basis Visual Trackers",
80+
"gitUrl": "https://github.com/BasisVR/BasisVisualTrackers.git"
81+
},
82+
{
83+
"id": "com.basis.openvr",
84+
"name": "Basis OpenVR Integration",
85+
"gitUrl": "https://github.com/BasisVR/BasisOpenVR.git"
86+
},
87+
{
88+
"id": "com.steam.steamvr",
89+
"name": "SteamVR (for Basis)",
90+
"gitUrl": "https://github.com/BasisVR/BasisSteamVR.git"
91+
},
92+
{
93+
"id": "com.valvesoftware.unity.openvr",
94+
"name": "OpenVR XR Plugin",
95+
"gitUrl": "https://github.com/BasisVR/BasisOpenVRPlugin.git"
96+
},
97+
{
98+
"id": "com.cnlohr.cilbox",
99+
"name": "Cilbox",
100+
"gitUrl": "https://github.com/BasisVR/BasisCilbox.git"
101+
},
102+
{
103+
"id": "com.cqf.urpvolumetricfog",
104+
"name": "URP Volumetric Fog",
105+
"gitUrl": "https://github.com/BasisVR/BasisVolumetricFog.git"
17106
}
18107
]
19108
}
20-
]
109+
]

tests/BasisPM.App.Tests/DeepLinkTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ public void TryParseInstall_rejects_a_non_deeplink()
5454
[Fact]
5555
public void TryParseBundle_extracts_the_id()
5656
{
57-
Assert.True(DeepLink.TryParseBundle("basispm://bundle?id=basis-starter", out var id));
58-
Assert.Equal("basis-starter", id);
57+
Assert.True(DeepLink.TryParseBundle("basispm://bundle?id=basis-recommended", out var id));
58+
Assert.Equal("basis-recommended", id);
5959
}
6060

6161
[Fact]

tests/BasisPM.App.Tests/PackageRowTests.cs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Avalonia.Headless.XUnit;
12
using BasisPM.App.ViewModels;
23
using BasisPM.Core.Models;
34
using Xunit;
@@ -20,23 +21,23 @@ private static CatalogPackageVersion Entry(
2021
Author = author is null ? null : new CatalogAuthor { Name = author },
2122
};
2223

23-
[Fact]
24+
[AvaloniaFact]
2425
public void Not_installed_row_shows_install()
2526
{
2627
var row = new PackageRow(Entry(), InstalledVersion: null);
2728
Assert.False(row.IsInstalled);
2829
Assert.Equal("Install", row.ButtonLabel);
2930
}
3031

31-
[Fact]
32+
[AvaloniaFact]
3233
public void Installed_row_shows_update()
3334
{
3435
var row = new PackageRow(Entry(), InstalledVersion: "1.0.0");
3536
Assert.True(row.IsInstalled);
3637
Assert.Equal("Update", row.ButtonLabel);
3738
}
3839

39-
[Fact]
40+
[AvaloniaFact]
4041
public void Passes_through_catalog_fields()
4142
{
4243
var row = new PackageRow(Entry(display: "Basis SDK", version: "2.1.0"), null);
@@ -46,7 +47,7 @@ public void Passes_through_catalog_fields()
4647
Assert.Equal("A package.", row.Description);
4748
}
4849

49-
[Fact]
50+
[AvaloniaFact]
5051
public void Author_and_unity_presence_flags()
5152
{
5253
var withBoth = new PackageRow(Entry(unity: "6000.0", author: "BasisVR"), null);
@@ -60,7 +61,7 @@ public void Author_and_unity_presence_flags()
6061
Assert.False(withNeither.HasUnity);
6162
}
6263

63-
[Fact]
64+
[AvaloniaFact]
6465
public void License_presence_flag_and_passthrough()
6566
{
6667
var withLicense = new PackageRow(Entry(license: "MIT AND Unlicense"), null);
@@ -71,7 +72,7 @@ public void License_presence_flag_and_passthrough()
7172
Assert.False(noLicense.HasLicense);
7273
}
7374

74-
[Theory]
75+
[AvaloniaTheory]
7576
[InlineData("Basis SDK", "B")]
7677
[InlineData(" spaced name", "S")]
7778
[InlineData("", "?")]
@@ -80,7 +81,7 @@ public void Initial_is_the_first_letter_or_question_mark(string display, string
8081
Assert.Equal(expected, new PackageRow(Entry(display: display), null).Initial);
8182
}
8283

83-
[Theory]
84+
[AvaloniaTheory]
8485
[InlineData("com.unity.2d.sprite", "Unity")]
8586
[InlineData("com.basis.sdk", "Basis")]
8687
[InlineData("foo", "Foo")]
@@ -90,7 +91,7 @@ public void Owner_is_derived_from_the_package_id(string name, string expectedOwn
9091
Assert.Equal(expectedOwner, new PackageRow(Entry(name: name), null).Owner);
9192
}
9293

93-
[Fact]
94+
[AvaloniaFact]
9495
public void InstalledPackageRow_exposes_its_fields()
9596
{
9697
var row = new InstalledPackageRow("com.x", "X Package", "https://github.com/x/x.git", IsFromGit: true);

tests/BasisPM.Server.Tests/ApiGetTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace BasisPM.Server.Tests;
1010
public sealed class ApiGetTests : IClassFixture<DisabledSubmissionsFactory>
1111
{
1212
private const string SeedPackageId = "com.basis.pooltable";
13-
private const string SeedBundleId = "basis-starter";
13+
private const string SeedBundleId = "basis-recommended";
1414
private readonly HttpClient _client;
1515

1616
public ApiGetTests(DisabledSubmissionsFactory factory) => _client = factory.CreateClient();

0 commit comments

Comments
 (0)