Rework run into a nice to use shell
This commit is contained in:
@@ -28,6 +28,67 @@ 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_SH_TTL_MS = 30 * 60 * 1000
|
||||
var BARE_OS_DISCORD_SH_SESSIONS = Object.create(null)
|
||||
var BARE_OS_DISCORD_SH_BINS = null
|
||||
var BARE_OS_DISCORD_SH_BINS_AT = 0
|
||||
var BARE_OS_DISCORD_SH_EXEC_MS = 20000
|
||||
var BARE_OS_DISCORD_SH_BUILTINS = [
|
||||
'alias',
|
||||
'unalias',
|
||||
'barerc',
|
||||
'cd',
|
||||
'export',
|
||||
'unset',
|
||||
'set',
|
||||
'true',
|
||||
'false',
|
||||
'echo',
|
||||
'pwd',
|
||||
'type',
|
||||
'command',
|
||||
'umask',
|
||||
'readonly',
|
||||
'test',
|
||||
'[',
|
||||
'jobs',
|
||||
'wait',
|
||||
':',
|
||||
'help'
|
||||
]
|
||||
var BARE_OS_DISCORD_SH_TTY = {
|
||||
edit: 1,
|
||||
nano: 1,
|
||||
vim: 1,
|
||||
vi: 1,
|
||||
btop: 1,
|
||||
baretop: 1,
|
||||
irc: 1,
|
||||
chat: 1,
|
||||
dhttop: 1,
|
||||
swarmmap: 1,
|
||||
summon: 1,
|
||||
'holepunch-view': 1,
|
||||
routeview: 1
|
||||
}
|
||||
var BARE_OS_DISCORD_SH_FLAGS = {
|
||||
ls: ['-a', '-l', '-la', '-lh', '-h', '-1', '-F'],
|
||||
grep: ['-i', '-v', '-n', '-r', '-E', '-F'],
|
||||
find: ['-name', '-type', '-size', '-path'],
|
||||
rm: ['-r', '-f', '-rf', '-v'],
|
||||
cp: ['-r', '-v', '-n'],
|
||||
mv: ['-v', '-n'],
|
||||
cat: ['-n', '-A'],
|
||||
head: ['-n'],
|
||||
tail: ['-n', '-f'],
|
||||
chmod: ['-R'],
|
||||
chown: ['-R'],
|
||||
df: ['-h', '-T'],
|
||||
du: ['-h', '-s', '-a'],
|
||||
ps: ['-e', '-f'],
|
||||
mkdir: ['-p'],
|
||||
tar: ['-xvf', '-cvf', '-tzf']
|
||||
}
|
||||
var BARE_OS_DISCORD_RUN_ALLOW = {
|
||||
uname: 1,
|
||||
whoami: 1,
|
||||
@@ -826,7 +887,7 @@ function discordNavRows() {
|
||||
{ id: 'sys:ps', label: 'Processes' },
|
||||
{ id: 'net:summary', label: 'Network' },
|
||||
{ id: 'say:compose', label: 'Say…', style: 3 },
|
||||
{ id: 'run:compose', label: 'Run…' }
|
||||
{ id: 'run:compose', label: 'Shell' }
|
||||
])
|
||||
const c = discordButtons([
|
||||
{ id: 'edit:new', label: 'Edit file…', style: 1 },
|
||||
@@ -1155,13 +1216,277 @@ async function discordSuggestMan(ctx, q) {
|
||||
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)
|
||||
function discordShKey(interaction) {
|
||||
return discordInteractionUserId(interaction) || 'anon'
|
||||
}
|
||||
|
||||
function discordShGc() {
|
||||
const now = Date.now()
|
||||
for (const k in BARE_OS_DISCORD_SH_SESSIONS) {
|
||||
const r = BARE_OS_DISCORD_SH_SESSIONS[k]
|
||||
if (!r || now - r.atMs > BARE_OS_DISCORD_SH_TTL_MS) delete BARE_OS_DISCORD_SH_SESSIONS[k]
|
||||
}
|
||||
names.sort()
|
||||
return discordFilterChoices(names, q)
|
||||
}
|
||||
|
||||
function discordShGet(interaction, ctx) {
|
||||
discordShGc()
|
||||
const key = discordShKey(interaction)
|
||||
let rec = BARE_OS_DISCORD_SH_SESSIONS[key]
|
||||
const env = discordCmdEnv(ctx)
|
||||
const home = env.HOME || '/home/' + discordSessionUser(ctx)
|
||||
if (!rec) {
|
||||
rec = {
|
||||
cwd: env.PWD || home || '~',
|
||||
oldpwd: home,
|
||||
hist: [],
|
||||
lastCmd: '',
|
||||
lastExit: 0,
|
||||
lastMs: 0,
|
||||
atMs: Date.now()
|
||||
}
|
||||
BARE_OS_DISCORD_SH_SESSIONS[key] = rec
|
||||
}
|
||||
rec.atMs = Date.now()
|
||||
if (!rec.cwd) rec.cwd = env.PWD || home || '~'
|
||||
return rec
|
||||
}
|
||||
|
||||
function discordShPrettyCwd(ctx, cwd) {
|
||||
const env = discordCmdEnv(ctx)
|
||||
const home = env.HOME || '/home/' + discordSessionUser(ctx)
|
||||
const c = String(cwd || '~')
|
||||
if (c === '~' || c === home) return '~'
|
||||
if (home && c.indexOf(home + '/') === 0) return '~' + c.slice(home.length)
|
||||
return c
|
||||
}
|
||||
|
||||
function discordShPrompt(ctx, rec) {
|
||||
return (
|
||||
discordSessionUser(ctx) +
|
||||
'@' +
|
||||
(discordCmdEnv(ctx).HOSTNAME || 'bare-os') +
|
||||
':' +
|
||||
discordShPrettyCwd(ctx, rec.cwd)
|
||||
)
|
||||
}
|
||||
|
||||
async function discordShPrepare(ctx, rec) {
|
||||
const env = discordCmdEnv(ctx)
|
||||
const cwd = rec.cwd || env.HOME || '~'
|
||||
rec.cwd = cwd
|
||||
env.PWD = cwd
|
||||
if (ctx.env && ctx.env !== env) ctx.env.PWD = cwd
|
||||
if (ctx.vfs && typeof ctx.vfs.chdir === 'function') {
|
||||
try {
|
||||
await ctx.vfs.chdir(cwd)
|
||||
} catch {
|
||||
/* keep env PWD */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function discordShHarvest(ctx, rec) {
|
||||
const env = discordCmdEnv(ctx)
|
||||
if (env.PWD) rec.cwd = String(env.PWD)
|
||||
if (ctx.vfs && ctx.vfs.env && ctx.vfs.env.PWD) rec.cwd = String(ctx.vfs.env.PWD)
|
||||
}
|
||||
|
||||
function discordShPushHist(rec, cmd, exit, ms, preview) {
|
||||
rec.lastCmd = cmd
|
||||
rec.lastExit = exit
|
||||
rec.lastMs = ms
|
||||
rec.hist = rec.hist.filter(function (h) {
|
||||
return h && h.cmd !== cmd
|
||||
})
|
||||
rec.hist.unshift({
|
||||
cmd: cmd,
|
||||
exit: exit,
|
||||
ms: ms,
|
||||
preview: String(preview || '').slice(0, 80)
|
||||
})
|
||||
if (rec.hist.length > 25) rec.hist.length = 25
|
||||
}
|
||||
|
||||
async function discordShCommandNames(ctx) {
|
||||
const now = Date.now()
|
||||
if (BARE_OS_DISCORD_SH_BINS && now - BARE_OS_DISCORD_SH_BINS_AT < 60000) {
|
||||
return BARE_OS_DISCORD_SH_BINS
|
||||
}
|
||||
const set = Object.create(null)
|
||||
for (let i = 0; i < BARE_OS_DISCORD_SH_BUILTINS.length; i++) {
|
||||
set[BARE_OS_DISCORD_SH_BUILTINS[i]] = 1
|
||||
}
|
||||
for (const k in BARE_OS_DISCORD_RUN_ALLOW) {
|
||||
if (Object.prototype.hasOwnProperty.call(BARE_OS_DISCORD_RUN_ALLOW, k) && k.indexOf(' ') < 0) {
|
||||
set[k] = 1
|
||||
}
|
||||
}
|
||||
if (ctx && ctx.shellAliases && typeof ctx.shellAliases === 'object') {
|
||||
const aks = Object.keys(ctx.shellAliases)
|
||||
for (let i = 0; i < aks.length; i++) set[aks[i]] = 1
|
||||
}
|
||||
try {
|
||||
if (ctx && ctx.vfs && typeof ctx.vfs.readdir === 'function') {
|
||||
const n = await ctx.vfs.readdir('/bin')
|
||||
if (Array.isArray(n)) {
|
||||
for (let i = 0; i < n.length; i++) set[String(n[i])] = 1
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const man = await discordCmdReadText(ctx, '/share/man/man.json')
|
||||
const j = discordTryJson(man)
|
||||
if (j && Array.isArray(j.pages)) {
|
||||
for (let i = 0; i < j.pages.length; i++) {
|
||||
if (j.pages[i] && j.pages[i].name) set[String(j.pages[i].name)] = 1
|
||||
}
|
||||
}
|
||||
const names = Object.keys(set).sort()
|
||||
BARE_OS_DISCORD_SH_BINS = names
|
||||
BARE_OS_DISCORD_SH_BINS_AT = now
|
||||
return names
|
||||
}
|
||||
|
||||
function discordShSplitLine(q) {
|
||||
const s = String(q || '')
|
||||
const m = /^(.*?)(\S*)$/.exec(s)
|
||||
return { prefix: m ? m[1] : '', token: m ? m[2] : s }
|
||||
}
|
||||
|
||||
function discordShFirstWord(q) {
|
||||
const s = String(q || '').trim()
|
||||
const i = s.search(/\s/)
|
||||
return i < 0 ? s : s.slice(0, i)
|
||||
}
|
||||
|
||||
async function discordShCompletePath(ctx, rec, token, dirsOnly) {
|
||||
let dir = rec.cwd || '~'
|
||||
let base = token
|
||||
const slash = token.lastIndexOf('/')
|
||||
if (slash >= 0) {
|
||||
const head = token.slice(0, slash + 1)
|
||||
base = token.slice(slash + 1)
|
||||
if (head.charAt(0) === '/' || head.charAt(0) === '~') dir = head === '/' ? '/' : head.replace(/\/$/, '') || '/'
|
||||
else dir = discordJoinPath(rec.cwd, head.replace(/\/$/, '') || '.')
|
||||
}
|
||||
let names = []
|
||||
try {
|
||||
if (ctx.vfs && typeof ctx.vfs.readdir === 'function') {
|
||||
const n = await ctx.vfs.readdir(dir)
|
||||
if (Array.isArray(n)) names = n
|
||||
}
|
||||
} catch {
|
||||
names = []
|
||||
}
|
||||
const out = []
|
||||
const want = String(base || '').toLowerCase()
|
||||
for (let i = 0; i < names.length && out.length < 20; i++) {
|
||||
const name = String(names[i])
|
||||
if (want && name.toLowerCase().indexOf(want) !== 0) continue
|
||||
const full = discordJoinPath(dir, name)
|
||||
let isDir = false
|
||||
try {
|
||||
const st = await discordFmStat(ctx, full)
|
||||
isDir = discordIsDirStat(st)
|
||||
} catch {
|
||||
isDir = false
|
||||
}
|
||||
if (dirsOnly && !isDir) continue
|
||||
const shown = (slash >= 0 ? token.slice(0, slash + 1) : '') + name + (isDir ? '/' : '')
|
||||
out.push({
|
||||
label: shown,
|
||||
value: shown,
|
||||
description: isDir ? 'directory' : 'file'
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function discordShPushChoice(out, seen, name, value, description) {
|
||||
const v = String(value || '').slice(0, 100)
|
||||
if (!v || seen[v]) return
|
||||
if (out.length >= 25) return
|
||||
seen[v] = 1
|
||||
out.push({
|
||||
name: String(name || v).slice(0, 100),
|
||||
value: v,
|
||||
description: description ? String(description).slice(0, 100) : undefined
|
||||
})
|
||||
}
|
||||
|
||||
async function discordSuggestRun(ctx, interaction, q) {
|
||||
const rec = discordShGet(interaction, ctx)
|
||||
const raw = String(q || '')
|
||||
const split = discordShSplitLine(raw)
|
||||
const first = discordShFirstWord(raw)
|
||||
const out = []
|
||||
const seen = Object.create(null)
|
||||
if (!raw.trim() || (!split.prefix && first === split.token)) {
|
||||
for (let i = 0; i < rec.hist.length && out.length < 6; i++) {
|
||||
const h = rec.hist[i]
|
||||
if (!h || !h.cmd) continue
|
||||
if (split.token && h.cmd.indexOf(split.token) !== 0) continue
|
||||
discordShPushChoice(out, seen, '↵ ' + h.cmd, h.cmd, 'history · exit ' + h.exit)
|
||||
}
|
||||
const cmds = await discordShCommandNames(ctx)
|
||||
const needle = split.token.toLowerCase()
|
||||
for (let i = 0; i < cmds.length && out.length < 25; i++) {
|
||||
if (needle && cmds[i].toLowerCase().indexOf(needle) !== 0) continue
|
||||
const kind = BARE_OS_DISCORD_SH_TTY[cmds[i]]
|
||||
? 'TUI (needs a real TTY)'
|
||||
: ctx.shellAliases && ctx.shellAliases[cmds[i]]
|
||||
? 'alias'
|
||||
: BARE_OS_DISCORD_SH_BUILTINS.indexOf(cmds[i]) >= 0
|
||||
? 'builtin'
|
||||
: 'command'
|
||||
discordShPushChoice(out, seen, cmds[i], cmds[i], kind)
|
||||
}
|
||||
return out
|
||||
}
|
||||
const flags = BARE_OS_DISCORD_SH_FLAGS[first]
|
||||
if (split.token.charAt(0) === '-' && flags) {
|
||||
for (let i = 0; i < flags.length && out.length < 25; i++) {
|
||||
if (split.token && flags[i].indexOf(split.token) !== 0) continue
|
||||
const line = (split.prefix + flags[i]).slice(0, 100)
|
||||
discordShPushChoice(out, seen, line, line, first + ' flag')
|
||||
}
|
||||
return out
|
||||
}
|
||||
const pathish =
|
||||
!split.token ||
|
||||
split.token.charAt(0) === '.' ||
|
||||
split.token.charAt(0) === '/' ||
|
||||
split.token.charAt(0) === '~' ||
|
||||
split.token.indexOf('/') >= 0 ||
|
||||
first === 'cd' ||
|
||||
first === 'ls' ||
|
||||
first === 'cat' ||
|
||||
first === 'rm' ||
|
||||
first === 'mv' ||
|
||||
first === 'cp' ||
|
||||
first === 'mkdir' ||
|
||||
first === 'stat' ||
|
||||
first === 'head' ||
|
||||
first === 'tail' ||
|
||||
first === 'edit'
|
||||
if (pathish) {
|
||||
const paths = await discordShCompletePath(ctx, rec, split.token, first === 'cd')
|
||||
for (let i = 0; i < paths.length && out.length < 25; i++) {
|
||||
const line = (split.prefix + paths[i].value).slice(0, 100)
|
||||
discordShPushChoice(out, seen, line, line, paths[i].description)
|
||||
}
|
||||
}
|
||||
if (!out.length && split.token) {
|
||||
const cmds = await discordShCommandNames(ctx)
|
||||
const needle = split.token.toLowerCase()
|
||||
for (let i = 0; i < cmds.length && out.length < 25; i++) {
|
||||
if (cmds[i].toLowerCase().indexOf(needle) !== 0) continue
|
||||
const line = (split.prefix + cmds[i]).slice(0, 100)
|
||||
discordShPushChoice(out, seen, line, line, 'command')
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function discordFmKey(interaction) {
|
||||
@@ -1450,7 +1775,7 @@ 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', 'Allowlisted command, logs, speak, man pages')
|
||||
discordField('/run /journal /say /man', 'Full shell (cwd + autocomplete), logs, speak, man pages')
|
||||
]
|
||||
})
|
||||
)
|
||||
@@ -2280,50 +2605,186 @@ function discordCmdSayBox(text) {
|
||||
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 }
|
||||
function discordShHud(ctx, interaction, note) {
|
||||
const rec = discordShGet(interaction, ctx)
|
||||
const fields = [
|
||||
discordField('Directory', '`' + discordShPrettyCwd(ctx, rec.cwd) + '`', true),
|
||||
discordField('Last exit', rec.lastCmd ? String(rec.lastExit) : '—', true),
|
||||
discordField('User', discordSessionUser(ctx), true)
|
||||
]
|
||||
if (rec.lastCmd) {
|
||||
fields.push(discordField('Last command', '`' + rec.lastCmd + '`', false))
|
||||
}
|
||||
const key = cmd.replace(/\s+/g, ' ')
|
||||
const bin = key.split(' ')[0]
|
||||
if (!BARE_OS_DISCORD_RUN_ALLOW[key] && !BARE_OS_DISCORD_RUN_ALLOW[bin]) {
|
||||
const hist = rec.hist.slice(0, 6)
|
||||
if (hist.length) {
|
||||
fields.push(
|
||||
discordField(
|
||||
'History',
|
||||
hist
|
||||
.map(function (h) {
|
||||
return '`' + h.cmd + '` · ' + h.exit
|
||||
})
|
||||
.join('\n')
|
||||
)
|
||||
)
|
||||
}
|
||||
const histSel = discordSelect(
|
||||
'sh:hist',
|
||||
'Replay a command…',
|
||||
rec.hist.slice(0, 20).map(function (h, i) {
|
||||
return {
|
||||
label: h.cmd.slice(0, 90),
|
||||
value: String(i),
|
||||
description: 'exit ' + h.exit + (h.ms ? ' · ' + h.ms + 'ms' : '')
|
||||
}
|
||||
})
|
||||
)
|
||||
const acts = discordButtons([
|
||||
{ id: 'sh:again', label: 'Repeat', style: 1 },
|
||||
{ id: 'sh:up', label: 'cd ..' },
|
||||
{ id: 'sh:home', label: 'cd ~' },
|
||||
{ id: 'sh:pwd', label: 'pwd' }
|
||||
])
|
||||
return discordResult(
|
||||
discordEmbed({
|
||||
title: 'Shell · ' + discordShPrompt(ctx, rec),
|
||||
desc:
|
||||
(note ? note + '\n' : '') +
|
||||
'Type **`/run cmd:`** and keep going — `cd`, pipes, `&&`, redirects, and `$VAR` all work.\n' +
|
||||
'Working directory sticks for this Discord user.',
|
||||
fields: fields,
|
||||
color: rec.lastCmd && rec.lastExit ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK,
|
||||
footer: discordShPrompt(ctx, rec) + ' · /run cmd:'
|
||||
}),
|
||||
{ components: [histSel, acts].filter(Boolean) }
|
||||
)
|
||||
}
|
||||
|
||||
async function discordCmdHandleRun(ctx, interaction, raw) {
|
||||
const rec = discordShGet(interaction, ctx)
|
||||
const cmd = String(raw || '').trim()
|
||||
if (!cmd) return discordShHud(ctx, interaction, '')
|
||||
if (cmd.length > 4000) {
|
||||
return { text: 'Command line is too long (max 4000).', ephemeral: true }
|
||||
}
|
||||
const first = discordShFirstWord(cmd)
|
||||
if (first === 'exit') {
|
||||
rec.lastCmd = cmd
|
||||
rec.lastExit = 0
|
||||
return discordShHud(ctx, interaction, 'Stayed attached — `exit` does not stop the Discord bot.')
|
||||
}
|
||||
if (first === 'logout' || first === 'login') {
|
||||
return {
|
||||
text:
|
||||
'not in allowlist. Try: uname, whoami, hostname, date, uptime, id, pwd, arch, nproc, help, motd, df, ps, procstat, uname -a',
|
||||
'`login` / `logout` need the guest TTY. This Discord shell stays as **' +
|
||||
discordSessionUser(ctx) +
|
||||
'**.',
|
||||
ephemeral: true
|
||||
}
|
||||
}
|
||||
if (typeof ctx.execLine !== 'function') {
|
||||
return { text: 'execLine unavailable', ephemeral: true }
|
||||
return { text: 'execLine unavailable in this session', ephemeral: true }
|
||||
}
|
||||
await discordShPrepare(ctx, rec)
|
||||
const t0 = Date.now()
|
||||
let out = ''
|
||||
let errNote = ''
|
||||
try {
|
||||
out = await discordCmdCapture(ctx, function () {
|
||||
return ctx.execLine(cmd, { timeoutMs: BARE_OS_DISCORD_SH_EXEC_MS })
|
||||
})
|
||||
} catch (err) {
|
||||
errNote = String((err && err.message) || err)
|
||||
out = (out ? out + '\n' : '') + errNote
|
||||
}
|
||||
const ms = Date.now() - t0
|
||||
const exit = ctx.exitCode == null ? (errNote ? 1 : 0) : Number(ctx.exitCode) || 0
|
||||
discordShHarvest(ctx, rec)
|
||||
discordShPushHist(rec, cmd, exit, ms, out)
|
||||
const tty = BARE_OS_DISCORD_SH_TTY[first]
|
||||
if (!String(out || '').trim() && (first === 'cd' || first === 'export' || first === 'unset' || first === 'alias')) {
|
||||
return discordShHud(ctx, interaction, '`$ ' + cmd + '` · exit **' + exit + '** · ' + ms + 'ms')
|
||||
}
|
||||
const out = await discordCmdCapture(ctx, function () {
|
||||
return ctx.execLine(key)
|
||||
})
|
||||
const parsed = discordTryJson(out)
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
return discordPrettySnapshot('$ ' + key, parsed)
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && String(out).length < 800) {
|
||||
return discordPrettySnapshot(discordShPrompt(ctx, rec) + ' $ ' + cmd, parsed, {
|
||||
color: exit ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK
|
||||
})
|
||||
}
|
||||
if (!out) {
|
||||
const lead =
|
||||
'`' +
|
||||
discordShPrompt(ctx, rec) +
|
||||
'`\n`$ ' +
|
||||
cmd +
|
||||
'`' +
|
||||
(tty ? '\n_Interactive TUI — needs a real terminal; output may be empty._' : '')
|
||||
if (!String(out || '').trim()) {
|
||||
return discordResult(
|
||||
discordEmbed({
|
||||
title: '$ ' + key,
|
||||
footer: 'ran as ' + discordSessionUser(ctx),
|
||||
desc: '(no output, exit ' + String(ctx.exitCode || 0) + ')'
|
||||
})
|
||||
title: 'Shell',
|
||||
desc: lead + '\n(no output)',
|
||||
color: exit ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK,
|
||||
footer: 'exit ' + exit + ' · ' + ms + 'ms · ' + discordShPrettyCwd(ctx, rec.cwd)
|
||||
}),
|
||||
{
|
||||
components: [
|
||||
discordButtons([
|
||||
{ id: 'sh:again', label: 'Repeat', style: 1 },
|
||||
{ id: 'sh:hud', label: 'Shell' }
|
||||
])
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
return {
|
||||
more: {
|
||||
title: '$ ' + key,
|
||||
title: 'Shell',
|
||||
lead: lead,
|
||||
body: discordCmdRedact(out),
|
||||
fence: true,
|
||||
footer: 'ran as ' + discordSessionUser(ctx)
|
||||
color: exit ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK,
|
||||
footer: 'exit ' + exit + ' · ' + ms + 'ms · ' + discordShPrettyCwd(ctx, rec.cwd),
|
||||
components: [
|
||||
discordButtons([
|
||||
{ id: 'sh:again', label: 'Repeat', style: 1 },
|
||||
{ id: 'sh:up', label: 'cd ..' },
|
||||
{ id: 'sh:home', label: 'cd ~' },
|
||||
{ id: 'sh:hud', label: 'Shell' }
|
||||
])
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function discordShAction(ctx, interaction, id) {
|
||||
const rec = discordShGet(interaction, ctx)
|
||||
if (id === 'sh:hud' || id === 'run:compose') {
|
||||
return discordSendResult(ctx, interaction, discordShHud(ctx, interaction, ''))
|
||||
}
|
||||
if (id === 'sh:pwd') {
|
||||
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, 'pwd'))
|
||||
}
|
||||
if (id === 'sh:again') {
|
||||
if (!rec.lastCmd) {
|
||||
return discordSendResult(ctx, interaction, discordShHud(ctx, interaction, 'No command to repeat.'))
|
||||
}
|
||||
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, rec.lastCmd))
|
||||
}
|
||||
if (id === 'sh:up') {
|
||||
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, 'cd ..'))
|
||||
}
|
||||
if (id === 'sh:home') {
|
||||
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, 'cd'))
|
||||
}
|
||||
if (id === 'sh:hist') {
|
||||
const i = Number((interaction.values && interaction.values[0]) || 0)
|
||||
const hit = rec.hist[i]
|
||||
if (!hit) return discordSendResult(ctx, interaction, discordShHud(ctx, interaction, ''))
|
||||
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, hit.cmd))
|
||||
}
|
||||
return discordSendResult(ctx, interaction, discordShHud(ctx, interaction, ''))
|
||||
}
|
||||
|
||||
async function discordCmdHandleJournal(ctx, unit) {
|
||||
if (typeof ctx.bareOsRunSystemctlCli === 'function' && unit) {
|
||||
const out = await discordCmdCapture(ctx, function () {
|
||||
@@ -2409,7 +2870,7 @@ function discordBuildSlashCommands(dj) {
|
||||
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 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')
|
||||
@@ -2494,7 +2955,7 @@ function discordBuildSlashCommands(dj) {
|
||||
}
|
||||
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(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)
|
||||
@@ -2551,11 +3012,7 @@ async function discordRouteCommand(ctx, name, sub, opt) {
|
||||
})
|
||||
)
|
||||
}
|
||||
if (name === 'run') {
|
||||
const cmd = opt('cmd')
|
||||
if (!cmd) return { modal: 'run' }
|
||||
return discordCmdHandleRun(ctx, cmd)
|
||||
}
|
||||
if (name === 'run') return { run: opt('cmd') || '' }
|
||||
if (name === 'journal') return discordCmdHandleJournal(ctx, opt('unit'))
|
||||
if (name === 'edit') {
|
||||
const path = opt('path')
|
||||
@@ -3951,6 +4408,7 @@ function discordIdleForgetSessions(userId) {
|
||||
delete BARE_OS_DISCORD_FM_SESSIONS[userId]
|
||||
delete BARE_OS_DISCORD_SET_SESSIONS[userId]
|
||||
delete BARE_OS_DISCORD_MORE_SESSIONS[userId]
|
||||
delete BARE_OS_DISCORD_SH_SESSIONS[userId]
|
||||
}
|
||||
|
||||
function discordUnwrapMessage(sent) {
|
||||
@@ -4069,6 +4527,9 @@ async function discordIdleWatch(interaction, payload, sent) {
|
||||
}
|
||||
|
||||
async function discordSendResult(ctx, interaction, result) {
|
||||
if (result && result.run !== undefined) {
|
||||
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, result.run))
|
||||
}
|
||||
if (result && result.more) {
|
||||
return discordSendResult(ctx, interaction, discordLongTextResult(interaction, result.more))
|
||||
}
|
||||
@@ -4138,13 +4599,7 @@ async function discordSendResult(ctx, interaction, result) {
|
||||
return
|
||||
}
|
||||
if (result && result.modal === 'run') {
|
||||
await discordShowModal(interaction, {
|
||||
id: 'run:modal',
|
||||
title: 'Run allowlisted command',
|
||||
label: 'Command',
|
||||
placeholder: 'uname -a'
|
||||
})
|
||||
return
|
||||
return discordSendResult(ctx, interaction, discordShHud(ctx, interaction, ''))
|
||||
}
|
||||
const payload = discordCmdReplyPayload(result)
|
||||
const isComp =
|
||||
@@ -4201,7 +4656,7 @@ async function discordDispatchAutocomplete(ctx, interaction) {
|
||||
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 === 'cmd') choices = await discordSuggestRun(ctx, interaction, q)
|
||||
else if (fname === 'path') {
|
||||
const cmd = String(interaction.commandName || '')
|
||||
choices =
|
||||
@@ -4385,6 +4840,10 @@ async function discordDispatchComponent(ctx, interaction) {
|
||||
const opt = function () {
|
||||
return ''
|
||||
}
|
||||
if (id.indexOf('sh:') === 0 || id === 'run:compose') {
|
||||
await discordShAction(ctx, interaction, id)
|
||||
return true
|
||||
}
|
||||
if (id === 'more:prev' || id === 'more:next') {
|
||||
const rec = discordMoreGet(interaction)
|
||||
if (!rec) {
|
||||
@@ -4415,15 +4874,6 @@ async function discordDispatchComponent(ctx, interaction) {
|
||||
})
|
||||
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',
|
||||
@@ -4752,7 +5202,7 @@ async function discordDispatchModal(ctx, interaction) {
|
||||
)
|
||||
}
|
||||
if (id === 'run:modal') {
|
||||
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, value))
|
||||
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, value))
|
||||
}
|
||||
return discordSendResult(ctx, interaction, discordPanel(ctx))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user