Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

๐Ÿ”ฎ scriptorium

livebook/manuscripts.livemd

๐Ÿ”ฎ scriptorium

Mix.install([
  # HTTP client
  {:req, "~> 0.5.8"},
  # JSON parser
  {:jason, "~> 1.4.4"},
  # For visualization
  {:kino, "~> 0.14.2"},
])

Application.put_env(:livebook, :deployment, [
  title: "scriptorium",
  public_access: true
])

port = System.get_env("LIVEBOOK_PORT", "8080")
hostname = System.get_env("LIVEBOOK_HOSTNAME", "localhost")
base_url = "http://#{hostname}:#{port}/apps"

Section

import Kino.Shorts

markdown("""
![](files/manuscripts_mechanics.png)

### Get Started with Manuscripts

Now that you have understood the basic network primitives, we can get into the specifics of manuscript mechanics. Let's start by understanding what we need for development.

Before we dive into *installation*, it's worth noting that we have two paths forward:
1. we can build from source
2. we can use pre-built binaries.

Building from source gives us the most control and access to the latest developments, including unreleased features and improvements. However, if you prefer a simpler approach, you can always download the latest stable release from the [GitHub releases page](https://github.com/chainbase-labs/manuscript-core/releases) or use the [scripts](https://github.com/chainbase-labs/manuscript-core?tab=readme-ov-file#getting-started-) which do it for you. We'll focus on building from source in this guide, as it provides the most comprehensive understanding of the manuscript tools.


`manuscript-core` requires several development tools to build and run properly. You might be running this Livebook either through a Docker container (where these requirements are pre-installed) or on your local machine. Let's examine the current environment to understand what tools are available:
""") |> Kino.render()

# Helper function defined as a variable
get_version = fn command, args ->
  try do
    {output, 0} = System.cmd(command, args)
    {:ok, String.trim(output)}
  rescue
    # Handle command not found
    ErlangError -> {:error, "Not installed"}
    # Handle other potential errors
    _ -> {:error, "Error checking version"}
  end
end

# Create frame and toggle button
version_frame = Kino.Frame.new()
toggle_button = Kino.Control.button("๐Ÿ”ง Check Development Requirements")

# Create an agent to store the visibility state
{:ok, state_agent} = Agent.start_link(fn -> false end)

# Create a styled box for our button
Kino.Layout.grid([toggle_button], boxed: true, gap: 8) |> Kino.render()
version_frame |> Kino.render()

# Add listener with toggle logic using agent
Kino.listen(toggle_button, fn _ ->
  new_state = Agent.get_and_update(state_agent, fn current_state ->
    new_state = !current_state
    {new_state, new_state}
  end)

  if new_state do
    # Safely get versions with proper formatting
    git_version = case get_version.("git", ["--version"]) do
      {:ok, output} -> output
      {:error, msg} -> "โŒ #{msg}"
    end

    make_version = case get_version.("make", ["--version"]) do
      {:ok, output} ->
        output
        |> String.split("\n")
        |> List.first()
      {:error, msg} -> "โŒ #{msg}"
    end

    go_version = case get_version.("go", ["version"]) do
      {:ok, output} -> output
      {:error, msg} -> "โŒ #{msg}"
    end

    cargo_version = case get_version.("cargo", ["--version"]) do
      {:ok, output} -> output
      {:error, msg} -> "โŒ #{msg}"
    end

    # Check if any tools are missing
    all_installed = not (
      String.contains?(git_version, "โŒ") or
      String.contains?(make_version, "โŒ") or
      String.contains?(go_version, "โŒ") or
      String.contains?(cargo_version, "โŒ")
    )

    version_check = """
    ### Development Requirements Status

    >    These are the development tools available in the current environment:
    >
    >    | Tool | Version | Required For |
    >    |------|---------|-------------|
    >    | Git  | #{git_version} | Repository management |
    >    | Make | #{make_version} | Build automation |
    >    | Go   | #{go_version} | manuscript-cli |
    >    | Rust | #{cargo_version} | manuscript-gui |

    #{if not all_installed do
      """
      โš ๏ธ Some required tools are missing. You'll need to install them before proceeding with the build process.
      If you're using the Docker container, this might indicate an issue with the container setup.
      """
    else
      "โœ… All required tools are available."
    end}
    """
    Kino.Frame.render(version_frame, Kino.Markdown.new(version_check))
  else
    Kino.Frame.render(version_frame, Kino.Markdown.new(""))
    Kino.nothing()
  end
end)

Kino.nothing()
markdown("""
#### Understanding the Build Process

`manuscript-core` is a monorepository containing both `manuscript-cli` (implemented in Go) and `manuscript-gui` (implemented in Rust). Here's how the components work together:

```mermaid
%%{init: {'theme': 'base', 'themeVariables': { 'fontSize': '16px'}, "htmlLabels": true}}%%
flowchart TD
    subgraph Mono["โœจ manuscript-core Monorepo โœจ"]
        direction TB
        Core["๐Ÿ“š manuscript-core"]
        MakeCLI["make cli"]
        MakeGUI["make gui"]
        CLI["๐Ÿ–ฅ๏ธ manuscript-cli
implemented in Go"] GUI["๐ŸŽจ manuscript-gui
implemented in Rust"] InstallCLI["sudo make install-cli"] InstallGUI["sudo make install-gui"] Core --> MakeCLI Core --> MakeGUI MakeCLI --> |"builds"| CLI MakeGUI --> |"builds"| GUI CLI --> InstallCLI GUI --> InstallGUI InstallCLI --> |"installs to"| /usr/bin/ InstallGUI --> |"installs to"| /usr/bin/ end style Mono fill:#f0f7ff,stroke:#4a9eff style Core fill:#4a9eff,color:#ffffff,stroke:#0066cc style MakeCLI fill:#00acc1,color:#ffffff,stroke:#007c91 style MakeGUI fill:#ff4081,color:#ffffff,stroke:#c60055 style CLI fill:#00bcd4,color:#000000,stroke:#008ba3 style GUI fill:#ff80ab,color:#000000,stroke:#c94f7c style InstallCLI fill:#00838f,color:#ffffff,stroke:#005662 style InstallGUI fill:#c51162,color:#ffffff,stroke:#880e4f ``` Fortunately, for us, they can both be built and installed together! We'll show how next. """
)
repo_url = "https://github.com/chainbase-labs/manuscript-core.git"
build_dir = "manuscript-core"

# Utility functions

stream_command = fn command, frame ->
  port = Port.open({:spawn, "/bin/sh -c '#{command}'"}, [:binary, :exit_status, :stderr_to_stdout])

  stream_output = fn stream_output, port, accumulated_output ->
    receive do
      {^port, {:data, new_output}} ->
        updated_output = accumulated_output <> new_output
        Kino.Frame.render(frame, Kino.Markdown.new("""
        ```
        #{updated_output}
        ```
        """))
        stream_output.(stream_output, port, updated_output)

      {^port, {:exit_status, status}} ->
        final_output = accumulated_output <> "\nProcess completed (status: #{status})"
        Kino.Frame.render(frame, Kino.Markdown.new("""
        ```
        #{final_output}
        ```
        """))
        status == 0

      _other ->
        stream_output.(stream_output, port, accumulated_output)
    end
  end

  stream_output.(stream_output, port, "")
end

Kino.nothing()
frames = %{
  status: Kino.Frame.new(),
  clone: Kino.Frame.new(),
  make: Kino.Frame.new(),
  install: Kino.Frame.new(),
  verify: Kino.Frame.new()
}

# Button setup
buttons = %{
  clone: Kino.Control.button("โ–ถ๏ธ Run on Livebook"),
  make: Kino.Control.button("โ–ถ๏ธ Run on Livebook"),
  install: Kino.Control.button("โ–ถ๏ธ Run on Livebook"),
  verify: Kino.Control.button("โ–ถ๏ธ Run on Livebook")
}

# Initialize build state
{:ok, build_state} = Agent.start_link(fn -> %{
  clone_complete: false,
  make_complete: false,
  install_complete: false,
  verify_complete: false
} end)

# Status rendering
render_status = fn state ->
  steps = [
    {"Repository Clone", state.clone_complete},
    {"Build Process", state.make_complete},
    {"Installation", state.install_complete},
    {"Verification", state.verify_complete}
  ]

  status_text = steps
  |> Enum.map(fn {name, complete} ->
    status = if complete, do: "โœ…", else: "โณ"
    "#{status} #{name}"
  end)
  |> Enum.join("\n")

  Kino.Markdown.new("### Build Progress\n#{status_text}")
end

Kino.nothing()
# Clone Repository handler
Kino.listen(buttons.clone, fn _event ->
  Kino.Frame.render(frames.clone, Kino.Markdown.new("Starting repository clone..."))

  Task.async(fn ->
    System.cmd("rm", ["-rf", build_dir])

    success = stream_command.(
      "git clone #{repo_url}",
      frames.clone
    )

    if success do
      Agent.update(build_state, &amp;Map.put(&amp;1, :clone_complete, true))
      Kino.Frame.render(frames.status, render_status.(Agent.get(build_state, &amp; &amp;1)))
    end
  end)
end)

# Make All handler
Kino.listen(buttons.make, fn _event ->
  state = Agent.get(build_state, &amp; &amp;1)

  if not state.clone_complete do
    Kino.Frame.render(frames.make, Kino.Markdown.new("""
    ```
    โš ๏ธ Error: Cannot proceed - Repository clone has not completed successfully
    ```
    """))
  else
    Kino.Frame.render(frames.make, Kino.Markdown.new("Starting build process..."))

    Task.async(fn ->
      build_steps = """
      cd manuscript-core && \
      make all 2>&1
      """

      build_success = stream_command.(build_steps, frames.make)

      if build_success do

        Agent.update(build_state, fn state ->
          Map.put(state, :make_complete, true)
        end)
        Kino.Frame.render(frames.status, render_status.(Agent.get(build_state, &amp; &amp;1)))

        Kino.Frame.render(frames.make, Kino.Markdown.new("""
        ```
        ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰ Build completed successfully! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰
        ```
        """))
      end
    end)
  end
end)

# Install handler
Kino.listen(buttons.install, fn _event ->
  state = Agent.get(build_state, &amp; &amp;1)

  if not state.make_complete do
    Kino.Frame.render(frames.install, Kino.Markdown.new("""
    ```
    โš ๏ธ Error: Build not completed
    ```
    """))
  else
    Kino.Frame.render(frames.install, Kino.Markdown.new("Starting installation..."))

    Task.async(fn ->
      install_command = """
      cd #{build_dir} && \
      make install 2>&1
      """

      success = stream_command.(install_command, frames.install)
      Agent.update(build_state, &amp;Map.put(&amp;1, :install_complete, true))

      if success do
        Kino.Frame.render(frames.status, render_status.(Agent.get(build_state, &amp; &amp;1)))
      else
        Kino.Frame.render(frames.install, Kino.Markdown.new("""
        ```
        โš ๏ธ Installation requires elevated privileges. Please run 'sudo make install' from the terminal.
        ```
        """))
      end
    end)
  end
end)

# Verify Installation handler
Kino.listen(buttons.verify, fn _event ->
  state = Agent.get(build_state, &amp; &amp;1)

  if not state.install_complete do
    Kino.Frame.render(frames.verify, Kino.Markdown.new("""
    ```
    โš ๏ธ Error: Installation not completed
    ```
    """))
  else
    Kino.Frame.render(frames.verify, Kino.Markdown.new("Verifying installation..."))

    Task.async(fn ->
      success = stream_command.("manuscript-cli version -v", frames.verify)

      if success do
        Agent.update(build_state, &amp;Map.put(&amp;1, :verify_complete, true))
        Kino.Frame.render(frames.status, render_status.(Agent.get(build_state, &amp; &amp;1)))
      end
    end)
  end
end)

Kino.Frame.render(frames.status, render_status.(Agent.get(build_state, &amp; &amp;1)))

# Initialize frames with command previews
frame_previews = %{
  clone: """
  ```bash
  $ git clone https://github.com/chainbase-labs/manuscript-core.git
  ```
  """,
  make: """
  ```bash
  $ cd manuscript-core && make all
  ```
  """,
  install: """
  ```bash
  $ sudo make install
  ```
  """,
  verify: """
  ```bash
  $ manuscript-cli version -v
  ```
  """
}

# Initialize each frame with its preview
for {frame_key, preview} <- frame_previews do
  Kino.Frame.render(frames[frame_key], Kino.Markdown.new(preview))
end


# Create and render the full layout
# Final layout with descriptions and right-aligned buttons
Kino.Layout.grid([
  frames.status,

  # Step 1
  Kino.Layout.grid([
    Kino.Markdown.new("### ๐Ÿ”ต Step 1: Clone the Repository"),
    buttons.clone
  ], columns: 2, gap: 300),
  Kino.Markdown.new("Downloads the latest manuscript source code from GitHub."),
  frames.clone,

  # Step 2
  Kino.Layout.grid([
    Kino.Markdown.new("### ๐Ÿ”ต Step 2: Build All Components"),
    buttons.make
  ], columns: 2, gap: 300),
  Kino.Markdown.new("Compiles both the CLI and GUI components, respectively. This will take several minutes to collect required packages."),
  frames.make,

  # Step 3
  Kino.Layout.grid([
    Kino.Markdown.new("### ๐Ÿ”ต Step 3: Install"),
    buttons.install
  ], columns: 2, gap: 300),
  Kino.Markdown.new("Installs the compiled binaries to your system. The Makefile places them in `/usr/bin/` or an appropriate execution path."),
  frames.install,

  # Step 4
  Kino.Layout.grid([
    Kino.Markdown.new("### ๐Ÿ”ต Step 4: Verify Installation"),
    buttons.verify
  ], columns: 2, gap: 300),
  Kino.Markdown.new("Confirms the installation was successful by running the `manuscript-cli version --verbose` command."),
  frames.verify
]) |> Kino.render()

Kino.HTML.new("""

"""
)
Kino.Markdown.new("""

### Congratulations ๐ŸŽ‰

Now, that you've installed the tools, you're ready to get started tinkering with actual manuscripts. To begin with, it's best to understand that there's a difference between the `GUI` and `CLI`.

#### `manusript-gui`

`manuscript-gui` is built from Rust as a Textual User Interface (TUI) tool to be used from a Terminal window. It provides a graphical experience customizing, configuring, testing, and deploying manuscripts. When working with manuscripts in the wild, it is the preferred route.

""") |> Kino.render()

gui_screencast =
  Kino.FS.file_path("GUI_Screencast.mp4")
  |> File.read!()
  |> Kino.Video.new(:mp4,autoplay: true, loop: true)
  |> Kino.render()

Kino.Markdown.new("""

Presently, full TUI apps are not supported from Livebook, so working with the GUI will be indirect in the `scriptoroium.`

Still, there are many important things to know about what `manuscript-gui` can do! With it you can easily do the following:

- Assess the Available Chains
- Create a Manuscript
- Edit the Manuscript Logic using SQL
- View Running Manuscript Jobs
- Deploy Locally

""")
Kino.Markdown.new("""
#### `manuscript-cli`

`manuscript-gui` is built from Golang as a Command Line Interface (CLI) tool to be used from a Terminal window as well.
It gives us a programmable interface for making and building manuscripts.


Let's learn the basic commands:
""") |> Kino.render()


cli_screenshot =
  Kino.FS.file_path("CLI_Screenshot.png")
  |> File.read!  
  |> Kino.Image.new(:png)
  |> Kino.render()


cli_frames = %{
  init: Kino.Frame.new(),
  init_interactive: Kino.Frame.new(),
  init_noninteractive: Kino.Frame.new(),
  config: Kino.Frame.new(),
  config_show: Kino.Frame.new(),
  config_summary: Kino.Frame.new(),
  list: Kino.Frame.new(),
  stop: Kino.Frame.new(),
  logs: Kino.Frame.new(),
  version: Kino.Frame.new(),
  help: Kino.Frame.new()
}

# Setup frames for command titles
title_frames = Map.new(Map.keys(cli_frames), fn key ->
  {key, Kino.Frame.new()}
end)

# Button setup for each command
cli_buttons = Map.new(Map.keys(cli_frames), fn key ->
  {key, Kino.Control.button("โ–ถ๏ธ Run on Livebook")}
end)

# Initialize frame status tracking
{:ok, frame_status} = Agent.start_link(fn ->
  Map.new(Map.keys(cli_frames), fn key -> {key, "๐Ÿ”ต"} end)
end)


# Helper function to update title status
update_command_title = fn frame_key, status ->
  Agent.update(frame_status, &amp;Map.put(&amp;1, frame_key, status))
  statuses = Agent.get(frame_status, &amp; &amp;1)

  title = case frame_key do
    :init -> "#{statuses.init} Basic Initialize"
    :init_interactive -> "#{statuses.init_interactive} Interactive Initialize"
    :init_noninteractive -> "#{statuses.init_noninteractive} Non-interactive Initialize"
    :config -> "#{statuses.config} View Config"
    :config_show -> "#{statuses.config_show} Show Full Config"
    :config_summary -> "#{statuses.config_summary} Show Config Summary"
    :list -> "#{statuses.list} List Jobs"
    :stop -> "#{statuses.stop} Stop Job"
    :logs -> "#{statuses.logs} View Logs"
    :version -> "#{statuses.version} Version Check"
    :help -> "#{statuses.help} Get Help"
  end

  Kino.Frame.render(title_frames[frame_key], Kino.Markdown.new("### #{title}"))
end

# Command definitions with actual commands to execute
cli_commands = %{
  init: %{
    preview: """
    ```bash
    $ manuscript-cli init [manuscript-name] [flags]
    ```
    """,
    command: "manuscript-cli init example-manuscript"
  },
  init_interactive: %{
    preview: """
    ```bash
    $ manuscript-cli init
    ```
    """,
    command: "manuscript-cli init"
  },
  init_noninteractive: %{
    preview: """
    ```bash
    $ manuscript-cli init my-manuscript \\
      --output postgresql \\
      --protocol ethereum \\
      --dataset blocks
    ```
    """,
    command: "manuscript-cli init my-manuscript --output postgresql --protocol ethereum --dataset blocks"
  },
  config: %{
    preview: """
    ```bash
    $ manuscript-cli config
    ```
    """,
    command: "manuscript-cli config"
  },
  config_show: %{
    preview: """
    ```bash
    $ manuscript-cli config show
    ```
    """,
    command: "manuscript-cli config show"
  },
  config_summary: %{
    preview: """
    ```bash
    $ manuscript-cli config show --summary
    ```
    """,
    command: "manuscript-cli config show --summary"
  },
  list: %{
    preview: """
    ```bash
    $ manuscript-cli list
    ```
    """,
    command: "manuscript-cli list"
  },
  stop: %{
    preview: """
    ```bash
    $ manuscript-cli stop example-manuscript
    ```
    """,
    command: "manuscript-cli stop example-manuscript"
  },
  logs: %{
    preview: """
    ```bash
    $ manuscript-cli logs example-manuscript
    ```
    """,
    command: "manuscript-cli logs example-manuscript"
  },
  version: %{
    preview: """
    ```bash
    $ manuscript-cli version
    ```
    """,
    command: "manuscript-cli version"
  },
  help: %{
    preview: """
    ```bash
    $ manuscript-cli help
    ```
    """,
    command: "manuscript-cli help"
  }
}

# Initialize frames with command previews
for {frame_key, %{preview: preview}} <- cli_commands do
  Kino.Frame.render(cli_frames[frame_key], Kino.Markdown.new(preview))
end

# Initialize all title frames with blue status
for {key, _frame} <- title_frames do
  update_command_title.(key, "๐Ÿ”ต")
end

# Add listeners for each button
for {key, button} <- cli_buttons do
  Kino.listen(button, fn _event ->
    # Set status to blue when starting
    update_command_title.(key, "๐Ÿ”ต")

    Task.async(fn ->
      success = stream_command.(
        cli_commands[key].command,
        cli_frames[key]
      )
      # Update status based on success
      status = if success, do: "๐ŸŸข", else: "๐Ÿ”ด"
      update_command_title.(key, status)
    end)
  end)
end

Kino.nothing()
# Create and render the full layout
Kino.Layout.grid([
  # Init Command Section
  Kino.Markdown.new("""
  ## ๐Ÿ“ฆ Initialize Commands
  The CLI's `init` command is used to create new manuscript projects. It has two main usage patterns: 1) interactive with no parameters, 2) non-interactive with parameters. To initialize a manuscript, you have to define several things:
  - `protocol` , sometimes referred to as the chain (e.g. `ethereum`, `zksync`, `solana`)
  - `dataset` , what specific data you want from the chain (e.g. `blocks` `transacations` are common)
  - `output`, either to a database(`postgresql`) or to the `console`
  """),

  # Basic Init
  Kino.Layout.grid([
    title_frames.init,
    cli_buttons.init
  ], columns: 2, gap: 300),
  Kino.Markdown.new("If you try to run `manuscript init` with the improper parameters, you'll get descriptive errors!"),
  cli_frames.init,

  # Interactive Init
  Kino.Layout.grid([
    title_frames.init_interactive,
    cli_buttons.init_interactive
  ], columns: 2, gap: 300),
  Kino.Markdown.new("Running `manuscript init` with no other text starts an interactive setup process with prompts for all settings. Note that this command is shown just for demo purposes and you cannot interact with it on this livebook."),
  cli_frames.init_interactive,

  # Non-interactive Init
  Kino.Layout.grid([
    title_frames.init_noninteractive,
    cli_buttons.init_noninteractive
  ], columns: 2, gap: 300),
  Kino.Markdown.new("Running `manuscript init` with a full list of parameters creates a new manuscript with all settings specified via its command line flags."),
  cli_frames.init_noninteractive,

  html("

"
), # Configuration Section Kino.Markdown.new(""" ## โš™๏ธ Configuration Commands The `config` commands let you view and manage manuscript configurations. Each manuscript created is actually a folder composed of several files and stored locally on disk. However, `manuscript_cli` also tracks all manuscripts on disk in a config file. By default this file is stored in the user's home directory's `.config/` folder """), # Basic Config Kino.Layout.grid([ title_frames.config, cli_buttons.config ], columns: 2, gap: 300), Kino.Markdown.new("We can run `config` to see the location of the config file. This is useful if you want to edit it directly."), cli_frames.config, # Config Show Kino.Layout.grid([ title_frames.config_show, cli_buttons.config_show ], columns: 2, gap: 300), Kino.Markdown.new("Running `config show` displays the complete configuration in detail. Notice the config file is actually in JSON format. This is equivalent to what you would get if you went, found this file on disk, and opened it."), cli_frames.config_show, # Config Summary Kino.Layout.grid([ title_frames.config_summary, cli_buttons.config_summary ], columns: 2, gap: 300), Kino.Markdown.new("We can generate a useful summary with feedback on our file by running `config show --summary`. This command shows a summary of the current configuration file, its location on disk, and any discrepancies between manuscripts on disk and those listed in the config file. It's quite useful for debugging."), cli_frames.config_summary, html("

"
), # Management Commands Section Kino.Markdown.new(""" ## ๐Ÿ› ๏ธ Management Commands There are also some necessary commands to know once you've gotten manuscript jobs running: """), # List Kino.Layout.grid([ title_frames.list, cli_buttons.list ], columns: 2, gap: 300), Kino.Markdown.new("The command `list` lists all running manuscript jobs. It also has the linux alias `ls`. If we have initialized, we should successfully see `my-manuscript` running."), cli_frames.list, # Logs Kino.Layout.grid([ title_frames.logs, cli_buttons.logs ], columns: 2, gap: 300), Kino.Markdown.new("The command `logs` shows logs for a running manuscript. Once again, this command may be extremely useful for debugging our manuscript state. Let's take a look at the logs of `my-manuscript`"), cli_frames.logs, # Stop Kino.Layout.grid([ title_frames.stop, cli_buttons.stop ], columns: 2, gap: 300), Kino.Markdown.new("The command `stop` stops a running manuscript job. Stopping is a destructive action that tears down and deletes running docker containers related to our manuscript. This is important so we don't consume unnecessary resources when not using our manuscript. Manuscripts can be reconstructed with the `deploy` command. Let's stop our `my-manuscript` from running."), cli_frames.stop, html("

"
), # Utility Commands Section Kino.Markdown.new(""" ## ๐Ÿ”ง Utility Commands General utility commands: """), # Version Kino.Layout.grid([ title_frames.version, cli_buttons.version ], columns: 2, gap: 300), Kino.Markdown.new("During the installation, you used `manuscript-cli version --verbose` to ensure you had a successful install. The normal `version` command shows the current version of `manuscript-core` being used by the CLI."), cli_frames.version, # Help Kino.Layout.grid([ title_frames.help, cli_buttons.help ], columns: 2, gap: 300), Kino.Markdown.new("Pressing `help` with any command gives us information about that command. Running `help` alone prints a list of all available commands."), cli_frames.help ])
next_link = "#{base_url}/scriptorium-manuscripts"
prev_link = "#{base_url}/scriptorium-network"

Kino.Markdown.new("""
### ๐Ÿงญ Navigation

โš™๏ธ To view the source code for this project visit https://github.com/KagemniKarimu/scriptorium

**[โ† Previous: Understanding the Chainbase Network](#{prev_link}) | [Next: Manuscripts & Their Mechanisms โ†’](#{next_link})**
""")