From f2932b4ba41aacc81ded7d319b0e0ce2ffa1fa49 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 21 Jun 2026 13:28:10 +0300 Subject: [PATCH] feat(extensions): implement LocalExtensionRegistry (EXT-2) - Add ExtensionPaths for locating ~/.querya/extensions - Add LocalExtensionRegistry for parsing manifest.json from extensions dir - Add installPath to ExtensionManifest --- lib/core/extensions/extension_paths.dart | 29 +++++++++ .../extensions/local_extension_registry.dart | 61 +++++++++++++++++++ .../extensions/models/extension_manifest.dart | 5 +- 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 lib/core/extensions/extension_paths.dart create mode 100644 lib/core/extensions/local_extension_registry.dart diff --git a/lib/core/extensions/extension_paths.dart b/lib/core/extensions/extension_paths.dart new file mode 100644 index 00000000..f05414f9 --- /dev/null +++ b/lib/core/extensions/extension_paths.dart @@ -0,0 +1,29 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +/// Centralizes extension file locations. +abstract final class ExtensionPaths { + static const _extensionsSegment = 'extensions'; + + /// Returns `~/.querya/extensions` on Linux/Mac, or equivalent `USERPROFILE\.querya\extensions` on Windows. + /// Falls back to application support directory if HOME is unavailable. + static Future extensionsDirectory() async { + final home = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; + if (home == null || home.isEmpty) { + final support = await getApplicationSupportDirectory(); + return Directory(p.join(support.path, _extensionsSegment)); + } + return Directory(p.join(home, '.querya', _extensionsSegment)); + } + + /// Creates the extensions directory if it doesn't exist. + static Future ensureExtensionsDirectory() async { + final dir = await extensionsDirectory(); + if (!await dir.exists()) { + await dir.create(recursive: true); + } + return dir; + } +} diff --git a/lib/core/extensions/local_extension_registry.dart b/lib/core/extensions/local_extension_registry.dart new file mode 100644 index 00000000..ddf17203 --- /dev/null +++ b/lib/core/extensions/local_extension_registry.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as p; + +import 'extension_paths.dart'; +import 'models/extension_manifest.dart'; + +/// Scans the local filesystem for extensions and loads their manifests. +class LocalExtensionRegistry { + LocalExtensionRegistry._(); + static final LocalExtensionRegistry instance = LocalExtensionRegistry._(); + + List _manifests = []; + bool _loaded = false; + + /// Returns an unmodifiable list of loaded manifests. + List get manifests => List.unmodifiable(_manifests); + + /// Reloads manifests from the disk. + Future reload() async { + _loaded = false; + await load(); + } + + /// Loads manifests from the extensions directory if not already loaded. + Future> load() async { + if (_loaded) return manifests; + + final dir = await ExtensionPaths.extensionsDirectory(); + final loadedManifests = []; + + if (await dir.exists()) { + // Use list() rather than listSync() to prevent blocking the UI + final entities = await dir.list().toList(); + for (final entity in entities) { + if (entity is Directory) { + final manifestFile = File(p.join(entity.path, 'manifest.json')); + if (await manifestFile.exists()) { + try { + final content = await manifestFile.readAsString(); + final json = jsonDecode(content) as Map; + final manifest = ExtensionManifest.fromJson( + json, + installPath: entity.path, + ); + loadedManifests.add(manifest); + } catch (e) { + // Log or ignore invalid manifests + // In the future, we could report these to an error logging service + } + } + } + } + } + + _manifests = loadedManifests; + _loaded = true; + return manifests; + } +} diff --git a/lib/core/extensions/models/extension_manifest.dart b/lib/core/extensions/models/extension_manifest.dart index dc15d7e5..cafa2e04 100644 --- a/lib/core/extensions/models/extension_manifest.dart +++ b/lib/core/extensions/models/extension_manifest.dart @@ -10,6 +10,7 @@ class ExtensionManifest { final String? main; final String? icon; final String? description; + final String? installPath; const ExtensionManifest({ required this.id, @@ -21,9 +22,10 @@ class ExtensionManifest { this.main, this.icon, this.description, + this.installPath, }); - factory ExtensionManifest.fromJson(Map json) { + factory ExtensionManifest.fromJson(Map json, {String? installPath}) { return ExtensionManifest( id: json['id'] as String, name: json['name'] as String, @@ -34,6 +36,7 @@ class ExtensionManifest { main: json['main'] as String?, icon: json['icon'] as String?, description: json['description'] as String?, + installPath: installPath, ); }