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
40	Runtime:Arc<ApplicationRunTime>,
41
42	Args:Vec<Value>,
43) -> Result<Value, String> {
44	let OTELStart = crate::IPC::DevLog::NowNano::Fn();
45
46	let VsixPath = match VsixPathFromArgs(&Args) {
47		Some(Path) => Path,
48
49		None => {
50			dev_log!("extensions", "extensions:install no-op: Arguments[0] missing or non-file URI");
51
52			crate::otel_span!("extensions:install:noop-missing-arg", OTELStart);
53
54			return Ok(Value::Null);
55		},
56	};
57
58	if VsixPath.extension().and_then(|Value| Value.to_str()) != Some("vsix") {
59		dev_log!("extensions", "extensions:install no-op: {} is not a .vsix", VsixPath.display());
60
61		crate::otel_span!("extensions:install:noop-not-vsix", OTELStart);
62
63		return Ok(Value::Null);
64	}
65
66	let InstallRoot = UserExtensionDirectory();
67
68	let Outcome = tokio::task::spawn_blocking(move || VsixInstaller::InstallVsix(&VsixPath, &InstallRoot))
69		.await
70		.map_err(|Error| format!("extensions:install join error: {}", Error))?
71		.map_err(|Error| format!("extensions:install failed: {}", Error))?;
72
73	Runtime
74		.Environment
75		.ApplicationState
76		.Extension
77		.ScannedExtensions
78		.AddOrUpdate(Outcome.Identifier.clone(), Outcome.Description.clone());
79
80	let Descriptor = serde_json::to_value(&Outcome.Description).unwrap_or(Value::Null);
81
82	NotifyCocoonDeltaExtensions(vec![Descriptor.clone()], Vec::new());
83
84	if let Err(Error) = ApplicationHandle.emit(
85		"sky://extensions/installed",
86		json!({
87			"identifier": Outcome.Identifier,
88			"version": Outcome.Version,
89			"location": Outcome.InstalledAt.to_string_lossy(),
90		}),
91	) {
92		dev_log!("extensions", "warn: failed to emit sky://extensions/installed: {}", Error);
93	}
94
95	dev_log!(
96		"extensions",
97		"extensions:install succeeded: {} v{} at {}",
98		Outcome.Identifier,
99		Outcome.Version,
100		Outcome.InstalledAt.display()
101	);
102
103	crate::otel_span!(
104		"extensions:install:ok",
105		OTELStart,
106		&[
107			("extension.identifier", Outcome.Identifier.as_str()),
108			("extension.version", Outcome.Version.as_str()),
109		]
110	);
111
112	// ILocalExtension envelope - matches `ExtensionsGetInstalled`
113	// so VS Code's ExtensionEnablementService merges it into the sidebar.
114	// `location` must carry `$mid: 1` so the renderer's `URI.revive()`
115	// runs; otherwise `resources.joinPath(local.location, …)` hits
116	// `uri.with is not a function`. Routed through `UriFromFilePath` so
117	// the marker never drops off.
118	Ok(json!({
119		"type": 1,
120		"isBuiltin": false,
121		"identifier": { "id": Outcome.Identifier },
122		"manifest": Descriptor,
123		"location": UriFromFilePath(Outcome.InstalledAt.to_string_lossy()),
124		"targetPlatform": "undefined",
125		"isValid": true,
126		"validations": [],
127		"preRelease": false,
128		"isWorkspaceScoped": false,
129		"isMachineScoped": false,
130		"isApplicationScoped": false,
131		"publisherId": null,
132		"isPreReleaseVersion": false,
133		"hasPreReleaseVersion": false,
134		"private": false,
135		"updated": false,
136		"pinned": false,
137		"forceAutoUpdate": false,
138		"source": "vsix",
139		"size": 0,
140	}))
141}