@smthrs/std 1.0.0-rc.7 → 1.0.0-rc.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +40 -36
- package/dist/cjs/Checkpoints.d.ts +37 -0
- package/dist/cjs/Checkpoints.d.ts.map +1 -1
- package/dist/cjs/Checkpoints.js +75 -0
- package/dist/cjs/Checkpoints.js.map +2 -2
- package/dist/cjs/PortableSearch.d.ts +13 -1
- package/dist/cjs/PortableSearch.d.ts.map +1 -1
- package/dist/cjs/PortableSearch.js +8 -1
- package/dist/cjs/PortableSearch.js.map +2 -2
- package/dist/cjs/RemoteSearch.d.ts +54 -0
- package/dist/cjs/RemoteSearch.d.ts.map +1 -0
- package/dist/cjs/RemoteSearch.js +134 -0
- package/dist/cjs/RemoteSearch.js.map +7 -0
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +2 -2
- package/dist/cjs/internal/EnvelopePrecheck.d.ts +2 -0
- package/dist/cjs/internal/EnvelopePrecheck.d.ts.map +1 -1
- package/dist/cjs/internal/EnvelopePrecheck.js +235 -36
- package/dist/cjs/internal/EnvelopePrecheck.js.map +3 -3
- package/dist/cjs/internal/Exec.d.ts +12 -7
- package/dist/cjs/internal/Exec.d.ts.map +1 -1
- package/dist/cjs/internal/Exec.js +8 -4
- package/dist/cjs/internal/Exec.js.map +2 -2
- package/dist/cjs/internal/RemoteTree.d.ts +60 -0
- package/dist/cjs/internal/RemoteTree.d.ts.map +1 -0
- package/dist/cjs/internal/RemoteTree.js +123 -0
- package/dist/cjs/internal/RemoteTree.js.map +7 -0
- package/dist/cjs/internal/Tar.d.ts +23 -0
- package/dist/cjs/internal/Tar.d.ts.map +1 -0
- package/dist/cjs/internal/Tar.js +86 -0
- package/dist/cjs/internal/Tar.js.map +7 -0
- package/dist/esm/Checkpoints.d.ts +37 -0
- package/dist/esm/Checkpoints.d.ts.map +1 -1
- package/dist/esm/Checkpoints.js +85 -0
- package/dist/esm/Checkpoints.js.map +1 -1
- package/dist/esm/PortableSearch.d.ts +13 -1
- package/dist/esm/PortableSearch.d.ts.map +1 -1
- package/dist/esm/PortableSearch.js +16 -0
- package/dist/esm/PortableSearch.js.map +1 -1
- package/dist/esm/RemoteSearch.d.ts +54 -0
- package/dist/esm/RemoteSearch.d.ts.map +1 -0
- package/dist/esm/RemoteSearch.js +162 -0
- package/dist/esm/RemoteSearch.js.map +1 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal/EnvelopePrecheck.d.ts +2 -0
- package/dist/esm/internal/EnvelopePrecheck.d.ts.map +1 -1
- package/dist/esm/internal/EnvelopePrecheck.js +321 -62
- package/dist/esm/internal/EnvelopePrecheck.js.map +1 -1
- package/dist/esm/internal/Exec.d.ts +12 -7
- package/dist/esm/internal/Exec.d.ts.map +1 -1
- package/dist/esm/internal/Exec.js +30 -9
- package/dist/esm/internal/Exec.js.map +1 -1
- package/dist/esm/internal/RemoteTree.d.ts +60 -0
- package/dist/esm/internal/RemoteTree.d.ts.map +1 -0
- package/dist/esm/internal/RemoteTree.js +132 -0
- package/dist/esm/internal/RemoteTree.js.map +1 -0
- package/dist/esm/internal/Tar.d.ts +23 -0
- package/dist/esm/internal/Tar.d.ts.map +1 -0
- package/dist/esm/internal/Tar.js +97 -0
- package/dist/esm/internal/Tar.js.map +1 -0
- package/docs/README.md +3 -1
- package/docs/api.md +48 -21
- package/docs/concepts/effects-and-capabilities.md +3 -1
- package/docs/guides/bind-the-standard-flows.md +3 -1
- package/docs/guides/pin-a-checkpoint.md +20 -0
- package/docs/guides/run-a-shell-command.md +4 -1
- package/docs/installation.md +13 -9
- package/docs/quickstart.md +1 -1
- package/docs/troubleshooting.md +20 -0
- package/package.json +16 -6
- package/src/Checkpoints.ts +137 -0
- package/src/PortableSearch.ts +20 -0
- package/src/RemoteSearch.ts +199 -0
- package/src/index.ts +3 -0
- package/src/internal/EnvelopePrecheck.ts +320 -64
- package/src/internal/Exec.ts +32 -8
- package/src/internal/RemoteTree.ts +159 -0
- package/src/internal/Tar.ts +95 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/internal/EnvelopePrecheck.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * The hermetic pre-check: a lexical scan of shell text for the paths it names,\n * and the refusal when one of them falls outside the declared envelope.\n *\n * The scan is fail-closed over explicit path tokens and nothing more. It\n * cannot prove confinement \u2014 a shell can name a path no lexer sees, through a\n * variable, a substitution, or a program that opens a file of its own \u2014 so a\n * kernel or host sandbox must eventually enforce and report the complete read\n * and write sets.\n *\n * It lives beside the flow rather than inside it because it is its own\n * concept, with its own vocabulary (Access, PathReference) and its own\n * security history: three independent bypasses, each sufficient on its own to\n * defeat it, are closed here.\n *\n * @since 1.0.0\n */\n\nimport type * as Path from \"@smthrs/kernel/Path\"\nimport * as StdError from \"../StdError.ts\"\nimport { withinEnvelope } from \"./Paths.ts\"\n\n/**\n * The filesystem envelope one invocation declared.\n *\n * Structural rather than the hermetic `Bash.Input` itself: the scan needs the\n * three fields and none of the rest, and depending on the flow module would\n * make the flow and its pre-check import each other.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface Envelope {\n readonly cwd?: string | undefined\n readonly reads: ReadonlyArray<string>\n readonly writes: ReadonlyArray<string>\n}\n\n/**\n * How a command touches a path it names.\n *\n * @category models\n * @since 1.0.0\n */\nexport type Access = \"read\" | \"write\"\n\n/**\n * One path a command names, and how it touches it.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface PathReference {\n readonly access: Access\n readonly value: string\n}\n\nconst commandSeparators = new Set([\"&&\", \"||\", \";\", \"|\", \"&\", \"\\n\"])\nconst redirections = new Set([\">\", \">>\", \"<\", \"<<\"])\nconst writeCommands = new Set([\"chmod\", \"chown\", \"mkdir\", \"rm\", \"rmdir\", \"tee\", \"touch\", \"truncate\", \"unlink\"])\nconst destinationCommands = new Set([\"cp\", \"install\", \"mv\"])\nconst prefixWrappers = new Set([\"env\", \"nice\", \"time\", \"sudo\", \"nohup\", \"xargs\", \"command\", \"exec\"])\n\n// Shell spaces may disappear, but a physical line ends one command. Retaining\n// LF prevents a command on line one from classifying every later line's paths.\n//\n// A word is one or more runs of bare characters and quoted sections, not a\n// bare run alone: `FOO=\"a b\" rm /work/target` used to tokenize as `FOO=\"a`,\n// `b\"`, `rm`, `/work/target`, and since only the first token carried an `=`\n// the command was read as `b\"` rather than `rm` \u2014 so the delete was\n// classified as a read and a hermetic call with `writes: []` spawned. The\n// quoted alternatives come first inside the group so a quoted span is taken\n// whole; the bare class excludes quotes so the branches cannot overlap.\nconst tokenize = (command: string): ReadonlyArray<string> =>\n command.match(/\\n|>>|<<|&&|\\|\\||[<>;|&]|(?:\"(?:\\\\.|[^\"\\\\])*\"|'[^']*'|[^\\s<>;|&\"']+)+/g) ?? []\n\nconst unquote = (token: string): string => {\n const first = token[0]\n const last = token[token.length - 1]\n return token.length >= 2 && (first === \"'\" || first === \"\\\"\") && last === first\n ? token.slice(1, -1)\n : token\n}\n\nconst pathValue = (token: string): string =>\n unquote(token)\n .replace(/^\\$\\(+/, \"\")\n .replace(/[),]+$/, \"\")\n\nconst isPathToken = (path: Path.Path, token: string): boolean =>\n token === \".\" ||\n token === \"..\" ||\n token.startsWith(\"./\") ||\n token.startsWith(\"../\") ||\n path.isAbsolute(token) ||\n /^[A-Za-z]:[\\\\/]/.test(token) ||\n token.includes(\"/\")\n\nconst segmentReferences = (path: Path.Path, tokens: ReadonlyArray<string>): ReadonlyArray<PathReference> => {\n const commentIndex = tokens.findIndex((token) => token.startsWith(\"#\"))\n const active = commentIndex === -1 ? tokens : tokens.slice(0, commentIndex)\n const hasWrapper = active.some((token) => prefixWrappers.has(path.basename(pathValue(token))))\n let commandIndex = active.findIndex((token) =>\n !redirections.has(token) &&\n !token.includes(\"=\") &&\n !prefixWrappers.has(path.basename(pathValue(token))) &&\n !token.startsWith(\"-\") &&\n token !== \"\"\n )\n if (hasWrapper) {\n // Wrapper options can carry their own values. Prefer a later known\n // mutating command so an option value cannot lend it a read classification.\n const mutatingIndex = active.findIndex((token) => {\n const command = path.basename(pathValue(token))\n return writeCommands.has(command) || destinationCommands.has(command)\n })\n if (mutatingIndex !== -1) commandIndex = mutatingIndex\n }\n if (commandIndex === -1) return []\n\n const command = path.basename(pathValue(active[commandIndex]!))\n const candidates: Array<{ readonly index: number; readonly value: string }> = []\n for (let index = 0; index < active.length; index++) {\n const token = active[index]!\n if (index === commandIndex || redirections.has(token) || token.startsWith(\"-\")) continue\n const value = pathValue(token)\n if (value !== \"\" && isPathToken(path, value)) candidates.push({ index, value })\n }\n\n const destinationIndex = destinationCommands.has(command)\n ? candidates[candidates.length - 1]?.index\n : undefined\n\n return candidates.map(({ index, value }) => {\n const previous = active[index - 1]\n const access: Access = previous === \">\" || previous === \">>\" ||\n writeCommands.has(command) || destinationIndex === index\n ? \"write\"\n : \"read\"\n return { access, value }\n })\n}\n\n/**\n * Every path token the shell text names, one segment at a time.\n *\n * @category pre-check\n * @since 1.0.0\n */\nexport const commandReferences = (path: Path.Path, command: string): ReadonlyArray<PathReference> => {\n const references: Array<PathReference> = []\n let segment: Array<string> = []\n for (const token of tokenize(command)) {\n if (commandSeparators.has(token)) {\n references.push(...segmentReferences(path, segment))\n segment = []\n } else {\n segment.push(token)\n }\n }\n references.push(...segmentReferences(path, segment))\n return references\n}\n\nconst resolvePath = (\n path: Path.Path,\n cwd: string,\n value: string\n): string => path.isAbsolute(value) ? path.normalize(value) : path.resolve(cwd, value)\n\nconst isDeclared = (\n path: Path.Path,\n cwd: string,\n declared: ReadonlyArray<string>,\n source: string\n): boolean =>\n withinEnvelope(\n declared.map((entry) => resolvePath(path, cwd, entry)),\n resolvePath(path, cwd, source)\n )\n\n/**\n * The refusal shell text earns by naming a path outside its declared\n * envelope, or `undefined` when every path it names is declared.\n *\n * @category pre-check\n * @since 1.0.0\n */\nexport const outsideEnvelope = (\n input: Envelope,\n text: string,\n path: Path.Path\n): StdError.StdError | undefined => {\n const base = path.resolve(\".\")\n const cwd = path.resolve(input.cwd ?? \".\")\n // Passing the default explicitly is not a declaration the caller owes. A\n // working directory is where the command runs, not a file it reads, and\n // `cwd: \".\"` names exactly what omitting `cwd` names \u2014 so refusing it asked\n // the caller to list the workspace root among its reads to say nothing at\n // all. Agents hit this constantly: one SWE-bench run spent ten of its\n // forty-eight tool calls re-issuing the same command after\n // \"Working directory is outside declared reads: <the workspace root>\". A\n // cwd that points somewhere else is still declared or refused.\n if (input.cwd !== undefined && cwd !== base && !isDeclared(path, cwd, input.reads, cwd)) {\n return new StdError.StdError({\n code: \"outside_declared_reads\",\n message: `Working directory is outside declared reads: ${cwd}`,\n path: cwd\n })\n }\n\n // The Shell contract currently has no sandbox or access-reporting surface.\n // This lexical scan is intentionally only a fail-closed pre-check for\n // explicit path tokens. It cannot prove confinement; a kernel/host sandbox\n // must eventually enforce and report the complete read and write sets.\n for (const reference of commandReferences(path, text)) {\n const resolved = resolvePath(path, cwd, reference.value)\n // /dev/* is the process plumbing every shell one-liner leans on:\n // `2>/dev/null`, `cat /dev/stdin`. Normalize first so a dot-dot segment\n // cannot disguise an ordinary filesystem path as process plumbing.\n if (resolved.startsWith(\"/dev/\")) continue\n const declared = reference.access === \"write\" ? input.writes : input.reads\n if (!isDeclared(path, cwd, declared, resolved)) {\n const code = reference.access === \"write\" ? \"outside_declared_writes\" : \"outside_declared_reads\"\n return new StdError.StdError({\n code,\n message: `Command path is outside declared ${reference.access}s: ${resolved}`,\n path: resolved\n })\n }\n }\n return undefined\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmBA,eAA0B;AAC1B,mBAA+B;AAqC/B,MAAM,oBAAoB,oBAAI,IAAI,CAAC,MAAM,MAAM,KAAK,KAAK,KAAK,IAAI,CAAC;
|
|
6
|
-
"names": [
|
|
4
|
+
"sourcesContent": ["/**\n * The hermetic pre-check: a lexical scan of shell text for the paths it names,\n * and the refusal when one of them falls outside the declared envelope.\n *\n * The scan is fail-closed over explicit path tokens and nothing more. It\n * cannot prove confinement \u2014 a shell can name a path no lexer sees, through a\n * variable, a substitution, or a program that opens a file of its own \u2014 so a\n * kernel or host sandbox must eventually enforce and report the complete read\n * and write sets.\n *\n * It lives beside the flow rather than inside it because it is its own\n * concept, with its own vocabulary (Access, PathReference) and its own\n * security history: three independent bypasses, each sufficient on its own to\n * defeat it, are closed here.\n *\n * @since 1.0.0\n */\n\nimport type * as Path from \"@smthrs/kernel/Path\"\nimport * as StdError from \"../StdError.ts\"\nimport { withinEnvelope } from \"./Paths.ts\"\n\n/**\n * The filesystem envelope one invocation declared.\n *\n * Structural rather than the hermetic `Bash.Input` itself: the scan needs the\n * three fields and none of the rest, and depending on the flow module would\n * make the flow and its pre-check import each other.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface Envelope {\n readonly cwd?: string | undefined\n readonly reads: ReadonlyArray<string>\n readonly writes: ReadonlyArray<string>\n}\n\n/**\n * How a command touches a path it names.\n *\n * @category models\n * @since 1.0.0\n */\nexport type Access = \"read\" | \"write\"\n\n/**\n * One path a command names, and how it touches it.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface PathReference {\n readonly access: Access\n readonly value: string\n}\n\nconst commandSeparators = new Set([\"&&\", \"||\", \";\", \";;\", \";&\", \";;&\", \"|\", \"|&\", \"&\", \"\\n\", \"(\", \")\"])\nconst writeRedirections = new Set([\">\", \">>\", \">|\", \"&>\", \"&>>\", \">&\", \"<>\"])\nconst readRedirections = new Set([\"<\", \"<&\"])\n// A here-document delimiter and a here-string are text, not file names.\nconst textRedirections = new Set([\"<<\", \"<<-\", \"<<<\"])\nconst isRedirection = (token: Token): boolean =>\n token.operator &&\n (writeRedirections.has(token.text) || readRedirections.has(token.text) || textRedirections.has(token.text))\n// Longest first, so `&>>` is not read as `&` then `>>`.\nconst operators = [\n \";;&\",\n \"&>>\",\n \"<<<\",\n \"<<-\",\n \";;\",\n \";&\",\n \"&&\",\n \"||\",\n \"|&\",\n \"&>\",\n \">>\",\n \">|\",\n \">&\",\n \"<>\",\n \"<<\",\n \"<&\",\n \"\\n\",\n \";\",\n \"|\",\n \"&\",\n \"(\",\n \")\",\n \">\",\n \"<\"\n]\n// Reserved words and grouping braces open or close a compound command; the\n// command they wrap stands after them.\nconst reservedWords = new Set([\n \"!\",\n \"{\",\n \"}\",\n \"case\",\n \"do\",\n \"done\",\n \"elif\",\n \"else\",\n \"esac\",\n \"fi\",\n \"for\",\n \"function\",\n \"if\",\n \"in\",\n \"select\",\n \"then\",\n \"until\",\n \"while\"\n])\nconst writeCommands = new Set([\"chmod\", \"chown\", \"mkdir\", \"rm\", \"rmdir\", \"tee\", \"touch\", \"truncate\", \"unlink\"])\nconst destinationCommands = new Set([\"cp\", \"install\", \"mv\"])\nconst prefixWrappers = new Set([\"env\", \"nice\", \"time\", \"sudo\", \"nohup\", \"xargs\", \"command\", \"exec\"])\n// A shell runs the argument of its `-c` flag as shell text of its own.\nconst shells = new Set([\"ash\", \"bash\", \"dash\", \"fish\", \"ksh\", \"mksh\", \"sh\", \"zsh\"])\nconst commandFlag = /^-[A-Za-z]*c[A-Za-z]*$/\n\n/**\n * One lexical unit of shell text: an operator, or a word with its quotes\n * removed. A command substitution inside a word stands as `$()` in the word;\n * its body is scanned as a command of its own.\n */\ninterface Token {\n readonly operator: boolean\n readonly text: string\n}\n\ninterface Lexed {\n readonly tokens: ReadonlyArray<Token>\n readonly substitutions: ReadonlyArray<string>\n}\n\n// The end of a `$(` body that starts at `from`: the index of its closing\n// parenthesis, skipping quoted spans and nested parentheses. An unclosed body\n// runs to the end of the text, which is how a fail-closed scan must read it.\n// Substitutions and quotes nest without limit in a shell, but each level\n// costs this scan a pass over the rest of the text and a stack frame. Fifty\n// thousand `$(` used to run for half a minute and then overflow the stack, so\n// past this depth the text is refused rather than scanned.\nconst maxNesting = 32\n\nconst tooDeep = (): StdError.StdError =>\n new StdError.StdError({\n code: \"invalid_input\",\n message: `Command nests substitutions or quotes deeper than ${maxNesting} levels`\n })\n\nconst substitutionEnd = (text: string, from: number, nesting: number): number => {\n if (nesting > maxNesting) throw tooDeep()\n let depth = 1\n let index = from\n while (index < text.length) {\n const char = text[index]!\n if (char === \"\\\\\") {\n index += 2\n } else if (char === \"'\") {\n const close = text.indexOf(\"'\", index + 1)\n index = close === -1 ? text.length : close + 1\n } else if (char === \"\\\"\") {\n index = doubleQuoteEnd(text, index + 1, nesting + 1) + 1\n } else if (char === \"`\") {\n index = backtickEnd(text, index + 1) + 1\n } else {\n if (char === \"(\") depth++\n if (char === \")\" && --depth === 0) return index\n index++\n }\n }\n return text.length\n}\n\nconst backtickEnd = (text: string, from: number): number => {\n let index = from\n while (index < text.length && text[index] !== \"`\") index += text[index] === \"\\\\\" ? 2 : 1\n return Math.min(index, text.length)\n}\n\nconst doubleQuoteEnd = (text: string, from: number, nesting: number): number => {\n if (nesting > maxNesting) throw tooDeep()\n let index = from\n while (index < text.length && text[index] !== \"\\\"\") {\n const char = text[index]!\n if (char === \"\\\\\") index += 2\n else if (char === \"`\") index = backtickEnd(text, index + 1) + 1\n else if (char === \"$\" && text[index + 1] === \"(\") index = substitutionEnd(text, index + 2, nesting + 1) + 1\n else index++\n }\n return Math.min(index, text.length)\n}\n\n// A hand-rolled lexer rather than one regular expression: a word is any run\n// of bare characters, quoted spans, and substitutions, and a substitution can\n// hold quotes and operators of its own. Splitting on operator characters\n// alone let `echo \"$(rm /work/target)\"` hide a delete inside one quoted word,\n// read `>|` as a pipe that turned the target into the next command's name,\n// and read `(rm /work/target)` as a command named `(rm`.\n//\n// Quotes are removed from a word's text, because `\"/outs\"ide/x` names\n// `/outside/x`; keeping the quotes made it a relative path under the\n// working directory.\n// Sticky, so the match starts at `lastIndex` without copying the rest of the\n// text for every word.\nconst descriptorPattern = /\\d+(?=[<>])/y\n\nconst lex = (command: string, nesting: number): Lexed => {\n const tokens: Array<Token> = []\n const substitutions: Array<string> = []\n let index = 0\n const substitution = (from: number, to: number): string => {\n substitutions.push(command.slice(from, to))\n return \"$()\"\n }\n while (index < command.length) {\n const char = command[index]!\n // Every blank the word loop stops at must be skipped here, or the lexer\n // would push an empty word without advancing.\n if (char !== \"\\n\" && /\\s/.test(char)) {\n index++\n continue\n }\n if (char === \"\\\\\" && command[index + 1] === \"\\n\") {\n index += 2\n continue\n }\n if (char === \"#\") {\n while (index < command.length && command[index] !== \"\\n\") index++\n continue\n }\n // A file-descriptor number belongs to the redirection after it: in\n // `2>/dev/null rm /work/target` the command is `rm`, not `2`.\n descriptorPattern.lastIndex = index\n const descriptor = descriptorPattern.exec(command)\n if (descriptor !== null) {\n index += descriptor[0].length\n continue\n }\n const operator = operators.find((candidate) => command.startsWith(candidate, index))\n if (operator !== undefined) {\n tokens.push({ operator: true, text: operator })\n index += operator.length\n continue\n }\n let text = \"\"\n while (index < command.length) {\n const next = command[index]!\n if (/[\\s<>;|&()]/.test(next)) break\n if (next === \"\\\\\") {\n // A backslash escapes shell syntax. Before any other character it is\n // kept, so `C:\\work\\input` stays the path it names on Windows rather\n // than collapsing to a word the scan cannot see as a path.\n const escaped = command[index + 1] ?? \"\"\n if (escaped !== \"\\n\") text += /[\\s<>;|&()'\"`$\\\\#]/.test(escaped) ? escaped : `\\\\${escaped}`\n index += 2\n } else if (next === \"'\") {\n const close = command.indexOf(\"'\", index + 1)\n const end = close === -1 ? command.length : close\n text += command.slice(index + 1, end)\n index = end + 1\n } else if (next === \"\\\"\") {\n const end = doubleQuoteEnd(command, index + 1, nesting + 1)\n let inner = index + 1\n while (inner < end) {\n const quoted = command[inner]!\n if (quoted === \"\\\\\" && /[$`\"\\\\\\n]/.test(command[inner + 1] ?? \"\")) {\n if (command[inner + 1] !== \"\\n\") text += command[inner + 1]\n inner += 2\n } else if (quoted === \"`\") {\n const close = backtickEnd(command, inner + 1)\n text += substitution(inner + 1, close)\n inner = close + 1\n } else if (quoted === \"$\" && command[inner + 1] === \"(\") {\n const close = substitutionEnd(command, inner + 2, nesting + 2)\n text += substitution(inner + 2, close)\n inner = close + 1\n } else {\n text += quoted\n inner++\n }\n }\n index = end + 1\n } else if (next === \"`\") {\n const close = backtickEnd(command, index + 1)\n text += substitution(index + 1, close)\n index = close + 1\n } else if (next === \"$\" && command[index + 1] === \"(\") {\n const close = substitutionEnd(command, index + 2, nesting + 1)\n text += substitution(index + 2, close)\n index = close + 1\n } else {\n text += next\n index++\n }\n }\n tokens.push({ operator: false, text })\n }\n return { tokens, substitutions }\n}\n\n// A parenthesis is an operator to the lexer, so one left in a word was quoted\n// and is part of the name; only a trailing comma is punctuation.\nconst pathValue = (token: string): string => token.replace(/,+$/, \"\")\n\nconst isPathToken = (path: Path.Path, token: string): boolean =>\n token === \".\" ||\n token === \"..\" ||\n token.startsWith(\"./\") ||\n token.startsWith(\"../\") ||\n path.isAbsolute(token) ||\n /^[A-Za-z]:[\\\\/]/.test(token) ||\n token.includes(\"/\")\n\nconst segmentReferences = (\n path: Path.Path,\n tokens: ReadonlyArray<Token>,\n nesting: number\n): ReadonlyArray<PathReference> => {\n // The word after a redirection operator is its target, never the command.\n const isTarget = (index: number): boolean => index > 0 && isRedirection(tokens[index - 1]!)\n const isWord = (index: number): boolean => !tokens[index]!.operator && !isTarget(index)\n const name = (index: number): string => path.basename(pathValue(tokens[index]!.text))\n const hasWrapper = tokens.some((_, index) => isWord(index) && prefixWrappers.has(name(index)))\n let commandIndex = tokens.findIndex((token, index) =>\n isWord(index) &&\n !token.text.includes(\"=\") &&\n !prefixWrappers.has(name(index)) &&\n !reservedWords.has(token.text) &&\n !token.text.startsWith(\"-\") &&\n token.text !== \"\"\n )\n if (hasWrapper) {\n // Wrapper options can carry their own values. Prefer a later known\n // mutating command so an option value cannot lend it a read classification.\n const mutatingIndex = tokens.findIndex((_, index) =>\n isWord(index) && (writeCommands.has(name(index)) || destinationCommands.has(name(index)))\n )\n if (mutatingIndex !== -1) commandIndex = mutatingIndex\n }\n\n const command = commandIndex === -1 ? \"\" : name(commandIndex)\n // Shell text handed to `eval` or to a shell's `-c` flag is a command, not a\n // path: reading `sh -c 'rm /work/target'` as one quoted word let a delete\n // pass as a read of a file named `rm /work/target`.\n const scripts = new Set<number>()\n if (command === \"eval\") {\n for (let index = commandIndex + 1; index < tokens.length; index++) if (isWord(index)) scripts.add(index)\n } else if (shells.has(command)) {\n const flag = tokens.findIndex((token, index) =>\n index > commandIndex && isWord(index) && commandFlag.test(token.text)\n )\n if (flag !== -1 && flag + 1 < tokens.length && isWord(flag + 1)) scripts.add(flag + 1)\n }\n const candidates: Array<{ readonly index: number; readonly value: string }> = []\n for (let index = 0; index < tokens.length; index++) {\n const token = tokens[index]!\n if (index === commandIndex || scripts.has(index) || token.operator) continue\n if (!isTarget(index) && token.text.startsWith(\"-\")) continue\n if (isTarget(index) && textRedirections.has(tokens[index - 1]!.text)) continue\n const value = pathValue(token.text)\n if (value !== \"\" && isPathToken(path, value)) candidates.push({ index, value })\n }\n\n const destinationIndex = destinationCommands.has(command)\n ? candidates.filter(({ index }) => !isTarget(index)).at(-1)?.index\n : undefined\n\n const script = [...scripts].map((index) => tokens[index]!.text).join(\" \")\n const nested = script === \"\" ? [] : references(path, script, nesting + 1)\n const named = candidates.map(({ index, value }): PathReference => {\n const access: Access = isTarget(index)\n ? writeRedirections.has(tokens[index - 1]!.text) ? \"write\" : \"read\"\n : writeCommands.has(command) || destinationIndex === index\n ? \"write\"\n : \"read\"\n return { access, value }\n })\n return [...named, ...nested]\n}\n\nconst references = (path: Path.Path, command: string, nesting: number): ReadonlyArray<PathReference> => {\n if (nesting > maxNesting) throw tooDeep()\n const { substitutions, tokens } = lex(command, nesting)\n const found: Array<PathReference> = []\n let segment: Array<Token> = []\n for (const token of tokens) {\n if (token.operator && commandSeparators.has(token.text)) {\n found.push(...segmentReferences(path, segment, nesting))\n segment = []\n } else {\n segment.push(token)\n }\n }\n found.push(...segmentReferences(path, segment, nesting))\n // A substitution runs its body as a command; its paths count like any other.\n for (const body of substitutions) found.push(...references(path, body, nesting + 1))\n return found\n}\n\n/**\n * Every path token the shell text names, one segment at a time.\n *\n * @throws {StdError.StdError} `invalid_input` when substitutions or quotes nest\n * deeper than the scan reads.\n * @category pre-check\n * @since 1.0.0\n */\nexport const commandReferences = (path: Path.Path, command: string): ReadonlyArray<PathReference> =>\n references(path, command, 0)\n\nconst resolvePath = (\n path: Path.Path,\n cwd: string,\n value: string\n): string => path.isAbsolute(value) ? path.normalize(value) : path.resolve(cwd, value)\n\nconst isDeclared = (\n path: Path.Path,\n cwd: string,\n declared: ReadonlyArray<string>,\n source: string\n): boolean =>\n withinEnvelope(\n declared.map((entry) => resolvePath(path, cwd, entry)),\n resolvePath(path, cwd, source)\n )\n\n/**\n * The refusal shell text earns by naming a path outside its declared\n * envelope, or `undefined` when every path it names is declared.\n *\n * @category pre-check\n * @since 1.0.0\n */\nexport const outsideEnvelope = (\n input: Envelope,\n text: string,\n path: Path.Path\n): StdError.StdError | undefined => {\n const base = path.resolve(\".\")\n const cwd = path.resolve(input.cwd ?? \".\")\n // Passing the default explicitly is not a declaration the caller owes. A\n // working directory is where the command runs, not a file it reads, and\n // `cwd: \".\"` names exactly what omitting `cwd` names \u2014 so refusing it asked\n // the caller to list the workspace root among its reads to say nothing at\n // all. Agents hit this constantly: one SWE-bench run spent ten of its\n // forty-eight tool calls re-issuing the same command after\n // \"Working directory is outside declared reads: <the workspace root>\". A\n // cwd that points somewhere else is still declared or refused.\n if (input.cwd !== undefined && cwd !== base && !isDeclared(path, cwd, input.reads, cwd)) {\n return new StdError.StdError({\n code: \"outside_declared_reads\",\n message: `Working directory is outside declared reads: ${cwd}`,\n path: cwd\n })\n }\n\n // The Shell contract currently has no sandbox or access-reporting surface.\n // This lexical scan is intentionally only a fail-closed pre-check for\n // explicit path tokens. It cannot prove confinement; a kernel/host sandbox\n // must eventually enforce and report the complete read and write sets.\n let named: ReadonlyArray<PathReference>\n try {\n named = commandReferences(path, text)\n } catch {\n // The depth bound is the one failure the scan raises, and a text the scan\n // cannot read is refused whatever the reason, never run unchecked.\n return tooDeep()\n }\n for (const reference of named) {\n const resolved = resolvePath(path, cwd, reference.value)\n // /dev/* is the process plumbing every shell one-liner leans on:\n // `2>/dev/null`, `cat /dev/stdin`. Normalize first so a dot-dot segment\n // cannot disguise an ordinary filesystem path as process plumbing.\n if (resolved.startsWith(\"/dev/\")) continue\n const declared = reference.access === \"write\" ? input.writes : input.reads\n if (!isDeclared(path, cwd, declared, resolved)) {\n const code = reference.access === \"write\" ? \"outside_declared_writes\" : \"outside_declared_reads\"\n return new StdError.StdError({\n code,\n message: `Command path is outside declared ${reference.access}s: ${resolved}`,\n path: resolved\n })\n }\n }\n return undefined\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
7
|
}
|
|
@@ -24,7 +24,7 @@ import * as StdError from "../StdError.ts";
|
|
|
24
24
|
* @category models
|
|
25
25
|
* @since 1.0.0
|
|
26
26
|
*/
|
|
27
|
-
export declare const ExecErrorCode: Schema.Literals<readonly ["timeout", "spawn_error", "capture_overflow"]>;
|
|
27
|
+
export declare const ExecErrorCode: Schema.Literals<readonly ["timeout", "spawn_error", "arguments_too_long", "capture_overflow"]>;
|
|
28
28
|
/**
|
|
29
29
|
* Failure codes a buffered execution can report.
|
|
30
30
|
*
|
|
@@ -33,7 +33,7 @@ export declare const ExecErrorCode: Schema.Literals<readonly ["timeout", "spawn_
|
|
|
33
33
|
*/
|
|
34
34
|
export type ExecErrorCode = typeof ExecErrorCode.Type;
|
|
35
35
|
declare const ExecError_base: Schema.Class<ExecError, Schema.TaggedStruct<"@smthrs/std/ExecError", {
|
|
36
|
-
readonly code: Schema.Literals<readonly ["timeout", "spawn_error", "capture_overflow"]>;
|
|
36
|
+
readonly code: Schema.Literals<readonly ["timeout", "spawn_error", "arguments_too_long", "capture_overflow"]>;
|
|
37
37
|
readonly message: Schema.String;
|
|
38
38
|
/** The limit a `timeout` ran past, in milliseconds. */
|
|
39
39
|
readonly limitMillis: Schema.optional<Schema.Number>;
|
|
@@ -42,8 +42,10 @@ declare const ExecError_base: Schema.Class<ExecError, Schema.TaggedStruct<"@smth
|
|
|
42
42
|
* A command that never produced an exit code.
|
|
43
43
|
*
|
|
44
44
|
* A non-zero exit is a successful {@link ExecResult}, not a failure; this
|
|
45
|
-
* error is reserved for a command that could not start
|
|
46
|
-
*
|
|
45
|
+
* error is reserved for a command that could not start (`arguments_too_long`
|
|
46
|
+
* when the operating system refused it for the size of its arguments) or was
|
|
47
|
+
* cut off by its timeout, or whose captured output exceeded a refusing capture
|
|
48
|
+
* bound.
|
|
47
49
|
*
|
|
48
50
|
* @category errors
|
|
49
51
|
* @since 1.0.0
|
|
@@ -125,7 +127,9 @@ export declare const exec: (command: string, options?: ExecOptions) => Effect.Ef
|
|
|
125
127
|
*
|
|
126
128
|
* A timeout keeps its own code, because "the command never finished" and "the
|
|
127
129
|
* command never started" are different things to a caller deciding whether to
|
|
128
|
-
* retry.
|
|
130
|
+
* retry. Arguments the operating system refused for their size are the
|
|
131
|
+
* caller's own input, which a shell flow publishes, so they read as
|
|
132
|
+
* `invalid_input`. Every other way of producing no exit code is a host failure.
|
|
129
133
|
*
|
|
130
134
|
* @category errors
|
|
131
135
|
* @since 1.0.0
|
|
@@ -135,8 +139,9 @@ export declare const toStdErrorCode: (error: ExecError) => StdError.Code;
|
|
|
135
139
|
* The model-facing failure one execution error becomes.
|
|
136
140
|
*
|
|
137
141
|
* A timeout names the command, which is what the caller asked for and all a
|
|
138
|
-
* timeout has to say.
|
|
139
|
-
*
|
|
142
|
+
* timeout has to say. Arguments too long to start carry the reason and the
|
|
143
|
+
* fix, and nothing of the command. Any other failure carries the message the
|
|
144
|
+
* execution produced, which already names the command.
|
|
140
145
|
*
|
|
141
146
|
* @category errors
|
|
142
147
|
* @since 1.0.0
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Exec.d.ts","sourceRoot":"","sources":["../../../src/internal/Exec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,mBAAmB,MAAM,oCAAoC,CAAA;AACzE,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAG1C;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,
|
|
1
|
+
{"version":3,"file":"Exec.d.ts","sourceRoot":"","sources":["../../../src/internal/Exec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,mBAAmB,MAAM,oCAAoC,CAAA;AACzE,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAG1C;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,gGAAwF,CAAA;AAElH;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAC,IAAI,CAAA;;;;IAiBnD,uDAAuD;;;AAfzD;;;;;;;;;;;GAWG;AACH,qBAAa,SAAU,SAAQ,cAK7B;CAAG;AAEL;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,sFAAsF;IACtF,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAA;IACnC,sFAAsF;IACtF,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAA;CACpC;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAAA;IAC3D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,SAAS,CAAA;IACjD;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC;;;;;;;;OAQG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7C;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;CACzC;AAED,KAAK,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;AA2O1C;;;;;;;;;GASG;AACH,eAAO,MAAM,IAAI,GACf,SAAS,MAAM,EACf,UAAS,WAAgB,KACxB,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,SAAS,EAAE,mBAAmB,CAAC,mBAAmB,CAazE,CAAA;AAEN;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,SAAS,KAAG,QAAQ,CAAC,IACsD,CAAA;AAEjH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,OAAO,SAAS,KAAG,QAAQ,CAAC,QASpE,CAAA"}
|
|
@@ -43,7 +43,7 @@ var Schema = __toESM(require("effect/Schema"), 1);
|
|
|
43
43
|
var Stream = __toESM(require("effect/Stream"), 1);
|
|
44
44
|
var StdError = __toESM(require("../StdError.js"), 1);
|
|
45
45
|
var import_Text = require("./Text.js");
|
|
46
|
-
const ExecErrorCode = Schema.Literals(["timeout", "spawn_error", "capture_overflow"]);
|
|
46
|
+
const ExecErrorCode = Schema.Literals(["timeout", "spawn_error", "arguments_too_long", "capture_overflow"]);
|
|
47
47
|
class ExecError extends Schema.TaggedError()("@smthrs/std/ExecError", {
|
|
48
48
|
code: ExecErrorCode,
|
|
49
49
|
message: Schema.String,
|
|
@@ -127,6 +127,10 @@ const boundedEnds = (stream, maxBytes) => {
|
|
|
127
127
|
}).pipe(Effect.map(decodeEnds));
|
|
128
128
|
};
|
|
129
129
|
const capture = (stream, maxBytes, overflow) => maxBytes === void 0 ? Effect.map(Stream.mkString(Stream.decodeText(stream)), (text) => ({ text, droppedBytes: 0 })) : overflow === "ends" ? boundedEnds(stream, maxBytes) : boundedTail(stream, maxBytes, overflow);
|
|
130
|
+
const argumentsTooLong = (error) => error.cause?.code === "E2BIG" ? new ExecError({
|
|
131
|
+
code: "arguments_too_long",
|
|
132
|
+
message: "Command not started: its arguments exceed the operating system's size limit (E2BIG). Send long text on standard input or in a file, not in the command line or its arguments."
|
|
133
|
+
}) : void 0;
|
|
130
134
|
const unbounded = (command, options) => Effect.gen(function* () {
|
|
131
135
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
132
136
|
const settings = {
|
|
@@ -171,7 +175,7 @@ const unbounded = (command, options) => Effect.gen(function* () {
|
|
|
171
175
|
}).pipe(
|
|
172
176
|
Effect.scoped,
|
|
173
177
|
Effect.mapError(
|
|
174
|
-
(error) => error instanceof ExecError ? error : new ExecError({
|
|
178
|
+
(error) => error instanceof ExecError ? error : argumentsTooLong(error) ?? new ExecError({
|
|
175
179
|
code: "spawn_error",
|
|
176
180
|
message: `exec: ${command}: ${error.message}`
|
|
177
181
|
})
|
|
@@ -187,10 +191,10 @@ const exec = (command, options = {}) => options.timeoutMs === void 0 ? unbounded
|
|
|
187
191
|
})
|
|
188
192
|
)
|
|
189
193
|
});
|
|
190
|
-
const toStdErrorCode = (error) => error.code === "timeout" ? "timeout" : "command_failed";
|
|
194
|
+
const toStdErrorCode = (error) => error.code === "timeout" ? "timeout" : error.code === "arguments_too_long" ? "invalid_input" : "command_failed";
|
|
191
195
|
const toStdError = (command, error) => new StdError.StdError({
|
|
192
196
|
code: toStdErrorCode(error),
|
|
193
|
-
message: error.code === "timeout" ? `Command timed out: ${command}` : `Command failed to start: ${error.message}`,
|
|
197
|
+
message: error.code === "timeout" ? `Command timed out: ${command}` : error.code === "arguments_too_long" ? error.message : `Command failed to start: ${error.message}`,
|
|
194
198
|
...error.limitMillis === void 0 ? {} : { limitMillis: error.limitMillis }
|
|
195
199
|
});
|
|
196
200
|
//# sourceMappingURL=Exec.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/internal/Exec.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Buffered shell execution over the permission-aware process spawner.\n *\n * `@smthrs/kernel` used to expose a `Shell` service whose `exec` returned a\n * buffered `{ stdout, stderr, exitCode }`. The kernel replaced it with\n * `ChildProcessSpawner`, Effect's own process boundary, which streams and has\n * no buffered form. This module is the buffered form the shell flows still\n * want, expressed in terms of the spawner so the `proc:spawn` capability check\n * and the command-line resource stay exactly where the kernel put them.\n *\n * Commands run through the platform shell (`shell: true`), matching the old\n * service: the flows take a command *line*, not an argv.\n *\n * @since 1.0.0\n */\n\nimport * as ChildProcessEnvironment from \"@smthrs/kernel/ChildProcessEnvironment\"\nimport * as ChildProcessSpawner from \"@smthrs/kernel/ChildProcessSpawner\"\nimport * as Effect from \"effect/Effect\"\nimport * as ChildProcess from \"effect/process/ChildProcess\"\nimport * as Schema from \"effect/Schema\"\nimport * as Stream from \"effect/Stream\"\nimport * as StdError from \"../StdError.ts\"\nimport { elideMiddle } from \"./Text.ts\"\n\n/**\n * Failure codes a buffered execution can report, matching the codes the\n * removed kernel `Shell` used so handler-level mapping is unchanged.\n *\n * @category models\n * @since 1.0.0\n */\nexport const ExecErrorCode = Schema.Literals([\"timeout\", \"spawn_error\", \"capture_overflow\"])\n\n/**\n * Failure codes a buffered execution can report.\n *\n * @category models\n * @since 1.0.0\n */\nexport type ExecErrorCode = typeof ExecErrorCode.Type\n\n/**\n * A command that never produced an exit code.\n *\n * A non-zero exit is a successful {@link ExecResult}, not a failure; this\n * error is reserved for a command that could not start or was cut off by its\n * timeout, or whose captured output exceeded a refusing capture bound.\n *\n * @category errors\n * @since 1.0.0\n */\nexport class ExecError extends Schema.TaggedError<ExecError>()(\"@smthrs/std/ExecError\", {\n code: ExecErrorCode,\n message: Schema.String,\n /** The limit a `timeout` ran past, in milliseconds. */\n limitMillis: Schema.optional(Schema.Number)\n}) {}\n\n/**\n * The buffered result of one command.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface ExecResult {\n readonly stdout: string\n readonly stderr: string\n readonly exitCode: number\n /** UTF-8 bytes of stdout dropped to stay inside the capture bound; see `overflow`. */\n readonly stdoutDroppedBytes: number\n /** UTF-8 bytes of stderr dropped to stay inside the capture bound; see `overflow`. */\n readonly stderrDroppedBytes: number\n}\n\n/**\n * Options accepted by {@link exec}.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface ExecOptions {\n readonly cwd?: string | undefined\n readonly env?: Readonly<Record<string, string>> | undefined\n readonly timeoutMs?: number | undefined\n /**\n * The program's arguments. Present means an argv, absent means a command\n * line for the platform shell. An argv is how a payload reaches a program\n * without being quoted into a string: nothing between here and `execve`\n * re-parses it.\n */\n readonly args?: ReadonlyArray<string> | undefined\n /**\n * Text written to the program's standard input, as data. A script delivered\n * this way is never quoted, escaped, or heredoc-terminated, which is the\n * whole point: quoting corruption cost the measured SWE-bench program twelve\n * probe failures and one instance's most expensive frame.\n */\n readonly stdin?: string | undefined\n /**\n * Bytes retained per captured stream. A stream that prints more keeps what\n * `overflow` names and reports what it dropped, so a command printing\n * gigabytes costs a bounded amount of memory rather than the whole of what it\n * printed.\n *\n * Absent means unbounded, which is what a caller wants when it needs every\n * byte a tool produced (`rg --files`, `git worktree list`).\n */\n readonly maxCaptureBytes?: number | undefined\n /**\n * What a stream past the bound keeps. `tail`, the default, keeps its last\n * bytes. `ends` keeps its first and last halves and puts one line naming the\n * dropped bytes where the middle was. `refuse` fails if either stream exceeds\n * the bound.\n */\n readonly overflow?: Overflow | undefined\n}\n\ntype Overflow = \"tail\" | \"ends\" | \"refuse\"\n\n/** One captured stream: the text kept, and the bytes dropped to keep it bounded. */\ninterface Capture {\n readonly text: string\n readonly droppedBytes: number\n}\n\ninterface Tail {\n chunks: Array<Uint8Array>\n bytes: number\n dropped: number\n}\n\n/** A stream's first bytes and its last ones: what `overflow: \"ends\"` keeps. */\ninterface Ends {\n readonly head: Array<Uint8Array>\n headBytes: number\n readonly tail: Tail\n}\n\nconst decoder = new TextDecoder(\"utf-8\")\n\nconst emptyTail = (): Tail => ({ chunks: [], bytes: 0, dropped: 0 })\n\n/** A UTF-8 continuation byte is `10xxxxxx`; every other byte starts a character. */\nconst isContinuation = (byte: number): boolean => (byte & 0b1100_0000) === 0b1000_0000\n\nconst join = (chunks: ReadonlyArray<Uint8Array>, size: number): Uint8Array => {\n const bytes = new Uint8Array(size)\n let offset = 0\n for (const chunk of chunks) {\n bytes.set(chunk, offset)\n offset += chunk.byteLength\n }\n return bytes\n}\n\n/** Appends one chunk and drops from the front whatever no longer fits. */\nconst keepTail = (state: Tail, chunk: Uint8Array, maxBytes: number): void => {\n if (chunk.byteLength === 0) return\n state.chunks.push(chunk)\n state.bytes += chunk.byteLength\n while (state.bytes > maxBytes && state.chunks.length > 0) {\n const excess = state.bytes - maxBytes\n const first = state.chunks[0]!\n if (first.byteLength <= excess) {\n state.chunks.shift()\n state.bytes -= first.byteLength\n state.dropped += first.byteLength\n } else {\n state.chunks[0] = first.subarray(excess)\n state.bytes -= excess\n state.dropped += excess\n }\n }\n}\n\n/**\n * Joins the retained chunks and decodes them, skipping the partial code point a\n * dropped head can leave behind.\n *\n * A code point is at most four bytes, so at most three leading continuation\n * bytes can precede the first whole character. Skipping them is counted as\n * dropped rather than decoded to a replacement character the process never\n * printed.\n */\nconst decodeTail = (state: Tail): Capture => {\n const bytes = join(state.chunks, state.bytes)\n if (state.dropped === 0) return { text: decoder.decode(bytes), droppedBytes: 0 }\n let start = 0\n while (start < bytes.byteLength && start < 3 && isContinuation(bytes[start]!)) start++\n return { text: decoder.decode(bytes.subarray(start)), droppedBytes: state.dropped + start }\n}\n\n/**\n * The length of a cut head without the character the cut left unfinished.\n *\n * Its last character starts at the last byte that is not a continuation byte,\n * and that byte says how many bytes the character needs. One that needs more\n * than the head holds is dropped, for the same reason {@link decodeTail} skips\n * a partial code point.\n */\nconst wholeLength = (bytes: Uint8Array): number => {\n for (let back = 1; back <= Math.min(3, bytes.byteLength); back++) {\n const byte = bytes[bytes.byteLength - back]!\n if (isContinuation(byte)) continue\n const width = byte >= 0b1111_0000 ? 4 : byte >= 0b1110_0000 ? 3 : byte >= 0b1100_0000 ? 2 : 1\n return width > back ? bytes.byteLength - back : bytes.byteLength\n }\n return bytes.byteLength\n}\n\n/**\n * Decodes a stream kept by its ends. A stream that fit is decoded whole;\n * otherwise each end is cut on a character boundary and one line naming every\n * byte between them stands where the middle was.\n */\nconst decodeEnds = (state: Ends): Capture => {\n if (state.tail.dropped === 0) {\n const whole = join([...state.head, ...state.tail.chunks], state.headBytes + state.tail.bytes)\n return { text: decoder.decode(whole), droppedBytes: 0 }\n }\n const head = join(state.head, state.headBytes)\n const kept = wholeLength(head)\n const tail = decodeTail(state.tail)\n const droppedBytes = head.byteLength - kept + tail.droppedBytes\n return { text: elideMiddle(decoder.decode(head.subarray(0, kept)), droppedBytes, tail.text), droppedBytes }\n}\n\nconst boundedTail = <E>(\n stream: Stream.Stream<Uint8Array, E>,\n maxBytes: number,\n overflow: Overflow | undefined\n): Effect.Effect<Capture, E> =>\n Stream.runFold(stream, emptyTail, (state, chunk) => {\n keepTail(state, chunk, maxBytes)\n return state\n }).pipe(Effect.map((state) =>\n overflow === \"refuse\" && state.dropped > 0\n ? { text: \"\", droppedBytes: state.dropped }\n : decodeTail(state)\n ))\n\n/**\n * Keeps the first half of the bound and the last half. The head fills first,\n * and every byte after it passes through the tail, which drops from its front:\n * what falls out of the tail is the middle.\n */\nconst boundedEnds = <E>(stream: Stream.Stream<Uint8Array, E>, maxBytes: number): Effect.Effect<Capture, E> => {\n const headBytes = Math.floor(maxBytes / 2)\n return Stream.runFold(stream, (): Ends => ({ head: [], headBytes: 0, tail: emptyTail() }), (state, chunk) => {\n const taken = Math.min(headBytes - state.headBytes, chunk.byteLength)\n if (taken > 0) {\n state.head.push(chunk.subarray(0, taken))\n state.headBytes += taken\n }\n keepTail(state.tail, chunk.subarray(taken), maxBytes - headBytes)\n return state\n }).pipe(Effect.map(decodeEnds))\n}\n\nconst capture = <E>(\n stream: Stream.Stream<Uint8Array, E>,\n maxBytes: number | undefined,\n overflow: Overflow | undefined\n): Effect.Effect<Capture, E> =>\n maxBytes === undefined\n ? Effect.map(Stream.mkString(Stream.decodeText(stream)), (text) => ({ text, droppedBytes: 0 }))\n : overflow === \"ends\"\n ? boundedEnds(stream, maxBytes)\n : boundedTail(stream, maxBytes, overflow)\n\nconst unbounded = (\n command: string,\n options: ExecOptions\n): Effect.Effect<ExecResult, ExecError, ChildProcessSpawner.ChildProcessSpawner> =>\n Effect.gen(function*() {\n const spawner = yield* ChildProcessSpawner.ChildProcessSpawner\n const settings: ChildProcess.CommandOptions = {\n ...(options.args === undefined ? { shell: true } : {}),\n ...(options.cwd === undefined ? {} : { cwd: options.cwd }),\n env: ChildProcessEnvironment.make(process.env, options.env),\n extendEnv: false,\n ...(options.stdin === undefined\n ? {}\n : { stdin: Stream.make(new TextEncoder().encode(options.stdin)) })\n }\n const handle = yield* spawner.spawn(\n options.args === undefined\n ? ChildProcess.make(command, settings)\n : ChildProcess.make(command, [...options.args], settings)\n )\n // stdout, stderr, and the exit code have to be consumed concurrently: a\n // command that fills a pipe blocks until the pipe is drained, so waiting\n // for exit first would deadlock on any output larger than the buffer.\n const [stdout, stderr, exitCode] = yield* Effect.all(\n [\n capture(handle.stdout, options.maxCaptureBytes, options.overflow),\n capture(handle.stderr, options.maxCaptureBytes, options.overflow),\n handle.exitCode\n ],\n { concurrency: \"unbounded\" }\n )\n if (options.overflow === \"refuse\") {\n const overflowed = [\n stdout.droppedBytes > 0 ? \"stdout\" : undefined,\n stderr.droppedBytes > 0 ? \"stderr\" : undefined\n ]\n .filter((stream): stream is string => stream !== undefined)\n if (overflowed.length > 0) {\n return yield* Effect.fail(\n new ExecError({\n code: \"capture_overflow\",\n message: `${command} ${\n overflowed.join(\" and \")\n } exceeded the ${options.maxCaptureBytes}-byte capture cap; refusing partial output`\n })\n )\n }\n }\n return {\n stdout: stdout.text,\n stderr: stderr.text,\n exitCode,\n stdoutDroppedBytes: stdout.droppedBytes,\n stderrDroppedBytes: stderr.droppedBytes\n }\n }).pipe(\n Effect.scoped,\n Effect.mapError((error) =>\n error instanceof ExecError ? error : new ExecError({\n code: \"spawn_error\",\n message: `exec: ${command}: ${error.message}`\n })\n )\n )\n\n/**\n * Runs a command line through the platform shell and buffers its output.\n *\n * Cancellation is scope closure: the spawn is scoped, so an interrupted fiber\n * \u2014 including the one a `timeoutMs` interrupts \u2014 terminates the process rather\n * than leaking it.\n *\n * @category execution\n * @since 1.0.0\n */\nexport const exec = (\n command: string,\n options: ExecOptions = {}\n): Effect.Effect<ExecResult, ExecError, ChildProcessSpawner.ChildProcessSpawner> =>\n options.timeoutMs === undefined\n ? unbounded(command, options)\n : Effect.timeoutOrElse(unbounded(command, options), {\n duration: options.timeoutMs,\n orElse: () =>\n Effect.fail(\n new ExecError({\n code: \"timeout\",\n message: `exec: \\`${command}\\` exceeded ${options.timeoutMs}ms`,\n limitMillis: options.timeoutMs\n })\n )\n })\n\n/**\n * The model-facing failure code a command that never produced an exit code\n * carries.\n *\n * A timeout keeps its own code, because \"the command never finished\" and \"the\n * command never started\" are different things to a caller deciding whether to\n * retry. Every other way of producing no exit code is a host failure.\n *\n * @category errors\n * @since 1.0.0\n */\nexport const toStdErrorCode = (error: ExecError): StdError.Code =>\n error.code === \"timeout\" ? \"timeout\" : \"command_failed\"\n\n/**\n * The model-facing failure one execution error becomes.\n *\n * A timeout names the command, which is what the caller asked for and all a\n * timeout has to say. Any other failure carries the message the execution\n * produced, which already names the command.\n *\n * @category errors\n * @since 1.0.0\n */\nexport const toStdError = (command: string, error: ExecError): StdError.StdError =>\n new StdError.StdError({\n code: toStdErrorCode(error),\n message: error.code === \"timeout\"\n ? `Command timed out: ${command}`\n : `Command failed to start: ${error.message}`,\n ...(error.limitMillis === undefined ? {} : { limitMillis: error.limitMillis })\n })\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,8BAAyC;AACzC,0BAAqC;AACrC,aAAwB;AACxB,mBAA8B;AAC9B,aAAwB;AACxB,aAAwB;AACxB,eAA0B;AAC1B,kBAA4B;AASrB,MAAM,gBAAgB,OAAO,SAAS,CAAC,WAAW,eAAe,kBAAkB,CAAC;
|
|
4
|
+
"sourcesContent": ["/**\n * Buffered shell execution over the permission-aware process spawner.\n *\n * `@smthrs/kernel` used to expose a `Shell` service whose `exec` returned a\n * buffered `{ stdout, stderr, exitCode }`. The kernel replaced it with\n * `ChildProcessSpawner`, Effect's own process boundary, which streams and has\n * no buffered form. This module is the buffered form the shell flows still\n * want, expressed in terms of the spawner so the `proc:spawn` capability check\n * and the command-line resource stay exactly where the kernel put them.\n *\n * Commands run through the platform shell (`shell: true`), matching the old\n * service: the flows take a command *line*, not an argv.\n *\n * @since 1.0.0\n */\n\nimport * as ChildProcessEnvironment from \"@smthrs/kernel/ChildProcessEnvironment\"\nimport * as ChildProcessSpawner from \"@smthrs/kernel/ChildProcessSpawner\"\nimport * as Effect from \"effect/Effect\"\nimport * as ChildProcess from \"effect/process/ChildProcess\"\nimport * as Schema from \"effect/Schema\"\nimport * as Stream from \"effect/Stream\"\nimport * as StdError from \"../StdError.ts\"\nimport { elideMiddle } from \"./Text.ts\"\n\n/**\n * Failure codes a buffered execution can report, matching the codes the\n * removed kernel `Shell` used so handler-level mapping is unchanged.\n *\n * @category models\n * @since 1.0.0\n */\nexport const ExecErrorCode = Schema.Literals([\"timeout\", \"spawn_error\", \"arguments_too_long\", \"capture_overflow\"])\n\n/**\n * Failure codes a buffered execution can report.\n *\n * @category models\n * @since 1.0.0\n */\nexport type ExecErrorCode = typeof ExecErrorCode.Type\n\n/**\n * A command that never produced an exit code.\n *\n * A non-zero exit is a successful {@link ExecResult}, not a failure; this\n * error is reserved for a command that could not start (`arguments_too_long`\n * when the operating system refused it for the size of its arguments) or was\n * cut off by its timeout, or whose captured output exceeded a refusing capture\n * bound.\n *\n * @category errors\n * @since 1.0.0\n */\nexport class ExecError extends Schema.TaggedError<ExecError>()(\"@smthrs/std/ExecError\", {\n code: ExecErrorCode,\n message: Schema.String,\n /** The limit a `timeout` ran past, in milliseconds. */\n limitMillis: Schema.optional(Schema.Number)\n}) {}\n\n/**\n * The buffered result of one command.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface ExecResult {\n readonly stdout: string\n readonly stderr: string\n readonly exitCode: number\n /** UTF-8 bytes of stdout dropped to stay inside the capture bound; see `overflow`. */\n readonly stdoutDroppedBytes: number\n /** UTF-8 bytes of stderr dropped to stay inside the capture bound; see `overflow`. */\n readonly stderrDroppedBytes: number\n}\n\n/**\n * Options accepted by {@link exec}.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface ExecOptions {\n readonly cwd?: string | undefined\n readonly env?: Readonly<Record<string, string>> | undefined\n readonly timeoutMs?: number | undefined\n /**\n * The program's arguments. Present means an argv, absent means a command\n * line for the platform shell. An argv is how a payload reaches a program\n * without being quoted into a string: nothing between here and `execve`\n * re-parses it.\n */\n readonly args?: ReadonlyArray<string> | undefined\n /**\n * Text written to the program's standard input, as data. A script delivered\n * this way is never quoted, escaped, or heredoc-terminated, which is the\n * whole point: quoting corruption cost the measured SWE-bench program twelve\n * probe failures and one instance's most expensive frame.\n */\n readonly stdin?: string | undefined\n /**\n * Bytes retained per captured stream. A stream that prints more keeps what\n * `overflow` names and reports what it dropped, so a command printing\n * gigabytes costs a bounded amount of memory rather than the whole of what it\n * printed.\n *\n * Absent means unbounded, which is what a caller wants when it needs every\n * byte a tool produced (`rg --files`, `git worktree list`).\n */\n readonly maxCaptureBytes?: number | undefined\n /**\n * What a stream past the bound keeps. `tail`, the default, keeps its last\n * bytes. `ends` keeps its first and last halves and puts one line naming the\n * dropped bytes where the middle was. `refuse` fails if either stream exceeds\n * the bound.\n */\n readonly overflow?: Overflow | undefined\n}\n\ntype Overflow = \"tail\" | \"ends\" | \"refuse\"\n\n/** One captured stream: the text kept, and the bytes dropped to keep it bounded. */\ninterface Capture {\n readonly text: string\n readonly droppedBytes: number\n}\n\ninterface Tail {\n chunks: Array<Uint8Array>\n bytes: number\n dropped: number\n}\n\n/** A stream's first bytes and its last ones: what `overflow: \"ends\"` keeps. */\ninterface Ends {\n readonly head: Array<Uint8Array>\n headBytes: number\n readonly tail: Tail\n}\n\nconst decoder = new TextDecoder(\"utf-8\")\n\nconst emptyTail = (): Tail => ({ chunks: [], bytes: 0, dropped: 0 })\n\n/** A UTF-8 continuation byte is `10xxxxxx`; every other byte starts a character. */\nconst isContinuation = (byte: number): boolean => (byte & 0b1100_0000) === 0b1000_0000\n\nconst join = (chunks: ReadonlyArray<Uint8Array>, size: number): Uint8Array => {\n const bytes = new Uint8Array(size)\n let offset = 0\n for (const chunk of chunks) {\n bytes.set(chunk, offset)\n offset += chunk.byteLength\n }\n return bytes\n}\n\n/** Appends one chunk and drops from the front whatever no longer fits. */\nconst keepTail = (state: Tail, chunk: Uint8Array, maxBytes: number): void => {\n if (chunk.byteLength === 0) return\n state.chunks.push(chunk)\n state.bytes += chunk.byteLength\n while (state.bytes > maxBytes && state.chunks.length > 0) {\n const excess = state.bytes - maxBytes\n const first = state.chunks[0]!\n if (first.byteLength <= excess) {\n state.chunks.shift()\n state.bytes -= first.byteLength\n state.dropped += first.byteLength\n } else {\n state.chunks[0] = first.subarray(excess)\n state.bytes -= excess\n state.dropped += excess\n }\n }\n}\n\n/**\n * Joins the retained chunks and decodes them, skipping the partial code point a\n * dropped head can leave behind.\n *\n * A code point is at most four bytes, so at most three leading continuation\n * bytes can precede the first whole character. Skipping them is counted as\n * dropped rather than decoded to a replacement character the process never\n * printed.\n */\nconst decodeTail = (state: Tail): Capture => {\n const bytes = join(state.chunks, state.bytes)\n if (state.dropped === 0) return { text: decoder.decode(bytes), droppedBytes: 0 }\n let start = 0\n while (start < bytes.byteLength && start < 3 && isContinuation(bytes[start]!)) start++\n return { text: decoder.decode(bytes.subarray(start)), droppedBytes: state.dropped + start }\n}\n\n/**\n * The length of a cut head without the character the cut left unfinished.\n *\n * Its last character starts at the last byte that is not a continuation byte,\n * and that byte says how many bytes the character needs. One that needs more\n * than the head holds is dropped, for the same reason {@link decodeTail} skips\n * a partial code point.\n */\nconst wholeLength = (bytes: Uint8Array): number => {\n for (let back = 1; back <= Math.min(3, bytes.byteLength); back++) {\n const byte = bytes[bytes.byteLength - back]!\n if (isContinuation(byte)) continue\n const width = byte >= 0b1111_0000 ? 4 : byte >= 0b1110_0000 ? 3 : byte >= 0b1100_0000 ? 2 : 1\n return width > back ? bytes.byteLength - back : bytes.byteLength\n }\n return bytes.byteLength\n}\n\n/**\n * Decodes a stream kept by its ends. A stream that fit is decoded whole;\n * otherwise each end is cut on a character boundary and one line naming every\n * byte between them stands where the middle was.\n */\nconst decodeEnds = (state: Ends): Capture => {\n if (state.tail.dropped === 0) {\n const whole = join([...state.head, ...state.tail.chunks], state.headBytes + state.tail.bytes)\n return { text: decoder.decode(whole), droppedBytes: 0 }\n }\n const head = join(state.head, state.headBytes)\n const kept = wholeLength(head)\n const tail = decodeTail(state.tail)\n const droppedBytes = head.byteLength - kept + tail.droppedBytes\n return { text: elideMiddle(decoder.decode(head.subarray(0, kept)), droppedBytes, tail.text), droppedBytes }\n}\n\nconst boundedTail = <E>(\n stream: Stream.Stream<Uint8Array, E>,\n maxBytes: number,\n overflow: Overflow | undefined\n): Effect.Effect<Capture, E> =>\n Stream.runFold(stream, emptyTail, (state, chunk) => {\n keepTail(state, chunk, maxBytes)\n return state\n }).pipe(Effect.map((state) =>\n overflow === \"refuse\" && state.dropped > 0\n ? { text: \"\", droppedBytes: state.dropped }\n : decodeTail(state)\n ))\n\n/**\n * Keeps the first half of the bound and the last half. The head fills first,\n * and every byte after it passes through the tail, which drops from its front:\n * what falls out of the tail is the middle.\n */\nconst boundedEnds = <E>(stream: Stream.Stream<Uint8Array, E>, maxBytes: number): Effect.Effect<Capture, E> => {\n const headBytes = Math.floor(maxBytes / 2)\n return Stream.runFold(stream, (): Ends => ({ head: [], headBytes: 0, tail: emptyTail() }), (state, chunk) => {\n const taken = Math.min(headBytes - state.headBytes, chunk.byteLength)\n if (taken > 0) {\n state.head.push(chunk.subarray(0, taken))\n state.headBytes += taken\n }\n keepTail(state.tail, chunk.subarray(taken), maxBytes - headBytes)\n return state\n }).pipe(Effect.map(decodeEnds))\n}\n\nconst capture = <E>(\n stream: Stream.Stream<Uint8Array, E>,\n maxBytes: number | undefined,\n overflow: Overflow | undefined\n): Effect.Effect<Capture, E> =>\n maxBytes === undefined\n ? Effect.map(Stream.mkString(Stream.decodeText(stream)), (text) => ({ text, droppedBytes: 0 }))\n : overflow === \"ends\"\n ? boundedEnds(stream, maxBytes)\n : boundedTail(stream, maxBytes, overflow)\n\n/**\n * A command whose arguments exceed the operating system's limit fails `execve`\n * with `E2BIG` before it starts: one argument over 128 KiB on Linux, all of\n * them over 1 MiB on macOS. A heredoc in a command line, or a file's text\n * passed as an argument, is one such argument. The platform error's own text\n * repeats the whole command line, so this text is written here instead, and a\n * caller may show it as it stands.\n */\nconst argumentsTooLong = (error: { readonly cause?: unknown }): ExecError | undefined =>\n (error.cause as { readonly code?: unknown } | undefined)?.code === \"E2BIG\"\n ? new ExecError({\n code: \"arguments_too_long\",\n message:\n \"Command not started: its arguments exceed the operating system's size limit (E2BIG). Send long text on standard input or in a file, not in the command line or its arguments.\"\n })\n : undefined\n\nconst unbounded = (\n command: string,\n options: ExecOptions\n): Effect.Effect<ExecResult, ExecError, ChildProcessSpawner.ChildProcessSpawner> =>\n Effect.gen(function*() {\n const spawner = yield* ChildProcessSpawner.ChildProcessSpawner\n const settings: ChildProcess.CommandOptions = {\n ...(options.args === undefined ? { shell: true } : {}),\n ...(options.cwd === undefined ? {} : { cwd: options.cwd }),\n env: ChildProcessEnvironment.make(process.env, options.env),\n extendEnv: false,\n ...(options.stdin === undefined\n ? {}\n : { stdin: Stream.make(new TextEncoder().encode(options.stdin)) })\n }\n const handle = yield* spawner.spawn(\n options.args === undefined\n ? ChildProcess.make(command, settings)\n : ChildProcess.make(command, [...options.args], settings)\n )\n // stdout, stderr, and the exit code have to be consumed concurrently: a\n // command that fills a pipe blocks until the pipe is drained, so waiting\n // for exit first would deadlock on any output larger than the buffer.\n const [stdout, stderr, exitCode] = yield* Effect.all(\n [\n capture(handle.stdout, options.maxCaptureBytes, options.overflow),\n capture(handle.stderr, options.maxCaptureBytes, options.overflow),\n handle.exitCode\n ],\n { concurrency: \"unbounded\" }\n )\n if (options.overflow === \"refuse\") {\n const overflowed = [\n stdout.droppedBytes > 0 ? \"stdout\" : undefined,\n stderr.droppedBytes > 0 ? \"stderr\" : undefined\n ]\n .filter((stream): stream is string => stream !== undefined)\n if (overflowed.length > 0) {\n return yield* Effect.fail(\n new ExecError({\n code: \"capture_overflow\",\n message: `${command} ${\n overflowed.join(\" and \")\n } exceeded the ${options.maxCaptureBytes}-byte capture cap; refusing partial output`\n })\n )\n }\n }\n return {\n stdout: stdout.text,\n stderr: stderr.text,\n exitCode,\n stdoutDroppedBytes: stdout.droppedBytes,\n stderrDroppedBytes: stderr.droppedBytes\n }\n }).pipe(\n Effect.scoped,\n Effect.mapError((error) =>\n error instanceof ExecError ? error : argumentsTooLong(error) ?? new ExecError({\n code: \"spawn_error\",\n message: `exec: ${command}: ${error.message}`\n })\n )\n )\n\n/**\n * Runs a command line through the platform shell and buffers its output.\n *\n * Cancellation is scope closure: the spawn is scoped, so an interrupted fiber\n * \u2014 including the one a `timeoutMs` interrupts \u2014 terminates the process rather\n * than leaking it.\n *\n * @category execution\n * @since 1.0.0\n */\nexport const exec = (\n command: string,\n options: ExecOptions = {}\n): Effect.Effect<ExecResult, ExecError, ChildProcessSpawner.ChildProcessSpawner> =>\n options.timeoutMs === undefined\n ? unbounded(command, options)\n : Effect.timeoutOrElse(unbounded(command, options), {\n duration: options.timeoutMs,\n orElse: () =>\n Effect.fail(\n new ExecError({\n code: \"timeout\",\n message: `exec: \\`${command}\\` exceeded ${options.timeoutMs}ms`,\n limitMillis: options.timeoutMs\n })\n )\n })\n\n/**\n * The model-facing failure code a command that never produced an exit code\n * carries.\n *\n * A timeout keeps its own code, because \"the command never finished\" and \"the\n * command never started\" are different things to a caller deciding whether to\n * retry. Arguments the operating system refused for their size are the\n * caller's own input, which a shell flow publishes, so they read as\n * `invalid_input`. Every other way of producing no exit code is a host failure.\n *\n * @category errors\n * @since 1.0.0\n */\nexport const toStdErrorCode = (error: ExecError): StdError.Code =>\n error.code === \"timeout\" ? \"timeout\" : error.code === \"arguments_too_long\" ? \"invalid_input\" : \"command_failed\"\n\n/**\n * The model-facing failure one execution error becomes.\n *\n * A timeout names the command, which is what the caller asked for and all a\n * timeout has to say. Arguments too long to start carry the reason and the\n * fix, and nothing of the command. Any other failure carries the message the\n * execution produced, which already names the command.\n *\n * @category errors\n * @since 1.0.0\n */\nexport const toStdError = (command: string, error: ExecError): StdError.StdError =>\n new StdError.StdError({\n code: toStdErrorCode(error),\n message: error.code === \"timeout\"\n ? `Command timed out: ${command}`\n : error.code === \"arguments_too_long\"\n ? error.message\n : `Command failed to start: ${error.message}`,\n ...(error.limitMillis === undefined ? {} : { limitMillis: error.limitMillis })\n })\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tree another machine listed, served as a read-only `FileSystem`.
|
|
3
|
+
*
|
|
4
|
+
* `RemoteSearch` lists a root on the machine with `find` and brings back the
|
|
5
|
+
* bytes the search reads with `tar`; this module holds what came back and
|
|
6
|
+
* answers what the walk, the globs, and the matcher ask of a filesystem, so
|
|
7
|
+
* `PortableSearch` can search the copy as it searches a host.
|
|
8
|
+
*
|
|
9
|
+
* @since 1.0.0-rc.8
|
|
10
|
+
*/
|
|
11
|
+
import type * as Path from "@smthrs/kernel/Path";
|
|
12
|
+
import * as FileSystem from "effect/FileSystem";
|
|
13
|
+
/**
|
|
14
|
+
* What one listed path is: `d` a directory, `u` a directory the machine's
|
|
15
|
+
* user may not list, `f` a file, `o` anything else, and a symbolic link to a
|
|
16
|
+
* directory (`D`), to a file (`F`), or to neither (`L`).
|
|
17
|
+
*
|
|
18
|
+
* @private
|
|
19
|
+
* @since 1.0.0-rc.8
|
|
20
|
+
*/
|
|
21
|
+
export type Kind = "d" | "u" | "f" | "o" | "D" | "F" | "L";
|
|
22
|
+
/**
|
|
23
|
+
* One machine's listing of a root, and the bytes it brought back, by
|
|
24
|
+
* absolute path.
|
|
25
|
+
*
|
|
26
|
+
* @private
|
|
27
|
+
* @since 1.0.0-rc.8
|
|
28
|
+
*/
|
|
29
|
+
export interface Tree {
|
|
30
|
+
readonly kinds: ReadonlyMap<string, Kind>;
|
|
31
|
+
readonly children: ReadonlyMap<string, ReadonlyArray<string>>;
|
|
32
|
+
readonly contents: ReadonlyMap<string, Uint8Array>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The member names `tar` wrote for absolute paths, which it strips of their
|
|
36
|
+
* leading `/`, as those paths again.
|
|
37
|
+
*
|
|
38
|
+
* @private
|
|
39
|
+
* @since 1.0.0-rc.8
|
|
40
|
+
*/
|
|
41
|
+
export declare const absolute: (members: ReadonlyMap<string, Uint8Array>) => ReadonlyMap<string, Uint8Array>;
|
|
42
|
+
/**
|
|
43
|
+
* Reads NUL-terminated `<kind><path>` records up to an `E` record, then the
|
|
44
|
+
* tar stream of `.gitignore` files after it. `root` is the listed root, which
|
|
45
|
+
* is nobody's child. `undefined` when the bytes are not that shape.
|
|
46
|
+
*
|
|
47
|
+
* @private
|
|
48
|
+
* @since 1.0.0-rc.8
|
|
49
|
+
*/
|
|
50
|
+
export declare const parse: (bytes: Uint8Array, root: string, path: Path.Path) => Tree | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* The tree as a read-only `FileSystem`, resolving paths with `path`. A file
|
|
53
|
+
* whose bytes did not come back is one the machine's user could not read,
|
|
54
|
+
* which the in-process peer skips exactly as it skips such a file on a host.
|
|
55
|
+
*
|
|
56
|
+
* @private
|
|
57
|
+
* @since 1.0.0-rc.8
|
|
58
|
+
*/
|
|
59
|
+
export declare const fileSystem: (tree: Tree, path: Path.Path) => FileSystem.FileSystem;
|
|
60
|
+
//# sourceMappingURL=RemoteTree.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RemoteTree.d.ts","sourceRoot":"","sources":["../../../src/internal/RemoteTree.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,KAAK,IAAI,MAAM,qBAAqB,CAAA;AAGhD,OAAO,KAAK,UAAU,MAAM,mBAAmB,CAAA;AAI/C;;;;;;;GAOG;AACH,MAAM,MAAM,IAAI,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;AAE1D;;;;;;GAMG;AACH,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IACzC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;IAC7D,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;CACnD;AAID;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,GAAI,SAAS,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,KAAG,WAAW,CAAC,MAAM,EAAE,UAAU,CAC/B,CAAA;AAEnE;;;;;;;GAOG;AACH,eAAO,MAAM,KAAK,GAAI,OAAO,UAAU,EAAE,MAAM,MAAM,EAAE,MAAM,IAAI,CAAC,IAAI,KAAG,IAAI,GAAG,SAuB/E,CAAA;AAiCD;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,GAAI,MAAM,IAAI,EAAE,MAAM,IAAI,CAAC,IAAI,KAAG,UAAU,CAAC,UAiCnE,CAAA"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var RemoteTree_exports = {};
|
|
30
|
+
__export(RemoteTree_exports, {
|
|
31
|
+
absolute: () => absolute,
|
|
32
|
+
fileSystem: () => fileSystem,
|
|
33
|
+
parse: () => parse
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(RemoteTree_exports);
|
|
36
|
+
var import_effect = require("effect");
|
|
37
|
+
var ByteSize = __toESM(require("effect/ByteSize"), 1);
|
|
38
|
+
var FileSystem = __toESM(require("effect/FileSystem"), 1);
|
|
39
|
+
var PlatformError = __toESM(require("effect/PlatformError"), 1);
|
|
40
|
+
var Tar = __toESM(require("./Tar.js"), 1);
|
|
41
|
+
const kinds = /* @__PURE__ */ new Set(["d", "u", "f", "o", "D", "F", "L"]);
|
|
42
|
+
const absolute = (members) => new Map([...members].map(([name, bytes]) => [`/${name}`, bytes]));
|
|
43
|
+
const parse = (bytes, root, path) => {
|
|
44
|
+
const decoder = new TextDecoder();
|
|
45
|
+
const listed = /* @__PURE__ */ new Map();
|
|
46
|
+
const children = /* @__PURE__ */ new Map();
|
|
47
|
+
let offset = 0;
|
|
48
|
+
for (; ; ) {
|
|
49
|
+
const end = bytes.indexOf(0, offset);
|
|
50
|
+
if (end === -1) return void 0;
|
|
51
|
+
const record = decoder.decode(bytes.subarray(offset, end));
|
|
52
|
+
offset = end + 1;
|
|
53
|
+
if (record === "E") break;
|
|
54
|
+
const kind = record.slice(0, 1);
|
|
55
|
+
if (!kinds.has(kind)) return void 0;
|
|
56
|
+
const entry = record.slice(1);
|
|
57
|
+
listed.set(entry, kind);
|
|
58
|
+
if (entry === root) continue;
|
|
59
|
+
const parent = path.dirname(entry);
|
|
60
|
+
const siblings = children.get(parent);
|
|
61
|
+
if (siblings === void 0) children.set(parent, [path.basename(entry)]);
|
|
62
|
+
else siblings.push(path.basename(entry));
|
|
63
|
+
}
|
|
64
|
+
const ignores = Tar.files(bytes.subarray(offset));
|
|
65
|
+
return ignores === void 0 ? void 0 : { kinds: listed, children, contents: absolute(ignores) };
|
|
66
|
+
};
|
|
67
|
+
const refusal = (tag, method, path) => PlatformError.systemError({ _tag: tag, module: "FileSystem", method, pathOrDescriptor: path });
|
|
68
|
+
const types = {
|
|
69
|
+
d: "Directory",
|
|
70
|
+
u: "Directory",
|
|
71
|
+
D: "Directory",
|
|
72
|
+
f: "File",
|
|
73
|
+
F: "File",
|
|
74
|
+
o: "Unknown",
|
|
75
|
+
L: void 0
|
|
76
|
+
};
|
|
77
|
+
const info = (type, size) => ({
|
|
78
|
+
type,
|
|
79
|
+
size: ByteSize.bytes(BigInt(size)),
|
|
80
|
+
mtime: import_effect.Option.none(),
|
|
81
|
+
atime: import_effect.Option.none(),
|
|
82
|
+
birthtime: import_effect.Option.none(),
|
|
83
|
+
dev: 0,
|
|
84
|
+
ino: import_effect.Option.none(),
|
|
85
|
+
mode: 0,
|
|
86
|
+
nlink: import_effect.Option.none(),
|
|
87
|
+
uid: import_effect.Option.none(),
|
|
88
|
+
gid: import_effect.Option.none(),
|
|
89
|
+
rdev: import_effect.Option.none(),
|
|
90
|
+
blksize: import_effect.Option.none(),
|
|
91
|
+
blocks: import_effect.Option.none()
|
|
92
|
+
});
|
|
93
|
+
const fileSystem = (tree, path) => {
|
|
94
|
+
const kindOf = (value) => tree.kinds.get(path.resolve(value));
|
|
95
|
+
const readFile = (value) => {
|
|
96
|
+
const bytes = tree.contents.get(path.resolve(value));
|
|
97
|
+
if (bytes !== void 0) return import_effect.Effect.succeed(bytes);
|
|
98
|
+
const type = types[kindOf(value) ?? "L"];
|
|
99
|
+
return import_effect.Effect.fail(
|
|
100
|
+
refusal(type === "File" ? "PermissionDenied" : type === void 0 ? "NotFound" : "BadResource", "readFile", value)
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
return FileSystem.makeNoop({
|
|
104
|
+
stat: (value) => {
|
|
105
|
+
const type = types[kindOf(value) ?? "L"];
|
|
106
|
+
return type === void 0 ? import_effect.Effect.fail(refusal("NotFound", "stat", value)) : import_effect.Effect.succeed(info(type, tree.contents.get(path.resolve(value))?.length ?? 0));
|
|
107
|
+
},
|
|
108
|
+
readDirectory: (value) => {
|
|
109
|
+
const kind = kindOf(value);
|
|
110
|
+
if (kind === "d") return import_effect.Effect.succeed([...tree.children.get(path.resolve(value)) ?? []]);
|
|
111
|
+
const tag = kind === "u" ? "PermissionDenied" : kind === void 0 || kind === "L" ? "NotFound" : "BadResource";
|
|
112
|
+
return import_effect.Effect.fail(refusal(tag, "readDirectory", value));
|
|
113
|
+
},
|
|
114
|
+
readLink: (value) => {
|
|
115
|
+
const kind = kindOf(value);
|
|
116
|
+
return kind === "D" || kind === "F" || kind === "L" ? import_effect.Effect.succeed("") : import_effect.Effect.fail(refusal(kind === void 0 ? "NotFound" : "BadResource", "readLink", value));
|
|
117
|
+
},
|
|
118
|
+
readFile,
|
|
119
|
+
readFileString: (value) => import_effect.Effect.map(readFile(value), (bytes) => new TextDecoder().decode(bytes)),
|
|
120
|
+
stream: (value) => import_effect.Stream.fromEffect(readFile(value))
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
//# sourceMappingURL=RemoteTree.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/internal/RemoteTree.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * A tree another machine listed, served as a read-only `FileSystem`.\n *\n * `RemoteSearch` lists a root on the machine with `find` and brings back the\n * bytes the search reads with `tar`; this module holds what came back and\n * answers what the walk, the globs, and the matcher ask of a filesystem, so\n * `PortableSearch` can search the copy as it searches a host.\n *\n * @since 1.0.0-rc.8\n */\n\nimport type * as Path from \"@smthrs/kernel/Path\"\nimport { Effect, Option, Stream } from \"effect\"\nimport * as ByteSize from \"effect/ByteSize\"\nimport * as FileSystem from \"effect/FileSystem\"\nimport * as PlatformError from \"effect/PlatformError\"\nimport * as Tar from \"./Tar.ts\"\n\n/**\n * What one listed path is: `d` a directory, `u` a directory the machine's\n * user may not list, `f` a file, `o` anything else, and a symbolic link to a\n * directory (`D`), to a file (`F`), or to neither (`L`).\n *\n * @private\n * @since 1.0.0-rc.8\n */\nexport type Kind = \"d\" | \"u\" | \"f\" | \"o\" | \"D\" | \"F\" | \"L\"\n\n/**\n * One machine's listing of a root, and the bytes it brought back, by\n * absolute path.\n *\n * @private\n * @since 1.0.0-rc.8\n */\nexport interface Tree {\n readonly kinds: ReadonlyMap<string, Kind>\n readonly children: ReadonlyMap<string, ReadonlyArray<string>>\n readonly contents: ReadonlyMap<string, Uint8Array>\n}\n\nconst kinds: ReadonlySet<string> = new Set([\"d\", \"u\", \"f\", \"o\", \"D\", \"F\", \"L\"])\n\n/**\n * The member names `tar` wrote for absolute paths, which it strips of their\n * leading `/`, as those paths again.\n *\n * @private\n * @since 1.0.0-rc.8\n */\nexport const absolute = (members: ReadonlyMap<string, Uint8Array>): ReadonlyMap<string, Uint8Array> =>\n new Map([...members].map(([name, bytes]) => [`/${name}`, bytes]))\n\n/**\n * Reads NUL-terminated `<kind><path>` records up to an `E` record, then the\n * tar stream of `.gitignore` files after it. `root` is the listed root, which\n * is nobody's child. `undefined` when the bytes are not that shape.\n *\n * @private\n * @since 1.0.0-rc.8\n */\nexport const parse = (bytes: Uint8Array, root: string, path: Path.Path): Tree | undefined => {\n const decoder = new TextDecoder()\n const listed = new Map<string, Kind>()\n const children = new Map<string, Array<string>>()\n let offset = 0\n for (;;) {\n const end = bytes.indexOf(0, offset)\n if (end === -1) return undefined\n const record = decoder.decode(bytes.subarray(offset, end))\n offset = end + 1\n if (record === \"E\") break\n const kind = record.slice(0, 1)\n if (!kinds.has(kind)) return undefined\n const entry = record.slice(1)\n listed.set(entry, kind as Kind)\n if (entry === root) continue\n const parent = path.dirname(entry)\n const siblings = children.get(parent)\n if (siblings === undefined) children.set(parent, [path.basename(entry)])\n else siblings.push(path.basename(entry))\n }\n const ignores = Tar.files(bytes.subarray(offset))\n return ignores === undefined ? undefined : { kinds: listed, children, contents: absolute(ignores) }\n}\n\nconst refusal = (tag: PlatformError.SystemErrorTag, method: string, path: string): PlatformError.PlatformError =>\n PlatformError.systemError({ _tag: tag, module: \"FileSystem\", method, pathOrDescriptor: path })\n\n/** What `stat` says each kind is: links resolved, a dangling one absent. */\nconst types: Readonly<Record<Kind, FileSystem.File.Type | undefined>> = {\n d: \"Directory\",\n u: \"Directory\",\n D: \"Directory\",\n f: \"File\",\n F: \"File\",\n o: \"Unknown\",\n L: undefined\n}\n\nconst info = (type: FileSystem.File.Type, size: number): FileSystem.File.Info => ({\n type,\n size: ByteSize.bytes(BigInt(size)),\n mtime: Option.none(),\n atime: Option.none(),\n birthtime: Option.none(),\n dev: 0,\n ino: Option.none(),\n mode: 0,\n nlink: Option.none(),\n uid: Option.none(),\n gid: Option.none(),\n rdev: Option.none(),\n blksize: Option.none(),\n blocks: Option.none()\n})\n\n/**\n * The tree as a read-only `FileSystem`, resolving paths with `path`. A file\n * whose bytes did not come back is one the machine's user could not read,\n * which the in-process peer skips exactly as it skips such a file on a host.\n *\n * @private\n * @since 1.0.0-rc.8\n */\nexport const fileSystem = (tree: Tree, path: Path.Path): FileSystem.FileSystem => {\n const kindOf = (value: string) => tree.kinds.get(path.resolve(value))\n const readFile = (value: string) => {\n const bytes = tree.contents.get(path.resolve(value))\n if (bytes !== undefined) return Effect.succeed(bytes)\n const type = types[kindOf(value) ?? \"L\"]\n return Effect.fail(\n refusal(type === \"File\" ? \"PermissionDenied\" : type === undefined ? \"NotFound\" : \"BadResource\", \"readFile\", value)\n )\n }\n return FileSystem.makeNoop({\n stat: (value) => {\n const type = types[kindOf(value) ?? \"L\"]\n return type === undefined\n ? Effect.fail(refusal(\"NotFound\", \"stat\", value))\n : Effect.succeed(info(type, tree.contents.get(path.resolve(value))?.length ?? 0))\n },\n readDirectory: (value) => {\n const kind = kindOf(value)\n if (kind === \"d\") return Effect.succeed([...tree.children.get(path.resolve(value)) ?? []])\n const tag = kind === \"u\" ? \"PermissionDenied\" : kind === undefined || kind === \"L\" ? \"NotFound\" : \"BadResource\"\n return Effect.fail(refusal(tag, \"readDirectory\", value))\n },\n readLink: (value) => {\n const kind = kindOf(value)\n return kind === \"D\" || kind === \"F\" || kind === \"L\"\n ? Effect.succeed(\"\")\n : Effect.fail(refusal(kind === undefined ? \"NotFound\" : \"BadResource\", \"readLink\", value))\n },\n readFile,\n readFileString: (value) => Effect.map(readFile(value), (bytes) => new TextDecoder().decode(bytes)),\n stream: (value) => Stream.fromEffect(readFile(value))\n })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The regular files of a tar stream.
|
|
3
|
+
*
|
|
4
|
+
* `RemoteSearch` brings a machine's files back with its own `tar`, because
|
|
5
|
+
* every image ships one and it frames each file with its name and size. GNU
|
|
6
|
+
* and BusyBox `tar` write GNU long names, `bsdtar` writes POSIX extended
|
|
7
|
+
* headers, and `xargs` may start `tar` more than once, so the stream can hold
|
|
8
|
+
* several archives one after another. This reads all of them.
|
|
9
|
+
*
|
|
10
|
+
* @since 1.0.0-rc.8
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Every regular file the stream holds, by member name, or `undefined` when
|
|
14
|
+
* the stream is not whole tar archives: a header whose checksum does not
|
|
15
|
+
* hold, a member cut short, or a last archive with no end. A `tar` that
|
|
16
|
+
* stopped between two members wrote no end, so its missing files are not
|
|
17
|
+
* mistaken for files it could not read.
|
|
18
|
+
*
|
|
19
|
+
* @private
|
|
20
|
+
* @since 1.0.0-rc.8
|
|
21
|
+
*/
|
|
22
|
+
export declare const files: (stream: Uint8Array) => ReadonlyMap<string, Uint8Array> | undefined;
|
|
23
|
+
//# sourceMappingURL=Tar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Tar.d.ts","sourceRoot":"","sources":["../../../src/internal/Tar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAsCH;;;;;;;;;GASG;AACH,eAAO,MAAM,KAAK,GAAI,QAAQ,UAAU,KAAG,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,SAoC5E,CAAA"}
|