Summary
Add a Phantom.Workspaces.exe update command-line verb that checks for and applies an update from the CLI (no GUI required), and document it — alongside every other command-line verb — in the /? help output. Today there is no CLI verb to trigger an update, and /? opens a GUI window whose text does not enumerate the management verbs.
Root Cause / Current State
The app has two disjoint command-line parsers, both flag-oriented, and no CLI-facing update entry point:
-
GUI-side (features/Phantom.Workspaces/CommandLineOptions.cs):
IsHelpRequested (L26) recognizes /?, -?, /h, -h, /help, --help.
TryGetConfigurationFilePath (L46) treats the first non-help positional token as a configuration-file path — so a positional update verb would be misinterpreted as a config file.
GetHelpText() (L69-87) only documents the config-file arg and /?. It does NOT list --install, --apply-update, --uninstall, --startup, --minimized, --silent, --install-root, etc.
-
Install-side (features/Phantom.Workspaces.Install/CommandLineOptions.cs), used for headless dispatch:
Parse(params string[]) (L48) recognizes --install (L64), --startup (L72), --minimized (L80), --uninstall (L88), --apply-update <dir> (L96), --help/-h (L110), and modifiers --silent (L119), --relaunch (L123), --purge (L127), --install-root <path> (L131).
- Unknown tokens produce
Invalid(...) with ExitCode.BadArguments (L141).
LaunchMode (features/Phantom.Workspaces.Install/LaunchMode.cs L7-29): Gui, Install, Startup, Minimized, ApplyUpdate, Uninstall, Help — no Update.
-
Dispatch:
features/Phantom.Workspaces/Program.cs Main (L22): L28 calls ManagementModeDispatcher.TryRun(args) (exits headlessly if it returns a code); L39 otherwise handles the help flag and falls through to Avalonia.
features/Phantom.Workspaces/ManagementModeDispatcher.cs: ManagementFlags = { "--install", "--apply-update", "--uninstall" } (L19) is the sentinel set that triggers headless dispatch. A new update verb MUST be added here or it falls through to the GUI. Parsing is delegated to the Install parser (L35); error text goes to Console.Error.WriteLine(options.Error) (L40). The dispatcher builds InstallLayout, RealFileSystem, SystemClock, RealProcessLauncher, StartupTaskService, HealthGate, ApplyUpdateRunner, ManagementModeRunner (L46-64) and calls runner.RunAsync(options, payloadDirectory, version) (L69).
features/Phantom.Workspaces.Install/ManagementModeRunner.cs: IsManagementMode (L42) is currently Install|ApplyUpdate|Uninstall; the RunAsync switch (L62) has RunInstall (L71), RunApplyUpdateAsync (L116, delegates to ApplyUpdateRunner), RunUninstall (L128).
-
Update services already exist in the same Phantom.Workspaces.Install assembly (natural reuse):
features/Phantom.Workspaces.Install/UpdateService.cs: CheckAsync (L60) → UpdateCheckResult; DownloadAndStageAsync(ReleaseInfo, ct) (L79) → staged version string; Apply(string version) (L116) repoints current.
- Reference pattern in the GUI:
features/Phantom.Workspaces/Services/Updates/UpdateController.cs DownloadInstallAndRelaunchAsync (L121-156) stages then spawns --apply-update <dir> --relaunch via IProcessLauncher and requests shutdown.
-
/? help is GUI-only: Program.cs L39 lets the help flag fall through to Avalonia; App.axaml.cs L218-226 opens new HelpWindow(); HelpWindow.axaml.cs L11 sets HelpText.Text = CommandLineOptions.GetHelpText(). A CLI user running Phantom.Workspaces.exe /? in a shell sees a GUI window, not console output. The app is WinExe (features/Phantom.Workspaces/Phantom.Workspaces.csproj L3 <OutputType>WinExe</OutputType>) with no AttachConsole/AllocConsole; console attach is explicitly forbidden by features/docs/design/build-and-installation.md:359. Existing headless output (e.g. --silent errors) relies on Console.Error.WriteLine, which works when launched from a parent shell that has bound stderr.
Affected Files
| File |
Location |
Role |
features/Phantom.Workspaces.Install/LaunchMode.cs |
L7-29 |
Add Update enum value. |
features/Phantom.Workspaces.Install/CommandLineOptions.cs |
L48 (Parse) |
Recognize positional update (and optionally --update) → LaunchMode.Update. |
features/Phantom.Workspaces/CommandLineOptions.cs |
L46 (TryGetConfigurationFilePath), L69-87 (GetHelpText) |
Guard update so it is not treated as a config-file path; enumerate all verbs in help text. |
features/Phantom.Workspaces/ManagementModeDispatcher.cs |
L19 (ManagementFlags), L35-69 |
Add the new verb to the sentinel set; construct/inject UpdateService. |
features/Phantom.Workspaces.Install/ManagementModeRunner.cs |
L42 (IsManagementMode), L62 (switch) |
Include Update in management modes; add RunUpdateAsync. |
features/Phantom.Workspaces.Install/UpdateService.cs |
L60, L79, L116 |
Reused by the new runner (CheckAsync, DownloadAndStageAsync, Apply). |
features/Phantom.Workspaces/Services/Updates/UpdateController.cs |
L121-156 |
Reference for stage-and-relaunch pattern. |
features/Phantom.Workspaces/App.axaml.cs |
L218-226 |
GUI HelpWindow path — text update flows via GetHelpText(). |
features/Phantom.Workspaces/HelpWindow.axaml.cs |
L11 |
Renders GetHelpText(). |
features/Phantom.Workspaces/Phantom.Workspaces.csproj |
L3 |
WinExe — informs the console-visibility decision for CLI /?. |
features/docs/design/build-and-installation.md |
L359 |
Existing prohibition against console attach — must be honored or explicitly revisited. |
Design / Fix
-
Enum: Add Update to LaunchMode (LaunchMode.cs).
-
Parser:
- In
Phantom.Workspaces.Install/CommandLineOptions.cs::Parse (L48+), recognize the positional token update (and, for convention consistency, optionally the flag form --update) → LaunchMode.Update. Preserve ExitCode.BadArguments for unknown tokens (L141).
- In
Phantom.Workspaces/CommandLineOptions.cs::TryGetConfigurationFilePath (L46), special-case update so it is NOT treated as a configuration-file path. (This is the critical positional-vs-flag conflict the maintainer must resolve; using a flag-only --update would sidestep it but the owner asked for update.)
-
Dispatch:
- Add
"update" (and "--update" if adopted) to ManagementModeDispatcher.ManagementFlags (L19) so it triggers headless dispatch.
- In
ManagementModeDispatcher.TryRun, construct/inject a UpdateService (mirroring how ApplyUpdateRunner is built at L46-64) and pass it into ManagementModeRunner.
-
Runner:
- Extend
ManagementModeRunner.IsManagementMode (L42) to include Update.
- Add a
LaunchMode.Update => await RunUpdateAsync(...) arm in the RunAsync switch (L62).
RunUpdateAsync behavior:
- Call
UpdateService.CheckAsync (L60).
- If
UpdateCheckResult indicates a newer release, call DownloadAndStageAsync (L79) then either (a) apply in-process via UpdateService.Apply (L116) or (b) spawn --apply-update <stagedDir> --relaunch via IProcessLauncher, mirroring UpdateController.DownloadInstallAndRelaunchAsync (L121-156).
- If already up to date, exit success with a short message.
- Emit progress/results via
Console.Error.WriteLine (existing convention, matches L40 of the dispatcher).
- Exit codes:
0 for updated OR already-latest; non-zero for failure (reuse existing ExitCode values where they fit, add a specific one only if genuinely needed).
-
Console-visible /?: Because the app is WinExe with no console attach, when args are a management verb or a help flag, write a full usage block to Console.Error.WriteLine from the headless path (this works from any parent shell that has bound stderr — same mechanism --silent and error paths already rely on). Do NOT introduce AttachConsole/AllocConsole unless the design-doc prohibition (build-and-installation.md:359) is explicitly revisited in a separate decision.
-
Help text: Update GetHelpText() (features/Phantom.Workspaces/CommandLineOptions.cs L69-87) AND the new console usage to enumerate ALL verbs:
Phantom.Workspaces.exe (default GUI)
Phantom.Workspaces.exe <config-file>
Phantom.Workspaces.exe update
Phantom.Workspaces.exe --install [--silent] [--install-root <path>]
Phantom.Workspaces.exe --apply-update <dir> [--relaunch]
Phantom.Workspaces.exe --uninstall [--purge]
Phantom.Workspaces.exe --startup
Phantom.Workspaces.exe --minimized
Phantom.Workspaces.exe /? | -? | /h | -h | /help | --help
The GUI HelpWindow will pick up the same text automatically via HelpWindow.axaml.cs L11.
-
Docs: Add a short update section to the README/help alongside the install instructions.
Expected Tests
New tests extend the existing classes (xUnit [Fact]/[Theory], Subject_Scenario_ExpectedOutcome PascalCase). Read the existing test files first to confirm exact helper/fake names (Harness, InMemoryFileSystem, ManualClock, FakeProcessLauncher, FakeScheduledTasks, FakeInstanceReleaseWaiter, etc.) and reuse them.
| Test Name |
Class |
What It Verifies |
Parse_Update_SelectsUpdateMode |
CommandLineOptionsTests (Phantom.Workspaces.Install.Tests) |
The positional update verb parses to LaunchMode.Update with IsValid == true and ExitCode.Success. |
Parse_UpdateFlag_SelectsUpdateMode |
CommandLineOptionsTests (Phantom.Workspaces.Install.Tests) |
If --update is also accepted, it parses to LaunchMode.Update (skip if flag form is not adopted). |
Parse_UpdatePositional_NotTreatedAsConfigPath |
CommandLineOptionsTests (GUI-side, Phantom.Workspaces.Tests) |
TryGetConfigurationFilePath("update") does NOT return update as a config-file path. |
Parse_UnknownVerb_IsInvalidWithBadArguments |
CommandLineOptionsTests (Phantom.Workspaces.Install.Tests) |
Regression: unknown tokens still yield ExitCode.BadArguments (guards against loosening the parser). |
IsManagementMode_ClassifiesUpdateAsManagement |
ManagementModeRunnerTests |
ManagementModeRunner.IsManagementMode(LaunchMode.Update) == true. |
RunAsync_Update_ChecksAndStagesLatestRelease |
ManagementModeRunnerTests |
With a fake UpdateService reporting a newer release, RunAsync calls CheckAsync → DownloadAndStageAsync and either applies or spawns --apply-update. |
RunAsync_Update_WhenNoNewerRelease_ReturnsSuccess |
ManagementModeRunnerTests |
With no newer release, RunAsync returns success without staging or applying. |
RunAsync_Update_WhenCheckFails_ReturnsFailureExitCode |
ManagementModeRunnerTests |
Update-check failure results in a non-zero ExitCode and a message on stderr. |
ManagementFlags_ContainsUpdate |
ManagementModeDispatcherTests (or add if absent) |
ManagementModeDispatcher.ManagementFlags includes "update" so headless dispatch triggers. |
GetHelpText_ListsUpdateVerb |
CommandLineOptionsTests (GUI-side, Phantom.Workspaces.Tests) |
GetHelpText() output contains the update verb and the other management verbs (--install, --apply-update, --uninstall, --startup, --minimized, --install-root, --silent). |
GetHelpText_ListsAllHelpFlags |
CommandLineOptionsTests (GUI-side) |
GetHelpText() output mentions /?, -?, /h, -h, /help, --help. |
Summary
Add a
Phantom.Workspaces.exe updatecommand-line verb that checks for and applies an update from the CLI (no GUI required), and document it — alongside every other command-line verb — in the/?help output. Today there is no CLI verb to trigger an update, and/?opens a GUI window whose text does not enumerate the management verbs.Root Cause / Current State
The app has two disjoint command-line parsers, both flag-oriented, and no CLI-facing update entry point:
GUI-side (
features/Phantom.Workspaces/CommandLineOptions.cs):IsHelpRequested(L26) recognizes/?,-?,/h,-h,/help,--help.TryGetConfigurationFilePath(L46) treats the first non-help positional token as a configuration-file path — so a positionalupdateverb would be misinterpreted as a config file.GetHelpText()(L69-87) only documents the config-file arg and/?. It does NOT list--install,--apply-update,--uninstall,--startup,--minimized,--silent,--install-root, etc.Install-side (
features/Phantom.Workspaces.Install/CommandLineOptions.cs), used for headless dispatch:Parse(params string[])(L48) recognizes--install(L64),--startup(L72),--minimized(L80),--uninstall(L88),--apply-update <dir>(L96),--help/-h(L110), and modifiers--silent(L119),--relaunch(L123),--purge(L127),--install-root <path>(L131).Invalid(...)withExitCode.BadArguments(L141).LaunchMode(features/Phantom.Workspaces.Install/LaunchMode.csL7-29):Gui, Install, Startup, Minimized, ApplyUpdate, Uninstall, Help— noUpdate.Dispatch:
features/Phantom.Workspaces/Program.csMain(L22): L28 callsManagementModeDispatcher.TryRun(args)(exits headlessly if it returns a code); L39 otherwise handles the help flag and falls through to Avalonia.features/Phantom.Workspaces/ManagementModeDispatcher.cs:ManagementFlags = { "--install", "--apply-update", "--uninstall" }(L19) is the sentinel set that triggers headless dispatch. A newupdateverb MUST be added here or it falls through to the GUI. Parsing is delegated to the Install parser (L35); error text goes toConsole.Error.WriteLine(options.Error)(L40). The dispatcher buildsInstallLayout,RealFileSystem,SystemClock,RealProcessLauncher,StartupTaskService,HealthGate,ApplyUpdateRunner,ManagementModeRunner(L46-64) and callsrunner.RunAsync(options, payloadDirectory, version)(L69).features/Phantom.Workspaces.Install/ManagementModeRunner.cs:IsManagementMode(L42) is currentlyInstall|ApplyUpdate|Uninstall; theRunAsyncswitch (L62) hasRunInstall(L71),RunApplyUpdateAsync(L116, delegates toApplyUpdateRunner),RunUninstall(L128).Update services already exist in the same
Phantom.Workspaces.Installassembly (natural reuse):features/Phantom.Workspaces.Install/UpdateService.cs:CheckAsync(L60) →UpdateCheckResult;DownloadAndStageAsync(ReleaseInfo, ct)(L79) → staged version string;Apply(string version)(L116) repointscurrent.features/Phantom.Workspaces/Services/Updates/UpdateController.csDownloadInstallAndRelaunchAsync(L121-156) stages then spawns--apply-update <dir> --relaunchviaIProcessLauncherand requests shutdown./?help is GUI-only:Program.csL39 lets the help flag fall through to Avalonia;App.axaml.csL218-226 opensnew HelpWindow();HelpWindow.axaml.csL11 setsHelpText.Text = CommandLineOptions.GetHelpText(). A CLI user runningPhantom.Workspaces.exe /?in a shell sees a GUI window, not console output. The app isWinExe(features/Phantom.Workspaces/Phantom.Workspaces.csprojL3<OutputType>WinExe</OutputType>) with noAttachConsole/AllocConsole; console attach is explicitly forbidden byfeatures/docs/design/build-and-installation.md:359. Existing headless output (e.g.--silenterrors) relies onConsole.Error.WriteLine, which works when launched from a parent shell that has bound stderr.Affected Files
features/Phantom.Workspaces.Install/LaunchMode.csUpdateenum value.features/Phantom.Workspaces.Install/CommandLineOptions.csParse)update(and optionally--update) →LaunchMode.Update.features/Phantom.Workspaces/CommandLineOptions.csTryGetConfigurationFilePath), L69-87 (GetHelpText)updateso it is not treated as a config-file path; enumerate all verbs in help text.features/Phantom.Workspaces/ManagementModeDispatcher.csManagementFlags), L35-69UpdateService.features/Phantom.Workspaces.Install/ManagementModeRunner.csIsManagementMode), L62 (switch)Updatein management modes; addRunUpdateAsync.features/Phantom.Workspaces.Install/UpdateService.csCheckAsync,DownloadAndStageAsync,Apply).features/Phantom.Workspaces/Services/Updates/UpdateController.csfeatures/Phantom.Workspaces/App.axaml.csHelpWindowpath — text update flows viaGetHelpText().features/Phantom.Workspaces/HelpWindow.axaml.csGetHelpText().features/Phantom.Workspaces/Phantom.Workspaces.csprojWinExe— informs the console-visibility decision for CLI/?.features/docs/design/build-and-installation.mdDesign / Fix
Enum: Add
UpdatetoLaunchMode(LaunchMode.cs).Parser:
Phantom.Workspaces.Install/CommandLineOptions.cs::Parse(L48+), recognize the positional tokenupdate(and, for convention consistency, optionally the flag form--update) →LaunchMode.Update. PreserveExitCode.BadArgumentsfor unknown tokens (L141).Phantom.Workspaces/CommandLineOptions.cs::TryGetConfigurationFilePath(L46), special-caseupdateso it is NOT treated as a configuration-file path. (This is the critical positional-vs-flag conflict the maintainer must resolve; using a flag-only--updatewould sidestep it but the owner asked forupdate.)Dispatch:
"update"(and"--update"if adopted) toManagementModeDispatcher.ManagementFlags(L19) so it triggers headless dispatch.ManagementModeDispatcher.TryRun, construct/inject aUpdateService(mirroring howApplyUpdateRunneris built at L46-64) and pass it intoManagementModeRunner.Runner:
ManagementModeRunner.IsManagementMode(L42) to includeUpdate.LaunchMode.Update => await RunUpdateAsync(...)arm in theRunAsyncswitch (L62).RunUpdateAsyncbehavior:UpdateService.CheckAsync(L60).UpdateCheckResultindicates a newer release, callDownloadAndStageAsync(L79) then either (a) apply in-process viaUpdateService.Apply(L116) or (b) spawn--apply-update <stagedDir> --relaunchviaIProcessLauncher, mirroringUpdateController.DownloadInstallAndRelaunchAsync(L121-156).Console.Error.WriteLine(existing convention, matches L40 of the dispatcher).0for updated OR already-latest; non-zero for failure (reuse existingExitCodevalues where they fit, add a specific one only if genuinely needed).Console-visible
/?: Because the app isWinExewith no console attach, when args are a management verb or a help flag, write a full usage block toConsole.Error.WriteLinefrom the headless path (this works from any parent shell that has bound stderr — same mechanism--silentand error paths already rely on). Do NOT introduceAttachConsole/AllocConsoleunless the design-doc prohibition (build-and-installation.md:359) is explicitly revisited in a separate decision.Help text: Update
GetHelpText()(features/Phantom.Workspaces/CommandLineOptions.csL69-87) AND the new console usage to enumerate ALL verbs:Phantom.Workspaces.exe(default GUI)Phantom.Workspaces.exe <config-file>Phantom.Workspaces.exe updatePhantom.Workspaces.exe --install [--silent] [--install-root <path>]Phantom.Workspaces.exe --apply-update <dir> [--relaunch]Phantom.Workspaces.exe --uninstall [--purge]Phantom.Workspaces.exe --startupPhantom.Workspaces.exe --minimizedPhantom.Workspaces.exe /? | -? | /h | -h | /help | --helpThe GUI
HelpWindowwill pick up the same text automatically viaHelpWindow.axaml.csL11.Docs: Add a short
updatesection to the README/help alongside the install instructions.Expected Tests
New tests extend the existing classes (xUnit
[Fact]/[Theory],Subject_Scenario_ExpectedOutcomePascalCase). Read the existing test files first to confirm exact helper/fake names (Harness,InMemoryFileSystem,ManualClock,FakeProcessLauncher,FakeScheduledTasks,FakeInstanceReleaseWaiter, etc.) and reuse them.Parse_Update_SelectsUpdateModeCommandLineOptionsTests(Phantom.Workspaces.Install.Tests)updateverb parses toLaunchMode.UpdatewithIsValid == trueandExitCode.Success.Parse_UpdateFlag_SelectsUpdateModeCommandLineOptionsTests(Phantom.Workspaces.Install.Tests)--updateis also accepted, it parses toLaunchMode.Update(skip if flag form is not adopted).Parse_UpdatePositional_NotTreatedAsConfigPathCommandLineOptionsTests(GUI-side,Phantom.Workspaces.Tests)TryGetConfigurationFilePath("update")does NOT returnupdateas a config-file path.Parse_UnknownVerb_IsInvalidWithBadArgumentsCommandLineOptionsTests(Phantom.Workspaces.Install.Tests)ExitCode.BadArguments(guards against loosening the parser).IsManagementMode_ClassifiesUpdateAsManagementManagementModeRunnerTestsManagementModeRunner.IsManagementMode(LaunchMode.Update) == true.RunAsync_Update_ChecksAndStagesLatestReleaseManagementModeRunnerTestsUpdateServicereporting a newer release,RunAsynccallsCheckAsync→DownloadAndStageAsyncand either applies or spawns--apply-update.RunAsync_Update_WhenNoNewerRelease_ReturnsSuccessManagementModeRunnerTestsRunAsyncreturns success without staging or applying.RunAsync_Update_WhenCheckFails_ReturnsFailureExitCodeManagementModeRunnerTestsExitCodeand a message on stderr.ManagementFlags_ContainsUpdateManagementModeDispatcherTests(or add if absent)ManagementModeDispatcher.ManagementFlagsincludes"update"so headless dispatch triggers.GetHelpText_ListsUpdateVerbCommandLineOptionsTests(GUI-side,Phantom.Workspaces.Tests)GetHelpText()output contains theupdateverb and the other management verbs (--install,--apply-update,--uninstall,--startup,--minimized,--install-root,--silent).GetHelpText_ListsAllHelpFlagsCommandLineOptionsTests(GUI-side)GetHelpText()output mentions/?,-?,/h,-h,/help,--help.