Template
Install Sunder.Package.Templates.
Developer documentation
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
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.
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.
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.csprojdotnet new install Sunder.Package.Templates
dotnet new sunder-package \
--name MyPackage \
--packageId my.company.package \
--packageName "My Package"
cd ./MyPackage
dotnet build ./MyPackage/MyPackage.csprojdotnet new install Sunder.Package.Templates
dotnet new sunder-package \
--name MyPackage \
--packageId my.company.package \
--packageName "My Package"
cd ./MyPackage
dotnet build ./MyPackage/MyPackage.csprojdotnet build emits an unpacked sunder-dev folder next to the compiled output. Load that folder into Sunder App with --dev-package.
& "C:\Path\To\Sunder.App.exe" `
--dev-package ".\MyPackage\bin\Debug\net10.0\sunder-dev"/Applications/Sunder.app/Contents/MacOS/Sunder.App \
--dev-package ./MyPackage/bin/Debug/net10.0/sunder-dev./Sunder.App \
--dev-package ./MyPackage/bin/Debug/net10.0/sunder-devdotnet publish emits a .sunderpkg archive. Validate it before installing locally or publishing to the Registry.
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.sunderpkgdotnet 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.sunderpkgdotnet 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.sunderpkgInstall Sunder.Package.Templates.
Create a package with a stable package id.
Emit sunder-dev with dotnet build.
Start Sunder App with --dev-package.
Validate and publish the generated .sunderpkg.
Model
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.
| Area | Owns |
|---|---|
| Sunder App | Desktop shell UI, package view activation, settings views, view caching, shell notifications, app-side faults, theme resources, and app branding. |
| Runtime Host | Installed package records, package archive validation, graph activation, runtime services, package configuration, secrets, callbacks, auth, faults, and package asset serving. |
Avalonia desktop shell that loads package UI contributions and presents package management UX.
Local process that owns installed package state, validation, activation, configuration, secrets, faults, and package assets.
Remote catalog and distribution infrastructure for package metadata, immutable versions, artifacts, icons, ownership, search, and dist tags.
The only installable runtime extension unit. Packages can add views, settings, services, tools, integrations, and extension contributions.
Unpacked sunder-dev output emitted by dotnet build for local package development.
Zip-based distributable archive emitted by dotnet publish and validated before install or update.
A NuGet package that exposes typed extension contracts. It is not a Sunder runtime package.
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
Sunder.Package.Templates installed as a dotnet new template package.--dev-package loading.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.0dotnet 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.0dotnet 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| Option | Meaning |
|---|---|
--packageId <id> | Required runtime package id written into generated metadata. |
--packageName <name> | Required user-facing package name written into metadata and starter view. |
--withContracts | Adds a sibling contracts project for public extension points. |
--createInPlace | Creates package files directly in the specified output folder instead of under a child project folder. |
--noDefaultView | Omits the default shell-visible package view. |
--withHostDependency | Adds runtime dependency metadata for another package. |
--hostPackageId <id> | Runtime package id required with --withHostDependency. |
--withHostContracts | Adds 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. |
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.
<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>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.
using Sunder.Sdk.Packaging;
[assembly: SunderPackage(
Id = "my.company.package",
Name = "My Package",
Summary = "Adds a custom Sunder workspace.",
Icon = "assets/icon.png")]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 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.
| Area | Primary types | Provides |
|---|---|---|
| Packaging metadata | SunderPackageAttribute, SunderPackageDependencyAttribute | Package identity and runtime package dependencies. |
| Module lifecycle | ISunderPackageModule | Package startup, service registration, and contribution registration. |
| Package context | IPackageContext | Package id, version, install path, storage, configuration, secrets, and logging. |
| Contributions | IPackageContributionRegistry | Views, settings views, background services, extensions, and configuration schemas. |
| Views | PackageViewRegistration, PackageViewPlacement | Shell-visible Avalonia package views. |
| Workspaces | IPackageWorkspaceFactory | Factory-created package workspaces and views. |
| Extensions | PackageExtensionPoint<T>, IPackageExtensionCatalog | Typed package-to-package contribution points and active contribution discovery. |
| Extension changes | IPackageExtensionCatalogMonitor | Structured change events when packages activate, deactivate, update, or fault. |
| Configuration | PackageConfigurationSchema, IPackageConfiguration | Host-rendered settings schemas and package configuration values. |
| Storage | IPackageStorageContext, IPackageFileStore, IPackageKeyValueStore | Package-scoped mutable files and key-value state. |
| Secrets | IPackageSecrets | Package-scoped secret values. |
| Logging | IPackageLogging, IPackageEventLogger | Package event logging and ILoggerFactory access. |
| Notifications | IPackageNotificationService | User-visible package notifications. |
| Background processes | IBackgroundProcessQueue, BackgroundProcessRequest | Host-visible queued work with progress, cancellation, and indicator placement. |
| Shell integration | IPackageShellViewService, IPackageViewNavigationTarget | Shell navigation and hotbar/workspace integration. |
| Settings navigation | IPackageSettingsNavigationService | Open global settings or another package's settings when the host supports it. |
| Package sessions | IPackageSessionService | Load, unload, and query installed or dev package sessions when the host supports it. |
| Callbacks and auth | IPackageCallbackHandler, IPackageAuthHandler | Local callback sessions plus auth status and disconnect integration. |
| Theme resources | SunderThemeKeys | Semantic resource keys for package UI. |
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.
| Member | Use |
|---|---|
PackageId | Stable runtime package id. |
Version | Active package version parsed from package metadata. |
InstallPath | Read-only installed or dev package content root. |
Storage | Package-scoped data, cache, logs, files, and key-value state. |
Configuration | Current host-rendered configuration values. |
Secrets | Package-scoped secret values. |
LoggerFactory | Microsoft.Extensions.Logging factory for runtime logs. |
Logging | Structured package event logger and logger factory. |
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);
}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.
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>());
}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.
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));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;
}
}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.
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")
])
])
]));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);
}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.
| Surface | Use |
|---|---|
Storage.DataRootPath | Package data root. |
Storage.CacheRootPath | Rebuildable cache root. |
Storage.LogsRootPath | Package log root used by package logging. |
Storage.Files.GetPath(...) | Safe package file-store paths; rejects rooted paths and parent traversal. |
Storage.State | String key-value state with sync read and async write/list/delete APIs. |
Secrets | String secret values through get/set/delete APIs. |
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);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.
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);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.
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;
}
}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.
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));
}
}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.
<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
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.
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")]sunder-package.json.Sunder.Sdk only when they need SDK extension-point types.Hosts that support live activation implement IPackageExtensionCatalogMonitor. Use it when open UI or cached state depends on extensions from other packages.
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>());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
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.
MyPackage/bin/Debug/net10.0/sunder-dev/
sunder-package.json
lib/
MyPackage.dll
MyPackage.pdb
MyPackage.deps.json
MyPackage.runtimeconfig.json
assets/
icon.pngdotnet msbuild .\MyPackage\MyPackage.csproj `
-t:PackSunderPackage `
-p:Configuration=Release `
-p:SunderPackageOutputPath=.\artifacts\packages\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 variable | Meaning |
|---|---|
--dev-package <folder> | Loads an unpacked sunder-dev package folder for local development. |
--watch | Watches 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_URL | Environment override for the app and CLI runtime URL. |
SUNDER_RUNTIME_HOST_PATH | Environment override for the Runtime Host executable or folder used by the app. |
& "C:\Path\To\Sunder.App.exe" `
--dev-package ".\HostPackage\bin\Debug\net10.0\sunder-dev" `
--dev-package ".\ExtensionPackage\bin\Debug\net10.0\sunder-dev"/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./Sunder.App \
--dev-package ./HostPackage/bin/Debug/net10.0/sunder-dev \
--dev-package ./ExtensionPackage/bin/Debug/net10.0/sunder-devPackages 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.
$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:5276Use 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.
& "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"./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./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-devObservability
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.
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.
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Sunder",
"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.
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Sunder",
"Packages",
"my.company.package",
"logs")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=inboxUse 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.
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
1.0.0 or 1.2.0-beta.1.summary for a concise one-sentence package description.{
"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"
}lib output.ISunderPackageModule.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.
MyPackage/
Assets/
icon.png
PackageMetadata.cs
[assembly: SunderPackage(
Id = "my.company.package",
Name = "My Package",
Icon = "assets/icon.png")]| Rule | Meaning |
|---|---|
| Relative paths only | Icon paths must not be rooted and must not contain parent directory traversal. |
| Build-time existence | The declared icon file must exist when the package is built. |
| Runtime serving | /api/packages/{packageId}/assets/{assetPath} serves active package assets. |
| Image support | BMP, GIF, ICO, JPG/JPEG, PNG, SVG/SVGZ, and WebP are recognized by runtime and Registry paths. |
| Fallback behavior | The app falls back to the first character of the package name if icon loading fails. |
.sunderpkg
manifest/
sunder-package.json
content-index.json
payload/
lib/
assets/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
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.
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.
| Name | Meaning |
|---|---|
--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_URL | Registry API URL environment override. |
SUNDER_REGISTRY_WEB_URL | Registry web URL environment override. |
SUNDER_REGISTRY_URL | Legacy alias that sets both Registry URLs. |
SUNDER_RUNTIME_URL | Local Runtime Host URL environment override. |
SUNDER_REGISTRY_TOKEN | Bearer token for authenticated publish and package management. |
SUNDER_ENVIRONMENT | CLI environment selection override. |
Authenticated commands resolve tokens in this order: --token,SUNDER_REGISTRY_TOKEN, then the token saved by sunder auth login.
sunder auth login
sunder auth status
sunder auth logoutsunder 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-prereleasesunder 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-prereleasesunder 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-prereleasesunder 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.0sunder 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.0sunder 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| Command | Purpose |
|---|---|
sunder auth login | Sign in through browser auth before publishing or managing package versions. |
sunder auth status | Show saved authentication state for the configured Registry. |
sunder auth logout | Remove 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 list | List 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 --all | Update 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. |
Yank, unyank, deprecate, undeprecate, and dist-tag mutations require package management auth. Listing dist tags and public package discovery are anonymous.
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 betaRegistry
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.
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.
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.
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 point names such as latest at immutable package versions. Use--no-latest when publishing without setting or promoting latest.
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.
sunder publish --file `
.\MyPackage.1.0.0.sunderpkg `
--dev-local `
--registry-url http://localhost:5288/sunder publish --file \
./MyPackage.1.0.0.sunderpkg \
--dev-local \
--registry-url http://localhost:5288/sunder publish --file \
./MyPackage.1.0.0.sunderpkg \
--dev-local \
--registry-url http://localhost:5288/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.
| Field | Meaning |
|---|---|
manifestVersion | Package manifest/archive schema version. This is not the SDK API version. |
sdkApiVersion | Broad SDK activation generation. Current value is 1. |
sdkPackageVersion | Informational Sunder.Sdk package/build version used by Sunder.Package.Build. |
requiredSdkCapabilities | Granular Host-required SDK features inferred from SDK contract usage. |
sdkVersion | Optional SDK version metadata when supplied by build properties. Compatibility decisions use sdkApiVersion and requiredSdkCapabilities. |
| Capability | SDK surface |
|---|---|
core.v1 | ISunderPackageModule, IPackageContext |
packaging.v1 | Package identity and dependency attributes |
contributions.v1 | IPackageContributionRegistry |
views.v1 | Package view registration and placement |
settings-views.v1 | Settings view registration |
settings-navigation.v1 | Package settings navigation service |
workspaces.v1 | Package view and workspace factories |
background-services.v1 | Package background services |
background-processes.v1 | Queued background process API, progress reporting, cancellation, and indicator placement |
extensions.v1 | Extension points, registration, and catalog queries |
extensions.changes.v1 | Extension catalog change monitoring |
configuration.schema.v1 | Package configuration schema contracts |
configuration.values.v1 | Package configuration value access |
storage.v1 | Package storage abstractions |
secrets.v1 | Package secret storage |
logging.v1 | Package logging abstractions |
notifications.v1 | Package notifications |
shell-view.v1 | Shell view, hotbar, and navigation services |
package-sessions.v1 | Package session load, unload, and status service |
callbacks.v1 | Generic callback sessions |
auth.v1 | Auth status and disconnect integration |
theming.v1 | Semantic 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.
Sunder.Package.Build infer the new capability from SDK usage.Fixes
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.
Check package id format, SemVer version, manifest presence, entry assembly, content-index hashes, icon path, duplicate paths, unsafe paths, and unindexed files.
Run sunder auth login, set SUNDER_REGISTRY_TOKEN, pass--token, or use --dev-local only against a development Registry endpoint.
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.
dotnet build `
.\src\Host\Sunder.App\Sunder.App.csproj `
--no-restore `
-p:OutputPath=.\artifacts\tmp\sunder-app\bin\ `
-p:IntermediateOutputPath=.\artifacts\tmp\sunder-app\objdotnet build \
./src/Host/Sunder.App/Sunder.App.csproj \
--no-restore \
-p:OutputPath=./artifacts/tmp/sunder-app/bin/ \
-p:IntermediateOutputPath=./artifacts/tmp/sunder-app/obj/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/