@@ -231,7 +231,47 @@ final class PluginManager {
231231
232232 // MARK: - Install / Uninstall
233233
234- func installPlugin( from zipURL: URL ) async throws -> PluginEntry {
234+ func installPlugin( from url: URL ) async throws -> PluginEntry {
235+ if url. pathExtension == " tableplugin " {
236+ return try await installBundle ( from: url)
237+ } else {
238+ return try await installFromZip ( from: url)
239+ }
240+ }
241+
242+ private func installBundle( from url: URL ) async throws -> PluginEntry {
243+ guard let sourceBundle = Bundle ( url: url) else {
244+ throw PluginError . invalidBundle ( " Cannot create bundle from \( url. lastPathComponent) " )
245+ }
246+
247+ try verifyCodeSignature ( bundle: sourceBundle)
248+
249+ let newBundleId = sourceBundle. bundleIdentifier ?? url. lastPathComponent
250+ if let existing = plugins. first ( where: { $0. id == newBundleId } ) , existing. source == . builtIn {
251+ throw PluginError . pluginConflict ( existingName: existing. name)
252+ }
253+
254+ if let existingIndex = plugins. firstIndex ( where: { $0. id == newBundleId } ) {
255+ unregisterCapabilities ( pluginId: newBundleId)
256+ plugins [ existingIndex] . bundle. unload ( )
257+ plugins. remove ( at: existingIndex)
258+ }
259+
260+ let fm = FileManager . default
261+ let destURL = userPluginsDir. appendingPathComponent ( url. lastPathComponent)
262+
263+ if fm. fileExists ( atPath: destURL. path) {
264+ try fm. removeItem ( at: destURL)
265+ }
266+ try fm. copyItem ( at: url, to: destURL)
267+
268+ let entry = try loadPlugin ( at: destURL, source: . userInstalled)
269+
270+ Self . logger. info ( " Installed plugin ' \( entry. name) ' v \( entry. version) " )
271+ return entry
272+ }
273+
274+ private func installFromZip( from url: URL ) async throws -> PluginEntry {
235275 let fm = FileManager . default
236276 let tempDir = fm. temporaryDirectory. appendingPathComponent ( UUID ( ) . uuidString, isDirectory: true )
237277
@@ -243,7 +283,7 @@ final class PluginManager {
243283
244284 let process = Process ( )
245285 process. executableURL = URL ( fileURLWithPath: " /usr/bin/ditto " )
246- process. arguments = [ " -xk " , zipURL . path, tempDir. path]
286+ process. arguments = [ " -xk " , url . path, tempDir. path]
247287
248288 try await withCheckedThrowingContinuation { ( continuation: CheckedContinuation < Void , Error > ) in
249289 process. terminationHandler = { proc in
0 commit comments