Files
bare-operating-system/packages/bare-os-booter/lib/bare-os-discord-commands-guest.cjs
T
Raven Scott 58e6e62ee9
Release rolling / release (push) Successful in 13m50s
better emebed design
2026-08-13 14:11:01 -04:00

4610 lines
139 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Bare OS Discord slash-command catalog (guest-safe: no import/export).
* Used by /bin/discord-bot (prepended) and the bare-os-discord initd unit.
* Host/pack must static-import this .cjs (createRequire fails under app.bundle).
*/
var BARE_OS_DISCORD_REPLY_MAX = 1900
var BARE_OS_DISCORD_FS_MAX = 12 * 1024
/** discord.js MessageFlags.Ephemeral (1 << 6). Avoid the deprecated `ephemeral` option. */
var BARE_OS_DISCORD_FLAG_EPHEMERAL = 64
var BARE_OS_DISCORD_WHITELIST_DENY =
'Access denied. Your Discord user id is not on DISCORD_ID_WHITELIST.'
var BARE_OS_DISCORD_COLOR = 0x5865f2
var BARE_OS_DISCORD_COLOR_OK = 0x57f287
var BARE_OS_DISCORD_COLOR_WARN = 0xfee75c
var BARE_OS_DISCORD_COLOR_ERR = 0xed4245
var BARE_OS_DISCORD_RUN_ALLOW = {
uname: 1,
whoami: 1,
hostname: 1,
date: 1,
uptime: 1,
id: 1,
pwd: 1,
arch: 1,
nproc: 1,
help: 1,
motd: 1,
true: 1,
false: 1,
df: 1,
ps: 1,
procstat: 1,
'uname -a': 1
}
var BARE_OS_DISCORD_KNOWN_UNITS = [
'bare-os-discord',
'bare-os-www',
'bare-os-chat',
'bare-holesail',
'kernel-logger'
]
var BARE_OS_DISCORD_EDIT_CHUNK = 4000
var BARE_OS_DISCORD_EDIT_MAX_CHUNKS = 5
var BARE_OS_DISCORD_IDLE_MS = 2 * 60 * 1000
var BARE_OS_DISCORD_EDIT_TTL_MS = BARE_OS_DISCORD_IDLE_MS
var BARE_OS_DISCORD_EDIT_SESSIONS = Object.create(null)
var BARE_OS_DISCORD_FM_PAGE = 20
var BARE_OS_DISCORD_FM_TTL_MS = BARE_OS_DISCORD_IDLE_MS
var BARE_OS_DISCORD_FM_SESSIONS = Object.create(null)
var BARE_OS_DISCORD_LIVE = Object.create(null)
var BARE_OS_DISCORD_IDLE_SEQ = 0
var BARE_OS_DISCORD_EDIT_SUGGEST = [
'~/notes.txt',
'~/.barerc',
'~/TODO.md',
'/tmp/scratch.txt'
]
var BARE_OS_DISCORD_FS_SUGGEST = [
'~',
'/etc/os-release',
'/etc/motd',
'/proc/uptime',
'/proc/meminfo',
'/proc/bare_os/features',
'/proc/bare_os/security_posture.json',
'/proc/bare_os/process_table.json',
'/proc/bare_os/net_summary.json',
'/var/log/bare-os/discord.log',
'/var/log/bare-os/kernel-console.log',
'/share/man/man.json',
'/tmp'
]
/**
* bare-process has no process.emitWarning. discord.js 14 calls it whenever
* reply options include the deprecated `ephemeral` key (even when false).
*/
function discordInstallProcessEmitWarning(proc) {
const p =
proc ||
(typeof globalThis.process !== 'undefined' ? globalThis.process : null)
if (!p || typeof p.emitWarning === 'function') return p
p.emitWarning = function emitWarning(warning, type, code) {
let name = 'Warning'
let id = ''
let msg = ''
if (warning && typeof warning === 'object' && warning.type && !(warning instanceof Error)) {
name = String(warning.type || 'Warning')
id = String(warning.code || '')
msg = String(warning.message || warning)
} else if (type && typeof type === 'object') {
name = String(type.type || 'Warning')
id = String(type.code || '')
msg = warning instanceof Error ? warning.message : String(warning)
} else {
name = typeof type === 'string' ? type : 'Warning'
id = typeof code === 'string' ? code : ''
msg = warning instanceof Error ? warning.message : String(warning)
}
const line = id ? name + ' [' + id + ']: ' + msg : name + ': ' + msg
try {
if (typeof p.emit === 'function') {
const err = warning instanceof Error ? warning : new Error(msg)
err.name = name
if (id) err.code = id
p.emit('warning', err)
}
} catch {
/* ignore */
}
try {
if (typeof console !== 'undefined' && typeof console.error === 'function') {
console.error(line)
}
} catch {
/* ignore */
}
}
return p
}
discordInstallProcessEmitWarning()
function discordCmdClip(text, max) {
const s = String(text == null ? '' : text)
const n = max || BARE_OS_DISCORD_REPLY_MAX
if (s.length <= n) return s
return s.slice(0, n - 20) + '\n…(truncated)'
}
function discordCmdRedact(text) {
return String(text == null ? '' : text)
.replace(/[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{5,}\.[A-Za-z0-9_\-]{20,}/g, '[token]')
.replace(/(DISCORD_TOKEN|BOT_TOKEN|TOKEN|SECRET|PASSWORD|PASSWD|API_KEY)\s*[=:]\s*\S+/gi, '$1=[redacted]')
}
function discordCmdFence(text, lang) {
const body = discordCmdClip(discordCmdRedact(text))
return '```' + (lang || '') + '\n' + body.replace(/```/g, '`ˋ`') + '\n```'
}
function discordTryJson(raw) {
if (raw && typeof raw === 'object') return raw
const s = String(raw || '').trim()
if (!s || (s.charAt(0) !== '{' && s.charAt(0) !== '[')) return null
try {
return JSON.parse(s)
} catch {
return null
}
}
function discordPrettyBytes(n) {
const x = Number(n)
if (!Number.isFinite(x) || x < 0) return String(n)
if (x < 1024) return String(Math.round(x)) + ' B'
const units = ['KiB', 'MiB', 'GiB', 'TiB']
let v = x / 1024
let i = 0
while (v >= 1024 && i < units.length - 1) {
v /= 1024
i++
}
return (v >= 10 ? v.toFixed(0) : v.toFixed(1)) + ' ' + units[i]
}
function discordIsByteishKey(k) {
return /byte|bytes|mem|rss|size|freemem|totalmem|avail|queued|f_bsize|f_frsize/i.test(
String(k || '')
)
}
function discordPrettyValue(key, val) {
if (val == null || val === '') return '—'
if (typeof val === 'boolean') return val ? 'yes' : 'no'
if (typeof val === 'number') {
if (discordIsByteishKey(key) && val >= 1024) return discordPrettyBytes(val)
if (key === 'atMs' || /AtMs$/.test(key)) {
if (val > 1e12) return new Date(val).toISOString()
return String(val)
}
if (/Ms$/.test(key) && val >= 1000) {
const s = Math.round(val / 1000)
if (s < 60) return s + 's'
return Math.floor(s / 60) + 'm ' + (s % 60) + 's'
}
return String(val)
}
if (typeof val === 'string') {
const t = discordCmdRedact(val)
if (/^[0-9a-f]{24,}$/i.test(t)) return '`' + t.slice(0, 16) + (t.length > 16 ? '…' : '') + '`'
return t.length > 180 ? t.slice(0, 177) + '…' : t
}
if (Array.isArray(val)) {
if (!val.length) return '(none)'
if (val.every(function (x) { return x == null || typeof x !== 'object' })) {
const shown = val.slice(0, 10).map(function (x) {
return String(x)
})
return shown.join(', ') + (val.length > 10 ? ' +' + (val.length - 10) : '')
}
return String(val.length) + ' items'
}
if (typeof val === 'object') return Object.keys(val).length + ' keys'
return String(val)
}
function discordSkipPrettyKey(k) {
return /TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE|CREDENTIAL|API_KEY/i.test(k) ||
k === 'note' ||
k === 'schema' ||
k === 'schemaVersion'
}
function discordValueEmpty(val) {
if (val == null) return true
if (val === '') return true
if (val === '—') return true
if (Array.isArray(val) && !val.length) return true
if (typeof val === 'object' && !Array.isArray(val) && !Object.keys(val).length) return true
return false
}
function discordHumanLabel(key) {
const known = {
schemaVersion: 'Schema',
topicHex: 'Topic',
peerCount: 'Peers',
seedHandshakeError: 'Handshake',
seedRole: 'Role',
replicationQueueDepth: 'Queue depth',
replicationQueue: 'Replication',
stagingSlot: 'Staging',
snapshotHints: 'Snapshot',
peerFirewallStats: 'Firewall',
peerFirewallAcceptedTotal: 'Accepted',
peerFirewallRejectedTotal: 'Rejected',
peerFirewallInboundTotal: 'Inbound',
peerFirewallOutboundTotal: 'Outbound',
peerFirewallE2e: 'Firewall e2e',
manifestPathCount: 'Manifests',
localRamBlockCount: 'RAM blocks',
hypercoreLengthHint: 'Core length',
queueDepthEstimate: 'Queue estimate',
snapshotWorkflowNote: 'Note',
activeSlot: 'Active slot',
pendingSlot: 'Pending',
previousSlot: 'Previous',
canarySlot: 'Canary',
drainDeadlineMs: 'Drain',
atMs: 'Updated'
}
if (known[key]) return known[key]
const s = String(key || '')
.replace(/[_-]+/g, ' ')
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/\s+/g, ' ')
.trim()
if (!s) return String(key || '')
return s.charAt(0).toUpperCase() + s.slice(1)
}
function discordCollectFields(obj, prefix, out, depth) {
if (!obj || typeof obj !== 'object' || Array.isArray(obj) || depth > 2) return
const keys = Object.keys(obj)
for (let i = 0; i < keys.length && out.length < 18; i++) {
const k = keys[i]
if (discordSkipPrettyKey(k)) continue
const val = obj[k]
if (discordValueEmpty(val)) continue
const label = prefix ? prefix + ' · ' + discordHumanLabel(k) : discordHumanLabel(k)
if (val && typeof val === 'object' && !Array.isArray(val) && depth < 2) {
const lines = []
const sub = Object.keys(val)
for (let j = 0; j < sub.length && lines.length < 8; j++) {
if (discordSkipPrettyKey(sub[j])) continue
const sv = val[sub[j]]
if (discordValueEmpty(sv)) continue
if (sv && typeof sv === 'object') continue
const pretty = discordPrettyValue(sub[j], sv)
if (pretty === '—' || pretty === '(none)') continue
lines.push('**' + discordHumanLabel(sub[j]) + '** ' + pretty)
}
if (lines.length) out.push(discordField(discordHumanLabel(k), lines.join('\n'), false))
} else {
const pretty = discordPrettyValue(k, val)
if (pretty === '—' || pretty === '(none)') continue
out.push(discordField(label, pretty, true))
}
}
}
function discordPrettyUptime(raw) {
const s = String(raw || '').trim()
const sec = Number(s.split(/\s+/)[0])
if (!Number.isFinite(sec) || sec < 0) return s || '—'
const d = Math.floor(sec / 86400)
const h = Math.floor((sec % 86400) / 3600)
const m = Math.floor((sec % 3600) / 60)
const parts = []
if (d) parts.push(d + 'd')
if (h || d) parts.push(h + 'h')
parts.push(m + 'm')
return parts.join(' ') + ' (' + Math.round(sec) + 's)'
}
function discordParseMeminfo(text) {
const out = {}
const lines = String(text || '').split(/\r?\n/)
for (let i = 0; i < lines.length; i++) {
const m = /^([A-Za-z0-9_()]+):\s+(\d+)/.exec(lines[i])
if (!m) continue
out[m[1]] = Number(m[2]) * 1024
}
return out
}
function discordParseSystemctlList(text) {
const rows = []
const lines = String(text || '').split(/\r?\n/)
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim()
if (!line || /^UNIT\b/.test(line) || line.indexOf('LOAD') === 0) continue
const parts = line.split(/\s+/)
if (parts.length < 4) continue
rows.push({
unit: parts[0].replace(/\.service$/, ''),
load: parts[1] || '',
preset: parts[2] || '',
active: parts[3] || '',
sub: parts[4] || '',
desc: parts.slice(5).join(' ')
})
}
return rows
}
function discordPrettySnapshot(title, raw, extras) {
const parsed = discordTryJson(raw)
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
const text = raw == null ? '' : String(raw)
return discordResult(
discordEmbed({
title: title,
desc: text
? discordCmdFence(discordCmdClip(text, 1400))
: 'unavailable'
}),
extras
)
}
const fields = []
discordCollectFields(parsed, '', fields, 0)
const note = typeof parsed.note === 'string' ? parsed.note.slice(0, 280) : ''
return discordResult(
discordEmbed({
title: title,
desc: note,
fields: fields.slice(0, 18),
color: extras && extras.color
}),
extras
)
}
function discordHex12(ctx, u8) {
if (!u8 || typeof u8.length !== 'number') return ''
if (ctx && ctx.b4a && typeof ctx.b4a.toString === 'function') {
try {
return String(ctx.b4a.toString(u8, 'hex') || '').slice(0, 12)
} catch {
/* fall through */
}
}
let hex = ''
const n = Math.min(u8.length, 6)
for (let i = 0; i < n; i++) {
const h = (u8[i] & 0xff).toString(16)
hex += h.length < 2 ? '0' + h : h
}
return hex.slice(0, 12)
}
/** Prefer live VFS env (identity mutates this). Keep ctx.env in sync. */
function discordSyncSessionIdentity(ctx) {
if (!ctx) return
const vfsEnv = ctx.vfs && ctx.vfs.env && typeof ctx.vfs.env === 'object' ? ctx.vfs.env : null
const env = vfsEnv || (ctx.env && typeof ctx.env === 'object' ? ctx.env : null)
if (!env) return
if (ctx.env && vfsEnv && ctx.env !== vfsEnv) {
const keys = [
'USER',
'LOGNAME',
'HOME',
'PWD',
'UID',
'GID',
'GROUP',
'SHELL',
'BARE_OS_IDENTITY',
'BARE_OS_PUBLIC_KEY'
]
for (let i = 0; i < keys.length; i++) {
const k = keys[i]
if (vfsEnv[k] != null && String(vfsEnv[k]) !== '') ctx.env[k] = vfsEnv[k]
}
}
const ident = ctx.identity
if (!ident || ident.state !== 'unlocked') return
let name = String(env.USER || env.LOGNAME || '').trim()
if (!name || name === 'guest') {
name = discordHex12(ctx, ident.publicKey)
if (!name) return
env.USER = name
env.LOGNAME = name
env.GROUP = name
if (ctx.env && ctx.env !== env) {
ctx.env.USER = name
ctx.env.LOGNAME = name
}
}
if (!env.HOME || env.HOME === '/home/guest') env.HOME = '/home/' + name
if (!env.PWD || env.PWD === '/home/guest') env.PWD = env.HOME
env.BARE_OS_IDENTITY = 'unlocked'
}
function discordCmdEnv(ctx) {
discordSyncSessionIdentity(ctx)
if (ctx && ctx.vfs && ctx.vfs.env) return ctx.vfs.env
if (ctx && ctx.env) return ctx.env
return {}
}
function discordSessionUser(ctx) {
const e = discordCmdEnv(ctx)
const u = String(e.USER || e.LOGNAME || e.USERNAME || '').trim()
return u || 'guest'
}
function discordUniqueComponents(rows) {
const seen = Object.create(null)
const out = []
if (!Array.isArray(rows)) return out
for (let i = 0; i < rows.length && out.length < 5; i++) {
const row = rows[i]
if (!row || !Array.isArray(row.components)) continue
const comps = []
for (let j = 0; j < row.components.length; j++) {
const c = row.components[j]
if (!c) continue
const id = c.custom_id != null ? String(c.custom_id) : ''
if (id) {
if (seen[id]) continue
seen[id] = 1
}
comps.push(c)
}
if (!comps.length) continue
out.push({
type: row.type || 1,
components: comps
})
}
return out
}
function discordCmdReplyPayload(result) {
const payload = {}
if (result && result.embeds && result.embeds.length) payload.embeds = result.embeds
if (result && result.components && result.components.length) {
payload.components = discordUniqueComponents(result.components)
if (!payload.components.length) delete payload.components
}
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
return payload
}
function discordEmbed(opts) {
const o = opts || {}
const e = {
color: o.color == null ? BARE_OS_DISCORD_COLOR : o.color,
timestamp: new Date().toISOString(),
footer: { text: o.footer || 'Bare OS · expires after 2m idle' }
}
if (o.title) e.title = String(o.title).slice(0, 256)
if (o.desc) e.description = discordCmdClip(discordCmdRedact(o.desc), 1800)
if (o.fields && o.fields.length) {
const fields = []
for (let i = 0; i < o.fields.length && fields.length < 25; i++) {
const f = o.fields[i]
if (!f) continue
const v = String(f.value == null ? '' : f.value).trim()
if (!v || v === '—') continue
fields.push(f)
}
if (fields.length) e.fields = fields
}
if (o.author) e.author = o.author
return e
}
function discordField(name, value, inline) {
let v = value == null || value === '' ? '—' : String(value)
v = discordCmdClip(discordCmdRedact(v), 1024)
if (!v) v = '—'
return { name: String(name).slice(0, 256), value: v, inline: Boolean(inline) }
}
function discordButtons(items) {
const row = { type: 1, components: [] }
for (let i = 0; i < items.length && row.components.length < 5; i++) {
const it = items[i]
if (!it || !it.id) continue
row.components.push({
type: 2,
style: it.style || 2,
label: String(it.label || it.id).slice(0, 80),
custom_id: String(it.id).slice(0, 100)
})
}
return row.components.length ? row : null
}
function discordSelect(id, placeholder, options) {
const opts = []
for (let i = 0; i < options.length && opts.length < 25; i++) {
const o = options[i]
if (!o) continue
const value = String(o.value || o.label || '').slice(0, 100)
if (!value) continue
const item = {
label: String(o.label || value).slice(0, 100),
value: value
}
if (o.description) item.description = String(o.description).slice(0, 100)
opts.push(item)
}
if (!opts.length) return null
return {
type: 1,
components: [
{
type: 3,
custom_id: String(id).slice(0, 100),
placeholder: String(placeholder || 'Choose…').slice(0, 150),
min_values: 1,
max_values: 1,
options: opts
}
]
}
}
function discordNavRows() {
const rows = []
const a = discordButtons([
{ id: 'nav:panel', label: 'Panel', style: 1 },
{ id: 'bare:status', label: 'Status', style: 1 },
{ id: 'bare:whoami', label: 'Whoami' },
{ id: 'bare:help', label: 'Help' },
{ id: 'sys:doctor', label: 'Doctor' }
])
const b = discordButtons([
{ id: 'svc:list', label: 'Services' },
{ id: 'sys:ps', label: 'Processes' },
{ id: 'net:summary', label: 'Network' },
{ id: 'say:compose', label: 'Say…', style: 3 },
{ id: 'run:compose', label: 'Run…' }
])
const c = discordButtons([
{ id: 'edit:new', label: 'Edit file…', style: 1 },
{ id: 'create:new', label: 'Create file…', style: 3 },
{ id: 'fm:home', label: 'Files', style: 1 },
{ id: 'set:home', label: 'Settings' }
])
if (a) rows.push(a)
if (b) rows.push(b)
if (c) rows.push(c)
return rows
}
function discordResult(embed, extra) {
const out = { embeds: [embed] }
const extraRows = extra && extra.components ? extra.components : []
const nav = extra && extra.nav === false ? [] : discordNavRows()
const rows = discordUniqueComponents(extraRows.concat(nav))
if (rows.length) out.components = rows
if (extra && extra.ephemeral) out.ephemeral = true
if (extra && extra.text) out.text = extra.text
if (extra && extra.editPath) out.editPath = extra.editPath
if (extra && extra.created) out.created = true
return out
}
async function discordCmdReadText(ctx, logicalPath) {
if (!logicalPath || typeof ctx.vfs?.readFile !== 'function') return ''
try {
const buf = await ctx.vfs.readFile(logicalPath)
if (!buf) return ''
if (typeof ctx.b4a?.toString === 'function') return ctx.b4a.toString(buf)
return String(buf)
} catch {
return ''
}
}
async function discordCmdReadJson(ctx, logicalPath) {
const t = await discordCmdReadText(ctx, logicalPath)
if (!t) return null
try {
return JSON.parse(t)
} catch {
return null
}
}
function discordCmdPathOk(raw) {
const p = String(raw || '').trim() || '.'
if (!p || p.indexOf('\0') >= 0) return null
if (p.indexOf('..') >= 0) return null
if (p === '~/.discord/.env' || p === '~/.discord.env') return null
const allow =
p === '.' ||
p === '~' ||
p.charAt(0) === '~' ||
p.indexOf('/proc') === 0 ||
p.indexOf('/etc') === 0 ||
p.indexOf('/var/log') === 0 ||
p.indexOf('/run') === 0 ||
p.indexOf('/home') === 0 ||
p.indexOf('/usr/share') === 0 ||
p.indexOf('/share') === 0 ||
p.indexOf('/tmp') === 0
return allow ? p : null
}
function discordCmdPathWriteOk(ctx, raw) {
const p = discordCmdPathOk(raw)
if (!p) return null
if (p === '~/.discord/.env' || p === '~/.discord.env') return null
if (p.indexOf('~/.discord') === 0) return null
const user = discordSessionUser(ctx)
const home = '/home/' + user
const allow =
p === '~' ||
p.indexOf('~/') === 0 ||
p.indexOf('/tmp/') === 0 ||
p === '/tmp' ||
p === home ||
p.indexOf(home + '/') === 0
return allow ? p : null
}
async function discordFileExists(ctx, p) {
if (!ctx || !ctx.vfs) return false
const stfn = ctx.vfs.lstat || ctx.vfs.stat
if (typeof stfn === 'function') {
try {
const st = await stfn.call(ctx.vfs, p)
return Boolean(st)
} catch {
return false
}
}
if (typeof ctx.vfs.readFile !== 'function') return false
try {
await ctx.vfs.readFile(p)
return true
} catch {
return false
}
}
function discordLooksBinary(text) {
const s = String(text || '')
if (s.indexOf('\0') >= 0) return true
let bad = 0
const n = Math.min(s.length, 800)
for (let i = 0; i < n; i++) {
const c = s.charCodeAt(i)
if (c < 9 || (c > 13 && c < 32)) bad++
}
return bad > 8
}
function discordTextToBuf(ctx, text) {
const s = String(text == null ? '' : text)
if (ctx && ctx.b4a && typeof ctx.b4a.from === 'function') return ctx.b4a.from(s)
if (typeof Buffer !== 'undefined') return Buffer.from(s, 'utf8')
const out = new Uint8Array(s.length)
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) & 0xff
return out
}
function discordEditGc() {
const now = Date.now()
for (const k in BARE_OS_DISCORD_EDIT_SESSIONS) {
const rec = BARE_OS_DISCORD_EDIT_SESSIONS[k]
if (!rec || now - rec.atMs > BARE_OS_DISCORD_EDIT_TTL_MS) {
delete BARE_OS_DISCORD_EDIT_SESSIONS[k]
}
}
}
function discordEditKey(interaction) {
return discordInteractionUserId(interaction) || 'anon'
}
function discordEditPut(interaction, rec) {
discordEditGc()
BARE_OS_DISCORD_EDIT_SESSIONS[discordEditKey(interaction)] = {
path: rec.path,
origLen: rec.origLen || 0,
truncated: !!rec.truncated,
created: !!rec.created,
atMs: Date.now()
}
}
function discordEditGet(interaction) {
discordEditGc()
const rec = BARE_OS_DISCORD_EDIT_SESSIONS[discordEditKey(interaction)]
return rec || null
}
function discordEditClear(interaction) {
delete BARE_OS_DISCORD_EDIT_SESSIONS[discordEditKey(interaction)]
}
function discordEditChunks(text) {
const s = String(text == null ? '' : text)
const cap = BARE_OS_DISCORD_EDIT_CHUNK * BARE_OS_DISCORD_EDIT_MAX_CHUNKS
const truncated = s.length > cap
const body = truncated ? s.slice(0, cap) : s
const chunks = []
if (!body.length) chunks.push('')
else {
for (let i = 0; i < body.length; i += BARE_OS_DISCORD_EDIT_CHUNK) {
chunks.push(body.slice(i, i + BARE_OS_DISCORD_EDIT_CHUNK))
}
}
return {
chunks: chunks.slice(0, BARE_OS_DISCORD_EDIT_MAX_CHUNKS),
truncated: truncated,
origLen: s.length
}
}
function discordEditModalPayload(path, chunks) {
const base = String(path || 'file').split('/').pop() || String(path)
const n = Math.max(1, chunks.length)
const components = []
for (let i = 0; i < n; i++) {
const label = n === 1 ? 'Contents (save closes the form)' : 'Part ' + (i + 1) + ' / ' + n
const field = {
type: 4,
custom_id: 'c' + i,
label: label.slice(0, 45),
style: 2,
required: false,
max_length: BARE_OS_DISCORD_EDIT_CHUNK
}
const v = String(chunks[i] || '').slice(0, BARE_OS_DISCORD_EDIT_CHUNK)
if (v) field.value = v
components.push({
type: 1,
components: [field]
})
}
return {
custom_id: 'edit:save',
title: ('Edit ' + base).slice(0, 45),
components: components
}
}
async function discordCmdCapture(ctx, fn) {
const lines = []
const cons = ctx.console || {}
const ol = cons.log
const oe = cons.error
cons.log = function (s) {
lines.push(String(s))
}
cons.error = function (s) {
lines.push(String(s))
}
try {
await fn()
} finally {
cons.log = ol
cons.error = oe
}
return lines.join('\n')
}
async function discordCmdOsRelease(ctx) {
const t = await discordCmdReadText(ctx, '/etc/os-release')
const out = {}
const lines = String(t).split(/\r?\n/)
for (let i = 0; i < lines.length; i++) {
const m = /^([A-Z0-9_]+)=(.*)$/.exec(lines[i].trim())
if (!m) continue
out[m[1]] = m[2].replace(/^"|"$/g, '')
}
return out
}
function discordParseIdWhitelist(raw) {
const ids = Object.create(null)
const s = String(raw == null ? '' : raw).trim()
if (!s) return ids
const parts = s.split(',')
for (let i = 0; i < parts.length; i++) {
let id = String(parts[i] || '').trim()
if (!id) continue
if (id.charAt(0) === '<' && id.charAt(id.length - 1) === '>') {
id = id.slice(1, -1)
if (id.charAt(0) === '@') id = id.slice(1)
if (id.charAt(0) === '!') id = id.slice(1)
id = id.trim()
}
if (id) ids[id] = 1
}
return ids
}
function discordWhitelistRaw(ctx) {
const e = discordCmdEnv(ctx)
return e.DISCORD_ID_WHITELIST || e.BARE_OS_DISCORD_ID_WHITELIST || ''
}
function discordUserAllowed(ctx, userId) {
const ids = discordParseIdWhitelist(discordWhitelistRaw(ctx))
let n = 0
for (const k in ids) {
if (Object.prototype.hasOwnProperty.call(ids, k)) n++
}
if (n === 0) return true
const id = String(userId == null ? '' : userId).trim()
return Boolean(id && ids[id])
}
function discordInteractionUserId(interaction) {
if (!interaction) return ''
if (interaction.user && interaction.user.id) return String(interaction.user.id)
const member = interaction.member
if (member && member.user && member.user.id) return String(member.user.id)
if (member && member.id) return String(member.id)
return ''
}
function discordFilterChoices(items, q, toChoice) {
const needle = String(q || '').toLowerCase()
const out = []
for (let i = 0; i < items.length && out.length < 25; i++) {
const raw = items[i]
const choice = toChoice ? toChoice(raw) : { name: String(raw), value: String(raw) }
if (!choice || !choice.value) continue
const hay = (choice.name + ' ' + choice.value).toLowerCase()
if (needle && hay.indexOf(needle) < 0) continue
out.push({ name: String(choice.name).slice(0, 100), value: String(choice.value).slice(0, 100) })
}
return out
}
async function discordSuggestUnits(ctx, q) {
const names = BARE_OS_DISCORD_KNOWN_UNITS.slice()
if (typeof ctx.bareOsRunSystemctlCli === 'function') {
const out = await discordCmdCapture(ctx, function () {
return ctx.bareOsRunSystemctlCli(['systemctl', 'list'])
})
const lines = String(out || '').split(/\r?\n/)
for (let i = 0; i < lines.length; i++) {
const m = /^([A-Za-z0-9_@.:-]+)\b/.exec(lines[i].trim())
if (!m) continue
const n = m[1].replace(/\.service$/, '')
if (names.indexOf(n) < 0) names.push(n)
}
}
return discordFilterChoices(names, q)
}
async function discordSuggestMan(ctx, q) {
const db = await discordCmdReadJson(ctx, '/share/man/man.json')
const pages = db && Array.isArray(db.pages) ? db.pages : []
const names = []
for (let i = 0; i < pages.length; i++) {
if (pages[i] && pages[i].name) names.push(String(pages[i].name))
}
if (!names.length) {
names.push('uname', 'whoami', 'login', 'discord-bot', 'systemctl', 'help')
}
return discordFilterChoices(names, q)
}
function discordSuggestRun(q) {
const names = []
for (const k in BARE_OS_DISCORD_RUN_ALLOW) {
if (Object.prototype.hasOwnProperty.call(BARE_OS_DISCORD_RUN_ALLOW, k)) names.push(k)
}
names.sort()
return discordFilterChoices(names, q)
}
function discordFmKey(interaction) {
return discordInteractionUserId(interaction) || 'anon'
}
function discordFmGc() {
const now = Date.now()
for (const k in BARE_OS_DISCORD_FM_SESSIONS) {
const rec = BARE_OS_DISCORD_FM_SESSIONS[k]
if (!rec || now - rec.atMs > BARE_OS_DISCORD_FM_TTL_MS) {
delete BARE_OS_DISCORD_FM_SESSIONS[k]
}
}
}
function discordFmGet(interaction, ctx) {
discordFmGc()
let rec = BARE_OS_DISCORD_FM_SESSIONS[discordFmKey(interaction)]
if (!rec) {
rec = {
cwd: '~',
page: 0,
sel: '',
confirm: '',
atMs: Date.now()
}
BARE_OS_DISCORD_FM_SESSIONS[discordFmKey(interaction)] = rec
}
rec.atMs = Date.now()
if (ctx) discordSyncSessionIdentity(ctx)
return rec
}
function discordJoinPath(dir, name) {
const n = String(name || '').replace(/^\/+/, '')
if (!n || n === '.') return dir || '~'
if (n === '..') return discordParentPath(dir)
const d = String(dir || '~').replace(/\/+$/, '') || '/'
if (d === '/') return '/' + n
if (d === '~') return '~/' + n
return d + '/' + n
}
function discordParentPath(p) {
const s = String(p || '~').replace(/\/+$/, '') || '/'
if (s === '/' || s === '~') return s
const i = s.lastIndexOf('/')
if (i <= 0) return s.charAt(0) === '~' ? '~' : '/'
const parent = s.slice(0, i)
return parent || (s.charAt(0) === '~' ? '~' : '/')
}
function discordBaseName(p) {
const s = String(p || '').replace(/\/+$/, '')
const i = s.lastIndexOf('/')
return i >= 0 ? s.slice(i + 1) : s
}
function discordIsDirStat(st) {
if (!st) return false
if (st.isDirectory === true) return true
if (st.type === 'directory') return true
return false
}
async function discordFmList(ctx, cwd) {
const p = discordCmdPathOk(cwd || '~') || '~'
if (typeof ctx.vfs?.readdir !== 'function') return { path: p, names: [], err: 'readdir unavailable' }
try {
const raw = await ctx.vfs.readdir(p)
const names = []
const list = Array.isArray(raw) ? raw : []
for (let i = 0; i < list.length; i++) {
const n = String(list[i] || '')
if (!n || n === '.bareos_empty') continue
names.push(n)
}
names.sort(function (a, b) {
return a < b ? -1 : a > b ? 1 : 0
})
return { path: p, names: names, err: '' }
} catch (err) {
return { path: p, names: [], err: (err && err.message) || String(err) }
}
}
async function discordFmStat(ctx, path) {
const stfn = ctx.vfs && (ctx.vfs.lstat || ctx.vfs.stat)
if (typeof stfn !== 'function') return null
try {
return await stfn.call(ctx.vfs, path)
} catch {
return null
}
}
async function discordFmView(ctx, interaction) {
const rec = discordFmGet(interaction, ctx)
const listing = await discordFmList(ctx, rec.cwd)
rec.cwd = listing.path
const names = listing.names
const pages = Math.max(1, Math.ceil(names.length / BARE_OS_DISCORD_FM_PAGE))
if (rec.page >= pages) rec.page = pages - 1
if (rec.page < 0) rec.page = 0
const start = rec.page * BARE_OS_DISCORD_FM_PAGE
const slice = names.slice(start, start + BARE_OS_DISCORD_FM_PAGE)
if (rec.sel && names.indexOf(rec.sel) < 0) rec.sel = ''
const writable = Boolean(discordCmdPathWriteOk(ctx, rec.cwd))
const fields = []
const options = []
if (rec.cwd !== '/' && rec.cwd !== '~') {
options.push({ label: '⬆ .. (parent)', value: '..', description: discordParentPath(rec.cwd) })
}
for (let i = 0; i < slice.length; i++) {
const name = slice[i]
const full = discordJoinPath(rec.cwd, name)
const st = await discordFmStat(ctx, full)
const dir = discordIsDirStat(st)
const size = st && st.size != null && !dir ? discordPrettyBytes(st.size) : dir ? 'dir' : ''
const mark = rec.sel === name ? '▸ ' : ''
fields.push(
discordField(
mark + (dir ? '📁 ' : '📄 ') + name,
size || (dir ? 'folder' : 'file'),
true
)
)
options.push({
label: (dir ? '📁 ' : '📄 ') + name,
value: name.slice(0, 100),
description: size || (dir ? 'open folder' : 'file')
})
}
const sel = discordSelect(
'fm:pick',
slice.length ? 'Open an entry…' : 'Empty folder',
options
)
const nav = discordButtons([
{ id: 'fm:up', label: 'Parent' },
{ id: 'fm:home', label: 'Home', style: 1 },
{ id: 'fm:goto', label: 'Go to…' },
{ id: 'fm:prev', label: '◀' },
{ id: 'fm:next', label: '▶' }
])
const acts = discordButtons(
writable
? [
{ id: 'fm:open', label: 'Open', style: 1 },
{ id: 'fm:edit', label: 'Edit' },
{ id: 'fm:new', label: 'New file', style: 3 },
{ id: 'fm:mkdir', label: 'New folder' },
{ id: 'fm:more', label: 'Manage…' }
]
: [
{ id: 'fm:open', label: 'Open', style: 1 },
{ id: 'nav:panel', label: 'Panel' }
]
)
const manage = rec.confirm
? discordButtons([
{ id: 'fm:ok', label: 'Confirm delete', style: 4 },
{ id: 'fm:no', label: 'Cancel', style: 2 }
])
: writable
? discordButtons([
{ id: 'fm:ren', label: 'Rename' },
{ id: 'fm:copy', label: 'Copy' },
{ id: 'fm:del', label: 'Delete', style: 4 },
{ id: 'fm:ref', label: 'Refresh' },
{ id: 'nav:panel', label: 'Panel' }
])
: discordButtons([{ id: 'fm:ref', label: 'Refresh' }])
const rows = [sel, nav, acts, manage].filter(Boolean)
let desc = '`' + rec.cwd + '` · ' + names.length + ' items · page ' + (rec.page + 1) + '/' + pages
desc += writable ? ' · writable' : ' · read-only'
if (rec.sel) desc += '\nSelected: **' + rec.sel + '**'
if (rec.confirm) desc += '\n**Delete `' + rec.confirm + '`?** This cannot be undone.'
if (listing.err) desc += '\n' + listing.err
return discordResult(
discordEmbed({
title: 'Files',
desc: desc,
fields: fields.slice(0, 20),
color: rec.confirm ? BARE_OS_DISCORD_COLOR_ERR : BARE_OS_DISCORD_COLOR,
footer: rec.cwd + ' · ' + discordSessionUser(ctx)
}),
{ components: rows, nav: false }
)
}
function discordSuggestEditPaths(ctx, q) {
const user = discordSessionUser(ctx)
const names = BARE_OS_DISCORD_EDIT_SUGGEST.concat([
'/home/' + user + '/notes.txt',
'/tmp/' + user + '.txt'
])
return discordFilterChoices(names, q)
}
function discordSuggestPaths(ctx, q) {
const e = discordCmdEnv(ctx)
const home = e.HOME || '/home/' + discordSessionUser(ctx)
const extra = [home, home + '/.discord', '/home/' + discordSessionUser(ctx)]
const names = BARE_OS_DISCORD_FS_SUGGEST.concat(extra)
return discordFilterChoices(names, q)
}
async function discordCmdStatus(ctx) {
const e = discordCmdEnv(ctx)
const os = await discordCmdOsRelease(ctx)
const user = discordSessionUser(ctx)
const ident = ctx && ctx.identity && ctx.identity.state ? String(ctx.identity.state) : e.BARE_OS_IDENTITY || 'guest'
return discordResult(
discordEmbed({
title: 'Bare OS session',
color: ident === 'unlocked' ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN,
author: { name: user + '@' + (e.HOSTNAME || e.NAME || 'bare-os') },
fields: [
discordField('OS', os.PRETTY_NAME || os.NAME || 'Bare OS', true),
discordField('User', user, true),
discordField('Identity', ident, true),
discordField('Home', e.HOME || '/home/' + user, true),
discordField('Shell', e.SHELL || '/bin/sh', true),
discordField('UID', e.UID || '—', true),
discordField('Version', os.VERSION_ID || e.BARE_OS_KERNEL_VERSION || e.BARE_OS_RELEASE || '—', true),
discordField('Arch', e.BARE_OS_ARCH || e.MACHINE || '—', true),
discordField('Booter', e.BARE_OS_BOOTER_PACKAGE_VERSION || '—', true)
]
})
)
}
async function discordCmdUname(ctx) {
const e = discordCmdEnv(ctx)
const os = await discordCmdOsRelease(ctx)
const line = [
os.NAME || 'BareOS',
e.HOSTNAME || 'bare-os',
os.VERSION_ID || e.BARE_OS_KERNEL_VERSION || '0.1',
e.BARE_OS_ARCH || e.MACHINE || 'unknown',
os.PRETTY_NAME || e.BARE_OS_BUILD || 'bare-userland'
].join(' ')
return discordResult(
discordEmbed({
title: 'uname',
desc: discordCmdFence(line)
})
)
}
async function discordCmdHandleBare(ctx, sub) {
const e = discordCmdEnv(ctx)
const user = discordSessionUser(ctx)
if (sub === 'ping') {
return discordResult(
discordEmbed({
title: 'Pong',
color: BARE_OS_DISCORD_COLOR_OK,
desc: 'Bare OS is online as **' + user + '**.'
})
)
}
if (sub === 'about') {
return discordResult(
discordEmbed({
title: 'Bare OS Discord bot',
desc:
'Slash control surface for **this logged-in session** (`' +
user +
'`). Commands run with that users `HOME`, VFS, and `systemctl`.'
})
)
}
if (sub === 'help') {
return discordResult(
discordEmbed({
title: 'Commands',
desc: 'Running as **' + user + '**. Use the buttons below, or slash commands.',
fields: [
discordField('/bare', 'Session — status, whoami, hostname, date, uptime, motd, uname'),
discordField('/sys', 'Host — disk, memory, processes, env, doctor, features, limits'),
discordField('/svc', 'Services — list, start, stop, restart, logs'),
discordField('/fs', 'Read-only VFS — ls, cat, stat, head'),
discordField('/net', 'Swarm — peers, summary'),
discordField('/files /edit /create', 'Browse and edit files under `~/` and `/tmp`'),
discordField('/settings', 'Live session knobs (theme, shell, Discord, agent, aliases)'),
discordField('/run /journal /say /man', 'Allowlisted command, logs, speak, man pages')
]
})
)
}
if (sub === 'status') return discordCmdStatus(ctx)
if (sub === 'uname') return discordCmdUname(ctx)
if (sub === 'whoami') {
return discordResult(
discordEmbed({
title: 'Whoami',
color: BARE_OS_DISCORD_COLOR_OK,
fields: [
discordField('User', user, true),
discordField('Home', '`' + (e.HOME || '/home/' + user) + '`', true),
discordField('Identity', e.BARE_OS_IDENTITY || (ctx.identity && ctx.identity.state) || '', true)
]
})
)
}
if (sub === 'hostname') {
return discordResult(
discordEmbed({
title: 'Hostname',
desc: '`' + String(e.HOSTNAME || e.NAME || 'bare-os') + '`'
})
)
}
if (sub === 'date') {
return discordResult(
discordEmbed({
title: 'Date',
desc: '`' + new Date().toISOString() + '`'
})
)
}
if (sub === 'uptime') {
const t = await discordCmdReadText(ctx, '/proc/uptime')
return discordResult(
discordEmbed({
title: 'Uptime',
fields: [
discordField('Session', t ? discordPrettyUptime(t) : 'unavailable', true)
]
})
)
}
if (sub === 'motd') {
const t = await discordCmdReadText(ctx, '/etc/motd')
return discordResult(
discordEmbed({
title: 'motd',
desc: t ? discordCmdFence(t) : '(no /etc/motd)'
})
)
}
return { text: 'unknown /bare subcommand', ephemeral: true }
}
function discordFormatHostDf(obj) {
const host = obj && obj.host && typeof obj.host === 'object' ? obj.host : obj
const sv = obj && obj.statvfs && typeof obj.statvfs === 'object' ? obj.statvfs : null
const fields = []
if (host) {
if (host.platform || host.arch) {
fields.push(
discordField(
'Host',
[host.platform, host.arch, host.hostname || host.machine]
.filter(Boolean)
.join(' · '),
true
)
)
}
if (host.totalmem != null) {
const used =
host.freemem != null ? Number(host.totalmem) - Number(host.freemem) : null
fields.push(discordField('RAM', discordPrettyBytes(host.totalmem), true))
if (host.freemem != null) {
fields.push(discordField('Free RAM', discordPrettyBytes(host.freemem), true))
}
if (used != null && host.totalmem) {
const pct = Math.round((used / Number(host.totalmem)) * 100)
fields.push(discordField('RAM used', pct + '%', true))
}
}
if (host.availableParallelism != null) {
fields.push(discordField('CPUs', String(host.availableParallelism), true))
}
if (host.release) fields.push(discordField('Release', host.release, true))
}
if (obj && obj.peers != null) fields.push(discordField('Peers', String(obj.peers), true))
if (sv) {
fields.push(discordField('Volume', sv.volume_class || sv.f_basetype || '—', true))
if (sv.pwd_logical) fields.push(discordField('PWD', sv.pwd_logical, true))
const bsize = Number(sv.f_frsize || sv.f_bsize || 4096)
if (sv.f_blocks != null) {
fields.push(discordField('System size', discordPrettyBytes(Number(sv.f_blocks) * bsize), true))
}
if (sv.f_bavail != null) {
fields.push(discordField('System avail', discordPrettyBytes(Number(sv.f_bavail) * bsize), true))
}
const pers = sv.personalDrive
if (pers && pers.f_blocks != null) {
const pb = Number(pers.f_frsize || pers.f_bsize || bsize)
fields.push(
discordField('Personal size', discordPrettyBytes(Number(pers.f_blocks) * pb), true)
)
}
}
if (obj && obj.pipeline && obj.pipeline.maxBytes != null) {
fields.push(discordField('Pipe cap', discordPrettyBytes(obj.pipeline.maxBytes), true))
}
return fields
}
function discordFormatMem(raw) {
const json = discordTryJson(raw)
if (json && json.host) {
return discordFormatHostDf(json)
}
const mi = discordParseMeminfo(raw)
const fields = []
const pick = [
['MemTotal', 'Total'],
['MemFree', 'Free'],
['MemAvailable', 'Available'],
['Buffers', 'Buffers'],
['Cached', 'Cached'],
['SwapTotal', 'Swap'],
['SwapFree', 'Swap free']
]
for (let i = 0; i < pick.length; i++) {
if (mi[pick[i][0]] != null) {
fields.push(discordField(pick[i][1], discordPrettyBytes(mi[pick[i][0]]), true))
}
}
if (mi.MemTotal && mi.MemAvailable != null) {
const pct = Math.round((1 - mi.MemAvailable / mi.MemTotal) * 100)
fields.push(discordField('Used', pct + '%', true))
}
return fields
}
function discordFormatRlimits(obj) {
const lim = obj && obj.rlimits && typeof obj.rlimits === 'object' ? obj.rlimits : obj
const fields = []
if (!lim || typeof lim !== 'object') return fields
const names = {
NOFILE: 'Open files',
NPROC: 'Processes',
AS: 'Address space',
DATA: 'Data',
STACK: 'Stack',
CORE: 'Core dump',
RSS: 'RSS',
CPU: 'CPU time',
FSIZE: 'File size',
NOVM: 'No VM',
MEMLOCK: 'Locked mem'
}
const keys = Object.keys(lim)
for (let i = 0; i < keys.length && fields.length < 18; i++) {
const raw = keys[i]
const v = lim[raw]
if (v && typeof v === 'object' && (v.cur != null || v.max != null)) {
const short = raw.replace(/^RLIMIT_/, '')
const label = names[short] || discordHumanLabel(short)
const cur = discordIsByteishKey(raw) ? discordPrettyBytes(v.cur) : String(v.cur)
const max = discordIsByteishKey(raw) ? discordPrettyBytes(v.max) : String(v.max)
fields.push(discordField(label, cur + ' / ' + max, true))
}
}
if (obj && obj.bareOsExecMaxDepth != null) {
fields.push(discordField('Exec depth', String(obj.bareOsExecMaxDepth), true))
}
return fields
}
function discordFormatFeatures(obj) {
const feat =
obj && obj.features && typeof obj.features === 'object' ? obj.features : obj
if (!feat || typeof feat !== 'object') return []
const on = []
const off = []
const keys = Object.keys(feat).sort()
for (let i = 0; i < keys.length; i++) {
const v = feat[keys[i]]
if (v === true || v === 1 || v === '1') on.push('`' + keys[i] + '`')
else if (v === false || v === 0 || v === '0') off.push('`' + keys[i] + '`')
}
const fields = []
if (on.length) fields.push(discordField('On', on.slice(0, 24).join(' ')))
if (off.length) fields.push(discordField('Off', off.slice(0, 16).join(' ')))
return fields
}
function discordFormatDoctor(obj) {
if (!obj || typeof obj !== 'object') {
return { fields: [], desc: '', color: BARE_OS_DISCORD_COLOR_WARN }
}
const pa = obj.peerAdmission && typeof obj.peerAdmission === 'object' ? obj.peerAdmission : {}
const hn = obj.hostnameMutation && typeof obj.hostnameMutation === 'object' ? obj.hostnameMutation : {}
const kh = obj.keyHandlePolicy && typeof obj.keyHandlePolicy === 'object' ? obj.keyHandlePolicy : {}
const fields = []
if (pa.allowlistConfigured != null) {
fields.push(discordField('Peer allowlist', pa.allowlistConfigured ? 'configured' : 'open', true))
}
if (pa.denylistConfigured != null) {
fields.push(discordField('Peer denylist', pa.denylistConfigured ? 'configured' : 'none', true))
}
if (pa.requireCapsConfigured != null) {
const n = pa.requireCapsTokenCount
fields.push(
discordField(
'Require caps',
pa.requireCapsConfigured ? (n ? n + ' tokens' : 'yes') : 'no',
true
)
)
}
if (hn.enabled != null) {
fields.push(discordField('Hostname set', hn.enabled ? 'allowed' : 'locked', true))
}
if (kh.defaultTtlMs) {
fields.push(discordField('Key TTL', discordPrettyValue('defaultTtlMs', kh.defaultTtlMs), true))
}
if (Array.isArray(obj.mfaExtensionPoints) && obj.mfaExtensionPoints.length) {
fields.push(discordField('MFA hooks', obj.mfaExtensionPoints.join(' · '), false))
}
const bits = []
if (pa.allowlistConfigured) bits.push('peer allowlist on')
else if (pa.allowlistConfigured === false) bits.push('peer allowlist **open**')
if (hn.enabled === true) bits.push('hostname mutation on')
else if (hn.enabled === false) bits.push('hostname locked')
return {
fields: fields,
desc: bits.join(' · '),
color: pa.allowlistConfigured ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN
}
}
async function discordCmdHandleSys(ctx, sub) {
if (sub === 'df') {
const t =
(await discordCmdReadText(ctx, '/proc/bare_os_resources')) ||
(await discordCmdReadText(ctx, '/proc/bare_os/resources')) ||
(await discordCmdReadText(ctx, '/proc/bare_os/host_os.json'))
const obj = discordTryJson(t)
const fields = obj ? discordFormatHostDf(obj) : []
if (fields.length) {
return discordResult(
discordEmbed({
title: 'Disk / host',
fields: fields
})
)
}
return discordPrettySnapshot('Disk / host', t)
}
if (sub === 'mem') {
const t =
(await discordCmdReadText(ctx, '/proc/meminfo')) ||
(await discordCmdReadText(ctx, '/proc/bare_os/host_os.json'))
const fields = discordFormatMem(t)
if (fields.length) {
return discordResult(discordEmbed({ title: 'Memory', fields: fields }))
}
return discordPrettySnapshot('Memory', t)
}
if (sub === 'ps') {
const table = await discordCmdReadJson(ctx, '/proc/bare_os/process_table.json')
const rows = table && Array.isArray(table.processes) ? table.processes : []
const fields = []
for (let i = 0; i < Math.min(rows.length, 18); i++) {
const r = rows[i] || {}
const name = String(r.name || r.comm || r.cmd || r.id || 'proc')
const st = String(r.state || r.status || '')
const pid = String(r.pid || r.id || i)
fields.push(discordField(name, 'pid ' + pid + (st ? ' · ' + st : ''), true))
}
return discordResult(
discordEmbed({
title: 'Processes · ' + rows.length,
desc: rows.length ? rows.length + ' logical processes in this session.' : 'No logical processes in the session table.',
fields: fields
})
)
}
if (sub === 'env') {
const e = discordCmdEnv(ctx)
const fields = [
discordField('User', e.USER, true),
discordField('Home', e.HOME, true),
discordField('Identity', e.BARE_OS_IDENTITY, true),
discordField('Shell', e.SHELL, true),
discordField('Pwd', e.PWD, true),
discordField(
'UID / GID',
e.UID || e.GID ? String(e.UID || '—') + ' / ' + String(e.GID || '—') : '',
true
)
]
const prefer = [
'HOSTNAME',
'PATH',
'TERM',
'TZ',
'LANG',
'EDITOR',
'PAGER',
'BARE_OS_THEME',
'BARE_OS_COLOR_DEPTH'
]
const skip = /TOKEN|SECRET|PASSWORD|PASSWD|KEY|PRIVATE|CREDENTIAL|^USER$|^HOME$|^SHELL$|^PWD$|^LOGNAME$|^UID$|^GID$|^GROUP$|^BARE_OS_IDENTITY$/i
const seen = Object.create(null)
for (let i = 0; i < prefer.length; i++) {
const k = prefer[i]
seen[k] = 1
if (e[k] == null || e[k] === '') continue
fields.push(discordField(discordHumanLabel(k.replace(/^BARE_OS_/, '')), String(e[k]).slice(0, 80), true))
}
const keys = Object.keys(e).sort()
for (let i = 0; i < keys.length && fields.length < 15; i++) {
if (seen[keys[i]] || skip.test(keys[i])) continue
if (e[keys[i]] == null || e[keys[i]] === '') continue
fields.push(discordField(keys[i], String(e[keys[i]]).slice(0, 72), true))
}
return discordResult(
discordEmbed({
title: 'Environment',
desc: 'Session env for **' + discordSessionUser(ctx) + '** (secrets omitted).',
fields: fields
}),
{ ephemeral: true }
)
}
if (sub === 'doctor') {
const t =
(await discordCmdReadText(ctx, '/proc/bare_os/security_posture.json')) ||
(await discordCmdReadText(ctx, '/proc/bare_os/debug.json'))
const obj = discordTryJson(t)
const fmt = obj ? discordFormatDoctor(obj) : null
if (fmt && fmt.fields.length) {
return discordResult(
discordEmbed({
title: 'Doctor',
desc: fmt.desc,
fields: fmt.fields,
color: fmt.color
})
)
}
return discordPrettySnapshot('Doctor', t, { color: fmt && fmt.color })
}
if (sub === 'features') {
const t =
(await discordCmdReadText(ctx, '/proc/bare_os/features')) ||
(await discordCmdReadText(ctx, '/proc/bare_os_features')) ||
(await discordCmdReadText(ctx, '/proc/bare_os/features.json'))
const obj = discordTryJson(t)
const fields = obj ? discordFormatFeatures(obj) : []
if (fields.length) {
return discordResult(
discordEmbed({
title: 'Features',
desc: 'Guest capability flags on this booter.',
fields: fields
})
)
}
return discordPrettySnapshot('Features', t)
}
if (sub === 'rlimits') {
const t = await discordCmdReadText(ctx, '/proc/bare_os/rlimits.json')
const obj = discordTryJson(t)
const fields = obj ? discordFormatRlimits(obj) : []
if (fields.length) {
return discordResult(
discordEmbed({
title: 'Resource limits',
desc: 'Soft / hard · Bare OS runtime caps (not Linux rlimits).',
fields: fields
})
)
}
return discordPrettySnapshot('Resource limits', t)
}
return { text: 'unknown /sys subcommand', ephemeral: true }
}
async function discordCmdHandleSvc(ctx, sub, unit) {
const name = String(unit || '').replace(/\.service$/, '')
if (typeof ctx.bareOsRunSystemctlCli !== 'function') {
return { text: 'systemctl is not available on this ctx', ephemeral: true }
}
if (sub === 'list') {
const out = await discordCmdCapture(ctx, function () {
return ctx.bareOsRunSystemctlCli(['systemctl', 'list'])
})
const parsed = discordParseSystemctlList(out)
const fields = []
for (let i = 0; i < parsed.length && fields.length < 20; i++) {
const r = parsed[i]
const bits = []
if (r.active) bits.push('**' + r.active + '**')
if (r.sub && r.sub !== r.active) bits.push(r.sub)
if (r.preset && r.preset !== 'enabled') bits.push(r.preset)
fields.push(discordField(r.unit, bits.join(' · ') || 'unknown', true))
}
const units = await discordSuggestUnits(ctx, '')
const sel = discordSelect(
'svc:pick',
'Inspect a unit…',
units.map(function (c) {
return { label: c.name, value: c.value }
})
)
return discordResult(
discordEmbed({
title: 'Services · ' + (parsed.length || 0),
desc: parsed.length
? 'Pick a unit to inspect, start, or stop.'
: out
? discordCmdClip(out, 800)
: 'No units registered in this session.',
fields: fields,
color: BARE_OS_DISCORD_COLOR
}),
{ components: sel ? [sel] : [] }
)
}
if (!name) return { text: 'unit name required — pick from autocomplete', ephemeral: true }
const argv = ['systemctl', sub === 'logs' ? 'status' : sub, name]
if (sub === 'logs') {
return discordCmdHandleJournal(ctx, name)
}
const out = await discordCmdCapture(ctx, function () {
return ctx.bareOsRunSystemctlCli(argv)
})
const act = discordButtons([
{ id: 'svc:status:' + name, label: 'Status', style: 1 },
{ id: 'svc:start:' + name, label: 'Start', style: 3 },
{ id: 'svc:stop:' + name, label: 'Stop', style: 4 },
{ id: 'svc:restart:' + name, label: 'Restart' },
{ id: 'svc:logs:' + name, label: 'Logs' }
])
const color =
sub === 'stop'
? BARE_OS_DISCORD_COLOR_WARN
: sub === 'start' || sub === 'restart'
? BARE_OS_DISCORD_COLOR_OK
: BARE_OS_DISCORD_COLOR
return discordResult(
discordEmbed({
title: name,
desc: out
? '`' +
sub +
'`\n' +
discordCmdFence(discordCmdClip(discordCmdRedact(out), 1100))
: 'No output from `systemctl ' + sub + '`.',
color: color
}),
{ components: act ? [act] : [] }
)
}
async function discordCmdHandleFs(ctx, sub, rawPath, nlines) {
const p = discordCmdPathOk(rawPath || '~')
if (!p) {
return {
text: 'path not allowed (no `..`; stay under /proc /etc /var/log /run /home ~ /share)',
ephemeral: true
}
}
if (sub === 'ls') {
if (typeof ctx.vfs?.readdir !== 'function') return { text: 'readdir unavailable', ephemeral: true }
try {
const names = await ctx.vfs.readdir(p)
const list = Array.isArray(names) ? names : []
const shown = list.slice(0, 60)
const desc = shown.length
? shown
.map(function (n) {
return '`' + String(n).replace(/`/g, "'") + '`'
})
.join(' ')
: '(empty)'
return discordResult(
discordEmbed({
title: 'Listing',
desc:
'`' +
p +
'` · ' +
list.length +
' ' +
(list.length === 1 ? 'entry' : 'entries') +
'\n' +
desc +
(list.length > shown.length ? '\n+' + (list.length - shown.length) + ' more' : ''),
footer: discordSessionUser(ctx)
})
)
} catch (err) {
return { text: 'ls failed: ' + ((err && err.message) || err), ephemeral: true }
}
}
if (sub === 'stat') {
const stfn = ctx.vfs && (ctx.vfs.lstat || ctx.vfs.stat)
if (typeof stfn !== 'function') return { text: 'stat unavailable', ephemeral: true }
try {
const st = await stfn.call(ctx.vfs, p)
const fields = []
const kind = st.isDirectory
? 'directory'
: st.isFile
? 'file'
: st.isSymbolicLink
? 'symlink'
: st.type || 'entry'
fields.push(discordField('Type', String(kind), true))
if (st.size != null) fields.push(discordField('Size', discordPrettyBytes(st.size), true))
if (st.mode != null) fields.push(discordField('Mode', String(st.mode), true))
if (st.mtime || st.mtimeMs) {
const ms = st.mtimeMs || Date.parse(st.mtime)
fields.push(
discordField('Modified', Number.isFinite(ms) ? new Date(ms).toISOString() : String(st.mtime), true)
)
}
if (st.path && st.path !== p) fields.push(discordField('Resolved', String(st.path), false))
return discordResult(
discordEmbed({
title: 'Stat',
desc: '`' + p + '`',
fields: fields.slice(0, 12)
})
)
} catch (err) {
return { text: 'stat failed: ' + ((err && err.message) || err), ephemeral: true }
}
}
const text = await discordCmdReadText(ctx, p)
if (!text) {
return discordResult(discordEmbed({ title: 'File', desc: '`' + p + '` is empty or unreadable.' }))
}
const writable = Boolean(discordCmdPathWriteOk(ctx, p))
const editRow = writable
? discordButtons([{ id: 'edit:open', label: 'Edit in modal', style: 1 }])
: null
const extra = {
components: editRow ? [editRow] : [],
editPath: writable ? p : ''
}
const asJson = discordTryJson(text)
if (asJson && typeof asJson === 'object' && !Array.isArray(asJson)) {
return discordPrettySnapshot(p, asJson, extra)
}
if (sub === 'head') {
const n = Math.max(1, Math.min(40, Number(nlines) || 12))
return discordResult(
discordEmbed({
title: 'Head',
desc: '`' + p + '` · first ' + n + ' lines\n' + discordCmdFence(text.split(/\r?\n/).slice(0, n).join('\n'))
}),
extra
)
}
return discordResult(
discordEmbed({
title: 'File',
desc: '`' + p + '`\n' + discordCmdFence(text.slice(0, BARE_OS_DISCORD_FS_MAX)),
footer: text.length + ' bytes · ' + discordSessionUser(ctx)
}),
extra
)
}
function discordFormatNetSummary(obj) {
if (!obj || typeof obj !== 'object') {
return { fields: [], desc: '', color: BARE_OS_DISCORD_COLOR }
}
const rq =
obj.replicationQueue && typeof obj.replicationQueue === 'object' ? obj.replicationQueue : {}
const st = obj.stagingSlot && typeof obj.stagingSlot === 'object' ? obj.stagingSlot : {}
const fields = []
const peers = obj.peerCount
const role = obj.seedRole || rq.role || st.role
const proto = rq.protocol || st.protocol || obj.protocol
if (peers != null) fields.push(discordField('Peers', String(peers), true))
if (role) fields.push(discordField('Role', String(role), true))
if (proto) fields.push(discordField('Protocol', '`' + String(proto) + '`', true))
if (obj.topicHex) {
const hex = String(obj.topicHex)
fields.push(
discordField('Topic', '`' + hex.slice(0, 16) + (hex.length > 16 ? '…' : '') + '`', true)
)
}
if (obj.replicationQueueDepth != null) {
fields.push(discordField('Queue depth', String(obj.replicationQueueDepth), true))
}
if (rq.queueDepthEstimate != null) {
fields.push(discordField('Queue estimate', String(rq.queueDepthEstimate), true))
}
if (rq.hypercoreLengthHint != null) {
fields.push(discordField('Core length', String(rq.hypercoreLengthHint), true))
}
if (rq.manifestPathCount != null) {
fields.push(discordField('Manifests', String(rq.manifestPathCount), true))
}
if (rq.localRamBlockCount != null) {
fields.push(discordField('RAM blocks', String(rq.localRamBlockCount), true))
}
if (st.activeSlot) fields.push(discordField('Active slot', String(st.activeSlot), true))
const fwA = obj.peerFirewallAcceptedTotal
const fwR = obj.peerFirewallRejectedTotal
const fwI = obj.peerFirewallInboundTotal
const fwO = obj.peerFirewallOutboundTotal
if (fwA != null || fwR != null || fwI != null || fwO != null) {
fields.push(
discordField(
'Firewall',
'accept ' +
(fwA == null ? '0' : fwA) +
' · reject ' +
(fwR == null ? '0' : fwR) +
'\nin ' +
(fwI == null ? '0' : fwI) +
' · out ' +
(fwO == null ? '0' : fwO),
true
)
)
}
if (obj.seedHandshakeError) {
fields.push(discordField('Handshake', String(obj.seedHandshakeError), false))
}
const peerN = Number(peers)
const head = []
if (Number.isFinite(peerN)) head.push('**' + peerN + '** peer' + (peerN === 1 ? '' : 's'))
if (role) head.push(String(role))
if (proto) head.push('`' + proto + '`')
let desc = head.join(' · ')
const note = rq.snapshotWorkflowNote || obj.note
if (note) desc += (desc ? '\n' : '') + '*' + String(note).slice(0, 220) + '*'
const at = rq.atMs || obj.atMs
return {
fields: fields,
desc: desc,
color: Number.isFinite(peerN) && peerN > 0 ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN,
footer: at ? 'Updated ' + new Date(Number(at)).toISOString() + ' · expires after 2m idle' : ''
}
}
function discordFormatSwarm(obj) {
if (!obj || typeof obj !== 'object') return []
const fields = []
if (obj.peerCount != null) fields.push(discordField('Peers', String(obj.peerCount), true))
if (obj.protocol) fields.push(discordField('Protocol', '`' + String(obj.protocol) + '`', true))
if (obj.topicCount != null) fields.push(discordField('Topics', String(obj.topicCount), true))
if (obj.topicHex) {
const hex = String(obj.topicHex)
fields.push(
discordField('Topic', '`' + hex.slice(0, 16) + (hex.length > 16 ? '…' : '') + '`', true)
)
}
const lc = obj.lifecycle
if (lc && typeof lc === 'object') {
const bits = []
const lk = Object.keys(lc)
for (let i = 0; i < lk.length && bits.length < 6; i++) {
if (typeof lc[lk[i]] === 'object' || discordValueEmpty(lc[lk[i]])) continue
bits.push('**' + discordHumanLabel(lk[i]) + '** ' + discordPrettyValue(lk[i], lc[lk[i]]))
}
if (bits.length) fields.push(discordField('Lifecycle', bits.join('\n'), false))
}
const sc = obj.peerScoringAggregates
if (sc && typeof sc === 'object') {
const banned = sc.bannedActiveCount
const hi = sc.highLatencyEwmaCount
const low = sc.lowSuccessRateBucketCount
if (banned != null || hi != null || low != null) {
fields.push(
discordField(
'Scoring',
'banned ' +
String(banned || 0) +
' · high-lat ' +
String(hi || 0) +
' · low-ok ' +
String(low || 0),
true
)
)
}
}
if (Array.isArray(obj.peers) && obj.peers.length) {
fields.push(discordField('Peer list', String(obj.peers.length), true))
}
return fields
}
async function discordCmdHandleNet(ctx, sub) {
if (sub === 'peers' || sub === 'swarm') {
const t =
(await discordCmdReadText(ctx, '/proc/bare_os/swarm')) ||
(await discordCmdReadText(ctx, '/proc/bare_os/swarm.json')) ||
(await discordCmdReadText(ctx, '/proc/bare_os_swarm'))
const obj = discordTryJson(t)
const fields = obj ? discordFormatSwarm(obj) : []
if (fields.length) {
const n = obj && obj.peerCount
return discordResult(
discordEmbed({
title: 'Swarm',
desc:
n != null
? '**' + n + '** peer' + (Number(n) === 1 ? '' : 's') + (obj.protocol ? ' · `' + obj.protocol + '`' : '')
: '',
fields: fields,
color: obj && obj.peerCount > 0 ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN
})
)
}
return discordPrettySnapshot('Swarm', t)
}
const t =
(await discordCmdReadText(ctx, '/proc/bare_os/net_summary.json')) ||
(await discordCmdReadText(ctx, '/proc/net/dev'))
const obj = discordTryJson(t)
if (obj) {
const fmt = discordFormatNetSummary(obj)
if (fmt.fields.length) {
return discordResult(
discordEmbed({
title: 'Network',
desc: fmt.desc,
fields: fmt.fields,
color: fmt.color,
footer: fmt.footer || undefined
})
)
}
return discordPrettySnapshot('Network', obj)
}
if (t && t.indexOf('Inter-|') >= 0) {
const lines = t.split(/\r?\n/).filter(Boolean)
return discordResult(
discordEmbed({
title: 'Network interfaces',
desc: discordCmdFence(lines.slice(0, 16).join('\n'))
})
)
}
return discordPrettySnapshot('Network', t)
}
async function discordCmdHandleMan(ctx, page) {
const name = String(page || '').replace(/[^a-zA-Z0-9._+-]/g, '')
if (!name) return { text: 'man page name required — start typing for autocomplete', ephemeral: true }
if (typeof ctx.execLine === 'function') {
const out = await discordCmdCapture(ctx, function () {
return ctx.execLine('man ' + name)
})
if (out) {
const clean = String(out)
.replace(/\x1b\[[0-9;]*m/g, '')
.trim()
const first = clean.split(/\r?\n/).slice(0, 28).join('\n')
return discordResult(
discordEmbed({
title: 'man ' + name,
desc: discordCmdClip(first, 1600)
})
)
}
}
const t = await discordCmdReadText(ctx, '/share/man/man.json')
if (!t) return { text: 'man database unavailable' }
try {
const db = JSON.parse(t)
const pages = (db && db.pages) || []
for (let i = 0; i < pages.length; i++) {
if (pages[i] && pages[i].name === name) {
const p = pages[i]
return discordResult(
discordEmbed({
title: 'man ' + (p.title || name),
desc:
(p.synopsis && p.synopsis[0] ? '`' + p.synopsis[0] + '`\n\n' : '') +
String(p.description || '').slice(0, 1400)
})
)
}
}
} catch {
/* fall through */
}
return { text: 'no man page for ' + name, ephemeral: true }
}
function discordCmdSayBox(text) {
const s = String(text || '').slice(0, 200)
const lines = s.split(/\r?\n/).slice(0, 6)
let w = 8
for (let i = 0; i < lines.length; i++) {
if (lines[i].length > w) w = lines[i].length
}
if (w > 48) w = 48
const bar = '+' + Array(w + 3).join('-') + '+'
const body = lines.map(function (ln) {
const t = ln.slice(0, w)
return '| ' + t + Array(w - t.length + 1).join(' ') + ' |'
})
return [bar, body.join('\n'), bar, ' \\', ' cow-ish · bare-os'].join('\n')
}
async function discordCmdHandleRun(ctx, raw) {
const cmd = String(raw || '').trim()
if (!cmd) return { text: 'command required — pick from autocomplete or use Run…', ephemeral: true }
if (/[;&|`$<>(){}]/.test(cmd)) {
return { text: 'metacharacters are not allowed', ephemeral: true }
}
const key = cmd.replace(/\s+/g, ' ')
const bin = key.split(' ')[0]
if (!BARE_OS_DISCORD_RUN_ALLOW[key] && !BARE_OS_DISCORD_RUN_ALLOW[bin]) {
return {
text:
'not in allowlist. Try: uname, whoami, hostname, date, uptime, id, pwd, arch, nproc, help, motd, df, ps, procstat, uname -a',
ephemeral: true
}
}
if (typeof ctx.execLine !== 'function') {
return { text: 'execLine unavailable', ephemeral: true }
}
const out = await discordCmdCapture(ctx, function () {
return ctx.execLine(key)
})
const parsed = discordTryJson(out)
if (parsed && typeof parsed === 'object') {
return discordPrettySnapshot('$ ' + key, parsed)
}
return discordResult(
discordEmbed({
title: '$ ' + key,
footer: 'ran as ' + discordSessionUser(ctx),
desc: out
? discordCmdFence(discordCmdClip(discordCmdRedact(out), 1400))
: '(no output, exit ' + String(ctx.exitCode || 0) + ')'
})
)
}
async function discordCmdHandleJournal(ctx, unit) {
if (typeof ctx.bareOsRunSystemctlCli === 'function' && unit) {
const out = await discordCmdCapture(ctx, function () {
return ctx.bareOsRunSystemctlCli([
'journalctl',
'-u',
String(unit),
'--lines',
'30'
])
})
const lines = String(out || '')
.split(/\r?\n/)
.filter(Boolean)
.slice(-20)
return discordResult(
discordEmbed({
title: 'Journal · ' + unit,
desc: lines.length
? discordCmdFence(
lines
.map(function (ln) {
return discordCmdRedact(ln).slice(0, 180)
})
.join('\n')
)
: 'Empty journal for `' + unit + '`.'
})
)
}
const t =
(await discordCmdReadText(ctx, '/var/log/bare-os/discord.log')) ||
(await discordCmdReadText(ctx, '/var/log/bare-os/kernel-console.log')) ||
(await discordCmdReadText(ctx, '/var/log/messages'))
const lines = String(t || '')
.split(/\r?\n/)
.filter(Boolean)
.slice(-18)
return discordResult(
discordEmbed({
title: 'Journal',
desc: lines.length
? discordCmdFence(
lines
.map(function (ln) {
return discordCmdRedact(ln).slice(0, 180)
})
.join('\n')
)
: 'No journal in `/var/log/bare-os`.'
})
)
}
function discordPanel(ctx) {
const user = discordSessionUser(ctx)
const e = discordCmdEnv(ctx)
return discordResult(
discordEmbed({
title: 'Bare OS control panel',
color: BARE_OS_DISCORD_COLOR,
desc:
'Signed in as **' +
user +
'** · `' +
(e.HOME || '/home/' + user) +
'`\nPick a button, or use a slash command.',
fields: [
discordField('User', user, true),
discordField('Identity', e.BARE_OS_IDENTITY || (ctx.identity && ctx.identity.state) || '', true),
discordField('Host', e.HOSTNAME || 'bare-os', true)
]
})
)
}
function discordCmdOpt(s, name, desc, required, autocomplete) {
if (!s || typeof s.addStringOption !== 'function') return s
return s.addStringOption(function (o) {
o.setName(name).setDescription(desc)
if (required && typeof o.setRequired === 'function') o.setRequired(true)
if (autocomplete && typeof o.setAutocomplete === 'function') o.setAutocomplete(true)
return o
})
}
function discordCmdAddSubs(builder, items) {
if (!builder || typeof builder.addSubcommand !== 'function') return false
for (let i = 0; i < items.length; i++) {
const it = items[i]
builder.addSubcommand(function (s) {
s.setName(it[0]).setDescription(it[1])
if (it[2]) it[2](s)
return s
})
}
return true
}
function discordBuildSlashCommands(dj) {
const B = dj && dj.SlashCommandBuilder
if (typeof B !== 'function') return []
const bare = new B().setName('bare').setDescription('Bare OS session (logged-in user)')
const sys = new B().setName('sys').setDescription('Bare OS system snapshots')
const svc = new B().setName('svc').setDescription('systemctl units')
const fsCmd = new B().setName('fs').setDescription('Read-only VFS')
const net = new B().setName('net').setDescription('Swarm / network')
const man = new B().setName('man').setDescription('Look up a man page')
const say = new B().setName('say').setDescription('Speak as Bare OS (or open a compose form)')
const run = new B().setName('run').setDescription('Run an allowlisted utility as the session user')
const journal = new B()
.setName('journal')
.setDescription('Tail a unit or system log')
const edit = new B()
.setName('edit')
.setDescription('Edit a text file in a Discord modal (home or /tmp)')
const create = new B()
.setName('create')
.setDescription('Create a new text file (pick a path, then enter contents)')
const files = new B()
.setName('files')
.setDescription('Browse and manage files (list, open, mkdir, rename, delete)')
const settings = new B()
.setName('settings')
.setDescription('Live session settings (theme, shell, discord, agent, aliases)')
const panel = new B()
.setName('panel')
.setDescription('Interactive Bare OS control panel')
const ping = new B().setName('ping').setDescription('Reply pong')
const ok =
discordCmdAddSubs(bare, [
['ping', 'Latency / liveness'],
['about', 'What this bot is'],
['help', 'Command map'],
['status', 'Session snapshot (logged-in user)'],
['whoami', 'Session user'],
['hostname', 'Guest hostname'],
['date', 'Clock'],
['uptime', '/proc/uptime'],
['motd', '/etc/motd'],
['uname', 'uname -a style']
]) &&
discordCmdAddSubs(sys, [
['df', 'Disk / host resources'],
['mem', 'Memory info'],
['ps', 'Process table'],
['env', 'Redacted environment'],
['doctor', 'Security / debug posture'],
['features', '/proc/bare_os_features'],
['rlimits', 'Resource limits']
]) &&
discordCmdAddSubs(svc, [
['list', 'systemctl list'],
['status', 'Unit status', function (s) {
discordCmdOpt(s, 'unit', 'Unit name', true, true)
}],
['start', 'Start a unit', function (s) {
discordCmdOpt(s, 'unit', 'Unit name', true, true)
}],
['stop', 'Stop a unit', function (s) {
discordCmdOpt(s, 'unit', 'Unit name', true, true)
}],
['restart', 'Restart a unit', function (s) {
discordCmdOpt(s, 'unit', 'Unit name', true, true)
}],
['logs', 'Unit logs', function (s) {
discordCmdOpt(s, 'unit', 'Unit name', true, true)
}]
]) &&
discordCmdAddSubs(fsCmd, [
['ls', 'List a directory', function (s) {
discordCmdOpt(s, 'path', 'VFS path', false, true)
}],
['cat', 'Read a file', function (s) {
discordCmdOpt(s, 'path', 'VFS path', true, true)
}],
['stat', 'Stat a path', function (s) {
discordCmdOpt(s, 'path', 'VFS path', true, true)
}],
['head', 'First lines of a file', function (s) {
discordCmdOpt(s, 'path', 'VFS path', true, true)
discordCmdOpt(s, 'lines', 'Line count', false, false)
}]
]) &&
discordCmdAddSubs(net, [
['peers', 'Swarm peers'],
['swarm', 'Swarm snapshot'],
['summary', 'net_summary']
])
if (!ok) {
return [ping.toJSON()]
}
discordCmdOpt(man, 'page', 'Command name (e.g. uname)', true, true)
discordCmdOpt(say, 'text', 'Text to box (omit to open a form)', false, false)
discordCmdOpt(run, 'cmd', 'Allowlisted utility', false, true)
discordCmdOpt(journal, 'unit', 'Unit name', false, true)
discordCmdOpt(edit, 'path', 'File under ~ or /tmp (omit to pick)', false, true)
discordCmdOpt(create, 'path', 'New file under ~ or /tmp (omit to pick)', false, true)
discordCmdOpt(files, 'path', 'Directory to open', false, true)
return [bare, sys, svc, fsCmd, net, man, say, run, journal, edit, create, files, settings, panel, ping].map(
function (c) {
return c.toJSON()
}
)
}
function discordShowModal(interaction, spec) {
if (!interaction || typeof interaction.showModal !== 'function') return false
return interaction.showModal({
custom_id: spec.id,
title: String(spec.title || 'Bare OS').slice(0, 45),
components: [
{
type: 1,
components: [
{
type: 4,
custom_id: spec.field || 'text',
label: String(spec.label || 'Value').slice(0, 45),
style: spec.paragraph ? 2 : 1,
required: true,
max_length: spec.max || 200,
placeholder: spec.placeholder || ''
}
]
}
]
})
}
async function discordRouteCommand(ctx, name, sub, opt) {
if (name === 'panel') return discordPanel(ctx)
if (name === 'ping' || (name === 'bare' && (sub === 'ping' || !sub))) {
return discordCmdHandleBare(ctx, 'ping')
}
if (name === 'bare') return discordCmdHandleBare(ctx, sub)
if (name === 'sys') return discordCmdHandleSys(ctx, sub)
if (name === 'svc') return discordCmdHandleSvc(ctx, sub, opt('unit'))
if (name === 'fs') return discordCmdHandleFs(ctx, sub, opt('path'), opt('lines'))
if (name === 'net') return discordCmdHandleNet(ctx, sub)
if (name === 'man') return discordCmdHandleMan(ctx, opt('page'))
if (name === 'say') {
const text = opt('text')
if (!text) return { modal: 'say' }
return discordResult(
discordEmbed({
title: 'say',
desc: discordCmdFence(discordCmdSayBox(text))
})
)
}
if (name === 'run') {
const cmd = opt('cmd')
if (!cmd) return { modal: 'run' }
return discordCmdHandleRun(ctx, cmd)
}
if (name === 'journal') return discordCmdHandleJournal(ctx, opt('unit'))
if (name === 'edit') {
const path = opt('path')
if (!path) return { modal: 'edit:path' }
return { edit: path }
}
if (name === 'create') {
const path = opt('path')
if (!path) return { createPick: true }
return { create: path }
}
if (name === 'files' || name === 'browse') {
return { files: opt('path') || '' }
}
if (name === 'settings') return { settings: true }
return { text: 'unknown command: /' + name, ephemeral: true }
}
async function discordBeginEdit(ctx, interaction, rawPath) {
const p = discordCmdPathWriteOk(ctx, rawPath)
if (!p) {
return discordSendResult(ctx, interaction, {
text:
'Cannot edit that path. Writable locations: `~/…`, `/home/' +
discordSessionUser(ctx) +
'/…`, `/tmp/…`. Token files are blocked.',
ephemeral: true
})
}
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') {
return discordSendResult(ctx, interaction, {
text: 'vfs.writeFile is unavailable in this session',
ephemeral: true
})
}
let text = await discordCmdReadText(ctx, p)
const created = !text
if (text && discordLooksBinary(text)) {
return discordSendResult(ctx, interaction, {
text: 'Refusing to open a binary file in a Discord modal: `' + p + '`',
ephemeral: true
})
}
const split = discordEditChunks(text)
discordEditPut(interaction, {
path: p,
origLen: split.origLen,
truncated: split.truncated,
created: created
})
if (typeof interaction.showModal !== 'function') {
return discordSendResult(ctx, interaction, {
text: 'This client cannot show Discord modals.',
ephemeral: true
})
}
try {
await interaction.showModal(discordEditModalPayload(p, split.chunks))
} catch (err) {
if (ctx.console && typeof ctx.console.error === 'function') {
ctx.console.error(
'discord-bot: edit modal failed: ' + ((err && err.message) || err)
)
}
return discordSendResult(ctx, interaction, {
text: 'Could not open the editor modal: ' + ((err && err.message) || err),
ephemeral: true
})
}
}
async function discordEditSaveFromModal(ctx, interaction) {
const rec = discordEditGet(interaction)
if (!rec || !rec.path) {
return {
text: 'Edit session expired (15 minutes). Run `/edit` again.',
ephemeral: true
}
}
const p = discordCmdPathWriteOk(ctx, rec.path)
if (!p) {
return { text: 'Write not allowed for `' + rec.path + '`', ephemeral: true }
}
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') {
return { text: 'vfs.writeFile is unavailable', ephemeral: true }
}
const parts = []
for (let i = 0; i < BARE_OS_DISCORD_EDIT_MAX_CHUNKS; i++) {
try {
if (interaction.fields && typeof interaction.fields.getTextInputValue === 'function') {
parts.push(String(interaction.fields.getTextInputValue('c' + i) || ''))
}
} catch {
/* missing chunk */
}
}
const body = parts.join('')
if (rec.created && (await discordFileExists(ctx, p))) {
return {
text: '`' + p + '` already exists. Use `/edit` to change it.',
ephemeral: true
}
}
try {
await ctx.vfs.writeFile(p, discordTextToBuf(ctx, body))
} catch (err) {
return {
text: 'Save failed: ' + ((err && err.message) || err),
ephemeral: true
}
}
discordEditClear(interaction)
const lines = body ? body.split(/\r?\n/).length : 0
return discordResult(
discordEmbed({
title: rec.created ? 'Created ' + p : 'Saved ' + p,
color: BARE_OS_DISCORD_COLOR_OK,
fields: [
discordField('Path', p, false),
discordField('Bytes', String(body.length), true),
discordField('Lines', String(lines), true),
discordField('User', discordSessionUser(ctx), true)
],
desc: rec.truncated
? 'Original file was larger than the Discord modal cap (20000 characters). Only the loaded window was saved.'
: 'Written through `ctx.vfs.writeFile` as **' +
discordSessionUser(ctx) +
'**.'
}),
{
components: [
discordButtons([
{ id: 'edit:open', label: 'Edit again', style: 1 },
{ id: 'edit:new', label: 'Edit another…' },
{ id: 'create:new', label: 'Create another…', style: 3 }
])
].filter(Boolean),
editPath: p
}
)
}
function discordCreatePicker(ctx) {
const choices = discordSuggestEditPaths(ctx, '')
const sel = discordSelect(
'create:pick',
'Choose a new file path…',
choices.map(function (c) {
return { label: c.name, value: c.value, description: 'Create this path' }
})
)
return discordResult(
discordEmbed({
title: 'Create a file',
desc:
'Pick a suggested path, or **Custom path…** to type one. Then enter the contents in a Discord modal.\nWritable: `~/…`, `/tmp/…`, `/home/' +
discordSessionUser(ctx) +
'/…`.'
}),
{
components: [
sel,
discordButtons([
{ id: 'create:custom', label: 'Custom path…', style: 1 }
])
].filter(Boolean)
}
)
}
var BARE_OS_DISCORD_SET_SESSIONS = Object.create(null)
var BARE_OS_DISCORD_SET_TTL_MS = BARE_OS_DISCORD_IDLE_MS
var BARE_OS_DISCORD_THEME_FALLBACK = [
'catppuccin_mocha',
'default',
'dracula',
'github_dark',
'gruvbox_dark',
'nord',
'solarized_dark',
'tokyo_night'
]
var BARE_OS_DISCORD_SET_PAGE = 8
var BARE_OS_DISCORD_SET_SECRET_RE = /token|secret|password|passwd|api[_-]?key|authorization|sasl/i
var BARE_OS_DISCORD_SET_GROUPS = [
{ id: 'appearance', label: 'Appearance', description: 'Theme, colors, TUI' },
{ id: 'shell', label: 'Shell', description: 'Live flags + completion' },
{ id: 'session', label: 'Session', description: 'Hostname, locale, editor' },
{ id: 'discord', label: 'Discord', description: 'Guild, whitelist (not token)' },
{ id: 'agent', label: 'Agent', description: '~/.agent/config.json' },
{ id: 'irc', label: 'IRC', description: '~/.irc/config.json' },
{ id: 'aliases', label: 'Aliases', description: 'shellAliases + ~/.barerc' }
]
var BARE_OS_DISCORD_SETTINGS = [
{
id: 'theme',
group: 'appearance',
label: 'Theme',
kind: 'theme',
env: 'BARE_OS_THEME',
persist: 'theme',
live: 'now (prompt / ls)'
},
{
id: 'color_depth',
group: 'appearance',
label: 'Color depth',
kind: 'enum',
env: 'BARE_OS_COLOR_DEPTH',
values: ['truecolor', '256', '16', '8'],
persist: 'export',
live: 'now (reapply theme)'
},
{
id: 'ls_lock',
group: 'appearance',
label: 'Lock LS_COLORS',
kind: 'bool',
env: 'BARE_OS_LS_COLORS_LOCKED',
persist: 'export',
live: 'now (reapply theme)'
},
{
id: 'tui_noalt',
group: 'appearance',
label: 'TUI no altscreen',
kind: 'bool',
env: 'BARE_OS_TUI_NO_ALTSCREEN',
persist: 'export',
live: 'next TUI'
},
{
id: 'no_color',
group: 'appearance',
label: 'NO_COLOR',
kind: 'bool',
env: 'NO_COLOR',
persist: 'export',
live: 'now (reapply theme)'
},
{
id: 'dircolors',
group: 'appearance',
label: 'dircolors file',
kind: 'string',
env: 'BARE_OS_DIRCOLORS',
persist: 'export',
live: 'now (reapply theme)'
},
{
id: 'compact',
group: 'shell',
label: 'Compact completion',
kind: 'bool',
env: 'BARE_OS_COMPACT_MENU',
persist: 'export',
live: 'next Tab'
},
{
id: 'errexit',
group: 'shell',
label: 'errexit (set -e)',
kind: 'bool',
env: 'BARE_OS_SHELL_ERREXIT',
persist: 'export',
live: 'next command'
},
{
id: 'nounset',
group: 'shell',
label: 'nounset (set -u)',
kind: 'bool',
env: 'BARE_OS_SHELL_NOUNSET',
persist: 'export',
live: 'next command'
},
{
id: 'noglob',
group: 'shell',
label: 'noglob (set -f)',
kind: 'bool',
env: 'BARE_OS_SHELL_NOGLOB',
persist: 'export',
live: 'next command'
},
{
id: 'pipefail',
group: 'shell',
label: 'pipefail',
kind: 'bool',
env: 'BARE_OS_SHELL_PIPEFAIL',
persist: 'export',
live: 'next pipeline'
},
{
id: 'pipestatus',
group: 'shell',
label: 'PIPESTATUS',
kind: 'bool',
env: 'BARE_OS_SHELL_PIPESTATUS',
persist: 'export',
live: 'next pipeline'
},
{
id: 'posix',
group: 'shell',
label: 'POSIX mode',
kind: 'bool',
env: 'BARE_OS_SHELL_POSIX_MODE',
persist: 'export',
live: 'next command'
},
{
id: 'grouping',
group: 'shell',
label: 'Grouping ( … )',
kind: 'bool',
env: 'BARE_OS_SHELL_GROUPING',
persist: 'export',
live: 'next command'
},
{
id: 'dbracket',
group: 'shell',
label: '[[ … ]] tests',
kind: 'bool',
env: 'BARE_OS_SHELL_DOUBLE_BRACKET',
persist: 'export',
live: 'next command'
},
{
id: 'cmdsubst',
group: 'shell',
label: 'Command subst',
kind: 'bool',
env: 'BARE_OS_SHELL_CMDSUBST',
persist: 'export',
live: 'next $(…)'
},
{
id: 'streaming',
group: 'shell',
label: 'Pipeline streaming',
kind: 'bool',
env: 'BARE_OS_SHELL_STREAMING',
persist: 'export',
live: 'next pipeline'
},
{
id: 'brace',
group: 'shell',
label: 'Brace expansion',
kind: 'bool',
env: 'BARE_OS_SHELL_BRACE_EXPANSION',
persist: 'export',
live: 'next command'
},
{
id: 'paramexp',
group: 'shell',
label: 'Param expansion',
kind: 'bool',
env: 'BARE_OS_SHELL_PARAM_EXPANSION',
persist: 'export',
live: 'next ${…}'
},
{
id: 'until',
group: 'shell',
label: 'until loops',
kind: 'bool',
env: 'BARE_OS_SHELL_UNTIL',
persist: 'export',
live: 'next command'
},
{
id: 'loopctl',
group: 'shell',
label: 'break / continue',
kind: 'bool',
env: 'BARE_OS_SHELL_LOOP_CONTROL',
persist: 'export',
live: 'next loop'
},
{
id: 'readb',
group: 'shell',
label: 'read builtin',
kind: 'bool',
env: 'BARE_OS_SHELL_READ_BUILTIN',
persist: 'export',
live: 'next read'
},
{
id: 'hostname',
group: 'session',
label: 'Hostname',
kind: 'string',
env: 'HOSTNAME',
persist: 'hostname',
live: 'now (needs BARE_OS_HOSTNAME_SET=1)'
},
{
id: 'tz',
group: 'session',
label: 'Timezone (TZ)',
kind: 'string',
env: 'TZ',
persist: 'export',
live: 'next date'
},
{
id: 'lang',
group: 'session',
label: 'Locale (LANG)',
kind: 'string',
env: 'LANG',
persist: 'export',
live: 'next command'
},
{
id: 'editor',
group: 'session',
label: 'EDITOR',
kind: 'string',
env: 'EDITOR',
persist: 'export',
live: 'next editor'
},
{
id: 'pager',
group: 'session',
label: 'PAGER',
kind: 'string',
env: 'PAGER',
persist: 'export',
live: 'next pager'
},
{
id: 'guild',
group: 'discord',
label: 'Guild id',
kind: 'string',
env: 'DISCORD_GUILD_ID',
persist: 'discord',
live: 'next slash register'
},
{
id: 'whitelist',
group: 'discord',
label: 'User id whitelist',
kind: 'string',
env: 'DISCORD_ID_WHITELIST',
persist: 'discord',
live: 'next command'
},
{
id: 'dbg',
group: 'discord',
label: 'Debug logs',
kind: 'bool',
env: 'DISCORD_DEBUG',
persist: 'discord',
live: 'next bot start'
},
{
id: 'msgc',
group: 'discord',
label: 'Message Content',
kind: 'bool',
env: 'DISCORD_MESSAGE_CONTENT',
persist: 'discord',
live: 'next bot start'
},
{
id: 'loginms',
group: 'discord',
label: 'Login timeout ms',
kind: 'number',
env: 'DISCORD_LOGIN_TIMEOUT_MS',
persist: 'discord',
live: 'next bot start'
},
{
id: 'ag_backend',
group: 'agent',
label: 'Backend',
kind: 'enum',
jsonKey: 'backend',
values: ['qvac', 'rest'],
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_profile',
group: 'agent',
label: 'QVAC profile',
kind: 'enum',
jsonKey: 'qvac_profile',
values: ['recommended', 'strong', 'tool-tiny'],
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_model',
group: 'agent',
label: 'Model',
kind: 'string',
jsonKey: 'model',
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_qvac_model',
group: 'agent',
label: 'QVAC model',
kind: 'string',
jsonKey: 'qvac_model',
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_owner',
group: 'agent',
label: 'Owner name',
kind: 'string',
jsonKey: 'owner_name',
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_label',
group: 'agent',
label: 'Agent label',
kind: 'string',
jsonKey: 'agent_label',
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_url',
group: 'agent',
label: 'REST base URL',
kind: 'string',
jsonKey: 'rest_base_url',
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_temp',
group: 'agent',
label: 'Temperature',
kind: 'number',
jsonKey: 'temperature',
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_maxtok',
group: 'agent',
label: 'Max tokens',
kind: 'number',
jsonKey: 'max_tokens',
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_maxiter',
group: 'agent',
label: 'Max iterations',
kind: 'number',
jsonKey: 'max_iterations',
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_stream',
group: 'agent',
label: 'Stream',
kind: 'bool',
jsonKey: 'stream',
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_del',
group: 'agent',
label: 'Allow delete',
kind: 'bool',
jsonKey: 'allow_delete',
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_reason',
group: 'agent',
label: 'Show reasoning',
kind: 'bool',
jsonKey: 'show_reasoning',
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_reason_mode',
group: 'agent',
label: 'Reasoning mode',
kind: 'enum',
jsonKey: 'reasoning_mode',
values: ['off', 'summary', 'trace'],
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_bridge',
group: 'agent',
label: 'Bridge mutations',
kind: 'bool',
jsonKey: 'allow_bridge_mutations',
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_notify',
group: 'agent',
label: 'Host notifications',
kind: 'bool',
jsonKey: 'allow_host_notifications',
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_hostact',
group: 'agent',
label: 'Host actions',
kind: 'bool',
jsonKey: 'allow_host_actions',
persist: 'agent',
live: 'next agent'
},
{
id: 'irc_nick',
group: 'irc',
label: 'Nick',
kind: 'string',
jsonKey: 'nick',
persist: 'irc',
live: 'next irc'
},
{
id: 'irc_join',
group: 'irc',
label: 'Autojoin',
kind: 'string',
jsonKey: 'autojoin',
persist: 'irc',
live: 'next irc'
},
{
id: 'irc_share',
group: 'irc',
label: 'Share channels',
kind: 'bool',
jsonKey: 'shareChannels',
persist: 'irc',
live: 'next irc'
}
]
function discordSetKey(interaction) {
return discordInteractionUserId(interaction) || 'anon'
}
function discordSetGet(interaction) {
const now = Date.now()
for (const k in BARE_OS_DISCORD_SET_SESSIONS) {
const r = BARE_OS_DISCORD_SET_SESSIONS[k]
if (!r || now - r.atMs > BARE_OS_DISCORD_SET_TTL_MS) delete BARE_OS_DISCORD_SET_SESSIONS[k]
}
let rec = BARE_OS_DISCORD_SET_SESSIONS[discordSetKey(interaction)]
if (!rec) {
rec = { group: 'appearance', sel: '', page: 0, pending: '', atMs: now }
BARE_OS_DISCORD_SET_SESSIONS[discordSetKey(interaction)] = rec
}
rec.atMs = now
if (rec.page == null) rec.page = 0
if (rec.pending == null) rec.pending = ''
return rec
}
function discordSettingsGet(interaction) {
return discordSetGet(interaction)
}
function discordSettingsSpec(id) {
for (let i = 0; i < BARE_OS_DISCORD_SETTINGS.length; i++) {
if (BARE_OS_DISCORD_SETTINGS[i].id === id) return BARE_OS_DISCORD_SETTINGS[i]
}
return null
}
function discordSettingsFind(id) {
return discordSettingsSpec(id)
}
function discordSettingsInGroup(group) {
const out = []
for (let i = 0; i < BARE_OS_DISCORD_SETTINGS.length; i++) {
if (BARE_OS_DISCORD_SETTINGS[i].group === group) out.push(BARE_OS_DISCORD_SETTINGS[i])
}
return out
}
function discordSettingsPageOf(items, page) {
const size = BARE_OS_DISCORD_SET_PAGE
const pages = Math.max(1, Math.ceil(items.length / size) || 1)
const p = Math.min(Math.max(0, page | 0), pages - 1)
return { slice: items.slice(p * size, p * size + size), page: p, pages: pages }
}
function discordThemeNames(ctx) {
if (ctx && typeof ctx.bareOsListThemes === 'function') {
try {
const n = ctx.bareOsListThemes()
if (Array.isArray(n) && n.length) return n.slice().sort()
} catch {
/* fall through */
}
}
return BARE_OS_DISCORD_THEME_FALLBACK.slice()
}
function discordEnvOn(raw) {
if (raw === true) return true
const v = String(raw == null ? '' : raw).trim().toLowerCase()
return v === '1' || v === 'true' || v === 'yes' || v === 'on'
}
function discordSettingsIsSecretKey(key) {
return BARE_OS_DISCORD_SET_SECRET_RE.test(String(key || ''))
}
function discordSettingsAliasName(raw) {
const t = String(raw || '').trim()
const eq = t.indexOf('=')
const name = (eq >= 0 ? t.slice(0, eq) : t).trim()
if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(name)) return ''
return name
}
function discordSettingsAliasValue(raw) {
const t = String(raw || '').trim()
const eq = t.indexOf('=')
if (eq < 0) return ''
let v = t.slice(eq + 1).trim()
if (
(v.charAt(0) === "'" && v.charAt(v.length - 1) === "'") ||
(v.charAt(0) === '"' && v.charAt(v.length - 1) === '"')
) {
v = v.slice(1, -1)
}
return v
}
function discordSettingsQuoteAlias(value) {
const v = String(value == null ? '' : value)
if (/^[A-Za-z0-9_./:+@%-]+$/.test(v)) return v
return "'" + v.replace(/'/g, "'\\''") + "'"
}
async function discordEnsureDir(ctx, path) {
if (!ctx || !ctx.vfs || typeof ctx.vfs.mkdir !== 'function') return
try {
await ctx.vfs.mkdir(path, { recursive: true })
} catch {
/* already exists or vfs without mkdir */
}
}
async function discordReadJsonFile(ctx, path) {
const t = await discordCmdReadText(ctx, path)
const j = discordTryJson(t)
return j && typeof j === 'object' && !Array.isArray(j) ? j : {}
}
async function discordWriteJsonFile(ctx, path, obj) {
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') {
throw new Error('writeFile unavailable')
}
const parent = discordParentPath(path)
if (parent && parent !== path) await discordEnsureDir(ctx, parent)
const sanitized = {}
const keys = Object.keys(obj || {})
for (let i = 0; i < keys.length; i++) {
const k = keys[i]
if (discordSettingsIsSecretKey(k)) {
sanitized[k] = obj[k]
continue
}
sanitized[k] = obj[k]
}
const body = JSON.stringify(sanitized, null, 2) + '\n'
await ctx.vfs.writeFile(path, discordTextToBuf(ctx, body))
}
async function discordPersistBarercLine(ctx, matcher, line) {
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') {
throw new Error('writeFile unavailable')
}
const text = await discordCmdReadText(ctx, '~/.barerc')
const lines = text ? text.split(/\r?\n/) : []
const drop = line == null || line === ''
let replaced = false
const out = []
for (let i = 0; i < lines.length; i++) {
if (matcher.test(lines[i])) {
if (!replaced && !drop) {
out.push(line)
replaced = true
}
} else out.push(lines[i])
}
if (!replaced && !drop) {
if (out.length && String(out[out.length - 1]).trim() !== '') out.push('')
out.push(line)
}
while (out.length && String(out[out.length - 1]).trim() === '') out.pop()
let next = out.join('\n')
if (next) next += '\n'
await ctx.vfs.writeFile('~/.barerc', discordTextToBuf(ctx, next))
}
async function discordPersistDiscordEnvKey(ctx, key, value) {
if (discordSettingsIsSecretKey(key) || key === 'DISCORD_TOKEN') {
throw new Error('refusing to write a secret key')
}
const path = '~/.discord/.env'
let text = await discordCmdReadText(ctx, path)
const line = key + '=' + String(value)
const re = new RegExp('^\\s*(?:export\\s+)?' + key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=.*$', 'm')
if (re.test(text)) text = text.replace(re, line)
else text = (text && !/\n$/.test(text) ? text + '\n' : text || '') + line + '\n'
await discordEnsureDir(ctx, '~/.discord')
await ctx.vfs.writeFile(path, discordTextToBuf(ctx, text))
}
async function discordSettingsCurrent(ctx, spec) {
const env = discordCmdEnv(ctx)
if (spec.persist === 'agent') {
const cfg = await discordReadJsonFile(ctx, '~/.agent/config.json')
return cfg[spec.jsonKey]
}
if (spec.persist === 'irc') {
const cfg = await discordReadJsonFile(ctx, '~/.irc/config.json')
const v = cfg[spec.jsonKey]
if (Array.isArray(v)) return v.join(',')
return v
}
if (spec.env) return env[spec.env]
return ''
}
function discordSettingsDisplay(spec, raw) {
if (spec.kind === 'bool') return discordEnvOn(raw) ? 'on' : 'off'
if (raw == null || raw === '') return '(unset)'
return String(raw)
}
function discordSettingsCoerce(spec, value) {
if (spec.kind === 'bool') {
const on = value === true || value === 'on' || discordEnvOn(value)
if (spec.persist === 'agent' || spec.persist === 'irc') return on
return on ? '1' : '0'
}
if (spec.kind === 'number') {
const n = Number(value)
if (!Number.isFinite(n)) throw new Error('not a number')
return n
}
if (spec.kind === 'theme') {
const name = String(value || '')
.toLowerCase()
.trim()
.replace(/\s+/g, '_')
if (!/^[a-z0-9_.-]+$/.test(name)) throw new Error('invalid theme name')
return name
}
if (spec.kind === 'enum') {
const want = String(value == null ? '' : value)
const ok = spec.values && spec.values.indexOf(want) >= 0
if (!ok) throw new Error('invalid value')
return want
}
return String(value == null ? '' : value)
}
async function discordSettingsApply(ctx, spec, value) {
if (!spec) throw new Error('unknown setting')
if (spec.env && discordSettingsIsSecretKey(spec.env)) {
throw new Error('refusing to write a secret')
}
if (spec.jsonKey && discordSettingsIsSecretKey(spec.jsonKey)) {
throw new Error('refusing to write a secret')
}
const env = discordCmdEnv(ctx)
let v = discordSettingsCoerce(spec, value)
if (spec.kind === 'hostname' || spec.persist === 'hostname') {
const host = String(v).trim()
if (!/^[A-Za-z0-9][A-Za-z0-9.-]{0,62}$/.test(host)) throw new Error('invalid hostname')
v = host
}
if (spec.env) {
env[spec.env] = String(v)
if (ctx.env && ctx.env !== env) ctx.env[spec.env] = String(v)
}
if (spec.persist === 'export') {
await discordPersistBarercLine(
ctx,
new RegExp('^\\s*export\\s+' + spec.env + '='),
'export ' + spec.env + '=' + v
)
} else if (spec.persist === 'theme') {
await discordPersistBarercLine(ctx, /^\s*theme\s+/, 'theme ' + v)
if (typeof ctx.bareOsApplyTheme === 'function') await ctx.bareOsApplyTheme()
} else if (spec.persist === 'discord') {
await discordPersistDiscordEnvKey(ctx, spec.env, String(v))
} else if (spec.persist === 'hostname') {
try {
if (typeof ctx.bareOsSetSessionHostname === 'function') {
ctx.bareOsSetSessionHostname(String(v))
} else {
env.HOSTNAME = String(v)
env.COMPUTERNAME = String(v)
if (ctx.env && ctx.env !== env) {
ctx.env.HOSTNAME = String(v)
ctx.env.COMPUTERNAME = String(v)
}
}
} catch {
env.HOSTNAME = String(v)
env.COMPUTERNAME = String(v)
if (ctx.env && ctx.env !== env) {
ctx.env.HOSTNAME = String(v)
ctx.env.COMPUTERNAME = String(v)
}
}
await discordPersistBarercLine(ctx, /^\s*export\s+HOSTNAME=/, 'export HOSTNAME=' + v)
} else if (spec.persist === 'agent') {
const cfg = await discordReadJsonFile(ctx, '~/.agent/config.json')
if (spec.jsonKey === 'backend') cfg.backend = String(v) === 'rest' ? 'rest' : 'qvac'
else cfg[spec.jsonKey] = v
await discordWriteJsonFile(ctx, '~/.agent/config.json', cfg)
} else if (spec.persist === 'irc') {
const cfg = await discordReadJsonFile(ctx, '~/.irc/config.json')
if (spec.jsonKey === 'autojoin') {
cfg.autojoin = String(v)
.split(',')
.map(function (s) {
return s.trim()
})
.filter(Boolean)
} else cfg[spec.jsonKey] = v
await discordWriteJsonFile(ctx, '~/.irc/config.json', cfg)
}
if (
spec.persist === 'theme' ||
spec.id === 'color_depth' ||
spec.id === 'ls_lock' ||
spec.id === 'no_color' ||
spec.id === 'dircolors'
) {
if (spec.persist !== 'theme' && typeof ctx.bareOsApplyTheme === 'function') {
await ctx.bareOsApplyTheme()
}
}
}
async function discordSettingsSetAlias(ctx, raw) {
const name = discordSettingsAliasName(raw)
const value = discordSettingsAliasValue(raw)
if (!name || !value) throw new Error('use name=command')
if (!ctx.shellAliases || typeof ctx.shellAliases !== 'object') ctx.shellAliases = {}
ctx.shellAliases[name] = value
await discordPersistBarercLine(
ctx,
new RegExp('^\\s*unalias\\s+' + name + '\\b'),
''
)
await discordPersistBarercLine(
ctx,
new RegExp('^\\s*alias\\s+' + name + '='),
'alias ' + name + '=' + discordSettingsQuoteAlias(value)
)
}
async function discordSettingsRemoveAlias(ctx, name) {
const n = discordSettingsAliasName(name)
if (!n) throw new Error('bad alias name')
if (ctx.shellAliases && n) delete ctx.shellAliases[n]
await discordPersistBarercLine(ctx, new RegExp('^\\s*alias\\s+' + n + '='), '')
await discordPersistBarercLine(ctx, new RegExp('^\\s*unalias\\s+' + n + '\\b'), 'unalias ' + n)
}
function discordSettingsChoices(ctx, spec) {
if (spec.kind === 'theme') {
return discordThemeNames(ctx)
.slice(0, 25)
.map(function (n) {
return { label: n, value: n }
})
}
if (spec.kind === 'enum' && spec.values) {
return spec.values.map(function (n) {
return { label: String(n), value: String(n) }
})
}
if (spec.kind === 'bool') {
return [
{ label: 'on', value: '1' },
{ label: 'off', value: '0' }
]
}
return []
}
function discordSettingsGroupMeta(id) {
for (let i = 0; i < BARE_OS_DISCORD_SET_GROUPS.length; i++) {
if (BARE_OS_DISCORD_SET_GROUPS[i].id === id) return BARE_OS_DISCORD_SET_GROUPS[i]
}
return { id: id, label: id, description: '' }
}
function discordSettingsAliasEntries(ctx) {
const al = (ctx.shellAliases && typeof ctx.shellAliases === 'object' && ctx.shellAliases) || {}
return Object.keys(al)
.sort()
.map(function (k) {
return { name: k, value: String(al[k]) }
})
}
async function discordSettingsView(ctx, interaction) {
const rec = discordSetGet(interaction)
const group = rec.group || 'appearance'
const meta = discordSettingsGroupMeta(group)
const fields = []
let pages = 1
let page = 0
let itemOpts = []
if (group === 'aliases') {
const entries = discordSettingsAliasEntries(ctx)
const pg = discordSettingsPageOf(entries, rec.page)
rec.page = pg.page
pages = pg.pages
page = pg.page
if (!entries.length) fields.push(discordField('Aliases', 'None defined. Add one from the menu.'))
for (let i = 0; i < pg.slice.length; i++) {
const e = pg.slice[i]
fields.push(discordField((rec.sel === e.name ? '▸ ' : '') + e.name, '`' + (e.value || '') + '`'))
}
itemOpts = [{ label: 'Add alias', value: 'alias_add', description: 'name=command' }].concat(
pg.slice.map(function (e) {
return { label: 'Remove ' + e.name, value: 'alias_rm:' + e.name, description: e.value.slice(0, 80) }
})
)
} else {
const items = discordSettingsInGroup(group)
const pg = discordSettingsPageOf(items, rec.page)
rec.page = pg.page
pages = pg.pages
page = pg.page
for (let i = 0; i < pg.slice.length; i++) {
const spec = pg.slice[i]
const cur = await discordSettingsCurrent(ctx, spec)
fields.push(
discordField(
(rec.sel === spec.id ? '▸ ' : '') + spec.label,
'**' + discordSettingsDisplay(spec, cur) + '**\n' + spec.live
)
)
}
itemOpts = pg.slice.map(function (s) {
return { label: s.label, value: s.id, description: s.live }
})
}
const gsel = discordSelect(
'set:group',
'Category',
BARE_OS_DISCORD_SET_GROUPS.map(function (g) {
return { label: g.label, value: g.id, description: g.description }
})
)
const isel = discordSelect('set:item', pages > 1 ? 'Setting… p' + (page + 1) + '/' + pages : 'Setting…', itemOpts)
const acts = discordButtons([
{ id: 'set:edit', label: 'Edit', style: 1 },
{ id: 'set:toggle', label: 'Toggle' },
{ id: 'set:reload', label: 'Reload barerc' },
{ id: 'nav:panel', label: 'Panel' }
])
const pageRow =
pages > 1
? discordButtons([
{ id: 'set:prev', label: '← Prev', style: 2 },
{ id: 'set:next', label: 'Next →', style: 2 }
])
: null
return discordResult(
discordEmbed({
title: 'Settings · ' + meta.label + (pages > 1 ? ' · ' + (page + 1) + '/' + pages : ''),
desc: meta.description + '. Secrets are never shown. Pick a row, then **Edit** or **Toggle**.',
fields: fields.slice(0, 20),
footer: discordSessionUser(ctx) + ' · expires after 2m idle'
}),
{ components: [gsel, isel, acts, pageRow].filter(Boolean), nav: false }
)
}
async function discordSettingsOpenEditor(ctx, interaction, spec) {
if (!spec) {
return discordSendResult(ctx, interaction, {
text: 'Pick a setting from the menu first.',
ephemeral: true
})
}
const rec = discordSetGet(interaction)
rec.sel = spec.id
rec.pending = spec.id
if (spec.kind === 'enum' || spec.kind === 'theme' || spec.kind === 'bool') {
const opts = discordSettingsChoices(ctx, spec)
const cur = await discordSettingsCurrent(ctx, spec)
return discordSendResult(
ctx,
interaction,
discordResult(
discordEmbed({
title: spec.label,
desc:
'Current: **' +
discordSettingsDisplay(spec, cur) +
'**. Applies live where possible and persists. ' +
spec.live +
'.'
}),
{
components: [discordSelect('set:val', spec.label, opts), discordButtons([{ id: 'set:back', label: 'Back' }])],
nav: false
}
)
)
}
const cur = await discordSettingsCurrent(ctx, spec)
await discordShowModal(interaction, {
id: 'set:str',
title: spec.label.slice(0, 45),
label: spec.label.slice(0, 45),
placeholder: discordSettingsDisplay(spec, cur).slice(0, 80),
max: 200
})
}
async function discordSettingsAction(ctx, interaction, id) {
const rec = discordSetGet(interaction)
if (id === 'set:home') {
rec.group = 'appearance'
rec.sel = ''
rec.page = 0
rec.pending = ''
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
}
if (id === 'set:group') {
rec.group = String((interaction.values && interaction.values[0]) || rec.group)
rec.sel = ''
rec.page = 0
rec.pending = ''
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
}
if (id === 'set:prev') {
rec.page = Math.max(0, (rec.page | 0) - 1)
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
}
if (id === 'set:next') {
rec.page = (rec.page | 0) + 1
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
}
if (id === 'set:item') {
const v = String((interaction.values && interaction.values[0]) || '')
if (v === 'alias_add') {
rec.sel = 'alias_add'
rec.pending = 'alias_add'
await discordShowModal(interaction, {
id: 'set:alias',
title: 'Add alias',
label: 'name=command',
placeholder: 'll=ls -la',
max: 80
})
return
}
if (v.indexOf('alias_rm:') === 0) {
try {
await discordSettingsRemoveAlias(ctx, v.slice(9))
} catch (err) {
return discordSendResult(ctx, interaction, {
text: 'Remove alias failed: ' + ((err && err.message) || err),
ephemeral: true
})
}
rec.sel = ''
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
}
rec.sel = v
rec.pending = v
return discordSettingsOpenEditor(ctx, interaction, discordSettingsSpec(v))
}
if (id === 'set:val') {
const spec = discordSettingsSpec(rec.sel)
const v = String((interaction.values && interaction.values[0]) || '')
if (spec) {
try {
await discordSettingsApply(ctx, spec, v)
} catch (err) {
return discordSendResult(ctx, interaction, {
text: 'Apply failed: ' + ((err && err.message) || err),
ephemeral: true
})
}
}
rec.pending = ''
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
}
if (id === 'set:toggle') {
const spec = discordSettingsSpec(rec.sel)
if (!spec || spec.kind !== 'bool') {
return discordSendResult(ctx, interaction, {
text: 'Select a boolean setting first, then Toggle.',
ephemeral: true
})
}
const cur = await discordSettingsCurrent(ctx, spec)
const next = discordEnvOn(cur) ? '0' : '1'
try {
await discordSettingsApply(ctx, spec, next)
} catch (err) {
return discordSendResult(ctx, interaction, {
text: 'Toggle failed: ' + ((err && err.message) || err),
ephemeral: true
})
}
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
}
if (id === 'set:edit') {
if (rec.group === 'aliases') {
rec.pending = 'alias_add'
await discordShowModal(interaction, {
id: 'set:alias',
title: 'Add alias',
label: 'name=command',
placeholder: 'll=ls -la',
max: 80
})
return
}
return discordSettingsOpenEditor(ctx, interaction, discordSettingsSpec(rec.sel))
}
if (id === 'set:reload') {
if (typeof ctx.execLine === 'function') {
try {
await ctx.execLine('barerc reload')
} catch {
/* ignore */
}
}
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
}
if (id === 'set:back') {
rec.pending = ''
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
}
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
}
async function discordBeginCreate(ctx, interaction, rawPath) {
const p = discordCmdPathWriteOk(ctx, rawPath)
if (!p) {
return discordSendResult(ctx, interaction, {
text:
'Cannot create that path. Use `~/file`, `/tmp/file`, or `/home/' +
discordSessionUser(ctx) +
'/file`. Token files are blocked.',
ephemeral: true
})
}
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') {
return discordSendResult(ctx, interaction, {
text: 'vfs.writeFile is unavailable in this session',
ephemeral: true
})
}
if (await discordFileExists(ctx, p)) {
discordEditPut(interaction, { path: p })
return discordSendResult(
ctx,
interaction,
discordResult(
discordEmbed({
title: 'Already exists',
color: BARE_OS_DISCORD_COLOR_WARN,
desc:
'`' +
p +
'` is already on disk. Use **Edit** to change it, or pick another path.'
}),
{
components: [
discordButtons([
{ id: 'edit:open', label: 'Edit instead', style: 1 },
{ id: 'create:new', label: 'Different path…', style: 3 }
])
],
editPath: p
}
)
)
}
const split = discordEditChunks('')
discordEditPut(interaction, {
path: p,
origLen: 0,
truncated: false,
created: true
})
if (typeof interaction.showModal !== 'function') {
return discordSendResult(ctx, interaction, {
text: 'This client cannot show Discord modals.',
ephemeral: true
})
}
try {
const modal = discordEditModalPayload(p, split.chunks)
modal.custom_id = 'create:save'
modal.title = ('Create ' + (String(p).split('/').pop() || p)).slice(0, 45)
await interaction.showModal(modal)
} catch (err) {
if (ctx.console && typeof ctx.console.error === 'function') {
ctx.console.error(
'discord-bot: create modal failed: ' + ((err && err.message) || err)
)
}
return discordSendResult(ctx, interaction, {
text: 'Could not open the create modal: ' + ((err && err.message) || err),
ephemeral: true
})
}
}
function discordIdleNow() {
return Date.now()
}
function discordIdleSchedule(fn, ms) {
const tfn = typeof setTimeout === 'function' ? setTimeout : globalThis.setTimeout
if (typeof tfn !== 'function') return null
const t = tfn(fn, ms)
if (t && typeof t.unref === 'function') t.unref()
return t
}
function discordIdleClearTimer(t) {
const cfn = typeof clearTimeout === 'function' ? clearTimeout : globalThis.clearTimeout
if (t != null && typeof cfn === 'function') cfn(t)
}
function discordIdleExpiredEmbeds(embeds) {
const out = []
if (!Array.isArray(embeds)) return out
for (let i = 0; i < embeds.length; i++) {
const e = embeds[i] || {}
const n = {}
for (const k in e) n[k] = e[k]
n.footer = { text: 'Expired after 2 minutes idle' }
n.color = BARE_OS_DISCORD_COLOR_WARN
out.push(n)
}
return out
}
function discordIdleForgetSessions(userId) {
if (!userId) return
for (const k in BARE_OS_DISCORD_LIVE) {
const r = BARE_OS_DISCORD_LIVE[k]
if (r && r.userId === userId) return
}
delete BARE_OS_DISCORD_EDIT_SESSIONS[userId]
delete BARE_OS_DISCORD_FM_SESSIONS[userId]
delete BARE_OS_DISCORD_SET_SESSIONS[userId]
}
function discordUnwrapMessage(sent) {
if (!sent || typeof sent !== 'object') return null
if (sent.resource && sent.resource.message) return sent.resource.message
if (sent.message && (sent.message.id || typeof sent.message.edit === 'function')) {
return sent.message
}
if (sent.id || typeof sent.delete === 'function' || typeof sent.edit === 'function') return sent
return null
}
async function discordResolvePostedMessage(interaction, sent) {
const unwrapped = discordUnwrapMessage(sent)
if (unwrapped) return unwrapped
if (interaction && interaction.message && (interaction.message.id || typeof interaction.message.delete === 'function')) {
return interaction.message
}
if (interaction && typeof interaction.fetchReply === 'function') {
try {
const m = await interaction.fetchReply()
return discordUnwrapMessage(m) || m
} catch {
return null
}
}
return null
}
function discordIdleArm(msg, payload, userId) {
if (!msg) return null
const hasUi =
(payload && payload.embeds && payload.embeds.length) ||
(payload && payload.components && payload.components.length)
if (!hasUi) return null
const id = String(msg.id || '') || 'anon:' + ++BARE_OS_DISCORD_IDLE_SEQ
const prev = BARE_OS_DISCORD_LIVE[id]
if (prev) discordIdleClearTimer(prev.timer)
const rec = {
id: id,
message: msg,
userId: userId || '',
embeds: payload && payload.embeds ? payload.embeds : null,
atMs: discordIdleNow(),
timer: null
}
BARE_OS_DISCORD_LIVE[id] = rec
rec.timer = discordIdleSchedule(function () {
Promise.resolve(discordIdleExpire(id)).catch(function () {})
}, BARE_OS_DISCORD_IDLE_MS)
return rec
}
async function discordIdleExpire(id) {
const rec = BARE_OS_DISCORD_LIVE[id]
if (!rec) return false
delete BARE_OS_DISCORD_LIVE[id]
discordIdleClearTimer(rec.timer)
rec.timer = null
const msg = rec.message
let cleaned = false
if (msg && typeof msg.delete === 'function') {
try {
await msg.delete()
cleaned = true
} catch {
cleaned = false
}
}
if (!cleaned && msg && typeof msg.edit === 'function') {
try {
const body = { components: [] }
if (rec.embeds && rec.embeds.length) body.embeds = discordIdleExpiredEmbeds(rec.embeds)
await msg.edit(body)
cleaned = true
} catch {
cleaned = false
}
}
discordIdleForgetSessions(rec.userId)
return cleaned
}
function discordIdleSweep() {
const now = discordIdleNow()
const due = []
for (const k in BARE_OS_DISCORD_LIVE) {
const r = BARE_OS_DISCORD_LIVE[k]
if (!r || now - r.atMs >= BARE_OS_DISCORD_IDLE_MS) due.push(k)
}
for (let i = 0; i < due.length; i++) {
Promise.resolve(discordIdleExpire(due[i])).catch(function () {})
}
}
function discordIdleTouch(interaction) {
const mid = interaction && interaction.message && interaction.message.id
if (!mid) return
const rec = BARE_OS_DISCORD_LIVE[String(mid)]
if (!rec) return
if (discordIdleNow() - rec.atMs >= BARE_OS_DISCORD_IDLE_MS) return
rec.atMs = discordIdleNow()
}
async function discordIdleWatch(interaction, payload, sent) {
if (!payload) return
if (
payload.flags === BARE_OS_DISCORD_FLAG_EPHEMERAL &&
!(payload.embeds && payload.embeds.length) &&
!(payload.components && payload.components.length)
) {
return
}
const msg = await discordResolvePostedMessage(interaction, sent)
discordIdleArm(msg, payload, discordInteractionUserId(interaction))
}
async function discordSendResult(ctx, interaction, result) {
if (result && result.editPath) {
const prev = discordEditGet(interaction)
discordEditPut(interaction, {
path: result.editPath,
created: !!(result.created || (prev && prev.created))
})
}
if (result && result.edit) {
await discordBeginEdit(ctx, interaction, result.edit)
return
}
if (result && result.create) {
await discordBeginCreate(ctx, interaction, result.create)
return
}
if (result && result.createPick) {
return discordSendResult(ctx, interaction, discordCreatePicker(ctx))
}
if (result && result.settings) {
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
}
if (result && result.files !== undefined) {
const rec = discordFmGet(interaction, ctx)
const want = String(result.files || '').trim()
if (want) {
const p = discordCmdPathOk(want)
if (p) {
rec.cwd = p
rec.page = 0
rec.sel = ''
rec.confirm = ''
}
}
return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction))
}
if (result && result.modal === 'create:path') {
await discordShowModal(interaction, {
id: 'create:path',
title: 'New file path',
label: 'Path under ~ or /tmp',
placeholder: '~/notes.txt',
max: 200
})
return
}
if (result && result.modal === 'edit:path') {
await discordShowModal(interaction, {
id: 'edit:path',
title: 'Open a file to edit',
label: 'Path under ~ or /tmp',
placeholder: '~/notes.txt',
max: 200
})
return
}
if (result && result.modal === 'say') {
await discordShowModal(interaction, {
id: 'say:modal',
title: 'Say as Bare OS',
label: 'Message',
paragraph: true,
placeholder: 'Hello from Bare OS'
})
return
}
if (result && result.modal === 'run') {
await discordShowModal(interaction, {
id: 'run:modal',
title: 'Run allowlisted command',
label: 'Command',
placeholder: 'uname -a'
})
return
}
const payload = discordCmdReplyPayload(result)
const isComp =
(typeof interaction.isButton === 'function' && interaction.isButton()) ||
(typeof interaction.isStringSelectMenu === 'function' &&
interaction.isStringSelectMenu())
let sent = null
try {
if (isComp && typeof interaction.update === 'function' && !interaction.replied && !interaction.deferred) {
sent = await interaction.update(payload)
} 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))
} else {
sent = await interaction.reply(Object.assign({ fetchReply: true }, payload))
}
} catch (err) {
if (ctx && ctx.console && typeof ctx.console.error === 'function') {
ctx.console.error(
'discord-bot: reply failed: ' + ((err && err.message) || err)
)
}
return
}
try {
await discordIdleWatch(interaction, payload, sent)
} catch {
/* tracking must not break the bot */
}
}
async function discordReplyWhitelistDenied(ctx, interaction) {
await discordSendResult(ctx, interaction, {
text: BARE_OS_DISCORD_WHITELIST_DENY,
ephemeral: true
})
}
async function discordDispatchAutocomplete(ctx, interaction) {
let focused = { name: '', value: '' }
try {
if (interaction.options && typeof interaction.options.getFocused === 'function') {
const f = interaction.options.getFocused(true)
if (f && typeof f === 'object') focused = f
else focused = { name: 'cmd', value: String(f || '') }
}
} catch {
focused = { name: '', value: '' }
}
const q = focused.value
const fname = String(focused.name || '')
let choices = []
try {
if (fname === 'unit') choices = await discordSuggestUnits(ctx, q)
else if (fname === 'page') choices = await discordSuggestMan(ctx, q)
else if (fname === 'cmd') choices = discordSuggestRun(q)
else if (fname === 'path') {
const cmd = String(interaction.commandName || '')
choices =
cmd === 'edit' || cmd === 'create'
? discordSuggestEditPaths(ctx, q)
: discordSuggestPaths(ctx, q)
}
} catch {
choices = []
}
try {
if (typeof interaction.respond === 'function') await interaction.respond(choices.slice(0, 25))
} catch {
/* ignore */
}
return true
}
async function discordFmDelete(ctx, path) {
if (typeof ctx.vfs.rm === 'function') {
await ctx.vfs.rm(path, { recursive: true, force: true })
return
}
const st = await discordFmStat(ctx, path)
if (discordIsDirStat(st) && typeof ctx.vfs.rmdir === 'function') {
await ctx.vfs.rmdir(path)
return
}
if (typeof ctx.vfs.unlink === 'function') await ctx.vfs.unlink(path)
else throw new Error('unlink unavailable')
}
async function discordFmCopyFile(ctx, from, to) {
if (typeof ctx.vfs.readFile !== 'function' || typeof ctx.vfs.writeFile !== 'function') {
throw new Error('copy requires readFile/writeFile')
}
const buf = await ctx.vfs.readFile(from)
await ctx.vfs.writeFile(to, buf)
}
async function discordFmAction(ctx, interaction, id) {
const rec = discordFmGet(interaction, ctx)
if (id === 'fm:goto') {
await discordShowModal(interaction, {
id: 'fm:goto',
title: 'Go to path',
label: 'Directory',
placeholder: rec.cwd || '~',
max: 200
})
return
}
if (id === 'fm:mkdir') {
await discordShowModal(interaction, {
id: 'fm:mkdir',
title: 'New folder in ' + discordBaseName(rec.cwd || '~'),
label: 'Folder name',
placeholder: 'docs',
max: 80
})
return
}
if (id === 'fm:ren') {
if (!rec.sel) {
return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction))
}
await discordShowModal(interaction, {
id: 'fm:ren',
title: 'Rename ' + rec.sel,
label: 'New name',
placeholder: rec.sel,
max: 80
})
return
}
if (id === 'fm:copy') {
if (!rec.sel) {
return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction))
}
await discordShowModal(interaction, {
id: 'fm:copy',
title: 'Copy ' + rec.sel,
label: 'Destination path',
placeholder: discordJoinPath(rec.cwd, rec.sel + '.copy'),
max: 200
})
return
}
if (id === 'fm:new') {
const name = 'new-' + String(Date.now()).slice(-6) + '.txt'
await discordBeginCreate(ctx, interaction, discordJoinPath(rec.cwd, name))
return
}
if (id === 'fm:edit') {
if (!rec.sel) {
return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction))
}
await discordBeginEdit(ctx, interaction, discordJoinPath(rec.cwd, rec.sel))
return
}
if (id === 'fm:up') {
rec.cwd = discordParentPath(rec.cwd)
rec.sel = ''
rec.page = 0
rec.confirm = ''
} else if (id === 'fm:home') {
rec.cwd = '~'
rec.sel = ''
rec.page = 0
rec.confirm = ''
} else if (id === 'fm:prev') {
rec.page--
rec.confirm = ''
} else if (id === 'fm:next') {
rec.page++
rec.confirm = ''
} else if (id === 'fm:ref' || id === 'fm:more' || id === 'fm:no') {
rec.confirm = ''
} else if (id === 'fm:pick') {
const v = String((interaction.values && interaction.values[0]) || '')
rec.confirm = ''
if (v === '..') {
rec.cwd = discordParentPath(rec.cwd)
rec.sel = ''
rec.page = 0
} else if (v) {
const full = discordJoinPath(rec.cwd, v)
const st = await discordFmStat(ctx, full)
if (discordIsDirStat(st)) {
rec.cwd = full
rec.sel = ''
rec.page = 0
} else {
rec.sel = v
return discordSendResult(ctx, interaction, await discordCmdHandleFs(ctx, 'cat', full))
}
}
} else if (id === 'fm:open') {
if (!rec.sel) {
/* stay */
} else {
const full = discordJoinPath(rec.cwd, rec.sel)
const st = await discordFmStat(ctx, full)
if (discordIsDirStat(st)) {
rec.cwd = full
rec.sel = ''
rec.page = 0
} else {
return discordSendResult(ctx, interaction, await discordCmdHandleFs(ctx, 'cat', full))
}
}
} else if (id === 'fm:del') {
rec.confirm = rec.sel || ''
} else if (id === 'fm:ok') {
const name = rec.confirm || rec.sel
rec.confirm = ''
if (name) {
const full = discordJoinPath(rec.cwd, name)
if (!discordCmdPathWriteOk(ctx, full)) {
return discordSendResult(ctx, interaction, {
text: 'Delete not allowed: `' + full + '`',
ephemeral: true
})
}
try {
await discordFmDelete(ctx, full)
rec.sel = ''
} catch (err) {
return discordSendResult(ctx, interaction, {
text: 'Delete failed: ' + ((err && err.message) || err),
ephemeral: true
})
}
}
}
return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction))
}
async function discordDispatchComponent(ctx, interaction) {
const id = String(interaction.customId || '')
const opt = function () {
return ''
}
if (id.indexOf('set:') === 0) {
await discordSettingsAction(ctx, interaction, id)
return true
}
if (id.indexOf('fm:') === 0) {
await discordFmAction(ctx, interaction, id)
return true
}
if (id === 'nav:panel') return discordSendResult(ctx, interaction, discordPanel(ctx))
if (id === 'say:compose') {
await discordShowModal(interaction, {
id: 'say:modal',
title: 'Say as Bare OS',
label: 'Message',
paragraph: true,
placeholder: 'Hello from Bare OS'
})
return true
}
if (id === 'run:compose') {
await discordShowModal(interaction, {
id: 'run:modal',
title: 'Run allowlisted command',
label: 'Command',
placeholder: 'uname -a'
})
return true
}
if (id === 'edit:new') {
await discordShowModal(interaction, {
id: 'edit:path',
title: 'Open a file to edit',
label: 'Path under ~ or /tmp',
placeholder: '~/notes.txt',
max: 200
})
return true
}
if (id === 'create:new' || id === 'create:custom') {
await discordShowModal(interaction, {
id: 'create:path',
title: 'New file path',
label: 'Path under ~ or /tmp',
placeholder: '~/notes.txt',
max: 200
})
return true
}
if (id === 'create:pick') {
const values = interaction.values || []
const path = values[0] ? String(values[0]) : ''
if (!path) {
return discordSendResult(ctx, interaction, discordCreatePicker(ctx))
}
await discordBeginCreate(ctx, interaction, path)
return true
}
if (id === 'create:open') {
const rec = discordEditGet(interaction)
const path = rec && rec.path
if (!path) {
await discordShowModal(interaction, {
id: 'create:path',
title: 'New file path',
label: 'Path under ~ or /tmp',
placeholder: '~/notes.txt',
max: 200
})
return true
}
await discordBeginCreate(ctx, interaction, path)
return true
}
if (id === 'edit:open') {
const rec = discordEditGet(interaction)
const path = rec && rec.path
if (!path) {
await discordShowModal(interaction, {
id: 'edit:path',
title: 'Open a file to edit',
label: 'Path under ~ or /tmp',
placeholder: '~/notes.txt',
max: 200
})
return true
}
await discordBeginEdit(ctx, interaction, path)
return true
}
if (id.indexOf('bare:') === 0) {
return discordSendResult(ctx, interaction, await discordCmdHandleBare(ctx, id.slice(5)))
}
if (id.indexOf('sys:') === 0) {
return discordSendResult(ctx, interaction, await discordCmdHandleSys(ctx, id.slice(4)))
}
if (id.indexOf('net:') === 0) {
return discordSendResult(ctx, interaction, await discordCmdHandleNet(ctx, id.slice(4)))
}
if (id === 'svc:list') {
return discordSendResult(ctx, interaction, await discordCmdHandleSvc(ctx, 'list', ''))
}
if (id === 'svc:pick') {
const values = interaction.values || []
const unit = values[0] ? String(values[0]) : ''
return discordSendResult(ctx, interaction, await discordCmdHandleSvc(ctx, 'status', unit))
}
const svcAct = /^svc:(status|start|stop|restart|logs):(.+)$/.exec(id)
if (svcAct) {
return discordSendResult(
ctx,
interaction,
await discordCmdHandleSvc(ctx, svcAct[1], svcAct[2])
)
}
return discordSendResult(ctx, interaction, await discordRouteCommand(ctx, 'panel', '', opt))
}
async function discordDispatchModal(ctx, interaction) {
const id = String(interaction.customId || '')
let value = ''
try {
if (interaction.fields && typeof interaction.fields.getTextInputValue === 'function') {
value = String(interaction.fields.getTextInputValue('text') || '')
}
} catch {
value = ''
}
if (id === 'fm:goto') {
const rec = discordFmGet(interaction, ctx)
const p = discordCmdPathOk(value)
if (p) {
rec.cwd = p
rec.page = 0
rec.sel = ''
rec.confirm = ''
}
return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction))
}
if (id === 'fm:mkdir') {
const rec = discordFmGet(interaction, ctx)
const name = String(value || '').replace(/[\/\0]/g, '')
const dest = discordJoinPath(rec.cwd, name)
if (!name || !discordCmdPathWriteOk(ctx, dest)) {
return discordSendResult(ctx, interaction, {
text: 'Cannot create folder `' + (name || '?') + '` here.',
ephemeral: true
})
}
try {
if (typeof ctx.vfs.mkdir !== 'function') throw new Error('mkdir unavailable')
await ctx.vfs.mkdir(dest, { recursive: true })
rec.sel = name
} catch (err) {
return discordSendResult(ctx, interaction, {
text: 'mkdir failed: ' + ((err && err.message) || err),
ephemeral: true
})
}
return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction))
}
if (id === 'fm:ren') {
const rec = discordFmGet(interaction, ctx)
const name = String(value || '').replace(/[\/\0]/g, '')
const from = rec.sel ? discordJoinPath(rec.cwd, rec.sel) : ''
const to = discordJoinPath(rec.cwd, name)
if (!from || !name || !discordCmdPathWriteOk(ctx, from) || !discordCmdPathWriteOk(ctx, to)) {
return discordSendResult(ctx, interaction, {
text: 'Rename not allowed.',
ephemeral: true
})
}
try {
const st = await discordFmStat(ctx, from)
if (discordIsDirStat(st)) throw new Error('renaming folders is not supported')
if (await discordFileExists(ctx, to)) throw new Error('destination exists')
await discordFmCopyFile(ctx, from, to)
await discordFmDelete(ctx, from)
rec.sel = name
} catch (err) {
return discordSendResult(ctx, interaction, {
text: 'Rename failed: ' + ((err && err.message) || err),
ephemeral: true
})
}
return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction))
}
if (id === 'fm:copy') {
const rec = discordFmGet(interaction, ctx)
const to = discordCmdPathWriteOk(ctx, value)
const from = rec.sel ? discordJoinPath(rec.cwd, rec.sel) : ''
if (!from || !to || !discordCmdPathWriteOk(ctx, from)) {
return discordSendResult(ctx, interaction, {
text: 'Copy not allowed.',
ephemeral: true
})
}
try {
const st = await discordFmStat(ctx, from)
if (discordIsDirStat(st)) throw new Error('copying folders is not supported')
if (await discordFileExists(ctx, to)) throw new Error('destination exists')
await discordFmCopyFile(ctx, from, to)
} catch (err) {
return discordSendResult(ctx, interaction, {
text: 'Copy failed: ' + ((err && err.message) || err),
ephemeral: true
})
}
return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction))
}
if (id === 'set:str') {
const rec = discordSettingsGet(interaction)
const spec = discordSettingsFind(rec.pending || rec.sel)
rec.pending = ''
if (!spec || (spec.kind !== 'string' && spec.kind !== 'number')) {
return discordSendResult(ctx, interaction, { text: 'No pending setting.', ephemeral: true })
}
try {
await discordSettingsApply(ctx, spec, value)
} catch (err) {
return discordSendResult(ctx, interaction, {
text: 'Could not apply `' + spec.id + '`: ' + ((err && err.message) || err),
ephemeral: true
})
}
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
}
if (id === 'set:alias') {
const rec = discordSettingsGet(interaction)
rec.pending = ''
rec.group = 'aliases'
try {
await discordSettingsSetAlias(ctx, value)
rec.sel = discordSettingsAliasName(value)
} catch (err) {
return discordSendResult(ctx, interaction, {
text: 'Alias failed: ' + ((err && err.message) || err),
ephemeral: true
})
}
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
}
if (id === 'edit:save' || id === 'create:save') {
if (id === 'create:save') {
const rec = discordEditGet(interaction)
if (rec) rec.created = true
}
return discordSendResult(ctx, interaction, await discordEditSaveFromModal(ctx, interaction))
}
if (id === 'create:path') {
const p = discordCmdPathWriteOk(ctx, value)
if (!p) {
return discordSendResult(ctx, interaction, {
text:
'Not writable: `' +
(value || '(empty)') +
'`. Use `~/file`, `/tmp/file`, or `/home/' +
discordSessionUser(ctx) +
'/file`.',
ephemeral: true
})
}
if (await discordFileExists(ctx, p)) {
discordEditPut(interaction, { path: p })
return discordSendResult(
ctx,
interaction,
discordResult(
discordEmbed({
title: 'Already exists',
color: BARE_OS_DISCORD_COLOR_WARN,
desc: '`' + p + '` is already on disk. Edit it, or choose another name.'
}),
{
components: [
discordButtons([
{ id: 'edit:open', label: 'Edit instead', style: 1 },
{ id: 'create:custom', label: 'Different path…', style: 3 }
])
],
editPath: p
}
)
)
}
discordEditPut(interaction, { path: p, created: true })
return discordSendResult(
ctx,
interaction,
discordResult(
discordEmbed({
title: 'Ready to create',
desc:
'`' +
p +
'`\nDiscord cannot open a second modal from this form. Click **Compose contents**.',
color: BARE_OS_DISCORD_COLOR
}),
{
components: [
discordButtons([{ id: 'create:open', label: 'Compose contents', style: 3 }])
],
editPath: p,
created: true
}
)
)
}
if (id === 'edit:path') {
const p = discordCmdPathWriteOk(ctx, value)
if (!p) {
return discordSendResult(ctx, interaction, {
text:
'Not writable: `' +
(value || '(empty)') +
'`. Use `~/file`, `/tmp/file`, or `/home/' +
discordSessionUser(ctx) +
'/file`.',
ephemeral: true
})
}
discordEditPut(interaction, { path: p })
const exists = Boolean(await discordCmdReadText(ctx, p))
return discordSendResult(
ctx,
interaction,
discordResult(
discordEmbed({
title: exists ? 'Ready to edit' : 'New file',
desc:
'`' +
p +
'`\nDiscord cannot open a second modal from this form. Click **Open editor**.',
color: BARE_OS_DISCORD_COLOR
}),
{
components: [
discordButtons([{ id: 'edit:open', label: 'Open editor', style: 1 }])
],
editPath: p
}
)
)
}
if (id === 'say:modal') {
return discordSendResult(
ctx,
interaction,
discordResult(
discordEmbed({
title: 'say',
desc: discordCmdFence(discordCmdSayBox(value))
})
)
)
}
if (id === 'run:modal') {
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, value))
}
return discordSendResult(ctx, interaction, discordPanel(ctx))
}
async function discordDispatchInteraction(ctx, interaction) {
if (!interaction) return false
discordSyncSessionIdentity(ctx)
discordIdleTouch(interaction)
discordIdleSweep()
if (typeof interaction.isAutocomplete === 'function' && interaction.isAutocomplete()) {
if (!discordUserAllowed(ctx, discordInteractionUserId(interaction))) {
try {
if (typeof interaction.respond === 'function') await interaction.respond([])
} catch {
/* ignore */
}
return true
}
return discordDispatchAutocomplete(ctx, interaction)
}
if (!discordUserAllowed(ctx, discordInteractionUserId(interaction))) {
if (ctx && ctx.console && typeof ctx.console.log === 'function') {
ctx.console.log(
'discord-bot: denied user ' + (discordInteractionUserId(interaction) || '?')
)
}
await discordReplyWhitelistDenied(ctx, interaction)
return true
}
if (typeof interaction.isButton === 'function' && interaction.isButton()) {
await discordDispatchComponent(ctx, interaction)
return true
}
if (typeof interaction.isStringSelectMenu === 'function' && interaction.isStringSelectMenu()) {
await discordDispatchComponent(ctx, interaction)
return true
}
if (typeof interaction.isModalSubmit === 'function' && interaction.isModalSubmit()) {
await discordDispatchModal(ctx, interaction)
return true
}
if (typeof interaction.isChatInputCommand !== 'function' || !interaction.isChatInputCommand()) {
return false
}
const name = String(interaction.commandName || '')
let sub = ''
let opt = function () {
return ''
}
if (interaction.options) {
if (typeof interaction.options.getSubcommand === 'function') {
try {
sub = String(interaction.options.getSubcommand(false) || '')
} catch {
sub = ''
}
}
opt = function (key) {
if (typeof interaction.options.getString === 'function') {
const v = interaction.options.getString(key)
return v == null ? '' : String(v)
}
return ''
}
}
let result
try {
result = await discordRouteCommand(ctx, name, sub, opt)
} catch (err) {
result = {
text: 'error: ' + ((err && err.message) || String(err)),
ephemeral: true
}
}
await discordSendResult(ctx, interaction, result)
return true
}
var bareOsDiscordCommands = {
buildSlashCommands: discordBuildSlashCommands,
dispatchInteraction: discordDispatchInteraction,
parseIdWhitelist: discordParseIdWhitelist,
userAllowed: discordUserAllowed,
installProcessEmitWarning: discordInstallProcessEmitWarning,
replyPayload: discordCmdReplyPayload,
syncSessionIdentity: discordSyncSessionIdentity,
sessionUser: discordSessionUser,
FLAG_EPHEMERAL: BARE_OS_DISCORD_FLAG_EPHEMERAL,
prettySnapshot: discordPrettySnapshot,
prettyBytes: discordPrettyBytes,
parseMeminfo: discordParseMeminfo,
parseSystemctlList: discordParseSystemctlList,
formatRlimits: discordFormatRlimits,
formatFeatures: discordFormatFeatures,
formatDoctor: discordFormatDoctor,
formatSwarm: discordFormatSwarm,
formatNetSummary: discordFormatNetSummary,
formatHostDf: discordFormatHostDf,
pathWriteOk: discordCmdPathWriteOk,
editChunks: discordEditChunks,
editPut: discordEditPut,
editGet: discordEditGet,
editModalPayload: discordEditModalPayload,
joinPath: discordJoinPath,
parentPath: discordParentPath,
settingsSpecs: BARE_OS_DISCORD_SETTINGS,
settingsApply: discordSettingsApply,
settingsCurrent: discordSettingsCurrent,
uniqueComponents: discordUniqueComponents,
navRows: discordNavRows,
result: discordResult,
idleMs: BARE_OS_DISCORD_IDLE_MS,
idleExpire: discordIdleExpire,
idleSweep: discordIdleSweep,
idleArm: discordIdleArm,
idleLive: function () {
return BARE_OS_DISCORD_LIVE
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = bareOsDiscordCommands
}