Add cowsay-style /bin/baresay and identical /bin/say (say maps to baresay

source via build.mjs). Ship lib/baresay-engine.js for wrapping, balloons,
and cowfile parsing; default character is bear under /share/baresay/.cow.

Register commands, man pages, rebuild man/completion/posix index, sync
kernel to seeder, bump Tier-1 command count in docs to 155, and add a
test that say and baresay bundles match.
This commit is contained in:
Raven Scott
2026-04-21 19:28:55 -04:00
parent ca77803198
commit be6ca4a40c
41 changed files with 2708 additions and 17 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ Files in this directory are **read from disk by the seeder** (or copied into `pa
## Contents
- `**init.js`** — Kernel entry: must define `async function start(ctx)`. Boot order: `**/etc/os-release**``**/etc/motd**` → optional `**/etc/bare-os/rc.profile.<profile>**` (profile from `**BARE_OS_BOOT_PROFILE**` or first line of `**/etc/bare-os/profile**`; the booter mirrors the resolved name in `**ctx.env.BARE_OS_BOOT_PROFILE_RESOLVED**` and `**/run/bare-os/boot_profile**`) → `**/etc/bare-os/rc**``**/etc/bare-os/rc.d/***` (sorted; digit-prefixed names only; skip dotfiles, `*~`, `README*`, `*.md`; optional `**BARE_OS_RC_D_SKIP**` comma list and `**prefix***` patterns) → optional `**/etc/bare-os/rc.local**``**/etc/bare-os/kernel.d/***` (same rules as `**rc.d**`) → banner → when `**BARE_OS_SKIP_REPL**`, optional **onboot** lines from `**BARE_OS_ONBOOT`** or `**/etc/bare-os/onboot**``**readLine` / `execLine**` loop. Boot `**execLine**` errors in trusted snippets are logged; with `**BARE_OS_BOOT_STRICT=1**` or `**true**`, the first throw calls `**requestBooterExit(1)**` and stops later boot phases. Custom kernels may call `**ctx.registerKernelShutdownHook(fn)**` before initd disposers; use `**ctx.bareOsRuntimeCaps**` for limits, pseudo paths, and `**features**` (`[developer-guide/02-the-context-object.md](../developer-guide/02-the-context-object.md)`).
- `**bin/**`**Tier-1 utilities** built by [bare-os-coreutils](../packages/bare-os-coreutils/README.md) plus `**sshd`** / `**bare-sshd**` from [bare-os-openssh](../packages/bare-os-openssh/) (**153** commands in `**COREUTILS_COMMANDS`**; `**sshd**` is listed for man/help but its concatenated script is emitted by the openssh package build, not coreutils `**src/**`). Each file is `**runtime.js**` + optional preamble (`**lib/md5.js**` for `**md5sum**`, `**lib/sha224.js**` for `**sha224sum**`, `**lib/*-engine.js**` for `**sed**`/`**awk**`, `**jq-engine.js**`, `**lib/man-render.js**`, `**lib/edit-*.js**` for `**edit**`/`**nano**`, lscolors for `**ls**`/`**dircolors**`, …) + `**async function run(ctx, argv)**` (no ESM `**import**` in `**src/**`). `**/bin/nano**` duplicates `**/bin/edit**` for familiarity; the shells default `**nano``edit**` alias uses the `**edit**` command name after expansion. `**dir**`/`**vdir**` invoke `**ls**` via `**ctx.runBinCommand**`.
- `**bin/**`**Tier-1 utilities** built by [bare-os-coreutils](../packages/bare-os-coreutils/README.md) plus `**sshd`** / `**bare-sshd**` from [bare-os-openssh](../packages/bare-os-openssh/) (**155** commands in `**COREUTILS_COMMANDS`**; `**sshd**` is listed for man/help but its concatenated script is emitted by the openssh package build, not coreutils `**src/**`). Each file is `**runtime.js**` + optional preamble (`**lib/md5.js**` for `**md5sum**`, `**lib/sha224.js**` for `**sha224sum**`, `**lib/*-engine.js**` for `**sed**`/`**awk**`, `**jq-engine.js**`, `**lib/man-render.js**`, `**lib/edit-*.js**` for `**edit**`/`**nano**`, lscolors for `**ls**`/`**dircolors**`, …) + `**async function run(ctx, argv)**` (no ESM `**import**` in `**src/**`). `**/bin/nano**` duplicates `**/bin/edit**` for familiarity; the shells default `**nano``edit**` alias uses the `**edit**` command name after expansion. `**dir**`/`**vdir**` invoke `**ls**` via `**ctx.runBinCommand**`.
- `**lib/bare/**` — Optional IIFE bundles + `**manifest.json**` for `**ctx.bare**` drive merge, built by [bare-os-bare-libs](../packages/bare-os-bare-libs/README.md). Same trust model as `**bin/**` (trusted seeded image).
- `**share/man/man.json**` — Merged manual database for `**/bin/man**` (built by `**bare-os-coreutils**`; see [handbook ch.10](../handbook/10-manpages-and-online-help.md)).
- `**etc/os-release**` — Static OS metadata (`NAME`, `VERSION`, …).
+502
View File
@@ -0,0 +1,502 @@
/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
/** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */
function bareFormatModeString(mode, type) {
const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-'
const perm = mode & 0o777
const r = (bit) => (perm & bit ? 'r' : '-')
const w = (bit) => (perm & bit ? 'w' : '-')
const x = (bit) => (perm & bit ? 'x' : '-')
return (
typeChar +
r(0o400) +
w(0o200) +
x(0o100) +
r(0o040) +
w(0o020) +
x(0o010) +
r(0o004) +
w(0o002) +
x(0o001)
)
}
/** @param {number} mtimeMs @param {number} [nowMs] */
function bareFormatLsMtime(mtimeMs, nowMs) {
const now = nowMs != null ? nowMs : Date.now()
const d = new Date(mtimeMs)
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
]
const mon = months[d.getMonth()]
const day = String(d.getDate()).padStart(2, ' ')
const sixMo = 180 * 24 * 3600 * 1000
if (Math.abs(now - mtimeMs) > sixMo) {
const yr = String(d.getFullYear()).padStart(4, ' ')
return mon + ' ' + day + ' ' + yr
}
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
return mon + ' ' + day + ' ' + hh + ':' + mm
}
/** @param {number} size */
function barePosixBlocks(size) {
return Math.ceil(Number(size) / 512) || 0
}
/**
* Raw stdout for NUL/binary when **`process.stdout.write`** is missing.
* If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it.
* @param {Record<string, unknown>} ctx
* @param {string | Uint8Array} chunk
* @returns {boolean}
*/
function bareOsEmitRaw(ctx, chunk) {
if (typeof ctx.bareOsBinWrite === 'function') {
const b4 = ctx.b4a
const u8 =
typeof chunk === 'string'
? b4 && typeof b4.from === 'function'
? b4.from(chunk)
: new TextEncoder().encode(chunk)
: chunk
ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8))
return true
}
const w = globalThis.process?.stdout?.write
if (typeof w === 'function') {
w.call(globalThis.process.stdout, chunk)
return true
}
return false
}
/**
* Bubble + cow rendering for baresay (no imports; prepended before src/baresay.js).
*/
/** @param {string} text @param {number} width @param {boolean} noWrap */
function bareBaresayWrap(text, width, noWrap) {
const t = String(text || '').replace(/\r\n/g, '\n')
if (noWrap) {
const line = t.replace(/\n/g, ' ')
return line.length ? [line] : ['']
}
const out = []
const lim = Math.max(8, Math.min(200, width | 0)) || 40
for (const para of t.split('\n')) {
if (!para.trim()) {
out.push('')
continue
}
let rest = para.trim()
while (rest.length) {
if (rest.length <= lim) {
out.push(rest)
break
}
let cut = rest.lastIndexOf(' ', lim)
if (cut <= 0) cut = lim
const piece = rest.slice(0, cut).trimEnd()
if (!piece.length) {
out.push(rest.slice(0, lim))
rest = rest.slice(lim).trimStart()
} else {
out.push(piece)
rest = rest.slice(cut).trimStart()
}
}
}
return out.length ? out : ['']
}
/**
* @param {string[]} lines
* @param {{ think?: boolean, rainbow?: boolean }} opts
*/
function bareBaresayBalloon(lines, opts) {
const think = !!(opts && opts.think)
const rainbow = !!(opts && opts.rainbow)
const rows = lines.map((l) => String(l))
const n = rows.length
const longest = Math.max(...rows.map((r) => r.length), 2)
const padded = rows.map((r) => r.padEnd(longest))
/** @type {string[]} */
const mid = []
if (n === 1) {
const inner = think
? '( ' + padded[0] + ' )'
: '< ' + padded[0] + ' >'
mid.push(inner)
} else if (think) {
for (let i = 0; i < n; i++) {
mid.push('( ' + padded[i] + ' )')
}
} else {
for (let i = 0; i < n; i++) {
let L = '|'
let R = '|'
if (i === 0) {
L = '/'
R = '\\'
} else if (i === n - 1) {
L = '\\'
R = '/'
}
mid.push(L + ' ' + padded[i] + ' ' + R)
}
}
const topUnders = Math.max(0, mid[0].length - 2)
const botDash = Math.max(0, mid[mid.length - 1].length - 2)
/** @type {string[]} */
const out = []
out.push(' ' + '_'.repeat(topUnders))
if (rainbow) {
const ansi = bareBaresayRainbowCodes()
let k = 0
for (const line of mid) {
const c = ansi[k % ansi.length]
k++
out.push(c + line + '\x1b[0m')
}
} else {
for (const line of mid) out.push(line)
}
out.push(' ' + '-'.repeat(botDash))
return out
}
function bareBaresayRainbowCodes() {
return [
'\x1b[31m',
'\x1b[91m',
'\x1b[33m',
'\x1b[32m',
'\x1b[36m',
'\x1b[34m',
'\x1b[35m'
]
}
/** Extract cow body from .cow text (Perl heredoc or raw template). */
function bareBaresayExtractCow(raw) {
const s = String(raw || '').replace(/^\ufeff/, '')
const m = s.match(/\$the_cow\s*=\s*<<"EOC"\s*\r?\n([\s\S]*?)\r?\nEOC\b/)
if (m) return m[1].replace(/\r\n/g, '\n')
return s.replace(/\r\n/g, '\n').trimEnd() + '\n'
}
/**
* @param {string} template
* @param {string} eyes two chars
* @param {string} tongue one char
* @param {string} thoughts
*/
function bareBaresayFillCow(template, eyes, tongue, thoughts) {
const e = (eyes || 'oo').slice(0, 2).padEnd(2, 'o')
const tg = (tongue || ' ').slice(0, 1)
const th = thoughts || '\\'
return template
.replace(/\$thoughts/g, th)
.replace(/\$eyes/g, e)
.replace(/\$tongue/g, tg)
}
/** @param {string} flag */
function bareBaresayPreset(flag) {
switch (flag) {
case 'b':
return { eyes: '==', tongue: ' ' }
case 'd':
return { eyes: 'XX', tongue: 'U' }
case 'g':
return { eyes: '$$', tongue: ' ' }
case 'p':
return { eyes: '@@', tongue: ' ' }
case 's':
return { eyes: '**', tongue: ' ' }
case 't':
return { eyes: '--', tongue: ' ' }
case 'w':
return { eyes: '@@', tongue: ' ' }
case 'y':
return { eyes: '..', tongue: ' ' }
default:
return { eyes: 'oo', tongue: ' ' }
}
}
/**
* @param {string} message
* @param {string} cowBody filled lines (no vars)
* @param {{ width: number, noWrap: boolean, think?: boolean, rainbow?: boolean }} opts
*/
function bareBaresayCompose(message, cowBody, opts) {
const width = opts.width | 0
const wrapped = bareBaresayWrap(message, width, opts.noWrap)
const balloon = bareBaresayBalloon(wrapped, {
think: opts.think,
rainbow: opts.rainbow
})
const cowLines = cowBody.replace(/\s+$/, '').split('\n')
return balloon.concat(cowLines).join('\n') + '\n'
}
/** Minimal bear if no cowfile on disk (ASCII). */
function bareBaresayFallbackBearTemplate() {
return [
' $thoughts',
' $thoughts .--.--.',
' $thoughts ( @ @ )',
' $thoughts / $eyes \\',
' $thoughts | $tongue |',
' $thoughts \\______/',
' $thoughts ||',
' $thoughts ~~'
].join('\n')
}
async function run(ctx, argv) {
const raw0 = argv && argv.length ? argv[0] : 'baresay'
const slash = String(raw0).lastIndexOf('/')
const prog =
slash >= 0 ? String(raw0).slice(slash + 1) || 'baresay' : String(raw0)
/** @param {string} msg */
const usage = (msg) => {
if (msg) ctx.console.error(prog + ': ' + msg)
ctx.console.error(
'usage: ' +
prog +
' [-h] [-l] [-n] [-W COLS] [-f name|path] [-e XX] [-T C] [-bdgpstwy] [--think] [--rainbow] [-R|--random] [MESSAGE...]\n' +
' ' +
prog +
' reads stdin when no MESSAGE and stdin is non-empty.'
)
ctx.exitCode = 2
}
let width = 40
let noWrap = false
let cowKey = 'bear'
let cowPath = ''
let listOnly = false
let eyes = 'oo'
let tongue = ' '
let think = false
let rainbow = false
let randomPick = false
/** @type {string[]} */
const operands = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '--') {
operands.push(...argv.slice(i + 1))
break
}
if (a === '-h' || a === '--help') {
usage('')
return
}
if (a === '-l') {
listOnly = true
continue
}
if (a === '-n') {
noWrap = true
continue
}
if (a === '--think' || a === '-th') {
think = true
continue
}
if (a === '--rainbow') {
rainbow = true
continue
}
if (a === '-R' || a === '--random') {
randomPick = true
continue
}
if (a === '-W' || a === '--width') {
const v = argv[++i]
const n = v != null ? parseInt(String(v), 10) : NaN
if (!Number.isFinite(n) || n < 1) {
usage('invalid width')
return
}
width = n
continue
}
if (a.startsWith('-W') && a.length > 2) {
const n = parseInt(a.slice(2), 10)
if (!Number.isFinite(n) || n < 1) {
usage('invalid width')
return
}
width = n
continue
}
if (a === '-f' || a === '--file') {
const v = argv[++i]
if (v == null) {
usage('missing cow file')
return
}
if (v.startsWith('/') || v.startsWith('.')) cowPath = v
else cowKey = v.replace(/\.cow$/i, '')
continue
}
if (a.startsWith('-f') && a.length > 2) {
const v = a.slice(2)
if (v.startsWith('/') || v.startsWith('.')) cowPath = v
else cowKey = v.replace(/\.cow$/i, '')
continue
}
if (a === '-e') {
const v = argv[++i]
if (v == null || String(v).length < 1) {
usage('missing eyes')
return
}
eyes = String(v).slice(0, 2).padEnd(2, ' ')
continue
}
if (a.startsWith('-e') && a.length > 2) {
eyes = a.slice(2, 4).padEnd(2, ' ')
continue
}
if (a === '-T') {
const v = argv[++i]
tongue = v != null && String(v).length ? String(v).slice(0, 1) : ' '
continue
}
if (a.startsWith('-T') && a.length > 2) {
tongue = a.slice(2, 3) || ' '
continue
}
{
const mm = a.match(/^-([bdgpstwy])$/)
if (mm) {
const p = bareBaresayPreset(mm[1])
eyes = p.eyes
tongue = p.tongue
continue
}
}
if (a.startsWith('-') && a.length > 1) {
usage('unknown option: ' + a)
return
}
operands.push(a)
}
const vfs = ctx.vfs
const b4 = ctx.b4a
/** @returns {Promise<string[]>} */
async function listShareCows() {
const dir = '/share/baresay'
/** @type {string[]} */
const names = []
if (!vfs || typeof vfs.readdir !== 'function') return names
try {
for await (const n of vfs.readdir(dir)) {
if (typeof n === 'string' && n.endsWith('.cow')) {
names.push(n.replace(/\.cow$/i, ''))
}
}
} catch {
/* ignore */
}
return names.sort()
}
if (listOnly) {
const names = await listShareCows()
for (const n of names) ctx.console.log(n)
ctx.exitCode = 0
return
}
/** @type {string} */
let message = operands.join(' ')
if (!message.trim()) {
message = bareStdin(ctx).replace(/\r\n/g, '\n').trimEnd()
}
if (randomPick) {
const names = await listShareCows()
const pool = names.length ? names : ['bear']
cowKey = pool[Math.floor(Math.random() * pool.length)]
cowPath = ''
}
/** @returns {Promise<string | null>} */
async function readCowAt(path) {
if (!vfs || typeof vfs.readFile !== 'function') return null
try {
const buf = await vfs.readFile(path)
if (!buf) return null
return b4 ? b4.toString(buf) : String(buf)
} catch {
return null
}
}
let rawCow = ''
if (cowPath) {
rawCow = (await readCowAt(cowPath)) || ''
} else {
const p = '/share/baresay/' + cowKey + '.cow'
rawCow = (await readCowAt(p)) || ''
}
if (!rawCow.trim()) {
if (cowKey === 'bear' && !cowPath) {
rawCow = bareBaresayFallbackBearTemplate()
} else {
ctx.console.error(
prog +
': cow file not found: ' +
(cowPath || '/share/baresay/' + cowKey + '.cow')
)
ctx.exitCode = 1
return
}
}
const template = bareBaresayExtractCow(rawCow)
const filled = bareBaresayFillCow(template, eyes, tongue, '\\')
const out = bareBaresayCompose(message, filled, {
width,
noWrap,
think,
rainbow
})
const w = globalThis.process?.stdout?.write
if (typeof w === 'function') {
w.call(globalThis.process.stdout, out)
} else {
ctx.console.log(out.replace(/\n$/, ''))
}
ctx.exitCode = 0
}
+1 -1
View File
@@ -87,7 +87,7 @@ function bareOsEmitRaw(ctx, chunk) {
return false
}
var BARE_OS_HELP_BIN_SPACED = "arch awk baretop base32 base64 basename basenc btop bundlebee cat chgrp chmod chown cksum clear cmp comm cp crontab curl cut date dd df diff dir dircolors dirname du echo edit env exit expand expr factor false find fmt fold getconf getfacl git git-pear grep groups hdms head help holesail hostid hostname hrpc iconv id install join journalctl jq kernel-boot-diff kernel-doctor kernel-explain kernel-fsck kernel-home-snapshot kernel-manifest-validate kernel-preflight kernel-triage kill link ln logger login logname logout ls man md5sum mkdir mkfifo mktemp mount mv nano nice nl nohup nproc numfmt od oidc-publish openssl openssl paste patch pathcap-verify pathchk pear-runtime-matrix pkg-swarm-index pr printenv printf procstat ps pwd readlink realpath rev rm rmdir savevault sed seq setfacl sh sha1sum sha224sum sha256sum sha384sum sha512sum shuf sidecar sleep sort split ssh-keygen ssh-keygen sshd sshd stat sum sync systemctl tac tail tar tar tee test theme time timeout touch tr true truncate tsort tty ulimit umount uname unexpand uniq unlink uptime users vdir wc wget which who whoami xargs xattr yes"
var BARE_OS_HELP_BIN_SPACED = "arch awk baresay baretop base32 base64 basename basenc btop bundlebee cat chgrp chmod chown cksum clear cmp comm cp crontab curl cut date dd df diff dir dircolors dirname du echo edit env exit expand expr factor false find fmt fold getconf getfacl git git-pear grep groups hdms head help holesail hostid hostname hrpc iconv id install join journalctl jq kernel-boot-diff kernel-doctor kernel-explain kernel-fsck kernel-home-snapshot kernel-manifest-validate kernel-preflight kernel-triage kill link ln logger login logname logout ls man md5sum mkdir mkfifo mktemp mount mv nano nice nl nohup nproc numfmt od oidc-publish openssl openssl paste patch pathcap-verify pathchk pear-runtime-matrix pkg-swarm-index pr printenv printf procstat ps pwd readlink realpath rev rm rmdir savevault say sed seq setfacl sh sha1sum sha224sum sha256sum sha384sum sha512sum shuf sidecar sleep sort split ssh-keygen ssh-keygen sshd sshd stat sum sync systemctl tac tail tar tar tee test theme time timeout touch tr true truncate tsort tty ulimit umount uname unexpand uniq unlink uptime users vdir wc wget which who whoami xargs xattr yes"
async function run(ctx, argv) {
ctx.console.log(
'Bare OS — default user: guest | shell builtins: alias, barerc, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: ' +
+502
View File
@@ -0,0 +1,502 @@
/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
/** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */
function bareFormatModeString(mode, type) {
const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-'
const perm = mode & 0o777
const r = (bit) => (perm & bit ? 'r' : '-')
const w = (bit) => (perm & bit ? 'w' : '-')
const x = (bit) => (perm & bit ? 'x' : '-')
return (
typeChar +
r(0o400) +
w(0o200) +
x(0o100) +
r(0o040) +
w(0o020) +
x(0o010) +
r(0o004) +
w(0o002) +
x(0o001)
)
}
/** @param {number} mtimeMs @param {number} [nowMs] */
function bareFormatLsMtime(mtimeMs, nowMs) {
const now = nowMs != null ? nowMs : Date.now()
const d = new Date(mtimeMs)
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
]
const mon = months[d.getMonth()]
const day = String(d.getDate()).padStart(2, ' ')
const sixMo = 180 * 24 * 3600 * 1000
if (Math.abs(now - mtimeMs) > sixMo) {
const yr = String(d.getFullYear()).padStart(4, ' ')
return mon + ' ' + day + ' ' + yr
}
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
return mon + ' ' + day + ' ' + hh + ':' + mm
}
/** @param {number} size */
function barePosixBlocks(size) {
return Math.ceil(Number(size) / 512) || 0
}
/**
* Raw stdout for NUL/binary when **`process.stdout.write`** is missing.
* If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it.
* @param {Record<string, unknown>} ctx
* @param {string | Uint8Array} chunk
* @returns {boolean}
*/
function bareOsEmitRaw(ctx, chunk) {
if (typeof ctx.bareOsBinWrite === 'function') {
const b4 = ctx.b4a
const u8 =
typeof chunk === 'string'
? b4 && typeof b4.from === 'function'
? b4.from(chunk)
: new TextEncoder().encode(chunk)
: chunk
ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8))
return true
}
const w = globalThis.process?.stdout?.write
if (typeof w === 'function') {
w.call(globalThis.process.stdout, chunk)
return true
}
return false
}
/**
* Bubble + cow rendering for baresay (no imports; prepended before src/baresay.js).
*/
/** @param {string} text @param {number} width @param {boolean} noWrap */
function bareBaresayWrap(text, width, noWrap) {
const t = String(text || '').replace(/\r\n/g, '\n')
if (noWrap) {
const line = t.replace(/\n/g, ' ')
return line.length ? [line] : ['']
}
const out = []
const lim = Math.max(8, Math.min(200, width | 0)) || 40
for (const para of t.split('\n')) {
if (!para.trim()) {
out.push('')
continue
}
let rest = para.trim()
while (rest.length) {
if (rest.length <= lim) {
out.push(rest)
break
}
let cut = rest.lastIndexOf(' ', lim)
if (cut <= 0) cut = lim
const piece = rest.slice(0, cut).trimEnd()
if (!piece.length) {
out.push(rest.slice(0, lim))
rest = rest.slice(lim).trimStart()
} else {
out.push(piece)
rest = rest.slice(cut).trimStart()
}
}
}
return out.length ? out : ['']
}
/**
* @param {string[]} lines
* @param {{ think?: boolean, rainbow?: boolean }} opts
*/
function bareBaresayBalloon(lines, opts) {
const think = !!(opts && opts.think)
const rainbow = !!(opts && opts.rainbow)
const rows = lines.map((l) => String(l))
const n = rows.length
const longest = Math.max(...rows.map((r) => r.length), 2)
const padded = rows.map((r) => r.padEnd(longest))
/** @type {string[]} */
const mid = []
if (n === 1) {
const inner = think
? '( ' + padded[0] + ' )'
: '< ' + padded[0] + ' >'
mid.push(inner)
} else if (think) {
for (let i = 0; i < n; i++) {
mid.push('( ' + padded[i] + ' )')
}
} else {
for (let i = 0; i < n; i++) {
let L = '|'
let R = '|'
if (i === 0) {
L = '/'
R = '\\'
} else if (i === n - 1) {
L = '\\'
R = '/'
}
mid.push(L + ' ' + padded[i] + ' ' + R)
}
}
const topUnders = Math.max(0, mid[0].length - 2)
const botDash = Math.max(0, mid[mid.length - 1].length - 2)
/** @type {string[]} */
const out = []
out.push(' ' + '_'.repeat(topUnders))
if (rainbow) {
const ansi = bareBaresayRainbowCodes()
let k = 0
for (const line of mid) {
const c = ansi[k % ansi.length]
k++
out.push(c + line + '\x1b[0m')
}
} else {
for (const line of mid) out.push(line)
}
out.push(' ' + '-'.repeat(botDash))
return out
}
function bareBaresayRainbowCodes() {
return [
'\x1b[31m',
'\x1b[91m',
'\x1b[33m',
'\x1b[32m',
'\x1b[36m',
'\x1b[34m',
'\x1b[35m'
]
}
/** Extract cow body from .cow text (Perl heredoc or raw template). */
function bareBaresayExtractCow(raw) {
const s = String(raw || '').replace(/^\ufeff/, '')
const m = s.match(/\$the_cow\s*=\s*<<"EOC"\s*\r?\n([\s\S]*?)\r?\nEOC\b/)
if (m) return m[1].replace(/\r\n/g, '\n')
return s.replace(/\r\n/g, '\n').trimEnd() + '\n'
}
/**
* @param {string} template
* @param {string} eyes two chars
* @param {string} tongue one char
* @param {string} thoughts
*/
function bareBaresayFillCow(template, eyes, tongue, thoughts) {
const e = (eyes || 'oo').slice(0, 2).padEnd(2, 'o')
const tg = (tongue || ' ').slice(0, 1)
const th = thoughts || '\\'
return template
.replace(/\$thoughts/g, th)
.replace(/\$eyes/g, e)
.replace(/\$tongue/g, tg)
}
/** @param {string} flag */
function bareBaresayPreset(flag) {
switch (flag) {
case 'b':
return { eyes: '==', tongue: ' ' }
case 'd':
return { eyes: 'XX', tongue: 'U' }
case 'g':
return { eyes: '$$', tongue: ' ' }
case 'p':
return { eyes: '@@', tongue: ' ' }
case 's':
return { eyes: '**', tongue: ' ' }
case 't':
return { eyes: '--', tongue: ' ' }
case 'w':
return { eyes: '@@', tongue: ' ' }
case 'y':
return { eyes: '..', tongue: ' ' }
default:
return { eyes: 'oo', tongue: ' ' }
}
}
/**
* @param {string} message
* @param {string} cowBody filled lines (no vars)
* @param {{ width: number, noWrap: boolean, think?: boolean, rainbow?: boolean }} opts
*/
function bareBaresayCompose(message, cowBody, opts) {
const width = opts.width | 0
const wrapped = bareBaresayWrap(message, width, opts.noWrap)
const balloon = bareBaresayBalloon(wrapped, {
think: opts.think,
rainbow: opts.rainbow
})
const cowLines = cowBody.replace(/\s+$/, '').split('\n')
return balloon.concat(cowLines).join('\n') + '\n'
}
/** Minimal bear if no cowfile on disk (ASCII). */
function bareBaresayFallbackBearTemplate() {
return [
' $thoughts',
' $thoughts .--.--.',
' $thoughts ( @ @ )',
' $thoughts / $eyes \\',
' $thoughts | $tongue |',
' $thoughts \\______/',
' $thoughts ||',
' $thoughts ~~'
].join('\n')
}
async function run(ctx, argv) {
const raw0 = argv && argv.length ? argv[0] : 'baresay'
const slash = String(raw0).lastIndexOf('/')
const prog =
slash >= 0 ? String(raw0).slice(slash + 1) || 'baresay' : String(raw0)
/** @param {string} msg */
const usage = (msg) => {
if (msg) ctx.console.error(prog + ': ' + msg)
ctx.console.error(
'usage: ' +
prog +
' [-h] [-l] [-n] [-W COLS] [-f name|path] [-e XX] [-T C] [-bdgpstwy] [--think] [--rainbow] [-R|--random] [MESSAGE...]\n' +
' ' +
prog +
' reads stdin when no MESSAGE and stdin is non-empty.'
)
ctx.exitCode = 2
}
let width = 40
let noWrap = false
let cowKey = 'bear'
let cowPath = ''
let listOnly = false
let eyes = 'oo'
let tongue = ' '
let think = false
let rainbow = false
let randomPick = false
/** @type {string[]} */
const operands = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '--') {
operands.push(...argv.slice(i + 1))
break
}
if (a === '-h' || a === '--help') {
usage('')
return
}
if (a === '-l') {
listOnly = true
continue
}
if (a === '-n') {
noWrap = true
continue
}
if (a === '--think' || a === '-th') {
think = true
continue
}
if (a === '--rainbow') {
rainbow = true
continue
}
if (a === '-R' || a === '--random') {
randomPick = true
continue
}
if (a === '-W' || a === '--width') {
const v = argv[++i]
const n = v != null ? parseInt(String(v), 10) : NaN
if (!Number.isFinite(n) || n < 1) {
usage('invalid width')
return
}
width = n
continue
}
if (a.startsWith('-W') && a.length > 2) {
const n = parseInt(a.slice(2), 10)
if (!Number.isFinite(n) || n < 1) {
usage('invalid width')
return
}
width = n
continue
}
if (a === '-f' || a === '--file') {
const v = argv[++i]
if (v == null) {
usage('missing cow file')
return
}
if (v.startsWith('/') || v.startsWith('.')) cowPath = v
else cowKey = v.replace(/\.cow$/i, '')
continue
}
if (a.startsWith('-f') && a.length > 2) {
const v = a.slice(2)
if (v.startsWith('/') || v.startsWith('.')) cowPath = v
else cowKey = v.replace(/\.cow$/i, '')
continue
}
if (a === '-e') {
const v = argv[++i]
if (v == null || String(v).length < 1) {
usage('missing eyes')
return
}
eyes = String(v).slice(0, 2).padEnd(2, ' ')
continue
}
if (a.startsWith('-e') && a.length > 2) {
eyes = a.slice(2, 4).padEnd(2, ' ')
continue
}
if (a === '-T') {
const v = argv[++i]
tongue = v != null && String(v).length ? String(v).slice(0, 1) : ' '
continue
}
if (a.startsWith('-T') && a.length > 2) {
tongue = a.slice(2, 3) || ' '
continue
}
{
const mm = a.match(/^-([bdgpstwy])$/)
if (mm) {
const p = bareBaresayPreset(mm[1])
eyes = p.eyes
tongue = p.tongue
continue
}
}
if (a.startsWith('-') && a.length > 1) {
usage('unknown option: ' + a)
return
}
operands.push(a)
}
const vfs = ctx.vfs
const b4 = ctx.b4a
/** @returns {Promise<string[]>} */
async function listShareCows() {
const dir = '/share/baresay'
/** @type {string[]} */
const names = []
if (!vfs || typeof vfs.readdir !== 'function') return names
try {
for await (const n of vfs.readdir(dir)) {
if (typeof n === 'string' && n.endsWith('.cow')) {
names.push(n.replace(/\.cow$/i, ''))
}
}
} catch {
/* ignore */
}
return names.sort()
}
if (listOnly) {
const names = await listShareCows()
for (const n of names) ctx.console.log(n)
ctx.exitCode = 0
return
}
/** @type {string} */
let message = operands.join(' ')
if (!message.trim()) {
message = bareStdin(ctx).replace(/\r\n/g, '\n').trimEnd()
}
if (randomPick) {
const names = await listShareCows()
const pool = names.length ? names : ['bear']
cowKey = pool[Math.floor(Math.random() * pool.length)]
cowPath = ''
}
/** @returns {Promise<string | null>} */
async function readCowAt(path) {
if (!vfs || typeof vfs.readFile !== 'function') return null
try {
const buf = await vfs.readFile(path)
if (!buf) return null
return b4 ? b4.toString(buf) : String(buf)
} catch {
return null
}
}
let rawCow = ''
if (cowPath) {
rawCow = (await readCowAt(cowPath)) || ''
} else {
const p = '/share/baresay/' + cowKey + '.cow'
rawCow = (await readCowAt(p)) || ''
}
if (!rawCow.trim()) {
if (cowKey === 'bear' && !cowPath) {
rawCow = bareBaresayFallbackBearTemplate()
} else {
ctx.console.error(
prog +
': cow file not found: ' +
(cowPath || '/share/baresay/' + cowKey + '.cow')
)
ctx.exitCode = 1
return
}
}
const template = bareBaresayExtractCow(rawCow)
const filled = bareBaresayFillCow(template, eyes, tongue, '\\')
const out = bareBaresayCompose(message, filled, {
width,
noWrap,
think,
rainbow
})
const w = globalThis.process?.stdout?.write
if (typeof w === 'function') {
w.call(globalThis.process.stdout, out)
} else {
ctx.console.log(out.replace(/\n$/, ''))
}
ctx.exitCode = 0
}
+9 -1
View File
@@ -1,7 +1,7 @@
{
"schema": 2,
"profileId": "bare-os-posix-like",
"generatedAt": "2026-04-21T23:08:29.149Z",
"generatedAt": "2026-04-21T23:27:43.821Z",
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
"commandIndex": [
{
@@ -12,6 +12,10 @@
"name": "awk",
"tier": "tier1_bin"
},
{
"name": "baresay",
"tier": "tier1_bin"
},
{
"name": "baretop",
"tier": "tier1_bin"
@@ -420,6 +424,10 @@
"name": "savevault",
"tier": "tier1_bin"
},
{
"name": "say",
"tier": "tier1_bin"
},
{
"name": "sed",
"tier": "tier1_bin"
+3 -1
View File
@@ -1,9 +1,10 @@
{
"schema": 1,
"atMs": 1776812909149,
"atMs": 1776814063820,
"commands": [
"arch",
"awk",
"baresay",
"baretop",
"base32",
"base64",
@@ -106,6 +107,7 @@
"rm",
"rmdir",
"savevault",
"say",
"sed",
"seq",
"setfacl",
+9
View File
@@ -0,0 +1,9 @@
$the_cow = <<"EOC";
$thoughts
$thoughts .--. Bare OS
$thoughts | ( $eyes ) |
$thoughts | ( $tongue ) |
$thoughts | --- |
$thoughts `-- mm --'
$thoughts ||
EOC
+8
View File
@@ -0,0 +1,8 @@
$the_cow = <<"EOC";
$thoughts
$thoughts /|
$thoughts _/ |__
$thoughts > $eyes $tongue <
$thoughts ^------^
$thoughts ~~ ~~
EOC
+8
View File
@@ -0,0 +1,8 @@
$the_cow = <<"EOC";
$thoughts
$thoughts /\\ /\\
$thoughts ( $eyes )
$thoughts -- $tongue--
$thoughts ||||
$thoughts ^^^^
EOC
+7
View File
@@ -0,0 +1,7 @@
$the_cow = <<"EOC";
$thoughts
$thoughts .
$thoughts ,-( $eyes )-.
$thoughts '--- $tongue ---'
$thoughts Pear / Bare
EOC
+9
View File
@@ -0,0 +1,9 @@
$the_cow = <<"EOC";
$thoughts
$thoughts +------+
$thoughts | $eyes |
$thoughts | $tongue |
$thoughts +------+
$thoughts ||||
$thoughts / \\
EOC
+8
View File
@@ -0,0 +1,8 @@
$the_cow = <<"EOC";
$thoughts
$thoughts .--.
$thoughts |$eyes|
$thoughts _/ $tongue -\\_
$thoughts / \\
$thoughts /_______\\
EOC
+7
View File
@@ -0,0 +1,7 @@
$the_cow = <<"EOC";
$thoughts
$thoughts ____
$thoughts / $eyes \\
$thoughts --' $tongue '--
$thoughts \\_______/
EOC
File diff suppressed because one or more lines are too long