| name | appi-plugin-creator |
| description | Scaffold a complete Appi plugin (ISource + ResultItemBase implementations, csproj, optional localization) and wire it up for debugging or installation. Use when the user wants to create a new Appi plugin, add a new source (database, file, HTTP, or custom), or asks how to query a new data source through Appi. |
Appi Plugin Creator
Creates a working Appi plugin: a .NET class library exposing one or more ISource implementations whose results are ResultItemBase subclasses. Reference examples live in examples/ (ExternalDemoSource = minimal, FileDemo = base class + localization, MsSqlDemo/MySqlDemo/SqLiteDemo = SQL via Dapper).
Step 1 — Gather requirements
Determine (ask only if not inferable from the request):
- Plugin name — used for project name and class prefix (e.g.
Jira → JiraSource, JiraResult).
- Data backend — picks the base type:
- SQL Server → subclass
MsSqlSource<TDto> (see examples/MsSqlDemo)
- MySQL/MariaDB → subclass
MySqlSource<TDto> (see examples/MySqlDemo)
- SQLite → subclass
SqLiteSource<TDto> (see examples/SqLiteDemo)
- Text file → subclass
FileSource (see examples/FileDemo)
- Anything else (HTTP, AD, custom) → implement
ISource directly (see examples/ExternalDemoSource, examples/HttpRequestDemo)
- Location — inside this repo under
examples/ (project references, debuggable) or standalone (NuGet packages Appi.Core / Appi.Infrastructure). Default for work in this repo: under examples/.
Step 2 — Create the project
Create <Name>/<Name>.csproj. Critical conventions (copy from examples/FileDemo/FileDemo.csproj):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Appi.Plugin.$(MSBuildProjectName)</AssemblyName>
<Configurations>Debug;Release;Release-with-examples</Configurations>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<BaseOutputPath>..\..\src\Ui.Appi\bin</BaseOutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release-with-examples|AnyCPU'">
<BaseOutputPath>..\..\src\Ui.Appi\bin</BaseOutputPath>
<Optimize>True</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<BaseOutputPath>bin</BaseOutputPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Infrastructure\Infrastructure.csproj" />
</ItemGroup>
</Project>
- The
Appi.Plugin. assembly-name prefix and the BaseOutputPath redirect are what make Debug-build plugins auto-load — do not drop them for in-repo plugins.
- If placing under
examples/, also add the project to Appi.sln in the examples solution folder (dotnet sln add <path> --solution-folder examples).
Step 3 — Implement the source
The class must implement ISource (src/Core/Abstractions/ISource.cs) directly or via an Infrastructure base class. Rules that are not obvious from the interface:
TypeName must be globally unique — sources.json references sources by bare class name and the resolver (ReflectionHelper.cs) scans all loaded assemblies for the first match. Always set TypeName = typeof(MySource).Name and pick a distinctive class name.
- Initial property values become the
sources.json entry when the plugin is registered — set sensible defaults for Name, Alias (single word, lowercase), Description, IsActive, SortOrder, Groups. Path/Arguments are free-form (SQL sources use Arguments for the connection string; FileSource uses Path for the file).
- Constructor injection works via reflection: any service registered in Program.cs can be a constructor parameter (
IHandlerHelper, IStringLocalizer<>, ILogger<>, IOptions<Preferences>, ...). Constructors are tried in descending parameter count; unregistered services arrive as null.
- Respect
options.CaseSensitive and options.Query in ReadAsync; return an empty enumerable rather than throwing when there is nothing to search.
Minimal direct implementation (adapted from examples/ExternalDemoSource):
using Core.Abstractions;
using Core.Models;
namespace <Name>
{
public class <Name>Source : ISource
{
private readonly IHandlerHelper _handlerHelper;
public string TypeName { get; set; } = typeof(<Name>Source).Name;
public string Name { get; set; } = "<display name>";
public string Alias { get; set; } = "<alias>";
public string Description { get; set; } = "<description>";
public bool IsActive { get; set; } = true;
public int SortOrder { get; set; } = 50;
public string? Path { get; set; }
public string? Arguments { get; set; }
public bool? IsQueryCommand { get; set; } = true;
public string[]? Groups { get; set; } = System.Array.Empty<string>();
public <Name>Source(IHandlerHelper handlerHelper)
{
_handlerHelper = handlerHelper ?? throw new ArgumentNullException(nameof(handlerHelper));
}
public async Task<IEnumerable<ResultItemBase>> ReadAsync(FindItemsOptions options)
{
}
}
}
SQL variant: subclass the matching *Source<TDto>, override GetSqlQuery(FindItemsOptions) returning a Dapper CommandDefinition (parameterize the query, use EncodeForLike for LIKE patterns) and Parse(TDto) — see examples/MsSqlDemo/UserMsSqlSource.cs. Leave Arguments = "INSERT_CONNECTIONSTRING_HERE" as default so users know to fill it in sources.json.
Step 4 — Implement the result item
Subclass ResultItemBase (SqlResultBase<TDto> for SQL). Contract:
ToString() — the line shown in the result list (use padding like $"{Name,-30}{Description,-35}" for columns).
GetActions() — actions offered after selection. Always include _handlerHelper.Back() and _handlerHelper.Exit(); add custom ActionItem { Name, Action } entries between them. ClipboardService/ProcessService (Infrastructure) cover copy-to-clipboard and open-URL/call actions.
[DetailViewColumn] on public properties defines the detail table shown for a selected item ([DetailViewColumn<T>] converts the value). Escape user data rendered by Spectre with _handlerHelper.EscapeMarkup(...).
See examples/MsSqlDemo/UserDatabaseMsSqlResult.cs for a complete example.
Step 5 — Localization (only if requested)
Follow examples/FileDemo: add [assembly: RootNamespace("<ProjectName>")], create Localization/<FullNamespace>.<ClassName>.<culture>.resx (e.g. Localization/<Name>.<Name>Source.de.resx with PublicResXFileCodeGenerator), inject IStringLocalizer<<Name>Source>. English strings are the inline defaults; resx files supply translations.
Step 6 — Build, register, verify
In-repo (Debug) flow:
dotnet build — DLL lands in src/Ui.Appi/bin/Debug/net10.0/ as Appi.Plugin.<Name>.dll and auto-loads.
- Register it in
sources.json without copying: dotnet run --project src/Ui.Appi -- config register-lib <full path to the built DLL> --register-only
- External plugins must be allowed once:
dotnet run --project src/Ui.Appi -- config allow-libs true (Windows: stored in registry HKCU\SOFTWARE\Appi).
Standalone flow: appi config register-lib "<path>.dll" (copies to %AppData%\Appi and appends to sources.json); use --copy-only when updating an already-registered plugin.
Verify:
dotnet run --project src/Ui.Appi -- list → the new source appears with its alias.
dotnet run --project src/Ui.Appi -- <term> -s <alias> → returns results; select one to check the detail view and actions.
- If the source is missing: check
sources.json in %AppData%\Appi for the TypeName entry, that allow-libs is true, and that the DLL name matches Appi.Plugin.*.dll (exe dir) or sits in %AppData%\Appi (any *.dll).
- Note for SQL sources: the query fails until the connection string is set in
sources.json (appi config open).