# Plan: `trapos-create-installer-disk` — one-shot TrapOS installer floppies ## Context Today TrapOS is installed by typing `wget run https://trapos.trapcloud.fr/install` on each fresh computer, then manually configuring settings (`sandbox.url`, `sandbox.password`, …) per machine. We want a **provisioning floppy**: run one program on a configured machine to "bake" an installer disk, then on any blank machine just insert the disk and boot — it installs the latest TrapOS online and clones *your* `.settings` onto the new computer, hands-free. The disk's value is **hands-free install + config cloning**, not offline file shipping. ## Resolved design decisions (from grill session) - **Install method**: autorun installs **online** via the existing `wget run` bootstrap. No files are bundled on the disk; the disk stays tiny and never goes stale vs the registry. - **Disk boot file**: CC only auto-runs `startup`/`startup.lua` from a floppy at boot. We put the autorun logic **directly in `/startup.lua`** (the name "autorun" is conceptual). - **Ordering (copy-then-install, no reboot)**: `install-trapos.lua` ends by handing off to the live `/startup/servers.lua` shell, which never returns. So the autorun **copies `.settings` first**, calls `settings.load()`, *then* runs the installer (whose `trapos-postinstall` respects the already-set `sandbox.url` and never touches `sandbox.password`, so the disk's config wins). **No changes to `install-trapos.lua`** and **no reboot** here — the boot/reboot rework is a separate effort. - **Sentinel**: autorun no-ops if `/trapos` exists. To reinstall: `rm /trapos`, reinsert, boot. - **Drive selection**: pick the **first eligible** drive = present, writable floppy that is **empty** (`#fs.list(mountPath) == 0`). Empty is preferred over non-empty. `0` empty → error (distinguish "disk not empty" vs "insert a blank floppy"); `2+` empty → print "multiple blank disks found, remove all but one" and stop. No interactive insert handling. - **Missing `/.settings` at creation**: warn + still build a valid (settings-less) installer. When `/.settings` exists, `settings.save()` first to flush in-session values, then copy. - **Label**: `TrapOS Installer`. - **Code shape**: injectable factory `apis/libinstallerdisk.lua` + thin CLI `programs/trapos-create-installer-disk.lua` + `tests/installer-disk.lua`; ship in `trapos-core`. Mirrors `apis/libccpm.lua` + `programs/ccpm.lua` + `tests/ccpm.lua`. ## Conventions to follow 2-space indent, semicolons, `local function` (per `AGENTS.md`). `require` absolute paths. Programs support `-version`/`--version` and `-help`/`--help` via `require('/apis/libversion')().forSelf()`. Colored output guarded by `term.isColor()` (see `printColored` in `programs/ccpm.lua`). Factory opts default to globals (see `createCcpm(opts)` in `apis/libccpm.lua`). ## 1. New: `apis/libinstallerdisk.lua` (testable factory) ```lua -- createInstallerDisk(opts) -> api -- opts.peripheral (default global `peripheral`) -- drive discovery -- opts.settings (default global `settings`) -- save() flush -- opts.settingsSourcePath (default '/.settings') -- computer config to clone -- opts.installerLabel (default 'TrapOS Installer') -- opts.installUrl (default 'https://trapos.trapcloud.fr/install') -- (uses real global `fs`; tests sandbox with isolated real dirs, like tests/ccpm.lua) ``` Public API: - `api.buildAutorun()` → returns the `startup.lua` source string (pure, fully unit-testable). - `api.findTargetDrive()` → `ok, { name, mountPath } | err`. Iterates `peripheral.find('drive')`/`getNames`, keeps present writable floppies (`isDiskPresent()` and `hasData()`), filters to empty (`#fs.list(getMountPath()) == 0`), applies the 0/1/2+ rules above with the distinct error messages. - `api.create()` → `ok, result | err`. Orchestrates: find drive → `setDiskLabel('TrapOS Installer')` → if `settingsSourcePath` exists: `settings.save()` then `fs.copy(settingsSourcePath, mount/.settings)` (delete dest first) else set `result.settingsWarning = true` → write `buildAutorun()` to `mount/startup.lua`. `result = { name, mountPath, label, settingsCopied, settingsWarning }`. `buildAutorun()` returns (note: copy-then-install, locate-own-disk-by-label, no reboot): ```lua -- TrapOS installer disk autorun (generated; do not edit) local INSTALLER_LABEL = 'TrapOS Installer'; local INSTALL_URL = 'https://trapos.trapcloud.fr/install'; if fs.exists('/trapos') then return; end -- already installed: leave machine alone local function installerMount() for _, name in ipairs(peripheral.getNames()) do if peripheral.getType(name) == 'drive' then local drive = peripheral.wrap(name); if drive.isDiskPresent() and drive.getDiskLabel() == INSTALLER_LABEL then return drive.getMountPath(); end end end end local mount = installerMount(); if mount then local src = fs.combine(mount, '.settings'); if fs.exists(src) then fs.delete('/.settings'); fs.copy(src, '/.settings'); settings.load(); end end print('TrapOS Installer: installing TrapOS...'); shell.run('wget', 'run', INSTALL_URL); ``` ## 2. New: `programs/trapos-create-installer-disk.lua` (thin CLI) - Handles `version`/`-version`/`--version` (prints `v' .. createVersion().forSelf()`) and `help`/`-help`/`--help` (usage), mirroring `programs/trapos-upgrade.lua`. - Calls `createInstallerDisk().create()`. On error: print message in red, `return`. On success: print a green summary (label set, `.settings` cloned / or the missing-settings warning, which drive), plus a hint: "Insert this disk into a blank computer and reboot it." - No confirmation prompt (target disk is required to be empty → low risk). ## 3. New: `tests/installer-disk.lua` Follow `tests/ccpm.lua`: `local createLibTest = require('/apis/libtest')`, isolated real-fs dirs per case, fake `peripheral` injected. Cases: - `buildAutorun()` contains the `/trapos` sentinel, the label, the install URL, copies `.settings` before `wget run`, and has no `os.reboot`. - `findTargetDrive`: zero drives → "insert a blank" error; one non-empty floppy → "not empty" error; one empty + one non-empty → picks the empty; two empty → "remove all but one" error. - `create`: sets label, writes `startup.lua` (== `buildAutorun()`) into the fake mount dir, copies a temp `settingsSourcePath` into `mount/.settings`; missing source → `settingsWarning` true and no copy. Fake `settings.save` is a recording no-op so the real `/.settings` is never touched. Fake `peripheral`: `getNames`/`getType`/`wrap` returning fake drives whose `getMountPath()` points at a real temp dir (e.g. `/installer-disk-test/disk-N`) so `fs.copy`/`fs.open`/`fs.list` operate on real fs; `isDiskPresent`/`hasData` return true; `set/getDiskLabel` store a field. ## 4. Packaging - `packages/trapos-core/ccpm.json`: append `"apis/libinstallerdisk.lua"` and `"programs/trapos-create-installer-disk.lua"` to `files`; bump `version` `0.6.3` → `0.7.0`. - `packages/index.json`: mirror `trapos-core` → `0.7.0`. ## Verification 1. `just check` — luacheck clean (respect `.luacheckrc` `lua51+cc`) and lychee link check. 2. `just test` (or `runtest tests/installer-disk.lua`) — suite prints `__TRAPOS_TEST_OK__`. 3. Optional headless probe: `just trapos-exec ''` to smoke-test against an emulated drive end-to-end. 4. In-game (human): run `trapos-create-installer-disk` on a configured machine → verify label `TrapOS Installer`, `startup.lua` + `.settings` present on the floppy. Insert into a blank computer, reboot → TrapOS installs and `.settings` is cloned. Confirm re-boot with disk still in is a no-op (because `/trapos` exists), and that `rm /trapos` + reboot reinstalls. ## Assumptions / caveats - Target blank computer has `shell.allow_disk_startup` enabled (CC default `true`) and HTTP enabled with `trapcloud.fr` reachable/allowed (CC default). - Literal order is **copy-then-install** (not "install-then-copy"); functionally the disk's settings still win. The pre-existing "exit the OS shell re-runs `servers.lua`" wart and any forced reboot are **out of scope** — owned by the separate boot/reboot rework.