@smthrs/std 1.0.0-rc.8 → 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.
Files changed (60) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +3 -1
  3. package/dist/cjs/PortableSearch.d.ts +13 -1
  4. package/dist/cjs/PortableSearch.d.ts.map +1 -1
  5. package/dist/cjs/PortableSearch.js +8 -1
  6. package/dist/cjs/PortableSearch.js.map +2 -2
  7. package/dist/cjs/RemoteSearch.d.ts +54 -0
  8. package/dist/cjs/RemoteSearch.d.ts.map +1 -0
  9. package/dist/cjs/RemoteSearch.js +134 -0
  10. package/dist/cjs/RemoteSearch.js.map +7 -0
  11. package/dist/cjs/index.d.ts +2 -0
  12. package/dist/cjs/index.d.ts.map +1 -1
  13. package/dist/cjs/index.js +2 -0
  14. package/dist/cjs/index.js.map +2 -2
  15. package/dist/cjs/internal/EnvelopePrecheck.d.ts.map +1 -1
  16. package/dist/cjs/internal/EnvelopePrecheck.js +21 -5
  17. package/dist/cjs/internal/EnvelopePrecheck.js.map +2 -2
  18. package/dist/cjs/internal/RemoteTree.d.ts +60 -0
  19. package/dist/cjs/internal/RemoteTree.d.ts.map +1 -0
  20. package/dist/cjs/internal/RemoteTree.js +123 -0
  21. package/dist/cjs/internal/RemoteTree.js.map +7 -0
  22. package/dist/cjs/internal/Tar.d.ts +23 -0
  23. package/dist/cjs/internal/Tar.d.ts.map +1 -0
  24. package/dist/cjs/internal/Tar.js +86 -0
  25. package/dist/cjs/internal/Tar.js.map +7 -0
  26. package/dist/esm/PortableSearch.d.ts +13 -1
  27. package/dist/esm/PortableSearch.d.ts.map +1 -1
  28. package/dist/esm/PortableSearch.js +16 -0
  29. package/dist/esm/PortableSearch.js.map +1 -1
  30. package/dist/esm/RemoteSearch.d.ts +54 -0
  31. package/dist/esm/RemoteSearch.d.ts.map +1 -0
  32. package/dist/esm/RemoteSearch.js +162 -0
  33. package/dist/esm/RemoteSearch.js.map +1 -0
  34. package/dist/esm/index.d.ts +2 -0
  35. package/dist/esm/index.d.ts.map +1 -1
  36. package/dist/esm/index.js +2 -0
  37. package/dist/esm/index.js.map +1 -1
  38. package/dist/esm/internal/EnvelopePrecheck.d.ts.map +1 -1
  39. package/dist/esm/internal/EnvelopePrecheck.js +29 -5
  40. package/dist/esm/internal/EnvelopePrecheck.js.map +1 -1
  41. package/dist/esm/internal/RemoteTree.d.ts +60 -0
  42. package/dist/esm/internal/RemoteTree.d.ts.map +1 -0
  43. package/dist/esm/internal/RemoteTree.js +132 -0
  44. package/dist/esm/internal/RemoteTree.js.map +1 -0
  45. package/dist/esm/internal/Tar.d.ts +23 -0
  46. package/dist/esm/internal/Tar.d.ts.map +1 -0
  47. package/dist/esm/internal/Tar.js +97 -0
  48. package/dist/esm/internal/Tar.js.map +1 -0
  49. package/docs/api.md +28 -4
  50. package/docs/concepts/effects-and-capabilities.md +3 -1
  51. package/docs/guides/bind-the-standard-flows.md +3 -1
  52. package/docs/guides/run-a-shell-command.md +4 -1
  53. package/docs/troubleshooting.md +8 -0
  54. package/package.json +16 -6
  55. package/src/PortableSearch.ts +20 -0
  56. package/src/RemoteSearch.ts +199 -0
  57. package/src/index.ts +3 -0
  58. package/src/internal/EnvelopePrecheck.ts +29 -5
  59. package/src/internal/RemoteTree.ts +159 -0
  60. 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 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\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 = (path: Path.Path, tokens: ReadonlyArray<Token>): 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 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 || token.operator || (!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 return candidates.map(({ index, value }) => {\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}\n\nconst references = (path: Path.Path, command: string, nesting: number): ReadonlyArray<PathReference> => {\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))\n segment = []\n } else {\n segment.push(token)\n }\n }\n found.push(...segmentReferences(path, segment))\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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",
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
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"}
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var Tar_exports = {};
20
+ __export(Tar_exports, {
21
+ files: () => files
22
+ });
23
+ module.exports = __toCommonJS(Tar_exports);
24
+ const decoder = new TextDecoder();
25
+ const text = (bytes) => {
26
+ const end = bytes.indexOf(0);
27
+ return decoder.decode(end === -1 ? bytes : bytes.subarray(0, end));
28
+ };
29
+ const octal = (bytes) => {
30
+ const digits = text(bytes).trim();
31
+ return /^[0-7]+$/.test(digits) ? Number.parseInt(digits, 8) : void 0;
32
+ };
33
+ const checksum = (header) => {
34
+ let sum = 0;
35
+ for (let index = 0; index < 512; index++) sum += index >= 148 && index < 156 ? 32 : header[index];
36
+ return sum;
37
+ };
38
+ const paxPath = (data) => {
39
+ let offset = 0;
40
+ let path;
41
+ while (offset < data.length) {
42
+ const space = data.indexOf(32, offset);
43
+ const length = space === -1 ? Number.NaN : Number(decoder.decode(data.subarray(offset, space)));
44
+ if (!Number.isSafeInteger(length) || length <= space - offset || offset + length > data.length) return path;
45
+ const record = decoder.decode(data.subarray(space + 1, offset + length - 1));
46
+ if (record.startsWith("path=")) path = record.slice("path=".length);
47
+ offset += length;
48
+ }
49
+ return path;
50
+ };
51
+ const files = (stream) => {
52
+ const found = /* @__PURE__ */ new Map();
53
+ let longName;
54
+ let extended;
55
+ let ended = true;
56
+ for (let offset = 0; offset + 512 <= stream.length; ) {
57
+ const header = stream.subarray(offset, offset + 512);
58
+ offset += 512;
59
+ ended = header.every((byte) => byte === 0);
60
+ if (ended) continue;
61
+ const size = octal(header.subarray(124, 136));
62
+ if (size === void 0 || octal(header.subarray(148, 156)) !== checksum(header)) return void 0;
63
+ if (offset + size > stream.length) return void 0;
64
+ const data = stream.subarray(offset, offset + size);
65
+ offset += Math.ceil(size / 512) * 512;
66
+ const type = String.fromCharCode(header[156]);
67
+ if (type === "L") {
68
+ longName = text(data);
69
+ continue;
70
+ }
71
+ if (type === "x") {
72
+ extended = paxPath(data) ?? extended;
73
+ continue;
74
+ }
75
+ if (type === "K" || type === "g") continue;
76
+ if (type === "0" || type === "\0" || type === "7") {
77
+ const prefix = decoder.decode(header.subarray(257, 263)) === "ustar\0" ? text(header.subarray(345, 500)) : "";
78
+ const short = text(header.subarray(0, 100));
79
+ found.set(extended ?? longName ?? (prefix === "" ? short : `${prefix}/${short}`), data.slice());
80
+ }
81
+ longName = void 0;
82
+ extended = void 0;
83
+ }
84
+ return ended ? found : void 0;
85
+ };
86
+ //# sourceMappingURL=Tar.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/internal/Tar.ts"],
4
+ "sourcesContent": ["/**\n * The regular files of a tar stream.\n *\n * `RemoteSearch` brings a machine's files back with its own `tar`, because\n * every image ships one and it frames each file with its name and size. GNU\n * and BusyBox `tar` write GNU long names, `bsdtar` writes POSIX extended\n * headers, and `xargs` may start `tar` more than once, so the stream can hold\n * several archives one after another. This reads all of them.\n *\n * @since 1.0.0-rc.8\n */\n\nconst decoder = new TextDecoder()\n\n/** Bytes up to the first NUL, as text. */\nconst text = (bytes: Uint8Array): string => {\n const end = bytes.indexOf(0)\n return decoder.decode(end === -1 ? bytes : bytes.subarray(0, end))\n}\n\n/** An octal header field, or `undefined` when it holds anything else. */\nconst octal = (bytes: Uint8Array): number | undefined => {\n const digits = text(bytes).trim()\n return /^[0-7]+$/.test(digits) ? Number.parseInt(digits, 8) : undefined\n}\n\n/** The header's checksum, computed with the checksum field read as spaces. */\nconst checksum = (header: Uint8Array): number => {\n let sum = 0\n for (let index = 0; index < 512; index++) sum += index >= 148 && index < 156 ? 32 : header[index]!\n return sum\n}\n\n/** The `path` record of a POSIX extended header: `<length> <key>=<value>\\n` records. */\nconst paxPath = (data: Uint8Array): string | undefined => {\n let offset = 0\n let path: string | undefined\n while (offset < data.length) {\n const space = data.indexOf(32, offset)\n const length = space === -1 ? Number.NaN : Number(decoder.decode(data.subarray(offset, space)))\n if (!Number.isSafeInteger(length) || length <= space - offset || offset + length > data.length) return path\n const record = decoder.decode(data.subarray(space + 1, offset + length - 1))\n if (record.startsWith(\"path=\")) path = record.slice(\"path=\".length)\n offset += length\n }\n return path\n}\n\n/**\n * Every regular file the stream holds, by member name, or `undefined` when\n * the stream is not whole tar archives: a header whose checksum does not\n * hold, a member cut short, or a last archive with no end. A `tar` that\n * stopped between two members wrote no end, so its missing files are not\n * mistaken for files it could not read.\n *\n * @private\n * @since 1.0.0-rc.8\n */\nexport const files = (stream: Uint8Array): ReadonlyMap<string, Uint8Array> | undefined => {\n const found = new Map<string, Uint8Array>()\n let longName: string | undefined\n let extended: string | undefined\n let ended = true\n for (let offset = 0; offset + 512 <= stream.length;) {\n const header = stream.subarray(offset, offset + 512)\n offset += 512\n // Zero blocks end one archive; `xargs` may have started another.\n ended = header.every((byte) => byte === 0)\n if (ended) continue\n const size = octal(header.subarray(124, 136))\n if (size === undefined || octal(header.subarray(148, 156)) !== checksum(header)) return undefined\n if (offset + size > stream.length) return undefined\n const data = stream.subarray(offset, offset + size)\n offset += Math.ceil(size / 512) * 512\n const type = String.fromCharCode(header[156]!)\n if (type === \"L\") {\n longName = text(data)\n continue\n }\n if (type === \"x\") {\n extended = paxPath(data) ?? extended\n continue\n }\n // A long link name and a global header describe no member of their own.\n if (type === \"K\" || type === \"g\") continue\n if (type === \"0\" || type === \"\\0\" || type === \"7\") {\n const prefix = decoder.decode(header.subarray(257, 263)) === \"ustar\\0\" ? text(header.subarray(345, 500)) : \"\"\n const short = text(header.subarray(0, 100))\n found.set(extended ?? longName ?? (prefix === \"\" ? short : `${prefix}/${short}`), data.slice())\n }\n longName = undefined\n extended = undefined\n }\n return ended ? found : undefined\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAYA,MAAM,UAAU,IAAI,YAAY;AAGhC,MAAM,OAAO,CAAC,UAA8B;AAC1C,QAAM,MAAM,MAAM,QAAQ,CAAC;AAC3B,SAAO,QAAQ,OAAO,QAAQ,KAAK,QAAQ,MAAM,SAAS,GAAG,GAAG,CAAC;AACnE;AAGA,MAAM,QAAQ,CAAC,UAA0C;AACvD,QAAM,SAAS,KAAK,KAAK,EAAE,KAAK;AAChC,SAAO,WAAW,KAAK,MAAM,IAAI,OAAO,SAAS,QAAQ,CAAC,IAAI;AAChE;AAGA,MAAM,WAAW,CAAC,WAA+B;AAC/C,MAAI,MAAM;AACV,WAAS,QAAQ,GAAG,QAAQ,KAAK,QAAS,QAAO,SAAS,OAAO,QAAQ,MAAM,KAAK,OAAO,KAAK;AAChG,SAAO;AACT;AAGA,MAAM,UAAU,CAAC,SAAyC;AACxD,MAAI,SAAS;AACb,MAAI;AACJ,SAAO,SAAS,KAAK,QAAQ;AAC3B,UAAM,QAAQ,KAAK,QAAQ,IAAI,MAAM;AACrC,UAAM,SAAS,UAAU,KAAK,OAAO,MAAM,OAAO,QAAQ,OAAO,KAAK,SAAS,QAAQ,KAAK,CAAC,CAAC;AAC9F,QAAI,CAAC,OAAO,cAAc,MAAM,KAAK,UAAU,QAAQ,UAAU,SAAS,SAAS,KAAK,OAAQ,QAAO;AACvG,UAAM,SAAS,QAAQ,OAAO,KAAK,SAAS,QAAQ,GAAG,SAAS,SAAS,CAAC,CAAC;AAC3E,QAAI,OAAO,WAAW,OAAO,EAAG,QAAO,OAAO,MAAM,QAAQ,MAAM;AAClE,cAAU;AAAA,EACZ;AACA,SAAO;AACT;AAYO,MAAM,QAAQ,CAAC,WAAoE;AACxF,QAAM,QAAQ,oBAAI,IAAwB;AAC1C,MAAI;AACJ,MAAI;AACJ,MAAI,QAAQ;AACZ,WAAS,SAAS,GAAG,SAAS,OAAO,OAAO,UAAS;AACnD,UAAM,SAAS,OAAO,SAAS,QAAQ,SAAS,GAAG;AACnD,cAAU;AAEV,YAAQ,OAAO,MAAM,CAAC,SAAS,SAAS,CAAC;AACzC,QAAI,MAAO;AACX,UAAM,OAAO,MAAM,OAAO,SAAS,KAAK,GAAG,CAAC;AAC5C,QAAI,SAAS,UAAa,MAAM,OAAO,SAAS,KAAK,GAAG,CAAC,MAAM,SAAS,MAAM,EAAG,QAAO;AACxF,QAAI,SAAS,OAAO,OAAO,OAAQ,QAAO;AAC1C,UAAM,OAAO,OAAO,SAAS,QAAQ,SAAS,IAAI;AAClD,cAAU,KAAK,KAAK,OAAO,GAAG,IAAI;AAClC,UAAM,OAAO,OAAO,aAAa,OAAO,GAAG,CAAE;AAC7C,QAAI,SAAS,KAAK;AAChB,iBAAW,KAAK,IAAI;AACpB;AAAA,IACF;AACA,QAAI,SAAS,KAAK;AAChB,iBAAW,QAAQ,IAAI,KAAK;AAC5B;AAAA,IACF;AAEA,QAAI,SAAS,OAAO,SAAS,IAAK;AAClC,QAAI,SAAS,OAAO,SAAS,QAAQ,SAAS,KAAK;AACjD,YAAM,SAAS,QAAQ,OAAO,OAAO,SAAS,KAAK,GAAG,CAAC,MAAM,YAAY,KAAK,OAAO,SAAS,KAAK,GAAG,CAAC,IAAI;AAC3G,YAAM,QAAQ,KAAK,OAAO,SAAS,GAAG,GAAG,CAAC;AAC1C,YAAM,IAAI,YAAY,aAAa,WAAW,KAAK,QAAQ,GAAG,MAAM,IAAI,KAAK,KAAK,KAAK,MAAM,CAAC;AAAA,IAChG;AACA,eAAW;AACX,eAAW;AAAA,EACb;AACA,SAAO,QAAQ,QAAQ;AACzB;",
6
+ "names": []
7
+ }
@@ -4,9 +4,21 @@
4
4
  * @since 1.0.0
5
5
  */
6
6
  import * as Path from "@smthrs/kernel/Path";
7
- import { type Context, Layer } from "effect";
7
+ import { type Context, Effect, Layer } from "effect";
8
8
  import * as FileSystem from "effect/FileSystem";
9
9
  import * as Search from "./Search.ts";
10
+ import * as StdError from "./StdError.ts";
11
+ /**
12
+ * The files this peer's `grep` reads for `input`, in the order it reads
13
+ * them: the walk under `input.root` with its `.gitignore` rules and
14
+ * `input.globs` applied and symbolic links left out, or the root itself when
15
+ * it names one file. A peer that has to bring a tree's bytes to this process
16
+ * before it can search them (`RemoteSearch`) fetches exactly these.
17
+ *
18
+ * @category queries
19
+ * @since 1.0.0-rc.8
20
+ */
21
+ export declare const searched: (input: Pick<Search.GrepInput, "root" | "hidden" | "noIgnore" | "globs">) => Effect.Effect<ReadonlyArray<string>, StdError.StdError, FileSystem.FileSystem | Path.Path>;
10
22
  /**
11
23
  * Captures filesystem and path services in the portable peer.
12
24
  *
@@ -1 +1 @@
1
- {"version":3,"file":"PortableSearch.d.ts","sourceRoot":"","sources":["../../src/PortableSearch.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,IAAI,MAAM,qBAAqB,CAAA;AAC3C,OAAO,EAAE,KAAK,OAAO,EAAU,KAAK,EAAU,MAAM,QAAQ,CAAA;AAC5D,OAAO,KAAK,UAAU,MAAM,mBAAmB,CAAA;AAM/C,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AAoOrC;;;;;GAKG;AACH,eAAO,MAAM,IAAI,GAAI,UAAU,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,KAAG,MAAM,CAAC,MAIvF,CAAA;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CAGtF,CAAA"}
1
+ {"version":3,"file":"PortableSearch.d.ts","sourceRoot":"","sources":["../../src/PortableSearch.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,IAAI,MAAM,qBAAqB,CAAA;AAC3C,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,EAAE,KAAK,EAAU,MAAM,QAAQ,CAAA;AAC5D,OAAO,KAAK,UAAU,MAAM,mBAAmB,CAAA;AAM/C,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AAErC,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AAkOzC;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,GACnB,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC,KACtE,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CAMxF,CAAA;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,IAAI,GAAI,UAAU,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,KAAG,MAAM,CAAC,MAIvF,CAAA;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CAGtF,CAAA"}
@@ -220,6 +220,22 @@ const glob = (input) => Effect.gen(function* () {
220
220
  ...(unsatisfiable === undefined ? {} : { notice: unsatisfiable })
221
221
  };
222
222
  });
223
+ /**
224
+ * The files this peer's `grep` reads for `input`, in the order it reads
225
+ * them: the walk under `input.root` with its `.gitignore` rules and
226
+ * `input.globs` applied and symbolic links left out, or the root itself when
227
+ * it names one file. A peer that has to bring a tree's bytes to this process
228
+ * before it can search them (`RemoteSearch`) fetches exactly these.
229
+ *
230
+ * @category queries
231
+ * @since 1.0.0-rc.8
232
+ */
233
+ export const searched = (input) => Effect.gen(function* () {
234
+ const fileSystem = yield* FileSystem.FileSystem;
235
+ const path = yield* Path.Path;
236
+ const walked = yield* Walk.files(fileSystem, path, input.root, input.hidden, input.noIgnore);
237
+ return yield* candidates(fileSystem, path, walked, input.root, input.globs);
238
+ });
223
239
  /**
224
240
  * Captures filesystem and path services in the portable peer.
225
241
  *