UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

245 lines • 10.7 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 { isEisdir, isFsError } from "../../../errors/fs.js"; import { fsErrorLine } from "../../../errors/render.js"; import { IOResult } from "../../../io/types.js"; import { openStreamSync, runStream } from "../../../rs/stream.js"; import { byteView, decodeText, encodeText } from "../../../shell/bytes.js"; import { concat } from "../../../utils/bytes.js"; import { specOf } from "../../spec/builtins.js"; import { FlagView } from "../../spec/flag_view.js"; import {} from "../../spec/types.js"; import { wordRefusalError } from "../../spec/usage.js"; import { matchWord } from "../../spec/word_choice.js"; import { guardInput } from "../utils/limit.js"; import { formatRecords } from "../utils/output.js"; import { isStdin, resolveSource, stdinStream } from "../utils/stream.js"; const decoder = new TextDecoder(); // The --total words in GNU's order, which is both the accepted set and what // `--total=x` lists back. No aliases, so one per line. const TOTAL_ARGS = ['auto', 'always', 'only', 'never']; export function parseFlags(bag) { const fl = new FlagView(bag, specOf('wc')); // `--total=` is NOT the default: GNU reads the empty word as a prefix of // every candidate and answers `ambiguous argument ''` (exit 1), which is // what the shared renderer words. Python read it as `auto` and exited 0, // the one py/ts split at this slot. `asStr` keeps the empty string (it // tests `typeof === 'string'`), so the empty word still reaches the // renderer rather than defaulting. const rawTotal = fl.asStr('total') ?? 'auto'; const match = matchWord(rawTotal, TOTAL_ARGS); if (!match.matched) { return (wordRefusalError('wc', '--total', rawTotal, TOTAL_ARGS, undefined, match.kind).message + '\n'); } return { lines: fl.asBool('lines'), words: fl.asBool('words'), bytes: fl.asBool('bytes'), chars: fl.asBool('chars'), maxLineLength: fl.asBool('max_line_length'), total: match.word, }; } // GNU wc's counts, streamed through mirage-rs. Only -l and -c count without // decoding; any other count decodes UTF-8, and a byte that decodes to no // character is part of a word but no character and no column. Mirrors // Python's wc. async function wc(source, ctx, flags) { const config = { lines: flags.lines, words: flags.words, bytes: flags.bytes, chars: flags.chars, max_line_length: flags.maxLineLength, }; let report = ''; for await (const part of runStream('wc', config, guardInput(source, ctx))) { report += decoder.decode(part); } const [lines = 0, words = 0, bytes = 0, chars = 0, maxLineLength = 0] = report .split(' ') .map((value) => Number(value)); return { lines, words, bytes, chars, maxLineLength }; } function selectedValues(counts, flags) { const selected = flags.lines || flags.words || flags.bytes || flags.chars || flags.maxLineLength; if (!selected) return [counts.lines, counts.words, counts.bytes]; const values = []; if (flags.lines) values.push(counts.lines); if (flags.words) values.push(counts.words); if (flags.chars) values.push(counts.chars); if (flags.bytes) values.push(counts.bytes); if (flags.maxLineLength) values.push(counts.maxLineLength); return values; } function addCounts(total, counts) { total.lines += counts.lines; total.words += counts.words; total.bytes += counts.bytes; total.chars += counts.chars; total.maxLineLength = Math.max(total.maxLineLength, counts.maxLineLength); } /** * GNU wc's column width, which it takes from the operands. * * One operand, or stdin, shown with one count prints unpadded. Otherwise the * width is the digits of the regular files' total size, and at least 7 once * any operand is a stream or a directory, whose size GNU cannot know * (coreutils 9.7). A redirected file reaches mirage as a stream, so * `wc < file` pads to 7 where GNU pads to the file's size. `sizes` holds, per * operand that opened, a regular file's size or null for a stream or a * directory; `operands` counts the operands given, 1 for stdin. Mirrors * Python's number_width. */ export function numberWidth(sizes, operands, counts) { if (operands <= 1 && counts === 1) return 1; let total = 0; for (const size of sizes) total += size ?? 0; const width = String(total).length; return sizes.includes(null) ? Math.max(width, 7) : width; } export function formatWcLines(rows, width = null) { const engine = openStreamSync('wc', { report: rows.map((row) => ({ values: row.values, label: row.label === null ? null : byteView(row.label), })), width, }); try { const output = [engine.finish()]; while (engine.busy()) output.push(engine.drain()); return decodeText(concat(output)).split('\n').slice(0, -1); } finally { engine.close(); } } // Append the `total` row --total asks for and render the report. `only` // prints the grand total alone and unlabeled; `auto` prints one when more // than one operand was counted. Returns null when there is nothing to print. export function formatCountRows(rows, totalValues, operandCount, total, width = null) { if (total === 'only') return encodeText(`${totalValues.join(' ')}\n`); const out = [...rows]; if (total === 'always' || (total === 'auto' && operandCount > 1)) { out.push({ values: totalValues, label: 'total' }); } if (out.length === 0) return null; return formatRecords(formatWcLines(out, width)); } /** How many columns a row shows under these flags. */ export function shownCounts(flags) { return selectedValues({ lines: 0, words: 0, bytes: 0, chars: 0, maxLineLength: 0 }, flags).length; } /** * Run wc over resolved operands, GNU semantics. Lines counted alone are asked * of `lineCount` first, so a mount that can count a file's lines on its own * (a `COUNT(*)` on a table) does not send it; null, or no `lineCount`, reads * the file. A row counted that way has no byte size, so the columns are as * wide as the printed numbers. Bytes counted alone are asked of `sizeOf` * first, as GNU takes a regular file's size from its stat. Mirrors Python's * `wc_generic` and `format_multi`. */ export async function wcGeneric(paths, texts, ctx, stream, lineCount, sizeOf) { stream = stdinStream(stream, ctx.stdin); const parsed = parseFlags(ctx.flags); if (typeof parsed === 'string') { return [null, new IOResult({ exitCode: 1, stderr: encodeText(parsed) })]; } if (paths.length > 0) { const rows = []; const records = []; const sizes = []; const total = { lines: 0, words: 0, bytes: 0, chars: 0, maxLineLength: 0 }; let err = ''; const counted = parsed.lines && !(parsed.words || parsed.bytes || parsed.chars || parsed.maxLineLength) && lineCount !== undefined ? lineCount : null; const sized = parsed.bytes && !(parsed.lines || parsed.words || parsed.chars || parsed.maxLineLength) && sizeOf !== undefined ? sizeOf : null; let pushed = false; for (const [run, p] of paths.entries()) { let counts; try { const found = counted === null || isStdin(p) ? null : await counted(p); const size = sized === null || isStdin(p) ? null : await sized(p); if (found !== null) { counts = { lines: found, words: 0, bytes: 0, chars: 0, maxLineLength: 0 }; pushed = true; } else if (size !== null) { counts = { lines: 0, words: 0, bytes: size, chars: 0, maxLineLength: 0 }; } else { counts = await wc(stream(p), ctx, parsed); } } catch (e) { if (!isFsError(e)) throw e; err += fsErrorLine('wc', p, e); // GNU opens a directory and fails only to read it, so it prints a row // of zeros beside the error and pads as for a stream. if (isEisdir(e)) { const zero = { lines: 0, words: 0, bytes: 0, chars: 0, maxLineLength: 0 }; rows.push({ values: selectedValues(zero, parsed), label: p.rawPath }); records.push({ path: p, run, values: selectedValues(zero, parsed) }); sizes.push(null); } continue; } rows.push({ values: selectedValues(counts, parsed), label: p.rawPath }); records.push({ path: p, run, values: selectedValues(counts, parsed) }); sizes.push(isStdin(p) ? null : counts.bytes); addCounts(total, counts); } const width = pushed ? null : numberWidth(sizes, paths.length, shownCounts(parsed)); const io = new IOResult({ exitCode: err === '' ? 0 : 1, stderr: err === '' ? null : encodeText(err), records, }); return [ formatCountRows(rows, selectedValues(total, parsed), paths.length, parsed.total, width), io, ]; } const counts = await wc(resolveSource(ctx.stdin), ctx, parsed); const values = selectedValues(counts, parsed); if (parsed.total === 'only') { return [encodeText(`${values.join(' ')}\n`), new IOResult()]; } const rows = [{ values, label: null }]; if (parsed.total === 'always') rows.push({ values, label: 'total' }); const width = numberWidth([null], 1, values.length); return [formatRecords(formatWcLines(rows, width)), new IOResult()]; } //# sourceMappingURL=wc.js.map