Skip to main content

Mountain/IPC/WindServiceHandlers/Extension/
ExtensionInstall.rs

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