feat(plugins): punktfunk-host plugins CLI — add/remove/list/enable/disable/status

One-liner plugin management replacing the manual scripting-dir + bunfig +
bun-add ritual: package ops forward to the bun runner (new sdk plugins
module + runner-cli subcommands, 11 tests green), enable/disable/status
drive the systemd unit on Linux and the PunktfunkScripting scheduled task
on Windows (installer support in the ISS). Docs page rewritten as .mdx
with per-platform Tabs (registered in mdx.tsx).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 16:34:19 +02:00
parent f250db96f4
commit f0c511c8fa
11 changed files with 1034 additions and 105 deletions
+66
View File
@@ -128,6 +128,10 @@ UninstallDisplayName=punktfunk host {#MyAppVersion}
; "Punktfunk Host" FileDescription (build.rs winresource) for Task Manager/Explorer; the file
; copy stays as the uninstall-entry icon.
UninstallDisplayIcon={app}\punktfunk.ico
; {app} goes on the machine PATH (see [Registry] + PathNeedsAdd/RemoveAppFromPath below) so the
; documented one-liners — `punktfunk-host plugins add playnite` — work by name from an elevated
; prompt. Broadcasts WM_SETTINGCHANGE so already-open shells pick it up after a restart.
ChangesEnvironment=yes
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
@@ -236,6 +240,14 @@ Source: "{#VkLayerDir}\pf_vkhdr_layer.json"; DestDir: "{app}\vklayer"; Flags: ig
; with the app). Operators who moved --mgmt-bind can append --mgmt-addr/--mgmt-port here.
Root: HKLM64; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; ValueType: string; \
ValueName: "PunktfunkTray"; ValueData: """{app}\punktfunk-tray.exe"""; Flags: uninsdeletevalue; Tasks: trayicon
; Put {app} on the MACHINE PATH so `punktfunk-host plugins add …` / `punktfunk-host service …` are
; runnable by name. Appended to the existing value ({olddata}) and guarded by PathNeedsAdd so a
; repair/upgrade never appends a duplicate. Deliberately NOT `uninsdeletevalue` — that would delete
; the whole Path value; the uninstaller surgically removes just our entry (RemoveAppFromPath).
; expandsz preserves the %SystemRoot%-style entries other software puts here.
Root: HKLM64; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \
ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"; \
Check: PathNeedsAdd(ExpandConstant('{app}'))
#ifdef WithVkLayer
; Register the HDR Vulkan implicit layer system-wide. The 64-bit Vulkan loader reads
; HKLM64\SOFTWARE\Khronos\Vulkan\ImplicitLayers; the value NAME is the manifest path and the DWORD
@@ -518,6 +530,60 @@ begin
end;
#endif
const
EnvKey = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment';
{ Is the install dir missing from the machine PATH? Guards the [Registry] append so a repair or
upgrade can't add a second copy. Compared case-insensitively and semicolon-delimited so a path
that merely CONTAINS ours as a substring (...\punktfunk-old) doesn't count as a match.
NOTE: never write a braced Inno constant inside a Pascal comment - these comments do NOT nest,
so its closing brace ends the comment early and the rest of the line is parsed as code. }
function PathNeedsAdd(Param: String): Boolean;
var
OrigPath: String;
begin
if not RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvKey, 'Path', OrigPath) then
begin
Result := True; { no Path value at all - the append creates it }
exit;
end;
Result := Pos(';' + Uppercase(Param) + ';', ';' + Uppercase(OrigPath) + ';') = 0;
end;
{ Remove exactly our install-dir entry from the machine PATH on uninstall, leaving every other
entry (and their order) intact. Rebuilds the value entry-by-entry rather than doing a substring
delete, so a partial match can never corrupt a neighbouring path. }
procedure RemoveAppFromPath;
var
OrigPath, NewPath, Entry: String;
Target: String;
P: Integer;
begin
if not RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvKey, 'Path', OrigPath) then
exit;
Target := Uppercase(ExpandConstant('{app}'));
NewPath := '';
{ Walk the semicolon-delimited list, copying through everything that isn't ours. }
OrigPath := OrigPath + ';';
repeat
P := Pos(';', OrigPath);
Entry := Trim(Copy(OrigPath, 1, P - 1));
OrigPath := Copy(OrigPath, P + 1, Length(OrigPath));
if (Entry <> '') and (Uppercase(Entry) <> Target) then
begin
if NewPath <> '' then NewPath := NewPath + ';';
NewPath := NewPath + Entry;
end;
until OrigPath = '';
RegWriteExpandStringValue(HKEY_LOCAL_MACHINE, EnvKey, 'Path', NewPath);
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
if CurUninstallStep = usPostUninstall then
RemoveAppFromPath;
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssInstall then