UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

183 lines • 7.05 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 { 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