@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
157 lines • 7.03 kB
JavaScript
// ========= 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