UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

157 lines • 7.03 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { IOResult } from "../../../../io/types.js"; import { encodeText } from "../../../../shell/bytes.js"; import { SET_OPTION_DEFAULTS, SET_OPTION_NAMES } from "../../../../shell/constants.js"; import { parseOptionWord } from "../../../../shell/options.js"; import { concat } from "../../../../utils/bytes.js"; import { singleQuote } from "../../../../utils/quote.js"; import { compareCodePoints } from "../../../../utils/sort.js"; import { setPositionalParams, visibleEnv } from "../../../session/state.js"; import { ExecutionNode } from "../../../types.js"; import { SET_QUOTED_CHARS } from "../constants.js"; import { CONTROL_RE } from "../declare/constants.js"; import { bashDeclareQuote } from "../declare/declare.js"; import { builtinError, builtinUsage } from "../shared.js"; /** * A value as bash's bare `set` prints it. * * `$'...'` when it holds a control character, the form `declare -p` * writes; single quotes when the shell would read a character in it * specially; bare otherwise. GNU prints `IFS=$' \t\n'`, `x='a b'` and * `y=a,b`. */ function listedValue(value) { if (CONTROL_RE.test(value)) return bashDeclareQuote(value); let quoted = value.startsWith('#'); for (let i = 0; i < value.length && !quoted; i++) { const ch = value.charAt(i); quoted = SET_QUOTED_CHARS.has(ch) || (ch === '~' && (i === 0 || '=:'.includes(value.charAt(i - 1)))); } return quoted ? singleQuote(value) : value; } /** * The first option letter of a `set` line that names no option, with the * sign it was given under, null when there is none. bash reads every option * word before it acts on any, so one such letter refuses the whole line and * nothing it sets is set. */ function unknownLetter(args) { let i = 0; while (i < args.length && args[i] !== '--') { const tok = args[i] ?? ''; const word = parseOptionWord(tok, args[i + 1] ?? null); if (word === null) return null; if (word.other !== '') return `${tok[0] ?? ''}${word.other[0] ?? ''}`; i += word.consumed; } return null; } export function handleSet(args, session, callStack = null) { if (args.length === 0) { const entries = Object.entries(visibleEnv(session)); entries.sort(([a], [b]) => compareCodePoints(a, b)); const lines = entries.map(([k, v]) => `${k}=${listedValue(v)}`); const out = encodeText(`${lines.join('\n')}\n`); return [out, new IOResult(), new ExecutionNode({ command: 'set', exitCode: 0 })]; } const bad = unknownLetter(args); if (bad !== null) { const err = concat([ builtinError('set', `${bad}: invalid option`), encodeText(builtinUsage('set')), ]); return [ null, new IOResult({ exitCode: 2, stderr: err }), new ExecutionNode({ command: 'set', exitCode: 2, stderr: err }), ]; } let i = 0; while (i < args.length) { const tok = args[i] ?? ''; if (tok === '--') { setPositionalParams(session, callStack, args.slice(i + 1)); return [null, new IOResult(), new ExecutionNode({ command: 'set', exitCode: 0 })]; } // `-o` and `+o` with nothing after them print the option table // instead of setting anything, in two different spellings: `-o` // as a padded name/value column, `+o` as lines that can be fed // back to `set`. Both are checked before the option grammar, // since a bare `-o` is not a setting. if ((tok === '-o' || tok === '+o') && i + 1 >= args.length) { const out = optionListing(session, tok === '+o'); return [out, new IOResult(), new ExecutionNode({ command: 'set', exitCode: 0 })]; } const word = parseOptionWord(tok, args[i + 1] ?? null); if (word === null) { setPositionalParams(session, callStack, args.slice(i)); break; } for (const [option, enable] of word.settings) { // `-o` takes a name rather than a letter, and a name bash does not // have is the one thing it refuses: exit 2, and the settings already // applied stay applied while the rest of the line is dropped. // Without this a typo — or an option mirage has yet to wire, as // `physical` once was — reads as success. if (!SET_OPTION_NAMES.has(option)) { const err = encodeText(`bash: set: ${option}: invalid option name\n`); return [ null, new IOResult({ exitCode: 2, stderr: err }), new ExecutionNode({ command: 'set', exitCode: 2, stderr: err }), ]; } session.shellOptions[option] = enable; } // Every letter bash has names an option here, the ones mirage does not // act on (`-a`, `-B`, `-H`) included, so a script can still turn those // on. A nested shell answers a letter bash lacks differently, which is // why the grammar hands it back. i += word.consumed; } return [null, new IOResult(), new ExecutionNode({ command: 'set', exitCode: 0 })]; } /** * Render `set -o` or `set +o` with no name after it. * * GNU 5.2.37 prints every option it knows, alphabetically, whether or * not the shell has been told anything about it: `-o` as a name padded * to 15 columns, a tab, then `on`/`off`, and `+o` as `set -o NAME` / * `set +o NAME` lines a script can source back. `interactive-comments` * is longer than the padding and simply overflows it, which is GNU's * own `%-15s\t%s` and not a special case. */ function optionListing(session, plus) { const lines = []; for (const [name, byDefault] of SET_OPTION_DEFAULTS) { const on = session.shellOptions[name] ?? byDefault; if (plus) { lines.push(`set ${on ? '-' : '+'}o ${name}`); } else { lines.push(`${name.padEnd(15)}\t${on ? 'on' : 'off'}`); } } return encodeText(`${lines.join('\n')}\n`); } /** The `set` arm. */ export function setBuiltin(call) { return Promise.resolve(handleSet([...call.argv.args], call.context.session, call.callStack)); } //# sourceMappingURL=set.js.map