Prune unused AI provider SDKs from CLI bundle#3735
Conversation
The Windows Squirrel maker crashes with `PathTooLongException` while packing the app: `@mistralai/mistralai`'s ~200-char auto-generated filenames, nested under `pi-coding-agent/node_modules`, push paths past Windows' 260-char limit. The AWS Bedrock SDK (`@aws-sdk`/`@smithy`) overflows for the same reason. These are lazy, dynamically-imported `pi-ai` providers, and Studio only ever exposes the Anthropic and OpenAI families (`tools/common/ai/models.ts`), so Mistral, Bedrock and Google are unreachable dead weight. Strip them from the bundled CLI during packaging — fixing the path limit (183 over-limit files to 0, worst path 236) and reclaiming tens of MB. A missing provider degrades gracefully through pi-ai's lazy loader. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR reduces the bundled CLI’s node_modules footprint during Studio packaging by pruning unused, lazily-loaded AI provider SDKs (Mistral, AWS Bedrock, Google) that are unreachable from Studio’s model registry, addressing Windows Squirrel PathTooLongException failures and shrinking installer size.
Changes:
- Add a packaging-time prune utility to remove specific unused provider SDK directories from all discovered
node_modulestrees (hoisted and nested). - Add Vitest project configuration and unit tests for the prune behavior.
- Invoke the prune step from Studio’s
electron-forgeprePackagehook after building the bundled CLI.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
vitest.config.ts |
Registers the new scripts Vitest project at the root level. |
scripts/vitest.config.ts |
Defines the scripts Vitest project configuration for running script unit tests. |
scripts/remove-unused-ai-providers.ts |
Implements recursive pruning of unused AI provider SDK directories from bundled CLI node_modules. |
scripts/remove-unused-ai-providers.test.ts |
Adds unit tests covering hoisted and nested pruning, and ensuring Anthropic/OpenAI + unrelated @google/* are preserved. |
apps/studio/forge.config.ts |
Calls the prune utility during prePackage to prevent Windows path-length issues in Squirrel packaging. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { readdirSync, rmSync, statSync } from 'fs'; | ||
| import { join } from 'path'; |
There was a problem hiding this comment.
| for ( const scoped of scopedEntries ) { | ||
| if ( scoped.isDirectory() ) { | ||
| pruneUnusedProviders( join( packageDir, scoped.name, 'node_modules' ) ); | ||
| } | ||
| } |
There was a problem hiding this comment.
| pruneUnusedProviders( join( packageDir, 'node_modules' ) ); | ||
| } |
There was a problem hiding this comment.
| try { | ||
| rmSync( dir, { recursive: true, force: true } ); | ||
| console.log( `Removed ${ target } from ${ nodeModulesDir }` ); | ||
| } catch ( e ) { | ||
| // Tolerate transient failures (Windows AV locks, permissions). This is a size / | ||
| // path-length optimization; aborting packaging over a stuck file would be worse. | ||
| console.warn( | ||
| `Could not remove ${ target } from ${ nodeModulesDir }: ${ | ||
| e instanceof Error ? e.message : String( e ) | ||
| }` | ||
| ); | ||
| } |
There was a problem hiding this comment.
Addressed in 22b72d81 — pruneUnusedProviders now returns the directories it couldn't remove, and the Forge hook throws on win32 when any remain (with the offending paths), so a failed prune fails the build with context instead of degrading into a later PathTooLongException. Non-Windows platforms keep tolerating a stuck removal, where the prune is only a size optimization.
Posted by Claude (Opus 4.8) on behalf of @mokagio with approval.
📊 Performance Test ResultsComparing 7498caf vs trunk app-size
site-editor
site-startup
Results are median values from multiple test runs. Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff) |
|
✅ Validated on Windows CI. Triggered the dev Windows build (Buildkite #17206) and both packaging jobs passed:
The x64 job cleared the exact Posted by Claude (Opus 4.8) on behalf of @mokagio with approval. |
Most packages have no nested `node_modules`, so the recursive walk bottomed out by letting `readdirSync` throw ENOENT and catching it — an expensive exception per miss across a large tree. Check existence up front so the common case is a cheap stat. Addresses Copilot review on #3735. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`pruneUnusedProviders` now returns the directories it couldn't remove, and the Forge hook throws on `win32` when any remain. A leftover provider tree there resurfaces as the very `PathTooLongException` the prune prevents, so surfacing the offending paths up front beats a later opaque Squirrel crash. Other platforms still tolerate a stuck removal — it's only a size optimization there. Addresses Copilot review on #3735. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for exploring that @mokagio . I tried different approaches in #3727 and #3732, but none solved the issue. Claude was proposing the pruning way, but I kept trying to find another way. I think it's reasonable to proceed with that approach, and in the future we could consider dropping such auto-generated dependencies, and/or migrating away from Squirrel - either by using another existing or custom maker for Forge, or better, migrating from Forge to Electron Builder. |
fredrikekelund
left a comment
There was a problem hiding this comment.
I haven't tested this, but fundamentally, the strategy seems sound to me. I think we can compress the actual implementation a bit, though (see my inline comments)
There was a problem hiding this comment.
Let's remove this test. Unit tests for a build script just doesn't make sense to me
There was a problem hiding this comment.
I would inline this logic in forge.config.ts and simplify it by following the apps/cli/vite.config.prod.ts example and relying on globSync to collect the paths we want to delete, then passing those to fs.rmSync
There was a problem hiding this comment.
Per my other comment, let's remove this
Note
@wojtekn this JS/TS manipulation of the Electron build structure is outside my area of expertise, so it's possible the result is a big AI slop. At the same time, it does seem to address the build failure issue with the long path, so it might be a good place to start.
I'm having my agent address the Copilot comments now. Next step will be to get a different one to look over the work for possible improvements.
Related issues
PathTooLongException)How AI was used in this PR
Claude Code (Opus 4.8) diagnosed the root cause from the CI logs, verified the fix empirically against a reproduced bundle tree, and wrote the prune script and tests. I reviewed the analysis and the diff.
Proposed Changes
The Windows Squirrel maker crashes with
PathTooLongExceptionwhile packing the app. The real cause is@earendil-works/pi-coding-agent's transitive@mistralai/mistralai— a speakeasy-generated SDK whose ~200-char filenames, nested underpi-coding-agent/node_modules, exceed Windows' 260-char path limit. The AWS Bedrock SDK (@aws-sdk/@smithy) overflows the same way.These are lazy, dynamically-imported
pi-aiproviders, and Studio only ever exposes the Anthropic and OpenAI model families (tools/common/ai/models.ts). Mistral, Bedrock and Google are therefore unreachable dead weight. This strips them from the bundled CLI during packaging, which:A missing provider degrades gracefully through pi-ai's lazy loader, so removing them is safe.
Testing Instructions
npm test -- --project scripts— unit tests for the prune (removes targets in hoisted + nested locations; keeps Anthropic/OpenAI and unrelated@google/*).pruneUnusedProvidersremoved every offender at both the top level and underpi-coding-agent, taking over-260 files from 183 to 0.make:windows-*) should now pass the Squirrel maker; macOS/Linux unaffected (and benefit from the smaller bundle).Pre-merge Checklist
npm run typecheckandeslintpass)Authored by Claude (Opus 4.8) on behalf of @mokagio with approval.