Developer documentation

Sunder package development.

Build packages for Sunder App with the SDK, run them locally through Runtime Host, validate distributable archives, and publish versions to Sunder Registry. This page covers the Sunder package platform only: package authoring, SDK, CLI, Registry workflow, compatibility, and common development failures.

Start here

Quick start: from template to Registry package.

This is the shortest complete path for package authors. It creates a package, builds the development output consumed by Sunder App, loads that output locally, produces a distributable archive, validates it, and publishes it to the configured Sunder Registry.

Create a package

Install the template, create a package project, and build it. Use a lowercase dot-separated package id such as my.company.package. Treat this id as stable once published.

Create and build
dotnet new install Sunder.Package.Templates
dotnet new sunder-package `
  --name MyPackage `
  --packageId my.company.package `
  --packageName "My Package"
cd .\MyPackage
dotnet build .\MyPackage\MyPackage.csproj
Create and build
dotnet new install Sunder.Package.Templates
dotnet new sunder-package \
  --name MyPackage \
  --packageId my.company.package \
  --packageName "My Package"
cd ./MyPackage
dotnet build ./MyPackage/MyPackage.csproj
Create and build
dotnet new install Sunder.Package.Templates
dotnet new sunder-package \
  --name MyPackage \
  --packageId my.company.package \
  --packageName "My Package"
cd ./MyPackage
dotnet build ./MyPackage/MyPackage.csproj

Run locally

dotnet build emits an unpacked sunder-dev folder next to the compiled output. Load that folder into Sunder App with --dev-package.

Load development output
& "C:\Path\To\Sunder.App.exe" `
  --dev-package ".\MyPackage\bin\Debug\net10.0\sunder-dev"
Load development output
/Applications/Sunder.app/Contents/MacOS/Sunder.App \
  --dev-package ./MyPackage/bin/Debug/net10.0/sunder-dev
Load development output
./Sunder.App \
  --dev-package ./MyPackage/bin/Debug/net10.0/sunder-dev

Validate and publish

dotnet publish emits a .sunderpkg archive. Validate it before installing locally or publishing to the Registry.

Publish workflow
dotnet publish .\MyPackage\MyPackage.csproj -c Release
sunder package validate `
  .\MyPackage\bin\Release\net10.0\publish\MyPackage.1.0.0.sunderpkg
sunder auth login
sunder publish --file `
  .\MyPackage\bin\Release\net10.0\publish\MyPackage.1.0.0.sunderpkg
Publish workflow
dotnet publish ./MyPackage/MyPackage.csproj -c Release
sunder package validate \
  ./MyPackage/bin/Release/net10.0/publish/MyPackage.1.0.0.sunderpkg
sunder auth login
sunder publish --file \
  ./MyPackage/bin/Release/net10.0/publish/MyPackage.1.0.0.sunderpkg
Publish workflow
dotnet publish ./MyPackage/MyPackage.csproj -c Release
sunder package validate \
  ./MyPackage/bin/Release/net10.0/publish/MyPackage.1.0.0.sunderpkg
sunder auth login
sunder publish --file \
  ./MyPackage/bin/Release/net10.0/publish/MyPackage.1.0.0.sunderpkg
01

Template

Install Sunder.Package.Templates.

02

Project

Create a package with a stable package id.

03

Build

Emit sunder-dev with dotnet build.

04

Load

Start Sunder App with --dev-package.

05

Publish

Validate and publish the generated .sunderpkg.

Model

Platform concepts and boundaries.

Sunder has one runtime extension unit: the package. The desktop app, runtime host, and Registry are separate parts of the platform with clear ownership boundaries.

App and Runtime Host process boundary

AreaOwns
Sunder AppDesktop shell UI, package view activation, settings views, view caching, shell notifications, app-side faults, theme resources, and app branding.
Runtime HostInstalled package records, package archive validation, graph activation, runtime services, package configuration, secrets, callbacks, auth, faults, and package asset serving.

Runtime units

Sunder App

Avalonia desktop shell that loads package UI contributions and presents package management UX.

Runtime Host

Local process that owns installed package state, validation, activation, configuration, secrets, faults, and package assets.

Sunder Registry

Remote catalog and distribution infrastructure for package metadata, immutable versions, artifacts, icons, ownership, search, and dist tags.

Package

The only installable runtime extension unit. Packages can add views, settings, services, tools, integrations, and extension contributions.

Dev package

Unpacked sunder-dev output emitted by dotnet build for local package development.

.sunderpkg

Zip-based distributable archive emitted by dotnet publish and validated before install or update.

Contracts package

A NuGet package that exposes typed extension contracts. It is not a Sunder runtime package.

Registry boundary

The Registry owns catalog metadata, immutable package versions, artifact storage, extracted icons, publisher permissions, dist tags, search, package details, install plans, and update resolution. Local installed versions, enabled or disabled state, configuration, secrets, and active runtime state belong to the local Runtime Host.

Authoring

Create Sunder packages with the SDK and template.

Prerequisites

  • .NET 10 SDK.
  • Sunder SDK packages available from the configured package feed or local source build.
  • Sunder.Package.Templates installed as a dotnet new template package.
  • A Sunder App install or source build for local --dev-package loading.
  • Sunder CLI for validation, local install, Registry auth, and publishing.

Template commands and options

Common templates
dotnet new sunder-package `
  --name MyPackage `
  --packageId my.company.package `
  --packageName "My Package"

dotnet new sunder-package `
  --name MyPackage `
  --packageId my.company.package `
  --packageName "My Package" `
  --createInPlace `
  --output .\MyPackage

dotnet new sunder-package `
  --name MyHeadlessPackage `
  --packageId my.company.headless `
  --packageName "My Headless Package" `
  --noDefaultView

dotnet new sunder-package `
  --name MyPackage `
  --packageId my.company.package `
  --packageName "My Package" `
  --withContracts

dotnet new sunder-package `
  --name MyExtension `
  --packageId my.company.extension `
  --packageName "My Extension" `
  --withHostDependency `
  --hostPackageId my.company.host

dotnet new sunder-package `
  --name MyTypedExtension `
  --packageId my.company.typedextension `
  --packageName "My Typed Extension" `
  --withHostContracts `
  --hostPackageId my.company.host `
  --hostContractsPackageId My.Company.Host.Contracts `
  --hostContractsVersion 1.0.0
Common templates
dotnet new sunder-package \
  --name MyPackage \
  --packageId my.company.package \
  --packageName "My Package"

dotnet new sunder-package \
  --name MyPackage \
  --packageId my.company.package \
  --packageName "My Package" \
  --createInPlace \
  --output ./MyPackage

dotnet new sunder-package \
  --name MyHeadlessPackage \
  --packageId my.company.headless \
  --packageName "My Headless Package" \
  --noDefaultView

dotnet new sunder-package \
  --name MyPackage \
  --packageId my.company.package \
  --packageName "My Package" \
  --withContracts

dotnet new sunder-package \
  --name MyExtension \
  --packageId my.company.extension \
  --packageName "My Extension" \
  --withHostDependency \
  --hostPackageId my.company.host

dotnet new sunder-package \
  --name MyTypedExtension \
  --packageId my.company.typedextension \
  --packageName "My Typed Extension" \
  --withHostContracts \
  --hostPackageId my.company.host \
  --hostContractsPackageId My.Company.Host.Contracts \
  --hostContractsVersion 1.0.0
Common templates
dotnet new sunder-package \
  --name MyPackage \
  --packageId my.company.package \
  --packageName "My Package"

dotnet new sunder-package \
  --name MyPackage \
  --packageId my.company.package \
  --packageName "My Package" \
  --createInPlace \
  --output ./MyPackage

dotnet new sunder-package \
  --name MyHeadlessPackage \
  --packageId my.company.headless \
  --packageName "My Headless Package" \
  --noDefaultView

dotnet new sunder-package \
  --name MyPackage \
  --packageId my.company.package \
  --packageName "My Package" \
  --withContracts

dotnet new sunder-package \
  --name MyExtension \
  --packageId my.company.extension \
  --packageName "My Extension" \
  --withHostDependency \
  --hostPackageId my.company.host

dotnet new sunder-package \
  --name MyTypedExtension \
  --packageId my.company.typedextension \
  --packageName "My Typed Extension" \
  --withHostContracts \
  --hostPackageId my.company.host \
  --hostContractsPackageId My.Company.Host.Contracts \
  --hostContractsVersion 1.0.0
OptionMeaning
--packageId <id>Required runtime package id written into generated metadata.
--packageName <name>Required user-facing package name written into metadata and starter view.
--withContractsAdds a sibling contracts project for public extension points.
--createInPlaceCreates package files directly in the specified output folder instead of under a child project folder.
--noDefaultViewOmits the default shell-visible package view.
--withHostDependencyAdds runtime dependency metadata for another package.
--hostPackageId <id>Runtime package id required with --withHostDependency.
--withHostContractsAdds host dependency metadata, a host contracts NuGet reference, and a typed extension stub.
--hostContractsPackageId <id>NuGet package id for host contracts.
--hostContractsVersion <version>NuGet package version for host contracts.

Generated project shape

Package projects target net10.0, reference Sunder.Sdk, and useSunder.Package.Build to generate dev output and package artifacts. Package projects do not reference Sunder App or Runtime Host implementation projects. Template variants can add sibling contracts projects or host contracts package references for typed extensions.

Package project file
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
    <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Avalonia" Version="12.0.3" />
    <PackageReference Include="Sunder.Sdk" Version="*" />
    <PackageReference Include="Sunder.Package.Build" Version="*" PrivateAssets="all" />
  </ItemGroup>
</Project>

Package metadata and module

Every package entry assembly must contain exactly one public, non-abstractISunderPackageModule implementation with a public parameterless constructor. Register services in ConfigureServices; register views, settings, background services, configuration schemas, and extensions in RegisterContributions.

Package metadata
using Sunder.Sdk.Packaging;

[assembly: SunderPackage(
    Id = "my.company.package",
    Name = "My Package",
    Summary = "Adds a custom Sunder workspace.",
    Icon = "assets/icon.png")]
Package module
using Microsoft.Extensions.DependencyInjection;
using Sunder.Sdk.Abstractions;

namespace MyCompany.Package;

public sealed class PackageModule : ISunderPackageModule
{
    public void ConfigureServices(IServiceCollection services, IPackageContext context)
    {
        services.AddTransient<MyViewModel>();
    }

    public void RegisterContributions(IPackageContributionRegistry registry, IServiceProvider services)
    {
        registry.RegisterPackageView<MyView>(new PackageViewRegistration(
            id: "my.company.package.main",
            name: "My Package"));
    }
}

SDK

Sunder SDK guide.

Sunder.Sdk is the contract layer between a package and the installed Sunder host. Use it to declare metadata, define a module entrypoint, register UI and runtime contributions, access package-scoped state, and integrate with host services without referencing implementation projects.

SDK areas

AreaPrimary typesProvides
Packaging metadataSunderPackageAttribute, SunderPackageDependencyAttributePackage identity and runtime package dependencies.
Module lifecycleISunderPackageModulePackage startup, service registration, and contribution registration.
Package contextIPackageContextPackage id, version, install path, storage, configuration, secrets, and logging.
ContributionsIPackageContributionRegistryViews, settings views, background services, extensions, and configuration schemas.
ViewsPackageViewRegistration, PackageViewPlacementShell-visible Avalonia package views.
WorkspacesIPackageWorkspaceFactoryFactory-created package workspaces and views.
ExtensionsPackageExtensionPoint<T>, IPackageExtensionCatalogTyped package-to-package contribution points and active contribution discovery.
Extension changesIPackageExtensionCatalogMonitorStructured change events when packages activate, deactivate, update, or fault.
ConfigurationPackageConfigurationSchema, IPackageConfigurationHost-rendered settings schemas and package configuration values.
StorageIPackageStorageContext, IPackageFileStore, IPackageKeyValueStorePackage-scoped mutable files and key-value state.
SecretsIPackageSecretsPackage-scoped secret values.
LoggingIPackageLogging, IPackageEventLoggerPackage event logging and ILoggerFactory access.
NotificationsIPackageNotificationServiceUser-visible package notifications.
Background processesIBackgroundProcessQueue, BackgroundProcessRequestHost-visible queued work with progress, cancellation, and indicator placement.
Shell integrationIPackageShellViewService, IPackageViewNavigationTargetShell navigation and hotbar/workspace integration.
Settings navigationIPackageSettingsNavigationServiceOpen global settings or another package's settings when the host supports it.
Package sessionsIPackageSessionServiceLoad, unload, and query installed or dev package sessions when the host supports it.
Callbacks and authIPackageCallbackHandler, IPackageAuthHandlerLocal callback sessions plus auth status and disconnect integration.
Theme resourcesSunderThemeKeysSemantic resource keys for package UI.

Package context

IPackageContext is passed to ConfigureServices and exposes the host-owned services for the active package. It is the safe boundary for package identity, installation location, mutable storage, configuration, secrets, and logging.

MemberUse
PackageIdStable runtime package id.
VersionActive package version parsed from package metadata.
InstallPathRead-only installed or dev package content root.
StoragePackage-scoped data, cache, logs, files, and key-value state.
ConfigurationCurrent host-rendered configuration values.
SecretsPackage-scoped secret values.
LoggerFactoryMicrosoft.Extensions.Logging factory for runtime logs.
LoggingStructured package event logger and logger factory.
Capture package context services
public void ConfigureServices(IServiceCollection services, IPackageContext context)
{
    services.AddSingleton(new MyPackageEnvironment(
        context.PackageId,
        context.Version,
        context.InstallPath,
        context.Storage.DataRootPath));

    services.AddSingleton(context.Storage);
    services.AddSingleton(context.Configuration);
    services.AddSingleton(context.Secrets);
    services.AddSingleton(context.Logging);
}

Contribution registry

Register package contributions in RegisterContributions. The registry is declarative: package views, settings views, background services, configuration schemas, and extension contributions are registered in code instead of being authored into the generated manifest.

Register common contributions
public void RegisterContributions(IPackageContributionRegistry registry, IServiceProvider services)
{
    registry.RegisterPackageView<MyView>(new PackageViewRegistration(
        id: "my.company.package.main",
        name: "My Package",
        icon: "assets/icon.png"));

    registry.RegisterSettingsView<MySettingsView>();
    registry.RegisterBackgroundService<MyBackgroundService>();
    registry.RegisterConfigurationSchema(MyConfiguration.Schema);
    registry.RegisterExtension(MyExtensionPoints.Providers, services.GetRequiredService<MyProvider>());
}

Views, workspaces, and background services

Package views are Avalonia controls with stable ids scoped under the package id. Use factories when the root view needs dependency-injected construction. Background services are package-owned workers that start and stop with package activation.

Factory-created workspace
using Avalonia.Controls;
using Sunder.Sdk.Abstractions;

public sealed class MyWorkspaceFactory : IPackageWorkspaceFactory
{
    public Control CreateRootView(IServiceProvider services)
    {
        return new MyView
        {
            DataContext = services.GetRequiredService<MyViewModel>()
        };
    }
}

registry.RegisterPackageViewFactory<MyWorkspaceFactory>(new PackageViewRegistration(
    id: "my.company.package.workspace",
    name: "Workspace",
    defaultPlacement: PackageViewPlacement.Middle,
    showInHotbarByDefault: true));
Background service
public sealed class MyBackgroundService : IPackageBackgroundService
{
    public Task StartAsync(CancellationToken cancellationToken = default)
    {
        // Start timers, subscriptions, or package-owned workers here.
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken = default)
    {
        // Stop work quickly when the package is deactivated.
        return Task.CompletedTask;
    }
}

Configuration schemas

Register host-rendered settings schemas in RegisterContributions. This lets Sunder render package settings without the package owning every settings UI detail. Current field kinds are Text, Secret, Boolean, and Select.

Host-rendered settings schema
using Sunder.Sdk.Configuration;

registry.RegisterConfigurationSchema(new PackageConfigurationSchema(
    PackageId: "my.company.package",
    PackageDisplayName: "My Package",
    Summary: "Package settings.",
    Sections:
    [
        new PackageConfigurationSection(
            SectionId: "general",
            Title: "General",
            Description: null,
            Fields:
            [
                new PackageConfigurationField(
                    Key: "enabled",
                    Label: "Enabled",
                    Kind: PackageConfigurationFieldKind.Boolean,
                    DefaultValue: "true"),
                new PackageConfigurationField(
                    Key: "api-key",
                    Label: "API key",
                    Kind: PackageConfigurationFieldKind.Secret,
                    Description: "Stored through the package secret store."),
                new PackageConfigurationField(
                    Key: "mode",
                    Label: "Mode",
                    Kind: PackageConfigurationFieldKind.Select,
                    DefaultValue: "balanced",
                    Options:
                    [
                        new PackageConfigurationOption("fast", "Fast"),
                        new PackageConfigurationOption("balanced", "Balanced")
                    ])
            ])
    ]));
Read configuration values
var enabled = bool.TryParse(context.Configuration.GetValue("enabled"), out var parsed) && parsed;
var mode = context.Configuration.GetValue("mode") ?? "balanced";
var apiKey = context.Secrets.GetSecret("api-key");

if (string.IsNullOrWhiteSpace(apiKey))
{
    await context.Logging.Events.WarningAsync(
        "my.company.package.configuration.missing_secret",
        "The API key has not been configured.",
        cancellationToken: cancellationToken);
}

Storage and secrets

Use package storage abstractions for mutable data. Do not write generated files, cached data, state, or secrets into InstallPath; installed package content is treated as package payload, not as a writable app-data folder.

SurfaceUse
Storage.DataRootPathPackage data root.
Storage.CacheRootPathRebuildable cache root.
Storage.LogsRootPathPackage log root used by package logging.
Storage.Files.GetPath(...)Safe package file-store paths; rejects rooted paths and parent traversal.
Storage.StateString key-value state with sync read and async write/list/delete APIs.
SecretsString secret values through get/set/delete APIs.
Package files and state
var profilePath = context.Storage.Files.GetPath("profiles/default.json");
Directory.CreateDirectory(Path.GetDirectoryName(profilePath)!);

await File.WriteAllTextAsync(profilePath, profileJson, cancellationToken);

await context.Storage.State.SetValueAsync(
    "sync.lastRun",
    DateTimeOffset.UtcNow.ToString("O"),
    cancellationToken);

var lastRun = await context.Storage.State.GetValueAsync("sync.lastRun", cancellationToken);

Notifications

IPackageNotificationService publishes user-visible package notifications. Choose the display mode and severity so the app can decide whether the event should appear as a toast, tray entry, or both.

Publish a package notification
using Sunder.Sdk.Notifications;

await notificationService.PublishAsync(new PackageNotificationRequest(
    Title: "Import complete",
    Message: "Your package import finished successfully.",
    DisplayMode: PackageNotificationDisplayMode.ToastAndTray,
    Severity: PackageNotificationSeverity.Success),
    cancellationToken);

Shell integration

IPackageShellViewService lets package code inspect hotbar views, add or remove a view, and open or close panels. Views or view models can implement IPackageViewNavigationTargetto receive navigation parameters when Sunder opens a package view.

Hotbar and navigation
public sealed class MyViewModel(IPackageShellViewService shellViewService)
{
    public async Task PinAndOpenAsync(CancellationToken cancellationToken)
    {
        await shellViewService.AddViewToDefaultHotbarAsync(
            "my.company.package.main",
            openPanel: true,
            parameters: new Dictionary<string, string?> { ["itemId"] = "42" },
            cancellationToken: cancellationToken);
    }
}

public sealed class MyView : UserControl, IPackageViewNavigationTarget
{
    public ValueTask OnNavigatedToAsync(
        PackageViewNavigationContext context,
        CancellationToken cancellationToken = default)
    {
        if (DataContext is MyViewModel viewModel
            && context.Parameters.TryGetValue("itemId", out var itemId))
        {
            viewModel.OpenItem(itemId);
        }

        return ValueTask.CompletedTask;
    }
}

Callbacks and auth

Use IPackageCallbackHandler for generic browser or local callback flows. UseIPackageAuthHandler when the package owns an auth connection and should expose status, authorization, callback completion, and disconnect behavior to the host.

Register callback and auth integration
using Sunder.Sdk.Authentication;
using Sunder.Sdk.Abstractions;

services.AddSingleton<MyOAuthHandler>();
services.AddSingleton<IPackageCallbackHandler>(serviceProvider => serviceProvider.GetRequiredService<MyOAuthHandler>());
services.AddSingleton<IPackageAuthHandler>(serviceProvider => serviceProvider.GetRequiredService<MyOAuthHandler>());

public sealed class MyOAuthHandler : IPackageAuthHandler
{
    private const string PackageId = "my.company.package";

    public ValueTask<PackageAuthStatus> GetStatusAsync(CancellationToken cancellationToken = default)
    {
        return ValueTask.FromResult(new PackageAuthStatus(
            PackageId,
            PackageAuthStatusKind.NotConnected,
            "Not connected.",
            CanAuthorize: true,
            CanDisconnect: false));
    }

    public Task<PackageAuthSessionStartResult?> StartAuthorizationAsync(
        PackageAuthSessionStartContext context,
        CancellationToken cancellationToken = default)
    {
        var launchUrl = "https://identity.example/authorize?redirect_uri="
            + Uri.EscapeDataString(context.CallbackUri.ToString());

        return Task.FromResult<PackageAuthSessionStartResult?>(new PackageAuthSessionStartResult(
            PackageId,
            context.AuthSessionId,
            PackageAuthFlowKind.Browser,
            launchUrl,
            "Continue in your browser."));
    }

    public Task<PackageAuthStatus> CompleteAuthorizationAsync(
        PackageAuthSessionCompletionContext context,
        CancellationToken cancellationToken = default)
    {
        return Task.FromResult(new PackageAuthStatus(
            PackageId,
            PackageAuthStatusKind.Connected,
            "Connected.",
            CanAuthorize: false,
            CanDisconnect: true));
    }

    public Task<PackageAuthStatus> DisconnectAsync(CancellationToken cancellationToken = default)
    {
        return Task.FromResult(new PackageAuthStatus(
            PackageId,
            PackageAuthStatusKind.NotConnected,
            "Disconnected.",
            CanAuthorize: true,
            CanDisconnect: false));
    }
}

Theme resources

Use SunderThemeKeys and Avalonia dynamic resources for shell-sensitive colors, surfaces, borders, radius, spacing, and font sizes. Dynamic resources keep package UI aligned with theme changes without referencing Sunder App implementation types.

Sunder.Brush.Background.AppSunder.Brush.Surface.BaseSunder.Brush.Surface.RaisedSunder.Brush.Border.SubtleSunder.Brush.Border.FocusSunder.Brush.Foreground.PrimarySunder.Brush.Foreground.SecondarySunder.Brush.AccentSunder.Brush.WarningSunder.Brush.DangerSunder.Radius.MediumSunder.Spacing.MediumSunder.FontSize.Body
Avalonia dynamic theme resources
<UserControl xmlns="https://github.com/avaloniaui"
             Background="{DynamicResource Sunder.Brush.Surface.Base}">
  <Border Background="{DynamicResource Sunder.Brush.Surface.Raised}"
          BorderBrush="{DynamicResource Sunder.Brush.Border.Subtle}"
          CornerRadius="{DynamicResource Sunder.Radius.Medium}">
    <StackPanel Spacing="{DynamicResource Sunder.Spacing.Medium}">
      <TextBlock Text="Package view"
                 FontSize="{DynamicResource Sunder.FontSize.SectionTitle}"
                 Foreground="{DynamicResource Sunder.Brush.Foreground.Primary}" />
      <TextBlock Text="Uses shell semantic resources."
                 Foreground="{DynamicResource Sunder.Brush.Foreground.Secondary}" />
    </StackPanel>
  </Border>
</UserControl>

Extensibility

Package-to-package extension model.

Runtime dependencies and typed contracts are separate. Runtime dependencies control activation order and availability. Contracts packages are NuGet packages that let extension packages compile against public extension point types without referencing host implementation projects.

Runtime dependencies

Runtime dependency metadata
using Sunder.Sdk.Packaging;

[assembly: SunderPackage(
    Id = "my.company.extension",
    Name = "My Extension",
    Summary = "Extends another Sunder package.",
    Icon = "assets/icon.png")]

[assembly: SunderPackageDependency(
    PackageId = "my.company.host",
    VersionRange = ">=1.0.0 <2.0.0")]

Contracts packages

  • They have no sunder-package.json.
  • They contain public extension point declarations and typed contribution contracts.
  • They do not reference the host package implementation.
  • They do not reference Sunder App or Runtime Host.
  • They reference Sunder.Sdk only when they need SDK extension-point types.

Extension catalog

Hosts that support live activation implement IPackageExtensionCatalogMonitor. Use it when open UI or cached state depends on extensions from other packages.

Define and register an extension
public interface IMyProvider
{
    string ProviderId { get; }
}

public static class MyExtensionPoints
{
    public static readonly PackageExtensionPoint<IMyProvider> Providers = new("my.company.host:providers");
}

registry.RegisterExtension(
    MyExtensionPoints.Providers,
    services.GetRequiredService<MyProvider>());
Consume and monitor extensions
var providers = extensionCatalog.GetExtensions(MyExtensionPoints.Providers);
foreach (var provider in providers)
{
    Console.WriteLine(provider.ProviderId);
}

if (extensionCatalog is IPackageExtensionCatalogMonitor monitor)
{
    monitor.Changed += (_, args) =>
    {
        if (args.IncludesExtensionPoint(MyExtensionPoints.Providers.Id))
        {
            RefreshProviders();
        }
    };
}

Development loop

Load and debug packages locally.

Dev output

dotnet build emits an unpacked development folder. This folder is what Sunder App consumes in local package development mode. Use the explicit pack target when you want a.sunderpkg without running a full publish operation; by default that target writes to the target directory unless SunderPackageOutputPath is set.

Dev package output
MyPackage/bin/Debug/net10.0/sunder-dev/
  sunder-package.json
  lib/
    MyPackage.dll
    MyPackage.pdb
    MyPackage.deps.json
    MyPackage.runtimeconfig.json
  assets/
    icon.png
Explicit package target
dotnet msbuild .\MyPackage\MyPackage.csproj `
  -t:PackSunderPackage `
  -p:Configuration=Release `
  -p:SunderPackageOutputPath=.\artifacts\packages\

App loading

The app normalizes dev package paths and sends them to Runtime Host. Runtime Host validates and activates runtime package content, then the app activates app-side views and settings from the original dev package folders.

Argument or variableMeaning
--dev-package <folder>Loads an unpacked sunder-dev package folder for local development.
--watchWatches development packages for changes. Requires at least one --dev-package.
--runtime-url <url>Connects Sunder App to an explicit Runtime Host URL.
--runtime-host-path <path>Points Sunder App at a Runtime Host executable or folder.
SUNDER_RUNTIME_URLEnvironment override for the app and CLI runtime URL.
SUNDER_RUNTIME_HOST_PATHEnvironment override for the Runtime Host executable or folder used by the app.
Load multiple dev packages
& "C:\Path\To\Sunder.App.exe" `
  --dev-package ".\HostPackage\bin\Debug\net10.0\sunder-dev" `
  --dev-package ".\ExtensionPackage\bin\Debug\net10.0\sunder-dev"
Load multiple dev packages
/Applications/Sunder.app/Contents/MacOS/Sunder.App \
  --dev-package ./HostPackage/bin/Debug/net10.0/sunder-dev \
  --dev-package ./ExtensionPackage/bin/Debug/net10.0/sunder-dev
Load multiple dev packages
./Sunder.App \
  --dev-package ./HostPackage/bin/Debug/net10.0/sunder-dev \
  --dev-package ./ExtensionPackage/bin/Debug/net10.0/sunder-dev

Helper scripts

Packages generated from the template include helper scripts for the common Windows development loop. Set the app path, optionally set a runtime host path, then run the script from the package project directory.

Generated helper scripts
$env:SUNDER_APP_PATH = "C:\Path\To\Sunder.App.exe"
.\run-sunder-dev.ps1

$env:SUNDER_RUNTIME_HOST_PATH = "C:\Path\To\Sunder.Runtime.Host.exe"
.\debug-sunder-runtime.ps1 -RuntimeUrl http://127.0.0.1:5276

Runtime debugging

Use SUNDER_WAIT_FOR_DEBUGGER=1 or --wait-for-debugger when Runtime Host should block until a debugger is attached. Use SUNDER_RUNTIME_URL or--runtime-url when working against a non-default local host URL.

Attach debugger to Runtime Host
& "C:\Path\To\Sunder.Runtime.Host.exe" `
  --wait-for-debugger `
  --urls http://127.0.0.1:5276
& "C:\Path\To\Sunder.App.exe" `
  --runtime-url http://127.0.0.1:5276 `
  --dev-package ".\MyPackage\bin\Debug\net10.0\sunder-dev"
Attach debugger to Runtime Host
./Sunder.Runtime.Host \
  --wait-for-debugger \
  --urls http://127.0.0.1:5276
/Applications/Sunder.app/Contents/MacOS/Sunder.App \
  --runtime-url http://127.0.0.1:5276 \
  --dev-package ./MyPackage/bin/Debug/net10.0/sunder-dev
Attach debugger to Runtime Host
./Sunder.Runtime.Host \
  --wait-for-debugger \
  --urls http://127.0.0.1:5276
./Sunder.App \
  --runtime-url http://127.0.0.1:5276 \
  --dev-package ./MyPackage/bin/Debug/net10.0/sunder-dev

Observability

Logs and diagnostics.

Sunder separates app/session logs from package logs. App logs describe shell startup, shutdown, package icon loading, and app-side failures. Package logs live under each package storage root and are written through the SDK logging surface.

Sunder app logs

Current app session logs are written under the Sunder local app-data log root. On Windows this resolves under %LocalAppData%\Sunder\logs. App logging also writes toTrace, and logging exceptions are swallowed so logging cannot interrupt app startup or shutdown.

App log root
Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
    "Sunder",
    "logs")

Package logs

Package contexts create a package-specific storage root under local app data. Package event logs and ILogger runtime logs are retained for seven days by default and roll by date.

Package log root
Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
    "Sunder",
    "Packages",
    "my.company.package",
    "logs")
Syslog-style package log line
May 13 10:21:45 workstation my.company.package[1234]: level=info event=my.company.package.import.completed msg="Import completed." item_count=42 package_version=1.0.0 source=events source_name=inbox

Logging API

Use context.LoggerFactory for normal ILogger categories and context.Logging.Events for structured package events. Attribute keys are normalized, common secret-like fields are redacted, and exception fields are recorded when provided.

Runtime and event logging
using Microsoft.Extensions.Logging;
using Sunder.Sdk.Logging;

var logger = context.LoggerFactory.CreateLogger("my.company.package.import");
logger.LogInformation("Import started for {SourceName}", sourceName);

try
{
    await importer.RunAsync(cancellationToken);
    await context.Logging.Events.InformationAsync(
        "my.company.package.import.completed",
        "Import completed.",
        new Dictionary<string, object?>
        {
            ["source.name"] = sourceName,
            ["item.count"] = itemCount,
        },
        cancellationToken);
}
catch (Exception ex)
{
    await context.Logging.Events.ErrorAsync(
        "my.company.package.import.failed",
        "Import failed.",
        new Dictionary<string, object?> { ["source.name"] = sourceName },
        ex,
        cancellationToken);
}

Standard

Package standard and archive validation.

Identity rules

  • Use lowercase dot-separated ASCII package ids.
  • Use ASCII lowercase letters, digits, and dots.
  • Do not use spaces, underscores, or display-name casing.
  • Do not rename a package id after publishing.
  • Use SemVer-compatible versions such as 1.0.0 or 1.2.0-beta.1.
  • Use summary for a concise one-sentence package description.

Generated manifest and module discovery

Generated manifest shape
{
  "manifestVersion": 1,
  "id": "my.company.package",
  "name": "My Package",
  "summary": "Adds a custom Sunder workspace.",
  "version": "1.0.0",
  "entryAssembly": "MyPackage.dll",
  "icon": "assets/icon.png",
  "dependsOn": [
    {
      "packageId": "my.company.host",
      "versionRange": ">=1.0.0 <2.0.0"
    }
  ],
  "sdkApiVersion": 1,
  "sdkPackageVersion": "1.0.0",
  "requiredSdkCapabilities": ["core.v1", "packaging.v1", "contributions.v1", "views.v1"],
  "sdkVersion": "1.0.0",
  "targetFramework": "net10.0"
}
  • The package entry assembly is loaded from package lib output.
  • The runtime finds public, non-abstract types implementing ISunderPackageModule.
  • Exactly one package module type must be discoverable.
  • The module type must have a public parameterless constructor.
  • Zero or multiple module types fail validation or activation.

Icons and assets

Package icon paths are relative package asset paths. Source files under Assets/** are copied to output under assets/**, runtime asset URLs are served through Runtime Host, and Registry package icons are extracted from package artifacts during publish.

Recommended icon setup
MyPackage/
  Assets/
    icon.png
  PackageMetadata.cs

[assembly: SunderPackage(
    Id = "my.company.package",
    Name = "My Package",
    Icon = "assets/icon.png")]
RuleMeaning
Relative paths onlyIcon paths must not be rooted and must not contain parent directory traversal.
Build-time existenceThe declared icon file must exist when the package is built.
Runtime serving/api/packages/{packageId}/assets/{assetPath} serves active package assets.
Image supportBMP, GIF, ICO, JPG/JPEG, PNG, SVG/SVGZ, and WebP are recognized by runtime and Registry paths.
Fallback behaviorThe app falls back to the first character of the package name if icon loading fails.

Archive layout

.sunderpkg layout
.sunderpkg
  manifest/
    sunder-package.json
    content-index.json
  payload/
    lib/
    assets/

Validation rules

Current validation rejects unsafe archive paths, missing manifest or index files, missing entry assemblies, missing icons, hash mismatches, size mismatches, duplicate indexed paths, and unindexed files.

CLI

Command-line reference for package authors.

Sunder.Cli provides Registry discovery and publishing plus local Runtime Host package install and update operations. Production defaults point to https://hub.sunderapp.io/and http://127.0.0.1:5275/ for Runtime Host.

Configuration and discovery

Discovery and runtime config
sunder --help
sunder search my.company
sunder info my.company.package
sunder list --runtime-url http://127.0.0.1:5275/

Debug builds load development settings by default and point Registry API/web URLs athttp://localhost:5288/. Production defaults point to the public Registry.

NameMeaning
--registry-api-url <url>Registry API URL.
--registry-web-url <url>Registry web URL used by browser auth.
--registry-url <url>Back-compatible alias that sets both Registry URLs.
--runtime-url <url>Local Runtime Host URL.
--timeout <duration>Registry request timeout. Accepts values like 15m, 900s, 900, or 00:15:00.
SUNDER_REGISTRY_API_URLRegistry API URL environment override.
SUNDER_REGISTRY_WEB_URLRegistry web URL environment override.
SUNDER_REGISTRY_URLLegacy alias that sets both Registry URLs.
SUNDER_RUNTIME_URLLocal Runtime Host URL environment override.
SUNDER_REGISTRY_TOKENBearer token for authenticated publish and package management.
SUNDER_ENVIRONMENTCLI environment selection override.

Authentication commands

Authenticated commands resolve tokens in this order: --token,SUNDER_REGISTRY_TOKEN, then the token saved by sunder auth login.

Auth state
sunder auth login
sunder auth status
sunder auth logout

Runtime commands

Install and update
sunder install my.company.package
sunder install my.company.package --version 1.0.0
sunder install --file .\MyPackage.1.0.0.sunderpkg
sunder install --file .\MyPackage.1.0.0.sunderpkg --allow-downgrade --reinstall
sunder update my.company.package
sunder update --all --include-prerelease
Install and update
sunder install my.company.package
sunder install my.company.package --version 1.0.0
sunder install --file ./MyPackage.1.0.0.sunderpkg
sunder install --file ./MyPackage.1.0.0.sunderpkg --allow-downgrade --reinstall
sunder update my.company.package
sunder update --all --include-prerelease
Install and update
sunder install my.company.package
sunder install my.company.package --version 1.0.0
sunder install --file ./MyPackage.1.0.0.sunderpkg
sunder install --file ./MyPackage.1.0.0.sunderpkg --allow-downgrade --reinstall
sunder update my.company.package
sunder update --all --include-prerelease

Publishing commands

Validate, publish, and tag
sunder auth login
sunder package validate .\MyPackage.1.0.0.sunderpkg
sunder validate .\MyPackage.1.0.0.sunderpkg
sunder publish --file `
  .\MyPackage.1.0.0.sunderpkg
sunder publish --file `
  .\MyPackage.1.0.0.sunderpkg `
  --no-latest
sunder publish --file `
  .\MyPackage.1.0.0.sunderpkg `
  --token $env:SUNDER_REGISTRY_TOKEN `
  --timeout 30m
sunder dist-tag set my.company.package latest 1.0.0
Validate, publish, and tag
sunder auth login
sunder package validate ./MyPackage.1.0.0.sunderpkg
sunder validate ./MyPackage.1.0.0.sunderpkg
sunder publish --file \
  ./MyPackage.1.0.0.sunderpkg
sunder publish --file \
  ./MyPackage.1.0.0.sunderpkg \
  --no-latest
sunder publish --file \
  ./MyPackage.1.0.0.sunderpkg \
  --token $SUNDER_REGISTRY_TOKEN \
  --timeout 30m
sunder dist-tag set my.company.package latest 1.0.0
Validate, publish, and tag
sunder auth login
sunder package validate ./MyPackage.1.0.0.sunderpkg
sunder validate ./MyPackage.1.0.0.sunderpkg
sunder publish --file \
  ./MyPackage.1.0.0.sunderpkg
sunder publish --file \
  ./MyPackage.1.0.0.sunderpkg \
  --no-latest
sunder publish --file \
  ./MyPackage.1.0.0.sunderpkg \
  --token $SUNDER_REGISTRY_TOKEN \
  --timeout 30m
sunder dist-tag set my.company.package latest 1.0.0
CommandPurpose
sunder auth loginSign in through browser auth before publishing or managing package versions.
sunder auth statusShow saved authentication state for the configured Registry.
sunder auth logoutRemove saved authentication for the configured Registry.
sunder search <query>Search public Registry packages anonymously.
sunder info <package-id>Show package details and optionally a specific version.
sunder listList packages installed in the local runtime.
sunder install <package-id>Install a Registry package by dist tag or exact version.
sunder install --file <archive>Install or update from a local .sunderpkg archive.
sunder update <package-id>Update one installed package.
sunder update --allUpdate all installed packages with available updates.
sunder package validate <archive>Validate archive structure, manifest, content index, hashes, paths, and files.
sunder validate <archive>Top-level alias for package archive validation.
sunder publish --file <archive>Publish a validated archive to the configured Registry.
sunder dist-tag set <id> <tag> <version>Move or create a Registry dist tag.
sunder dist-tag list <id>List Registry dist tags for a package.
sunder dist-tag delete <id> <tag>Delete a Registry dist tag.
sunder yank <id> <version>Mark a published version as yanked when package management auth is available.
sunder unyank <id> <version>Remove a yank marker from a published version.
sunder deprecate <id> <version>Attach a deprecation message to a published version.
sunder undeprecate <id> <version>Clear a version deprecation message.

Lifecycle commands

Yank, unyank, deprecate, undeprecate, and dist-tag mutations require package management auth. Listing dist tags and public package discovery are anonymous.

Package lifecycle commands
sunder yank my.company.package 1.0.0
sunder unyank my.company.package 1.0.0
sunder deprecate my.company.package 1.0.0 --message "Use 1.1.0 instead."
sunder undeprecate my.company.package 1.0.0
sunder dist-tag list my.company.package
sunder dist-tag delete my.company.package beta

Registry

Registry publishing and package lifecycle.

The Registry is the remote package catalog and distribution system. It stores package metadata, immutable versions, package artifacts, extracted icon media, profile media, ownership permissions, package stats, dependents, and dist tags. It does not own local installed package state.

Discovery

Public package search, details, versions, version details, icons, artifact downloads, dist-tag resolution, update resolution, and dependency-aware install plans are anonymous. Runtime Host downloads and applies package artifacts locally after Registry planning.

Publish

The CLI validates the archive before upload. Authenticated publish requires browser sign-in,SUNDER_REGISTRY_TOKEN, or --token. The Registry rejects duplicate package versions, stores immutable artifacts, extracts valid package icons, records artifact hashes and sizes, and optionally creates or promotes latest.

Profiles, media, and stats

Public package details can include profile metadata, profile media, stats, dependents, and view/download counts. Public profile and media endpoints serve package profile content, while package version icon endpoints serve icons extracted from published archives.

Dist tags

Dist tags point names such as latest at immutable package versions. Use--no-latest when publishing without setting or promoting latest.

Ownership and local Registry development

Package ownership is enforced for non-development publish. Authenticated owner and maintainer workflows cover package deletion, dist tags, yanking, deprecation, package profile updates, profile media upload/delete, current-user package dashboards, and maintainer add/remove/list. Use --dev-local only against a Registry server running in Development mode.

Local development publish
sunder publish --file `
  .\MyPackage.1.0.0.sunderpkg `
  --dev-local `
  --registry-url http://localhost:5288/
Local development publish
sunder publish --file \
  ./MyPackage.1.0.0.sunderpkg \
  --dev-local \
  --registry-url http://localhost:5288/
Local development publish
sunder publish --file \
  ./MyPackage.1.0.0.sunderpkg \
  --dev-local \
  --registry-url http://localhost:5288/

Compatibility

Host, SDK, and package compatibility.

Runtime Host validates SDK compatibility before loading a package assembly because packages use the Host-bundled Sunder.Sdk.dll. Existing shipped public SDK contracts should not be broken in place; new behavior is added through new contracts and capability names.

Version fields

FieldMeaning
manifestVersionPackage manifest/archive schema version. This is not the SDK API version.
sdkApiVersionBroad SDK activation generation. Current value is 1.
sdkPackageVersionInformational Sunder.Sdk package/build version used by Sunder.Package.Build.
requiredSdkCapabilitiesGranular Host-required SDK features inferred from SDK contract usage.
sdkVersionOptional SDK version metadata when supplied by build properties. Compatibility decisions use sdkApiVersion and requiredSdkCapabilities.

Current capabilities

CapabilitySDK surface
core.v1ISunderPackageModule, IPackageContext
packaging.v1Package identity and dependency attributes
contributions.v1IPackageContributionRegistry
views.v1Package view registration and placement
settings-views.v1Settings view registration
settings-navigation.v1Package settings navigation service
workspaces.v1Package view and workspace factories
background-services.v1Package background services
background-processes.v1Queued background process API, progress reporting, cancellation, and indicator placement
extensions.v1Extension points, registration, and catalog queries
extensions.changes.v1Extension catalog change monitoring
configuration.schema.v1Package configuration schema contracts
configuration.values.v1Package configuration value access
storage.v1Package storage abstractions
secrets.v1Package secret storage
logging.v1Package logging abstractions
notifications.v1Package notifications
shell-view.v1Shell view, hotbar, and navigation services
package-sessions.v1Package session load, unload, and status service
callbacks.v1Generic callback sessions
auth.v1Auth status and disconnect integration
theming.v1Semantic Sunder theme keys

Sunder.Package.Build infers required capabilities by scanning compiled package SDK usage. Manual MSBuild entries are reserved for unusual dynamic or reflection scenarios.

Future breaking-change process

  1. Add a new contract or namespace for the changed area.
  2. Add a new capability constant and annotate the new contract surface.
  3. Keep the previous capability working where practical.
  4. Update the Host compatibility profile to declare support.
  5. Let Sunder.Package.Build infer the new capability from SDK usage.
  6. Deprecate old capabilities through docs and warnings before removal.

Fixes

Troubleshooting package development.

Runtime not reachable

Commands such as sunder list, sunder install, andsunder update require Runtime Host. Start Sunder App or pass--runtime-url / SUNDER_RUNTIME_URL when using a custom host URL.

Validation failures

Check package id format, SemVer version, manifest presence, entry assembly, content-index hashes, icon path, duplicate paths, unsafe paths, and unindexed files.

Auth and publish failures

Run sunder auth login, set SUNDER_REGISTRY_TOKEN, pass--token, or use --dev-local only against a development Registry endpoint.

Build output locks

If Sunder App, Runtime Host, or a local Registry server is running on Windows, normal build outputs can be locked. Build affected projects to alternate output and intermediate paths.

Alternate output paths
dotnet build `
  .\src\Host\Sunder.App\Sunder.App.csproj `
  --no-restore `
  -p:OutputPath=.\artifacts\tmp\sunder-app\bin\ `
  -p:IntermediateOutputPath=.\artifacts\tmp\sunder-app\obj
Alternate output paths
dotnet build \
  ./src/Host/Sunder.App/Sunder.App.csproj \
  --no-restore \
  -p:OutputPath=./artifacts/tmp/sunder-app/bin/ \
  -p:IntermediateOutputPath=./artifacts/tmp/sunder-app/obj/
Alternate output paths
dotnet build \
  ./src/Host/Sunder.App/Sunder.App.csproj \
  --no-restore \
  -p:OutputPath=./artifacts/tmp/sunder-app/bin/ \
  -p:IntermediateOutputPath=./artifacts/tmp/sunder-app/obj/