One parentless commit with only the files that the open seed tasks touch and the modules they import. The full squashed import (SPEC 8.6) is a later step: this tree is not the cutoff tree. T2T-StandIn-Source-Commit: 43f95b7333efe73aeaebf806023f17bffea27aba
413 lines
20 KiB
JavaScript
413 lines
20 KiB
JavaScript
// Shell del proyecto: un intérprete POSIX-lite que opera SOBRE LOS FICHEROS
|
|
// REALES de la carpeta abierta (el FileSystemDirectoryHandle de code.js). No es
|
|
// una emulación: `ls`, `cat`, `grep`, `mkdir`, `rm`, `echo > fichero`… tocan el
|
|
// disco de verdad y el editor/árbol se refrescan. Es el mismo mundo que ve la
|
|
// elfa, así que también lo exponemos como herramienta `terminal.run`.
|
|
//
|
|
// Runtimes reales (node/npm/python): SOLO con un Bridge local conectado (ver
|
|
// bridge.js) se ejecutan de VERDAD en tu máquina — ver `capabilities()`.
|
|
import * as code from './tools/code.js';
|
|
import * as bridgeClient from './bridge.js';
|
|
|
|
let cwd = []; // directorio actual, segmentos relativos a la raíz
|
|
const history = []; // historial de comandos (para ↑/↓ y `history`)
|
|
let onFsChange = () => {}; // el IDE refresca árbol/pestañas tras mutaciones
|
|
let onOpenFile = () => {}; // `open <fichero>` lo abre en Monaco
|
|
|
|
export function setHooks({ fsChange, openFile } = {}) {
|
|
if (fsChange) onFsChange = fsChange;
|
|
if (openFile) onOpenFile = openFile;
|
|
}
|
|
export function reset() { cwd = []; }
|
|
export function cwdString() { return '/' + cwd.join('/'); }
|
|
export function getHistory() { return history.slice(); }
|
|
|
|
// ¿Podemos lanzar runtimes reales? Solo por Bridge local conectado de verdad
|
|
// (WebContainers requiere licencia comercial de StackBlitz en producción —
|
|
// descartado por ahora, ver bridge.js).
|
|
export function capabilities() {
|
|
const bridge = bridgeClient.isConnected();
|
|
return { crossOriginIsolated: !!self.crossOriginIsolated, webcontainers: false, bridge, realRuntime: bridge };
|
|
}
|
|
|
|
// ---- resolución de rutas ----
|
|
// Combina un argumento con el cwd y normaliza . y .. → segmentos desde la raíz.
|
|
function resolve(arg = '') {
|
|
const base = arg.startsWith('/') ? [] : [...cwd];
|
|
for (const seg of String(arg).split('/')) {
|
|
if (!seg || seg === '.') continue;
|
|
if (seg === '..') base.pop();
|
|
else base.push(seg);
|
|
}
|
|
return base;
|
|
}
|
|
const root = () => { const h = code.handle(); if (!h) throw new Error('no hay proyecto abierto'); return h; };
|
|
|
|
async function dirHandle(segs, { create = false } = {}) {
|
|
let dir = root();
|
|
for (const s of segs) dir = await dir.getDirectoryHandle(s, { create });
|
|
return dir;
|
|
}
|
|
async function parentAndName(segs) {
|
|
const s = [...segs]; const name = s.pop();
|
|
if (name == null) throw new Error('ruta vacía');
|
|
return { parent: await dirHandle(s), name };
|
|
}
|
|
async function fileHandle(segs, opts) {
|
|
const { parent, name } = await parentAndName(segs);
|
|
return parent.getFileHandle(name, opts);
|
|
}
|
|
async function readText(segs) { return (await (await fileHandle(segs)).getFile()).text(); }
|
|
async function writeText(segs, text) {
|
|
const fh = await fileHandle(segs, { create: true });
|
|
const w = await fh.createWritable(); await w.write(text); await w.close();
|
|
code.invalidateFileList?.(); onFsChange();
|
|
}
|
|
// ¿ruta es directorio? (para `ls fichero`, `grep dir`, `cd`)
|
|
async function kindOf(segs) {
|
|
if (!segs.length) return 'directory';
|
|
const { parent, name } = await parentAndName(segs);
|
|
try { await parent.getDirectoryHandle(name); return 'directory'; } catch {}
|
|
try { await parent.getFileHandle(name); return 'file'; } catch {}
|
|
return null;
|
|
}
|
|
|
|
const IGNORE = new Set(['node_modules', '.git', 'dist', 'build', 'target', '__pycache__', '.next', 'venv', '.venv', '.DS_Store']);
|
|
const CYAN = s => `\x1b[36m${s}\x1b[0m`, RED = s => `\x1b[31m${s}\x1b[0m`, DIM = s => `\x1b[90m${s}\x1b[0m`, GRN = s => `\x1b[32m${s}\x1b[0m`, YEL = s => `\x1b[33m${s}\x1b[0m`;
|
|
const globToRe = g => new RegExp('^' + g.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i');
|
|
const human = n => n < 1024 ? n + 'B' : n < 1048576 ? (n / 1024).toFixed(1) + 'K' : (n / 1048576).toFixed(1) + 'M';
|
|
|
|
// ---- comandos: (args[], stdin) → stdout (string). Lanzan Error en fallo. ----
|
|
const CMDS = {
|
|
help() {
|
|
return 'Elffuss shell — opera sobre los ficheros REALES del proyecto:\n' +
|
|
' ' + GRN('ls') + ' [-la] [ruta] ' + GRN('cd') + ' [ruta] ' + GRN('pwd') + ' ' + GRN('tree') + ' [ruta]\n' +
|
|
' ' + GRN('cat') + ' <f...> ' + GRN('head') + '/' + GRN('tail') + ' [-n N] ' + GRN('wc') + ' [-l] ' + GRN('grep') + ' [-in] <patrón> [ruta]\n' +
|
|
' ' + GRN('find') + ' [ruta] [-name g] ' + GRN('echo') + ' <txt> [> f] ' + GRN('mkdir') + '/' + GRN('touch') + '/' + GRN('rm') + ' [-r]/' + GRN('mv') + '/' + GRN('cp') + '\n' +
|
|
' ' + GRN('open') + ' <f> (en el editor) ' + GRN('git') + ' status|log|branch ' + GRN('history') + ' ' + GRN('clear') + '\n' +
|
|
' Soporta tuberías ' + YEL('|') + ' y redirección ' + YEL('>') + ' ' + YEL('>>') + '. ' +
|
|
(capabilities().bridge ? GRN('node/npm/python disponibles (Bridge local).') : DIM('node/npm/python: conecta el Bridge local (⚙ Ajustes).'));
|
|
},
|
|
pwd() { return cwdString(); },
|
|
async cd(args) {
|
|
const target = args.find(a => !a.startsWith('-'));
|
|
if (!target || target === '~') { cwd = []; return ''; }
|
|
const segs = resolve(target);
|
|
if (await kindOf(segs) !== 'directory') throw new Error(`cd: no existe el directorio: ${target}`);
|
|
cwd = segs; return '';
|
|
},
|
|
async ls(args) {
|
|
const long = args.some(a => /^-\w*l/.test(a)), all = args.some(a => /^-\w*a/.test(a));
|
|
const target = args.find(a => !a.startsWith('-')) || '';
|
|
const segs = resolve(target);
|
|
const k = await kindOf(segs);
|
|
if (k === 'file') return target; // ls de un fichero
|
|
if (k !== 'directory') throw new Error(`ls: no existe: ${target || '.'}`);
|
|
const dir = await dirHandle(segs);
|
|
const rows = [];
|
|
for await (const e of dir.values()) {
|
|
if (!all && (e.name.startsWith('.') || IGNORE.has(e.name))) continue;
|
|
rows.push(e);
|
|
}
|
|
rows.sort((a, b) => (a.kind !== b.kind ? (a.kind === 'directory' ? -1 : 1) : a.name.localeCompare(b.name)));
|
|
if (!long) return rows.map(e => e.kind === 'directory' ? CYAN(e.name + '/') : e.name).join(' ');
|
|
const out = [];
|
|
for (const e of rows) {
|
|
if (e.kind === 'directory') out.push(`${DIM('dir ')} - ${CYAN(e.name + '/')}`);
|
|
else { const f = await e.getFile(); out.push(`${DIM('file')} ${human(f.size).padStart(6)} ${e.name}`); }
|
|
}
|
|
return out.join('\n');
|
|
},
|
|
async cat(args, stdin) {
|
|
const files = args.filter(a => !a.startsWith('-'));
|
|
if (!files.length) return stdin;
|
|
const parts = [];
|
|
for (const f of files) {
|
|
try { parts.push(await readText(resolve(f))); }
|
|
catch { throw new Error(`cat: ${f}: no existe`); }
|
|
}
|
|
return parts.join('');
|
|
},
|
|
async head(args, stdin) {
|
|
const n = +(args[args.indexOf('-n') + 1]) || 10;
|
|
const f = args.find(a => !a.startsWith('-') && !/^\d+$/.test(a));
|
|
const text = f ? await readText(resolve(f)) : stdin;
|
|
return text.split('\n').slice(0, n).join('\n');
|
|
},
|
|
async tail(args, stdin) {
|
|
const n = +(args[args.indexOf('-n') + 1]) || 10;
|
|
const f = args.find(a => !a.startsWith('-') && !/^\d+$/.test(a));
|
|
const text = f ? await readText(resolve(f)) : stdin;
|
|
const lines = text.split('\n'); return lines.slice(Math.max(0, lines.length - n)).join('\n');
|
|
},
|
|
async wc(args, stdin) {
|
|
const onlyLines = args.includes('-l');
|
|
const f = args.find(a => !a.startsWith('-'));
|
|
const text = f ? await readText(resolve(f)) : stdin;
|
|
const lines = text === '' ? 0 : text.split('\n').length;
|
|
if (onlyLines) return String(lines);
|
|
const words = (text.match(/\S+/g) || []).length;
|
|
return `${lines}\t${words}\t${text.length}${f ? '\t' + f : ''}`;
|
|
},
|
|
async tree(args) {
|
|
const target = args.find(a => !a.startsWith('-')) || '';
|
|
return code.tree({ path: resolve(target).join('/'), depth: 3 });
|
|
},
|
|
async grep(args, stdin) {
|
|
const insensitive = args.some(a => /^-\w*i/.test(a)), numbers = args.some(a => /^-\w*n/.test(a));
|
|
const rest = args.filter(a => !a.startsWith('-'));
|
|
const pattern = rest.shift();
|
|
if (!pattern) throw new Error('grep: falta el patrón');
|
|
const re = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), insensitive ? 'i' : '');
|
|
const hit = (line, i, prefix = '') => (prefix ? CYAN(prefix) + ':' : '') + (numbers ? DIM(i + 1 + ':') : '') + line.replace(re, m => `\x1b[1;31m${m}\x1b[0m`);
|
|
if (!rest.length) { // desde stdin (tubería)
|
|
return stdin.split('\n').map((l, i) => re.test(l) ? hit(l, i) : null).filter(x => x != null).join('\n');
|
|
}
|
|
const out = []; let scanned = 0;
|
|
const scanText = (text, label) =>
|
|
text.split('\n').forEach((l, i) => { if (re.test(l) && out.length < 400) out.push(hit(l, i, label)); });
|
|
for (const target of rest) {
|
|
const segs = resolve(target);
|
|
const k = await kindOf(segs);
|
|
if (k === 'file') scanText(await readText(segs).catch(() => ''), target);
|
|
else if (k === 'directory') { // recursivo
|
|
const walk = async (dir, prefix) => {
|
|
for await (const e of dir.values()) {
|
|
if (IGNORE.has(e.name) || out.length >= 400 || scanned > 600) continue;
|
|
const p = prefix ? prefix + '/' + e.name : e.name;
|
|
if (e.kind === 'directory') { await walk(e, p); continue; }
|
|
const f = await e.getFile();
|
|
if (f.size < 400_000) { scanned++; scanText(await f.text(), (target === '.' ? '' : target.replace(/\/$/, '') + '/') + p); }
|
|
}
|
|
};
|
|
await walk(await dirHandle(segs), '');
|
|
} else throw new Error(`grep: ${target}: no existe`);
|
|
}
|
|
return out.join('\n') || DIM('(sin coincidencias)');
|
|
},
|
|
async find(args) {
|
|
const nameIdx = args.indexOf('-name');
|
|
const glob = nameIdx >= 0 ? globToRe(args[nameIdx + 1]) : null;
|
|
const start = args.find(a => !a.startsWith('-') && a !== (nameIdx >= 0 ? args[nameIdx + 1] : null)) || '.';
|
|
const out = []; let count = 0;
|
|
const walk = async (dir, prefix) => {
|
|
for await (const e of dir.values()) {
|
|
if (IGNORE.has(e.name) || count > 2000) continue;
|
|
const p = prefix ? prefix + '/' + e.name : e.name;
|
|
count++;
|
|
if (!glob || glob.test(e.name)) out.push(e.kind === 'directory' ? CYAN(p) : p);
|
|
if (e.kind === 'directory') await walk(e, p);
|
|
}
|
|
};
|
|
const segs = resolve(start === '.' ? '' : start);
|
|
if (await kindOf(segs) !== 'directory') throw new Error(`find: ${start}: no es un directorio`);
|
|
await walk(await dirHandle(segs), start === '.' ? '' : start);
|
|
return out.join('\n') || DIM('(nada)');
|
|
},
|
|
echo(args) { return args.join(' ').replace(/^["']|["']$/g, ''); },
|
|
async mkdir(args) {
|
|
const t = args.find(a => !a.startsWith('-')); if (!t) throw new Error('mkdir: falta el nombre');
|
|
await dirHandle(resolve(t), { create: true }); onFsChange(); return '';
|
|
},
|
|
async touch(args) {
|
|
const t = args.find(a => !a.startsWith('-')); if (!t) throw new Error('touch: falta el nombre');
|
|
const segs = resolve(t);
|
|
if (await kindOf(segs) == null) await writeText(segs, ''); else onFsChange();
|
|
return '';
|
|
},
|
|
// Soporta comodines (*, ?) y varios objetivos a la vez: `rm mejoras-*.md`.
|
|
// Sin esto, un patrón con * se buscaba como nombre LITERAL → «no existe»
|
|
// aunque hubiera decenas de coincidencias reales (bug real, visto en uso).
|
|
async rm(args) {
|
|
const recursive = args.some(a => /^-\w*r/.test(a));
|
|
const targets = args.filter(a => !a.startsWith('-'));
|
|
if (!targets.length) throw new Error('rm: falta la ruta');
|
|
const removed = [];
|
|
for (const t of targets) {
|
|
if (/[*?]/.test(t)) {
|
|
const parts = t.split('/'); const pattern = parts.pop();
|
|
const dirSegs = resolve(parts.join('/'));
|
|
let dir; try { dir = await dirHandle(dirSegs); } catch { throw new Error(`rm: ${t}: no existe el directorio`); }
|
|
const re = globToRe(pattern);
|
|
const matches = [];
|
|
for await (const e of dir.values()) if (re.test(e.name)) matches.push(e);
|
|
if (!matches.length) throw new Error(`rm: ${t}: sin coincidencias`);
|
|
for (const e of matches) {
|
|
if (e.kind === 'directory' && !recursive) throw new Error(`rm: ${e.name}: es un directorio (usa -r)`);
|
|
await dir.removeEntry(e.name, { recursive });
|
|
removed.push((dirSegs.length ? dirSegs.join('/') + '/' : '') + e.name);
|
|
}
|
|
} else {
|
|
const segs = resolve(t); const k = await kindOf(segs);
|
|
if (k == null) throw new Error(`rm: ${t}: no existe`);
|
|
if (k === 'directory' && !recursive) throw new Error(`rm: ${t}: es un directorio (usa -r)`);
|
|
const { parent, name } = await parentAndName(segs);
|
|
await parent.removeEntry(name, { recursive });
|
|
removed.push(t);
|
|
}
|
|
}
|
|
code.invalidateFileList?.(); onFsChange();
|
|
return removed.length > 1 ? `Eliminados ${removed.length}: ${removed.join(', ')}` : '';
|
|
},
|
|
async cp(args) {
|
|
const rest = args.filter(a => !a.startsWith('-'));
|
|
const [src, dst] = rest; if (!src || !dst) throw new Error('cp: uso: cp <origen> <destino>');
|
|
await copyPath(resolve(src), resolve(dst)); onFsChange(); return '';
|
|
},
|
|
async mv(args) {
|
|
const rest = args.filter(a => !a.startsWith('-'));
|
|
const [src, dst] = rest; if (!src || !dst) throw new Error('mv: uso: mv <origen> <destino>');
|
|
await copyPath(resolve(src), resolve(dst));
|
|
const { parent, name } = await parentAndName(resolve(src));
|
|
await parent.removeEntry(name, { recursive: true });
|
|
code.invalidateFileList?.(); onFsChange(); return '';
|
|
},
|
|
async open(args) {
|
|
const t = args.find(a => !a.startsWith('-')); if (!t) throw new Error('open: falta el fichero');
|
|
const segs = resolve(t);
|
|
if (await kindOf(segs) !== 'file') throw new Error(`open: ${t}: no es un fichero`);
|
|
onOpenFile(segs.join('/')); return DIM(`abriendo ${segs.join('/')} en el editor…`);
|
|
},
|
|
history() { return history.map((h, i) => `${DIM((i + 1) + '')} ${h}`).join('\n'); },
|
|
whoami() { return 'elffuss'; },
|
|
date() { return new Date().toString(); },
|
|
clear() { return '\f'; }, // la UI lo interpreta
|
|
async git(args) {
|
|
const sub = args[0];
|
|
const info = await code.gitInfo();
|
|
if (!info.isRepo) return DIM('no es un repositorio git (no hay .git)');
|
|
if (sub === 'branch' || !sub) return '* ' + GRN(info.branch);
|
|
if (sub === 'status') return `En la rama ${GRN(info.branch)}\n` + (info.lastCommit ? DIM(`último commit: ${info.lastCommit.msg}`) : DIM('sin commits todavía')) + '\n' + DIM('(git de solo lectura aquí; commit/push reales no disponibles todavía)');
|
|
if (sub === 'log') return info.lastCommit ? `${YEL('commit ' + info.branch)}\nAutor: ${info.lastCommit.author}\nFecha: ${new Date(info.lastCommit.when).toLocaleString()}\n\n ${info.lastCommit.msg}` : DIM('sin commits');
|
|
return DIM(`git ${sub}: de solo lectura aquí (branch/status/log).`);
|
|
},
|
|
};
|
|
// tokenize() ya quitó las comillas originales (p.ej. -e "console.log(1)" llega
|
|
// como el arg SUELTO console.log(1)) — para reenviarlo al bridge (que lo pasa
|
|
// a `sh -c`) hay que re-entrecomillarlo, si no un espacio o un paréntesis
|
|
// rompe el comando al otro lado (bug real: -e "console.log('x')" fallaba).
|
|
const shellQuote = s => `'${String(s).replace(/'/g, "'\\''")}'`;
|
|
// runtimes REALES solo si hay un Bridge local conectado (⚙ Ajustes) — se
|
|
// ejecutan en tu máquina de verdad, en la carpeta configurada del bridge (no
|
|
// en el cwd virtual del proyecto).
|
|
for (const r of ['node', 'npm', 'npx', 'python', 'python3', 'pip', 'yarn', 'pnpm', 'bun', 'deno', 'cargo', 'go'])
|
|
CMDS[r] = async (args) => {
|
|
if (!bridgeClient.isConnected()) {
|
|
throw new Error(`${r}: runtime real no disponible.\n${DIM('Descarga y arranca el Bridge local (⚙ Ajustes → 🔌 Bridge local) para ejecución real en tu máquina — se conecta solo en cuanto lo detecte. Mientras tanto, la elfa edita ficheros y tú navegas con los comandos de arriba (help).')}`);
|
|
}
|
|
const full = r + (args.length ? ' ' + args.map(shellQuote).join(' ') : '');
|
|
let out = '';
|
|
const code = await bridgeClient.exec(full, { onOut: t => { out += t; }, onErr: t => { out += RED(t); } });
|
|
if (code) out += `\n${DIM('(código de salida ' + code + ')')}`;
|
|
return out || DIM('(sin salida)');
|
|
};
|
|
|
|
// copia recursiva por handles (la File System API no tiene move nativo)
|
|
async function copyPath(srcSegs, dstSegs) {
|
|
const k = await kindOf(srcSegs);
|
|
if (k == null) throw new Error('origen no existe');
|
|
if (k === 'file') { await writeText(dstSegs, await readText(srcSegs)); return; }
|
|
const src = await dirHandle(srcSegs);
|
|
await dirHandle(dstSegs, { create: true });
|
|
for await (const e of src.values()) await copyPath([...srcSegs, e.name], [...dstSegs, e.name]);
|
|
}
|
|
|
|
// ---- parser: tuberías, redirección, comillas ----
|
|
function splitTop(line, sep) { // parte por `sep` respetando comillas
|
|
const parts = []; let cur = '', q = '';
|
|
for (const ch of line) {
|
|
if (q) { cur += ch; if (ch === q) q = ''; }
|
|
else if (ch === '"' || ch === "'") { q = ch; cur += ch; }
|
|
else if (ch === sep) { parts.push(cur); cur = ''; }
|
|
else cur += ch;
|
|
}
|
|
parts.push(cur); return parts;
|
|
}
|
|
function tokenize(seg) {
|
|
const toks = []; let cur = '', q = '';
|
|
for (const ch of seg.trim()) {
|
|
if (q) { if (ch === q) q = ''; else cur += ch; }
|
|
else if (ch === '"' || ch === "'") q = ch;
|
|
else if (/\s/.test(ch)) { if (cur) { toks.push(cur); cur = ''; } }
|
|
else cur += ch;
|
|
}
|
|
if (cur) toks.push(cur);
|
|
return toks;
|
|
}
|
|
|
|
// Ejecuta una línea completa (encadenado &&/;, tuberías, redirección).
|
|
// `\f` en el retorno = orden de limpiar pantalla (para la UI).
|
|
export async function exec(line, { record = true } = {}) {
|
|
line = line.trim();
|
|
if (!line) return '';
|
|
if (record) history.push(line);
|
|
const segments = tokenizeChain(line);
|
|
if (segments.length === 1) return runPipeline(segments[0].cmd);
|
|
// encadenado: A && B (B solo si A fue bien) · A ; B (B siempre)
|
|
const outs = []; let ok = true;
|
|
for (const { cmd, op } of segments) {
|
|
if (op === '&&' && !ok) continue;
|
|
try { outs.push(await runPipeline(cmd)); ok = true; }
|
|
catch (e) { ok = false; outs.push('\x1b[31m' + (e && e.message || e) + '\x1b[0m'); }
|
|
}
|
|
if (outs.every(o => o === '\f')) return '\f';
|
|
return outs.filter(o => o && o !== '\f').join('\n');
|
|
}
|
|
|
|
// Divide la línea en segmentos con su operador previo (&& / ; ), respetando comillas.
|
|
function tokenizeChain(line) {
|
|
const segs = []; let cur = '', q = '', op = null;
|
|
for (let i = 0; i < line.length; i++) {
|
|
const ch = line[i];
|
|
if (q) { cur += ch; if (ch === q) q = ''; continue; }
|
|
if (ch === '"' || ch === "'") { q = ch; cur += ch; continue; }
|
|
if (ch === '&' && line[i + 1] === '&') { segs.push({ cmd: cur.trim(), op }); op = '&&'; cur = ''; i++; continue; }
|
|
if (ch === ';') { segs.push({ cmd: cur.trim(), op }); op = ';'; cur = ''; continue; }
|
|
cur += ch;
|
|
}
|
|
if (cur.trim()) segs.push({ cmd: cur.trim(), op });
|
|
return segs.filter(s => s.cmd);
|
|
}
|
|
|
|
// Ejecuta un pipeline (una etapa o varias unidas por |, con redirección final).
|
|
async function runPipeline(line) {
|
|
const stages = splitTop(line, '|').map(s => s.trim()).filter(Boolean);
|
|
let data = '';
|
|
for (let i = 0; i < stages.length; i++) {
|
|
let seg = stages[i], redir = null;
|
|
if (i === stages.length - 1) {
|
|
const m = seg.match(/(>>?)\s*(\S+)\s*$/);
|
|
if (m) { redir = { append: m[1] === '>>', file: m[2] }; seg = seg.slice(0, m.index); }
|
|
}
|
|
const toks = tokenize(seg);
|
|
const name = toks.shift();
|
|
if (!name) continue;
|
|
const fn = CMDS[name];
|
|
if (!fn) throw new Error(`${name}: comando no encontrado (prueba ${GRN('help')})`);
|
|
data = (await fn(toks, data)) ?? '';
|
|
if (redir) {
|
|
const segs = resolve(redir.file);
|
|
const prev = redir.append ? await readText(segs).catch(() => '') : '';
|
|
await writeText(segs, prev + stripAnsi(data));
|
|
data = '';
|
|
}
|
|
}
|
|
return data;
|
|
}
|
|
|
|
export const stripAnsi = s => String(s).replace(/\x1b\[[0-9;]*m/g, '');
|
|
|
|
// Entradas de un directorio (para el completado con Tab del terminal).
|
|
export async function listDir(dirPart = '') {
|
|
const dir = await dirHandle(resolve(dirPart));
|
|
const out = [];
|
|
for await (const e of dir.values()) if (!IGNORE.has(e.name)) out.push({ name: e.name, kind: e.kind });
|
|
return out;
|
|
}
|
|
|
|
// Ejecuta una orden desde la elfa (tool terminal.run): devuelve texto plano.
|
|
export async function runForAgent(command) {
|
|
const out = await exec(command, { record: true });
|
|
return out === '\f' ? '(pantalla limpiada)' : (stripAnsi(out) || '(sin salida)');
|
|
}
|