Files
bare-operating-system/packages/bare-os-booter/lib/shell/shell-runtime.js
T
2026-08-18 18:11:28 -04:00

147 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Shell runtime helpers: exit-status env, pipeline caps, child merge, audit.
*/
/** Env key mirroring last command exit status (POSIX `$?` parity). */
export const BARE_OS_EXIT_STATUS_ENV = 'BARE_OS_EXIT_STATUS'
/**
* Mirror `ctx.exitCode` into `ctx.vfs.env` so kernels and `echo $?` see last status.
* @param {Record<string, unknown>} ctx
*/
export function syncBareOsExitStatusEnv(ctx) {
const env = ctx.vfs?.env
if (!env || typeof env !== 'object') return
const n = Number(ctx.exitCode)
env[BARE_OS_EXIT_STATUS_ENV] = String(Number.isFinite(n) ? n : 0)
}
/** Default caps for simulated pipeline capture (`console.log` between stages). */
export const DEFAULT_PIPELINE_MAX_STAGES = 32
export const DEFAULT_PIPELINE_MAX_CAPTURE_BYTES = 2 * 1024 * 1024
export const DEFAULT_PIPELINE_MAX_CAPTURE_LINES = 50000
/**
* Resolved simulated pipeline limits for the current `vfs.env` / session env.
* @param {Record<string, string> | null | undefined} env
*/
export function getBareOsPipelineLimits(env) {
const o = env && typeof env === 'object' ? env : {}
const parse = (key, def) => {
const v = o[key]
if (v == null || v === '') return def
const n = Number.parseInt(String(v), 10)
return Number.isFinite(n) && n > 0 ? n : def
}
/** Upper bounds on simulated capture (after streaming multiplier); tunable for high-RAM hosts. */
const absMaxBytes = parse(
'BARE_OS_PIPELINE_ABS_MAX_BYTES',
512 * 1024 * 1024
)
const absMaxLines = parse('BARE_OS_PIPELINE_ABS_MAX_LINES', 2_000_000)
const streamOn =
o.BARE_OS_SHELL_STREAMING === '1' || o.BARE_OS_SHELL_STREAMING === 'true'
const multRaw = Number.parseFloat(
String(o.BARE_OS_SHELL_STREAMING_MULT || '4')
)
const mult =
streamOn && Number.isFinite(multRaw) && multRaw > 1
? Math.min(multRaw, 16)
: 1
const baseBytes = parse(
'BARE_OS_PIPELINE_MAX_BYTES',
DEFAULT_PIPELINE_MAX_CAPTURE_BYTES
)
const baseLines = parse(
'BARE_OS_PIPELINE_MAX_LINES',
DEFAULT_PIPELINE_MAX_CAPTURE_LINES
)
const effectiveBytes = Math.floor(baseBytes * mult)
const effectiveLines = Math.floor(baseLines * mult)
return {
maxStages: parse(
'BARE_OS_PIPELINE_MAX_STAGES',
DEFAULT_PIPELINE_MAX_STAGES
),
maxBytes: Math.min(effectiveBytes, absMaxBytes),
maxLines: Math.min(effectiveLines, absMaxLines),
streamingMultiplier: mult,
/** True when `BARE_OS_SHELL_STREAMING` relaxes caps via multiplier. */
streamingEnabled: streamOn,
/** Parsed `BARE_OS_PIPELINE_MAX_*` before multiplier (for operator snapshots). */
baseMaxBytes: baseBytes,
baseMaxLines: baseLines,
/** Hard ceilings after multiplier (`BARE_OS_PIPELINE_ABS_MAX_*`; defaults 512MiB / 2M lines). */
absCapBytes: absMaxBytes,
absCapLines: absMaxLines
}
}
/**
* When `suspend-job` sets `stopped` on a background entry, yield between statements
* until `fg` / `bg` clears it (cooperative logical job control; no host SIGTSTP).
* @param {{ stopped?: boolean }} entry
*/
export function waitWhileShellJobStopped(entry) {
if (!entry || !entry.stopped) return Promise.resolve()
return new Promise((resolve) => {
const id = setInterval(() => {
if (!entry.stopped) {
clearInterval(id)
resolve(undefined)
}
}, 10)
})
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} event
* @param {Record<string, unknown>} payload
*/
export function appendShellAuditEvent(ctx, event, payload = {}) {
if (!Array.isArray(ctx.shellAuditEvents)) ctx.shellAuditEvents = []
ctx.shellAuditEvents.push({
schema: 1,
ts: Date.now(),
event,
...payload
})
}
/**
* @param {Record<string, unknown>} ctx
* @param {Record<string, unknown>} childCtx
*/
export function mergeChildExitCode(ctx, childCtx) {
if (childCtx.exitCode !== undefined && childCtx.exitCode !== null) {
ctx.exitCode = childCtx.exitCode
}
}
/**
* @param {unknown} identity
* @returns {identity is { state: unknown, publicKey: unknown, secretKey: unknown }}
*/
export function isMergeableIdentitySession(identity) {
if (!identity || typeof identity !== 'object') return false
return (
Object.prototype.hasOwnProperty.call(identity, 'state') &&
Object.prototype.hasOwnProperty.call(identity, 'publicKey') &&
Object.prototype.hasOwnProperty.call(identity, 'secretKey')
)
}
/**
* Merge shell child-command side effects we intentionally allow to flow back.
* Today this includes exit status and identity session state.
* @param {Record<string, unknown>} ctx
* @param {Record<string, unknown>} childCtx
*/
export function mergePipelineChildCtx(ctx, childCtx) {
mergeChildExitCode(ctx, childCtx)
if (isMergeableIdentitySession(childCtx.identity)) {
ctx.identity = /** @type {Record<string, unknown>} */ (childCtx.identity)
}
}