Skip to main content

Mountain/IPC/WindServiceHandlers/Extension/
ExtensionInstall.rs

1#![allow(non_snake_case)]
2//! `extensions:install` IPC handler - local VSIX only. Gallery installs are
3//! declined (Land has no marketplace backend) and return `null`.
4//!
5//! Sequence:
6//!   1. Resolve the VSIX path from `Arguments[0]` (string or UriComponents).
7//!   2. Reject non-`.vsix` files.
8//!   3. Unpack into the user-scope extension directory via
9//!      `VsixInstaller::InstallVsix`.
10//!   4. Register with `ScannedExtensions` so `GetExtensions()` reflects the
11//!      install on the next read.
12//!   5. Fire-and-forget `$deltaExtensions` + `$activateByEvent` to Cocoon so
13//!      the extension activates without a workbench reload.
14//!   6. Emit `sky://extensions/installed` so Wind refreshes the sidebar.
15//!   7. Return an `ILocalExtension` envelope shaped for VS Code's
16//!      ExtensionEnablementService sidebar merge path.
17
18use std::sync::Arc;
19
20use serde_json::{Value, json};
21use tauri::{AppHandle, Emitter};
22
23use crate::{
24	ExtensionManagement::VsixInstaller,
25	IPC::{
26		UriComponents::FromFilePath::Fn as UriFromFilePath,
27		WindServiceHandlers::Extension::{
28			NotifyCocoonDeltaExtensions::NotifyCocoonDeltaExtensions,
29			UserExtensionDirectory::UserExtensionDirectory,
30			VsixPathFromArgs::VsixPathFromArgs,
31		},
32	},
33	RunTime::ApplicationRunTime::ApplicationRunTime,
34	dev_log,
35};
36
37pub async fn ExtensionInstall(
38	ApplicationHandle:AppHandle,
39	Runtime:Arc<ApplicationRunTime>,
40	Args:Vec<Value>,
41) -> Result<Value, String> {
42	let OTELStart = crate::IPC::DevLog::NowNano::Fn();
43
44	let VsixPath = match VsixPathFromArgs(&Args) {
45		Some(Path) => Path,
46		None => {
47			dev_log!("extensions", "extensions:install no-op: Arguments[0] missing or non-file URI");
48			crate::otel_span!("extensions:install:noop-missing-arg", OTELStart);
49			return Ok(Value::Null);
50		},
51	};
52
53	if VsixPath.extension().and_then(|Value| Value.to_str()) != Some("vsix") {
54		dev_log!("extensions", "extensions:install no-op: {} is not a .vsix", VsixPath.display());
55		crate::otel_span!("extensions:install:noop-not-vsix", OTELStart);
56		return Ok(Value::Null);
57	}
58
59	let InstallRoot = UserExtensionDirectory();
60
61	let Outcome = tokio::task::spawn_blocking(move || VsixInstaller::InstallVsix(&VsixPath, &InstallRoot))
62		.await
63		.map_err(|Error| format!("extensions:install join error: {}", Error))?
64		.map_err(|Error| format!("extensions:install failed: {}", Error))?;
65
66	Runtime
67		.Environment
68		.ApplicationState
69		.Extension
70		.ScannedExtensions
71		.AddOrUpdate(Outcome.Identifier.clone(), Outcome.Description.clone());
72
73	let Descriptor = serde_json::to_value(&Outcome.Description).unwrap_or(Value::Null);
74
75	NotifyCocoonDeltaExtensions(vec![Descriptor.clone()], Vec::new());
76
77	if let Err(Error) = ApplicationHandle.emit(
78		"sky://extensions/installed",
79		json!({
80			"identifier": Outcome.Identifier,
81			"version": Outcome.Version,
82			"location": Outcome.InstalledAt.to_string_lossy(),
83		}),
84	) {
85		dev_log!("extensions", "warn: failed to emit sky://extensions/installed: {}", Error);
86	}
87
88	dev_log!(
89		"extensions",
90		"extensions:install succeeded: {} v{} at {}",
91		Outcome.Identifier,
92		Outcome.Version,
93		Outcome.InstalledAt.display()
94	);
95
96	crate::otel_span!(
97		"extensions:install:ok",
98		OTELStart,
99		&[
100			("extension.identifier", Outcome.Identifier.as_str()),
101			("extension.version", Outcome.Version.as_str()),
102		]
103	);
104
105	// ILocalExtension envelope - matches `ExtensionsGetInstalled`
106	// so VS Code's ExtensionEnablementService merges it into the sidebar.
107	// `location` must carry `$mid: 1` so the renderer's `URI.revive()`
108	// runs; otherwise `resources.joinPath(local.location, …)` hits
109	// `uri.with is not a function`. Routed through `UriFromFilePath` so
110	// the marker never drops off.
111	Ok(json!({
112		"type": 1,
113		"isBuiltin": false,
114		"identifier": { "id": Outcome.Identifier },
115		"manifest": Descriptor,
116		"location": UriFromFilePath(Outcome.InstalledAt.to_string_lossy()),
117		"targetPlatform": "undefined",
118		"isValid": true,
119		"validations": [],
120		"preRelease": false,
121		"isWorkspaceScoped": false,
122		"isMachineScoped": false,
123		"isApplicationScoped": false,
124		"publisherId": null,
125		"isPreReleaseVersion": false,
126		"hasPreReleaseVersion": false,
127		"private": false,
128		"updated": false,
129		"pinned": false,
130		"forceAutoUpdate": false,
131		"source": "vsix",
132		"size": 0,
133	}))
134}