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
+12 -1
View File
@@ -99,6 +99,7 @@ If `ctx.bare.discordJS` is missing, check `ctx.env.BARE_OS_DISCORD_LOAD_ERROR` (
| Path | When to use | How it starts |
| --- | --- | --- |
| **`init-discord`** | First-time setup: `.env` + hello plugin | Interactive prompts or `--yes --token …` |
| **Stock `/bin/discord-bot`** | Full Bare OS slash surface (`/bare`, `/sys`, `/fs`, …) | Foreground in the guest shell |
| **`bare-os-discord` unit** | Same catalog, background after identity unlock | `systemctl start` when `~/.discord/.env` exists |
| **Guest script** (`~/my-bot.js`) | Your own commands; copy the ping-pong example | `./my-bot.js` or `my-bot.js` in `$PWD` |
@@ -123,7 +124,15 @@ The stock binary and the initd unit share one guest-safe catalog: [`bare-os-disc
## Secrets, `.env`, and token load order
Preferred guest file:
Preferred setup:
```
init-discord
```
That asks for **token**, **guild id**, and **whitelist**, creates `~/.discord/` and `~/.discord/plugins/`, writes `~/.discord/.env`, and installs `/hello` from [`examples/discord-plugins/hello.json`](../examples/discord-plugins/hello.json). Flags (`--token`, `--guild`, `--whitelist`, `--yes`) work for scripts. The full token is never printed.
Or write the file yourself:
```bash
# ~/.discord/.env (VFS path on the personal drive)
@@ -654,6 +663,7 @@ Do not assert on live tokens in CI. Redact `DISCORD_TOKEN` in any fixture `.env`
| macOS `SIGKILL` after replacing booter | Code signature invalidated | Atomic replace + codesign a new inode |
| Channel `ping` ignored | No Message Content | Expected on Guilds-only. Use `/ping` |
| `reply failed: process.emitWarning is not a function` | Bare `process` lacks Node's `emitWarning`; older bots passed `ephemeral:` | Rebuild `/bin/discord-bot` / booter. Replies use `flags` (64) instead of `ephemeral`. Restart the bot. |
| `Supplying "fetchReply" … is deprecated` | Older catalog passed `fetchReply: true` on `reply` / `followUp` | Rebuild. Replies no longer send that option; the message is fetched afterwards for the idle timer. |
`--debug` / `DISCORD_DEBUG=1` prints discord.js debug during login. Token JSON is redacted in the stock `ws send` hook (`"token":"***"`).
@@ -674,6 +684,7 @@ Do not assert on live tokens in CI. Redact `DISCORD_TOKEN` in any fixture `.env`
| Path | Role |
| --- | --- |
| [`packages/bare-os-coreutils/src/init-discord.js`](../packages/bare-os-coreutils/src/init-discord.js) | Setup wizard (`~/.discord/.env` + hello plugin) |
| [`packages/bare-os-coreutils/src/discord-bot.js`](../packages/bare-os-coreutils/src/discord-bot.js) | Foreground guest bot (`run`) |
| [`packages/bare-os-coreutils/man/pages/discord-bot.json`](../packages/bare-os-coreutils/man/pages/discord-bot.json) | `man discord-bot` |
| [`packages/bare-os-booter/lib/bare-os-discord-commands-guest.cjs`](../packages/bare-os-booter/lib/bare-os-discord-commands-guest.cjs) | Slash catalog (guest-safe CJS) |
+4 -2
View File
@@ -1022,6 +1022,8 @@ function discordCmdReplyPayload(result) {
if (result && result.text) payload.content = discordCmdClip(result.text)
if (!payload.content && !payload.embeds) payload.content = '(no output)'
if (result && result.ephemeral) payload.flags = BARE_OS_DISCORD_FLAG_EPHEMERAL
delete payload.fetchReply
delete payload.ephemeral
return payload
}
@@ -5496,9 +5498,9 @@ async function discordSendResult(ctx, interaction, result) {
} else if (interaction.deferred && typeof interaction.editReply === 'function') {
sent = await interaction.editReply(payload)
} else if (interaction.replied && typeof interaction.followUp === 'function') {
sent = await interaction.followUp(Object.assign({ fetchReply: true }, payload))
sent = await interaction.followUp(payload)
} else {
sent = await interaction.reply(Object.assign({ fetchReply: true }, payload))
sent = await interaction.reply(payload)
}
} catch (err) {
if (ctx && ctx.console && typeof ctx.console.error === 'function') {
+1 -1
View File
@@ -311,7 +311,7 @@ function bareOsHexEncode(u8) {
return s
}
var BARE_OS_HELP_BIN_SPACED = "agent appctl appstore arch awk baresay baretop base32 base64 basename basenc btop bundlebee cat chat chgrp chmod chown cksum clear cmp comm corestorectl cp crontab ctxbaredoctor curl cut date dd df dhtctl dhtscan dhttop diff dir dircolors dirname discord-bot du echo edit env exit expand expr factor false find fmt fold getconf getfacl git git-pear grep groups hdms head help holepunch-view holesail hostid hostname hrpc hypershell-board iconv id install irc join journalctl jq kernel-boot-diff kernel-doctor kernel-explain kernel-fsck kernel-home-snapshot kernel-manifest-validate kernel-preflight kernel-triage kill link ln locale logger login logname logout ls man md5sum meshdrop mkdir mkfifo mktemp mount mv nano nice nl nohup nproc numfmt od oidc-publish openssl openssl p2ping p2ptrace paste patch pathcap-verify pathchk pear pear-runtime-matrix peerctl peerdiscover peernote pkg-swarm-index pr printenv printf procstat ps pwd readlink realpath rev rm rmdir routeview savevault say sed seq setfacl sh sha1sum sha224sum sha256sum sha384sum sha512sum shuf sidecar sleep sort split ssh ssh-keygen ssh-keygen sshd sshd stat sum summon swarmdoctor swarmmap swarmtop sync systemctl tac tail tar tar taskmesh tee telnet test theme time timeout touch tr true truncate trustctl tsort tty tui ulimit umount uname unexpand uniq unlink uptime users vdir wc wget which who whoami whois xargs xattr yes"
var BARE_OS_HELP_BIN_SPACED = "agent appctl appstore arch awk baresay baretop base32 base64 basename basenc btop bundlebee cat chat chgrp chmod chown cksum clear cmp comm corestorectl cp crontab ctxbaredoctor curl cut date dd df dhtctl dhtscan dhttop diff dir dircolors dirname discord-bot du echo edit env exit expand expr factor false find fmt fold getconf getfacl git git-pear grep groups hdms head help holepunch-view holesail hostid hostname hrpc hypershell-board iconv id init-discord install irc join journalctl jq kernel-boot-diff kernel-doctor kernel-explain kernel-fsck kernel-home-snapshot kernel-manifest-validate kernel-preflight kernel-triage kill link ln locale logger login logname logout ls man md5sum meshdrop mkdir mkfifo mktemp mount mv nano nice nl nohup nproc numfmt od oidc-publish openssl openssl p2ping p2ptrace paste patch pathcap-verify pathchk pear pear-runtime-matrix peerctl peerdiscover peernote pkg-swarm-index pr printenv printf procstat ps pwd readlink realpath rev rm rmdir routeview savevault say sed seq setfacl sh sha1sum sha224sum sha256sum sha384sum sha512sum shuf sidecar sleep sort split ssh ssh-keygen ssh-keygen sshd sshd stat sum summon swarmdoctor swarmmap swarmtop sync systemctl tac tail tar tar taskmesh tee telnet test theme time timeout touch tr true truncate trustctl tsort tty tui ulimit umount uname unexpand uniq unlink uptime users vdir wc wget which who whoami whois xargs xattr yes"
async function run(ctx, argv) {
ctx.console.log(
'Bare OS — default user: guest | shell builtins: alias, barerc, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: ' +
+539
View File
@@ -0,0 +1,539 @@
/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
/** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */
function bareFormatModeString(mode, type) {
const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-'
const perm = mode & 0o777
const r = (bit) => (perm & bit ? 'r' : '-')
const w = (bit) => (perm & bit ? 'w' : '-')
const x = (bit) => (perm & bit ? 'x' : '-')
return (
typeChar +
r(0o400) +
w(0o200) +
x(0o100) +
r(0o040) +
w(0o020) +
x(0o010) +
r(0o004) +
w(0o002) +
x(0o001)
)
}
/** @param {number} mtimeMs @param {number} [nowMs] */
function bareFormatLsMtime(mtimeMs, nowMs) {
const now = nowMs != null ? nowMs : Date.now()
const d = new Date(mtimeMs)
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
]
const mon = months[d.getMonth()]
const day = String(d.getDate()).padStart(2, ' ')
const sixMo = 180 * 24 * 3600 * 1000
if (Math.abs(now - mtimeMs) > sixMo) {
const yr = String(d.getFullYear()).padStart(4, ' ')
return mon + ' ' + day + ' ' + yr
}
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
return mon + ' ' + day + ' ' + hh + ':' + mm
}
/** @param {number} size */
function barePosixBlocks(size) {
return Math.ceil(Number(size) / 512) || 0
}
/**
* Raw stdout for NUL/binary when **`process.stdout.write`** is missing.
* If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it.
* @param {Record<string, unknown>} ctx
* @param {string | Uint8Array} chunk
* @returns {boolean}
*/
function bareOsEmitRaw(ctx, chunk) {
if (typeof ctx.bareOsBinWrite === 'function') {
const b4 = ctx.b4a
const u8 =
typeof chunk === 'string'
? b4 && typeof b4.from === 'function'
? b4.from(chunk)
: new TextEncoder().encode(chunk)
: chunk
ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8))
return true
}
const w = globalThis.process?.stdout?.write
if (typeof w === 'function') {
w.call(globalThis.process.stdout, chunk)
return true
}
return false
}
/** Session env map (`vfs.env`, then `ctx.env`). Never throws. */
function bareOsEnv(ctx) {
const v = ctx && ctx.vfs && ctx.vfs.env
if (v && typeof v === 'object') return v
const e = ctx && ctx.env
if (e && typeof e === 'object') return e
return {}
}
/**
* Strict POSIX-ish decimal integer (no octal, no exponent, no empty).
* @param {unknown} s
* @returns {number}
*/
function bareOsParseDecInt(s) {
const t = String(s == null ? '' : s).trim()
if (!/^[+-]?(?:0|[1-9][0-9]*)$/.test(t)) return NaN
const n = Number.parseInt(t, 10)
return Number.isSafeInteger(n) ? n : NaN
}
/** @param {unknown} s */
function bareOsParseNonNegInt(s) {
const n = bareOsParseDecInt(s)
return n >= 0 ? n : NaN
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} name
* @param {number} fallback
* @param {number} [min]
* @param {number} [max]
*/
function bareOsEnvInt(ctx, name, fallback, min, max) {
const raw = bareOsEnv(ctx)[name]
if (raw == null || raw === '') return fallback
const n = Number.parseInt(String(raw), 10)
if (!Number.isFinite(n)) return fallback
let v = n
if (min != null && v < min) v = min
if (max != null && v > max) v = max
return v
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} msg
* @param {number} [code]
*/
function bareOsFail(ctx, msg, code) {
if (msg) ctx.console.error(msg)
ctx.exitCode = code == null ? 1 : code
}
/** @param {unknown} e */
function bareOsIsNotFoundErr(e) {
const code = e && typeof e === 'object' ? e.code : ''
if (code === 'ENOENT') return true
const msg = String((e && e.message) || e || '')
return /ENOENT|No such file|not found/i.test(msg)
}
/**
* @param {Record<string, unknown>} ctx
* @param {unknown} buf
* @returns {Uint8Array}
*/
function bareOsToU8(ctx, buf) {
if (!buf) return new Uint8Array(0)
if (buf instanceof Uint8Array) return buf
if (ctx && ctx.b4a && typeof ctx.b4a.from === 'function') return ctx.b4a.from(buf)
return new Uint8Array(buf)
}
/** @param {string} dir @param {string} name */
function bareOsJoinPath(dir, name) {
const d = String(dir || '').replace(/\/+$/, '')
const n = String(name || '').replace(/^\/+/, '')
if (!d || d === '/') return '/' + n
return d + '/' + n
}
/** @param {string} p */
function bareOsBaseName(p) {
const t = String(p || '').replace(/\/+$/, '')
if (!t || t === '/') return t === '/' ? '/' : ''
const i = t.lastIndexOf('/')
return i < 0 ? t : t.slice(i + 1) || t
}
/** @param {string} p */
function bareOsParentDir(p) {
const t = String(p || '').replace(/\/+$/, '') || '/'
if (t === '/') return '/'
const i = t.lastIndexOf('/')
return i <= 0 ? '/' : t.slice(0, i) || '/'
}
/** @param {string} p */
function bareOsNormPath(p) {
return String(p || '').replace(/\/+$/, '') || '/'
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} p
*/
function bareOsResolvePath(ctx, p) {
if (ctx && ctx.vfs && typeof ctx.vfs.resolveLogical === 'function') {
try {
return String(ctx.vfs.resolveLogical(p) || p)
} catch {
/* fall through */
}
}
return String(p || '')
}
/**
* True when dest is src or lives under src (self-copy / self-move).
* @param {Record<string, unknown>} ctx
* @param {string} src
* @param {string} dest
*/
function bareOsDestInsideSrc(ctx, src, dest) {
const s = bareOsNormPath(bareOsResolvePath(ctx, src))
const d = bareOsNormPath(bareOsResolvePath(ctx, dest))
if (s === d) return true
if (s === '/') return d !== '/'
return d === s || d.startsWith(s + '/')
}
const BARE_OS_B64_ALPH =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
/** @param {Uint8Array} u8 */
function bareOsB64Encode(u8) {
let out = ''
let i = 0
for (; i + 2 < u8.length; i += 3) {
const n = (u8[i] << 16) | (u8[i + 1] << 8) | u8[i + 2]
out +=
BARE_OS_B64_ALPH[(n >> 18) & 63] +
BARE_OS_B64_ALPH[(n >> 12) & 63] +
BARE_OS_B64_ALPH[(n >> 6) & 63] +
BARE_OS_B64_ALPH[n & 63]
}
const rest = u8.length - i
if (rest === 1) {
const n = u8[i] << 16
out += BARE_OS_B64_ALPH[(n >> 18) & 63] + BARE_OS_B64_ALPH[(n >> 12) & 63] + '=='
} else if (rest === 2) {
const n = (u8[i] << 16) | (u8[i + 1] << 8)
out +=
BARE_OS_B64_ALPH[(n >> 18) & 63] +
BARE_OS_B64_ALPH[(n >> 12) & 63] +
BARE_OS_B64_ALPH[(n >> 6) & 63] +
'='
}
return out
}
/**
* RFC 4648 Base64 decode (also accepts URL-safe alphabet). Rejects junk.
* @param {string} s
* @returns {Uint8Array}
*/
function bareOsB64Decode(s) {
const t = String(s).replace(/\s+/g, '')
if (!t) return new Uint8Array(0)
if (t.length % 4 === 1) throw new Error('invalid base64 length')
let pad = 0
if (t.endsWith('==')) pad = 2
else if (t.endsWith('=')) pad = 1
const body = pad ? t.slice(0, t.length - pad) : t
const bytes = []
let buf = 0
let bits = 0
for (let i = 0; i < body.length; i++) {
const c = body[i]
let v = BARE_OS_B64_ALPH.indexOf(c)
if (v < 0) {
if (c === '-') v = 62
else if (c === '_') v = 63
else throw new Error('invalid base64 character')
}
buf = (buf << 6) | v
bits += 6
if (bits >= 8) {
bits -= 8
bytes.push((buf >> bits) & 255)
}
}
if (pad) {
const want = Math.floor((body.length * 6) / 8)
if (bytes.length > want) bytes.length = want
}
return new Uint8Array(bytes)
}
/**
* @param {string} s
* @returns {Uint8Array}
*/
function bareOsHexDecode(s) {
const t = String(s).replace(/\s+/g, '')
if (t.length % 2 !== 0) throw new Error('odd hex length')
const out = new Uint8Array(t.length / 2)
for (let i = 0; i < out.length; i++) {
const pair = t.slice(i * 2, i * 2 + 2)
if (!/^[0-9a-fA-F]{2}$/.test(pair)) throw new Error('invalid hex')
out[i] = Number.parseInt(pair, 16)
}
return out
}
/** @param {Uint8Array} u8 */
function bareOsHexEncode(u8) {
let s = ''
for (let i = 0; i < u8.length; i++) s += u8[i].toString(16).padStart(2, '0')
return s
}
/**
* 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
}
+5 -1
View File
@@ -1,7 +1,7 @@
{
"schema": 2,
"profileId": "bare-os-posix-like",
"generatedAt": "2026-08-13T18:35:08.437Z",
"generatedAt": "2026-08-13T18:38:53.092Z",
"note": "Sparse POSIX Issue 7 coverage hints; commandIndex is generated each coreutils build.",
"commandIndex": [
{
@@ -264,6 +264,10 @@
"name": "id",
"tier": "tier1_bin"
},
{
"name": "init-discord",
"tier": "tier1_bin"
},
{
"name": "install",
"tier": "tier1_bin"
+2 -1
View File
@@ -1,6 +1,6 @@
{
"schema": 1,
"atMs": 1786646108433,
"atMs": 1786646333088,
"commands": [
"agent",
"appctl",
@@ -67,6 +67,7 @@
"hypershell-board",
"iconv",
"id",
"init-discord",
"install",
"irc",
"join",
File diff suppressed because one or more lines are too long
@@ -709,6 +709,8 @@ function discordCmdReplyPayload(result) {
if (result && result.text) payload.content = discordCmdClip(result.text)
if (!payload.content && !payload.embeds) payload.content = '(no output)'
if (result && result.ephemeral) payload.flags = BARE_OS_DISCORD_FLAG_EPHEMERAL
delete payload.fetchReply
delete payload.ephemeral
return payload
}
@@ -5183,9 +5185,9 @@ async function discordSendResult(ctx, interaction, result) {
} else if (interaction.deferred && typeof interaction.editReply === 'function') {
sent = await interaction.editReply(payload)
} else if (interaction.replied && typeof interaction.followUp === 'function') {
sent = await interaction.followUp(Object.assign({ fetchReply: true }, payload))
sent = await interaction.followUp(payload)
} else {
sent = await interaction.reply(Object.assign({ fetchReply: true }, payload))
sent = await interaction.reply(payload)
}
} catch (err) {
if (ctx && ctx.console && typeof ctx.console.error === 'function') {
@@ -1390,6 +1390,8 @@ test('process.emitWarning polyfill and reply payload avoid Bare throw', (t) => {
t.ok(!('ephemeral' in open))
const hidden = cmds.replyPayload({ text: 'nope', ephemeral: true })
t.ok(!('ephemeral' in hidden))
t.ok(!('fetchReply' in hidden))
t.ok(!('fetchReply' in open))
t.is(hidden.flags, 64)
const ws = req('./vendor/bare-discord-js/src/adapters/whatwg-ws.cjs')
t.is(typeof ws.installBareOsProcessEmitWarning, 'function')
@@ -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'])
})
@@ -1022,6 +1022,8 @@ function discordCmdReplyPayload(result) {
if (result && result.text) payload.content = discordCmdClip(result.text)
if (!payload.content && !payload.embeds) payload.content = '(no output)'
if (result && result.ephemeral) payload.flags = BARE_OS_DISCORD_FLAG_EPHEMERAL
delete payload.fetchReply
delete payload.ephemeral
return payload
}
@@ -5496,9 +5498,9 @@ async function discordSendResult(ctx, interaction, result) {
} else if (interaction.deferred && typeof interaction.editReply === 'function') {
sent = await interaction.editReply(payload)
} else if (interaction.replied && typeof interaction.followUp === 'function') {
sent = await interaction.followUp(Object.assign({ fetchReply: true }, payload))
sent = await interaction.followUp(payload)
} else {
sent = await interaction.reply(Object.assign({ fetchReply: true }, payload))
sent = await interaction.reply(payload)
}
} catch (err) {
if (ctx && ctx.console && typeof ctx.console.error === 'function') {
+1 -1
View File
@@ -311,7 +311,7 @@ function bareOsHexEncode(u8) {
return s
}
var BARE_OS_HELP_BIN_SPACED = "agent appctl appstore arch awk baresay baretop base32 base64 basename basenc btop bundlebee cat chat chgrp chmod chown cksum clear cmp comm corestorectl cp crontab ctxbaredoctor curl cut date dd df dhtctl dhtscan dhttop diff dir dircolors dirname discord-bot du echo edit env exit expand expr factor false find fmt fold getconf getfacl git git-pear grep groups hdms head help holepunch-view holesail hostid hostname hrpc hypershell-board iconv id install irc join journalctl jq kernel-boot-diff kernel-doctor kernel-explain kernel-fsck kernel-home-snapshot kernel-manifest-validate kernel-preflight kernel-triage kill link ln locale logger login logname logout ls man md5sum meshdrop mkdir mkfifo mktemp mount mv nano nice nl nohup nproc numfmt od oidc-publish openssl openssl p2ping p2ptrace paste patch pathcap-verify pathchk pear pear-runtime-matrix peerctl peerdiscover peernote pkg-swarm-index pr printenv printf procstat ps pwd readlink realpath rev rm rmdir routeview savevault say sed seq setfacl sh sha1sum sha224sum sha256sum sha384sum sha512sum shuf sidecar sleep sort split ssh ssh-keygen ssh-keygen sshd sshd stat sum summon swarmdoctor swarmmap swarmtop sync systemctl tac tail tar tar taskmesh tee telnet test theme time timeout touch tr true truncate trustctl tsort tty tui ulimit umount uname unexpand uniq unlink uptime users vdir wc wget which who whoami whois xargs xattr yes"
var BARE_OS_HELP_BIN_SPACED = "agent appctl appstore arch awk baresay baretop base32 base64 basename basenc btop bundlebee cat chat chgrp chmod chown cksum clear cmp comm corestorectl cp crontab ctxbaredoctor curl cut date dd df dhtctl dhtscan dhttop diff dir dircolors dirname discord-bot du echo edit env exit expand expr factor false find fmt fold getconf getfacl git git-pear grep groups hdms head help holepunch-view holesail hostid hostname hrpc hypershell-board iconv id init-discord install irc join journalctl jq kernel-boot-diff kernel-doctor kernel-explain kernel-fsck kernel-home-snapshot kernel-manifest-validate kernel-preflight kernel-triage kill link ln locale logger login logname logout ls man md5sum meshdrop mkdir mkfifo mktemp mount mv nano nice nl nohup nproc numfmt od oidc-publish openssl openssl p2ping p2ptrace paste patch pathcap-verify pathchk pear pear-runtime-matrix peerctl peerdiscover peernote pkg-swarm-index pr printenv printf procstat ps pwd readlink realpath rev rm rmdir routeview savevault say sed seq setfacl sh sha1sum sha224sum sha256sum sha384sum sha512sum shuf sidecar sleep sort split ssh ssh-keygen ssh-keygen sshd sshd stat sum summon swarmdoctor swarmmap swarmtop sync systemctl tac tail tar tar taskmesh tee telnet test theme time timeout touch tr true truncate trustctl tsort tty tui ulimit umount uname unexpand uniq unlink uptime users vdir wc wget which who whoami whois xargs xattr yes"
async function run(ctx, argv) {
ctx.console.log(
'Bare OS — default user: guest | shell builtins: alias, barerc, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: ' +
@@ -0,0 +1,539 @@
/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
/** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */
function bareFormatModeString(mode, type) {
const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-'
const perm = mode & 0o777
const r = (bit) => (perm & bit ? 'r' : '-')
const w = (bit) => (perm & bit ? 'w' : '-')
const x = (bit) => (perm & bit ? 'x' : '-')
return (
typeChar +
r(0o400) +
w(0o200) +
x(0o100) +
r(0o040) +
w(0o020) +
x(0o010) +
r(0o004) +
w(0o002) +
x(0o001)
)
}
/** @param {number} mtimeMs @param {number} [nowMs] */
function bareFormatLsMtime(mtimeMs, nowMs) {
const now = nowMs != null ? nowMs : Date.now()
const d = new Date(mtimeMs)
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
]
const mon = months[d.getMonth()]
const day = String(d.getDate()).padStart(2, ' ')
const sixMo = 180 * 24 * 3600 * 1000
if (Math.abs(now - mtimeMs) > sixMo) {
const yr = String(d.getFullYear()).padStart(4, ' ')
return mon + ' ' + day + ' ' + yr
}
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
return mon + ' ' + day + ' ' + hh + ':' + mm
}
/** @param {number} size */
function barePosixBlocks(size) {
return Math.ceil(Number(size) / 512) || 0
}
/**
* Raw stdout for NUL/binary when **`process.stdout.write`** is missing.
* If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it.
* @param {Record<string, unknown>} ctx
* @param {string | Uint8Array} chunk
* @returns {boolean}
*/
function bareOsEmitRaw(ctx, chunk) {
if (typeof ctx.bareOsBinWrite === 'function') {
const b4 = ctx.b4a
const u8 =
typeof chunk === 'string'
? b4 && typeof b4.from === 'function'
? b4.from(chunk)
: new TextEncoder().encode(chunk)
: chunk
ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8))
return true
}
const w = globalThis.process?.stdout?.write
if (typeof w === 'function') {
w.call(globalThis.process.stdout, chunk)
return true
}
return false
}
/** Session env map (`vfs.env`, then `ctx.env`). Never throws. */
function bareOsEnv(ctx) {
const v = ctx && ctx.vfs && ctx.vfs.env
if (v && typeof v === 'object') return v
const e = ctx && ctx.env
if (e && typeof e === 'object') return e
return {}
}
/**
* Strict POSIX-ish decimal integer (no octal, no exponent, no empty).
* @param {unknown} s
* @returns {number}
*/
function bareOsParseDecInt(s) {
const t = String(s == null ? '' : s).trim()
if (!/^[+-]?(?:0|[1-9][0-9]*)$/.test(t)) return NaN
const n = Number.parseInt(t, 10)
return Number.isSafeInteger(n) ? n : NaN
}
/** @param {unknown} s */
function bareOsParseNonNegInt(s) {
const n = bareOsParseDecInt(s)
return n >= 0 ? n : NaN
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} name
* @param {number} fallback
* @param {number} [min]
* @param {number} [max]
*/
function bareOsEnvInt(ctx, name, fallback, min, max) {
const raw = bareOsEnv(ctx)[name]
if (raw == null || raw === '') return fallback
const n = Number.parseInt(String(raw), 10)
if (!Number.isFinite(n)) return fallback
let v = n
if (min != null && v < min) v = min
if (max != null && v > max) v = max
return v
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} msg
* @param {number} [code]
*/
function bareOsFail(ctx, msg, code) {
if (msg) ctx.console.error(msg)
ctx.exitCode = code == null ? 1 : code
}
/** @param {unknown} e */
function bareOsIsNotFoundErr(e) {
const code = e && typeof e === 'object' ? e.code : ''
if (code === 'ENOENT') return true
const msg = String((e && e.message) || e || '')
return /ENOENT|No such file|not found/i.test(msg)
}
/**
* @param {Record<string, unknown>} ctx
* @param {unknown} buf
* @returns {Uint8Array}
*/
function bareOsToU8(ctx, buf) {
if (!buf) return new Uint8Array(0)
if (buf instanceof Uint8Array) return buf
if (ctx && ctx.b4a && typeof ctx.b4a.from === 'function') return ctx.b4a.from(buf)
return new Uint8Array(buf)
}
/** @param {string} dir @param {string} name */
function bareOsJoinPath(dir, name) {
const d = String(dir || '').replace(/\/+$/, '')
const n = String(name || '').replace(/^\/+/, '')
if (!d || d === '/') return '/' + n
return d + '/' + n
}
/** @param {string} p */
function bareOsBaseName(p) {
const t = String(p || '').replace(/\/+$/, '')
if (!t || t === '/') return t === '/' ? '/' : ''
const i = t.lastIndexOf('/')
return i < 0 ? t : t.slice(i + 1) || t
}
/** @param {string} p */
function bareOsParentDir(p) {
const t = String(p || '').replace(/\/+$/, '') || '/'
if (t === '/') return '/'
const i = t.lastIndexOf('/')
return i <= 0 ? '/' : t.slice(0, i) || '/'
}
/** @param {string} p */
function bareOsNormPath(p) {
return String(p || '').replace(/\/+$/, '') || '/'
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} p
*/
function bareOsResolvePath(ctx, p) {
if (ctx && ctx.vfs && typeof ctx.vfs.resolveLogical === 'function') {
try {
return String(ctx.vfs.resolveLogical(p) || p)
} catch {
/* fall through */
}
}
return String(p || '')
}
/**
* True when dest is src or lives under src (self-copy / self-move).
* @param {Record<string, unknown>} ctx
* @param {string} src
* @param {string} dest
*/
function bareOsDestInsideSrc(ctx, src, dest) {
const s = bareOsNormPath(bareOsResolvePath(ctx, src))
const d = bareOsNormPath(bareOsResolvePath(ctx, dest))
if (s === d) return true
if (s === '/') return d !== '/'
return d === s || d.startsWith(s + '/')
}
const BARE_OS_B64_ALPH =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
/** @param {Uint8Array} u8 */
function bareOsB64Encode(u8) {
let out = ''
let i = 0
for (; i + 2 < u8.length; i += 3) {
const n = (u8[i] << 16) | (u8[i + 1] << 8) | u8[i + 2]
out +=
BARE_OS_B64_ALPH[(n >> 18) & 63] +
BARE_OS_B64_ALPH[(n >> 12) & 63] +
BARE_OS_B64_ALPH[(n >> 6) & 63] +
BARE_OS_B64_ALPH[n & 63]
}
const rest = u8.length - i
if (rest === 1) {
const n = u8[i] << 16
out += BARE_OS_B64_ALPH[(n >> 18) & 63] + BARE_OS_B64_ALPH[(n >> 12) & 63] + '=='
} else if (rest === 2) {
const n = (u8[i] << 16) | (u8[i + 1] << 8)
out +=
BARE_OS_B64_ALPH[(n >> 18) & 63] +
BARE_OS_B64_ALPH[(n >> 12) & 63] +
BARE_OS_B64_ALPH[(n >> 6) & 63] +
'='
}
return out
}
/**
* RFC 4648 Base64 decode (also accepts URL-safe alphabet). Rejects junk.
* @param {string} s
* @returns {Uint8Array}
*/
function bareOsB64Decode(s) {
const t = String(s).replace(/\s+/g, '')
if (!t) return new Uint8Array(0)
if (t.length % 4 === 1) throw new Error('invalid base64 length')
let pad = 0
if (t.endsWith('==')) pad = 2
else if (t.endsWith('=')) pad = 1
const body = pad ? t.slice(0, t.length - pad) : t
const bytes = []
let buf = 0
let bits = 0
for (let i = 0; i < body.length; i++) {
const c = body[i]
let v = BARE_OS_B64_ALPH.indexOf(c)
if (v < 0) {
if (c === '-') v = 62
else if (c === '_') v = 63
else throw new Error('invalid base64 character')
}
buf = (buf << 6) | v
bits += 6
if (bits >= 8) {
bits -= 8
bytes.push((buf >> bits) & 255)
}
}
if (pad) {
const want = Math.floor((body.length * 6) / 8)
if (bytes.length > want) bytes.length = want
}
return new Uint8Array(bytes)
}
/**
* @param {string} s
* @returns {Uint8Array}
*/
function bareOsHexDecode(s) {
const t = String(s).replace(/\s+/g, '')
if (t.length % 2 !== 0) throw new Error('odd hex length')
const out = new Uint8Array(t.length / 2)
for (let i = 0; i < out.length; i++) {
const pair = t.slice(i * 2, i * 2 + 2)
if (!/^[0-9a-fA-F]{2}$/.test(pair)) throw new Error('invalid hex')
out[i] = Number.parseInt(pair, 16)
}
return out
}
/** @param {Uint8Array} u8 */
function bareOsHexEncode(u8) {
let s = ''
for (let i = 0; i < u8.length; i++) s += u8[i].toString(16).padStart(2, '0')
return s
}
/**
* 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
}
@@ -1,7 +1,7 @@
{
"schema": 2,
"profileId": "bare-os-posix-like",
"generatedAt": "2026-08-13T18:35:08.437Z",
"generatedAt": "2026-08-13T18:38:53.092Z",
"note": "Sparse POSIX Issue 7 coverage hints; commandIndex is generated each coreutils build.",
"commandIndex": [
{
@@ -264,6 +264,10 @@
"name": "id",
"tier": "tier1_bin"
},
{
"name": "init-discord",
"tier": "tier1_bin"
},
{
"name": "install",
"tier": "tier1_bin"
@@ -1,6 +1,6 @@
{
"schema": 1,
"atMs": 1786646108433,
"atMs": 1786646333088,
"commands": [
"agent",
"appctl",
@@ -67,6 +67,7 @@
"hypershell-board",
"iconv",
"id",
"init-discord",
"install",
"irc",
"join",
File diff suppressed because one or more lines are too long