Imported from alkaphreak/MarsTech-Uplink (
AGENTS.md). Install upstream withnpx skills add alkaphreak/MarsTech-Uplink. Copyright stays with the author.
AGENTS.md
Project at a glance
MarsTech-Uplinkis a macOS-only Kotlin CLI that orchestrates local system/package-manager updates; it is not a service app.- Runtime starts in
src/main/kotlin/space/marstech/uplink/Main.kt:main()->MacUpdateCommand.call()->runUpdate(). runUpdate()is organized into fixed phases: pre-flight, backups, updates, final summary. Preserve that phase model when adding features.--backup-onlyflag (RunContext.backupOnly) skips all phases except backups, then exits 0; pre-flight and updates are never called.--only <tool>skips pre-flight and backups entirely — only the updater phase runs (seeMain.ktlines 83–86). Valid tools:brew,sdkman,npm,uv,codex,rustup,cargo,pipx,gh,macos,mas,ohmyzsh,selfupdate. (pipruns automatically but is not registered invalidTools—--only pipis rejected.)--config <path>overrides the default config file location (Config.configFileOverride).- Exit code:
runUpdate()returns1ifctx.summaryFailedis non-empty, otherwise0.
Core architecture
- Shared mutable run state lives in
src/main/kotlin/space/marstech/uplink/RunContext.kt. - Most behavior is implemented as
RunContextextension functions, not classes: seeBackups.kt,PreFlight.kt,Updaters.kt, andSummary.kt. - Updaters run concurrently through
launchAsync(...)inMain.kt; output is buffered per thread viaRunContext.taskBufferso logs do not interleave. brewandcodexare intentionally coupled: they run inside one async task becausecodexis upgraded through Homebrew and must come afterbrewUpdate().selfUpdate()runs last in the pipeline to avoid replacing the binary while other tasks are still in flight. It prefersghCLI for tag lookup and falls back to the GitHub REST API viacurl.pip(pipUpdate()) upgradespip/pip3itself viapip install --upgrade pip; it is wired intobuildUpdateFuturesand controlled by[tools] pipinconfig.toml, but is not invalidToolsso--only pipis rejected at the CLI layer.- Tool detection is front-loaded in
buildToolsPresent()(ProcessUtils.kt) and then read throughRunContext.toolPresent(); tests often stub this map instead of invoking realwhichcalls. RunContext.shouldRun(tool)is the single gate controlling whether an updater executes:--onlyoverrides config; otherwise the[tools]section ofconfig.tomldecides. Always callshouldRun()(vialaunchIf) rather than invoking updaters directly.
Project-specific patterns to follow
- For a new updater, add a
fun RunContext.<name>Update()inUpdaters.ktand use this pattern:section("...")for headings- early
dryRunbranch that only logs and updates summary state toolPresent(...)/ file existence guards- record outcomes in
summaryUpdated,summarySkipped,summaryWarnings,summaryFailed
- Use
bufPrint(...)instead ofprintln(...)inside updater/backups/pre-flight code; direct prints will break the buffered parallel output model.bufPrintalso writes every line to the log file in real-time vialogImmediate(). - Use
runProcess,runShell,captureOutput, orrunCapturedfromProcessUtils.ktinstead of rawProcessBuilderunless you need custom process handling. - Keep CLI-facing strings explicit and operational; this tool is closer to an automation script than a domain model.
Filesystem and external integration points
Config.repoRootresolves throughAppConfig(loaded from the user config file); shell snapshots are written underconfs/snapshots/...relative to that root.Config.UPLINK_VERSIONis the single source of truth for the current version string; used byselfUpdate()to compare against the latest GitHub Release tag.- Task durations are accumulated in
RunContext.taskDurationsand printed as aTimings:table in the final summary (sorted slowest-first). - KeeWeb source and backup destination are configured via
AppConfig.keewebSource/AppConfig.keewebBackupDir; paths default to~/KeeWeb/myKeeweb.kdbxand~/Backup/Apps/KeeWeb. - KeeWeb backups are device-scoped:
backupKeewebDb()writes files asYYYY-MM-DD-<device>-<source>.kdbxand enforces retention per device pattern, not globally per source filename. - User config file:
~/Library/Application Support/marstech/marstech-uplink/config.toml— auto-created with placeholder defaults on first run. SeeAppConfig.ktfor all keys.- The
[tools]section maps every tool name to a boolean; setohmyzsh = falseto permanently skip a tool without touching the CLI. Managed viaToolsConfigand read throughRunContext.shouldRun(). Includesbackup_shellsandbackup_keewebflags for the backup phase. - If a key is missing from an existing config file,
AppConfig.repairMissingKeys()injects it with its default value in-place on every startup — no manual migration needed when adding new config keys.
- The
- Logs are always appended to
~/Library/Logs/marstech/marstech-uplink/marstech-uplink-YYYY-MM-DD.log. - External commands currently orchestrated include:
brew,sdk,npm/node,uv,rustup,cargo/cargo-install-update,pipx,pip/pip3,gh,softwareupdate,mas,omz,zsh,osascript,scutil,hostname,curl,unzip. - ANSI colors are centralized in
Colors.ktand rely on Jansi setup/teardown inMain.kt; do not add manual TTY detection.
Build, test, and debug workflow
- Verified test command:
mvn test. - Common build commands from the repo:
mvn verifymvn package-> producestarget/marstech-uplink.jarmvn -Pnative package-> GraalVM native binary build./build-install.sh-> bootstraps SDKMAN from.sdkmanrc, installs/activates the required GraalVM JDK if missing, runsmvn clean installthenmvn -Pnative package -DskipTests, copies the native binary to~/.local/bin/marstech-uplink, and runs a smoke test.
- Verified smoke test for the packaged CLI:
java -jar target/marstech-uplink.jar --dry-run --only brew. - Use
--dry-runand--only <tool>for safe debugging of one updater without touching the full machine.
Non-obvious caveats
- Tests are smoke-level, not hermetic.
BackupsTest.backupKeewebDb()may copy a real KeeWeb database if the configured source path exists on the current machine. - Config paths in
AppConfigdefault to generic placeholders (~/KeeWeb/…,~/MyWorkspace/…); the real paths are user-defined inconfig.toml. macosUpdate()dry-run still callssoftwareupdate --list; under Surefire this emits a knownCorrupted channel by directly writing to native streamwarning intarget/surefire-reports/*.dumpstream.- Ignore
target/for source edits; the real implementation lives only undersrc/main/kotlinandsrc/test/kotlin.