feat: stage 0, an empty RunicNPC that releases through CI
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in -1m44s

The repository RunicNPC is built in (docs/runicnpc/PLAN.md §9, stage 0):

- plugin/RunicNPC.cs: `// Requires: Kits` (D217), `[Info]` with the 0.0.0
  placeholder the release stamps, `RunicNpc_ApiVersion()` (API 1), and
  `rnpc.status`, which reports the version and which hooks have fired. It
  spawns nothing.
- plugin.toml: the API version, the framework floors it was loaded on
  (Oxide 2.0.7726, Carbon 2.0.259) and requires_plugins = ["Kits"].
- scripts/checkPlugin.js, adapted from Rust-Plugins': every hook listed and
  void unless written down; chat-command signatures; every RunicNpc_ call
  reachable by Call (the HumanNPC trap, PLAN.md §1.2); ApiVersion,
  `// Requires:` and [Info] agreeing with plugin.toml. 23 self-tests, including
  the real plugin and a CRLF checkout.
- PR Checks on PRs into main and edge; the release workflow on main, with
  Rust-Plugins' release engine unchanged and an adapter that ships
  runicnpc-<ver>.tar.gz (runicnpc/RunicNPC.cs + manifest.json) and SHA256SUMS.
  No bundle dispatch until stage 4.
- tools/: the rig panel scripts, with the panel and server ids moved into a
  git-ignored tools/rigs.json. `con.js` became `console.js`: CON is a reserved
  device name on Windows, and git there cannot open the file.
- README, CONTRIBUTING (edge-based flow, AI disclosure, borrow-not-copy),
  SECURITY, the code of conduct, issue and PR templates.

`feat:` so the cutover to main cuts the first release, 0.1.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-29 21:01:49 -05:00
parent 8641d8fb2e
commit 249f2e513f
19 changed files with 2027 additions and 4 deletions

85
tools/rig.js Normal file
View File

@@ -0,0 +1,85 @@
// A rig's server through the panel's client API: state, power, a console command, and files.
//
// node tools/rig.js <rig> state
// node tools/rig.js <rig> power <start|stop|restart|kill>
// node tools/rig.js <rig> cmd "<console command>" (fire and forget; console.js reads the reply)
// node tools/rig.js <rig> ls <dir>
// node tools/rig.js <rig> read <path> (TAIL=n prints the last n lines)
// node tools/rig.js <rig> write <path> <local file>
// node tools/rig.js <rig> rm <path>
//
// Paths are the server's, from its root: /oxide/plugins/RunicNPC.cs, /carbon/plugins/RunicNPC.cs.
//
// A directory the panel's file API creates is NOT writable by the game (docs/runicnpc/PLAN.md
// §1.5). Let the plugin create its own data directory, then write files into it.
const fs = require('fs')
const { load, serverId, unmsys } = require('./panel')
const config = load()
const [rig, op, a, b] = process.argv.slice(2).map(unmsys)
const base = `${config.panel}/api/client/servers/${serverId(config, rig)}`
const headers = (type = 'application/json') => ({
Authorization: 'Bearer ' + config.key,
Accept: 'application/json',
'Content-Type': type,
})
async function main() {
let r
switch (op) {
case 'state':
r = await fetch(`${base}/resources`, { headers: headers() })
break
case 'power':
r = await fetch(`${base}/power`, { method: 'POST', headers: headers(), body: JSON.stringify({ signal: a }) })
break
case 'cmd':
r = await fetch(`${base}/command`, { method: 'POST', headers: headers(), body: JSON.stringify({ command: a }) })
break
case 'ls':
r = await fetch(`${base}/files/list?directory=${encodeURIComponent(a)}`, { headers: headers() })
break
case 'read':
r = await fetch(`${base}/files/contents?file=${encodeURIComponent(a)}`, { headers: headers() })
break
case 'write':
r = await fetch(`${base}/files/write?file=${encodeURIComponent(a)}`, {
method: 'POST',
headers: headers('text/plain'),
body: fs.readFileSync(b),
})
break
case 'rm':
r = await fetch(`${base}/files/delete`, {
method: 'POST',
headers: headers(),
body: JSON.stringify({ root: a.slice(0, a.lastIndexOf('/')) || '/', files: [a.slice(a.lastIndexOf('/') + 1)] }),
})
break
default:
console.error('usage: node tools/rig.js <rig> state|power|cmd|ls|read|write|rm ...')
process.exit(2)
}
const text = await r.text()
if (!r.ok) {
console.error(r.status, text)
process.exit(1)
}
if (op === 'state') return console.log(JSON.parse(text).attributes.current_state)
if (op === 'ls') {
const rows = (JSON.parse(text).data || []).map(
(f) => `${f.attributes.is_file ? 'f' : 'd'} ${f.attributes.size}\t${f.attributes.name}`
)
return console.log(rows.join('\n'))
}
if (op === 'read' && process.env.TAIL) return console.log(text.split('\n').slice(-Number(process.env.TAIL)).join('\n'))
console.log(op === 'read' ? text : r.status)
}
main().catch((e) => {
console.error(e.message || e)
process.exit(1)
})