fixes and init bin
Release rolling / release (push) Successful in 12m7s

This commit is contained in:
Raven Scott
2026-08-13 14:39:09 -04:00
parent 879ad78bed
commit 6434ac957a
20 changed files with 1521 additions and 16 deletions
@@ -68,6 +68,7 @@ export const COREUTILS_COMMANDS = [
'hypershell-board',
'iconv',
'id',
'init-discord',
'install',
'irc',
'join',
@@ -0,0 +1,60 @@
{
"name": "init-discord",
"section": 1,
"title": "init-discord",
"synopsis": [
"init-discord [--token TOKEN] [--guild ID] [--whitelist ID,ID] [--yes] [--force] [--plugin-only]"
],
"description": "Set up the stock Discord bot on this session. Creates ~/.discord and ~/.discord/plugins, writes ~/.discord/.env (DISCORD_TOKEN, optional DISCORD_GUILD_ID and DISCORD_ID_WHITELIST), and installs the hello-world plugin ~/.discord/plugins/hello.json (/hello). Interactive by default: prompts for token, guild id, and whitelist. Never prints the full token. After setup run discord-bot --check and start bare-os-discord.",
"options": [
{
"flag": "--token TOKEN",
"meaning": "Bot token (Developer Portal → Bot → Token). Prefer the interactive prompt so it is not stored in shell history."
},
{
"flag": "--guild ID",
"meaning": "Guild id for instant slash-command registration."
},
{
"flag": "--whitelist ID,ID",
"meaning": "Comma-separated Discord user ids allowed to use the bot."
},
{
"flag": "--yes",
"meaning": "Non-interactive. Requires --token or an existing ~/.discord/.env token."
},
{
"flag": "--force",
"meaning": "Replace an existing token and overwrite hello.json."
},
{
"flag": "--plugin-only",
"meaning": "Only create ~/.discord/plugins/hello.json; do not change .env."
}
],
"keywords": ["discord", "bot", "setup", "init", "plugin"],
"environment": [
"Writes DISCORD_TOKEN, DISCORD_GUILD_ID, DISCORD_ID_WHITELIST into ~/.discord/.env and the current session env."
],
"seeAlso": [
{
"name": "discord-bot",
"section": 1
},
{
"name": "devguide-21-discord-bots",
"section": 7
}
],
"bareOsNotes": "Guest VFS paths. The initd unit bare-os-discord appears in systemctl list only after ~/.discord/.env exists with a token.",
"examples": [
{
"caption": "interactive setup",
"code": "init-discord"
},
{
"caption": "scripted setup (avoid --token on a recorded TTY if you can)",
"code": "init-discord --yes --token \"$DISCORD_TOKEN\" --guild 123 --whitelist 456"
}
]
}
+1 -1
View File
@@ -7,7 +7,7 @@
"description": "Build JS /bin utilities for bare-operating-system (concat + stage to kernel/)",
"scripts": {
"build": "node ./scripts/ensure-man-pages.mjs && node ./build.mjs",
"test": "node ./test/clear-sequence.test.mjs && node ./test/help-bin-list.test.mjs && node ./test/whois-rdap.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/edit-tui-sdk.test.mjs && node ./test/baresay-say-bundle.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-tui-sdk.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-compose.test.mjs && node ./test/baretop-incremental.test.mjs && node ./test/baretop-interaction-latency.test.mjs && node ./test/baretop-missing-signals.test.mjs && node ./test/baretop-stress-snapshot.test.mjs && node ./test/baretop-ui-helpers.test.mjs && node ./test/baretop-perf-baseline.test.mjs && node ./test/irc-parse.test.mjs && node ./test/irc-client.test.mjs && node ./test/irc-dial.test.mjs && node ./test/irc-commands.test.mjs && node ./test/irc-tui-sdk.test.mjs && node ./test/summon-engine.test.mjs && node ./test/summon-tui-sdk.test.mjs && node ./test/posix-test-int-compare.test.mjs && node ./test/posix-test-bracket-argv.test.mjs && node ./test/posix-utils-edge.test.mjs && node ./test/posix-golden-issue7.test.mjs && node ./test/getconf-pathconf-union-mirror.test.mjs && node ./test/getconf-posix-shm.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs && node ./test/xcu-issue7-sweep.test.mjs && node ./test/expand-tab-stops.test.mjs && node ./test/pkg-swarm-index-pathcap.test.mjs && node ./test/sha224-sha384-sum.test.mjs && node ./test/hardening.test.mjs && node ./test/agent-sse-parse.test.mjs && node ./test/agent-web-fetch.test.mjs && node ./test/agent-helpers.test.mjs && node ./test/agent-tools-run-command.test.mjs && node ./test/agent-workspace.test.mjs && node ./test/agent-skills.test.mjs && node ./test/agent-config-surface.test.mjs && node ./test/agent-trim-ctx.test.mjs && node ./test/agent-qvac.test.mjs && node ./test/telnet-protocol.test.mjs && node ./test/telnet-cli.test.mjs && node ./test/login.test.mjs && node ./test/p2p-suite-bundles.test.mjs && node ./test/p2p-suite-tui-sdk.test.mjs && node ./test/chat-tui-sdk.test.mjs && node ./test/swarmtop-peer-visibility.test.mjs",
"test": "node ./test/clear-sequence.test.mjs && node ./test/init-discord.test.mjs && node ./test/help-bin-list.test.mjs && node ./test/whois-rdap.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/edit-tui-sdk.test.mjs && node ./test/baresay-say-bundle.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-tui-sdk.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-compose.test.mjs && node ./test/baretop-incremental.test.mjs && node ./test/baretop-interaction-latency.test.mjs && node ./test/baretop-missing-signals.test.mjs && node ./test/baretop-stress-snapshot.test.mjs && node ./test/baretop-ui-helpers.test.mjs && node ./test/baretop-perf-baseline.test.mjs && node ./test/irc-parse.test.mjs && node ./test/irc-client.test.mjs && node ./test/irc-dial.test.mjs && node ./test/irc-commands.test.mjs && node ./test/irc-tui-sdk.test.mjs && node ./test/summon-engine.test.mjs && node ./test/summon-tui-sdk.test.mjs && node ./test/posix-test-int-compare.test.mjs && node ./test/posix-test-bracket-argv.test.mjs && node ./test/posix-utils-edge.test.mjs && node ./test/posix-golden-issue7.test.mjs && node ./test/getconf-pathconf-union-mirror.test.mjs && node ./test/getconf-posix-shm.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs && node ./test/xcu-issue7-sweep.test.mjs && node ./test/expand-tab-stops.test.mjs && node ./test/pkg-swarm-index-pathcap.test.mjs && node ./test/sha224-sha384-sum.test.mjs && node ./test/hardening.test.mjs && node ./test/agent-sse-parse.test.mjs && node ./test/agent-web-fetch.test.mjs && node ./test/agent-helpers.test.mjs && node ./test/agent-tools-run-command.test.mjs && node ./test/agent-workspace.test.mjs && node ./test/agent-skills.test.mjs && node ./test/agent-config-surface.test.mjs && node ./test/agent-trim-ctx.test.mjs && node ./test/agent-qvac.test.mjs && node ./test/telnet-protocol.test.mjs && node ./test/telnet-cli.test.mjs && node ./test/login.test.mjs && node ./test/p2p-suite-bundles.test.mjs && node ./test/p2p-suite-tui-sdk.test.mjs && node ./test/chat-tui-sdk.test.mjs && node ./test/swarmtop-peer-visibility.test.mjs",
"perf:baretop": "node ./test/baretop-perf-baseline.test.mjs"
}
}
@@ -0,0 +1,226 @@
/**
* Interactive Discord bot setup: ~/.discord/.env + hello-world plugin.
*/
var INIT_DISCORD_HELLO_JSON =
'{\n' +
' "name": "hello",\n' +
' "description": "Greet someone from Bare OS",\n' +
' "version": "1.0.0",\n' +
' "options": [\n' +
' {\n' +
' "name": "who",\n' +
' "description": "Name to greet",\n' +
' "required": false\n' +
' }\n' +
' ],\n' +
' "run": "echo Hello, ${who:-world} — from $USER on $HOSTNAME",\n' +
' "title": "hello"\n' +
'}\n'
function initDiscordHasFlag(argv, names) {
for (let i = 1; i < argv.length; i++) {
const a = String(argv[i] || '')
for (let n = 0; n < names.length; n++) {
if (a === names[n]) return true
}
}
return false
}
function initDiscordArg(argv, names) {
for (let i = 1; i < argv.length; i++) {
const a = String(argv[i] || '')
for (let n = 0; n < names.length; n++) {
const name = names[n]
if (a === name) return argv[i + 1] != null ? String(argv[i + 1]) : ''
if (a.indexOf(name + '=') === 0) return a.slice(name.length + 1)
}
}
return ''
}
function initDiscordParseEnv(text) {
const out = {}
const lines = String(text || '').split(/\r?\n/)
for (let i = 0; i < lines.length; i++) {
let s = lines[i].replace(/^\uFEFF/, '').trim()
if (!s || s.charAt(0) === '#') continue
if (s.indexOf('export ') === 0) s = s.slice(7).trim()
const eq = s.indexOf('=')
if (eq < 1) continue
let val = s.slice(eq + 1).trim()
if (
(val.charAt(0) === '"' && val.charAt(val.length - 1) === '"') ||
(val.charAt(0) === "'" && val.charAt(val.length - 1) === "'")
) {
val = val.slice(1, -1)
}
out[s.slice(0, eq).trim()] = val
}
return out
}
function initDiscordMaskToken(tok) {
const t = String(tok || '')
if (t.length < 8) return '(set)'
return t.slice(0, 4) + '…' + t.slice(-4)
}
function initDiscordUpsertEnv(text, key, value) {
const line = key + '=' + value
const re = new RegExp('^\\s*(?:export\\s+)?' + key.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&') + '=.*$', 'm')
if (re.test(text)) return text.replace(re, line)
const body = text && !/\n$/.test(text) ? text + '\n' : text || ''
return body + line + '\n'
}
async function initDiscordReadText(ctx, path) {
try {
const buf = await ctx.vfs.readFile(path)
if (!buf) return ''
if (ctx.b4a && typeof ctx.b4a.toString === 'function') return ctx.b4a.toString(buf)
return String(buf)
} catch {
return ''
}
}
async function initDiscordWriteText(ctx, path, text) {
const buf = ctx.b4a && typeof ctx.b4a.from === 'function' ? ctx.b4a.from(text) : text
await ctx.vfs.writeFile(path, buf)
}
async function initDiscordMkdir(ctx, path) {
if (!ctx.vfs || typeof ctx.vfs.mkdir !== 'function') return
try {
await ctx.vfs.mkdir(path, { recursive: true })
} catch {
/* exists */
}
}
async function initDiscordAsk(ctx, prompt, fallback) {
if (typeof ctx.readLine !== 'function') return fallback || ''
const hint = fallback ? ' [' + fallback + ']' : ''
const raw = await ctx.readLine(prompt + hint + ': ')
const s = String(raw == null ? '' : raw).trim()
return s || fallback || ''
}
async function run(ctx, argv) {
const argv0 = argv[0] || 'init-discord'
if (initDiscordHasFlag(argv, ['-h', '--help'])) {
ctx.console.log(
'usage: ' +
argv0 +
' [--token TOKEN] [--guild ID] [--whitelist ID,ID] [--yes] [--force] [--plugin-only]\n\n' +
'Create ~/.discord, write ~/.discord/.env, and install a hello-world plugin.\n' +
'Interactive by default (asks for token, guild id, whitelist). Use flags for scripts.\n\n' +
' --token TOKEN Bot token (Developer Portal → Bot). Never logged.\n' +
' --guild ID Guild id for instant slash registration.\n' +
' --whitelist ID,ID Discord user ids allowed to use the bot.\n' +
' --yes Non-interactive: require --token (or an existing .env).\n' +
' --force Overwrite an existing token / hello.json.\n' +
' --plugin-only Only ensure the hello plugin; leave .env alone.\n\n' +
'Then: discord-bot --check --env ~/.discord/.env\n' +
' systemctl daemon-reload && systemctl start bare-os-discord'
)
ctx.exitCode = 0
return
}
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') {
ctx.console.error(argv0 + ': vfs.writeFile unavailable')
ctx.exitCode = 1
return
}
const yes = initDiscordHasFlag(argv, ['--yes', '-y'])
const force = initDiscordHasFlag(argv, ['--force'])
const pluginOnly = initDiscordHasFlag(argv, ['--plugin-only'])
let tokenFlag = initDiscordArg(argv, ['--token'])
let guildFlag = initDiscordArg(argv, ['--guild', '--guild-id'])
let whiteFlag = initDiscordArg(argv, ['--whitelist', '--id-whitelist'])
await initDiscordMkdir(ctx, '~/.discord')
await initDiscordMkdir(ctx, '~/.discord/plugins')
const envPath = '~/.discord/.env'
const prevText = await initDiscordReadText(ctx, envPath)
const prev = initDiscordParseEnv(prevText)
const prevToken = prev.DISCORD_TOKEN || prev.BOT_TOKEN || ''
const prevGuild = prev.DISCORD_GUILD_ID || ''
const prevWhite = prev.DISCORD_ID_WHITELIST || ''
if (!pluginOnly) {
let token = tokenFlag
let guild = guildFlag
let white = whiteFlag
if (!yes) {
ctx.console.log('Bare OS Discord setup')
ctx.console.log('Token from Developer Portal → Bot → Reset Token (not the OAuth2 client secret).')
if (prevToken && !token) {
ctx.console.log('Existing token: ' + initDiscordMaskToken(prevToken) + ' (Enter keeps it)')
}
token = await initDiscordAsk(ctx, 'DISCORD_TOKEN', token || '')
if (!token && prevToken) token = prevToken
guild = await initDiscordAsk(
ctx,
'DISCORD_GUILD_ID (optional, instant slash commands)',
guild || prevGuild
)
white = await initDiscordAsk(
ctx,
'DISCORD_ID_WHITELIST (your user id, comma-separated)',
white || prevWhite
)
} else if (!token) {
token = prevToken
}
if (!token) {
ctx.console.error(argv0 + ': DISCORD_TOKEN is required (flag --token or interactive prompt)')
ctx.exitCode = 1
return
}
if (prevToken && token !== prevToken && !force && yes) {
ctx.console.error(argv0 + ': ~/.discord/.env already has a token (pass --force to replace)')
ctx.exitCode = 1
return
}
let next = prevText || '# Bare OS Discord bot\n'
next = initDiscordUpsertEnv(next, 'DISCORD_TOKEN', token)
if (guild) next = initDiscordUpsertEnv(next, 'DISCORD_GUILD_ID', guild)
if (white) next = initDiscordUpsertEnv(next, 'DISCORD_ID_WHITELIST', white)
await initDiscordWriteText(ctx, envPath, next)
if (ctx.env) {
ctx.env.DISCORD_TOKEN = token
if (guild) ctx.env.DISCORD_GUILD_ID = guild
if (white) ctx.env.DISCORD_ID_WHITELIST = white
}
if (ctx.vfs.env) {
ctx.vfs.env.DISCORD_TOKEN = token
if (guild) ctx.vfs.env.DISCORD_GUILD_ID = guild
if (white) ctx.vfs.env.DISCORD_ID_WHITELIST = white
}
ctx.console.log('Wrote ' + envPath + ' (token ' + initDiscordMaskToken(token) + ')')
if (guild) ctx.console.log('Guild id ' + guild)
if (white) ctx.console.log('Whitelist ' + white)
}
const pluginPath = '~/.discord/plugins/hello.json'
const existingPlugin = await initDiscordReadText(ctx, pluginPath)
if (existingPlugin && !force) {
ctx.console.log('Kept existing ' + pluginPath)
} else {
await initDiscordWriteText(ctx, pluginPath, INIT_DISCORD_HELLO_JSON)
ctx.console.log('Installed example plugin ' + pluginPath + ' (/hello)')
}
ctx.console.log('')
ctx.console.log('Next:')
ctx.console.log(' discord-bot --check --env ~/.discord/.env')
ctx.console.log(' discord-bot --env ~/.discord/.env')
ctx.console.log(' systemctl daemon-reload && systemctl start bare-os-discord')
ctx.console.log(' /plugins reload (after the bot is online)')
ctx.exitCode = 0
}
@@ -0,0 +1,111 @@
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import test from 'brittle'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
async function loadBin() {
const runtime = await readFile(path.join(__dirname, '../lib/runtime.js'), 'utf8')
const body = await readFile(path.join(__dirname, '../src/init-discord.js'), 'utf8')
return new AsyncFunction(
'ctx',
'argv',
`${runtime}\n${body}\nif (typeof run === 'function') return await run(ctx, argv)\n`
)
}
function makeCtx(tree) {
const env = { USER: 'alice', HOME: '/home/alice' }
const logs = []
const errs = []
return {
logs,
errs,
env,
exitCode: 0,
console: {
log: (m) => logs.push(String(m)),
error: (m) => errs.push(String(m))
},
b4a: {
toString: (b) => Buffer.from(b).toString('utf8'),
from: (s) => Buffer.from(String(s))
},
vfs: {
env,
mkdir: async (p) => {
if (!tree[p]) tree[p] = []
},
readFile: async (p) => {
if (typeof tree[p] !== 'string') {
const e = new Error('ENOENT')
e.code = 'ENOENT'
throw e
}
return Buffer.from(tree[p])
},
writeFile: async (p, buf) => {
tree[p] = Buffer.from(buf).toString('utf8')
}
}
}
}
test('init-discord --help', async (t) => {
const run = await loadBin()
const tree = {}
const ctx = makeCtx(tree)
await run(ctx, ['init-discord', '--help'])
t.is(ctx.exitCode, 0)
t.ok(ctx.logs.some((l) => /--token/.test(l)))
})
test('init-discord --yes writes env and hello plugin without echoing the token', async (t) => {
const run = await loadBin()
const tree = {}
const ctx = makeCtx(tree)
await run(ctx, [
'init-discord',
'--yes',
'--token',
'abc.def.secret-token',
'--guild',
'111',
'--whitelist',
'222,333'
])
t.is(ctx.exitCode, 0)
t.ok(/DISCORD_TOKEN=abc\.def\.secret-token/.test(tree['~/.discord/.env']))
t.ok(/DISCORD_GUILD_ID=111/.test(tree['~/.discord/.env']))
t.ok(/DISCORD_ID_WHITELIST=222,333/.test(tree['~/.discord/.env']))
t.ok(/"name": "hello"/.test(tree['~/.discord/plugins/hello.json']))
t.ok(ctx.logs.some((l) => /hello\.json/.test(l)))
t.absent(ctx.logs.some((l) => /secret-token/.test(l)), 'never prints full token')
t.is(ctx.env.DISCORD_GUILD_ID, '111')
})
test('init-discord --yes refuses to clobber a token without --force', async (t) => {
const run = await loadBin()
const tree = {
'~/.discord/.env': 'DISCORD_TOKEN=old.token.value\n'
}
const ctx = makeCtx(tree)
await run(ctx, ['init-discord', '--yes', '--token', 'new.token.value'])
t.is(ctx.exitCode, 1)
t.ok(/old\.token\.value/.test(tree['~/.discord/.env']))
await run(ctx, ['init-discord', '--yes', '--force', '--token', 'new.token.value'])
t.is(ctx.exitCode, 0)
t.ok(/new\.token\.value/.test(tree['~/.discord/.env']))
})
test('init-discord --plugin-only installs hello without requiring a token', async (t) => {
const run = await loadBin()
const tree = {}
const ctx = makeCtx(tree)
await run(ctx, ['init-discord', '--plugin-only', '--yes'])
t.is(ctx.exitCode, 0)
t.ok(/hello/.test(tree['~/.discord/plugins/hello.json']))
t.absent(tree['~/.discord/.env'])
})