Skip to content

Instantly share code, notes, and snippets.

@marad
Last active April 13, 2025 16:37
Show Gist options
  • Save marad/08d0c156afb35e9464aa01553d166224 to your computer and use it in GitHub Desktop.
Save marad/08d0c156afb35e9464aa01553d166224 to your computer and use it in GitHub Desktop.

Plugin Dependency System

The PDS for SilverBullet provides a structured approach to managing dependencies between plugins and controlling their load order. The architecture is based on a central module that coordinates the entire process through SilverBullet's event system.

Core Concepts

Centralized Dependency Management

The central PDS library manages the plugin registration process, dependency resolution, and initialization in the proper order. This prevents issues with random loading order that could occur with the standard SilverBullet script loading mechanism.

Event-Based System

The system utilizes SilverBullet's event mechanism for plugin discovery and communication. The main process flows as follows:

  1. The system core emits a pds:discoverPlugins event
  2. Plugins respond with their manifests and initialization functions
  3. The core analyzes dependencies and determines the initialization order
  4. The core calls the plugin initialization functions in the correct order

Plugin Registration

Each plugin must register with the system by responding to the pds:discoverPlugins event. The response should include plugin id, manifest and initialiation function:

event.listen {
  name = "pds:discoverPlugins",
  run = function()
    return {
      id = "my-plugin",         -- Unique plugin identifier
      manifest = {
        name = "My Plugin",     -- Display name
        version = "1.0.0",      -- Version in SemVer format
        dependencies = {        -- Required dependencies
          { id = "core", minVersion = "1.0.0" },
          { id = "utils", minVersion = "0.5.0", optional = true }
        }
      },
      initialize = function(dependencies)
        -- Plugin initialization code
        -- `dependencies` contains initialized dependencies
        
        -- Returns initialization status and optional exports
        return {
          success = true,
          exports = {
            -- Functions/objects exported for other plugins
            someFunction = function() ... end,
            someData = { ... }
          }
        }
      end
    }
  end
}

Plugin Manifest

The manifest contains plugin metadata and its dependencies:

Field Type Description
name string Plugin display name
version string Plugin version in SemVer format
description string Optional plugin description
author string Optional author information
dependencies array List of plugin dependencies

Dependency Declaration

Each dependency is defined as an object with the following fields:

Field Type Description
id string Required plugin identifier
minVersion string Minimum required version (optional)
maxVersion string Maximum supported version (optional)
optional boolean Whether the dependency is optional (default: false)

Initialization Function

The initialize function is called by the PDS system after resolving all dependencies. It receives a dependencies object containing all required plugins (by their identifiers).

initialize = function(dependencies)
  -- `dependencies` is a map of id -> exports
  local utils = dependencies["utils"]
  if utils then -- Check optional dependency
    utils.doSomething()
  end
  
  -- Prepare and return exported functionality
  return {
    success = true,
    exports = {
      -- API exported for other plugins
      processData = function(data) ... end
    }
  }
end

Return Value of the Initialization Function

The initialization function should return an object with information about the initialization status and optional exports:

Field Type Description
success boolean Whether initialization succeeded
exports table Object containing exported functions/data
error string Optional error information (when success = false)

System Initialization Process

  1. The PDS core starts after SilverBullet loads (event system:ready)
  2. The core emits the pds:discoverPlugins event
  3. Collects responses (manifests) from all plugins
  4. Builds a dependency graph and sorts it topologically
  5. Verifies that all dependencies are available and version compatible
  6. Initializes plugins in the correct order
  7. Passes exports between dependent plugins
  8. Handles initialization errors and missing dependencies

Usage Examples

Basic Plugin Example

-- Base plugin with no dependencies
event.listen {
  name = "pds:discoverPlugins",
  run = function()
    return {
      id = "math-utils",
      manifest = {
        name = "Math Utilities",
        version = "1.0.0"
      },
      initialize = function()
        -- Implementation of math functions
        local function add(a, b) return a + b end
        local function multiply(a, b) return a * b end
        
        return {
          success = true,
          exports = {
            add = add,
            multiply = multiply
          }
        }
      end
    }
  end
}

Plugin with Dependencies Example

-- Plugin using another plugin
event.listen {
  name = "pds:discoverPlugins",
  run = function()
    return {
      id = "calculator",
      manifest = {
        name = "Calculator",
        version = "1.0.0",
        dependencies = {
          { id = "math-utils", minVersion = "1.0.0" }
        }
      },
      initialize = function(dependencies)
        local mathUtils = dependencies["math-utils"]
        
        local function calculate(expression)
          -- Uses functions from math-utils
          -- ...
          return result
        end
        
        return {
          success = true,
          exports = {
            calculate = calculate
          }
        }
      end
    }
  end
}

Troubleshooting

Circular Dependencies

The system detects cycles in the dependency graph and reports an error. Plugins should be redesigned to avoid circular dependencies.

Missing Dependencies

When a required dependency is not available, the system notifies about this issue. You can mark a dependency as optional if the plugin can function without it.

Version Conflicts

The system checks version compatibility according to SemVer rules. Conflicts are reported during initialization.

Best Practices

  1. Minimal Dependencies - Design plugins with the minimum necessary dependencies
  2. Optional Dependencies - Mark dependencies as optional when possible
  3. Proper Versioning - Follow SemVer rules when updating plugins
  4. Well-defined API - Export only necessary functionality
  5. Error Handling - Always check for the availability of optional dependencies

Usage Examples

Here are examples of how to use the PDS plugin registration system:

-- Example 1: Basic plugin with no dependencies
event.listen {
  name = "pds:discoverPlugins",
  run = function()
    return {
      id = "my-utils",
      manifest = {
        name = "My Utilities",
        version = "1.0.0",
        description = "Common utility functions",
        author = "John Doe"
      },
      initialize = function()
        -- Implementation
        local function formatDate(date)
          -- Date formatting logic
          return "formatted date"
        end
        
        return {
          success = true,
          exports = {
            formatDate = formatDate
          }
        }
      end
  end
}

-- Example 2: Plugin with dependencies
event.listen {
  name = "pds:discoverPlugins",
  run = function()

    return {
      id = "advanced-tools",
      manifest = {
        name = "Advanced Tools",
        version = "0.5.0",
        dependencies = {
          { id = "my-utils", minVersion = "1.0.0" },
          { id = "optional-feature", optional = true }
        }
      },
      initialize = function(dependencies)
        local utils = dependencies["my-utils"]
        local optFeature = dependencies["optional-feature"]
        
        local function process(data)
          -- Use utils
          local date = utils.formatDate(data.timestamp)
          
          -- Use optional dependency if available
          if optFeature then
            data = optFeature.preprocess(data)
          end
          
          return data
        end
        
        return {
          success = true,
          exports = {
            process = process
          }
        }
      end
  end
}

Implementation

-- SemVer module - handling semantic versioning
local semver = {
  -- SemVer module version
  _VERSION = "1.0.0"
}

-- Basic semver parsing with improved handling
function semver.parse(version)
  if type(version) ~= "string" then return nil end
  
  -- Handling prerelease and build metadata
  local versionPart = version:match("^([^-+]+)")
  if not versionPart then return nil end
  
  local major, minor, patch = versionPart:match("^(%d+)%.(%d+)%.(%d+)$")
  if not major then
    major, minor = versionPart:match("^(%d+)%.(%d+)$")
    if not major then
      major = versionPart:match("^(%d+)$")
      if not major then return nil end
      minor, patch = "0", "0"
    else
      patch = "0"
    end
  end
  
  return {
    major = tonumber(major),
    minor = tonumber(minor),
    patch = tonumber(patch)
  }
end

-- Compare versions with better error handling
function semver.compare(v1, v2)
  local ver1 = semver.parse(v1)
  local ver2 = semver.parse(v2)
  
  if not ver1 or not ver2 then return nil end
  
  if ver1.major ~= ver2.major then
    return ver1.major < ver2.major and -1 or 1
  end
  if ver1.minor ~= ver2.minor then
    return ver1.minor < ver2.minor and -1 or 1
  end
  if ver1.patch ~= ver2.patch then
    return ver1.patch < ver2.patch and -1 or 1
  end
  return 0
end

-- Check if version is in range with more descriptive code
function semver.inRange(version, minVersion, maxVersion)
  -- If there are no constraints, the version is always compatible
  if not minVersion and not maxVersion then return true end
  
  -- Check lower constraint
  if minVersion and semver.compare(version, minVersion) < 0 then
    return false
  end
  
  -- Check upper constraint
  if maxVersion and semver.compare(version, maxVersion) > 0 then
    return false
  end
  
  return true
end

-- PDS Core module
PDSCore = PDSCore or {
  -- Main data
  plugins = {},
  initialized = {},
  exports = {},
  debug = true,
  errors = {},
  
  -- Configuration
  config = {
    problemsPageName = "_problems",
    logLevel = "info",          -- Logging level: debug, info, warn, error
    autoNavigateToProblems = true  -- Whether to automatically redirect to problems page
  }
}


function PDSCore:log(level, ...)
  local levels = {
    debug = 1,
    info = 2,
    warn = 3,
    error = 4
  }
  
  local currentLevel = levels[self.config.logLevel] or levels.info
  
  if levels[level] >= currentLevel then
    local prefix = "[PDS:" .. level:upper() .. "]"
    local args = {...}
    local message = ""
    
    for i, arg in ipairs(args) do
      message = message .. tostring(arg)
      if i < #args then message = message .. " " end
    end
    
    print(prefix, message)
  end
end

--- Handles and logs an error that occurred during plugin operation.
--- @param pluginId string The ID of the plugin that encountered the error
--- @param errorType string The category of error (e.g., "not_found", "missing_dependency", "runtime_error")
--- @param errorMessage string Human-readable error message explaining what went wrong
--- @param details table|nil Optional table containing additional error details/context
--- @return table The created error object containing all error information
function PDSCore:handleError(pluginId, errorType, errorMessage, details)
  local errorObj = {
    pluginId = pluginId,
    pluginName = self.plugins[pluginId] and self.plugins[pluginId].manifest.name or pluginId,
    type = errorType,
    message = errorMessage,
    details = details,
    timestamp = os.time()
  }
  
  table.insert(self.errors, errorObj)
  self:log("error", pluginId, errorType, errorMessage)
  
  return errorObj
end

-- Checking for cycles in dependency graph with cycle reporting
function PDSCore:findCycles(graph)
  local visited = {}
  local recStack = {}
  local cycleNodes = {}
  
  local function dfs(node, path)
    if not visited[node] then
      visited[node] = true
      recStack[node] = true
      path = path or {}
      table.insert(path, node)
      
      for _, neighbor in ipairs(graph[node] or {}) do
        if not visited[neighbor] then
          local result, cycle = dfs(neighbor, {unpack(path)})
          if not result then 
            return false, cycle
          end
        elseif recStack[neighbor] then
          -- Cycle detected - extract the cycle path
          local cycleFound = {}
          local startFound = false
          
          table.insert(path, neighbor) -- Complete the cycle
          
          for _, n in ipairs(path) do
            if n == neighbor then
              startFound = true
            end
            
            if startFound then
              table.insert(cycleFound, n)
            end
          end
          
          return false, cycleFound -- Return cycle path
        end
      end
    end
    
    recStack[node] = false
    return true, nil
  end
  
  for node in pairs(graph) do
    if not visited[node] then
      local result, cycle = dfs(node)
      if not result then
        return true, cycle -- Cycle detected with path
      end
    end
  end
  
  return false, nil -- No cycles
end

--- Constructs a directed graph of plugin dependencies.
--- This function creates a dependency graph where each node is a plugin ID
--- and edges represent dependencies between plugins.
--- Only required (non-optional) dependencies are considered.
---
--- @return table A directed graph represented as an adjacency list where
---               graph[id] contains an array of plugin IDs that the plugin depends on
function PDSCore:buildDependencyGraph()
  -- Use cache if data hasn't changed
  if self.graphCache and not self.pluginsModified then
    return self.graphCache
  end
  
  local graph = {}
  
  for id, plugin in pairs(self.plugins) do
    graph[id] = {}
    for _, dep in ipairs(plugin.manifest.dependencies or {}) do
      if not dep.optional then
        table.insert(graph[id], dep.id)
      end
    end
  end
  
  -- Save in cache
  self.graphCache = graph
  self.pluginsModified = false
  
  return graph
end

--- Performs a topological sort on the dependency graph to determine correct initialization order.
--- Implementation uses a depth-first search approach with cycle detection.
---
--- @param graph table The dependency graph as an adjacency list where graph[node] contains an array of dependencies
--- @return table|nil sorted A sorted array of plugin IDs in the order they should be initialized, or nil if sorting failed
--- @return table|nil cycle If a cycle is detected, returns the starting node of the cycle
function PDSCore:topologicalSort(graph)
  local visited = {}
  local result = {}
  local cycleDetected = false
  local cycleStart = nil
  
  local function visit(node, path)
    path = path or {}
    
    if visited[node] == 2 then
      return true -- Already processed
    end
    
    if visited[node] == 1 then
      cycleDetected = true
      cycleStart = node
      return false -- Cycle detected
    end
    
    visited[node] = 1 -- Visiting
    path[node] = true
    
    for _, dep in ipairs(graph[node] or {}) do
      if not visit(dep, path) then
        if not cycleStart and path[dep] then
          cycleStart = dep
        end
        return false
      end
    end
    
    path[node] = nil
    visited[node] = 2 -- Visited
    table.insert(result, node)
    return true
  end
  
  for node, _ in pairs(graph) do
    if not visited[node] and not visit(node) then
      -- If cycle detected, provide information about it
      if cycleDetected then
        local cycle = {cycleStart}
        self:log("error", "Dependency cycle detected starting with", cycleStart)
        return nil, cycle
      end
      return nil
    end
  end
  
  -- Reverse order for correct sequence
  local sorted = {}
  for i = #result, 1, -1 do
    table.insert(sorted, result[i])
  end
  
  return sorted
end

--- Checks if all dependencies of a plugin are available and version compatible.
--- This function verifies both the existence of required dependencies and their version compatibility.
--- @param plugin table The plugin object containing manifest with dependencies
--- @return table missing Array of missing required dependency IDs
--- @return table incompatible Array of incompatible dependencies with version details
function PDSCore:checkDependencies(plugin)
  local missing = {}
  local incompatible = {}
  
  for _, dep in ipairs(plugin.manifest.dependencies or {}) do
    if not self.plugins[dep.id] then
      if not dep.optional then
        table.insert(missing, dep.id)
      end
    elseif dep.minVersion or dep.maxVersion then
      local depVersion = self.plugins[dep.id].manifest.version
      
      -- Check version compatibility
      if not semver.inRange(depVersion, dep.minVersion, dep.maxVersion) then
        table.insert(incompatible, {
          id = dep.id,
          required = (dep.minVersion or "0.0.0") .. " - " .. (dep.maxVersion or "∞"),
          actual = depVersion
        })
      end
    end
  end
  
  return missing, incompatible
end

---Initializes a plugin and its dependencies.
---@param id string Plugin identifier to initialize
---@return boolean success Whether initialization was successful
---@return table|nil exports Exported functions and data from the plugin
---@return string|nil error Error message in case of failure
function PDSCore:initializePlugin(id)
  -- If plugin already initialized, return its status and exports
  if self.initialized[id] ~= nil then
    return self.initialized[id], self.exports[id], self.initialized[id] == false and "Plugin already failed to initialize" or nil
  end
  
  local plugin = self.plugins[id]
  if not plugin then
    return false, nil, self:handleError(id, "not_found", "Plugin not found").message
  end
  
  self:log("debug", "Preparing dependencies for", id)
  
  -- Prepare dependencies for initialization
  local dependencies = {}
  for _, dep in ipairs(plugin.manifest.dependencies or {}) do
    if self.plugins[dep.id] then
      -- Check version compatibility
      if dep.minVersion or dep.maxVersion then
        local depVersion = self.plugins[dep.id].manifest.version
        if not semver.inRange(depVersion, dep.minVersion, dep.maxVersion) then
          if not dep.optional then
            local errorMsg = "Incompatible dependency version: " .. dep.id .. 
                            " (required: " .. (dep.minVersion or "0.0.0") .. " - " .. (dep.maxVersion or "∞") .. 
                            ", actual: " .. depVersion .. ")"
            return false, nil, self:handleError(id, "incompatible_version", errorMsg).message
          else
            self:log("warn", "Incompatible optional dependency version:", dep.id, "for plugin", id)
          end
        end
      end
      
      -- Initialize dependency
      local success, exports, error = self:initializePlugin(dep.id)
      
      if not success and not dep.optional then
        return false, nil, self:handleError(id, "dependency_failed", 
                                           "Dependency " .. dep.id .. " failed to initialize: " .. (error or "Unknown error")).message
      end
      
      if success then
        dependencies[dep.id] = exports
      elseif dep.optional then
        self:log("info", "Optional dependency", dep.id, "not available for", id)
      end
    elseif not dep.optional then
      return false, nil, self:handleError(id, "missing_dependency", "Required dependency " .. dep.id .. " not found").message
    end
  end
  
  -- Initialize plugin
  self:log("info", "Initializing plugin:", id)
  
  -- Use pcall for safe initialization call
  local success, result = pcall(function()
    return plugin.initialize(dependencies)
  end)
  
  if not success then
    self.initialized[id] = false
    return false, nil, self:handleError(id, "runtime_error", "Runtime error: " .. tostring(result)).message
  end
  
  if not result or type(result) ~= "table" or not result.success then
    self.initialized[id] = false
    local errorMsg = result and result.error or "Initialization failed with no specific error"
    return false, nil, self:handleError(id, "init_failed", errorMsg).message
  end
  
  -- Successful initialization
  self.initialized[id] = true
  self.exports[id] = result.exports or {}
  
  self:log("info", "Plugin", id, "initialized successfully")
  return true, self.exports[id]
end

function PDSCore:initializePlugins(sortedIds)
  local results = {}
  self.errors = {} -- Clear previous errors
  
  for _, id in ipairs(sortedIds) do
    local success, exports, error = self:initializePlugin(id)
    results[id] = {
      success = success,
      error = error
    }
  end
  
  -- If there are errors, create problems page
  if #self.errors > 0 then
    self:createProblemsPage()
  end
  
  return results
end

-- Better generation of problems page content
function PDSCore:generateProblemsContent()
  local content = "# Plugin Initialization Problems\n\n"
  
  -- Summary
  if #self.errors == 1 then
    content = content .. "⚠️ **1 plugin failed to initialize.**\n\n"
  else
    content = content .. "⚠️ **" .. #self.errors .. " plugins failed to initialize.**\n\n"
  end
  
  -- Detailed error information
  for _, err in ipairs(self.errors) do
    content = content .. "## " .. err.pluginName .. " (`" .. err.pluginId .. "`)\n\n"
    content = content .. "**Error Type:** " .. err.type .. "\n\n"
    content = content .. "**Error Message:** " .. err.message .. "\n\n"
    
    if err.details and type(err.details) == "table" then
      content = content .. "**Details:**\n\n```\n"
      -- Simplified error details display
      for k, v in pairs(err.details) do
        content = content .. k .. ": " .. tostring(v) .. "\n"
      end
      content = content .. "```\n\n"
    end
    
    -- Add suggested actions based on error type
    content = content .. "**Suggested Action:** "
    
    if err.type == "not_found" then
      content = content .. "Make sure the plugin ID is correct and the plugin is installed.\n\n"
    elseif err.type == "missing_dependency" then
      content = content .. "Install the required dependency or update the plugin to make this dependency optional.\n\n"
    elseif err.type == "incompatible_version" then
      content = content .. "Update the required plugin to a compatible version.\n\n"
    elseif err.type == "runtime_error" then
      content = content .. "There is a bug in the plugin code. Contact the plugin author.\n\n"
    else
      content = content .. "Check the plugin code and dependencies.\n\n"
    end
  end
  
  content = content .. "---\n\n"
  content = content .. "Use the command `PDS: Reload Plugins` after fixing the issues.\n"
  
  return content
end

-- Improved creation of problems page with better organization
function PDSCore:createProblemsPage()
  -- Remove previous problems page if exists
  if space.pageExists(self.config.problemsPageName) then
    space.deletePage(self.config.problemsPageName)
  end
  
  -- If no errors, do not create page
  if #self.errors == 0 then
    return
  end
  
  -- Generate page content
  local content = self:generateProblemsContent()
  
  -- Save page
  space.writePage(self.config.problemsPageName, content)
  
  -- Notify user
  if #self.errors == 1 then
    editor.flashNotification("1 plugin failed to initialize. See " .. self.config.problemsPageName .. " page for details.", "error")
  else
    editor.flashNotification(#self.errors .. " plugins failed to initialize. See " .. self.config.problemsPageName .. " page for details.", "error")
  end
  
  -- Automatically navigate to problems page
  if self.config.autoNavigateToProblems then
    editor.navigate(self.config.problemsPageName)
  end
end

-- Main PDSCore plugin management functions

-- Plugin discovery with better reporting
function PDSCore:discoverPlugins()
  self:log("info", "Starting plugin discovery")
  
  -- Previously collected plugins for comparison
  local previousPluginCount = 0
  for _ in pairs(self.plugins) do previousPluginCount = previousPluginCount + 1 end
  
  local responses = event.dispatch("pds:discoverPlugins")
  local discoveredCount = 0
  
  for _, response in ipairs(responses or {}) do
    if response and response.id and response.manifest then
      -- Basic data validation
      if type(response.id) ~= "string" or response.id == "" then
        self:log("warn", "Skipped plugin with invalid ID")
      elseif type(response.manifest) ~= "table" then
        self:log("warn", "Skipped plugin", response.id, "with invalid manifest")
      elseif type(response.initialize) ~= "function" then
        self:log("warn", "Plugin", response.id, "has no initialization function")
      else
        -- Version validation and standardization
        if not response.manifest.version then
          self:log("warn", "Plugin", response.id, "has no version, using 0.0.1")
          response.manifest.version = "0.0.1"
        end
        
        if not semver.parse(response.manifest.version) then
          self:log("warn", "Plugin", response.id, "has invalid version format:", response.manifest.version)
          response.manifest.version = "0.0.1"
        end
        
        -- Data standardization
        response.manifest.name = response.manifest.name or response.id
        response.manifest.dependencies = response.manifest.dependencies or {}
        
        -- Add plugin to registry
        self.plugins[response.id] = response
        discoveredCount = discoveredCount + 1
        
        self:log("info", "Discovered plugin:", response.manifest.name, "(", response.id, ")", "version:", response.manifest.version)
      end
    end
  end
  
  -- Set plugin modification flag for cache refresh
  if discoveredCount ~= previousPlginCount then
    self.pluginsModified = true
  end
  
  self:log("info", "Plugin discovery completed, found:", discoveredCount)
  return discoveredCount
end

-- PDSCore configuration with externals
function PDSCore:configure(config)
  config = config or {}
  
  -- Deep copy settings
  local function deepMerge(target, source)
    for k, v in pairs(source) do
      if type(v) == "table" and type(target[k]) == "table" then
        deepMerge(target[k], v)
      else
        target[k] = v
      end
    end
    return target
  end
  
  -- Apply settings
  self.config = deepMerge(self.config, config)
  
  -- Update dependent settings
  self.debug = self.config.debug or false
  
  return self
end

-- Improved start function with configuration
function PDSCore:start(config)
  self:log("info", "Starting PDS system")
  
  -- Apply optional configuration
  if config then
    self:configure(config)
  end
  
  -- Remove existing problems page
  if space.pageExists(self.config.problemsPageName) then
    space.deletePage(self.config.problemsPageName)
  end
  
  -- Clear system state
  self.initialized = {}
  self.exports = {}
  self.errors = {}
  
  -- Discover plugins
  local discoveredCount = self:discoverPlugins()
  if discoveredCount == 0 then
    self:log("warn", "No plugins found")
    return true -- Technically success, though nothing to do
  end
  
  -- Build dependency graph
  local graph = self:buildDependencyGraph()
  
  -- Check for cycles
  local hasCycle, cyclePath = self:findCycles(graph)
  if hasCycle then
    local cycleStr = table.concat(cyclePath, " → ")
    self:log("error", "Error: Detected circular dependency between plugins: " .. cycleStr)
    
    -- Create information page about the problem with cycle details
    space.writePage(self.config.problemsPageName, 
      "# Plugin Loading Error\n\n" ..
      "Circular dependencies detected between plugins. System cannot continue.\n\n" ..
      "**Dependency cycle detected:** `" .. cycleStr .. "`\n\n" ..
      "Please modify one of these plugins to break the dependency cycle."
    )
    editor.flashNotification("Circular dependencies detected. See " .. self.config.problemsPageName .. " page", "error")
    if self.config.autoNavigateToProblems then
      editor.navigate(self.config.problemsPageName)
    end
    
    return false
  end
  
  -- Topologically sort plugins
  local sortedIds, cycle = self:topologicalSort(graph)
  if not sortedIds then
    if cycle then
      self:log("error", "Error: Detected circular dependency in cycle:", table.concat(cycle, " -> "))
    else
      self:log("error", "Error: Cannot topologically sort plugins")
    end
    
    -- Create information page about the problem
    space.writePage(self.config.problemsPageName, 
      "# Plugin Loading Error\n\n" ..
      "Cannot properly sort plugins. System cannot continue.\n\n" ..
      (cycle and ("Detected cycle: " .. table.concat(cycle, " -> ") .. "\n\n") or "") ..
      "Please check plugin configuration and ensure they don't create dependency cycles."
    )
    editor.flashNotification("Dependency problem detected. See " .. self.config.problemsPageName .. " page", "error")
    if self.config.autoNavigateToProblems then
      editor.navigate(self.config.problemsPageName)
    end
    
    return false
  end
  
  self:log("info", "Initialization order:", table.concat(sortedIds, ", "))
  
  -- Initialize in sorted order
  local results = self:initializePlugins(sortedIds)
  
  -- Report results
  local success = true
  local successCount = 0
  for id, result in pairs(results) do
    if not result.success then
      success = false
      self:log("error", "Plugin", id, "failed to initialize:", result.error)
    else
      successCount = successCount + 1
      self:log("info", "Plugin", id, "initialized successfully")
    end
  end
  
  self:log("info", "Plugin initialization completed. Success:", successCount, "/", #sortedIds)
  
  -- If no errors detected but there are on the list, this means sudden runtime errors
  if success and #self.errors > 0 then
    self:createProblemsPage()
    return false
  end
  
  return success
end

-- Initialize PDS core when system is ready
event.listen {
  name = "system:ready",
  run = function()
    PDSCore:configure({
      debug = true,
      logLevel = "info", -- Possible options: debug, info, warn, error
      autoNavigateToProblems = true
    }):start()
  end
}

-- SilverBullet command to reload plugins
command.define {
  name = "PDS: Reload Plugins",
  description = "Clears plugin cache and reloads them from scratch",
  run = function()
    system.invokeCommand("System: Reload")
    editor.flashNotification("Reloading PDS plugins...", "info")
    PDSCore:start()
    editor.flashNotification("PDS plugins have been reloaded", "info")
  end
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment