Skip to content

CmdPal JS/TS Extensions - Phase 5: gallery npm install/uninstall - #49326

Open
michaeljolley wants to merge 17 commits into
dev/mjolley/phase-4-cmdpal-servicefrom
dev/mjolley/phase-5-cmdpal-gallery
Open

CmdPal JS/TS Extensions - Phase 5: gallery npm install/uninstall#49326
michaeljolley wants to merge 17 commits into
dev/mjolley/phase-4-cmdpal-servicefrom
dev/mjolley/phase-5-cmdpal-gallery

Conversation

@michaeljolley

@michaeljolley michaeljolley commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Warning

This PR is one in a series of PRs focused on adding TypeScript/JavaScript extension support to Command Palette.

This PR should not be merged until PR #49329 is merged into it.

Note

To test the final result, run the branch associated with PR #49364.

Summary

Phase 5 of the CmdPal JS/TS extensions effort (tracking issue #48707). This adds gallery install/uninstall for JavaScript/TypeScript (jsonrpc) extensions.

Stacks on Phase 4 (PR #49325). Base branch is dev/mjolley/phase-4-cmdpal-service, not main. Stays a draft until reviewed.

What this does

  • Recognizes gallery entries whose install source has type: jsonrpc with a nested npm block ({ package, registry }), distinct from the existing COM/WinGet/Store/url sources.
  • Install: runs npm install <package> into JSExtensions/<name>/ (the shared Phase 4 path, exposed via IJsExtensionHost.ExtensionsRootPath). Optional registry is honored. Progress/errors surface in the gallery item UI, consistent with the existing install UX.
  • Uninstall: terminates the extension's Node.js process (IJsExtensionHost.StopExtension, backed by the Phase 4 service) then deletes JSExtensions/<name>/. The Phase 4 FileSystemWatcher handles the actual load/unload; this layer does not mutate provider lists directly.
  • Guards against npm not being installed with a clear, user-visible message instead of an unhandled exception.

Design

  • INpmCommandRunner / NpmCommandRunner isolate the real npm process and directory side effects.
  • IJsExtensionInstaller / NpmJsExtensionInstaller orchestrate install/uninstall, with a path-traversal guard that keeps the target directory inside the JSExtensions root.
  • Both are mockable seams, so unit tests never run npm or touch disk.

Tests

  • ExtensionGalleryItemViewModelTests: jsonrpc entry classification (HasJsonRpcSource, JsonRpcPackageId, JsonRpcRegistry, source appears in Sources) and install/uninstall command behavior via a mocked IJsExtensionInstaller.
  • NpmJsExtensionInstallerTests: target-directory resolution (root + name), npm-not-found guard, cleanup on failure, path-traversal rejection, and stop-before-delete sequencing on uninstall.
  • Build exits 0; all CmdPal *.UnitTests pass.

Known caveat

npm install <package> places the package under JSExtensions/<name>/node_modules/<package>/ rather than at JSExtensions/<name>/package.json. Phase 4 discovery scans immediate subdirectories for a package.json with a cmdpal section, so the discovery step may need a follow-up refinement. The gallery-side flow and abstractions here are correct; this is called out for reviewer awareness.

Scope

Atomic to Phase 5 (install + uninstall only). Marketplace/auto-update remain out of scope.

Recognize gallery entries of type jsonrpc (npm package + optional registry)
and install/uninstall them into the Phase 4 JSExtensions directory so the
FileSystemWatcher loads/unloads them.

- Add GalleryNpmPackage model and Npm block on GalleryInstallSource.
- Expose the shared JSExtensions root and process termination from
  JsonRpcExtensionService via IJsExtensionHost.
- Add IJsExtensionInstaller/NpmJsExtensionInstaller and
  INpmCommandRunner/NpmCommandRunner as mockable seams; guard against npm
  not being installed with a user-visible error.
- Wire jsonrpc install/uninstall commands, state, and UI into the gallery
  item view model and page.
- Add unit tests for jsonrpc entry classification, install/uninstall
  command behavior, and installer path resolution plus stop-before-delete
  sequencing.

Co-authored-by: Copilot App <[email protected]>
Copilot-Session: 1fac744e-2d8a-4f58-9044-6a1565b19b37
michaeljolley and others added 5 commits July 15, 2026 11:56
…ce' into dev/mjolley/phase-5-cmdpal-gallery

# Conflicts:
#	src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/JsonRpcExtensionService.cs
Fixes three gallery install/uninstall review notes:

- Uninstall now surfaces directory-deletion failure. RemoveDirectory
  returns a bool and UninstallAsync reports failure (instead of Ok)
  when the extension files could not be removed, so the UI no longer
  marks an extension uninstalled while it remains on disk.
- Install now verifies the extension is discoverable before reporting
  success. After npm exits, IsExtensionDiscoverable checks for a
  loadable manifest at the directory root; if absent, the directory is
  cleaned up and a clear error is returned. This keeps success honest
  and composes with the Phase 6 layout materialization.
- npm install now has a five minute timeout. A linked cancellation
  source bounds WaitForExitAsync, kills the process tree on timeout,
  and returns a clear error, so the gallery cannot stay on Installing
  indefinitely. External cancellation is still distinguished and
  rethrown.

Co-authored-by: Copilot App <[email protected]>
Copilot-Session: 1fac744e-2d8a-4f58-9044-6a1565b19b37
Align the hardcoded expected install root in the npm installer tests with
the CmdPal data folder path segment change from CommandPalette to CmdPal.

Co-authored-by: Copilot App <[email protected]>
Copilot-Session: f69b1dcf-ed86-4e74-8373-b8414e1ac961
michaeljolley and others added 11 commits July 21, 2026 13:26
Co-authored-by: Copilot App <[email protected]>
Copilot-Session: 762976e0-2453-40ca-8034-978a04a19d1e
Co-authored-by: Copilot App <[email protected]>
Copilot-Session: 3f0875f9-177f-40db-888c-248a044d2bb9
Co-authored-by: Copilot App <[email protected]>

Copilot-Session: 9470f549-1997-44de-b0a6-1ce4562a51f0
…-gallery

# Conflicts:
#	src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/JsonRpcExtensionService.cs
…hase 5)

Rework the Command Palette JS extension gallery install/uninstall flow into a
transaction over an immutable, approved artifact per the C5 contract.

Artifact identity and validation (NpmArtifact):
- Require package (npm name grammar), exact version (no ranges or dist-tags),
  sha512 Subresource-Integrity, and an optional absolute HTTPS registry on an
  approved allowlist. Incomplete catalog entries are not installable.
- Install spec is always name@exactversion, passed via ArgumentList, and can
  never be read as a flag, URL, path, git, or tarball source.

Runner (NpmCommandRunner):
- npm always runs with --ignore-scripts --save-exact --no-audit --no-fund
  --loglevel=error so package lifecycle scripts never run.
- Read the resolved integrity from the lockfile for post-install verification.
- RemoveDirectory refuses reparse points and retries with a bounded backoff.

Installer (NpmJsExtensionInstaller):
- Stage into a unique dir outside the watched root, verify integrity, identity,
  and version, assemble the discovery layout, then atomically promote with
  Directory.Move and await host provider registration before reporting success.
- Block reinstalling into an existing or active directory (upgrade policy).
- Serialize install and uninstall of the same canonical directory; allow
  different directories to run in parallel.
- Uninstall stops the extension, then deletes with reparse and canonical
  containment checks. Staging is always cleaned up in a finally block.

Host (IJsExtensionHost, JsonRpcExtensionService):
- Add IsExtensionInstalled and RefreshAndAwaitProviderAsync so installed state
  is derived from a loadable manifest in the host, not the catalog.

View model and UI:
- Seed IsInstalled from the host, pass version, integrity, and a cancellation
  token, and expose a cancel affordance mirroring the WinGet flow.
- All user-facing install and uninstall messages come from Resources.resx.

Add tests for the artifact validation matrix, argument construction, reparse
refusal, scoped and unscoped layouts, delayed discovery, integrity and version
verification, registry rejection, lifecycle-script disabling, rollback on
timeout and cancel, existing-install preservation, and concurrency.

Co-authored-by: Copilot App <[email protected]>
Copilot-Session: 234c59f1-a32b-4c98-97bb-ab50a4d07fd9
Propagates the stale-test and golden-fixture corrections so the SDK
typecheck stays green at this layer.

Co-authored-by: Copilot App <[email protected]>
Copilot-Session: 311db8aa-f8db-47bb-943f-5447037946c7
Address the blocking command-injection defect and related install/uninstall
robustness issues in the Command Palette JS extension gallery installer:

r2-p5-01 (BLOCK): canonicalize the npm registry to a strict https origin
(approved host, no userinfo/port/query/fragment/path) and invoke npm through
node.exe with npm-cli.js instead of the npm.cmd batch shim, so untrusted
arguments never reach cmd.exe.
r2-p5-02: gate installs on a lockfile-integrity check that requires every
resolved dependency to be registry-sourced https with an SRI hash, failing
closed otherwise.
r2-p5-03: on canceled or failed installs, stop the host provider before
removing the promoted directory on every rollback path.
r2-p5-04: refuse uninstall when the extensions root resolves through a
reparse point outside its expected location.
r2-p5-05: report a package as installed when present on disk even if the
provider failed to load, so corrupt installs can be uninstalled.
r2-p5-06: thread the uninstall cancellation token through the stop and delete
steps.

Co-authored-by: Copilot App <[email protected]>
Copilot-Session: 311db8aa-f8db-47bb-943f-5447037946c7
Bring the phase-4 service round-2 fixes down onto phase-5. Resolved JsonRpcExtensionService.cs as a union: kept the phase-5 installer members and the phase-4 RegistrationOutcome enum plus its register-block usage.

Co-authored-by: Copilot App <[email protected]>
Copilot-Session: 311db8aa-f8db-47bb-943f-5447037946c7
@michaeljolley
michaeljolley force-pushed the dev/mjolley/phase-4-cmdpal-service branch from ca60ab1 to ca9c628 Compare July 27, 2026 21:14
@michaeljolley
michaeljolley force-pushed the dev/mjolley/phase-5-cmdpal-gallery branch from 8d8c090 to a20c022 Compare July 27, 2026 21:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Product-Command Palette Refers to the Command Palette utility

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants