This commit is contained in:
Raven Scott
2026-08-13 14:36:15 -04:00
parent 993514f796
commit 879ad78bed
17 changed files with 2601 additions and 327 deletions
@@ -1775,7 +1775,8 @@ async function discordCmdHandleBare(ctx, sub) {
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', 'Full shell (cwd + autocomplete), logs, speak, man pages')
discordField('/run /journal /say /man', 'Full shell (cwd + autocomplete), logs, speak, man pages'),
discordField('/plugins', 'List, reload, enable/disable ~/.discord/plugins')
]
})
)
@@ -2860,113 +2861,682 @@ function discordCmdAddSubs(builder, items) {
return true
}
function discordBuildSlashCommands(dj) {
var BARE_OS_DISCORD_PLUGIN_DIR = '~/.discord/plugins'
var BARE_OS_DISCORD_PLUGIN_MAX = 40
var BARE_OS_DISCORD_PLUGIN_SRC_MAX = 64 * 1024
var BARE_OS_DISCORD_PLUGIN_RESERVED = {
bare: 1,
sys: 1,
svc: 1,
fs: 1,
net: 1,
man: 1,
say: 1,
run: 1,
journal: 1,
edit: 1,
create: 1,
files: 1,
browse: 1,
settings: 1,
panel: 1,
ping: 1,
plugins: 1
}
var BARE_OS_DISCORD_PLUGINS = []
var BARE_OS_DISCORD_PLUGINS_READY = false
var BARE_OS_DISCORD_PLUGIN_REREGISTER = null
function discordPluginNameOk(name) {
return /^[a-z0-9][a-z0-9-]{0,31}$/.test(String(name || ''))
}
function discordPluginsSetRegistrar(fn) {
BARE_OS_DISCORD_PLUGIN_REREGISTER = typeof fn === 'function' ? fn : null
}
function discordPluginFind(name) {
const n = String(name || '')
for (let i = 0; i < BARE_OS_DISCORD_PLUGINS.length; i++) {
if (BARE_OS_DISCORD_PLUGINS[i].name === n && BARE_OS_DISCORD_PLUGINS[i].ok) {
return BARE_OS_DISCORD_PLUGINS[i]
}
}
return null
}
function discordPluginExpand(tpl, opt, env) {
return String(tpl == null ? '' : tpl).replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, function (_, a, def, b) {
const key = a || b
if (opt) {
const v = opt(key)
if (v != null && v !== '') return String(v)
}
if (env && env[key] != null && env[key] !== '') return String(env[key])
return def != null ? String(def) : ''
})
}
function discordPluginSdk(ctx, plug) {
const prefix = 'plug:' + plug.name + ':'
const bot = {
name: plug.name,
version: plug.manifest && plug.manifest.version,
command: function (spec) {
if (spec && typeof spec === 'object') plug.handlers.push(spec)
return bot
},
embed: function (opts) {
return discordEmbed(opts)
},
field: discordField,
result: function (embed, extra) {
return discordResult(embed, extra)
},
fence: discordCmdFence,
redact: discordCmdRedact,
more: function (opts) {
return { more: opts }
},
id: function (rest) {
return prefix + String(rest || '').replace(/^plug:[^:]+:/, '')
},
user: function () {
return discordSessionUser(ctx)
},
home: function () {
return discordCmdEnv(ctx).HOME || '/home/' + discordSessionUser(ctx)
},
env: function () {
const e = discordCmdEnv(ctx)
const out = {}
const skip = /TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|PRIVATE|CREDENTIAL/i
const keys = Object.keys(e)
for (let i = 0; i < keys.length; i++) {
if (skip.test(keys[i])) continue
out[keys[i]] = e[keys[i]]
}
return out
},
read: function (path) {
return discordCmdReadText(ctx, path)
},
write: function (path, text) {
const p = discordCmdPathWriteOk(ctx, path)
if (!p) return Promise.reject(new Error('not writable: ' + path))
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') {
return Promise.reject(new Error('writeFile unavailable'))
}
return ctx.vfs.writeFile(p, discordTextToBuf(ctx, String(text == null ? '' : text)))
},
ls: async function (path) {
if (!ctx.vfs || typeof ctx.vfs.readdir !== 'function') return []
const names = await ctx.vfs.readdir(path || '~')
return Array.isArray(names) ? names : []
},
sh: function (line) {
if (typeof ctx.execLine !== 'function') {
return Promise.reject(new Error('execLine unavailable'))
}
return discordCmdCapture(ctx, function () {
return ctx.execLine(String(line || ''), { timeoutMs: BARE_OS_DISCORD_SH_EXEC_MS })
})
}
}
return bot
}
function discordPluginNormalize(raw, file) {
const m = raw && typeof raw === 'object' ? raw : {}
const name = String(m.name || '').toLowerCase()
return {
name: name,
description: String(m.description || name || 'plugin'),
version: String(m.version || '1.0.0'),
enabled: m.enabled !== false,
options: Array.isArray(m.options) ? m.options : [],
subcommands: Array.isArray(m.subcommands) ? m.subcommands : [],
run: typeof m.run === 'string' ? m.run : m.shell || '',
title: m.title || '',
readFile: m.file || '',
embed: m.embed && typeof m.embed === 'object' ? m.embed : null,
ephemeral: !!m.ephemeral,
source: file || '',
handlers: [],
onComponent: typeof m.onComponent === 'function' ? m.onComponent : null,
onModal: typeof m.onModal === 'function' ? m.onModal : null,
onAutocomplete: typeof m.onAutocomplete === 'function' ? m.onAutocomplete : null,
runFn: typeof m.run === 'function' ? m.run : null,
manifest: m,
ok: false,
error: '',
kind: 'json'
}
}
async function discordPluginEvalJs(ctx, plug, source) {
const bot = discordPluginSdk(ctx, plug)
let AsyncFn
try {
AsyncFn = Object.getPrototypeOf(async function () {}).constructor
} catch {
AsyncFn = Function
}
const body =
String(source) +
'\n;' +
'if (typeof register === "function") await register(bot);\n' +
'else if (typeof plugin === "object" && plugin) bot.command(plugin);\n'
const fn = new AsyncFn('bot', 'sdk', 'register', body)
function register(spec) {
bot.command(spec)
}
await fn(bot, bot, register)
if (plug.handlers.length) {
const first = plug.handlers[0]
if (!plug.name && first.name) plug.name = String(first.name).toLowerCase()
if (first.description && plug.description === plug.name) plug.description = first.description
if (first.options) plug.options = first.options
if (first.subcommands) plug.subcommands = first.subcommands
if (typeof first.run === 'function') plug.runFn = first.run
if (typeof first.onComponent === 'function') plug.onComponent = first.onComponent
if (typeof first.onModal === 'function') plug.onModal = first.onModal
if (typeof first.onAutocomplete === 'function') plug.onAutocomplete = first.onAutocomplete
}
plug.kind = 'js'
}
async function discordPluginsDisabledSet(ctx) {
const text = await discordCmdReadText(ctx, BARE_OS_DISCORD_PLUGIN_DIR + '/disabled.txt')
const set = Object.create(null)
const lines = String(text || '').split(/\r?\n/)
for (let i = 0; i < lines.length; i++) {
const n = lines[i].trim()
if (!n || n.charAt(0) === '#') continue
set[n] = 1
}
return set
}
async function discordPluginsWriteDisabled(ctx, set) {
const names = Object.keys(set).sort()
const body = names.length ? names.join('\n') + '\n' : ''
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') throw new Error('writeFile unavailable')
await discordEnsureDir(ctx, BARE_OS_DISCORD_PLUGIN_DIR)
await ctx.vfs.writeFile(BARE_OS_DISCORD_PLUGIN_DIR + '/disabled.txt', discordTextToBuf(ctx, body))
}
async function discordPluginsLoad(ctx) {
BARE_OS_DISCORD_PLUGINS = []
BARE_OS_DISCORD_PLUGINS_READY = true
if (!ctx || !ctx.vfs) return BARE_OS_DISCORD_PLUGINS
await discordEnsureDir(ctx, BARE_OS_DISCORD_PLUGIN_DIR)
let names = []
try {
if (typeof ctx.vfs.readdir === 'function') {
names = await ctx.vfs.readdir(BARE_OS_DISCORD_PLUGIN_DIR)
}
} catch {
names = []
}
if (!Array.isArray(names)) names = []
const disabled = await discordPluginsDisabledSet(ctx)
const seen = Object.create(null)
for (let i = 0; i < names.length && BARE_OS_DISCORD_PLUGINS.length < BARE_OS_DISCORD_PLUGIN_MAX; i++) {
const entry = String(names[i] || '')
if (!entry || entry.charAt(0) === '.' || entry === 'disabled.txt' || entry === 'README.md') continue
if (/\.disabled$/.test(entry)) continue
let id = entry.replace(/\.(json|js)$/i, '')
let kind = ''
let srcPath = ''
let jsonPath = ''
if (/\.json$/i.test(entry)) {
kind = 'json'
jsonPath = BARE_OS_DISCORD_PLUGIN_DIR + '/' + entry
} else if (/\.js$/i.test(entry)) {
kind = 'js'
srcPath = BARE_OS_DISCORD_PLUGIN_DIR + '/' + entry
} else {
jsonPath = BARE_OS_DISCORD_PLUGIN_DIR + '/' + entry + '/plugin.json'
srcPath = BARE_OS_DISCORD_PLUGIN_DIR + '/' + entry + '/index.js'
const hasJson = await discordFileExists(ctx, jsonPath)
const hasJs = await discordFileExists(ctx, srcPath)
if (hasJson) kind = 'dir-json'
else if (hasJs) kind = 'dir-js'
else continue
if (!hasJson) jsonPath = ''
if (!hasJs) srcPath = ''
}
if (seen[id]) continue
seen[id] = 1
const plug = discordPluginNormalize({ name: id }, entry)
plug.source = BARE_OS_DISCORD_PLUGIN_DIR + '/' + entry
if (disabled[id]) {
plug.enabled = false
plug.error = 'disabled'
BARE_OS_DISCORD_PLUGINS.push(plug)
continue
}
try {
if (jsonPath) {
const raw = await discordCmdReadText(ctx, jsonPath)
if (raw.length > BARE_OS_DISCORD_PLUGIN_SRC_MAX) throw new Error('manifest too large')
const j = discordTryJson(raw)
if (!j) throw new Error('invalid JSON')
const n = discordPluginNormalize(j, entry)
plug.name = n.name || id
plug.description = n.description
plug.version = n.version
plug.enabled = n.enabled
plug.options = n.options
plug.subcommands = n.subcommands
plug.run = n.run
plug.title = n.title
plug.readFile = n.readFile
plug.embed = n.embed
plug.ephemeral = n.ephemeral
plug.manifest = j
}
if (srcPath) {
const src = await discordCmdReadText(ctx, srcPath)
if (src.length > BARE_OS_DISCORD_PLUGIN_SRC_MAX) throw new Error('script too large')
await discordPluginEvalJs(ctx, plug, src)
}
if (!discordPluginNameOk(plug.name)) throw new Error('bad command name (use a-z0-9-, 132)')
if (BARE_OS_DISCORD_PLUGIN_RESERVED[plug.name]) throw new Error('name is reserved: /' + plug.name)
if (plug.enabled === false) {
plug.error = 'disabled in manifest'
BARE_OS_DISCORD_PLUGINS.push(plug)
continue
}
plug.ok = true
BARE_OS_DISCORD_PLUGINS.push(plug)
} catch (err) {
plug.ok = false
plug.error = String((err && err.message) || err)
BARE_OS_DISCORD_PLUGINS.push(plug)
}
}
return BARE_OS_DISCORD_PLUGINS
}
async function discordPluginsEnsure(ctx) {
if (!BARE_OS_DISCORD_PLUGINS_READY) await discordPluginsLoad(ctx)
return BARE_OS_DISCORD_PLUGINS
}
function discordPluginAddOptions(builder, opts) {
if (!opts || !opts.length) return
for (let i = 0; i < opts.length && i < 10; i++) {
const o = opts[i]
if (!o || !o.name) continue
discordCmdOpt(
builder,
String(o.name).slice(0, 32),
String(o.description || o.name).slice(0, 100),
!!o.required,
!!o.autocomplete
)
}
}
function discordPluginsSlash(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('Non-interactive Bare OS shell (cwd, pipes, cd, history)')
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', 'Shell line (cd, pipes, &&, redirects, $VAR)', 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()
const out = []
for (let i = 0; i < BARE_OS_DISCORD_PLUGINS.length; i++) {
const p = BARE_OS_DISCORD_PLUGINS[i]
if (!p.ok) continue
try {
const c = new B().setName(p.name).setDescription(String(p.description || p.name).slice(0, 100))
if (p.subcommands && p.subcommands.length) {
const items = []
for (let s = 0; s < p.subcommands.length && s < 10; s++) {
const sc = p.subcommands[s]
if (!sc || !sc.name) continue
items.push([
String(sc.name).slice(0, 32),
String(sc.description || sc.name).slice(0, 100),
sc.options
? function (b) {
discordPluginAddOptions(b, sc.options)
}
: null
])
}
discordCmdAddSubs(c, items)
} else {
discordPluginAddOptions(c, p.options)
}
out.push(c.toJSON())
} catch {
/* skip bad slash shape */
}
}
return out
}
function discordPluginEvent(ctx, interaction, plug, sub, opt) {
const bot = discordPluginSdk(ctx, plug)
return {
name: plug.name,
sub: sub || '',
opt: opt || function () {
return ''
},
interaction: interaction,
userId: discordInteractionUserId(interaction),
user: discordSessionUser(ctx),
ctx: ctx,
bot: bot,
embed: bot.embed,
field: bot.field,
fence: bot.fence,
sh: bot.sh,
read: bot.read,
write: bot.write,
ls: bot.ls
}
}
async function discordPluginInvoke(ctx, interaction, plug, sub, opt) {
const event = discordPluginEvent(ctx, interaction, plug, sub, opt)
let spec = plug
if (sub && plug.subcommands) {
for (let i = 0; i < plug.subcommands.length; i++) {
if (plug.subcommands[i] && plug.subcommands[i].name === sub) {
spec = plug.subcommands[i]
break
}
}
}
const runFn = spec.runFn || plug.runFn || (typeof spec.run === 'function' ? spec.run : null)
if (typeof runFn === 'function') {
const out = await runFn(event)
if (out == null) return { text: '(no output)', ephemeral: true }
if (typeof out === 'string') return { text: discordCmdRedact(out), ephemeral: !!(spec.ephemeral || plug.ephemeral) }
if (out.embeds || out.text || out.more || out.modal || out.components) {
if ((spec.ephemeral || plug.ephemeral) && out.ephemeral == null) out.ephemeral = true
return out
}
if (out.title || out.desc || out.fields) {
return discordResult(discordEmbed(out), { ephemeral: !!(spec.ephemeral || plug.ephemeral) })
}
return { text: String(out), ephemeral: !!(spec.ephemeral || plug.ephemeral) }
}
if (spec.readFile || spec.file || (plug.embed && plug.embed.file)) {
const path = spec.readFile || spec.file || plug.embed.file
const body = await discordCmdReadText(ctx, path)
return {
more: {
title: spec.title || plug.title || plug.name,
body: body || '(empty)',
fence: true
}
}
}
const shell = typeof spec.run === 'string' && spec.run ? spec.run : plug.run
if (shell) {
const line = discordPluginExpand(shell, opt, discordCmdEnv(ctx))
const out = await discordCmdCapture(ctx, function () {
if (typeof ctx.execLine !== 'function') throw new Error('execLine unavailable')
return ctx.execLine(line, { timeoutMs: BARE_OS_DISCORD_SH_EXEC_MS })
})
return {
more: {
title: spec.title || plug.title || '/' + plug.name,
lead: '`' + line + '`',
body: out || '(no output)',
fence: true,
footer: 'plugin · ' + plug.name + ' · ' + discordSessionUser(ctx)
}
}
}
if (spec.embed || plug.embed) {
const em = spec.embed || plug.embed
return discordResult(
discordEmbed({
title: em.title || plug.title || plug.name,
desc: em.desc || em.description || '',
fields: em.fields
}),
{ ephemeral: !!(spec.ephemeral || plug.ephemeral) }
)
}
return { text: 'Plugin /' + plug.name + ' has nothing to run.', ephemeral: true }
}
async function discordPluginComponent(ctx, interaction, id) {
const parts = String(id || '').split(':')
const name = parts[1] || ''
const rest = parts.slice(2).join(':')
const plug = discordPluginFind(name)
if (!plug || typeof plug.onComponent !== 'function') {
return discordSendResult(ctx, interaction, {
text: 'Plugin button expired or unknown.',
ephemeral: true
})
}
const event = discordPluginEvent(ctx, interaction, plug, '', function () {
return ''
})
event.id = rest
event.values = interaction.values || []
const out = await plug.onComponent(event)
if (out == null) return discordSendResult(ctx, interaction, { text: 'ok', ephemeral: true })
return discordSendResult(ctx, interaction, typeof out === 'string' ? { text: out } : out)
}
async function discordPluginsAdmin(ctx, interaction, sub, opt) {
await discordPluginsEnsure(ctx)
const action = sub || 'list'
if (action === 'reload') {
await discordPluginsLoad(ctx)
let note = 'Reloaded **' + BARE_OS_DISCORD_PLUGINS.length + '** plugin file(s).'
if (typeof BARE_OS_DISCORD_PLUGIN_REREGISTER === 'function') {
try {
await BARE_OS_DISCORD_PLUGIN_REREGISTER()
note += ' Slash commands re-registered.'
} catch (err) {
note += ' Reload dispatch ok; slash re-register failed: ' + ((err && err.message) || err)
}
} else {
note += ' Restart `discord-bot` (or the initd unit) to publish new slash names to Discord.'
}
return discordPluginsAdmin(ctx, interaction, 'list', opt).then(function (res) {
if (res && res.embeds && res.embeds[0]) {
res.embeds[0].description = note + (res.embeds[0].description ? '\n' + res.embeds[0].description : '')
}
return res
})
}
if (action === 'disable' || action === 'enable') {
const want = String(opt('name') || '').toLowerCase()
if (!want) return { text: 'plugin name required', ephemeral: true }
const set = await discordPluginsDisabledSet(ctx)
if (action === 'disable') set[want] = 1
else delete set[want]
await discordPluginsWriteDisabled(ctx, set)
await discordPluginsLoad(ctx)
return {
text: (action === 'disable' ? 'Disabled' : 'Enabled') + ' `' + want + '`. Restart or `/plugins reload` to refresh slash names.',
ephemeral: true
}
}
if (action === 'info') {
const want = String(opt('name') || '').toLowerCase()
let plug = null
for (let i = 0; i < BARE_OS_DISCORD_PLUGINS.length; i++) {
if (BARE_OS_DISCORD_PLUGINS[i].name === want) plug = BARE_OS_DISCORD_PLUGINS[i]
}
if (!plug) return { text: 'No plugin named `' + want + '`.', ephemeral: true }
return discordResult(
discordEmbed({
title: '/' + plug.name,
desc: plug.description,
fields: [
discordField('Status', plug.ok ? 'loaded' : plug.error || 'error', true),
discordField('Kind', plug.kind, true),
discordField('Version', plug.version, true),
discordField('File', plug.source || plug.file || '—', false)
],
color: plug.ok ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN
})
)
}
const fields = []
for (let i = 0; i < BARE_OS_DISCORD_PLUGINS.length && fields.length < 20; i++) {
const p = BARE_OS_DISCORD_PLUGINS[i]
fields.push(
discordField(
'/' + p.name,
(p.ok ? '**on**' : '**off**') + ' · ' + (p.error || p.description || p.kind)
)
)
}
return discordResult(
discordEmbed({
title: 'Plugins',
desc:
'Drop JSON or JS in `~/.discord/plugins`. See **developer-guide ch.21** for the SDK.\n' +
BARE_OS_DISCORD_PLUGINS.length +
' discovered.',
fields: fields.length ? fields : [discordField('plugins', 'None yet. Add `~/.discord/plugins/hello.json`.')]
})
)
}
function discordBuildSlashCommands(dj, ctx) {
const B = dj && dj.SlashCommandBuilder
if (typeof B !== 'function') return []
const finish = function () {
const B2 = dj.SlashCommandBuilder
const bare = new B2().setName('bare').setDescription('Bare OS session (logged-in user)')
const sys = new B2().setName('sys').setDescription('Bare OS system snapshots')
const svc = new B2().setName('svc').setDescription('systemctl units')
const fsCmd = new B2().setName('fs').setDescription('Read-only VFS')
const net = new B2().setName('net').setDescription('Swarm / network')
const man = new B2().setName('man').setDescription('Look up a man page')
const say = new B2().setName('say').setDescription('Speak as Bare OS (or open a compose form)')
const run = new B2().setName('run').setDescription('Non-interactive Bare OS shell (cwd, pipes, cd, history)')
const journal = new B2()
.setName('journal')
.setDescription('Tail a unit or system log')
const edit = new B2()
.setName('edit')
.setDescription('Edit a text file in a Discord modal (home or /tmp)')
const create = new B2()
.setName('create')
.setDescription('Create a new text file (pick a path, then enter contents)')
const files = new B2()
.setName('files')
.setDescription('Browse and manage files (list, open, mkdir, rename, delete)')
const settings = new B2()
.setName('settings')
.setDescription('Live session settings (theme, shell, discord, agent, aliases)')
const plugins = new B2()
.setName('plugins')
.setDescription('List, reload, and manage ~/.discord/plugins')
const panel = new B2()
.setName('panel')
.setDescription('Interactive Bare OS control panel')
const ping = new B2().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']
]) &&
discordCmdAddSubs(plugins, [
['list', 'Loaded plugins'],
['reload', 'Rescan ~/.discord/plugins'],
['info', 'Plugin details', function (s) {
discordCmdOpt(s, 'name', 'Plugin name', true, false)
}],
['disable', 'Disable a plugin', function (s) {
discordCmdOpt(s, 'name', 'Plugin name', true, false)
}],
['enable', 'Enable a plugin', function (s) {
discordCmdOpt(s, 'name', 'Plugin name', true, false)
}]
])
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', 'Shell line (cd, pipes, &&, redirects, $VAR)', 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)
const stock = [bare, sys, svc, fsCmd, net, man, say, run, journal, edit, create, files, settings, plugins, panel, ping].map(
function (c) {
return c.toJSON()
}
)
return stock.concat(discordPluginsSlash(dj))
}
if (ctx && typeof ctx.then !== 'function') {
return Promise.resolve(discordPluginsEnsure(ctx)).then(finish)
}
return finish()
}
function discordShowModal(interaction, spec) {
if (!interaction || typeof interaction.showModal !== 'function') return false
return interaction.showModal({
@@ -4663,6 +5233,22 @@ async function discordDispatchAutocomplete(ctx, interaction) {
cmd === 'edit' || cmd === 'create'
? discordSuggestEditPaths(ctx, q)
: discordSuggestPaths(ctx, q)
} else if (fname === 'name' && String(interaction.commandName || '') === 'plugins') {
await discordPluginsEnsure(ctx)
const names = []
for (let i = 0; i < BARE_OS_DISCORD_PLUGINS.length; i++) names.push(BARE_OS_DISCORD_PLUGINS[i].name)
choices = discordFilterChoices(names, q)
} else {
const plug = discordPluginFind(interaction.commandName)
if (plug && typeof plug.onAutocomplete === 'function') {
const ev = discordPluginEvent(ctx, interaction, plug, '', function () {
return q
})
ev.focused = fname
ev.query = q
const got = await plug.onAutocomplete(ev)
if (Array.isArray(got)) choices = discordFilterChoices(got, q)
}
}
} catch {
choices = []
@@ -4840,6 +5426,10 @@ async function discordDispatchComponent(ctx, interaction) {
const opt = function () {
return ''
}
if (id.indexOf('plug:') === 0) {
await discordPluginComponent(ctx, interaction, id)
return true
}
if (id.indexOf('sh:') === 0 || id === 'run:compose') {
await discordShAction(ctx, interaction, id)
return true
@@ -4965,6 +5555,26 @@ async function discordDispatchComponent(ctx, interaction) {
async function discordDispatchModal(ctx, interaction) {
const id = String(interaction.customId || '')
if (id.indexOf('plug:') === 0) {
const parts = id.split(':')
const plug = discordPluginFind(parts[1] || '')
if (plug && typeof plug.onModal === 'function') {
const ev = discordPluginEvent(ctx, interaction, plug, '', function () {
return ''
})
ev.id = parts.slice(2).join(':')
ev.value = ''
try {
if (interaction.fields && typeof interaction.fields.getTextInputValue === 'function') {
ev.value = String(interaction.fields.getTextInputValue('text') || '')
}
} catch {
ev.value = ''
}
const out = await plug.onModal(ev)
return discordSendResult(ctx, interaction, typeof out === 'string' ? { text: out } : out || { text: 'ok' })
}
}
let value = ''
try {
if (interaction.fields && typeof interaction.fields.getTextInputValue === 'function') {
@@ -5221,6 +5831,7 @@ async function discordDispatchInteraction(ctx, interaction) {
}
return true
}
await discordPluginsEnsure(ctx)
return discordDispatchAutocomplete(ctx, interaction)
}
if (!discordUserAllowed(ctx, discordInteractionUserId(interaction))) {
@@ -5270,7 +5881,14 @@ async function discordDispatchInteraction(ctx, interaction) {
}
let result
try {
result = await discordRouteCommand(ctx, name, sub, opt)
await discordPluginsEnsure(ctx)
if (name === 'plugins') {
result = await discordPluginsAdmin(ctx, interaction, sub, opt)
} else {
const plug = discordPluginFind(name)
if (plug) result = await discordPluginInvoke(ctx, interaction, plug, sub, opt)
else result = await discordRouteCommand(ctx, name, sub, opt)
}
} catch (err) {
result = {
text: 'error: ' + ((err && err.message) || String(err)),
@@ -5325,7 +5943,12 @@ var bareOsDiscordCommands = {
idleArm: discordIdleArm,
idleLive: function () {
return BARE_OS_DISCORD_LIVE
}
},
loadPlugins: discordPluginsLoad,
listPlugins: function () {
return BARE_OS_DISCORD_PLUGINS
},
setPluginRegistrar: discordPluginsSetRegistrar
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = bareOsDiscordCommands