๐ฎ 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("""

### 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, &Map.put(&1, :clone_complete, true))
Kino.Frame.render(frames.status, render_status.(Agent.get(build_state, & &1)))
end
end)
end)
# Make All handler
Kino.listen(buttons.make, fn _event ->
state = Agent.get(build_state, & &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, & &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, & &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, &Map.put(&1, :install_complete, true))
if success do
Kino.Frame.render(frames.status, render_status.(Agent.get(build_state, & &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, & &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, &Map.put(&1, :verify_complete, true))
Kino.Frame.render(frames.status, render_status.(Agent.get(build_state, & &1)))
end
end)
end
end)
Kino.Frame.render(frames.status, render_status.(Agent.get(build_state, & &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, &Map.put(&1, frame_key, status))
statuses = Agent.get(frame_status, & &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})**
""")