diff --git a/README.md b/README.md index b43e977..4bc3cbd 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,32 @@ -# Trap's ComputerCraft APIs +# TrapOS + +A small in-game operating system for ComputerCraft / CC:Tweaked, built around a single-threaded event loop and a routed networking layer. ## Installation + ``` wget run https://raw.githubusercontent.com/guillaumearm/cc-libs/master/install.lua ``` -Install the beta branch: +Install the beta branch (one-time opt-in, asks for confirmation): ``` wget run https://raw.githubusercontent.com/guillaumearm/cc-libs/next/install.lua --beta ``` +Once a machine is on beta, `upgrade` keeps it on beta — `--beta` is not needed again. Use `upgrade --stable` to go back to the stable branch. + +After install, every boot shows a colored MOTD with the installed version and branch (lime for stable, orange + `[BETA]` for beta). + +## Manifest + +The installed file list, version, autostart servers, and current branch all live in `manifest.json` at the repo root. A copy is persisted locally at `/trapos/manifest.json` after install, and drives `upgrade`, `startup/motd.lua`, and `startup/servers.lua`. + ## APIs - `/apis/eventloop`: a simple event loop API. - `/apis/net`: an API to simplify sending and receiving routed messages, based on the `eventloop` library. ## Servers -All servers are automatically started at boot. +Servers listed in `manifest.autostart` are launched at boot by `startup/servers.lua`. - `/servers/ping-server`: allows a machine to respond to a `ping` command. @@ -23,7 +34,7 @@ All servers are automatically started at boot. - `router`: routes messages. You need to set up a router to use all `apis/net`-based programs and libraries. - `ping`: pings machines using `apis/net`. - `events`: emits and logs computer events. -- `upgrade`: upgrades the machine. Use `upgrade --beta` to install from the beta branch. +- `upgrade`: upgrades the machine. Reads `/trapos/manifest.json` to stay on the current branch; use `--beta` to opt in or `--stable` to opt out. ## Development See [DEVELOPMENT.md](./DEVELOPMENT.md) for development setup and workflow. diff --git a/docs/adrs/README.md b/docs/adrs/README.md index 24ceb58..cc2819e 100644 --- a/docs/adrs/README.md +++ b/docs/adrs/README.md @@ -11,3 +11,4 @@ Future ADRs can reuse the shape of the existing files when it is useful. - [`adr-0001-target-computercraft.md`](adr-0001-target-computercraft.md) - Target ComputerCraft. - [`adr-0002-use-eventloop-for-async-code.md`](adr-0002-use-eventloop-for-async-code.md) - Use eventloop for async code. - [`adr-0003-current-net-api-state.md`](adr-0003-current-net-api-state.md) - Current net API state. +- [`adr-0004-trapos-branding-and-manifest.md`](adr-0004-trapos-branding-and-manifest.md) - TrapOS branding and manifest-driven installs. diff --git a/docs/adrs/adr-0004-trapos-branding-and-manifest.md b/docs/adrs/adr-0004-trapos-branding-and-manifest.md new file mode 100644 index 0000000..a2ba812 --- /dev/null +++ b/docs/adrs/adr-0004-trapos-branding-and-manifest.md @@ -0,0 +1,47 @@ +# ADR 0004: TrapOS Branding And Manifest-Driven Installs + +## Status + +Accepted + +## Date + +2026-06-07 + +## Context + +The project started as a loose collection of ComputerCraft / CC:Tweaked APIs and programs. As the surface area grew (eventloop, net, router, ping, events, upgrade) and the install/upgrade flow gained a beta channel, three pain points emerged: + +- `install.lua` carries a hardcoded `LIST_FILES` table that has to be edited every time a file is added or removed. Adding a single shipped file means editing the file itself, the installer, and sometimes `startup/servers.lua`. +- The `--beta` flag is not persisted. The user has to remember to pass it on every `upgrade`, which makes the beta channel awkward to live on. +- The system has no visible identity at boot. There is no name, no version line, nothing that confirms which branch is installed. + +At the same time, the codebase has outgrown the "Trap's ComputerCraft APIs" framing. It is closer to a small in-game operating system than a library, and treating it as one unlocks a clearer story (a name, a version, a manifest, a boot banner). + +## Decision + +Adopt the name **TrapOS** and a manifest-driven install architecture. + +- A single `manifest.json` at the repo root is the source of truth for the project: name, version, branch, list of shipped files, and the list of servers to autostart at boot. Parsed with `textutils.unserializeJSON` / `textutils.serializeJSON` (built-in to CC:Tweaked). +- A local copy of that manifest is written to `/trapos/manifest.json` at the end of every install. This local file is the authoritative system state on the computer: + - `branch` is the persisted beta opt-in. Once a user installs with `--beta` (and confirms a one-time `(y/N)` prompt), subsequent `upgrade` calls auto-target the `next` branch with no flag needed. + - `version` is what the boot MOTD displays. + - `files` and `autostart` drive the next install and the boot sequence. +- A new `startup/motd.lua` prints a colored `TrapOS v` line at boot — lime for stable, orange with a `[BETA]` tag for beta. Guarded on `term.isColor()` so monochrome terminals still get the text. +- `startup/servers.lua` reads `autostart` from the local manifest instead of carrying its own hardcoded list. +- The shipped install URL stays at `https://raw.githubusercontent.com/guillaumearm/cc-libs/...` for now. Renaming the GitHub repository is a follow-up, tracked in the "Future Work" section below. + +## Consequences + +- Adding a new file or autostart server is now a one-line edit to `manifest.json`. Both `install.lua` and `startup/servers.lua` pick it up automatically. +- The beta channel becomes a real opt-in: a single confirmed `upgrade --beta` is enough to live on `next`. A new `--stable` flag exists for the symmetric opt-out. +- The boot banner gives users an immediate sanity check ("am I on the right branch, the right version?"). +- The system gains an explicit local state directory (`/trapos/`) and a clear contract for what lives there. +- The installer takes a hard dependency on `textutils.serializeJSON` / `unserializeJSON`, which require CC:Tweaked ≥ 1.79. This is well within any reasonable target version for Minecraft 1.21. +- Existing computers running the old installer still upgrade cleanly: the old `upgrade` fetches the new `install.lua`, which then creates `/trapos/manifest.json` from the manifest it just downloaded. + +## Future Work + +- **Repository rename.** Once the install flow has stabilized on `TrapOS`, the GitHub repository will be renamed from `cc-libs` to a name that matches (likely `trapos`). The install URL inside `install.lua` and `programs/upgrade.lua` will be updated in the same PR, and a redirect at the old URL is sufficient for in-the-wild installs. +- **Package manager.** The longer-term direction floated during planning was a small package manager where each directory in the repo is a package. The current manifest is a deliberate step toward that — same shape (name, version, files), single-package case — and can grow into multi-manifest discovery without changing the install/upgrade contract. +- **Per-version migration system.** Today the installer carries a static list of legacy files to delete. A future change can replace this with a per-version migration block driven by the manifest. diff --git a/install.lua b/install.lua index 355e32d..823877b 100644 --- a/install.lua +++ b/install.lua @@ -1,66 +1,142 @@ -local _VERSION = '2.3.0' +local _VERSION = '3.0.0'; -local LIST_FILES = { - -- startup - 'startup/servers.lua', - -- servers - 'servers/ping-server.lua', - -- programs - 'programs/router.lua', -- router is not in servers folder because he's not ran on every machines - 'programs/events.lua', - 'programs/ping.lua', - 'programs/upgrade.lua', - -- apis - 'apis/net.lua', - 'apis/eventloop.lua', -}; +local REPO_BASE = 'https://raw.githubusercontent.com/guillaumearm/cc-libs/'; +local LOCAL_STATE_DIR = '/trapos'; +local LOCAL_MANIFEST_PATH = '/trapos/manifest.json'; local function printUsage() print('install usage:'); print(); - print('\t\t\twget run '); - print('\t\t\twget run --beta'); + print('\t\twget run '); + print('\t\twget run --beta'); + print('\t\twget run --stable'); +end + +local function readLocalManifest() + if not fs.exists(LOCAL_MANIFEST_PATH) then return nil end + local f = fs.open(LOCAL_MANIFEST_PATH, 'r'); + if not f then return nil end + local data = f.readAll(); + f.close(); + if not data or data == '' then return nil end + return textutils.unserializeJSON(data); +end + +local function writeLocalManifest(manifest) + fs.makeDir(LOCAL_STATE_DIR); + local f = fs.open(LOCAL_MANIFEST_PATH, 'w'); + if not f then return false end + f.write(textutils.serializeJSON(manifest)); + f.close(); + return true; +end + +local function confirmBeta() + print(); + print('You are about to install the BETA branch (next).'); + print('Beta builds may be unstable. Continue? (y/N)'); + write('> '); + local answer = read(); + if not answer then return false end + answer = answer:lower(); + return answer == 'y' or answer == 'yes'; +end + +local function fetchManifest(branch) + local url = REPO_BASE .. branch .. '/manifest.json'; + local response = http.get(url); + if not response then return nil end + local body = response.readAll(); + response.close(); + if not body or body == '' then return nil end + return textutils.unserializeJSON(body); end local command = ...; -local branch = 'master'; +local forceBeta = false; +local forceStable = false; + +if command == 'version' or command == '-version' or command == '--version' then + print('install v' .. _VERSION); + return; +end + +if command == 'help' or command == '-help' or command == '--help' then + printUsage(); + return; +end if command == '--beta' or command == '-beta' then - branch = 'next'; + forceBeta = true; +elseif command == '--stable' or command == '-stable' then + forceStable = true; elseif command ~= nil and command ~= '' then printUsage(); return; end --- remove old files -fs.delete('ping-server.lua'); -- replaced by `servers/ping-server.lua` -fs.delete('ping.lua') -- replaced by `programs/ping.lua` -fs.delete('cube.lua') -- replaced by `programs/cube.lua` -fs.delete('router.lua') -- replaced by `programs/router.lua` -fs.delete('servers/cube-startup.lua'); -- replaced by `servers/cube-boot.lua` +local localManifest = readLocalManifest(); +local localBranch = localManifest and localManifest.branch or nil; +local branch; + +if forceBeta then + branch = 'next'; + if localBranch ~= 'next' then + if not confirmBeta() then + print('Aborted.'); + return; + end + end +elseif forceStable then + branch = 'master'; +else + branch = localBranch or 'master'; +end + +print('Fetching manifest from branch: ' .. branch); +local manifest = fetchManifest(branch); +if not manifest or type(manifest.files) ~= 'table' then + print('Failed to fetch or parse manifest.json from ' .. branch); + return; +end + +-- The persisted branch reflects the actually-used branch, not the manifest default. +manifest.branch = branch; + +local REPO_PREFIX = REPO_BASE .. branch .. '/'; + +-- Legacy file cleanup (pre-manifest installs). +fs.delete('ping-server.lua'); +fs.delete('ping.lua'); +fs.delete('cube.lua'); +fs.delete('router.lua'); +fs.delete('servers/cube-startup.lua'); fs.delete('programs/cube.lua'); fs.delete('programs/goo.lua'); fs.delete('servers/cube-server.lua'); fs.delete('servers/cube-boot.lua'); -local REPO_PREFIX = 'https://raw.githubusercontent.com/guillaumearm/cc-libs/' .. branch .. '/' - -local previousDir = shell.dir() - -shell.setDir('/') +local previousDir = shell.dir(); +shell.setDir('/'); fs.makeDir('/programs'); fs.makeDir('/apis'); fs.makeDir('/startup'); fs.makeDir('/servers'); +fs.makeDir(LOCAL_STATE_DIR); -for _, filePath in pairs(LIST_FILES) do - fs.delete(filePath) - shell.execute('wget', REPO_PREFIX .. filePath, filePath) +for _, filePath in ipairs(manifest.files) do + fs.delete(filePath); + shell.execute('wget', REPO_PREFIX .. filePath, filePath); end -print() -print('=> Execute startup/servers.lua') -shell.execute('/startup/servers.lua') +if not writeLocalManifest(manifest) then + print('Warning: failed to write local manifest'); +end -shell.setDir(previousDir) +print(); +print('=> TrapOS v' .. (manifest.version or '?') .. ' installed (branch: ' .. branch .. ')'); +print('=> Execute startup/servers.lua'); +shell.execute('/startup/servers.lua'); + +shell.setDir(previousDir); diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..cd13fcd --- /dev/null +++ b/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "TrapOS", + "version": "0.2.0", + "branch": "master", + "files": [ + "startup/motd.lua", + "startup/servers.lua", + "servers/ping-server.lua", + "programs/router.lua", + "programs/events.lua", + "programs/ping.lua", + "programs/upgrade.lua", + "apis/net.lua", + "apis/eventloop.lua" + ], + "autostart": [ + "servers/ping-server" + ] +} diff --git a/programs/upgrade.lua b/programs/upgrade.lua index 2433a60..6f093f1 100644 --- a/programs/upgrade.lua +++ b/programs/upgrade.lua @@ -1,19 +1,31 @@ -local _VERSION = '1.3.0'; +local _VERSION = '1.4.0'; -local INSTALL_URL = 'https://raw.githubusercontent.com/guillaumearm/cc-libs/master/install.lua'; -local BETA_INSTALL_URL = 'https://raw.githubusercontent.com/guillaumearm/cc-libs/next/install.lua'; - -local command = ...; +local REPO_BASE = 'https://raw.githubusercontent.com/guillaumearm/cc-libs/'; +local LOCAL_MANIFEST_PATH = '/trapos/manifest.json'; local function printUsage() print('upgrade usage:'); print(); - print('\t\t\tupgrade'); - print('\t\t\tupgrade --beta'); - print('\t\t\tupgrade version'); - print('\t\t\tupgrade help'); + print('\t\tupgrade'); + print('\t\tupgrade --beta'); + print('\t\tupgrade --stable'); + print('\t\tupgrade version'); + print('\t\tupgrade help'); end +local function readLocalBranch() + if not fs.exists(LOCAL_MANIFEST_PATH) then return nil end + local f = fs.open(LOCAL_MANIFEST_PATH, 'r'); + if not f then return nil end + local data = f.readAll(); + f.close(); + if not data or data == '' then return nil end + local manifest = textutils.unserializeJSON(data); + return manifest and manifest.branch or nil; +end + +local command = ...; + if command == 'version' or command == '-version' or command == '--version' then print('upgrade v' .. _VERSION); return; @@ -24,14 +36,26 @@ if command == 'help' or command == '-help' or command == '--help' then return; end -if command == '--beta' or command == '-beta' then - shell.execute('wget', 'run', BETA_INSTALL_URL, '--beta'); - return; -end +local branch; +local extraFlag; -if command ~= nil and command ~= '' then +if command == '--beta' or command == '-beta' then + branch = 'next'; + extraFlag = '--beta'; +elseif command == '--stable' or command == '-stable' then + branch = 'master'; + extraFlag = '--stable'; +elseif command ~= nil and command ~= '' then printUsage(); return; +else + branch = readLocalBranch() or 'master'; end -shell.execute('wget', 'run', INSTALL_URL); +local installUrl = REPO_BASE .. branch .. '/install.lua'; + +if extraFlag then + shell.execute('wget', 'run', installUrl, extraFlag); +else + shell.execute('wget', 'run', installUrl); +end diff --git a/startup/motd.lua b/startup/motd.lua new file mode 100644 index 0000000..d1e2ec6 --- /dev/null +++ b/startup/motd.lua @@ -0,0 +1,45 @@ +local _VERSION = '1.0.0'; + +local LOCAL_MANIFEST_PATH = '/trapos/manifest.json'; + +local function readLocalManifest() + if not fs.exists(LOCAL_MANIFEST_PATH) then return nil end + local f = fs.open(LOCAL_MANIFEST_PATH, 'r'); + if not f then return nil end + local data = f.readAll(); + f.close(); + if not data or data == '' then return nil end + return textutils.unserializeJSON(data); +end + +local manifest = readLocalManifest(); +if not manifest then return end + +local name = manifest.name or 'TrapOS'; +local version = manifest.version or '?'; +local branch = manifest.branch or 'master'; +local isBeta = branch == 'next'; + +local hasColor = term.isColor and term.isColor(); +local previousColor; + +if hasColor then + previousColor = term.getTextColor(); + if isBeta then + term.setTextColor(colors.orange); + else + term.setTextColor(colors.lime); + end +end + +if isBeta then + print(name .. ' v' .. version .. ' [BETA]'); +else + print(name .. ' v' .. version); +end + +if hasColor and previousColor then + term.setTextColor(previousColor); +end + +print(); diff --git a/startup/servers.lua b/startup/servers.lua index 62dd035..f697966 100644 --- a/startup/servers.lua +++ b/startup/servers.lua @@ -1,8 +1,16 @@ -local _VERSION = '1.1.2' +local _VERSION = '1.2.0' -local SERVERS = { - "servers/ping-server", -}; +local LOCAL_MANIFEST_PATH = '/trapos/manifest.json'; + +local function readLocalManifest() + if not fs.exists(LOCAL_MANIFEST_PATH) then return nil end + local f = fs.open(LOCAL_MANIFEST_PATH, 'r'); + if not f then return nil end + local data = f.readAll(); + f.close(); + if not data or data == '' then return nil end + return textutils.unserializeJSON(data); +end local function init() shell.setPath(shell.path() .. ':/programs'); @@ -54,12 +62,16 @@ local function getServerFns(serverList) return servers; end +local manifest = readLocalManifest(); +local SERVERS = (manifest and manifest.autostart) or {}; + local servers = getServerFns(SERVERS); -print("\nStarting servers..."); - -for _, v in ipairs(SERVERS) do - print("\t\t" .. v) +if #SERVERS > 0 then + print("\nStarting servers..."); + for _, v in ipairs(SERVERS) do + print("\t\t" .. v) + end end parallel.waitForAll(shellFn, table.unpack(servers));