Skip to content

Prune unused AI provider SDKs from CLI bundle#3735

Open
mokagio wants to merge 5 commits into
trunkfrom
flatten-cli-bundle-deps
Open

Prune unused AI provider SDKs from CLI bundle#3735
mokagio wants to merge 5 commits into
trunkfrom
flatten-cli-bundle-deps

Conversation

@mokagio

@mokagio mokagio commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

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

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 PathTooLongException while 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 under pi-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-ai providers, 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:

  • fixes the path limit — 183 over-limit files → 0 (worst path drops to 236 / 260), and
  • reclaims tens of MB from the installer.

A missing provider degrades gracefully through pi-ai's lazy loader, so removing them is safe.

Note: this is an alternative to #3727, opened in parallel rather than stacked because that PR's commits (forge ignore, registry key, nuget.exe swap) are no-ops for this failure — the exception is thrown by Squirrel's legacy Update.exe (Path.LegacyNormalizePath), which the ignore/registry/nuget levers don't reach. See #3727 for that analysis.

Testing Instructions

  • npm test -- --project scripts — unit tests for the prune (removes targets in hoisted + nested locations; keeps Anthropic/OpenAI and unrelated @google/*).
  • Verified end-to-end against a reproduced bundle install: pruneUnusedProviders removed every offender at both the top level and under pi-coding-agent, taking over-260 files from 183 to 0.
  • The Windows build (make:windows-*) should now pass the Squirrel maker; macOS/Linux unaffected (and benefit from the smaller bundle).

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors? (npm run typecheck and eslint pass)

Authored by Claude (Opus 4.8) on behalf of @mokagio with approval.

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>
Copilot AI review requested due to automatic review settings June 9, 2026 00:57
@mokagio mokagio self-assigned this Jun 9, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_modules trees (hoisted and nested).
  • Add Vitest project configuration and unit tests for the prune behavior.
  • Invoke the prune step from Studio’s electron-forge prePackage hook 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.

Comment thread scripts/remove-unused-ai-providers.ts Outdated
Comment on lines +18 to +19
import { readdirSync, rmSync, statSync } from 'fs';
import { join } from 'path';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 11f57ba1 — added an existsSync guard at the top of pruneUnusedProviders so the common missing-node_modules case short-circuits with a cheap stat instead of a thrown-and-caught ENOENT.

Posted by Claude (Opus 4.8) on behalf of @mokagio with approval.

Comment thread scripts/remove-unused-ai-providers.ts Outdated
Comment on lines +84 to +88
for ( const scoped of scopedEntries ) {
if ( scoped.isDirectory() ) {
pruneUnusedProviders( join( packageDir, scoped.name, 'node_modules' ) );
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 11f57ba1 — added an existsSync guard at the top of pruneUnusedProviders so the common missing-node_modules case short-circuits with a cheap stat instead of a thrown-and-caught ENOENT.

Posted by Claude (Opus 4.8) on behalf of @mokagio with approval.

Comment thread scripts/remove-unused-ai-providers.ts Outdated
Comment on lines +91 to +92
pruneUnusedProviders( join( packageDir, 'node_modules' ) );
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 11f57ba1 — added an existsSync guard at the top of pruneUnusedProviders so the common missing-node_modules case short-circuits with a cheap stat instead of a thrown-and-caught ENOENT.

Posted by Claude (Opus 4.8) on behalf of @mokagio with approval.

Comment thread scripts/remove-unused-ai-providers.ts Outdated
Comment on lines +42 to +53
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 )
}`
);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 22b72d81pruneUnusedProviders 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.

@wpmobilebot

wpmobilebot commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

📊 Performance Test Results

Comparing 7498caf vs trunk

app-size

Metric trunk 7498caf Diff Change
App Size (Mac) 1384.36 MB 1358.07 MB 26.29 MB 🟢 -1.9%

site-editor

Metric trunk 7498caf Diff Change
load 1642 ms 1623 ms 19 ms ⚪ 0.0%

site-startup

Metric trunk 7498caf Diff Change
siteCreation 9046 ms 9050 ms +4 ms ⚪ 0.0%
siteStartup 4419 ms 4415 ms 4 ms ⚪ 0.0%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff)

@mokagio

mokagio commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

Validated on Windows CI. Triggered the dev Windows build (Buildkite #17206) and both packaging jobs passed:

  • 🔨 Windows Dev Build — x64: passed
  • 🔨 Windows Dev Build — arm64: passed

The x64 job cleared the exact Making a squirrel distributable step that crashed #3727, with no PathTooLongException — the prune removed the over-long provider trees before Squirrel walked the directory. All standard checks (Lint, Unit, E2E, Performance, CodeQL) are green too.


Posted by Claude (Opus 4.8) on behalf of @mokagio with approval.

@mokagio mokagio requested a review from wojtekn June 9, 2026 01:42
mokagio and others added 2 commits June 9, 2026 11:44
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>
@wojtekn

wojtekn commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

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 fredrikekelund left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this test. Unit tests for a build script just doesn't make sense to me

Comment thread scripts/remove-unused-ai-providers.ts Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread scripts/vitest.config.ts Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per my other comment, let's remove this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants