Part of #5.
#9 removed a launchd restart loop for the database: a file Uplink cannot open no longer takes the app with it. The same loop is still standing on settings.json, which is read three lines earlier, so the fix does not hold where it is reached first.
What is there today
readSettingsmerges the parsed file raw,{ ...DEFAULT_SETTINGS, ...parsed }(app/Uplink/settings.ts#L55), and validates exactly one field:engine, which falls back with a comment anticipating "a hand-edited or downgraded file" (app/Uplink/settings.ts#L56-L59).- Nothing checks that
allowedis an array,workdira string,modela string or null, oropenAtLoginandpausedbooleans. Thetry/catcharound it coversJSON.parseonly (app/Uplink/settings.ts#L62-L65). settingsEnvthen callssettings.allowed.join(',')(app/Uplink/settings.ts#L79).- That runs at
app/Uplink/desktop-agent.ts#L160, before theSqliteStoreconstruction that #9 wrapped in atry(app/Uplink/desktop-agent.ts#L169). app/Desktop/launcher.tshas no top-level handler, andstartDesktopAgentis awaited at module scope (app/Desktop/launcher.ts#L54).- The LaunchAgent sets
KeepAlivewithSuccessfulExitfalse (app/Uplink/desktop-agent.ts#L145), so a non-zero exit is restarted.
Reproduced against the real module: a settings.json holding "allowed": null returns unchanged from readSettings, and settingsEnv throws TypeError: null is not an object (evaluating 'settings.allowed.join'). The throw escapes startDesktopAgent, the process exits non-zero, launchd restarts it, and it happens again. No menubar ever appears, so there is nothing to say why.
A null allowed reaches the file the same ways the engine comment already anticipates: a hand edit, a downgrade to a build with a different shape, or a partial write from a crash mid-save.
Proposal
- Coerce every field in
readSettingsthe wayenginealready is, right after the merge atapp/Uplink/settings.ts#L55:allowedto an array,workdirandmodelto their types,openAtLoginandpausedto booleans, each falling back to the default rather than refusing to start. - The
/api/settingshandler already validates every field on the write path (app/Uplink/desktop-agent.ts#L622-L632). Share one validator between the two rather than writing the checks twice.
Requirements
- Fall back, never throw. This file is read before the menubar exists, so a refusal is invisible by construction.
- Every field, not the ones that look risky.
enginewas guarded because someone thought of it. The point is to stop depending on that. - The write path and the read path agree. A settings file the app itself wrote must always read back.
Acceptance
- A test writes a
settings.jsonwith a wrong type in each field, one at a time, and assertsreadSettingsreturns usable settings andsettingsEnvdoes not throw. - A test asserts a file written by
writeSettingsreads back unchanged. - Deleting
settings.jsonand starting the app still works.