update
Release rolling / release (push) Successful in 10m45s

This commit is contained in:
Raven Scott
2026-08-13 13:16:39 -04:00
parent 09bf370e06
commit 88f6c201c0
14 changed files with 2153 additions and 33 deletions
@@ -40,6 +40,16 @@ var BARE_OS_DISCORD_KNOWN_UNITS = [
'bare-holesail',
'kernel-logger'
]
var BARE_OS_DISCORD_EDIT_CHUNK = 4000
var BARE_OS_DISCORD_EDIT_MAX_CHUNKS = 5
var BARE_OS_DISCORD_EDIT_TTL_MS = 15 * 60 * 1000
var BARE_OS_DISCORD_EDIT_SESSIONS = Object.create(null)
var BARE_OS_DISCORD_EDIT_SUGGEST = [
'~/notes.txt',
'~/.barerc',
'~/TODO.md',
'/tmp/scratch.txt'
]
var BARE_OS_DISCORD_FS_SUGGEST = [
'~',
'/etc/os-release',
@@ -466,8 +476,13 @@ function discordNavRows() {
{ 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 }
])
if (a) rows.push(a)
if (b) rows.push(b)
if (c) rows.push(c)
return rows
}
@@ -479,6 +494,8 @@ function discordResult(embed, extra) {
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
}
@@ -524,6 +541,146 @@ function discordCmdPathOk(raw) {
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 || {}
@@ -653,6 +810,15 @@ function discordSuggestRun(q) {
return discordFilterChoices(names, q)
}
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)
@@ -737,7 +903,10 @@ async function discordCmdHandleBare(ctx, sub) {
discordField('/svc', 'list · status · start · stop · restart · logs *(unit autocomplete)*'),
discordField('/fs', 'ls · cat · stat · head *(path autocomplete)*'),
discordField('/net', 'peers · swarm · summary'),
discordField('/man /run /journal /say /panel /ping', 'Lookups, allowlisted run, logs, compose modal')
discordField(
'/man /run /journal /say /edit /create /panel /ping',
'Lookups, run, logs, **edit** / **create** file modals'
)
]
})
)
@@ -1178,9 +1347,17 @@ async function discordCmdHandleFs(ctx, sub, rawPath, nlines) {
if (!text) {
return discordResult(discordEmbed({ title: p, desc: '(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((sub === 'head' ? 'head ' : '') + p, asJson)
return discordPrettySnapshot((sub === 'head' ? 'head ' : '') + p, asJson, extra)
}
if (sub === 'head') {
const n = Math.max(1, Math.min(40, Number(nlines) || 12))
@@ -1188,7 +1365,8 @@ async function discordCmdHandleFs(ctx, sub, rawPath, nlines) {
discordEmbed({
title: 'head ' + p,
desc: discordCmdFence(text.split(/\r?\n/).slice(0, n).join('\n'))
})
}),
extra
)
}
return discordResult(
@@ -1196,7 +1374,8 @@ async function discordCmdHandleFs(ctx, sub, rawPath, nlines) {
title: p,
desc: discordCmdFence(text.slice(0, BARE_OS_DISCORD_FS_MAX)),
footer: text.length + ' bytes · ' + discordSessionUser(ctx)
})
}),
extra
)
}
@@ -1479,6 +1658,12 @@ function discordBuildSlashCommands(dj) {
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 panel = new B()
.setName('panel')
.setDescription('Interactive Bare OS control panel')
@@ -1550,7 +1735,9 @@ function discordBuildSlashCommands(dj) {
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)
return [bare, sys, svc, fsCmd, net, man, say, run, journal, panel, ping].map(
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)
return [bare, sys, svc, fsCmd, net, man, say, run, journal, edit, create, panel, ping].map(
function (c) {
return c.toJSON()
}
@@ -1608,10 +1795,284 @@ async function discordRouteCommand(ctx, name, sub, opt) {
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 }
}
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)
}
)
}
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
})
}
}
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.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',
@@ -1680,7 +2141,13 @@ async function discordDispatchAutocomplete(ctx, interaction) {
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') choices = discordSuggestPaths(ctx, q)
else if (fname === 'path') {
const cmd = String(interaction.commandName || '')
choices =
cmd === 'edit' || cmd === 'create'
? discordSuggestEditPaths(ctx, q)
: discordSuggestPaths(ctx, q)
}
} catch {
choices = []
}
@@ -1717,6 +2184,67 @@ async function discordDispatchComponent(ctx, interaction) {
})
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)))
}
@@ -1755,6 +2283,108 @@ async function discordDispatchModal(ctx, interaction) {
} catch {
value = ''
}
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,
@@ -1862,7 +2492,12 @@ var bareOsDiscordCommands = {
formatRlimits: discordFormatRlimits,
formatFeatures: discordFormatFeatures,
formatSwarm: discordFormatSwarm,
formatHostDf: discordFormatHostDf
formatHostDf: discordFormatHostDf,
pathWriteOk: discordCmdPathWriteOk,
editChunks: discordEditChunks,
editPut: discordEditPut,
editGet: discordEditGet,
editModalPayload: discordEditModalPayload
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = bareOsDiscordCommands