# `Omni.Tools.FileSystem.FS`
[🔗](https://github.com/aaronrussell/omni_tools/blob/v0.1.0/lib/omni/tools/file_system/fs.ex#L1)

Filesystem operations scoped to a base directory.

This module is the reusable core of `Omni.Tools.FileSystem` — it works
independently of the tool machinery. Construct an `%FS{}` with `new/1`,
then call operations directly:

    fs = FS.new(base_dir: "/data/workspace", read_only: true)
    {:ok, content} = FS.read(fs, "notes/todo.md")
    {:ok, entries} = FS.list(fs)

All user-supplied ids are validated against a path policy before any
disk access. See `resolve/2` for the rules.

## Symlinks

Path resolution follows symlinks (inherits `File.*` behaviour). This
module does not attempt to detect or block symlink escapes — it is not
a security boundary. OS-level sandboxing is the right tool for that.

# `t`

```elixir
@type t() :: %Omni.Tools.FileSystem.FS{
  base_dir: String.t(),
  nested?: boolean(),
  read_only?: boolean()
}
```

A configured filesystem scope.

# `delete`

```elixir
@spec delete(t(), String.t()) :: :ok | {:error, term()}
```

Deletes a file.

    :ok = FS.delete(fs, "old-report.html")

# `list`

```elixir
@spec list(t()) :: {:ok, [Omni.Tools.FileSystem.Entry.t()]}
```

Lists all regular files under the base directory.

In nested mode, walks recursively and returns ids as base-relative paths
(e.g. `"sub/dir/file.txt"`). In flat mode, lists only direct children.
Includes dotfiles and dot-directories. Results are sorted by id.

    {:ok, entries} = FS.list(fs)

# `new`

```elixir
@spec new(keyword()) :: t()
```

Creates a new filesystem scope.

## Options

  * `:base_dir` (required) — absolute path to an existing directory.
  * `:read_only` — when `true`, write/patch/delete operations return
    `{:error, :read_only}`. Defaults to `false`.
  * `:nested` — when `true`, ids may contain path separators (subdirectories).
    When `false`, only bare filenames are accepted. Defaults to `true`.

Raises `ArgumentError` if `:base_dir` is missing, not absolute, or
does not exist on disk.

# `patch`

```elixir
@spec patch(t(), String.t(), String.t(), String.t()) ::
  {:ok, Omni.Tools.FileSystem.Entry.t()} | {:error, term()}
```

Applies a targeted find-and-replace edit to a file.

The `search` string must appear exactly once in the file. Returns an
error if it appears zero times or more than once — the error includes
the count so the caller can refine the search string.

    {:ok, entry} = FS.patch(fs, "config.json", ~s("v1"), ~s("v2"))

# `read`

```elixir
@spec read(t(), String.t()) :: {:ok, binary()} | {:error, term()}
```

Reads the content of a file.

    {:ok, content} = FS.read(fs, "notes/todo.md")

# `resolve`

```elixir
@spec resolve(t(), String.t()) ::
  {:ok, String.t()} | {:error, {:invalid_id, String.t()}}
```

Resolves a user-supplied `id` to an absolute path under the base directory.

Returns `{:ok, abs_path}` or `{:error, {:invalid_id, message}}`.

## Path policy

  * Must be non-empty.
  * Must be relative (no leading `/`, `~/`, or `..` segments).
  * Must not contain null bytes.
  * In flat mode, must not contain path separators (`/` or `\`).

# `write`

```elixir
@spec write(t(), String.t(), binary()) ::
  {:ok, Omni.Tools.FileSystem.Entry.t()} | {:error, term()}
```

Writes content to a file (creates or overwrites).

In nested mode, parent directories are created automatically.
Returns `{:ok, %Entry{}}` on success.

    {:ok, entry} = FS.write(fs, "report.html", "<h1>Hello</h1>")

---

*Consult [api-reference.md](api-reference.md) for complete listing*
