feat: add TrapOS manifest install flow

This commit is contained in:
Guillaume ARM 2026-06-07 23:40:32 +02:00
parent c7d925cf18
commit c47b6e0ae4
8 changed files with 299 additions and 64 deletions

View File

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

View File

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

View File

@ -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<version>` 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.

View File

@ -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 <install-url>');
print('\t\t\twget run <install-url> --beta');
print('\t\twget run <install-url>');
print('\t\twget run <install-url> --beta');
print('\t\twget run <install-url> --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);

19
manifest.json Normal file
View File

@ -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"
]
}

View File

@ -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

45
startup/motd.lua Normal file
View File

@ -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();

View File

@ -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
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));