From 156c2bbbae32f6bfbe56c1dd18e992437cf5006b Mon Sep 17 00:00:00 2001 From: Christof Lauriers Date: Sat, 28 Mar 2026 11:45:39 +0100 Subject: [PATCH 1/4] Add stealth mode and Cloudflare bypass infrastructure - Add --stealth flag to CLI (root + codegen commands) that launches Brave via playwright-extra stealth plugin (Node.js) and connects over CDP - Add --profile-path flag to copy cf_clearance cookies + DPAPI key from real Brave profile into the platform profile for Cloudflare bypass - Add stealth-launcher.js: Node.js script that starts Brave with the puppeteer-extra-plugin-stealth plugin and emits READY:{port} on stdout - Add package.json with playwright-extra and stealth plugin dependencies - Add BrowserContextFactory.GetStealthContextAsync: starts the Node.js launcher, waits for READY signal, then connects via ConnectOverCDPAsync - Add anti-automation patches to persistent context: removes navigator.webdriver, spoofs canvas/WebGL/plugins, adds --disable-blink-features=AutomationControlled - Update .gitignore to exclude node_modules/ and package-lock.json - Update README and CLAUDE.md with stealth mode usage and Cloudflare notes Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 4 + CLAUDE.md | 27 ++++ README.md | 37 +++-- package.json | 30 ++++ src/ModelPublisher.Cli/Program.cs | 39 ++++- .../Platforms/MakerWorldPublisher.cs | 126 +++++++++------ .../Platforms/PatreonPublisher.cs | 9 +- .../Platforms/PrintablesPublisher.cs | 9 +- src/ModelPublisher.Core/PublishCommand.cs | 7 +- .../Shared/BrowserContextFactory.cs | 150 +++++++++++++++++- stealth-launcher.js | 51 ++++++ 11 files changed, 409 insertions(+), 80 deletions(-) create mode 100644 package.json create mode 100644 stealth-launcher.js diff --git a/.gitignore b/.gitignore index 40b2916..1026bcc 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,7 @@ profiles/ ## dotnet *.nupkg + +## Node.js (stealth launcher dependencies) +node_modules/ +package-lock.json diff --git a/CLAUDE.md b/CLAUDE.md index 80eefb5..17c7a65 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -108,3 +108,30 @@ dotnet run --project src\ModelPublisher.Cli -- "C:\Users\chris\Downloads\Models\ ## Browser profiles Persistent Chromium sessions stored in `profiles/{platformKey}/` relative to working directory. Gitignored — contains login cookies. On first run per platform, the browser pauses for manual login. + +## Cloudflare / bot detection +Cloudflare detects Playwright's CDP protocol and blocks access even when a human clicks "I'm human". This affects MakerWorld and Patreon. Two mitigations are in place in `BrowserContextFactory`: +- `--disable-blink-features=AutomationControlled` launch arg +- Init script removing `navigator.webdriver`, spoofing canvas/WebGL/plugins + +When these aren't enough, use `--profile-path` to run with the real Brave profile (which has `cf_clearance` cookies from normal browsing). **Brave must be fully closed first.** + +```bash +# For publishing: +dotnet run --project src\ModelPublisher.Cli -- "manifest.json" --platforms makerworld --profile-path "C:\Users\chris\AppData\Local\BraveSoftware\Brave-Browser\User Data\Profile 1" + +# For codegen: +dotnet run --project src\ModelPublisher.Cli -- codegen makerworld https://makerworld.com/en/my/models/publish?type=original --profile-path "C:\Users\chris\AppData\Local\BraveSoftware\Brave-Browser\User Data\Profile 1" +``` + +Google OAuth is blocked in all automated browsers (Playwright injects CDP which Google detects). Log into Cloudflare-protected platforms using email/password, not Google, during the AuthGuard pause. + +## Running Playwright codegen +`npx playwright codegen` does NOT work from Claude Code's bash shell (node not on PATH, GUI can't open in background). Always run codegen manually from a Windows terminal. + +**For sites without Cloudflare / bot detection** — use the .NET playwright.ps1 (built output): +```powershell +pwsh "C:\Source\ModelPublisher\src\ModelPublisher.Cli\bin\Debug\net10.0\playwright.ps1" codegen https://example.com +``` + +**For sites with Cloudflare / Google auth (e.g. Patreon)** — codegen won't work (Chromium is blocked). Instead: open the page in the regular Brave browser, interact with the form, and use DevTools (F12) to inspect elements. Paste HTML snippets into the conversation to extract selectors. diff --git a/README.md b/README.md index 15bc40a..ead6222 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Automates publishing 3D printable models across multiple platforms using Playwri | `printables` | Printables.com | Free + Premium | Working | | `makerworld` | MakerWorld.com | Free | Stub — needs codegen selectors | | `cults3d` | Cults3D.com | Free | Stub — needs codegen selectors | -| `thangs` | Thangs.com | Free + Premium | Stub — needs codegen selectors | +| `thangs` | Thangs.com | Free + Premium | Working | | `makeronline` | Maker.online | Free | Stub — needs codegen selectors | | `patreon` | Patreon.com | Free + Premium | Stub — clipboard paste approach, needs verification | @@ -43,9 +43,12 @@ pwsh src/ModelPublisher.Cli/bin/Debug/net10.0/playwright.ps1 install chromium For platforms marked as stubs, use Playwright's codegen tool to capture live selectors, then fill them in to the publisher class: ```bash -pwsh src/ModelPublisher.Cli/bin/Debug/net10.0/playwright.ps1 codegen --target csharp https://www.makerworld.com/upload +# Close Brave first, then run: +dotnet run --project src\ModelPublisher.Cli -- codegen --profile-path "C:\Users\chris\AppData\Local\BraveSoftware\Brave-Browser\User Data\Profile 1" ``` +The `--profile-path` option lets you reuse your real Brave session (already logged in). Brave must be fully closed before running this command. + ## Release Workflow 1. Design, print, and photograph your model. @@ -69,25 +72,33 @@ dotnet run --project src/ModelPublisher.Cli -- releases/my-model-name/manifest.j # Publish to specific platforms only dotnet run --project src/ModelPublisher.Cli -- releases/my-model-name/manifest.json --platforms printables makerworld -# From C:\Source\ModelPublisher: -dotnet run --project src/ModelPublisher.Cli -- "C:\Source\ModelPublisher\releases\my-model-name\manifest.json" +# Use your real Brave profile to bypass Cloudflare (close Brave first) +dotnet run --project src/ModelPublisher.Cli -- releases/my-model-name/manifest.json --platforms makerworld --profile-path "C:\Users\chris\AppData\Local\BraveSoftware\Brave-Browser\User Data\Profile 1" ``` -## Human-in-the-loop steps - -Every platform pauses before the final publish action and waits for you to press Enter. This lets you: -- Catch any mis-filled fields -- Select categories or subcategories not covered by automation -- Handle CAPTCHAs if they appear - -Patreon additionally requires manual access tier selection due to the complexity of their UI. - ## Auth On first run per platform, if you're not logged in, the browser will pause and prompt you to log in manually. After logging in, press Enter in the terminal. The session is saved to `profiles//` and reused on subsequent runs. The `profiles/` directory is gitignored. Do not commit it — it contains your session cookies. +### Cloudflare-protected platforms (MakerWorld, Patreon) + +Cloudflare detects Playwright's CDP protocol at the network level. Use `--stealth` to launch via [playwright-extra](https://github.com/berstend/puppeteer-extra) with the stealth plugin, which patches the browser to remove automation markers: + +```bash +# Stealth mode (bypasses Cloudflare bot detection) +dotnet run --project src\ModelPublisher.Cli -- "manifest.json" --platforms makerworld --stealth + +# Stealth + real Brave profile (best combination — close Brave first) +dotnet run --project src\ModelPublisher.Cli -- "manifest.json" --platforms makerworld --stealth --profile-path "C:\Users\chris\AppData\Local\BraveSoftware\Brave-Browser\User Data\Profile 1" + +# Stealth codegen (for inspecting selectors on Cloudflare-protected sites) +dotnet run --project src\ModelPublisher.Cli -- codegen makerworld https://makerworld.com/en/my/models/publish?type=original --stealth +``` + +Requires Node.js and `npm install` in the project root (one-time setup). + ## Adding a new platform 1. Create `src/ModelPublisher.Core/Platforms/MyNewPublisher.cs` implementing `IPlatformPublisher`. diff --git a/package.json b/package.json new file mode 100644 index 0000000..5f39b49 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "modelpublisher", + "version": "1.0.0", + "description": "[![.NET](https://github.com/TheCraftyMaker/ModelPublisher/actions/workflows/dotnet.yml/badge.svg)](https://github.com/TheCraftyMaker/ModelPublisher/actions/workflows/dotnet.yml) [![CodeQL Advanced](https://github.com/TheCraftyMaker/ModelPublisher/actions/workflows/codeql.yml/badge.svg)](https://github.com/TheCraftyMaker/ModelPublisher/actions/workflows/codeql.yml) [![Dependabot Updates](https://github.com/TheCraftyMaker/ModelPublisher/actions/workflows/dependabot/dependabot-updates/badge.svg)](https://github.com/TheCraftyMaker/ModelPublisher/actions/workflows/dependabot/dependabot-updates)", + "main": "index.js", + "directories": { + "doc": "docs", + "test": "tests" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/TheCraftyMaker/ModelPublisher.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "bugs": { + "url": "https://github.com/TheCraftyMaker/ModelPublisher/issues" + }, + "homepage": "https://github.com/TheCraftyMaker/ModelPublisher#readme", + "dependencies": { + "playwright": "^1.58.2", + "playwright-extra": "^4.3.6", + "puppeteer-extra-plugin-stealth": "^2.11.2" + } +} diff --git a/src/ModelPublisher.Cli/Program.cs b/src/ModelPublisher.Cli/Program.cs index 6703f3b..e15f215 100644 --- a/src/ModelPublisher.Cli/Program.cs +++ b/src/ModelPublisher.Cli/Program.cs @@ -16,10 +16,22 @@ }; platformsOption.Aliases.Add("-p"); +var profilePathOption = new Option("--profile-path") +{ + Description = "Use a custom browser profile directory (e.g. your real Brave profile) to bypass Cloudflare. Close Brave first." +}; + +var stealthOption = new Option("--stealth") +{ + Description = "Launch via playwright-extra stealth plugin to bypass Cloudflare bot detection." +}; + var rootCommand = new RootCommand("ModelPublisher — automates 3D model publishing across platforms.") { manifestArg, - platformsOption + platformsOption, + profilePathOption, + stealthOption }; rootCommand.SetAction(async (parseResult, ct) => @@ -32,10 +44,14 @@ .LeftJustified() .Color(Color.Cyan1)); + var profilePath = parseResult.GetValue(profilePathOption); + var stealth = parseResult.GetValue(stealthOption); var command = new PublishCommand(); return await command.ExecuteAsync( manifest.FullName, platforms.Length > 0 ? platforms : null, + profilePath, + stealth, ct ); }); @@ -43,21 +59,38 @@ // codegen — opens Brave with the platform's persistent profile and pauses for inspection var codegenPlatformArg = new Argument("platform") { Description = "Platform key (e.g. thangs)" }; var codegenUrlArg = new Argument("url") { Description = "URL to navigate to" }; +var codegenProfileOption = new Option("--profile-path") +{ + Description = "Override the profile directory (e.g. point to your real Brave profile while it's closed)." +}; +var codegenStealthOption = new Option("--stealth") +{ + Description = "Use playwright-extra stealth plugin (for Cloudflare-protected sites)." +}; + var codegenCommand = new Command("codegen", "Open Brave with a platform profile for selector inspection") { codegenPlatformArg, - codegenUrlArg + codegenUrlArg, + codegenProfileOption, + codegenStealthOption }; codegenCommand.SetAction(async (parseResult, ct) => { var platformKey = parseResult.GetValue(codegenPlatformArg)!; var url = parseResult.GetValue(codegenUrlArg)!; + var profilePath = parseResult.GetValue(codegenProfileOption); + var stealth = parseResult.GetValue(codegenStealthOption); AnsiConsole.MarkupLine($"[cyan]Opening Brave with profile '[bold]{platformKey}[/]' at {Markup.Escape(url)}[/]"); + if (stealth) AnsiConsole.MarkupLine("[yellow]Stealth mode enabled (playwright-extra)[/]"); + if (profilePath != null) AnsiConsole.MarkupLine($"[yellow]Using custom profile: {Markup.Escape(profilePath)}[/]"); AnsiConsole.MarkupLine("[yellow]Use the Playwright Inspector to inspect selectors. Close the browser when done.[/]"); using var playwright = await Playwright.CreateAsync(); - var context = await BrowserContextFactory.GetPersistentContextAsync(playwright, platformKey); + var context = stealth + ? await BrowserContextFactory.GetStealthContextAsync(playwright, profilePath) + : await BrowserContextFactory.GetPersistentContextAsync(playwright, platformKey, profilePathOverride: profilePath); var page = await context.NewPageAsync(); await page.GotoAsync(url); await page.PauseAsync(); diff --git a/src/ModelPublisher.Core/Platforms/MakerWorldPublisher.cs b/src/ModelPublisher.Core/Platforms/MakerWorldPublisher.cs index 7aafee5..17e9be0 100644 --- a/src/ModelPublisher.Core/Platforms/MakerWorldPublisher.cs +++ b/src/ModelPublisher.Core/Platforms/MakerWorldPublisher.cs @@ -17,59 +17,30 @@ public class MakerWorldPublisher : IPlatformPublisher { public string PlatformKey => "makerworld"; public string PlatformName => "MakerWorld"; - + public bool IsFreeOnly => true; public bool SupportsMarkdown => true; - public string Disclaimer => ""; + public string Disclaimer => GetDisclaimer(); - public async Task PublishFreeAsync(ReleaseManifest manifest, IPage page, CancellationToken ct = default) + public async Task PublishFreeAsync(ReleaseManifest manifest, IPage page, + CancellationToken ct = default) { try { - await page.GotoAsync("https://makerworld.com/en/create"); - - await AuthGuard.EnsureLoggedInAsync(page, PlatformName, async p => - { - return await p.Locator(".user-avatar, [data-testid='user-avatar'], .avatar-wrapper") - .First.IsVisibleAsync(); - }, ct); - - AnsiConsole.MarkupLine($"[cyan][[{PlatformName}]][/] Filling model details..."); - - // TODO: Replace selectors after running codegen - await page.Locator("input[name='title'], input[placeholder*='title' i]").First.FillAsync(manifest.Title); - - var descEditor = page.Locator("textarea[name='description'], [contenteditable='true']").First; - await descEditor.ClickAsync(); - await descEditor.FillAsync(manifest.GetDescription(this)); - - // Tags - foreach (var tag in manifest.Tags) - { - var tagInput = page.Locator("input[placeholder*='tag' i]").First; - await tagInput.FillAsync(tag); - await tagInput.PressAsync("Enter"); - await page.WaitForTimeoutAsync(300); - } - - // Model file upload - AnsiConsole.MarkupLine($"[cyan][[{PlatformName}]][/] Uploading model file..."); - var modelInput = page.Locator("input[type='file']").First; - // await modelInput.SetInputFilesAsync(manifest.ResolveFilePath(manifest.Files.Model)); - await page.WaitForLoadStateAsync(LoadState.NetworkIdle); - - // Photos - AnsiConsole.MarkupLine($"[cyan][[{PlatformName}]][/] Uploading photos..."); - var photoInput = page.Locator("input[type='file'][accept*='image']").First; - await FileUploadHelper.UploadSequentialAsync(page, photoInput, - manifest.Files.Photos.Select(manifest.ResolveFilePath), PlatformName); - - AnsiConsole.MarkupLine($"[yellow][[{PlatformName}]][/] Review the form in the browser. Press [green]Enter[/] to publish..."); - await Task.Run(() => Console.ReadLine(), ct); - - await page.Locator("button:has-text('Publish'), button[type='submit']").First.ClickAsync(); - await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + await page.GotoAsync("https://makerworld.com/en/my/models/publish?type=original"); + + await AuthGuard + .EnsureLoggedInAsync(page, PlatformName, + async p => await p + .Locator("img[src='https://public-cdn.bblmw.com/avatar/d59a9d40-0c79-11ee-9a50-b1cd743d2b1a." + + "jpg?x-oss-process=image/resize,w_60/format,webp']").First + .IsVisibleAsync(), ct); + + // Step 1: Specific platform print profile(s) + await UploadFiles(manifest, page); + + return new PublishResult(PlatformName, true, page.Url, null); } @@ -83,4 +54,65 @@ public Task PublishPremiumAsync(ReleaseManifest manifest, IPage p { throw new NotImplementedException(); } -} + + private async Task UploadFiles(ReleaseManifest manifest, IPage page) + { + // Step 1: Specific platform print profile(s) + var config = manifest.GetPlatformConfig(PlatformKey); + if (config != null && config.PrintProfiles.Any()) + { + AnsiConsole.MarkupLine($"[cyan][[{PlatformName}]][/] Uploading MakerWorld specific profile..."); + + await page + .GetByRole(AriaRole.Radio, new() { Name = "Yes (earn extra points reward)" }) + .CheckAsync(); + + var profileInput = page.Locator("input[type='file'][accept='.3mf']").First; + + var profilePath = config.PrintProfiles.Select(manifest.ResolveFilePath).First(); + + await FileUploadHelper.UploadSequentialAsync(page, profileInput, [profilePath], PlatformName); + } + else + { + AnsiConsole.MarkupLine( + $"[cyan][[{PlatformName}]][/] No MakerWorld specific profile found. Skipping..."); + + await page + .GetByRole(AriaRole.Radio, new() { Name = "I have STL/CAD files or other types of 3MF files" }) + .CheckAsync(); + } + + // Step 2: Model files + AnsiConsole.MarkupLine($"[cyan][[{PlatformName}]][/] Uploading model files..."); + + var pathsToExclude = new List(); + if (config != null && config.PrintProfiles.Any()) + { + pathsToExclude.AddRange(config.PrintProfiles.Select(manifest.ResolveFilePath)); + } + + var nonProfileModelFiles = manifest.Files.Models + .Select(manifest.ResolveFilePath) + .Except(pathsToExclude); + + var modelInput = page.Locator( + "input[type='file'][accept*='.3ds, .amf, .blend, .dwg, .dxf, .f3d, .f3z, .factory, " + + ".fcstd, .iges, .ipt, .obj, .ply, .py, .rsdoc, .scad, .shape, .shapr, .skp, .sldasm, " + + ".sldprt, .slvs, .step, .stl, .stp, .studio3, .zip, .3mf, .stpz, .fcstd']").First; + + await FileUploadHelper.UploadSequentialAsync(page, modelInput, nonProfileModelFiles, PlatformName); + + await page + .GetByRole(AriaRole.Button, new() { Name = "Next Step" }) + .ClickAsync(); + } + + private static string GetDisclaimer() + { + return "_ALL The Crafty Maker designs are protected by Copyright Law. By downloading, YOU HAVE NO RIGHT " + + "to sell any digital files or reproductions from those files. If you want a commercial license to " + + "LEGALLY SELL 3D prints, you'll need a [The Crafty Seller Patreon](https://patreon.com/TheCraftyMaker) " + + "subscription._"; + } +} \ No newline at end of file diff --git a/src/ModelPublisher.Core/Platforms/PatreonPublisher.cs b/src/ModelPublisher.Core/Platforms/PatreonPublisher.cs index cae236a..9f79740 100644 --- a/src/ModelPublisher.Core/Platforms/PatreonPublisher.cs +++ b/src/ModelPublisher.Core/Platforms/PatreonPublisher.cs @@ -38,13 +38,10 @@ public async Task PublishFreeAsync(ReleaseManifest manifest, IPag { try { - await page.GotoAsync("https://www.patreon.com/posts/create"); + await page.GotoAsync("https://www.patreon.com/c/TheCraftyMaker"); - await AuthGuard.EnsureLoggedInAsync(page, PlatformName, async p => - { - return await p.Locator("[data-tag='user-avatar'], .UserAvatar, [aria-label*='profile' i]") - .First.IsVisibleAsync(); - }, ct); + await AuthGuard.EnsureLoggedInAsync(page, PlatformName, + async p => await p.Locator("[data-tag='account-menu-toggle-switcher']").First.IsVisibleAsync(), ct); AnsiConsole.MarkupLine($"[cyan][[{PlatformName}]][/] Filling post details..."); diff --git a/src/ModelPublisher.Core/Platforms/PrintablesPublisher.cs b/src/ModelPublisher.Core/Platforms/PrintablesPublisher.cs index e834e06..dd38188 100644 --- a/src/ModelPublisher.Core/Platforms/PrintablesPublisher.cs +++ b/src/ModelPublisher.Core/Platforms/PrintablesPublisher.cs @@ -25,12 +25,9 @@ public async Task PublishFreeAsync(ReleaseManifest manifest, IPag { await page.GotoAsync("https://www.printables.com/model/create"); - await AuthGuard.EnsureLoggedInAsync(page, PlatformName, async p => - { - // Logged-in indicator: avatar or user menu visible - return await p.Locator("[data-cy='user-menu'], .user-avatar, [aria-label='User menu']") - .First.IsVisibleAsync(); - }, ct); + await AuthGuard + .EnsureLoggedInAsync(page, PlatformName, + async p => await p.Locator("[data-cy='user-menu'], .user-avatar, [aria-label='User menu']").First.IsVisibleAsync(), ct); // Step 1: Model files AnsiConsole.MarkupLine($"[cyan][[{PlatformName}]][/] Uploading model file..."); diff --git a/src/ModelPublisher.Core/PublishCommand.cs b/src/ModelPublisher.Core/PublishCommand.cs index d94f991..4232fa4 100644 --- a/src/ModelPublisher.Core/PublishCommand.cs +++ b/src/ModelPublisher.Core/PublishCommand.cs @@ -28,6 +28,8 @@ public class PublishCommand public async Task ExecuteAsync( string manifestPath, string[]? onlyPlatforms, + string? profilePathOverride = null, + bool stealth = false, CancellationToken ct = default) { if (!File.Exists(manifestPath)) @@ -88,8 +90,9 @@ public async Task ExecuteAsync( var tasks = runnablePublishers.Select(async x => { - await using var context = await BrowserContextFactory.GetPersistentContextAsync( - playwright, x.Publisher.PlatformKey); + await using var context = stealth + ? await BrowserContextFactory.GetStealthContextAsync(playwright, profilePathOverride) + : await BrowserContextFactory.GetPersistentContextAsync(playwright, x.Publisher.PlatformKey, profilePathOverride: profilePathOverride); var page = await context.NewPageAsync(); diff --git a/src/ModelPublisher.Core/Shared/BrowserContextFactory.cs b/src/ModelPublisher.Core/Shared/BrowserContextFactory.cs index 872c294..7f88aa6 100644 --- a/src/ModelPublisher.Core/Shared/BrowserContextFactory.cs +++ b/src/ModelPublisher.Core/Shared/BrowserContextFactory.cs @@ -1,3 +1,6 @@ +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; using Microsoft.Playwright; namespace ModelPublisher.Core.Shared; @@ -12,18 +15,159 @@ public static class BrowserContextFactory public static async Task GetPersistentContextAsync( IPlaywright playwright, string platformKey, - bool headless = false) + bool headless = false, + string? profilePathOverride = null) { var profilePath = Path.GetFullPath(Path.Combine("profiles", platformKey)); Directory.CreateDirectory(profilePath); - return await playwright.Chromium.LaunchPersistentContextAsync(profilePath, new() + if (profilePathOverride != null) + { + // Copy session cookies + encryption key from the real Brave profile into the platform profile. + // This lets us bypass Cloudflare using cf_clearance cookies from normal browsing. + // DPAPI decryption works because we're the same Windows user. + var profileName = Path.GetFileName(profilePathOverride); + var userDataDir = Path.GetDirectoryName(profilePathOverride)!; + var isNamedProfile = profileName.StartsWith("Profile ", StringComparison.OrdinalIgnoreCase) || profileName == "Default"; + var sourceProfileDir = isNamedProfile ? profilePathOverride : profilePathOverride; + var sourceUserDataDir = isNamedProfile ? userDataDir : profilePathOverride; + + // Local State holds the AES key used to decrypt cookies (itself DPAPI-protected) + var localStateSrc = Path.Combine(sourceUserDataDir, "Local State"); + if (File.Exists(localStateSrc)) + { + File.Copy(localStateSrc, Path.Combine(profilePath, "Local State"), overwrite: true); + Console.WriteLine($"[BrowserContextFactory] Copied Local State from {localStateSrc}"); + } + else Console.WriteLine($"[BrowserContextFactory] Local State not found at {localStateSrc}"); + + // Cookies + var cookiesSrc = Path.Combine(sourceProfileDir, "Network", "Cookies"); + if (File.Exists(cookiesSrc)) + { + var cookiesDest = Path.Combine(profilePath, "Default", "Network"); + Directory.CreateDirectory(cookiesDest); + File.Copy(cookiesSrc, Path.Combine(cookiesDest, "Cookies"), overwrite: true); + Console.WriteLine($"[BrowserContextFactory] Copied Cookies from {cookiesSrc}"); + } + else Console.WriteLine($"[BrowserContextFactory] Cookies not found at {cookiesSrc}"); + } + + var context = await playwright.Chromium.LaunchPersistentContextAsync(profilePath, new() { Headless = headless, SlowMo = 80, ExecutablePath = @"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe", ViewportSize = new ViewportSize { Width = 1400, Height = 900 }, - UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + Permissions = ["geolocation"], + Args = ["--disable-blink-features=AutomationControlled"] }); + + // Remove automation markers and spoof fingerprints to evade Cloudflare bot detection + await context.AddInitScriptAsync(""" + // 1. Remove webdriver flag + Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); + delete navigator.__proto__.webdriver; + + // 2. Restore chrome object + window.chrome = { runtime: {} }; + + // 3. Realistic languages + Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] }); + + // 4. Realistic plugins + Object.defineProperty(navigator, 'plugins', { get: () => [ + { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' }, + { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' } + ]}); + + // 5. Canvas fingerprint noise + const origToDataURL = HTMLCanvasElement.prototype.toDataURL; + HTMLCanvasElement.prototype.toDataURL = function(type) { + const ctx = this.getContext('2d'); + if (ctx) { + try { + const imageData = ctx.getImageData(0, 0, this.width, this.height); + for (let i = 0; i < imageData.data.length; i += 4) { + imageData.data[i] += Math.floor(Math.random() * 3) - 1; + } + ctx.putImageData(imageData, 0, 0); + } catch {} + } + return origToDataURL.call(this, type); + }; + + // 6. WebGL fingerprint spoofing + const getParameter = WebGLRenderingContext.prototype.getParameter; + WebGLRenderingContext.prototype.getParameter = function(parameter) { + if (parameter === 37445) return 'Intel Inc.'; + if (parameter === 37446) return 'Intel Iris OpenGL Engine'; + return getParameter.call(this, parameter); + }; + """); + + return context; + } + + /// + /// Launches Brave via the stealth-launcher.js Node.js script (playwright-extra + stealth plugin) + /// and connects to it over CDP. Use this for Cloudflare-protected platforms. + /// + public static async Task GetStealthContextAsync( + IPlaywright playwright, + string? profilePathOverride = null) + { + var port = GetFreePort(); + // stealth-launcher.js lives in the project root; during dotnet run the CWD is the project root + var launcherPath = Path.GetFullPath(Path.Combine( + AppContext.BaseDirectory, "..", "..", "..", "..", "..", "stealth-launcher.js")); + var nodeExe = @"C:\Program Files\nodejs\node.exe"; + + var arguments = $"\"{launcherPath}\" {port}"; + if (profilePathOverride != null) + arguments += $" \"{profilePathOverride}\""; + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = nodeExe, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + } + }; + process.Start(); + + // Wait for the READY signal from the launcher + bool ready = false; + while (!ready) + { + var line = await process.StandardOutput.ReadLineAsync(); + if (line == null) break; + if (line.StartsWith("READY:")) + ready = true; + } + + if (!ready) + { + var stderr = await process.StandardError.ReadToEndAsync(); + throw new InvalidOperationException($"stealth-launcher did not emit a READY signal. stderr: {stderr}"); + } + + var browser = await playwright.Chromium.ConnectOverCDPAsync($"http://localhost:{port}"); + return browser.Contexts.FirstOrDefault() + ?? await browser.NewContextAsync(); + } + + private static int GetFreePort() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; } } diff --git a/stealth-launcher.js b/stealth-launcher.js new file mode 100644 index 0000000..bf70ddc --- /dev/null +++ b/stealth-launcher.js @@ -0,0 +1,51 @@ +'use strict'; +// Launches Brave with playwright-extra stealth plugin and exposes a CDP endpoint. +// .NET connects to it via ConnectOverCDPAsync to run publisher code without bot detection. +// +// Usage: node stealth-launcher.js [profilePath] +// profilePath: e.g. "C:\Users\...\Brave-Browser\User Data\Profile 1" +// If it ends in "Profile N" or "Default", splits into --user-data-dir + --profile-directory. + +const { chromium } = require('playwright-extra'); +const StealthPlugin = require('puppeteer-extra-plugin-stealth'); +chromium.use(StealthPlugin()); + +const path = require('path'); + +const cdpPort = parseInt(process.argv[2] || '9222', 10); +const profilePath = process.argv[3] || null; + +(async () => { + const args = [ + `--remote-debugging-port=${cdpPort}`, + '--disable-blink-features=AutomationControlled', + '--no-first-run', + '--no-default-browser-check', + ]; + + if (profilePath) { + const profileName = path.basename(profilePath); + const isNamedProfile = /^Profile \d+$/i.test(profileName) || profileName === 'Default'; + if (isNamedProfile) { + args.push(`--user-data-dir=${path.dirname(profilePath)}`); + args.push(`--profile-directory=${profileName}`); + } else { + args.push(`--user-data-dir=${profilePath}`); + } + } + + const browser = await chromium.launch({ + headless: false, + executablePath: 'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe', + args, + }); + + // Signal ready — .NET already knows the port + process.stdout.write(`READY:${cdpPort}\n`); + + // Exit when .NET disconnects + browser.on('disconnected', () => process.exit(0)); +})().catch(err => { + process.stderr.write(`stealth-launcher error: ${err.message}\n`); + process.exit(1); +}); \ No newline at end of file From 88e00b3fa3d9753ed2f09855554154cff77c4451 Mon Sep 17 00:00:00 2001 From: Christof Lauriers Date: Sat, 28 Mar 2026 17:23:02 +0100 Subject: [PATCH 2/4] Add networkIdleTimeoutMs option and improve Brave profile handling - FileUploadHelper: Add `networkIdleTimeoutMs` parameter for wait customization; handle timeout exceptions during SPA loads. - stealth-launcher.js: Refactor Brave profile logic to correctly handle platform-specific profiles. - BrowserContextFactory: Support dynamic Brave profile paths based on `platformKey`; ensure directory creation for platform profiles. - PublishCommand: Pass `platformKey` to stealth context creation for profile-specific contexts. - Clean up unused solution folder from ModelPublisher.sln. --- ModelPublisher.sln | 2 - src/ModelPublisher.Cli/Program.cs | 2 +- src/ModelPublisher.Core/PublishCommand.cs | 2 +- .../Shared/BrowserContextFactory.cs | 13 +++++-- .../Shared/FileUploadHelper.cs | 18 +++++++-- stealth-launcher.js | 38 +++++++++++-------- 6 files changed, 48 insertions(+), 27 deletions(-) diff --git a/ModelPublisher.sln b/ModelPublisher.sln index 6b859ea..077f8bb 100644 --- a/ModelPublisher.sln +++ b/ModelPublisher.sln @@ -11,8 +11,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05 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 diff --git a/src/ModelPublisher.Cli/Program.cs b/src/ModelPublisher.Cli/Program.cs index e15f215..fec6995 100644 --- a/src/ModelPublisher.Cli/Program.cs +++ b/src/ModelPublisher.Cli/Program.cs @@ -89,7 +89,7 @@ using var playwright = await Playwright.CreateAsync(); var context = stealth - ? await BrowserContextFactory.GetStealthContextAsync(playwright, profilePath) + ? await BrowserContextFactory.GetStealthContextAsync(playwright, platformKey, profilePath) : await BrowserContextFactory.GetPersistentContextAsync(playwright, platformKey, profilePathOverride: profilePath); var page = await context.NewPageAsync(); await page.GotoAsync(url); diff --git a/src/ModelPublisher.Core/PublishCommand.cs b/src/ModelPublisher.Core/PublishCommand.cs index 4232fa4..7f3e802 100644 --- a/src/ModelPublisher.Core/PublishCommand.cs +++ b/src/ModelPublisher.Core/PublishCommand.cs @@ -91,7 +91,7 @@ public async Task ExecuteAsync( var tasks = runnablePublishers.Select(async x => { await using var context = stealth - ? await BrowserContextFactory.GetStealthContextAsync(playwright, profilePathOverride) + ? await BrowserContextFactory.GetStealthContextAsync(playwright, x.Publisher.PlatformKey, profilePathOverride) : await BrowserContextFactory.GetPersistentContextAsync(playwright, x.Publisher.PlatformKey, profilePathOverride: profilePathOverride); var page = await context.NewPageAsync(); diff --git a/src/ModelPublisher.Core/Shared/BrowserContextFactory.cs b/src/ModelPublisher.Core/Shared/BrowserContextFactory.cs index 7f88aa6..bbe527f 100644 --- a/src/ModelPublisher.Core/Shared/BrowserContextFactory.cs +++ b/src/ModelPublisher.Core/Shared/BrowserContextFactory.cs @@ -116,6 +116,7 @@ await context.AddInitScriptAsync(""" /// public static async Task GetStealthContextAsync( IPlaywright playwright, + string platformKey, string? profilePathOverride = null) { var port = GetFreePort(); @@ -124,21 +125,25 @@ public static async Task GetStealthContextAsync( AppContext.BaseDirectory, "..", "..", "..", "..", "..", "stealth-launcher.js")); var nodeExe = @"C:\Program Files\nodejs\node.exe"; - var arguments = $"\"{launcherPath}\" {port}"; - if (profilePathOverride != null) - arguments += $" \"{profilePathOverride}\""; + // Use the real Brave profile if provided, otherwise fall back to the persistent platform profile + var effectiveProfilePath = profilePathOverride + ?? Path.GetFullPath(Path.Combine("profiles", platformKey)); + Directory.CreateDirectory(Path.GetFullPath(Path.Combine("profiles", platformKey))); var process = new Process { StartInfo = new ProcessStartInfo { FileName = nodeExe, - Arguments = arguments, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, } }; + // Use ArgumentList so .NET handles quoting — avoids newline/space issues with paths + process.StartInfo.ArgumentList.Add(launcherPath); + process.StartInfo.ArgumentList.Add(port.ToString()); + process.StartInfo.ArgumentList.Add(effectiveProfilePath); process.Start(); // Wait for the READY signal from the launcher diff --git a/src/ModelPublisher.Core/Shared/FileUploadHelper.cs b/src/ModelPublisher.Core/Shared/FileUploadHelper.cs index 14443c5..d7a9175 100644 --- a/src/ModelPublisher.Core/Shared/FileUploadHelper.cs +++ b/src/ModelPublisher.Core/Shared/FileUploadHelper.cs @@ -27,7 +27,8 @@ public static async Task UploadSequentialAsync( IPage page, ILocator fileInput, IEnumerable filePaths, - string platformName) + string platformName, + int networkIdleTimeoutMs = 0) { foreach (var path in filePaths) { @@ -35,10 +36,19 @@ public static async Task UploadSequentialAsync( throw new FileNotFoundException($"Upload file not found: {path}"); AnsiConsole.MarkupLine($" [dim]Uploading {Path.GetFileName(path)}...[/] to {platformName}"); - + await fileInput.SetInputFilesAsync(path); - - await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + var loadStateOptions = networkIdleTimeoutMs > 0 + ? new PageWaitForLoadStateOptions { Timeout = networkIdleTimeoutMs } + : null; + + try + { + await page.WaitForLoadStateAsync(LoadState.NetworkIdle, loadStateOptions); + } + catch (TimeoutException) { /* SPA keeps background requests open — best-effort wait */ } + await page.WaitForTimeoutAsync(300); } } diff --git a/stealth-launcher.js b/stealth-launcher.js index bf70ddc..bc3a5ec 100644 --- a/stealth-launcher.js +++ b/stealth-launcher.js @@ -4,7 +4,8 @@ // // Usage: node stealth-launcher.js [profilePath] // profilePath: e.g. "C:\Users\...\Brave-Browser\User Data\Profile 1" -// If it ends in "Profile N" or "Default", splits into --user-data-dir + --profile-directory. +// If it ends in "Profile N" or "Default", splits into userDataDir + --profile-directory. +// Otherwise used directly as userDataDir (e.g. our own profiles/makerworld dir). const { chromium } = require('playwright-extra'); const StealthPlugin = require('puppeteer-extra-plugin-stealth'); @@ -16,36 +17,43 @@ const cdpPort = parseInt(process.argv[2] || '9222', 10); const profilePath = process.argv[3] || null; (async () => { - const args = [ + const commonArgs = [ `--remote-debugging-port=${cdpPort}`, '--disable-blink-features=AutomationControlled', '--no-first-run', '--no-default-browser-check', ]; + const launchOptions = { + headless: false, + executablePath: 'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe', + args: commonArgs, + }; + + let context; + if (profilePath) { const profileName = path.basename(profilePath); const isNamedProfile = /^Profile \d+$/i.test(profileName) || profileName === 'Default'; + const userDataDir = isNamedProfile ? path.dirname(profilePath) : profilePath; + if (isNamedProfile) { - args.push(`--user-data-dir=${path.dirname(profilePath)}`); - args.push(`--profile-directory=${profileName}`); - } else { - args.push(`--user-data-dir=${profilePath}`); + launchOptions.args = [...commonArgs, `--profile-directory=${profileName}`]; } - } - const browser = await chromium.launch({ - headless: false, - executablePath: 'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe', - args, - }); + // Must use launchPersistentContext — playwright-extra blocks --user-data-dir as a launch arg + context = await chromium.launchPersistentContext(userDataDir, launchOptions); + } else { + const browser = await chromium.launch(launchOptions); + context = browser.contexts()[0] || await browser.newContext(); + } // Signal ready — .NET already knows the port process.stdout.write(`READY:${cdpPort}\n`); - // Exit when .NET disconnects - browser.on('disconnected', () => process.exit(0)); + // Exit when the context/browser closes + context.on('close', () => process.exit(0)); })().catch(err => { process.stderr.write(`stealth-launcher error: ${err.message}\n`); process.exit(1); -}); \ No newline at end of file +}); From 0b9ca01e535256608ad23958c490d139700c99d7 Mon Sep 17 00:00:00 2001 From: Christof Lauriers Date: Sat, 28 Mar 2026 17:26:49 +0100 Subject: [PATCH 3/4] Add VS Code debug configs and fix MakerWorld model upload timeout - Add .vscode/launch.json and tasks.json with MakerWorld and Printables debug profiles so F5 in VS Code runs with the correct CLI args - Add src/ModelPublisher.Cli/Properties/launchSettings.json for Visual Studio - Pass networkIdleTimeoutMs: 10_000 to both MakerWorld upload calls so the second upload (model files) does not hang waiting for NetworkIdle on a SPA Co-Authored-By: Claude Sonnet 4.6 --- .vscode/launch.json | 31 +++++++++++++++++++ .vscode/tasks.json | 12 +++++++ .../Properties/launchSettings.json | 12 +++++++ .../Platforms/MakerWorldPublisher.cs | 8 ++--- 4 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 src/ModelPublisher.Cli/Properties/launchSettings.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..471d3fc --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,31 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "MakerWorld (stealth)", + "type": "coreclr", + "request": "launch", + "program": "${workspaceFolder}/src/ModelPublisher.Cli/bin/Debug/net10.0/modelpublisher.dll", + "args": [ + "C:\\Users\\chris\\Downloads\\Models\\Star Trek Ships\\manifest.json", + "--platforms", "makerworld", + "--stealth", + "--profile-path", "C:\\Users\\chris\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Profile 1" + ], + "cwd": "${workspaceFolder}", + "preLaunchTask": "build" + }, + { + "name": "Printables", + "type": "coreclr", + "request": "launch", + "program": "${workspaceFolder}/src/ModelPublisher.Cli/bin/Debug/net10.0/modelpublisher.dll", + "args": [ + "C:\\Users\\chris\\Downloads\\Models\\Star Trek Ships\\manifest.json", + "--platforms", "printables" + ], + "cwd": "${workspaceFolder}", + "preLaunchTask": "build" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..ce311b5 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": ["build", "${workspaceFolder}/src/ModelPublisher.Cli/ModelPublisher.Cli.csproj"], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/src/ModelPublisher.Cli/Properties/launchSettings.json b/src/ModelPublisher.Cli/Properties/launchSettings.json new file mode 100644 index 0000000..02aabc2 --- /dev/null +++ b/src/ModelPublisher.Cli/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "MakerWorld (stealth)": { + "commandName": "Project", + "commandLineArgs": "\"C:\\Users\\chris\\Downloads\\Models\\Star Trek Ships\\manifest.json\" --platforms makerworld --stealth --profile-path \"C:\\Users\\chris\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Profile 1\"" + }, + "Printables": { + "commandName": "Project", + "commandLineArgs": "\"C:\\Users\\chris\\Downloads\\Models\\Star Trek Ships\\manifest.json\" --platforms printables" + } + } +} \ No newline at end of file diff --git a/src/ModelPublisher.Core/Platforms/MakerWorldPublisher.cs b/src/ModelPublisher.Core/Platforms/MakerWorldPublisher.cs index 17e9be0..f4d0ab2 100644 --- a/src/ModelPublisher.Core/Platforms/MakerWorldPublisher.cs +++ b/src/ModelPublisher.Core/Platforms/MakerWorldPublisher.cs @@ -71,7 +71,7 @@ await page var profilePath = config.PrintProfiles.Select(manifest.ResolveFilePath).First(); - await FileUploadHelper.UploadSequentialAsync(page, profileInput, [profilePath], PlatformName); + await FileUploadHelper.UploadSequentialAsync(page, profileInput, [profilePath], PlatformName, networkIdleTimeoutMs: 10_000); } else { @@ -97,11 +97,9 @@ await page .Except(pathsToExclude); var modelInput = page.Locator( - "input[type='file'][accept*='.3ds, .amf, .blend, .dwg, .dxf, .f3d, .f3z, .factory, " + - ".fcstd, .iges, .ipt, .obj, .ply, .py, .rsdoc, .scad, .shape, .shapr, .skp, .sldasm, " + - ".sldprt, .slvs, .step, .stl, .stp, .studio3, .zip, .3mf, .stpz, .fcstd']").First; + "input[type='file'][accept='.3ds, .amf, .blend, .dwg, .dxf, .f3d, .f3z, .factory, .fcstd, .iges, .ipt, .obj, .ply, .py, .rsdoc, .scad, .shape, .shapr, .skp, .sldasm, .sldprt, .slvs, .step, .stl, .stp, .studio3, .zip, .3mf, .stpz, .fcstd']").First; - await FileUploadHelper.UploadSequentialAsync(page, modelInput, nonProfileModelFiles, PlatformName); + await FileUploadHelper.UploadSequentialAsync(page, modelInput, nonProfileModelFiles, PlatformName, networkIdleTimeoutMs: 10_000); await page .GetByRole(AriaRole.Button, new() { Name = "Next Step" }) From 26ac9b22017e44aad88293860d760b3a9b1cfe1c Mon Sep 17 00:00:00 2001 From: Christof Lauriers Date: Sat, 28 Mar 2026 17:48:49 +0100 Subject: [PATCH 4/4] Remove IDE debug configs from tracking, add to .gitignore These files contain machine-specific paths and should not be shared. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 4 +++ .vscode/launch.json | 31 ------------------- .vscode/tasks.json | 12 ------- .../Properties/launchSettings.json | 12 ------- 4 files changed, 4 insertions(+), 55 deletions(-) delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/tasks.json delete mode 100644 src/ModelPublisher.Cli/Properties/launchSettings.json diff --git a/.gitignore b/.gitignore index 1026bcc..6c134a3 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ profiles/ *.suo *.sln.docstates +## Local IDE debug configs — contain machine-specific paths +.vscode/ +src/ModelPublisher.Cli/Properties/launchSettings.json + ## dotnet *.nupkg diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 471d3fc..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "MakerWorld (stealth)", - "type": "coreclr", - "request": "launch", - "program": "${workspaceFolder}/src/ModelPublisher.Cli/bin/Debug/net10.0/modelpublisher.dll", - "args": [ - "C:\\Users\\chris\\Downloads\\Models\\Star Trek Ships\\manifest.json", - "--platforms", "makerworld", - "--stealth", - "--profile-path", "C:\\Users\\chris\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Profile 1" - ], - "cwd": "${workspaceFolder}", - "preLaunchTask": "build" - }, - { - "name": "Printables", - "type": "coreclr", - "request": "launch", - "program": "${workspaceFolder}/src/ModelPublisher.Cli/bin/Debug/net10.0/modelpublisher.dll", - "args": [ - "C:\\Users\\chris\\Downloads\\Models\\Star Trek Ships\\manifest.json", - "--platforms", "printables" - ], - "cwd": "${workspaceFolder}", - "preLaunchTask": "build" - } - ] -} diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index ce311b5..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "build", - "command": "dotnet", - "type": "process", - "args": ["build", "${workspaceFolder}/src/ModelPublisher.Cli/ModelPublisher.Cli.csproj"], - "problemMatcher": "$msCompile" - } - ] -} \ No newline at end of file diff --git a/src/ModelPublisher.Cli/Properties/launchSettings.json b/src/ModelPublisher.Cli/Properties/launchSettings.json deleted file mode 100644 index 02aabc2..0000000 --- a/src/ModelPublisher.Cli/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "MakerWorld (stealth)": { - "commandName": "Project", - "commandLineArgs": "\"C:\\Users\\chris\\Downloads\\Models\\Star Trek Ships\\manifest.json\" --platforms makerworld --stealth --profile-path \"C:\\Users\\chris\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Profile 1\"" - }, - "Printables": { - "commandName": "Project", - "commandLineArgs": "\"C:\\Users\\chris\\Downloads\\Models\\Star Trek Ships\\manifest.json\" --platforms printables" - } - } -} \ No newline at end of file