ruby_llm-browser 0.1.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +61 -0
- data/CODE_OF_CONDUCT.md +11 -0
- data/CONTRIBUTING.md +42 -0
- data/LICENSE.txt +21 -0
- data/NOTICE.md +92 -0
- data/README.md +139 -0
- data/SECURITY.md +46 -0
- data/docs/API.md +177 -0
- data/docs/CNAME +1 -0
- data/docs/RAILS.md +167 -0
- data/docs/README.md +16 -0
- data/docs/RELEASE_ACTION_PLAN.md +81 -0
- data/docs/RELEASE_AUDIT.md +158 -0
- data/docs/RELEASING.md +131 -0
- data/docs/SECURITY_MODEL.md +117 -0
- data/docs/SPEC_BASELINE.md +107 -0
- data/docs/WORK.md +260 -0
- data/docs/_advanced/live-backend-qualification.md +74 -0
- data/docs/_advanced/rails-integration.md +56 -0
- data/docs/_config.yml +109 -0
- data/docs/_core_features/inspect-extract.md +91 -0
- data/docs/_core_features/offline-act.md +110 -0
- data/docs/_core_features/operation-lifecycle.md +63 -0
- data/docs/_core_features/source-policy.md +61 -0
- data/docs/_data/navigation.yml +12 -0
- data/docs/_data/sidebar.yml +8 -0
- data/docs/_data/social_links.yml +3 -0
- data/docs/_getting_started/configuration.md +68 -0
- data/docs/_getting_started/getting-started.md +77 -0
- data/docs/_getting_started/overview.md +52 -0
- data/docs/_plugins/versioned_docs.rb +46 -0
- data/docs/_reference/api-reference.md +29 -0
- data/docs/_reference/release-history.md +23 -0
- data/docs/_reference/upgrading.md +33 -0
- data/docs/bin/build-api.sh +23 -0
- data/docs/bin/build.sh +161 -0
- data/docs/bin/export-api-markdown.rb +53 -0
- data/docs/bin/link_versions.rb +54 -0
- data/docs/bin/postprocess_api_seo.rb +129 -0
- data/docs/bin/prepare_versions.rb +40 -0
- data/docs/bin/serve.sh +12 -0
- data/docs/bin/versions.yml +14 -0
- data/docs/index.md +82 -0
- data/examples/plain_ruby.rb +34 -0
- data/examples/rails_adapter.rb +47 -0
- data/lib/generators/ruby_llm/browser/install_generator.rb +29 -0
- data/lib/generators/ruby_llm/browser/templates/create_ruby_llm_browser_tables.rb.tt +60 -0
- data/lib/ruby_llm/browser/approval.rb +128 -0
- data/lib/ruby_llm/browser/approval_store.rb +38 -0
- data/lib/ruby_llm/browser/backend.rb +99 -0
- data/lib/ruby_llm/browser/budget.rb +90 -0
- data/lib/ruby_llm/browser/capabilities.rb +76 -0
- data/lib/ruby_llm/browser/configuration.rb +74 -0
- data/lib/ruby_llm/browser/context.rb +117 -0
- data/lib/ruby_llm/browser/doctor.rb +134 -0
- data/lib/ruby_llm/browser/engine.rb +326 -0
- data/lib/ruby_llm/browser/errors.rb +86 -0
- data/lib/ruby_llm/browser/fake_backend.rb +134 -0
- data/lib/ruby_llm/browser/immutable.rb +19 -0
- data/lib/ruby_llm/browser/in_memory_approval_store.rb +67 -0
- data/lib/ruby_llm/browser/in_memory_operation_store.rb +174 -0
- data/lib/ruby_llm/browser/operation.rb +218 -0
- data/lib/ruby_llm/browser/operation_store.rb +92 -0
- data/lib/ruby_llm/browser/policy.rb +298 -0
- data/lib/ruby_llm/browser/rails/active_record_approval_store.rb +72 -0
- data/lib/ruby_llm/browser/rails/active_record_operation_store.rb +231 -0
- data/lib/ruby_llm/browser/rails/actor_record.rb +13 -0
- data/lib/ruby_llm/browser/rails/approval_record.rb +49 -0
- data/lib/ruby_llm/browser/rails/operation_job.rb +163 -0
- data/lib/ruby_llm/browser/rails/operation_record.rb +56 -0
- data/lib/ruby_llm/browser/rails/reaper.rb +117 -0
- data/lib/ruby_llm/browser/rails/schema.rb +81 -0
- data/lib/ruby_llm/browser/rails.rb +79 -0
- data/lib/ruby_llm/browser/redactor.rb +159 -0
- data/lib/ruby_llm/browser/result.rb +102 -0
- data/lib/ruby_llm/browser/stagehand_backend.rb +332 -0
- data/lib/ruby_llm/browser/tool.rb +70 -0
- data/lib/ruby_llm/browser/tools/act.rb +91 -0
- data/lib/ruby_llm/browser/tools/extract.rb +60 -0
- data/lib/ruby_llm/browser/tools/inspect.rb +38 -0
- data/lib/ruby_llm/browser/url_guard.rb +253 -0
- data/lib/ruby_llm/browser/version.rb +9 -0
- data/lib/ruby_llm/browser.rb +79 -0
- metadata +267 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Runs once per build, after Jekyll reads the site.
|
|
4
|
+
#
|
|
5
|
+
# - docs_unreleased is true for the /next/ build, and for an unversioned build
|
|
6
|
+
# while _config.yml says `released: false`. Pages use it to show the install
|
|
7
|
+
# command for the default branch. A build of a release tag is never
|
|
8
|
+
# unreleased, even when that tag forgot to set `released: true`.
|
|
9
|
+
# - A versioned build takes `version` from _data/versions.yml, which
|
|
10
|
+
# bin/prepare_versions.rb fills in from the tag, so a tag that forgot to bump
|
|
11
|
+
# `version` still shows the right install command.
|
|
12
|
+
# - An absolute canonical_url on this site becomes a path, so the theme adds
|
|
13
|
+
# this build's base path (/next/, /v1/) and the page stays in its own
|
|
14
|
+
# build's llms.txt and sitemap.
|
|
15
|
+
# - llms.txt names the documented version, and links to the site's /api/
|
|
16
|
+
# point at this build's API reference.
|
|
17
|
+
Jekyll::Hooks.register :site, :post_read, priority: :high do |site|
|
|
18
|
+
versions = site.data['versions'].is_a?(Hash) ? site.data['versions'] : {}
|
|
19
|
+
current = versions['current'].to_s
|
|
20
|
+
if current.empty?
|
|
21
|
+
site.config['docs_unreleased'] = site.config['released'] == false
|
|
22
|
+
else
|
|
23
|
+
site.config['docs_unreleased'] = [ 'next', 'Next (unreleased)' ].include?(current)
|
|
24
|
+
release = versions['release'].to_s
|
|
25
|
+
site.config['version'] = release unless release.empty?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
origin = site.config['url'].to_s.delete_suffix('/')
|
|
29
|
+
base = site.config['baseurl'].to_s.delete_suffix('/')
|
|
30
|
+
root = "#{origin}#{versions.fetch('base', base).to_s.delete_suffix('/')}"
|
|
31
|
+
unless origin.empty?
|
|
32
|
+
(site.pages + site.collections.values.flat_map(&:docs)).each do |page|
|
|
33
|
+
canonical = page.data['canonical_url']
|
|
34
|
+
next unless canonical.is_a?(String) && canonical.start_with?("#{root}/")
|
|
35
|
+
|
|
36
|
+
page.data['canonical_url'] = canonical.delete_prefix(root)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
llms = site.config.dig('jekyll_vitepress', 'llms')
|
|
41
|
+
next unless llms.is_a?(Hash) && !current.empty?
|
|
42
|
+
|
|
43
|
+
label = current == 'next' ? 'Next (unreleased)' : current
|
|
44
|
+
llms['description'] = "Developer documentation for #{site.config['title']} #{label}."
|
|
45
|
+
llms['details'] = llms['details'].to_s.gsub("#{root}/api/", "#{origin}#{base}/api/") unless origin.empty?
|
|
46
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: API Reference
|
|
4
|
+
nav_order: 1
|
|
5
|
+
guide: false
|
|
6
|
+
description: Find the RubyLLM::Browser classes for tools, policy, results, backends, and Rails jobs.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# {{ page.title }}
|
|
10
|
+
|
|
11
|
+
{{ page.description }}
|
|
12
|
+
{: .fs-6 .fw-300 }
|
|
13
|
+
|
|
14
|
+
The [generated RDoc reference]({{ '/api/' | relative_url }}) documents classes and public methods from the Ruby source. Start with the task you need to perform:
|
|
15
|
+
|
|
16
|
+
| Task | Public API |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| Build trusted inputs for an operation | `Context`, `Policy`, `Budget` |
|
|
19
|
+
| Inspect a page or extract structured data | `Inspect`, `Extract`, `Engine` |
|
|
20
|
+
| Simulate one host-approved action offline | `Act`, `Approval`, `ApprovalStore` |
|
|
21
|
+
| Read a bounded result | `Result` |
|
|
22
|
+
| Use an offline backend | `FakeBackend` |
|
|
23
|
+
| Adapt the Stagehand Ruby SDK | `StagehandBackend`, `Capabilities`, `Doctor` |
|
|
24
|
+
| Persist operation or approval state | `OperationStore`, `ApprovalStore` and their implementations |
|
|
25
|
+
| Run Rails background jobs and cleanup | `RubyLLM::Browser::Rails::OperationJob`, `Reaper` |
|
|
26
|
+
|
|
27
|
+
Start with [Getting Started]({% link _getting_started/getting-started.md %}) for a runnable offline example. The [API and configuration guide]({% link API.md %}) explains inputs, results, limits, and errors.
|
|
28
|
+
|
|
29
|
+
The reference is generated from source comments. Build it locally with `docs/bin/build-api.sh tmp/api`.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Release History
|
|
4
|
+
nav_order: 9
|
|
5
|
+
guide: false
|
|
6
|
+
description: Follow release tags and their documentation for RubyLLM::Browser.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# {{ page.title }}
|
|
10
|
+
|
|
11
|
+
{{ page.description }}
|
|
12
|
+
{: .fs-6 .fw-300 }
|
|
13
|
+
|
|
14
|
+
{% if site.docs_unreleased %}The current source build follows `main`. The latest stable release is `0.1.0`.
|
|
15
|
+
{% else %}This documentation describes the `{{ site.version }}` release. Use the tag and
|
|
16
|
+
release notes to identify the exact source for this version.
|
|
17
|
+
{% endif %}
|
|
18
|
+
|
|
19
|
+
| Release | Date | Highlights |
|
|
20
|
+
| --- | --- | --- |
|
|
21
|
+
| [0.1.0](https://github.com/yorzi/ruby_llm-browser/releases/tag/v0.1.0) | 2026-10-18 | Offline read-only Inspect and Extract, Rails adapter, and Act simulation; live Stagehand and live Act are unsupported |
|
|
22
|
+
|
|
23
|
+
For changes to existing applications, follow [Upgrading]({% link _reference/upgrading.md %}).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Upgrading
|
|
4
|
+
nav_order: 2
|
|
5
|
+
description: Check the supported upgrade path before moving a RubyLLM::Browser application to a new version.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# {{ page.title }}
|
|
9
|
+
|
|
10
|
+
{{ page.description }}
|
|
11
|
+
{: .fs-6 .fw-300 }
|
|
12
|
+
|
|
13
|
+
After reading this guide, you will know:
|
|
14
|
+
|
|
15
|
+
* What changed in the first public release.
|
|
16
|
+
* Where to find migration steps for earlier development snapshots.
|
|
17
|
+
* What evidence must exist before a release tag is treated as supported.
|
|
18
|
+
|
|
19
|
+
## Public Releases
|
|
20
|
+
|
|
21
|
+
Version `{{ site.version }}` is the first public release. It has no earlier public version
|
|
22
|
+
to migrate from. Install it with `bundle add ruby_llm-browser --version "~> {{ site.version }}"`.
|
|
23
|
+
|
|
24
|
+
## Earlier Development Snapshots
|
|
25
|
+
|
|
26
|
+
If you ran an earlier development Rails generator, do not drop existing operation data
|
|
27
|
+
or rerun a create-table migration. Follow the additive migration instructions under
|
|
28
|
+
"Upgrading an earlier development snapshot" in the [Rails guide]({% link RAILS.md %}). Drain workers during the migration and do not reuse idempotency keys for legacy rows that lack request digests.
|
|
29
|
+
|
|
30
|
+
## Next Steps
|
|
31
|
+
|
|
32
|
+
* Review the [release history]({% link _reference/release-history.md %}) for version records.
|
|
33
|
+
* Review the [release audit]({% link RELEASE_AUDIT.md %}) before enabling a live backend.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Build the RDoc API reference, with a Markdown copy of every class page and
|
|
3
|
+
# the guides' search and social metadata, into <output-dir>.
|
|
4
|
+
# docs/bin/build-api.sh <output-dir> [source-root]
|
|
5
|
+
# bin/build.sh sets SITE_URL to the build's address (such as
|
|
6
|
+
# https://example.com/next) and ROOT_URL to the production site's.
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
out="${1:?usage: build-api.sh <output-dir> [source-root]}"
|
|
10
|
+
docs="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
11
|
+
source_root="${2:-$(git -C "$docs" rev-parse --show-toplevel)}"
|
|
12
|
+
case "$out" in /*) ;; *) out="$PWD/$out" ;; esac
|
|
13
|
+
|
|
14
|
+
( cd "$source_root" && ruby -rrdoc/rdoc -e 'RDoc::RDoc.new.document(ARGV)' -- --output "$out" --quiet lib )
|
|
15
|
+
ruby "$docs/bin/export-api-markdown.rb" "$out" "$source_root" "RubyLLM::Browser"
|
|
16
|
+
|
|
17
|
+
main="RubyLLM::Browser"
|
|
18
|
+
if [[ -n "$main" && -f "$out/$main.html" ]]; then
|
|
19
|
+
# Both pages sit at the same depth, so the module page's relative links work at /api/.
|
|
20
|
+
cp "$out/$main.html" "$out/index.html"
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
ruby "$docs/bin/postprocess_api_seo.rb" "$out" "$docs/_config.yml" "$main.html"
|
data/docs/bin/build.sh
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Build the docs into docs/_site.
|
|
3
|
+
# Once the latest release tag has docs, that release is built at /, the working
|
|
4
|
+
# tree at /next/, and the newest tag of each older major version that has docs
|
|
5
|
+
# at /vN/. Until then, the working tree is built at /.
|
|
6
|
+
# docs/bin/build.sh BASE=/repo docs/bin/build.sh SITE=/tmp/out docs/bin/build.sh
|
|
7
|
+
# BASE is the site's base path. It defaults to baseurl in _config.yml; the
|
|
8
|
+
# workflow sets it from GitHub Pages, and an empty BASE means the site is at the
|
|
9
|
+
# root of its domain.
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
unset RUBYOPT RUBYLIB BUNDLE_GEMFILE BUNDLE_BIN_PATH BUNDLE_BIN BUNDLE_APP_CONFIG
|
|
13
|
+
|
|
14
|
+
docs="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
15
|
+
repo="$(git -C "$docs" rev-parse --show-toplevel)"
|
|
16
|
+
repo="$(cd "$repo" && pwd -P)"
|
|
17
|
+
docs="$(cd "$docs" && pwd -P)"
|
|
18
|
+
rel="${docs#"$repo"/}"
|
|
19
|
+
site_input="${SITE:-$docs/_site}"
|
|
20
|
+
site_input="${site_input%/}"
|
|
21
|
+
site_name="${site_input##*/}"
|
|
22
|
+
site_parent="${site_input%/*}"
|
|
23
|
+
if [[ "$site_input" == /* ]]; then
|
|
24
|
+
[[ -n "$site_parent" ]] || site_parent="/"
|
|
25
|
+
else
|
|
26
|
+
[[ -n "$site_parent" ]] || site_parent="."
|
|
27
|
+
site_parent="$PWD/$site_parent"
|
|
28
|
+
fi
|
|
29
|
+
site_parent="$(cd "$site_parent" && pwd -P)"
|
|
30
|
+
if [[ -z "$site_name" || "$site_name" == "." || "$site_name" == ".." ]]; then
|
|
31
|
+
echo "Refusing unsafe site output path: ${SITE:-$docs/_site}" >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
site="$site_parent/$site_name"
|
|
35
|
+
if [[ "$site" == "/" || "$site" == "$repo" || "$site" == "$docs" || -L "$site" ]]; then
|
|
36
|
+
echo "Refusing unsafe site output path: $site" >&2
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
if [[ "$site" == "$repo/"* && "$site" != "$docs/_site" ]]; then
|
|
40
|
+
echo "Refusing site output inside the source tree: $site" >&2
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
marker="${site}.ruby-llm-browser-generated"
|
|
44
|
+
marker_contents="Generated by docs/bin/build.sh for $site"
|
|
45
|
+
if [[ -L "$marker" ]]; then
|
|
46
|
+
echo "Refusing to replace an existing unowned output directory: $site" >&2
|
|
47
|
+
echo "Choose a new SITE path or remove that output directory yourself." >&2
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
if [[ -e "$site" ]] && [[ ! -d "$site" || ! -f "$marker" || "$(<"$marker")" != "$marker_contents" ]]; then
|
|
51
|
+
echo "Refusing to replace an existing unowned output directory: $site" >&2
|
|
52
|
+
echo "Choose a new SITE path or remove that output directory yourself." >&2
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
gemfile="$docs/Gemfile"
|
|
56
|
+
read_config() { ruby -ryaml -e 'puts YAML.load_file(ARGV.fetch(0))[ARGV.fetch(1)].to_s.chomp("/")' "$docs/_config.yml" "$1"; }
|
|
57
|
+
origin="$(read_config url)"
|
|
58
|
+
BASE="${BASE-$(read_config baseurl)}"
|
|
59
|
+
BASE="${BASE%/}"
|
|
60
|
+
|
|
61
|
+
# A tag has docs when its tree holds this site's _config.yml on the same theme.
|
|
62
|
+
has_docs() { git -C "$repo" show "$1:$rel/_config.yml" 2>/dev/null | grep -q 'jekyll-vitepress-theme'; }
|
|
63
|
+
major() { local version="${1#v}"; echo "${version%%.*}"; }
|
|
64
|
+
|
|
65
|
+
tags="$(git -C "$repo" tag --list --sort=-v:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' || true)"
|
|
66
|
+
latest="$(printf '%s\n' "$tags" | head -n 1)"
|
|
67
|
+
release=""
|
|
68
|
+
if [[ -n "$latest" ]] && has_docs "$latest"; then
|
|
69
|
+
release="$latest"
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
build_api() {
|
|
73
|
+
local output="$1" source_root="$2" prefix="$3"
|
|
74
|
+
if [[ -x "$docs/bin/build-api.sh" ]]; then
|
|
75
|
+
SITE_URL="$origin$prefix" ROOT_URL="$origin$BASE" "$docs/bin/build-api.sh" "$output/api" "$source_root"
|
|
76
|
+
fi
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if [[ -z "$release" ]]; then
|
|
80
|
+
( cd "$docs" && BUNDLE_GEMFILE="$gemfile" bundle exec jekyll build --baseurl "$BASE" -d "$site" --quiet )
|
|
81
|
+
build_api "$site" "$repo" "$BASE"
|
|
82
|
+
printf '%s\n' "$marker_contents" > "$marker"
|
|
83
|
+
echo "Built the working tree at ${BASE:-/}. No release has docs yet, so there is no /next/."
|
|
84
|
+
exit 0
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
archives=()
|
|
88
|
+
top="$(major "$release")"
|
|
89
|
+
seen=" "
|
|
90
|
+
while read -r tag; do
|
|
91
|
+
[[ -n "$tag" ]] || continue
|
|
92
|
+
number="$(major "$tag")"
|
|
93
|
+
if (( number >= top )) || [[ "$seen" == *" $number "* ]]; then continue; fi
|
|
94
|
+
seen="$seen$number "
|
|
95
|
+
if has_docs "$tag"; then archives+=("$tag"); fi
|
|
96
|
+
done <<< "$tags"
|
|
97
|
+
|
|
98
|
+
workspace="$(mktemp -d)"
|
|
99
|
+
trap 'rm -rf "$workspace"' EXIT
|
|
100
|
+
|
|
101
|
+
extract() {
|
|
102
|
+
local tag="$1" target="$2" path
|
|
103
|
+
local paths=("$rel")
|
|
104
|
+
for path in lib .rdoc_options README.md; do
|
|
105
|
+
if git -C "$repo" cat-file -e "$tag:$path" 2>/dev/null; then paths+=("$path"); fi
|
|
106
|
+
done
|
|
107
|
+
mkdir -p "$target"
|
|
108
|
+
git -C "$repo" archive "$tag" -- "${paths[@]}" | tar -x -C "$target"
|
|
109
|
+
if [[ ! -f "$target/.rdoc_options" && -f "$repo/.rdoc_options" ]]; then
|
|
110
|
+
cp "$repo/.rdoc_options" "$target/.rdoc_options"
|
|
111
|
+
fi
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
prepare() {
|
|
115
|
+
local source="$1" channel="$2"
|
|
116
|
+
mkdir -p "$source/_data" "$source/_plugins"
|
|
117
|
+
cp "$docs/bin/versions.yml" "$source/_data/versions.yml"
|
|
118
|
+
cp "$docs/_plugins/versioned_docs.rb" "$source/_plugins/"
|
|
119
|
+
ruby "$docs/bin/prepare_versions.rb" "$source/_data/versions.yml" "$channel" "$BASE" "$release" \
|
|
120
|
+
${archives[@]+"${archives[@]}"}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
build() {
|
|
124
|
+
local source="$1" output="$2" prefix="$3" source_root="$4"
|
|
125
|
+
( cd "$source" && BUNDLE_GEMFILE="$gemfile" bundle exec jekyll build --baseurl "$prefix" -d "$output" --quiet )
|
|
126
|
+
build_api "$output" "$source_root" "$prefix"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
extract "$release" "$workspace/stable"
|
|
130
|
+
prepare "$workspace/stable/$rel" stable
|
|
131
|
+
build "$workspace/stable/$rel" "$workspace/stable-out" "$BASE" "$workspace/stable"
|
|
132
|
+
|
|
133
|
+
mkdir -p "$workspace/next"
|
|
134
|
+
rsync -a --exclude=_site --exclude=vendor --exclude=.jekyll-cache --exclude=.bundle "$docs/" "$workspace/next/"
|
|
135
|
+
prepare "$workspace/next" next
|
|
136
|
+
build "$workspace/next" "$workspace/next-out" "$BASE/next" "$repo"
|
|
137
|
+
|
|
138
|
+
channels=(next)
|
|
139
|
+
for tag in ${archives[@]+"${archives[@]}"}; do
|
|
140
|
+
channel="v$(major "$tag")"
|
|
141
|
+
extract "$tag" "$workspace/$channel"
|
|
142
|
+
prepare "$workspace/$channel/$rel" "$channel"
|
|
143
|
+
build "$workspace/$channel/$rel" "$workspace/$channel-out" "$BASE/$channel" "$workspace/$channel"
|
|
144
|
+
channels+=("$channel")
|
|
145
|
+
done
|
|
146
|
+
|
|
147
|
+
rm -rf "$site"
|
|
148
|
+
mkdir -p "$site"
|
|
149
|
+
cp -a "$workspace/stable-out/." "$site/"
|
|
150
|
+
for channel in "${channels[@]}"; do
|
|
151
|
+
mkdir -p "$site/$channel"
|
|
152
|
+
cp -a "$workspace/$channel-out/." "$site/$channel/"
|
|
153
|
+
done
|
|
154
|
+
ruby "$docs/bin/link_versions.rb" "$site" "$origin" "$BASE" "${channels[@]}"
|
|
155
|
+
printf '%s\n' "$marker_contents" > "$marker"
|
|
156
|
+
|
|
157
|
+
summary="Built $release at ${BASE:-/} and the working tree at $BASE/next/"
|
|
158
|
+
for tag in ${archives[@]+"${archives[@]}"}; do
|
|
159
|
+
summary="$summary, $tag at $BASE/v$(major "$tag")/"
|
|
160
|
+
done
|
|
161
|
+
echo "$summary."
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Writes a Markdown page for every documented class and module, plus an
|
|
4
|
+
# index.md, next to the RDoc HTML. Coding assistants read these.
|
|
5
|
+
# export-api-markdown.rb <output-dir> <source-root> <project-name>
|
|
6
|
+
|
|
7
|
+
require 'fileutils'
|
|
8
|
+
require 'open3'
|
|
9
|
+
require 'rbconfig'
|
|
10
|
+
require 'tmpdir'
|
|
11
|
+
|
|
12
|
+
output_dir = File.expand_path(ARGV.fetch(0))
|
|
13
|
+
source_root = File.expand_path(ARGV.fetch(1))
|
|
14
|
+
project = ARGV.fetch(2)
|
|
15
|
+
rdoc = [ RbConfig.ruby, Gem.bin_path('rdoc', 'rdoc') ]
|
|
16
|
+
ri = [ RbConfig.ruby, Gem.bin_path('rdoc', 'ri') ]
|
|
17
|
+
|
|
18
|
+
def capture!(*command, chdir:)
|
|
19
|
+
stdout, stderr, status = Open3.capture3(*command, chdir: chdir)
|
|
20
|
+
abort "#{command.join(' ')} failed\n#{stderr}" unless status.success?
|
|
21
|
+
|
|
22
|
+
stdout
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def clean(markdown, ri_dir)
|
|
26
|
+
markdown
|
|
27
|
+
.gsub(/\s*\(from\s+#{Regexp.escape(ri_dir)}\)/m, '')
|
|
28
|
+
.gsub(%r{<code>(.*?)</code>}m, '`\1`')
|
|
29
|
+
.gsub(/[ \t]+$/, '')
|
|
30
|
+
.gsub(/\n{3,}/, "\n\n")
|
|
31
|
+
.strip
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
FileUtils.mkdir_p(output_dir)
|
|
35
|
+
|
|
36
|
+
Dir.mktmpdir('api-ri') do |tmp|
|
|
37
|
+
ri_dir = File.join(tmp, 'ri')
|
|
38
|
+
capture!(*rdoc, '--format', 'ri', '--output', ri_dir, '--quiet', 'lib', chdir: source_root)
|
|
39
|
+
options = [ '--no-standard-docs', '--doc-dir', ri_dir, '--format', 'markdown', '--width', '1000' ]
|
|
40
|
+
names = capture!(*ri, *options, '--list', chdir: source_root).lines.map(&:strip).reject(&:empty?).sort
|
|
41
|
+
|
|
42
|
+
index = [ "# #{project} API Reference", '', '<!-- Generated from RDoc. Do not edit. -->', '',
|
|
43
|
+
"Markdown pages for every public #{project} class and module.", '' ]
|
|
44
|
+
names.each do |name|
|
|
45
|
+
path = "#{name.gsub('::', '/')}.md"
|
|
46
|
+
destination = File.join(output_dir, path)
|
|
47
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
48
|
+
markdown = capture!(*ri, *options, '--all', name, chdir: source_root)
|
|
49
|
+
File.write(destination, "#{clean(markdown, ri_dir)}\n")
|
|
50
|
+
index << "- [#{name}](#{path})"
|
|
51
|
+
end
|
|
52
|
+
File.write(File.join(output_dir, 'index.md'), "#{index.join("\n")}\n")
|
|
53
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Ties the versioned builds together once bin/build.sh has assembled them.
|
|
5
|
+
# A page that only an archived major version still has gets a redirect at its
|
|
6
|
+
# old root URL, so links from before the major release keep working, and
|
|
7
|
+
# robots.txt lists the sitemap of every build under the root.
|
|
8
|
+
# link_versions.rb <site-dir> <origin> <base-path> <channel> [channel ...]
|
|
9
|
+
|
|
10
|
+
require 'cgi'
|
|
11
|
+
require 'fileutils'
|
|
12
|
+
require 'json'
|
|
13
|
+
|
|
14
|
+
site, origin, base, *channels = ARGV
|
|
15
|
+
abort 'usage: link_versions.rb <site-dir> <origin> <base-path> <channel> [channel ...]' if channels.empty?
|
|
16
|
+
site = File.expand_path(site)
|
|
17
|
+
|
|
18
|
+
def redirect_page(path, target, canonical)
|
|
19
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
20
|
+
escaped = CGI.escapeHTML(target)
|
|
21
|
+
File.write(path, <<~HTML)
|
|
22
|
+
<!doctype html>
|
|
23
|
+
<html lang="en">
|
|
24
|
+
<head>
|
|
25
|
+
<meta charset="utf-8">
|
|
26
|
+
<title>This page has moved</title>
|
|
27
|
+
<link rel="canonical" href="#{CGI.escapeHTML(canonical)}">
|
|
28
|
+
<meta name="robots" content="noindex,follow">
|
|
29
|
+
<script>window.location.replace(#{JSON.generate(target)} + window.location.search + window.location.hash);</script>
|
|
30
|
+
<meta http-equiv="refresh" content="0;url=#{escaped}">
|
|
31
|
+
</head>
|
|
32
|
+
<body><p>This page has moved to <a href="#{escaped}">#{escaped}</a>.</p></body>
|
|
33
|
+
</html>
|
|
34
|
+
HTML
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
versioned = channels.map { |channel| "#{channel}/" }
|
|
38
|
+
channels.grep(/\Av\d+\z/).each do |archive|
|
|
39
|
+
Dir.glob('**/*.html', base: File.join(site, archive)).sort.each do |relative|
|
|
40
|
+
next if relative == '404.html' || relative.start_with?(*versioned)
|
|
41
|
+
next if File.exist?(File.join(site, relative))
|
|
42
|
+
|
|
43
|
+
target = "#{base}/#{archive}/#{relative.delete_suffix('index.html')}"
|
|
44
|
+
redirect_page(File.join(site, relative), target, "#{origin}#{target}")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
robots = File.join(site, 'robots.txt')
|
|
49
|
+
if File.file?(robots) && origin.match?(%r{\Ahttps?://})
|
|
50
|
+
sitemaps = channels.select { |channel| File.file?(File.join(site, channel, 'sitemap.xml')) }
|
|
51
|
+
File.open(robots, 'a') do |file|
|
|
52
|
+
sitemaps.each { |channel| file.puts "Sitemap: #{origin}#{base}/#{channel}/sitemap.xml" }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Gives the RDoc pages the search and social metadata the theme gives the
|
|
5
|
+
# guides: a canonical URL, robots, Open Graph and Twitter image tags, and
|
|
6
|
+
# JSON-LD. Adds the pages to the build's sitemap.xml, and points links to the
|
|
7
|
+
# production site (the .rdoc_options footer) at this build.
|
|
8
|
+
# postprocess_api_seo.rb <api-dir> <_config.yml> <main-page.html>
|
|
9
|
+
# SITE_URL is the build's address and ROOT_URL the production site's; both
|
|
10
|
+
# default to `url` plus `baseurl` from _config.yml.
|
|
11
|
+
|
|
12
|
+
require 'cgi'
|
|
13
|
+
require 'date'
|
|
14
|
+
require 'json'
|
|
15
|
+
require 'pathname'
|
|
16
|
+
require 'yaml'
|
|
17
|
+
|
|
18
|
+
output_dir = Pathname(ARGV.fetch(0)).expand_path
|
|
19
|
+
config_path = Pathname(ARGV.fetch(1)).expand_path
|
|
20
|
+
main_page = ARGV.fetch(2, 'index.html')
|
|
21
|
+
config = YAML.safe_load_file(config_path, permitted_classes: [ Date, Time ], aliases: true) || {}
|
|
22
|
+
trim = ->(url) { url.to_s.sub(%r{/+\z}, '') }
|
|
23
|
+
configured = "#{trim.call(config['url'])}#{trim.call(config['baseurl'])}"
|
|
24
|
+
site_root = trim.call(ENV.fetch('SITE_URL', configured))
|
|
25
|
+
production_root = trim.call(ENV.fetch('ROOT_URL', configured))
|
|
26
|
+
unless site_root.match?(%r{\Ahttps?://})
|
|
27
|
+
warn 'postprocess_api_seo: url in _config.yml is not an absolute http(s) address; skipping API metadata'
|
|
28
|
+
exit
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
name = config['title'].to_s
|
|
32
|
+
description = config['description'].to_s
|
|
33
|
+
language = (config['lang'] || 'en-US').to_s
|
|
34
|
+
seo = config.dig('jekyll_vitepress', 'seo')
|
|
35
|
+
image = seo.is_a?(Hash) ? seo['image'] : nil
|
|
36
|
+
image = image['path'] if image.is_a?(Hash)
|
|
37
|
+
image ||= '/assets/images/social/index.png' if config_path.dirname.join('_plugins', 'social_cards.rb').file?
|
|
38
|
+
image_url = image && (image.to_s.match?(%r{\Ahttps?://}) ? image.to_s : "#{site_root}#{image}")
|
|
39
|
+
default_robots = 'index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1'
|
|
40
|
+
index_pages = [ 'index.html', main_page ].uniq
|
|
41
|
+
api_urls = []
|
|
42
|
+
|
|
43
|
+
def tag(name, attributes)
|
|
44
|
+
rendered = attributes.map { |key, value| %(#{key}="#{CGI.escapeHTML(value)}") }.join(' ')
|
|
45
|
+
"<#{name} #{rendered}>"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Escapes the characters that would let a value close the ld+json block early.
|
|
49
|
+
def json_ld(graph)
|
|
50
|
+
JSON.generate(graph).gsub(/[<>&
]/) { |char| format('\\u%04x', char.ord) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# A link to the production site moves to this build only when this build has
|
|
54
|
+
# the page, so a footer that names another base path is left alone.
|
|
55
|
+
build_root = output_dir.parent
|
|
56
|
+
in_build = lambda do |route|
|
|
57
|
+
route = route.split(/[?#]/).first.to_s
|
|
58
|
+
candidate = build_root.join(route)
|
|
59
|
+
route.empty? || candidate.file? || candidate.join('index.html').file?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
output_dir.glob('**/*.html').sort.each do |path|
|
|
63
|
+
relative_path = path.relative_path_from(output_dir).to_s
|
|
64
|
+
html = path.read
|
|
65
|
+
unless production_root == site_root
|
|
66
|
+
html = html.gsub(%r{href="#{Regexp.escape(production_root)}/([^"]*)"}) do
|
|
67
|
+
route = Regexp.last_match(1)
|
|
68
|
+
in_build.call(route) ? %(href="#{site_root}/#{route}") : Regexp.last_match(0)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
redirect = html.match?(/<meta[^>]+http-equiv="refresh"/i)
|
|
72
|
+
target = html[/<meta[^>]+http-equiv="refresh"[^>]+url=([^";]+)[^>]*>/i, 1]
|
|
73
|
+
canonical = if index_pages.include?(relative_path)
|
|
74
|
+
"#{site_root}/api/"
|
|
75
|
+
elsif redirect && target
|
|
76
|
+
"#{site_root}/api/#{target}"
|
|
77
|
+
else
|
|
78
|
+
"#{site_root}/api/#{relative_path}"
|
|
79
|
+
end
|
|
80
|
+
title = CGI.unescapeHTML(html[%r{<title>(.*?)</title>}mi, 1].to_s).strip
|
|
81
|
+
page_description = CGI.unescapeHTML(html[/<meta\s+name="description"\s+content="([^"]*)"/mi, 1].to_s).strip
|
|
82
|
+
|
|
83
|
+
html = html.sub(/<link\s+rel="canonical"\s+href="[^"]*">/i, '')
|
|
84
|
+
metadata = [
|
|
85
|
+
tag('link', 'rel' => 'canonical', 'href' => canonical),
|
|
86
|
+
tag('meta', 'name' => 'robots', 'content' => redirect ? 'noindex,follow' : default_robots),
|
|
87
|
+
tag('meta', 'property' => 'og:url', 'content' => canonical),
|
|
88
|
+
tag('meta', 'property' => 'og:site_name', 'content' => name)
|
|
89
|
+
]
|
|
90
|
+
if image_url
|
|
91
|
+
# RDoc writes og:title, og:description, and a summary Twitter card already.
|
|
92
|
+
html = html.sub(/(<meta\s+name="twitter:card"\s+content=")summary(")/i, '\1summary_large_image\2')
|
|
93
|
+
metadata << tag('meta', 'property' => 'og:image', 'content' => image_url)
|
|
94
|
+
metadata << tag('meta', 'property' => 'og:image:alt', 'content' => name)
|
|
95
|
+
metadata << tag('meta', 'name' => 'twitter:image', 'content' => image_url)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
unless redirect
|
|
99
|
+
website_id = "#{site_root}/#website"
|
|
100
|
+
page = {
|
|
101
|
+
'@type' => 'WebPage', '@id' => "#{canonical}#webpage", 'url' => canonical,
|
|
102
|
+
'name' => title, 'description' => page_description,
|
|
103
|
+
'isPartOf' => { '@id' => website_id }, 'inLanguage' => language
|
|
104
|
+
}
|
|
105
|
+
graph = [ {
|
|
106
|
+
'@type' => 'WebSite', '@id' => website_id, 'url' => "#{site_root}/",
|
|
107
|
+
'name' => name, 'description' => description, 'inLanguage' => language
|
|
108
|
+
} ]
|
|
109
|
+
if image_url
|
|
110
|
+
graph << { '@type' => 'ImageObject', '@id' => "#{image_url}#primaryimage", 'url' => image_url,
|
|
111
|
+
'contentUrl' => image_url, 'caption' => name }
|
|
112
|
+
page['primaryImageOfPage'] = { '@id' => "#{image_url}#primaryimage" }
|
|
113
|
+
end
|
|
114
|
+
graph << page
|
|
115
|
+
metadata << %(<script type="application/ld+json">#{json_ld({ '@context' => 'https://schema.org', '@graph' => graph })}</script>)
|
|
116
|
+
api_urls << canonical
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
html = html.sub(/<head>/i, "<head>\n#{metadata.join("\n")}")
|
|
120
|
+
html = html.sub(/<body\b/i, "</head>\n<body") unless html.match?(%r{</head>}i)
|
|
121
|
+
html = "#{html.rstrip}\n</html>\n" unless html.match?(%r{</html>}i)
|
|
122
|
+
path.write(html)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
sitemap_path = output_dir.parent.join('sitemap.xml')
|
|
126
|
+
if sitemap_path.file? && !api_urls.empty?
|
|
127
|
+
rows = api_urls.uniq.sort.map { |url| " <url>\n <loc>#{CGI.escapeHTML(url)}</loc>\n </url>" }.join("\n")
|
|
128
|
+
sitemap_path.write(sitemap_path.read.sub(%r{</urlset>}, "#{rows}\n</urlset>"))
|
|
129
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Fills in the version selector for one build: the release numbers, the base
|
|
5
|
+
# path, which entry is current, and one entry per archived major version.
|
|
6
|
+
# prepare_versions.rb <versions.yml> <stable|next|vN> <base-path> <release-tag> [archive-tag ...]
|
|
7
|
+
#
|
|
8
|
+
# It also records `release` (the version this build documents, which
|
|
9
|
+
# _plugins/versioned_docs.rb puts in site.version) and `base` (the site's
|
|
10
|
+
# base path without the version prefix).
|
|
11
|
+
|
|
12
|
+
require 'yaml'
|
|
13
|
+
|
|
14
|
+
path, current, base, release, *archives = ARGV
|
|
15
|
+
abort 'usage: prepare_versions.rb <versions.yml> <channel> <base-path> <release-tag> [archive-tag ...]' unless release
|
|
16
|
+
|
|
17
|
+
number = ->(tag) { tag.delete_prefix('v') }
|
|
18
|
+
major = ->(tag) { number.call(tag).split('.').first }
|
|
19
|
+
|
|
20
|
+
versions = YAML.load_file(path)
|
|
21
|
+
items = versions.fetch('items')
|
|
22
|
+
items.reject! { |item| item['archive'] }
|
|
23
|
+
position = items.index { |item| item['external'] } || items.size
|
|
24
|
+
archives.each_with_index do |tag, offset|
|
|
25
|
+
items.insert(position + offset,
|
|
26
|
+
{ 'id' => "v#{major.call(tag)}", 'title' => number.call(tag), 'url' => "/v#{major.call(tag)}/",
|
|
27
|
+
'archive' => true })
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
items.each do |item|
|
|
31
|
+
item['title'] = "#{number.call(release)} (stable)" if item['id'] == 'stable'
|
|
32
|
+
item['url'] = "#{base}#{item.fetch('url')}" unless item['external']
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
entry = items.find { |item| item['id'] == current } or abort "no version entry with id #{current}"
|
|
36
|
+
documented = archives.find { |tag| "v#{major.call(tag)}" == current } || release
|
|
37
|
+
versions['current'] = entry.fetch('title')
|
|
38
|
+
versions['release'] = number.call(documented)
|
|
39
|
+
versions['base'] = base
|
|
40
|
+
File.write(path, YAML.dump(versions))
|
data/docs/bin/serve.sh
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Preview the working tree at http://127.0.0.1:4000 with live reload. Ctrl-C stops it.
|
|
3
|
+
# docs/bin/serve.sh PORT=4002 docs/bin/serve.sh
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
unset RUBYOPT RUBYLIB BUNDLE_GEMFILE BUNDLE_BIN_PATH BUNDLE_BIN BUNDLE_APP_CONFIG
|
|
7
|
+
|
|
8
|
+
docs="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
9
|
+
cd "$docs"
|
|
10
|
+
exec env BUNDLE_GEMFILE="$docs/Gemfile" bundle exec jekyll serve \
|
|
11
|
+
--host 127.0.0.1 --port "${PORT:-4000}" \
|
|
12
|
+
--livereload --livereload-port "${LIVERELOAD_PORT:-35729}"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# The version selector. bin/build.sh copies it into each versioned build once
|
|
2
|
+
# a release has docs; prepare_versions.rb fills in the release number.
|
|
3
|
+
current: stable
|
|
4
|
+
items:
|
|
5
|
+
- id: stable
|
|
6
|
+
title: Stable
|
|
7
|
+
url: /
|
|
8
|
+
- id: next
|
|
9
|
+
title: Next (unreleased)
|
|
10
|
+
url: /next/
|
|
11
|
+
- id: changelog
|
|
12
|
+
title: Changelog
|
|
13
|
+
url: https://github.com/yorzi/ruby_llm-browser/releases
|
|
14
|
+
external: true
|