From b3bc852b3d8546cfa5c486a618b2f67b87ec3ef0 Mon Sep 17 00:00:00 2001 From: Christof Lauriers Date: Sat, 14 Mar 2026 21:25:08 +0100 Subject: [PATCH 01/14] Add design spec for platform-specific config and print profiles Co-Authored-By: Claude Sonnet 4.6 --- ...4-platform-config-print-profiles-design.md | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-14-platform-config-print-profiles-design.md diff --git a/docs/superpowers/specs/2026-03-14-platform-config-print-profiles-design.md b/docs/superpowers/specs/2026-03-14-platform-config-print-profiles-design.md new file mode 100644 index 0000000..05c2e23 --- /dev/null +++ b/docs/superpowers/specs/2026-03-14-platform-config-print-profiles-design.md @@ -0,0 +1,150 @@ +# Design: Platform-Specific Config with Print Profiles + +**Date:** 2026-03-14 +**Status:** Approved + +## Problem + +Publishers need per-platform file attachments (print profiles — typically `.3mf` files) alongside the existing common fields. The current manifest `Platforms` dictionary stores raw `JsonElement` values, requiring manual property extraction. There is no shared model for platform config, and no canonical place to put shared optional fields like print profiles. + +## Goal + +- Add `print_profiles` as a list of relative file paths on each platform's config block. +- Introduce a typed `PlatformConfig` base record so publishers get IDE-supported, type-safe access to platform config. +- Keep the JSON manifest format backward-compatible (existing manifests without `print_profiles` continue to work). +- Make it easy to add future platform-specific fields without touching shared code. + +## Non-Goals + +- Uploading or validating print profile files (responsibility of each publisher). +- Changing the platform key format or the `Platforms` dictionary structure. + +--- + +## Architecture + +### New: `Models/PlatformConfig.cs` + +Shared base record for all platform config blocks: + +```csharp +public record PlatformConfig +{ + [JsonPropertyName("tier")] + public string Tier { get; init; } = "free"; + + [JsonPropertyName("print_profiles")] + public List PrintProfiles { get; init; } = []; +} +``` + +`PrintProfiles` contains relative paths resolved via the existing `manifest.ResolveFilePath()`. + +### New: `Platforms/PatreonConfig.cs` + +Moves Patreon's existing extra fields from ad-hoc `JsonElement` access into a typed subclass: + +```csharp +public record PatreonConfig : PlatformConfig +{ + [JsonPropertyName("free_post")] + public bool FreePost { get; init; } = true; + + [JsonPropertyName("access_tier_id")] + public string? AccessTierId { get; init; } +} +``` + +### Updated: `Models/ReleaseManifest.cs` + +`Platforms` stays `Dictionary` — no change to deserialization or JSON format. + +Add one helper method: + +```csharp +public T? GetPlatformConfig(string platformKey) where T : PlatformConfig, new() +{ + if (!Platforms.TryGetValue(platformKey, out var el)) + return null; + return JsonSerializer.Deserialize(el, JsonOptions) ?? new T(); +} +``` + +Returns `null` when the platform key is absent — preserving the existing gate in `PublishCommand` where `.Where(x => x.Tier != null)` filters out unlisted platforms. A private static `JsonOptions` with `PropertyNameCaseInsensitive = true` is added to the class (deserialization only; `WriteIndented` is irrelevant here). No custom converter is needed: `JsonSerializer.Deserialize(el, options)` works correctly for concrete derived types without any `[JsonDerivedType]` attributes. + +### Updated: `PublishCommand.cs` + +`ResolveTier` replaces its manual `JsonElement` property extraction with: + +```csharp +var config = manifest.GetPlatformConfig(publisher.PlatformKey); +if (config is null) return null; +return config.Tier is "free" or "premium" ? config.Tier : "free"; +``` + +The `null` return when the key is absent is preserved — `PublishCommand`'s existing `.Where(x => x.Tier != null)` gate continues to filter out platforms not listed in the manifest. + +### Updated: `Platforms/PatreonPublisher.cs` + +`PatreonPublisher` currently has no `JsonElement` access — its extra fields (`free_post`, `access_tier_id`) are not yet read from the manifest. This change adds first-time typed access: the publisher calls `manifest.GetPlatformConfig(PlatformKey)` so that when those fields are implemented they use the typed config rather than raw `JsonElement`. + +--- + +## Manifest JSON Format + +No breaking changes. `print_profiles` is optional and defaults to an empty list when omitted. Platforms that do not use print profiles simply leave the key out — `PlatformConfig.PrintProfiles` will be `[]`. + +```json +"platforms": { + "printables": { + "tier": "free", + "print_profiles": ["./profiles/printables-0.2mm.3mf"] + }, + "makerworld": { + "tier": "free", + "print_profiles": [ + "./profiles/makerworld-0.2mm.3mf", + "./profiles/makerworld-0.4mm.3mf" + ] + }, + "patreon": { + "tier": "premium", + "free_post": false, + "access_tier_id": "YOUR_TIER_ID_HERE" + // print_profiles omitted — defaults to [] + } +} +``` + +Note: path resolution for print profiles uses the existing `manifest.ResolveFilePath()`. File existence is not validated at load time — that is the responsibility of each publisher at upload time. + +--- + +## Data Flow + +1. `PublishCommand` calls `manifest.GetPlatformConfig(publisher.PlatformKey)` to resolve tier. +2. Publisher receives `manifest` as before. +3. Publisher calls `manifest.GetPlatformConfig(PlatformKey)` to get its typed config. +4. Publisher iterates `config.PrintProfiles`, calls `manifest.ResolveFilePath(path)` on each to get absolute paths. +5. Publisher uploads resolved paths using the existing `FileUploadHelper`. + +--- + +## Adding Future Platform-Specific Fields + +- If a platform needs unique fields: create `MyPlatformConfig : PlatformConfig` and call `GetPlatformConfig()`. +- If a field is useful across all platforms: add it to `PlatformConfig` directly. +- No changes required to `ReleaseManifest`, `PublishCommand`, or any other publisher. + +--- + +## Files Changed + +| File | Change | +|------|--------| +| `src/ModelPublisher.Core/Models/PlatformConfig.cs` | **New** — base record | +| `src/ModelPublisher.Core/Platforms/PatreonConfig.cs` | **New** — Patreon-specific subclass | +| `src/ModelPublisher.Core/Models/ReleaseManifest.cs` | Add `GetPlatformConfig()` helper | +| `src/ModelPublisher.Core/PublishCommand.cs` | Simplify `ResolveTier` | +| `src/ModelPublisher.Core/Platforms/PatreonPublisher.cs` | Use `PatreonConfig` instead of raw `JsonElement` | +| `releases/example-model/manifest.json` | Add `print_profiles` example entries | From ff114f9425c34c98420ebdc8a19cbb770c797b23 Mon Sep 17 00:00:00 2001 From: Christof Lauriers Date: Sat, 14 Mar 2026 21:33:33 +0100 Subject: [PATCH 02/14] Add implementation plan for platform config and print profiles Co-Authored-By: Claude Sonnet 4.6 --- ...26-03-14-platform-config-print-profiles.md | 601 ++++++++++++++++++ 1 file changed, 601 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-14-platform-config-print-profiles.md diff --git a/docs/superpowers/plans/2026-03-14-platform-config-print-profiles.md b/docs/superpowers/plans/2026-03-14-platform-config-print-profiles.md new file mode 100644 index 0000000..785272b --- /dev/null +++ b/docs/superpowers/plans/2026-03-14-platform-config-print-profiles.md @@ -0,0 +1,601 @@ +# Platform-Specific Config & Print Profiles Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Introduce a typed `PlatformConfig` base record with `print_profiles` support, a `PatreonConfig` subclass, a `GetPlatformConfig()` helper on `ReleaseManifest`, and simplify `ResolveTier` in `PublishCommand`. + +**Architecture:** A new `PlatformConfig` record lives in `Models/` and holds the fields common to all platform config blocks (`tier`, `print_profiles`). Platform-specific subclasses (starting with `PatreonConfig`) extend it. `ReleaseManifest` keeps its `Dictionary` storage but gains a generic `GetPlatformConfig()` method that deserializes on demand. + +**Tech Stack:** .NET 10, C#, System.Text.Json, xUnit 2.x, FluentAssertions + +--- + +## Chunk 1: Test project + PlatformConfig base + GetPlatformConfig + +### Task 1: Create the test project + +No test project currently exists. We create one and wire it into the solution. + +**Files:** +- Create: `tests/ModelPublisher.Core.Tests/ModelPublisher.Core.Tests.csproj` +- Modify: `ModelPublisher.sln` + +- [ ] **Step 1: Scaffold test project** + +Run from the **worktree root** (`C:\Source\ModelPublisher\.claude\worktrees\flamboyant-spence`): + +```bash +cd /c/Source/ModelPublisher/.claude/worktrees/flamboyant-spence +dotnet new xunit -n ModelPublisher.Core.Tests -o tests/ModelPublisher.Core.Tests --framework net10.0 +``` + +Expected: `tests/ModelPublisher.Core.Tests/` created with a `.csproj` and `UnitTest1.cs`. + +- [ ] **Step 2: Add FluentAssertions and reference Core** + +Edit `tests/ModelPublisher.Core.Tests/ModelPublisher.Core.Tests.csproj` to match: + +```xml + + + net10.0 + enable + enable + latest + false + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + +``` + +- [ ] **Step 3: Add project to solution** + +```bash +cd /c/Source/ModelPublisher/.claude/worktrees/flamboyant-spence +dotnet sln add tests/ModelPublisher.Core.Tests/ModelPublisher.Core.Tests.csproj +``` + +- [ ] **Step 4: Delete the placeholder test file** + +```bash +rm tests/ModelPublisher.Core.Tests/UnitTest1.cs +``` + +- [ ] **Step 5: Verify build** + +```bash +dotnet build +``` + +Expected: Build succeeded, 0 errors. + +- [ ] **Step 6: Commit** + +```bash +git add tests/ ModelPublisher.sln +git commit -m "$(cat <<'EOF' +Add ModelPublisher.Core.Tests xUnit project + +Co-Authored-By: Claude Sonnet 4.6 +EOF +)" +``` + +--- + +### Task 2: PlatformConfig base record + +**Files:** +- Create: `src/ModelPublisher.Core/Models/PlatformConfig.cs` +- Create: `tests/ModelPublisher.Core.Tests/Models/PlatformConfigTests.cs` + +- [ ] **Step 1: Write the failing tests** + +Create `tests/ModelPublisher.Core.Tests/Models/PlatformConfigTests.cs`: + +```csharp +using System.Text.Json; +using FluentAssertions; +using ModelPublisher.Core.Models; + +namespace ModelPublisher.Core.Tests.Models; + +public class PlatformConfigTests +{ + [Fact] + public void Deserialize_WithTierAndProfiles_PopulatesBothFields() + { + var json = """{"tier":"premium","print_profiles":["./a.3mf","./b.3mf"]}"""; + var config = JsonSerializer.Deserialize(json); + config!.Tier.Should().Be("premium"); + config.PrintProfiles.Should().Equal("./a.3mf", "./b.3mf"); + } + + [Fact] + public void Deserialize_WithNoFields_UsesDefaults() + { + var json = "{}"; + var config = JsonSerializer.Deserialize(json); + config!.Tier.Should().Be("free"); + config.PrintProfiles.Should().BeEmpty(); + } + + [Fact] + public void Deserialize_WithNoPrintProfiles_DefaultsToEmpty() + { + var json = """{"tier":"free"}"""; + var config = JsonSerializer.Deserialize(json); + config!.PrintProfiles.Should().BeEmpty(); + } +} +``` + +- [ ] **Step 2: Run tests — expect compile failure** + +```bash +cd /c/Source/ModelPublisher/.claude/worktrees/flamboyant-spence +dotnet test tests/ModelPublisher.Core.Tests/ +``` + +Expected: Build error — `PlatformConfig` does not exist yet. + +- [ ] **Step 3: Create PlatformConfig** + +Create `src/ModelPublisher.Core/Models/PlatformConfig.cs`: + +```csharp +using System.Text.Json.Serialization; + +namespace ModelPublisher.Core.Models; + +public record PlatformConfig +{ + [JsonPropertyName("tier")] + public string Tier { get; init; } = "free"; + + [JsonPropertyName("print_profiles")] + public List PrintProfiles { get; init; } = []; +} +``` + +- [ ] **Step 4: Run tests — expect pass** + +```bash +dotnet test tests/ModelPublisher.Core.Tests/ --logger "console;verbosity=normal" +``` + +Expected: 3 tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add src/ModelPublisher.Core/Models/PlatformConfig.cs \ + tests/ModelPublisher.Core.Tests/Models/PlatformConfigTests.cs +git commit -m "$(cat <<'EOF' +Add PlatformConfig base record with tier and print_profiles + +Co-Authored-By: Claude Sonnet 4.6 +EOF +)" +``` + +--- + +### Task 3: PatreonConfig subclass + +**Files:** +- Create: `src/ModelPublisher.Core/Platforms/PatreonConfig.cs` +- Create: `tests/ModelPublisher.Core.Tests/Platforms/PatreonConfigTests.cs` + +- [ ] **Step 1: Write the failing tests** + +Create `tests/ModelPublisher.Core.Tests/Platforms/PatreonConfigTests.cs`: + +```csharp +using System.Text.Json; +using FluentAssertions; +using ModelPublisher.Core.Platforms; + +namespace ModelPublisher.Core.Tests.Platforms; + +public class PatreonConfigTests +{ + [Fact] + public void Deserialize_WithAllFields_PopulatesCorrectly() + { + var json = """{"tier":"premium","free_post":false,"access_tier_id":"tier_abc123"}"""; + var config = JsonSerializer.Deserialize(json); + config!.Tier.Should().Be("premium"); + config.FreePost.Should().BeFalse(); + config.AccessTierId.Should().Be("tier_abc123"); + config.PrintProfiles.Should().BeEmpty(); + } + + [Fact] + public void Deserialize_WithNoOptionalFields_UsesDefaults() + { + var json = "{}"; + var config = JsonSerializer.Deserialize(json); + config!.Tier.Should().Be("free"); + config.FreePost.Should().BeTrue(); + config.AccessTierId.Should().BeNull(); + } + + [Fact] + public void Deserialize_InheritsBasePrintProfiles() + { + var json = """{"print_profiles":["./x.3mf"]}"""; + var config = JsonSerializer.Deserialize(json); + config!.PrintProfiles.Should().Equal("./x.3mf"); + } +} +``` + +- [ ] **Step 2: Run tests — expect compile failure** + +```bash +dotnet test tests/ModelPublisher.Core.Tests/ +``` + +Expected: Build error — `PatreonConfig` does not exist. + +- [ ] **Step 3: Create PatreonConfig** + +Create `src/ModelPublisher.Core/Platforms/PatreonConfig.cs`: + +```csharp +using System.Text.Json.Serialization; +using ModelPublisher.Core.Models; + +namespace ModelPublisher.Core.Platforms; + +public record PatreonConfig : PlatformConfig +{ + [JsonPropertyName("free_post")] + public bool FreePost { get; init; } = true; + + [JsonPropertyName("access_tier_id")] + public string? AccessTierId { get; init; } +} +``` + +- [ ] **Step 4: Run tests — expect pass** + +```bash +dotnet test tests/ModelPublisher.Core.Tests/ --logger "console;verbosity=normal" +``` + +Expected: All tests pass (including previous 3). + +- [ ] **Step 5: Commit** + +```bash +git add src/ModelPublisher.Core/Platforms/PatreonConfig.cs \ + tests/ModelPublisher.Core.Tests/Platforms/PatreonConfigTests.cs +git commit -m "$(cat <<'EOF' +Add PatreonConfig subclass with free_post and access_tier_id + +Co-Authored-By: Claude Sonnet 4.6 +EOF +)" +``` + +--- + +### Task 4: GetPlatformConfig() on ReleaseManifest + +**Files:** +- Modify: `src/ModelPublisher.Core/Models/ReleaseManifest.cs` +- Create: `tests/ModelPublisher.Core.Tests/Models/ReleaseManifestGetPlatformConfigTests.cs` + +- [ ] **Step 1: Write the failing tests** + +Create `tests/ModelPublisher.Core.Tests/Models/ReleaseManifestGetPlatformConfigTests.cs`: + +```csharp +using System.Text.Json; +using FluentAssertions; +using ModelPublisher.Core.Models; +using ModelPublisher.Core.Platforms; + +namespace ModelPublisher.Core.Tests.Models; + +public class ReleaseManifestGetPlatformConfigTests +{ + private static ReleaseManifest DeserializeManifest(string platformsJson) + { + var json = $$""" + { + "title": "Test", + "platforms": {{platformsJson}} + } + """; + return JsonSerializer.Deserialize(json, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; + } + + [Fact] + public void GetPlatformConfig_AbsentKey_ReturnsNull() + { + var manifest = DeserializeManifest("""{"printables":{"tier":"free"}}"""); + manifest.GetPlatformConfig("makerworld").Should().BeNull(); + } + + [Fact] + public void GetPlatformConfig_BaseType_DeserializesTierAndProfiles() + { + var manifest = DeserializeManifest( + """{"printables":{"tier":"premium","print_profiles":["./profile.3mf"]}}"""); + var config = manifest.GetPlatformConfig("printables"); + config!.Tier.Should().Be("premium"); + config.PrintProfiles.Should().Equal("./profile.3mf"); + } + + [Fact] + public void GetPlatformConfig_DerivedType_DeserializesExtraFields() + { + var manifest = DeserializeManifest( + """{"patreon":{"tier":"premium","free_post":false,"access_tier_id":"t123"}}"""); + var config = manifest.GetPlatformConfig("patreon"); + config!.Tier.Should().Be("premium"); + config.FreePost.Should().BeFalse(); + config.AccessTierId.Should().Be("t123"); + } + + [Fact] + public void GetPlatformConfig_NoOptionalFields_ReturnsDefaults() + { + var manifest = DeserializeManifest("""{"printables":{}}"""); + var config = manifest.GetPlatformConfig("printables"); + config!.Tier.Should().Be("free"); + config.PrintProfiles.Should().BeEmpty(); + } +} +``` + +- [ ] **Step 2: Run tests — expect compile failure** + +```bash +dotnet test tests/ModelPublisher.Core.Tests/ +``` + +Expected: Build error — `GetPlatformConfig` does not exist. + +- [ ] **Step 3: Add GetPlatformConfig() and JsonOptions to ReleaseManifest** + +Open `src/ModelPublisher.Core/Models/ReleaseManifest.cs`. Add a `using System.Text.Json;` at the top (it's already there). Then add to the `ReleaseManifest` class body, after the existing `ResolveFilePath` method: + +```csharp +private static readonly JsonSerializerOptions ConfigJsonOptions = new() +{ + PropertyNameCaseInsensitive = true +}; + +public T? GetPlatformConfig(string platformKey) where T : PlatformConfig, new() +{ + if (!Platforms.TryGetValue(platformKey, out var el)) + return null; + return JsonSerializer.Deserialize(el, ConfigJsonOptions) ?? new T(); +} +``` + +- [ ] **Step 4: Run tests — expect pass** + +```bash +dotnet test tests/ModelPublisher.Core.Tests/ --logger "console;verbosity=normal" +``` + +Expected: All tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add src/ModelPublisher.Core/Models/ReleaseManifest.cs \ + tests/ModelPublisher.Core.Tests/Models/ReleaseManifestGetPlatformConfigTests.cs +git commit -m "$(cat <<'EOF' +Add GetPlatformConfig() helper to ReleaseManifest + +Co-Authored-By: Claude Sonnet 4.6 +EOF +)" +``` + +--- + +## Chunk 2: Wire up PublishCommand + PatreonPublisher + example manifest + +### Task 5: Simplify ResolveTier in PublishCommand + +**Files:** +- Modify: `src/ModelPublisher.Core/PublishCommand.cs` + +No new tests needed — `ResolveTier` is a private static method tested via integration (running the CLI). The behavior is unchanged; this is purely a simplification refactor. + +- [ ] **Step 1: Update ResolveTier** + +In `src/ModelPublisher.Core/PublishCommand.cs`, replace the `ResolveTier` method (lines 147–160): + +```csharp +private static string? ResolveTier(ReleaseManifest manifest, IPlatformPublisher publisher) +{ + var config = manifest.GetPlatformConfig(publisher.PlatformKey); + if (config is null) return null; + var tier = config.Tier.ToLowerInvariant(); + return tier is "free" or "premium" ? tier : "free"; +} +``` + +- [ ] **Step 2: Build to verify no compile errors** + +```bash +dotnet build +``` + +Expected: Build succeeded, 0 errors. + +- [ ] **Step 3: Run all tests** + +```bash +dotnet test tests/ModelPublisher.Core.Tests/ --logger "console;verbosity=normal" +``` + +Expected: All tests pass. + +- [ ] **Step 4: Commit** + +```bash +git add src/ModelPublisher.Core/PublishCommand.cs +git commit -m "$(cat <<'EOF' +Simplify ResolveTier to use GetPlatformConfig + +Co-Authored-By: Claude Sonnet 4.6 +EOF +)" +``` + +--- + +### Task 6: Update PatreonPublisher to use PatreonConfig + +**Files:** +- Modify: `src/ModelPublisher.Core/Platforms/PatreonPublisher.cs` + +`PatreonPublisher` currently reads neither `free_post` nor `access_tier_id` from the manifest. This task adds the call to `GetPlatformConfig()` so that when those fields are implemented they use the typed config. No behavior changes are introduced. + +- [ ] **Step 1: Add PatreonConfig access in PublishFreeAsync** + +In `src/ModelPublisher.Core/Platforms/PatreonPublisher.cs`, inside `PublishFreeAsync`, add the config retrieval immediately after the `try {` opening (before the `GotoAsync` call): + +```csharp +// TODO: use config.FreePost and config.AccessTierId when Patreon automation is implemented +var config = manifest.GetPlatformConfig(PlatformKey) ?? new PatreonConfig(); +_ = config; +``` + +The `?? new PatreonConfig()` fallback is safe — Patreon is only run when the key is present in the manifest, but this keeps the code null-safe. The `_ = config` discard suppresses the unused-variable warning since the fields are not yet consumed. + +- [ ] **Step 2: Build and run all tests** + +```bash +dotnet build && dotnet test tests/ModelPublisher.Core.Tests/ --logger "console;verbosity=normal" +``` + +Expected: Build succeeded, all tests pass. + +- [ ] **Step 3: Commit** + +```bash +git add src/ModelPublisher.Core/Platforms/PatreonPublisher.cs +git commit -m "$(cat <<'EOF' +Use PatreonConfig in PatreonPublisher for typed config access + +Co-Authored-By: Claude Sonnet 4.6 +EOF +)" +``` + +--- + +### Task 7: Update example manifest with print_profiles + +**Files:** +- Modify: `releases/example-model/manifest.json` + +- [ ] **Step 1: Add print_profiles to two platforms in the example manifest** + +Update `releases/example-model/manifest.json` — add `print_profiles` to `printables` and `makerworld` to demonstrate the feature. Keep other platforms unchanged. + +```json +{ + "title": "Modular Cable Management Clips", + "description": "...", + "tags": ["cable-management", "desk", "organizer", "snap-fit", "modular"], + "license": "CC-BY-4.0", + "files": { + "models": [ + "./cable-clip-narrow.3mf", + "./cable-clip-wide.3mf" + ], + "cover": "./photo1.jpg", + "photos": [ + "./photo-mounted.jpg", + "./photo-detail.jpg" + ] + }, + "platforms": { + "printables": { + "tier": "free", + "print_profiles": ["./profiles/printables-0.2mm-pla.3mf"] + }, + "makerworld": { + "tier": "free", + "print_profiles": ["./profiles/makerworld-0.2mm-pla.3mf"] + }, + "cults3d": { + "tier": "free" + }, + "thangs": { + "tier": "free" + }, + "makeronline": { + "tier": "free" + }, + "patreon": { + "tier": "premium", + "free_post": false, + "access_tier_id": "YOUR_TIER_ID_HERE" + } + } +} +``` + +Note: The `./profiles/` files don't need to physically exist in the example — they're illustrative. Publishers only resolve paths when uploading. + +- [ ] **Step 2: Build to ensure manifest change doesn't break anything** + +```bash +dotnet build +``` + +Expected: Build succeeded. + +- [ ] **Step 3: Commit** + +```bash +git add releases/example-model/manifest.json +git commit -m "$(cat <<'EOF' +Add print_profiles examples to example manifest + +Co-Authored-By: Claude Sonnet 4.6 +EOF +)" +``` + +--- + +### Task 8: Push and open PR + +- [ ] **Step 1: Push branch** + +```bash +git push +``` + +- [ ] **Step 2: Open PR** + +```powershell +powershell.exe -Command "& 'C:\Program Files\GitHub CLI\gh.exe' pr create --repo TheCraftyMaker/ModelPublisher --title 'Add typed platform config with print_profiles support' --base master --head claude/flamboyant-spence --body 'Introduces a typed PlatformConfig base record and GetPlatformConfig() helper on ReleaseManifest, replacing manual JsonElement parsing. Adds print_profiles as a list of relative file paths per platform. PatreonConfig subclass captures Patreon-specific fields. ResolveTier in PublishCommand simplified. Backward-compatible — existing manifests without print_profiles continue to work.'" +``` + +- [ ] **Step 3: Share PR URL with user** From 3edb98417c6d3f0003adfde5e7bca92fc4e81c67 Mon Sep 17 00:00:00 2001 From: Christof Lauriers Date: Sat, 14 Mar 2026 21:35:53 +0100 Subject: [PATCH 03/14] Move implementation plan to specs/ alongside design doc Co-Authored-By: Claude Sonnet 4.6 --- .../2026-03-14-platform-config-print-profiles-plan.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/superpowers/{plans/2026-03-14-platform-config-print-profiles.md => specs/2026-03-14-platform-config-print-profiles-plan.md} (100%) diff --git a/docs/superpowers/plans/2026-03-14-platform-config-print-profiles.md b/docs/superpowers/specs/2026-03-14-platform-config-print-profiles-plan.md similarity index 100% rename from docs/superpowers/plans/2026-03-14-platform-config-print-profiles.md rename to docs/superpowers/specs/2026-03-14-platform-config-print-profiles-plan.md From 91f0390040ba2298960cbf71fff719e5566ffdeb Mon Sep 17 00:00:00 2001 From: Christof Lauriers Date: Sat, 14 Mar 2026 21:50:37 +0100 Subject: [PATCH 04/14] Add ModelPublisher.Core.Tests xUnit project Co-Authored-By: Claude Sonnet 4.6 --- ModelPublisher.sln | 46 ++++++++++++++++++- .../ModelPublisher.Core.Tests.csproj | 21 +++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 tests/ModelPublisher.Core.Tests/ModelPublisher.Core.Tests.csproj diff --git a/ModelPublisher.sln b/ModelPublisher.sln index 4d3ce5a..6b859ea 100644 --- a/ModelPublisher.sln +++ b/ModelPublisher.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 @@ -7,19 +7,63 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelPublisher.Cli", "src\M EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelPublisher.Core", "src\ModelPublisher.Core\ModelPublisher.Core.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelPublisher.Core.Tests", "tests\ModelPublisher.Core.Tests\ModelPublisher.Core.Tests.csproj", "{B3713944-A5ED-4A22-81E6-646CA6369288}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x64.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x64.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x86.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x86.Build.0 = Debug|Any CPU {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x64.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x64.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x86.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x86.Build.0 = Release|Any CPU + {B3713944-A5ED-4A22-81E6-646CA6369288}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B3713944-A5ED-4A22-81E6-646CA6369288}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B3713944-A5ED-4A22-81E6-646CA6369288}.Debug|x64.ActiveCfg = Debug|Any CPU + {B3713944-A5ED-4A22-81E6-646CA6369288}.Debug|x64.Build.0 = Debug|Any CPU + {B3713944-A5ED-4A22-81E6-646CA6369288}.Debug|x86.ActiveCfg = Debug|Any CPU + {B3713944-A5ED-4A22-81E6-646CA6369288}.Debug|x86.Build.0 = Debug|Any CPU + {B3713944-A5ED-4A22-81E6-646CA6369288}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B3713944-A5ED-4A22-81E6-646CA6369288}.Release|Any CPU.Build.0 = Release|Any CPU + {B3713944-A5ED-4A22-81E6-646CA6369288}.Release|x64.ActiveCfg = Release|Any CPU + {B3713944-A5ED-4A22-81E6-646CA6369288}.Release|x64.Build.0 = Release|Any CPU + {B3713944-A5ED-4A22-81E6-646CA6369288}.Release|x86.ActiveCfg = Release|Any CPU + {B3713944-A5ED-4A22-81E6-646CA6369288}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B3713944-A5ED-4A22-81E6-646CA6369288} = {0AB3BF05-4346-4AA6-1389-037BE0695223} EndGlobalSection EndGlobal diff --git a/tests/ModelPublisher.Core.Tests/ModelPublisher.Core.Tests.csproj b/tests/ModelPublisher.Core.Tests/ModelPublisher.Core.Tests.csproj new file mode 100644 index 0000000..237be61 --- /dev/null +++ b/tests/ModelPublisher.Core.Tests/ModelPublisher.Core.Tests.csproj @@ -0,0 +1,21 @@ + + + net10.0 + enable + enable + latest + false + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + From f229a710430fa3c22ab3003d4594cb6b211d3134 Mon Sep 17 00:00:00 2001 From: Christof Lauriers Date: Sat, 14 Mar 2026 21:54:00 +0100 Subject: [PATCH 05/14] Add PlatformConfig base record with tier and print_profiles Co-Authored-By: Claude Sonnet 4.6 --- .../Models/PlatformConfig.cs | 12 +++++++ .../Models/PlatformConfigTests.cs | 35 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 src/ModelPublisher.Core/Models/PlatformConfig.cs create mode 100644 tests/ModelPublisher.Core.Tests/Models/PlatformConfigTests.cs diff --git a/src/ModelPublisher.Core/Models/PlatformConfig.cs b/src/ModelPublisher.Core/Models/PlatformConfig.cs new file mode 100644 index 0000000..fb40a3a --- /dev/null +++ b/src/ModelPublisher.Core/Models/PlatformConfig.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace ModelPublisher.Core.Models; + +public record PlatformConfig +{ + [JsonPropertyName("tier")] + public string Tier { get; init; } = "free"; + + [JsonPropertyName("print_profiles")] + public List PrintProfiles { get; init; } = []; +} diff --git a/tests/ModelPublisher.Core.Tests/Models/PlatformConfigTests.cs b/tests/ModelPublisher.Core.Tests/Models/PlatformConfigTests.cs new file mode 100644 index 0000000..e029736 --- /dev/null +++ b/tests/ModelPublisher.Core.Tests/Models/PlatformConfigTests.cs @@ -0,0 +1,35 @@ +using System.Text.Json; +using FluentAssertions; +using ModelPublisher.Core.Models; +using Xunit; + +namespace ModelPublisher.Core.Tests.Models; + +public class PlatformConfigTests +{ + [Fact] + public void Deserialize_WithTierAndProfiles_PopulatesBothFields() + { + var json = """{"tier":"premium","print_profiles":["./a.3mf","./b.3mf"]}"""; + var config = JsonSerializer.Deserialize(json); + config!.Tier.Should().Be("premium"); + config.PrintProfiles.Should().Equal("./a.3mf", "./b.3mf"); + } + + [Fact] + public void Deserialize_WithNoFields_UsesDefaults() + { + var json = "{}"; + var config = JsonSerializer.Deserialize(json); + config!.Tier.Should().Be("free"); + config.PrintProfiles.Should().BeEmpty(); + } + + [Fact] + public void Deserialize_WithNoPrintProfiles_DefaultsToEmpty() + { + var json = """{"tier":"free"}"""; + var config = JsonSerializer.Deserialize(json); + config!.PrintProfiles.Should().BeEmpty(); + } +} From 0e12f57e6864b45db13af41c4aa86e955fac2837 Mon Sep 17 00:00:00 2001 From: Christof Lauriers Date: Sat, 14 Mar 2026 21:57:43 +0100 Subject: [PATCH 06/14] Add PatreonConfig subclass with free_post and access_tier_id Co-Authored-By: Claude Sonnet 4.6 --- .../Platforms/PatreonConfig.cs | 13 +++++++ .../Platforms/PatreonConfigTests.cs | 38 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 src/ModelPublisher.Core/Platforms/PatreonConfig.cs create mode 100644 tests/ModelPublisher.Core.Tests/Platforms/PatreonConfigTests.cs diff --git a/src/ModelPublisher.Core/Platforms/PatreonConfig.cs b/src/ModelPublisher.Core/Platforms/PatreonConfig.cs new file mode 100644 index 0000000..70358a9 --- /dev/null +++ b/src/ModelPublisher.Core/Platforms/PatreonConfig.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; +using ModelPublisher.Core.Models; + +namespace ModelPublisher.Core.Platforms; + +public record PatreonConfig : PlatformConfig +{ + [JsonPropertyName("free_post")] + public bool FreePost { get; init; } = true; + + [JsonPropertyName("access_tier_id")] + public string? AccessTierId { get; init; } +} diff --git a/tests/ModelPublisher.Core.Tests/Platforms/PatreonConfigTests.cs b/tests/ModelPublisher.Core.Tests/Platforms/PatreonConfigTests.cs new file mode 100644 index 0000000..eab799f --- /dev/null +++ b/tests/ModelPublisher.Core.Tests/Platforms/PatreonConfigTests.cs @@ -0,0 +1,38 @@ +using System.Text.Json; +using FluentAssertions; +using ModelPublisher.Core.Platforms; +using Xunit; + +namespace ModelPublisher.Core.Tests.Platforms; + +public class PatreonConfigTests +{ + [Fact] + public void Deserialize_WithAllFields_PopulatesCorrectly() + { + var json = """{"tier":"premium","free_post":false,"access_tier_id":"tier_abc123"}"""; + var config = JsonSerializer.Deserialize(json); + config!.Tier.Should().Be("premium"); + config.FreePost.Should().BeFalse(); + config.AccessTierId.Should().Be("tier_abc123"); + config.PrintProfiles.Should().BeEmpty(); + } + + [Fact] + public void Deserialize_WithNoOptionalFields_UsesDefaults() + { + var json = "{}"; + var config = JsonSerializer.Deserialize(json); + config!.Tier.Should().Be("free"); + config.FreePost.Should().BeTrue(); + config.AccessTierId.Should().BeNull(); + } + + [Fact] + public void Deserialize_InheritsBasePrintProfiles() + { + var json = """{"print_profiles":["./x.3mf"]}"""; + var config = JsonSerializer.Deserialize(json); + config!.PrintProfiles.Should().Equal("./x.3mf"); + } +} From 6def9dc2f0d94554d06b42935753693286c0676b Mon Sep 17 00:00:00 2001 From: Christof Lauriers Date: Sat, 14 Mar 2026 22:00:11 +0100 Subject: [PATCH 07/14] Add GetPlatformConfig() helper to ReleaseManifest Co-Authored-By: Claude Sonnet 4.6 --- .../Models/ReleaseManifest.cs | 12 ++++ .../ReleaseManifestGetPlatformConfigTests.cs | 59 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 tests/ModelPublisher.Core.Tests/Models/ReleaseManifestGetPlatformConfigTests.cs diff --git a/src/ModelPublisher.Core/Models/ReleaseManifest.cs b/src/ModelPublisher.Core/Models/ReleaseManifest.cs index 0853a45..584c8eb 100644 --- a/src/ModelPublisher.Core/Models/ReleaseManifest.cs +++ b/src/ModelPublisher.Core/Models/ReleaseManifest.cs @@ -60,6 +60,18 @@ private static string AppendDisclaimer(string description, string disclaimer) if (string.IsNullOrWhiteSpace(disclaimer)) return description; return $"{description}\n\n---\n\n{disclaimer}"; } + + private static readonly JsonSerializerOptions ConfigJsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + public T? GetPlatformConfig(string platformKey) where T : PlatformConfig, new() + { + if (!Platforms.TryGetValue(platformKey, out var el)) + return null; + return JsonSerializer.Deserialize(el, ConfigJsonOptions) ?? new T(); + } } public class ManifestFiles diff --git a/tests/ModelPublisher.Core.Tests/Models/ReleaseManifestGetPlatformConfigTests.cs b/tests/ModelPublisher.Core.Tests/Models/ReleaseManifestGetPlatformConfigTests.cs new file mode 100644 index 0000000..02e6175 --- /dev/null +++ b/tests/ModelPublisher.Core.Tests/Models/ReleaseManifestGetPlatformConfigTests.cs @@ -0,0 +1,59 @@ +using System.Text.Json; +using FluentAssertions; +using ModelPublisher.Core.Models; +using ModelPublisher.Core.Platforms; +using Xunit; + +namespace ModelPublisher.Core.Tests.Models; + +public class ReleaseManifestGetPlatformConfigTests +{ + private static ReleaseManifest DeserializeManifest(string platformsJson) + { + var json = $$""" + { + "title": "Test", + "platforms": {{platformsJson}} + } + """; + return JsonSerializer.Deserialize(json, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; + } + + [Fact] + public void GetPlatformConfig_AbsentKey_ReturnsNull() + { + var manifest = DeserializeManifest("""{"printables":{"tier":"free"}}"""); + manifest.GetPlatformConfig("makerworld").Should().BeNull(); + } + + [Fact] + public void GetPlatformConfig_BaseType_DeserializesTierAndProfiles() + { + var manifest = DeserializeManifest( + """{"printables":{"tier":"premium","print_profiles":["./profile.3mf"]}}"""); + var config = manifest.GetPlatformConfig("printables"); + config!.Tier.Should().Be("premium"); + config.PrintProfiles.Should().Equal("./profile.3mf"); + } + + [Fact] + public void GetPlatformConfig_DerivedType_DeserializesExtraFields() + { + var manifest = DeserializeManifest( + """{"patreon":{"tier":"premium","free_post":false,"access_tier_id":"t123"}}"""); + var config = manifest.GetPlatformConfig("patreon"); + config!.Tier.Should().Be("premium"); + config.FreePost.Should().BeFalse(); + config.AccessTierId.Should().Be("t123"); + } + + [Fact] + public void GetPlatformConfig_NoOptionalFields_ReturnsDefaults() + { + var manifest = DeserializeManifest("""{"printables":{}}"""); + var config = manifest.GetPlatformConfig("printables"); + config!.Tier.Should().Be("free"); + config.PrintProfiles.Should().BeEmpty(); + } +} From bb248ebd4ac89842521e254f1f3f6fa48fd48363 Mon Sep 17 00:00:00 2001 From: Christof Lauriers Date: Sat, 14 Mar 2026 22:04:15 +0100 Subject: [PATCH 08/14] Simplify ResolveTier to use GetPlatformConfig Co-Authored-By: Claude Sonnet 4.6 --- src/ModelPublisher.Core/PublishCommand.cs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/ModelPublisher.Core/PublishCommand.cs b/src/ModelPublisher.Core/PublishCommand.cs index 3384cbe..3cc254c 100644 --- a/src/ModelPublisher.Core/PublishCommand.cs +++ b/src/ModelPublisher.Core/PublishCommand.cs @@ -146,16 +146,9 @@ await File.WriteAllTextAsync( /// private static string? ResolveTier(ReleaseManifest manifest, IPlatformPublisher publisher) { - if (!manifest.Platforms.TryGetValue(publisher.PlatformKey, out var config)) - return null; - - if (config.ValueKind == JsonValueKind.Object - && config.TryGetProperty("tier", out var tierProp)) - { - var tier = tierProp.GetString()?.ToLowerInvariant(); - return tier is "free" or "premium" ? tier : "free"; - } - - return "free"; + var config = manifest.GetPlatformConfig(publisher.PlatformKey); + if (config is null) return null; + var tier = config.Tier.ToLowerInvariant(); + return tier is "free" or "premium" ? tier : "free"; } } \ No newline at end of file From 730e343e363d16eef41f4d48498ee8afd33d777f Mon Sep 17 00:00:00 2001 From: Christof Lauriers Date: Sat, 14 Mar 2026 22:04:53 +0100 Subject: [PATCH 09/14] Use PatreonConfig in PatreonPublisher for typed config access Co-Authored-By: Claude Sonnet 4.6 --- src/ModelPublisher.Core/Platforms/PatreonPublisher.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ModelPublisher.Core/Platforms/PatreonPublisher.cs b/src/ModelPublisher.Core/Platforms/PatreonPublisher.cs index cae236a..69e4587 100644 --- a/src/ModelPublisher.Core/Platforms/PatreonPublisher.cs +++ b/src/ModelPublisher.Core/Platforms/PatreonPublisher.cs @@ -38,6 +38,10 @@ public async Task PublishFreeAsync(ReleaseManifest manifest, IPag { try { + // TODO: use config.FreePost and config.AccessTierId when Patreon automation is implemented + var config = manifest.GetPlatformConfig(PlatformKey) ?? new PatreonConfig(); + _ = config; + await page.GotoAsync("https://www.patreon.com/posts/create"); await AuthGuard.EnsureLoggedInAsync(page, PlatformName, async p => From bab6afdfd829c6c7384867ffdb5c6e66bcdbf751 Mon Sep 17 00:00:00 2001 From: Christof Lauriers Date: Sat, 14 Mar 2026 22:07:31 +0100 Subject: [PATCH 10/14] Add print_profiles examples to example manifest Co-Authored-By: Claude Sonnet 4.6 --- releases/example-model/manifest.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/releases/example-model/manifest.json b/releases/example-model/manifest.json index f905ed5..7b7fef4 100644 --- a/releases/example-model/manifest.json +++ b/releases/example-model/manifest.json @@ -16,10 +16,12 @@ }, "platforms": { "printables": { - "tier": "free" + "tier": "free", + "print_profiles": ["./profiles/printables-0.2mm-pla.3mf"] }, "makerworld": { - "tier": "free" + "tier": "free", + "print_profiles": ["./profiles/makerworld-0.2mm-pla.3mf"] }, "cults3d": { "tier": "free" From 288644ee32c0fbbb259e3f82f5385c4797e8e8d5 Mon Sep 17 00:00:00 2001 From: Christof Lauriers Date: Sat, 14 Mar 2026 22:19:12 +0100 Subject: [PATCH 11/14] Move specs from docs/superpowers/specs/ to docs/specs/ Co-Authored-By: Claude Sonnet 4.6 --- .claude/worktrees/beautiful-banzai | 1 + .claude/worktrees/dazzling-bell | 1 + .claude/worktrees/trusting-morse | 1 + .../specs/2026-03-14-platform-config-print-profiles-design.md | 0 .../specs/2026-03-14-platform-config-print-profiles-plan.md | 0 5 files changed, 3 insertions(+) create mode 160000 .claude/worktrees/beautiful-banzai create mode 160000 .claude/worktrees/dazzling-bell create mode 160000 .claude/worktrees/trusting-morse rename docs/{superpowers => }/specs/2026-03-14-platform-config-print-profiles-design.md (100%) rename docs/{superpowers => }/specs/2026-03-14-platform-config-print-profiles-plan.md (100%) diff --git a/.claude/worktrees/beautiful-banzai b/.claude/worktrees/beautiful-banzai new file mode 160000 index 0000000..36e1f0f --- /dev/null +++ b/.claude/worktrees/beautiful-banzai @@ -0,0 +1 @@ +Subproject commit 36e1f0fad8d8e8e963875746bb2240f87028082d diff --git a/.claude/worktrees/dazzling-bell b/.claude/worktrees/dazzling-bell new file mode 160000 index 0000000..36e1f0f --- /dev/null +++ b/.claude/worktrees/dazzling-bell @@ -0,0 +1 @@ +Subproject commit 36e1f0fad8d8e8e963875746bb2240f87028082d diff --git a/.claude/worktrees/trusting-morse b/.claude/worktrees/trusting-morse new file mode 160000 index 0000000..8aae897 --- /dev/null +++ b/.claude/worktrees/trusting-morse @@ -0,0 +1 @@ +Subproject commit 8aae897bce1e59b8e62beac4b0c6dbaf014c3879 diff --git a/docs/superpowers/specs/2026-03-14-platform-config-print-profiles-design.md b/docs/specs/2026-03-14-platform-config-print-profiles-design.md similarity index 100% rename from docs/superpowers/specs/2026-03-14-platform-config-print-profiles-design.md rename to docs/specs/2026-03-14-platform-config-print-profiles-design.md diff --git a/docs/superpowers/specs/2026-03-14-platform-config-print-profiles-plan.md b/docs/specs/2026-03-14-platform-config-print-profiles-plan.md similarity index 100% rename from docs/superpowers/specs/2026-03-14-platform-config-print-profiles-plan.md rename to docs/specs/2026-03-14-platform-config-print-profiles-plan.md From 660ad8c6193511154e2caad1f7f6e4669c10c9a8 Mon Sep 17 00:00:00 2001 From: Christof Lauriers Date: Sat, 14 Mar 2026 22:20:31 +0100 Subject: [PATCH 12/14] Remove accidentally staged worktree submodule entries Co-Authored-By: Claude Sonnet 4.6 --- .claude/worktrees/beautiful-banzai | 1 - .claude/worktrees/dazzling-bell | 1 - .claude/worktrees/trusting-morse | 1 - 3 files changed, 3 deletions(-) delete mode 160000 .claude/worktrees/beautiful-banzai delete mode 160000 .claude/worktrees/dazzling-bell delete mode 160000 .claude/worktrees/trusting-morse diff --git a/.claude/worktrees/beautiful-banzai b/.claude/worktrees/beautiful-banzai deleted file mode 160000 index 36e1f0f..0000000 --- a/.claude/worktrees/beautiful-banzai +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 36e1f0fad8d8e8e963875746bb2240f87028082d diff --git a/.claude/worktrees/dazzling-bell b/.claude/worktrees/dazzling-bell deleted file mode 160000 index 36e1f0f..0000000 --- a/.claude/worktrees/dazzling-bell +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 36e1f0fad8d8e8e963875746bb2240f87028082d diff --git a/.claude/worktrees/trusting-morse b/.claude/worktrees/trusting-morse deleted file mode 160000 index 8aae897..0000000 --- a/.claude/worktrees/trusting-morse +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8aae897bce1e59b8e62beac4b0c6dbaf014c3879 From a20b750ae826cb55dabf5a0a6c5f6519e0bee2c6 Mon Sep 17 00:00:00 2001 From: Christof Lauriers Date: Sat, 14 Mar 2026 22:22:05 +0100 Subject: [PATCH 13/14] Move specs into per-feature subfolder docs/specs/platform-config-print-profiles/ Co-Authored-By: Claude Sonnet 4.6 --- .../design.md} | 0 .../plan.md} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename docs/specs/{2026-03-14-platform-config-print-profiles-design.md => platform-config-print-profiles/design.md} (100%) rename docs/specs/{2026-03-14-platform-config-print-profiles-plan.md => platform-config-print-profiles/plan.md} (100%) diff --git a/docs/specs/2026-03-14-platform-config-print-profiles-design.md b/docs/specs/platform-config-print-profiles/design.md similarity index 100% rename from docs/specs/2026-03-14-platform-config-print-profiles-design.md rename to docs/specs/platform-config-print-profiles/design.md diff --git a/docs/specs/2026-03-14-platform-config-print-profiles-plan.md b/docs/specs/platform-config-print-profiles/plan.md similarity index 100% rename from docs/specs/2026-03-14-platform-config-print-profiles-plan.md rename to docs/specs/platform-config-print-profiles/plan.md From 7f605357dc9a036ba867fa3d08941d9184b119d2 Mon Sep 17 00:00:00 2001 From: Christof Lauriers Date: Sat, 14 Mar 2026 22:53:08 +0100 Subject: [PATCH 14/14] Remove slop _ = config discard and update CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PatreonPublisher.cs: replace `var config = ...; _ = config;` with a TODO comment documenting the intended usage — the discard was suppressing an unused-variable warning instead of fixing it. CLAUDE.md: add PlatformConfig/PatreonConfig to key source files table, update manifest format example with print_profiles and Patreon fields, document GetPlatformConfig usage, and add Slopwatch section. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 23 +++++++++++++++++-- .../Platforms/PatreonPublisher.cs | 4 +--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d8552fc..80eefb5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,7 +26,9 @@ dotnet run --project src\ModelPublisher.Cli -- "C:\Users\chris\Downloads\Models\ | `src/ModelPublisher.Core/Shared/AuthGuard.cs` | Pauses for human login when not authenticated | | `src/ModelPublisher.Core/Shared/FileUploadHelper.cs` | `UploadSequentialAsync` — uploads one file at a time, waits for NetworkIdle | | `src/ModelPublisher.Core/Shared/MarkdownHelper.cs` | `ToPlainText` and `ToTipTapHtml` — converts markdown for platforms that need it | -| `src/ModelPublisher.Core/Models/ReleaseManifest.cs` | Deserializes manifest.json | +| `src/ModelPublisher.Core/Models/ReleaseManifest.cs` | Deserializes manifest.json; `GetPlatformConfig()` deserializes typed platform config | +| `src/ModelPublisher.Core/Models/PlatformConfig.cs` | Base record with `Tier` + `PrintProfiles` — all platform configs inherit from this | +| `src/ModelPublisher.Core/Platforms/PatreonConfig.cs` | Patreon-specific config: `FreePost`, `AccessTierId` | | `src/ModelPublisher.Core/Models/PublishResult.cs` | Result record — `Tier` is set by orchestrator via `with`, not by publishers | ## Manifest format @@ -42,12 +44,23 @@ dotnet run --project src\ModelPublisher.Cli -- "C:\Users\chris\Downloads\Models\ "photos": ["./cover-photo.jpg", "./detail.jpg"] }, "platforms": { - "printables": { "tier": "free" } + "printables": { + "tier": "free", + "print_profiles": ["./profiles/printables-0.2mm.3mf"] + }, + "patreon": { + "tier": "premium", + "free_post": false, + "access_tier_id": "YOUR_TIER_ID" + } } } ``` - `cover` is optional. If set, `PhotosOrdered(coverFirst)` deduplicates and positions it. - `manifest.ManifestDirectory` is set after deserialization; use `ResolveFilePath()` for all file paths. +- `print_profiles` is optional on any platform; defaults to `[]`. Paths are relative to manifest dir. +- To read typed config in a publisher: `manifest.GetPlatformConfig(PlatformKey)` (returns `null` if platform not listed). Use a subclass (e.g. `PatreonConfig`) for platform-specific fields. +- `Platforms` stays `Dictionary` internally — `GetPlatformConfig` deserializes on demand. ## Platform status | Key | Platform | Status | @@ -79,6 +92,12 @@ dotnet run --project src\ModelPublisher.Cli -- "C:\Users\chris\Downloads\Models\ - **Spectre.Console**: any string containing `[` or `]` from user data must be wrapped in `Markup.Escape()`. - **System.CommandLine 2.0.3**: `SetAction` + `ParseResult.GetValue` only — old `Handler` API removed. +## Slopwatch +- Installed globally: `dotnet tool install --global Slopwatch.Cmd` (v0.4.0) +- Baseline initialized at `.slopwatch/baseline.json` (0 pre-existing issues on master) +- Run after code changes: `powershell.exe -Command "cd 'C:\Source\ModelPublisher'; slopwatch analyze -d ."` +- Detects: disabled tests, empty catch blocks, warning suppression, arbitrary delays, NoWarn in csproj, CPM bypass + ## GitHub workflow - Repo: https://github.com/TheCraftyMaker/ModelPublisher - `master` is protected — PRs required, no direct pushes, enforce_admins=true diff --git a/src/ModelPublisher.Core/Platforms/PatreonPublisher.cs b/src/ModelPublisher.Core/Platforms/PatreonPublisher.cs index 69e4587..94f4293 100644 --- a/src/ModelPublisher.Core/Platforms/PatreonPublisher.cs +++ b/src/ModelPublisher.Core/Platforms/PatreonPublisher.cs @@ -38,9 +38,7 @@ public async Task PublishFreeAsync(ReleaseManifest manifest, IPag { try { - // TODO: use config.FreePost and config.AccessTierId when Patreon automation is implemented - var config = manifest.GetPlatformConfig(PlatformKey) ?? new PatreonConfig(); - _ = config; + // TODO: read manifest.GetPlatformConfig(PlatformKey) for FreePost and AccessTierId when Patreon automation is implemented await page.GotoAsync("https://www.patreon.com/posts/create");