Skip to content

Add a \"Phantom.Workspaces.exe update\" command-line verb and document all verbs in /? help #1296

Description

@JoshuaRowePhantom

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:

  1. 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.
  2. 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.
  3. 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).
  4. 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.
  5. /? 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

  1. Enum: Add Update to LaunchMode (LaunchMode.cs).

  2. 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.)
  3. 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.
  4. Runner:

    • Extend ManagementModeRunner.IsManagementMode (L42) to include Update.
    • Add a LaunchMode.Update => await RunUpdateAsync(...) arm in the RunAsync switch (L62).
    • RunUpdateAsync behavior:
      1. Call UpdateService.CheckAsync (L60).
      2. 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).
      3. 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).
  5. 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.

  6. 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.

  7. 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 CheckAsyncDownloadAndStageAsync 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.

Metadata

Metadata

Labels

bugSomething isn't workingdiagnosedRoot cause identifiednext-upqueuedIn the active work queue (tracked in work-queue.md)verified-locallyImplementation has been verified locally

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions