@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
183 lines • 7.05 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 { runAsProgram } from "../../../../context/session_context.js";
import { encodeText } from "../../../../shell/bytes.js";
import { shellJoin } from "../../../../shell/join.js";
import { ownRecord, varsFromEnv } from "../../../session/session.js";
import { envSnapshot } from "../../../session/state.js";
import { ExecutionNode } from "../../../types.js";
import { fail, ok } from "../shared.js";
import { ENV_HELP_HINT } from "./constants.js";
function envError(message) {
return fail('env', `${message}\n${ENV_HELP_HINT}`, 125);
}
/**
* What `env` runs and the environment it runs it in, read off its words:
* the command's words, the environment in `environ` order, whether `-i`
* cleared it and whether `-0` was given; or the message for words env
* refuses. Mirrors the Python `env_child`.
*/
export function envChild(args, session) {
let ignoreEnv = false;
let nullSep = false;
const unset = [];
let i = 0;
while (i < args.length) {
const tok = args[i] ?? '';
if (tok === '--') {
i += 1;
break;
}
if (tok === '-i' || tok === '--ignore-environment') {
ignoreEnv = true;
i += 1;
continue;
}
if (tok === '-0' || tok === '--null') {
nullSep = true;
i += 1;
continue;
}
if (tok === '-') {
// GNU: "a mere - implies -i".
ignoreEnv = true;
i += 1;
continue;
}
if (tok === '--unset') {
if (i + 1 >= args.length)
return "env: option '--unset' requires an argument";
unset.push(args[i + 1] ?? '');
i += 2;
continue;
}
if (tok.startsWith('--unset=')) {
unset.push(tok.slice('--unset='.length));
i += 1;
continue;
}
if (tok.startsWith('--'))
return `env: unrecognized option '${tok}'`;
if (tok.startsWith('-') && tok.length > 1) {
let j = 1;
let consumedNext = false;
while (j < tok.length) {
const ch = tok[j];
if (ch === 'i') {
ignoreEnv = true;
}
else if (ch === '0') {
nullSep = true;
}
else if (ch === 'u') {
const rest = tok.slice(j + 1);
if (rest !== '') {
unset.push(rest);
}
else if (i + 1 < args.length) {
unset.push(args[i + 1] ?? '');
consumedNext = true;
}
else {
return "env: option requires an argument -- 'u'";
}
break;
}
else {
return `env: invalid option -- '${ch ?? ''}'`;
}
j += 1;
}
i += consumedNext ? 2 : 1;
continue;
}
break;
}
const dropSet = new Set(unset);
const source = ignoreEnv ? {} : envSnapshot(session);
const base = ownRecord();
for (const [k, v] of Object.entries(source)) {
if (!dropSet.has(k))
base[k] = v;
}
while (i < args.length && (args[i] ?? '').includes('=') && !(args[i] ?? '').startsWith('=')) {
const tok = args[i] ?? '';
const eq = tok.indexOf('=');
base[tok.slice(0, eq)] = tok.slice(eq + 1);
i += 1;
}
return [args.slice(i), base, ignoreEnv, nullSep];
}
/**
* The session's variables as `env`'s command sees them. Only the scalars
* are replaced: arrays were never part of the env the old two-container
* store swapped, and bash does not put one in a child's environment
* either. A still-unfetched managed entry is a scalar in waiting, so it
* is replaced too: surviving the swap would let the inner line fetch and
* read a name `-i` or `-u` just cleared.
*
* Built through `varsFromEnv`, the one conversion from an embedder's
* process environment to session records, so the export attribute is
* stamped in exactly one place rather than restated here. Seeded plain,
* `env -i FOO=bar printenv FOO` printed nothing, since the process view a
* command reads carries only exported names. Mirrors the Python
* `env_vars`.
*/
export function envVars(session, base) {
const swapped = ownRecord();
for (const [name, v] of Object.entries(session.vars)) {
if (typeof v.value !== 'string' && v.managed === undefined)
swapped[name] = v;
}
Object.assign(swapped, varsFromEnv(base));
return swapped;
}
export async function handleEnv(executeFn, args, session, stdin = null) {
const child = envChild(args, session);
if (typeof child === 'string')
return envError(child);
const [command, base, ignoreEnv, nullSep] = child;
if (command.length > 0 && nullSep) {
return envError('env: cannot specify --null (-0) with command');
}
if (command.length === 0) {
const sep = nullSep ? '\0' : '\n';
const out = encodeText(Object.entries(base)
.map(([k, v]) => `${k}=${v}${sep}`)
.join(''));
return ok('env', out);
}
const saved = session.vars;
session.vars = envVars(session, base);
// The command gets its own copy of the functions `export -f` marked,
// empty under `-i`: they are part of bash's environment, as its
// `BASH_FUNC_NAME%%` entries.
const exported = session.exportedFunctions;
session.exportedFunctions = ignoreEnv ? new Set() : new Set(exported);
try {
// env execs its command, so a builtin that is also a program answers as
// the program.
const io = await runAsProgram(session, () => executeFn(shellJoin(command), { sessionId: session.sessionId, stdin }));
return [io.stdout, io, new ExecutionNode({ command: 'env', exitCode: io.exitCode })];
}
finally {
session.vars = saved;
session.exportedFunctions = exported;
}
}
/** The `env` arm. */
export async function envBuiltin(call) {
return handleEnv(call.executeFn, [...call.argv.args], call.context.session, call.stdin);
}
//# sourceMappingURL=env.js.map