UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

153 lines • 7.04 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 { sameLine } from "../../shell/helpers.js"; import { NodeType as NT } from "../../shell/types.js"; import { OPENERS, WORDS } from "./constants.js"; function descendants(node) { return [node, ...node.children.flatMap(descendants)]; } /** The rows of blanks among `rows`, each an entry no command starts. */ function blanks(rows) { return rows .split('\n') .filter((row) => row !== '' && row.trim() === '') .map((text) => ({ text, at: null })); } /** * The entries bash's history adds for an input it reads interactively: * one per command it reads, a command and what else is on the line it * ends on. A command spanning lines is one entry, its lines joined as * `cmdhist` joins them: a break inside a quoted word, a substitution or * a heredoc stays, a backslash's goes, and any other is a `; ` or, after * a token a command reads on after (`then`, `|`, `{`, a case pattern's * `)`), a space. A comment line inside a command is dropped, a line of * blanks is an entry of its own and an empty line none. Pinned against * Debian Bash 5.2.37 with `set +H`. Mirrors Python's history_entries. * * @param root the parsed input. */ export function historyEntries(root) { const source = root.sourceText ?? root.text; const base = root.startIndex ?? 0; const typed = (index) => root.offsets?.[index] ?? index; const breaks = new Map(); for (let i = 0; i < root.text.length; i++) if (root.text[i] === '\n') breaks.set(typed(base + i), base + i); const groups = []; for (const child of root.children) { if (child.isNamed !== true) continue; const group = groups[groups.length - 1]; const last = group?.[group.length - 1]; if (group !== undefined && last !== undefined && sameLine(root, last, child)) group.push(child); else groups.push([child]); } const entries = []; let read = 0; for (const group of groups) { const first = group[0]; if (first === undefined) continue; const nodes = group.flatMap(descendants); const folded = nodes.filter((node) => node.heredoc !== undefined); const inside = (node) => folded.some((doc) => (doc.startIndex ?? 0) <= (node.startIndex ?? 0) && (node.endIndex ?? 0) <= (doc.endIndex ?? 0)); const start = source.lastIndexOf('\n', typed(first.startIndex ?? 0) - 1) + 1; const end = Math.max(...nodes .filter((node) => node.children.length === 0 && !inside(node)) .map((node) => typed((node.endIndex ?? 0) - 1) + 1), ...folded.map((doc) => doc.heredoc?.end ?? 0)); const found = source.indexOf('\n', end - 1); const stop = found < 0 ? source.length : found; entries.push(...blanks(source.slice(read, start))); read = stop + 1; if (nodes.some((node) => node.type === NT.ERROR)) { entries.push({ text: source.slice(start, stop), at: typed(first.startIndex ?? 0) }); continue; } const comments = new Set(nodes.filter((node) => node.type === NT.COMMENT).map((node) => typed(node.startIndex ?? 0))); const within = (index) => nodes.some((node) => WORDS.has(node.type) && (node.startIndex ?? 0) < index && index < (node.endIndex ?? 0)); const joint = (at) => { const docs = folded.filter((doc) => doc.heredoc !== undefined && doc.heredoc.wordEnd <= at && at < doc.heredoc.end); if (docs.length > 0) { if (!docs.every((doc) => at === (doc.heredoc?.end ?? 0) - 1)) return '\n'; return docs.some((doc) => within(doc.startIndex ?? 0)) ? '\n\n' : '\n '; } const index = breaks.get(at); if (index === undefined) { let before = base; for (let i = base; i < base + root.text.length; i++) if (typed(i) < at - 1) before = i; return within(before) || source[at - 1] !== '\\' ? '\n' : ''; } if (within(index)) return '\n'; if (nodes.some((node) => node.type === NT.ARRAY && (node.startIndex ?? 0) < index && index < (node.endIndex ?? 0))) return ' '; let leaf; for (const node of nodes) if (node.children.length === 0 && (node.endIndex ?? 0) <= index && (leaf === undefined || (node.endIndex ?? 0) >= (leaf.endIndex ?? 0))) leaf = node; if (leaf === undefined) return '; '; if (leaf.type === NT.COMMENT) return '\n'; if (leaf.isNamed !== true && OPENERS.has(leaf.type)) return ' '; const pattern = leaf.parent?.type; if (leaf.type === ')' && (pattern === NT.CASE_ITEM || pattern === NT.FUNCTION_DEFINITION)) return ' '; return '; '; }; let text = ''; let blank = false; let comment = false; for (let at = start;;) { const next = source.indexOf('\n', at); const ends = next < 0 || next > stop ? stop : next; const row = source.slice(at, ends); if (at === start) text = row; else { const between = joint(at - 1); const word = between.startsWith('\n'); if (!word && comments.has(at + row.length - row.trimStart().length)) comment = true; else { const joined = comment ? '\n' : blank && !word ? ' ' : between; text = (between === '' ? text.slice(0, -1) : text) + joined + row; blank = !word && row.trim() === ''; comment = false; } } if (ends >= stop) break; at = ends + 1; } if (folded.some((doc) => (doc.heredoc?.end ?? 0) - 1 === stop)) text += '\n'; entries.push({ text, at: typed(first.startIndex ?? 0) }); } entries.push(...blanks(source.slice(read))); return entries; } //# sourceMappingURL=entries.js.map