rubino-agent 0.3.0 → 0.5.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 +4 -4
- data/.rubocop_todo.yml +11 -2
- data/AGENTS.md +1 -1
- data/CHANGELOG.md +172 -5
- data/CONTRIBUTING.md +10 -1
- data/README.md +14 -5
- data/Rakefile +31 -0
- data/docs/agents.md +42 -23
- data/docs/architecture.md +2 -2
- data/docs/commands.md +35 -3
- data/docs/configuration.md +20 -23
- data/docs/getting-started.md +5 -3
- data/docs/security.md +16 -5
- data/docs/skills.md +31 -0
- data/docs/troubleshooting.md +1 -1
- data/exe/rubino +16 -2
- data/install.sh +721 -59
- data/lib/rubino/active_agent.rb +73 -0
- data/lib/rubino/agent/action_claim_guard.rb +881 -0
- data/lib/rubino/agent/agent_registry.rb +5 -2
- data/lib/rubino/agent/definition.rb +1 -9
- data/lib/rubino/agent/fallback_chain.rb +0 -6
- data/lib/rubino/agent/iteration_budget.rb +109 -3
- data/lib/rubino/agent/loop.rb +476 -20
- data/lib/rubino/agent/model_call_runner.rb +81 -3
- data/lib/rubino/agent/prompts/build.txt +22 -5
- data/lib/rubino/agent/response_validator.rb +8 -0
- data/lib/rubino/agent/runner.rb +133 -8
- data/lib/rubino/agent/tool_executor.rb +166 -14
- data/lib/rubino/agent/truncation_continuation.rb +4 -1
- data/lib/rubino/api/server.rb +19 -0
- data/lib/rubino/attachments/classify.rb +35 -17
- data/lib/rubino/boot/config_guard.rb +71 -0
- data/lib/rubino/cli/chat/completion_builder.rb +42 -6
- data/lib/rubino/cli/chat/idle_card_host.rb +7 -1
- data/lib/rubino/cli/chat/session_resolver.rb +87 -21
- data/lib/rubino/cli/chat_command.rb +1189 -50
- data/lib/rubino/cli/commands.rb +282 -2
- data/lib/rubino/cli/config_command.rb +68 -8
- data/lib/rubino/cli/doctor_command.rb +204 -12
- data/lib/rubino/cli/jobs_command.rb +12 -0
- data/lib/rubino/cli/memory_command.rb +53 -20
- data/lib/rubino/cli/onboarding_wizard.rb +79 -6
- data/lib/rubino/cli/session_command.rb +172 -18
- data/lib/rubino/cli/setup_command.rb +131 -8
- data/lib/rubino/cli/skills_command.rb +183 -9
- data/lib/rubino/cli/trust_gate.rb +16 -7
- data/lib/rubino/commands/built_ins.rb +2 -0
- data/lib/rubino/commands/command.rb +12 -2
- data/lib/rubino/commands/executor.rb +149 -12
- data/lib/rubino/commands/handlers/agent_switch.rb +100 -0
- data/lib/rubino/commands/handlers/agents.rb +156 -41
- data/lib/rubino/commands/handlers/config.rb +4 -1
- data/lib/rubino/commands/handlers/help.rb +113 -14
- data/lib/rubino/commands/handlers/memory.rb +15 -5
- data/lib/rubino/commands/handlers/sessions.rb +26 -3
- data/lib/rubino/commands/handlers/status.rb +9 -4
- data/lib/rubino/commands/loader.rb +12 -0
- data/lib/rubino/config/configuration.rb +86 -24
- data/lib/rubino/config/defaults.rb +140 -33
- data/lib/rubino/config/loader.rb +62 -12
- data/lib/rubino/config/validator.rb +341 -0
- data/lib/rubino/config/writer.rb +123 -31
- data/lib/rubino/context/compressor.rb +184 -22
- data/lib/rubino/context/environment_inspector.rb +2 -2
- data/lib/rubino/context/file_discovery.rb +2 -2
- data/lib/rubino/context/message_boundary.rb +27 -1
- data/lib/rubino/context/project_languages.rb +90 -0
- data/lib/rubino/context/prompt_assembler.rb +105 -22
- data/lib/rubino/context/summary_builder.rb +45 -4
- data/lib/rubino/context/token_budget.rb +36 -11
- data/lib/rubino/context/token_estimate.rb +45 -0
- data/lib/rubino/context/tool_result_pruner.rb +81 -0
- data/lib/rubino/database/connection.rb +154 -3
- data/lib/rubino/database/migrations/001_create_initial_schema.rb +314 -40
- data/lib/rubino/database/migrator.rb +98 -5
- data/lib/rubino/documents/cap_exceeded.rb +13 -0
- data/lib/rubino/documents/converters/csv.rb +4 -3
- data/lib/rubino/documents/converters/docx.rb +29 -5
- data/lib/rubino/documents/converters/html.rb +5 -1
- data/lib/rubino/documents/converters/json.rb +2 -1
- data/lib/rubino/documents/converters/pdf.rb +11 -2
- data/lib/rubino/documents/converters/plain.rb +2 -1
- data/lib/rubino/documents/converters/pptx.rb +11 -2
- data/lib/rubino/documents/converters/xlsx.rb +35 -4
- data/lib/rubino/documents/converters/xml.rb +2 -1
- data/lib/rubino/documents/limits.rb +210 -0
- data/lib/rubino/documents.rb +10 -3
- data/lib/rubino/errors.rb +36 -5
- data/lib/rubino/interaction/cancel_token.rb +19 -3
- data/lib/rubino/interaction/events.rb +13 -0
- data/lib/rubino/interaction/lifecycle.rb +99 -13
- data/lib/rubino/interaction/polishing.rb +176 -0
- data/lib/rubino/jobs/cron_job_repository.rb +5 -8
- data/lib/rubino/jobs/handlers/cleanup_sessions_job.rb +11 -0
- data/lib/rubino/jobs/handlers/distill_skill_job.rb +65 -9
- data/lib/rubino/jobs/queue.rb +63 -8
- data/lib/rubino/jobs/runner.rb +24 -6
- data/lib/rubino/jobs/worker.rb +0 -4
- data/lib/rubino/llm/adapter_response.rb +47 -4
- data/lib/rubino/llm/credential_check.rb +15 -16
- data/lib/rubino/llm/error_classifier.rb +89 -1
- data/lib/rubino/llm/inline_think_filter.rb +69 -12
- data/lib/rubino/llm/request.rb +30 -3
- data/lib/rubino/llm/ruby_llm_adapter.rb +394 -46
- data/lib/rubino/llm/tool_bridge.rb +113 -9
- data/lib/rubino/mcp/manager.rb +18 -1
- data/lib/rubino/mcp/mcp_tool_wrapper.rb +14 -3
- data/lib/rubino/memory/aux_retry.rb +107 -0
- data/lib/rubino/memory/backends/sqlite.rb +73 -44
- data/lib/rubino/memory/backends.rb +23 -7
- data/lib/rubino/memory/salience_gate.rb +103 -0
- data/lib/rubino/memory/sqlite_extraction.rb +70 -0
- data/lib/rubino/memory/sqlite_extraction_prompt.rb +11 -0
- data/lib/rubino/memory/store.rb +33 -5
- data/lib/rubino/memory/threat_scanner.rb +52 -0
- data/lib/rubino/output/cost.rb +52 -0
- data/lib/rubino/output/headless_block_latch.rb +53 -0
- data/lib/rubino/output/result_serializer.rb +222 -0
- data/lib/rubino/output/turn_recorder.rb +77 -0
- data/lib/rubino/security/approval_policy.rb +227 -32
- data/lib/rubino/security/command_allowlist.rb +79 -4
- data/lib/rubino/security/doom_loop_detector.rb +21 -2
- data/lib/rubino/security/hardline_guard.rb +189 -16
- data/lib/rubino/security/pattern_matcher.rb +28 -5
- data/lib/rubino/security/prefix_deriver.rb +25 -6
- data/lib/rubino/security/readonly_commands.rb +145 -5
- data/lib/rubino/security/secret_path.rb +134 -0
- data/lib/rubino/security/url_safety.rb +255 -0
- data/lib/rubino/session/repository.rb +212 -11
- data/lib/rubino/session/store.rb +139 -14
- data/lib/rubino/skills/installer.rb +230 -0
- data/lib/rubino/skills/prompt_index.rb +2 -2
- data/lib/rubino/skills/registry.rb +52 -1
- data/lib/rubino/skills/skill.rb +64 -3
- data/lib/rubino/skills/skill_tool.rb +16 -5
- data/lib/rubino/tools/background_tasks.rb +157 -13
- data/lib/rubino/tools/base.rb +204 -3
- data/lib/rubino/tools/edit_tool.rb +73 -18
- data/lib/rubino/tools/glob_tool.rb +48 -9
- data/lib/rubino/tools/grep_tool.rb +103 -9
- data/lib/rubino/tools/multi_edit_tool.rb +64 -9
- data/lib/rubino/tools/patch_tool.rb +5 -0
- data/lib/rubino/tools/read_attachment_tool.rb +3 -1
- data/lib/rubino/tools/read_tool.rb +33 -15
- data/lib/rubino/tools/read_tracker.rb +153 -35
- data/lib/rubino/tools/registry.rb +113 -12
- data/lib/rubino/tools/result.rb +9 -1
- data/lib/rubino/tools/ruby_tool.rb +0 -0
- data/lib/rubino/tools/shell_registry.rb +70 -0
- data/lib/rubino/tools/shell_tool.rb +40 -1
- data/lib/rubino/tools/summarize_file_tool.rb +6 -0
- data/lib/rubino/tools/task_stop_tool.rb +10 -16
- data/lib/rubino/tools/task_tool.rb +36 -8
- data/lib/rubino/tools/vision_tool.rb +5 -0
- data/lib/rubino/tools/webfetch_tool.rb +39 -7
- data/lib/rubino/tools/websearch_tool.rb +92 -30
- data/lib/rubino/tools/write_tool.rb +23 -4
- data/lib/rubino/ui/api.rb +10 -1
- data/lib/rubino/ui/base.rb +11 -0
- data/lib/rubino/ui/bottom_composer.rb +382 -74
- data/lib/rubino/ui/cli.rb +515 -83
- data/lib/rubino/ui/completion_menu.rb +11 -7
- data/lib/rubino/ui/headless_trace.rb +63 -0
- data/lib/rubino/ui/live_region.rb +70 -7
- data/lib/rubino/ui/markdown_renderer.rb +142 -7
- data/lib/rubino/ui/notifier.rb +0 -2
- data/lib/rubino/ui/null.rb +52 -5
- data/lib/rubino/ui/paste_store.rb +16 -2
- data/lib/rubino/ui/queued_indicators.rb +6 -1
- data/lib/rubino/ui/status_bar.rb +61 -7
- data/lib/rubino/ui/streaming_markdown.rb +59 -6
- data/lib/rubino/ui/subagent_view.rb +29 -4
- data/lib/rubino/ui/tool_label.rb +52 -0
- data/lib/rubino/update_check.rb +39 -4
- data/lib/rubino/util/atomic_file.rb +117 -0
- data/lib/rubino/util/ignore_rules.rb +120 -0
- data/lib/rubino/util/output.rb +229 -12
- data/lib/rubino/util/secrets_mask.rb +70 -7
- data/lib/rubino/util/spill_store.rb +153 -0
- data/lib/rubino/version.rb +1 -1
- data/lib/rubino/workspace.rb +9 -1
- data/lib/rubino.rb +191 -7
- data/rubino-agent.gemspec +1 -0
- data/skills/ruby-expert/SKILL.md +1 -0
- metadata +42 -12
- data/lib/rubino/agent/router.rb +0 -65
- data/lib/rubino/database/migrations/002_create_runs.rb +0 -45
- data/lib/rubino/database/migrations/003_create_skill_states.rb +0 -15
- data/lib/rubino/database/migrations/004_create_cron_jobs.rb +0 -36
- data/lib/rubino/database/migrations/005_create_oauth_connections.rb +0 -27
- data/lib/rubino/database/migrations/006_create_webhook_deliveries.rb +0 -34
- data/lib/rubino/database/migrations/007_create_messages_fts.rb +0 -59
- data/lib/rubino/database/migrations/008_create_memory_facts.rb +0 -75
- data/lib/rubino/database/migrations/009_create_memory_graph.rb +0 -55
- data/lib/rubino/database/migrations/010_add_owner_pid_to_sessions.rb +0 -20
data/install.sh
CHANGED
|
@@ -4,17 +4,37 @@
|
|
|
4
4
|
#
|
|
5
5
|
# curl -fsSL https://raw.githubusercontent.com/Jhonnyr97/rubino-agent/main/install.sh | bash
|
|
6
6
|
#
|
|
7
|
-
# What it does (all in user space, no sudo):
|
|
8
|
-
#
|
|
9
|
-
# - Linux: via `rv` (https://github.com/spinel-coop/rv), a fast Ruby
|
|
10
|
-
# version manager that fetches a precompiled Ruby (no build step).
|
|
11
|
-
# - macOS: if Homebrew is present you're asked whether to use Homebrew
|
|
12
|
-
# (`brew install ruby`) or rv; if Homebrew is absent it uses rv directly.
|
|
13
|
-
# 2. Installs the `rubino-agent` gem under that Ruby. If a published gem with
|
|
14
|
-
# the CLI isn't available yet, it falls back to building from this repo.
|
|
15
|
-
# 3. Prints the exact PATH line for the `rubino` executable.
|
|
7
|
+
# What it does (all in user space, no sudo): provisions a Ruby toolchain and
|
|
8
|
+
# installs the `rubino-agent` gem under it. There are THREE install methods:
|
|
16
9
|
#
|
|
17
|
-
#
|
|
10
|
+
# - Homebrew (macOS): `brew install ruby`, then `gem install rubino-agent`.
|
|
11
|
+
# - rv: fetches a precompiled Ruby via rv
|
|
12
|
+
# (https://github.com/spinel-coop/rv), then installs the gem.
|
|
13
|
+
# - mise: uses mise (https://mise.jdx.dev) and its `gem:` backend,
|
|
14
|
+
# which provisions a Ruby (`ruby@3.3`, precompiled) and
|
|
15
|
+
# registers `rubino` as a mise tool. mise additionally
|
|
16
|
+
# supports a global (user-wide) or local (per-project) scope.
|
|
17
|
+
#
|
|
18
|
+
# Method selection:
|
|
19
|
+
# - macOS (interactive): you're asked to pick 1) Homebrew 2) rv 3) mise.
|
|
20
|
+
# Default is Homebrew when present, else rv.
|
|
21
|
+
# - Linux (interactive): you're asked to pick 1) rv 2) mise (Homebrew offered
|
|
22
|
+
# only if `brew` is on PATH). Default is rv.
|
|
23
|
+
# - Non-interactive override: RUBINO_INSTALL_METHOD=brew|rv|mise.
|
|
24
|
+
#
|
|
25
|
+
# For the mise method only, choose the scope:
|
|
26
|
+
# RUBINO_INSTALL_SCOPE=global # default; user-wide (~/.config/mise/config.toml)
|
|
27
|
+
# RUBINO_INSTALL_SCOPE=local # this project/directory only (./mise.toml)
|
|
28
|
+
# (Or answer the follow-up prompt.)
|
|
29
|
+
#
|
|
30
|
+
# Other overrides:
|
|
31
|
+
# RUBINO_RUBY_VERSION=3.3.3 # Ruby pinned for the rv method.
|
|
32
|
+
#
|
|
33
|
+
# If a published gem with the CLI isn't available yet (brew/rv methods), the
|
|
34
|
+
# script falls back to building from this repo.
|
|
35
|
+
#
|
|
36
|
+
# Prerequisites: a C toolchain (cc/clang + make) is required because the gem
|
|
37
|
+
# builds native extensions. On Debian/Ubuntu: `apt-get install build-essential`.
|
|
18
38
|
#
|
|
19
39
|
# Security note: you are piping a script from the internet into a shell.
|
|
20
40
|
# Review it first: curl -fsSL <url> -o install.sh && less install.sh && bash install.sh
|
|
@@ -39,9 +59,17 @@ RUBY_VERSION="${RUBINO_RUBY_VERSION:-3.3.3}"
|
|
|
39
59
|
GEM_NAME="rubino-agent"
|
|
40
60
|
BIN_NAME="rubino"
|
|
41
61
|
|
|
42
|
-
#
|
|
62
|
+
# mise tool spec for the gem backend, and the Ruby to provision when mise has none.
|
|
63
|
+
GEM_TOOL="gem:${GEM_NAME}"
|
|
64
|
+
RUBY_TOOL="ruby@3.3"
|
|
65
|
+
|
|
66
|
+
# Optional: brew | rv | mise. When unset on macOS (or Linux with brew present),
|
|
67
|
+
# we prompt. Linux without brew prompts between rv and mise.
|
|
43
68
|
INSTALL_METHOD="${RUBINO_INSTALL_METHOD:-}"
|
|
44
69
|
|
|
70
|
+
# Optional (mise method only): global | local. When unset and interactive, we prompt.
|
|
71
|
+
INSTALL_SCOPE="${RUBINO_INSTALL_SCOPE:-}"
|
|
72
|
+
|
|
45
73
|
# --- output helpers ---------------------------------------------------------
|
|
46
74
|
|
|
47
75
|
if [ -t 1 ]; then
|
|
@@ -56,6 +84,201 @@ ok() { printf '%s==>%s %s\n' "$GREEN" "$RESET" "$*"; }
|
|
|
56
84
|
warn() { printf '%s==>%s %s\n' "$YELLOW" "$RESET" "$*" >&2; }
|
|
57
85
|
die() { printf '%serror:%s %s\n' "$RED" "$RESET" "$*" >&2; exit 1; }
|
|
58
86
|
|
|
87
|
+
# --- shell-rc activation (persist PATH / mise activation) -------------------
|
|
88
|
+
|
|
89
|
+
# Marker comment we drop next to the line(s) we append, so re-runs are
|
|
90
|
+
# idempotent and users can find/remove what we added.
|
|
91
|
+
RC_MARKER="# added by rubino installer (https://github.com/${REPO_OWNER}/${REPO_NAME})"
|
|
92
|
+
|
|
93
|
+
# Opt out of any shell-rc modification with RUBINO_NO_MODIFY_RC=1.
|
|
94
|
+
RUBINO_NO_MODIFY_RC="${RUBINO_NO_MODIFY_RC:-0}"
|
|
95
|
+
|
|
96
|
+
# Set by persist_to_rc() to the rc file(s) it touched. Init for `set -u` safety.
|
|
97
|
+
PERSISTED_RC=""
|
|
98
|
+
|
|
99
|
+
# The rc file the user is most likely to open/read (shown in hints). Honor
|
|
100
|
+
# $SHELL; default to bash so a `curl | bash` run (where $SHELL may be empty
|
|
101
|
+
# under -u) still persists.
|
|
102
|
+
detect_shell_rc() {
|
|
103
|
+
local shell_name
|
|
104
|
+
shell_name="$(basename "${SHELL:-bash}")"
|
|
105
|
+
case "$shell_name" in
|
|
106
|
+
zsh) printf '%s\n' "${ZDOTDIR:-$HOME}/.zshrc" ;;
|
|
107
|
+
bash) printf '%s\n' "$HOME/.bashrc" ;;
|
|
108
|
+
fish) printf '%s\n' "${__fish_config_dir:-$HOME/.config/fish}/config.fish" ;;
|
|
109
|
+
*) printf '%s\n' "$HOME/.profile" ;;
|
|
110
|
+
esac
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# The set of startup files to persist into. We cover BOTH the interactive rc
|
|
114
|
+
# (sourced when you open a terminal) AND the login-shell profile (sourced by
|
|
115
|
+
# `bash -l` / SSH logins) — on some distros the interactive rc early-returns for
|
|
116
|
+
# non-interactive shells, so the profile is what makes a login shell pick it up.
|
|
117
|
+
rc_targets() {
|
|
118
|
+
local shell_name
|
|
119
|
+
shell_name="$(basename "${SHELL:-bash}")"
|
|
120
|
+
case "$shell_name" in
|
|
121
|
+
zsh)
|
|
122
|
+
printf '%s\n' "${ZDOTDIR:-$HOME}/.zshrc"
|
|
123
|
+
printf '%s\n' "${ZDOTDIR:-$HOME}/.zprofile"
|
|
124
|
+
;;
|
|
125
|
+
bash)
|
|
126
|
+
printf '%s\n' "$HOME/.bashrc"
|
|
127
|
+
# bash login shells read the first of .bash_profile/.bash_login/.profile;
|
|
128
|
+
# prefer an existing one, else .profile (Debian/Ubuntu default sources .bashrc).
|
|
129
|
+
if [ -e "$HOME/.bash_profile" ]; then printf '%s\n' "$HOME/.bash_profile"
|
|
130
|
+
elif [ -e "$HOME/.bash_login" ]; then printf '%s\n' "$HOME/.bash_login"
|
|
131
|
+
else printf '%s\n' "$HOME/.profile"
|
|
132
|
+
fi
|
|
133
|
+
;;
|
|
134
|
+
fish)
|
|
135
|
+
# fish does NOT read ~/.profile (it isn't POSIX); its config lives in
|
|
136
|
+
# config.fish, sourced for every fish session (login + interactive).
|
|
137
|
+
printf '%s\n' "${__fish_config_dir:-$HOME/.config/fish}/config.fish"
|
|
138
|
+
;;
|
|
139
|
+
*)
|
|
140
|
+
printf '%s\n' "$HOME/.profile"
|
|
141
|
+
;;
|
|
142
|
+
esac
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# The shell-correct line that prepends $1 (a directory) to PATH, persisted into
|
|
146
|
+
# an rc file. POSIX shells (bash/zsh/sh) get `export PATH="DIR:$PATH"`; fish does
|
|
147
|
+
# NOT understand that syntax (no `$PATH` colon list, no `export`) — it needs
|
|
148
|
+
# `fish_add_path DIR`. Writing the POSIX form into config.fish would be ignored
|
|
149
|
+
# (or error), leaving fish users with a broken PATH while we report success
|
|
150
|
+
# (INST-R3-1). Args: $1 = bindir.
|
|
151
|
+
path_persist_line() {
|
|
152
|
+
local dir="$1" shell_name
|
|
153
|
+
shell_name="$(basename "${SHELL:-bash}")"
|
|
154
|
+
case "$shell_name" in
|
|
155
|
+
fish) printf 'fish_add_path %s\n' "$dir" ;;
|
|
156
|
+
*) printf 'export PATH="%s:$PATH"\n' "$dir" ;;
|
|
157
|
+
esac
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# Acquire an exclusive per-rc lock, run a command, release. The lock makes the
|
|
161
|
+
# check-then-append in _append_line_to_rc atomic: without it two concurrent
|
|
162
|
+
# installs both pass the `grep -qF` (the line is in neither yet) and both append,
|
|
163
|
+
# producing DUPLICATE activation blocks (TOCTOU).
|
|
164
|
+
#
|
|
165
|
+
# We use `mkdir` as the mutex primitive, not `flock`: mkdir is atomic on every
|
|
166
|
+
# POSIX filesystem and present on macOS/busybox alike (flock ships with
|
|
167
|
+
# util-linux and is absent on stock macOS). Spin with a short sleep until the
|
|
168
|
+
# lock dir is ours, with a stale-lock timeout so a crashed installer can't wedge
|
|
169
|
+
# the next one forever. Falls back to running unlocked only if even mkdir is
|
|
170
|
+
# somehow unavailable. Args: $1 = lock dir, $2... = command to run while held.
|
|
171
|
+
with_rc_lock() {
|
|
172
|
+
local lockdir="$1"; shift
|
|
173
|
+
local waited=0
|
|
174
|
+
# Try for up to ~5s (50 * 0.1s), then assume the holder died and proceed.
|
|
175
|
+
while ! mkdir "$lockdir" 2>/dev/null; do
|
|
176
|
+
if [ "$waited" -ge 50 ]; then
|
|
177
|
+
rm -rf "$lockdir" 2>/dev/null || true
|
|
178
|
+
mkdir "$lockdir" 2>/dev/null || break
|
|
179
|
+
break
|
|
180
|
+
fi
|
|
181
|
+
sleep 0.1
|
|
182
|
+
waited=$((waited + 1))
|
|
183
|
+
done
|
|
184
|
+
# Ensure we drop the lock even if the command fails.
|
|
185
|
+
"$@"
|
|
186
|
+
local rc=$?
|
|
187
|
+
rmdir "$lockdir" 2>/dev/null || true
|
|
188
|
+
return "$rc"
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# Append a single line, once, to one rc file. Idempotent via RC_MARKER + a grep
|
|
192
|
+
# for the exact line. MUST run under with_rc_lock so the grep-then-append can't
|
|
193
|
+
# race a concurrent install. Echoes "touched" if the line is present afterward.
|
|
194
|
+
_append_line_to_rc() {
|
|
195
|
+
local line="$1" rc="$2"
|
|
196
|
+
# Create the file if missing (login shells will source it). Ensure the parent
|
|
197
|
+
# dir exists first — fish's config.fish lives under ~/.config/fish, which may
|
|
198
|
+
# not exist yet on a fresh box (a bare `: >"$rc"` would then fail silently).
|
|
199
|
+
[ -e "$rc" ] || mkdir -p "$(dirname "$rc")" 2>/dev/null || true
|
|
200
|
+
[ -e "$rc" ] || : >"$rc" 2>/dev/null || return 0
|
|
201
|
+
if grep -qF "$line" "$rc" 2>/dev/null; then
|
|
202
|
+
printf 'touched'
|
|
203
|
+
return 0
|
|
204
|
+
fi
|
|
205
|
+
if {
|
|
206
|
+
printf '\n%s\n' "$RC_MARKER"
|
|
207
|
+
printf '%s\n' "$line"
|
|
208
|
+
} >>"$rc" 2>/dev/null; then
|
|
209
|
+
printf 'touched'
|
|
210
|
+
fi
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# Append a single line, once, to each startup file from rc_targets(). Guarded by
|
|
214
|
+
# RC_MARKER + a grep for the exact line so re-runs don't duplicate, and by an
|
|
215
|
+
# exclusive per-rc lock so CONCURRENT installs don't duplicate either (TOCTOU).
|
|
216
|
+
# Sets PERSISTED_RC to the space-separated files it touched. Returns 0 if the
|
|
217
|
+
# line is present in at least one target afterward.
|
|
218
|
+
persist_to_rc() {
|
|
219
|
+
local line="$1" rc any=1 touched="" res
|
|
220
|
+
[ "$RUBINO_NO_MODIFY_RC" = "1" ] && return 1
|
|
221
|
+
while IFS= read -r rc; do
|
|
222
|
+
[ -n "$rc" ] || continue
|
|
223
|
+
# The subshell scopes the "touched" capture; the lock serializes the
|
|
224
|
+
# check-then-append against any other installer touching this same rc.
|
|
225
|
+
res="$(with_rc_lock "${rc}.rubino.lock.d" _append_line_to_rc "$line" "$rc")"
|
|
226
|
+
if [ "$res" = "touched" ]; then
|
|
227
|
+
touched="${touched:+$touched }$rc"; any=0
|
|
228
|
+
fi
|
|
229
|
+
done <<EOF
|
|
230
|
+
$(rc_targets)
|
|
231
|
+
EOF
|
|
232
|
+
PERSISTED_RC="$touched"
|
|
233
|
+
return "$any"
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# Post-install gate: confirm `$BIN_NAME` is reachable from a FRESH interactive/
|
|
237
|
+
# login shell (not just this process). If it isn't, fail loudly with the exact
|
|
238
|
+
# line to paste — never print a success banner over a broken install.
|
|
239
|
+
# Args: $1 = the activation/PATH line we tried to persist (for the error hint).
|
|
240
|
+
verify_fresh_shell() {
|
|
241
|
+
local fix_line="$1" shell_name
|
|
242
|
+
shell_name="$(basename "${SHELL:-bash}")"
|
|
243
|
+
|
|
244
|
+
# Probe a fresh login+interactive shell — interactive so the rc body (which on
|
|
245
|
+
# some distros is guarded by a non-interactive early-return) actually runs,
|
|
246
|
+
# login so profile files are sourced too. This mirrors opening a new terminal.
|
|
247
|
+
local found=1
|
|
248
|
+
case "$shell_name" in
|
|
249
|
+
zsh) zsh -i -c "command -v ${BIN_NAME} >/dev/null 2>&1" >/dev/null 2>&1 || found=0 ;;
|
|
250
|
+
# fish: probe fish itself (a fresh login+interactive session sources
|
|
251
|
+
# config.fish), NOT bash -lic — bash would find a POSIX export the user's
|
|
252
|
+
# fish never reads, reporting a false success over a broken fish (INST-R3-1).
|
|
253
|
+
# `type -q` is fish's `command -v`. If fish isn't installed to probe with,
|
|
254
|
+
# fall through to a best-effort PATH check rather than claim success.
|
|
255
|
+
fish)
|
|
256
|
+
if command -v fish >/dev/null 2>&1; then
|
|
257
|
+
fish -l -i -c "type -q ${BIN_NAME}" >/dev/null 2>&1 || found=0
|
|
258
|
+
else
|
|
259
|
+
command -v "${BIN_NAME}" >/dev/null 2>&1 || found=0
|
|
260
|
+
fi
|
|
261
|
+
;;
|
|
262
|
+
*) bash -lic "command -v ${BIN_NAME} >/dev/null 2>&1" >/dev/null 2>&1 || found=0 ;;
|
|
263
|
+
esac
|
|
264
|
+
|
|
265
|
+
if [ "$found" -eq 1 ]; then
|
|
266
|
+
ok "Verified: a fresh login shell finds '${BIN_NAME}'."
|
|
267
|
+
return 0
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
# Broken: tell the user exactly what to do, then exit non-zero.
|
|
271
|
+
printf '\n'
|
|
272
|
+
warn "${BIN_NAME} was installed but a fresh login shell can't find it yet."
|
|
273
|
+
if [ "$RUBINO_NO_MODIFY_RC" = "1" ]; then
|
|
274
|
+
warn "RUBINO_NO_MODIFY_RC=1 is set, so no shell rc was modified."
|
|
275
|
+
fi
|
|
276
|
+
printf '%sAdd this line to your shell profile%s (%s), then open a new shell:\n' \
|
|
277
|
+
"$BOLD" "$RESET" "$(detect_shell_rc)" >&2
|
|
278
|
+
printf '\n %s\n\n' "$fix_line" >&2
|
|
279
|
+
die "post-install check failed: '${BIN_NAME}' not on PATH in a fresh shell (see the line above)."
|
|
280
|
+
}
|
|
281
|
+
|
|
59
282
|
# --- preflight: OS / arch ---------------------------------------------------
|
|
60
283
|
|
|
61
284
|
OS="$(uname -s)"
|
|
@@ -77,57 +300,233 @@ need() { command -v "$1" >/dev/null 2>&1 || die "required command not found: $1
|
|
|
77
300
|
need curl
|
|
78
301
|
need uname
|
|
79
302
|
|
|
80
|
-
# ---
|
|
303
|
+
# --- preflight: build prerequisites (#242) ----------------------------------
|
|
304
|
+
#
|
|
305
|
+
# The gem builds native extensions (e.g. nio4r), so a C toolchain is always
|
|
306
|
+
# needed; the rv/mise methods additionally fetch + unpack precompiled tarballs
|
|
307
|
+
# (xz) and may clone the repo (git). Rather than let `gem install` blow up deep
|
|
308
|
+
# in a native build, check the prerequisites the CHOSEN method needs up front:
|
|
309
|
+
# install them automatically when we're privileged (root + a known pkg manager),
|
|
310
|
+
# otherwise fail with a single actionable command the user can copy-paste.
|
|
311
|
+
|
|
312
|
+
# True when we can install OS packages non-interactively (root, or sudo present).
|
|
313
|
+
can_install_pkgs() {
|
|
314
|
+
[ "$(id -u 2>/dev/null || echo 1000)" = "0" ] || command -v sudo >/dev/null 2>&1
|
|
315
|
+
}
|
|
81
316
|
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
317
|
+
# Run a privileged command (direct as root, else via sudo).
|
|
318
|
+
as_root() {
|
|
319
|
+
if [ "$(id -u 2>/dev/null || echo 1000)" = "0" ]; then "$@"; else sudo "$@"; fi
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
# Detect a C compiler (any of cc/gcc/clang) — toolchains name it differently.
|
|
323
|
+
have_cc() { for c in cc gcc clang; do command -v "$c" >/dev/null 2>&1 && return 0; done; return 1; }
|
|
324
|
+
|
|
325
|
+
# Map an abstract prerequisite to the package providing it, install via the
|
|
326
|
+
# host's package manager. Returns non-zero if we couldn't install it.
|
|
327
|
+
install_pkg_for() {
|
|
328
|
+
local want="$1" # one of: toolchain xz git curl
|
|
329
|
+
if command -v apt-get >/dev/null 2>&1; then
|
|
330
|
+
local pkg
|
|
331
|
+
case "$want" in
|
|
332
|
+
toolchain) pkg="build-essential" ;;
|
|
333
|
+
xz) pkg="xz-utils" ;;
|
|
334
|
+
git) pkg="git" ;;
|
|
335
|
+
curl) pkg="curl" ;;
|
|
336
|
+
esac
|
|
337
|
+
as_root env DEBIAN_FRONTEND=noninteractive apt-get update -qq >/dev/null 2>&1 || true
|
|
338
|
+
as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "$pkg" >/dev/null 2>&1
|
|
339
|
+
elif command -v dnf >/dev/null 2>&1; then
|
|
340
|
+
case "$want" in
|
|
341
|
+
toolchain) as_root dnf install -y -q gcc make >/dev/null 2>&1 ;;
|
|
342
|
+
xz) as_root dnf install -y -q xz >/dev/null 2>&1 ;;
|
|
343
|
+
git) as_root dnf install -y -q git >/dev/null 2>&1 ;;
|
|
344
|
+
curl) as_root dnf install -y -q curl >/dev/null 2>&1 ;;
|
|
345
|
+
esac
|
|
346
|
+
elif command -v apk >/dev/null 2>&1; then
|
|
347
|
+
case "$want" in
|
|
348
|
+
toolchain) as_root apk add --no-cache build-base >/dev/null 2>&1 ;;
|
|
349
|
+
xz) as_root apk add --no-cache xz >/dev/null 2>&1 ;;
|
|
350
|
+
git) as_root apk add --no-cache git >/dev/null 2>&1 ;;
|
|
351
|
+
curl) as_root apk add --no-cache curl >/dev/null 2>&1 ;;
|
|
352
|
+
esac
|
|
353
|
+
else
|
|
354
|
+
return 1
|
|
89
355
|
fi
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
# The copy-paste install command we suggest when we can't install ourselves.
|
|
359
|
+
pkg_hint() {
|
|
360
|
+
case "$PLATFORM" in
|
|
361
|
+
macos) printf 'xcode-select --install # C toolchain (Homebrew also provides git/curl)' ;;
|
|
362
|
+
linux)
|
|
363
|
+
if command -v apt-get >/dev/null 2>&1; then printf 'sudo apt-get install -y build-essential xz-utils git curl'
|
|
364
|
+
elif command -v dnf >/dev/null 2>&1; then printf 'sudo dnf install -y gcc make xz git curl'
|
|
365
|
+
elif command -v apk >/dev/null 2>&1; then printf 'sudo apk add build-base xz git curl'
|
|
366
|
+
else printf 'install a C toolchain (gcc/clang + make), xz, git and curl with your package manager'
|
|
367
|
+
fi
|
|
368
|
+
;;
|
|
369
|
+
esac
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
# Check (and, when privileged, install) the prerequisites the chosen method
|
|
373
|
+
# needs. Args: $1 = method (brew|rv|mise). Fails loudly if a hard requirement is
|
|
374
|
+
# missing and we can't provide it.
|
|
375
|
+
preflight_prereqs() {
|
|
376
|
+
local method="$1"
|
|
377
|
+
# Every method ends up building native extensions → needs a C toolchain.
|
|
378
|
+
# rv/mise fetch and unpack xz tarballs → need xz. The git fallback needs git.
|
|
379
|
+
# (curl is already required at the top of the script.)
|
|
380
|
+
local needs="toolchain"
|
|
381
|
+
case "$method" in
|
|
382
|
+
rv|mise) needs="toolchain xz git" ;;
|
|
383
|
+
brew) needs="toolchain git" ;;
|
|
384
|
+
esac
|
|
385
|
+
|
|
386
|
+
local want missing="" installed=""
|
|
387
|
+
for want in $needs; do
|
|
388
|
+
local present=0
|
|
389
|
+
case "$want" in
|
|
390
|
+
toolchain) have_cc && command -v make >/dev/null 2>&1 && present=1 ;;
|
|
391
|
+
*) command -v "$want" >/dev/null 2>&1 && present=1 ;;
|
|
392
|
+
esac
|
|
393
|
+
[ "$present" -eq 1 ] && continue
|
|
394
|
+
|
|
395
|
+
# macOS: don't try to install the toolchain ourselves (xcode-select is
|
|
396
|
+
# interactive); just record it as missing for the actionable error.
|
|
397
|
+
if [ "$PLATFORM" = "macos" ] || ! can_install_pkgs; then
|
|
398
|
+
missing="${missing:+$missing }$want"
|
|
399
|
+
continue
|
|
400
|
+
fi
|
|
401
|
+
|
|
402
|
+
info "Missing build prerequisite '${want}'; installing it..."
|
|
403
|
+
if install_pkg_for "$want"; then
|
|
404
|
+
# Re-check so we don't claim success on a no-op package manager.
|
|
405
|
+
local ok_now=0
|
|
406
|
+
case "$want" in
|
|
407
|
+
toolchain) have_cc && command -v make >/dev/null 2>&1 && ok_now=1 ;;
|
|
408
|
+
*) command -v "$want" >/dev/null 2>&1 && ok_now=1 ;;
|
|
409
|
+
esac
|
|
410
|
+
if [ "$ok_now" -eq 1 ]; then installed="${installed:+$installed }$want"
|
|
411
|
+
else missing="${missing:+$missing }$want"; fi
|
|
412
|
+
else
|
|
413
|
+
missing="${missing:+$missing }$want"
|
|
414
|
+
fi
|
|
415
|
+
done
|
|
416
|
+
|
|
417
|
+
[ -n "$installed" ] && ok "Installed build prerequisites: ${installed}."
|
|
418
|
+
|
|
419
|
+
if [ -n "$missing" ]; then
|
|
420
|
+
warn "Missing build prerequisite(s): ${missing}."
|
|
421
|
+
warn "rubino's gem builds native extensions and the ${method} method needs these to proceed."
|
|
422
|
+
printf '%sInstall them, then re-run this installer:%s\n' "$BOLD" "$RESET" >&2
|
|
423
|
+
printf '\n %s\n\n' "$(pkg_hint)" >&2
|
|
424
|
+
die "missing build prerequisites: ${missing} (see the command above)."
|
|
425
|
+
fi
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
# --- choose install method (brew | rv | mise) -------------------------------
|
|
429
|
+
|
|
430
|
+
# Honor an explicit RUBINO_INSTALL_METHOD. Otherwise prompt on /dev/tty (so it
|
|
431
|
+
# works under `curl ... | bash`, where stdin is the script itself). The offered
|
|
432
|
+
# options differ by platform: macOS leads with Homebrew (when present); Linux
|
|
433
|
+
# offers rv + mise (and Homebrew only if `brew` happens to be on PATH).
|
|
434
|
+
#
|
|
435
|
+
# A /dev/tty node can exist (e.g. in a container) yet not be openable when
|
|
436
|
+
# there's no controlling terminal. Probe an actual open before prompting so we
|
|
437
|
+
# silently fall back to the default instead of erroring on the redirect.
|
|
438
|
+
tty_usable() { { : >/dev/tty; } 2>/dev/null && { : </dev/tty; } 2>/dev/null; }
|
|
90
439
|
|
|
440
|
+
choose_method() {
|
|
91
441
|
case "$INSTALL_METHOD" in
|
|
92
442
|
brew) printf 'brew\n'; return 0 ;;
|
|
93
443
|
rv) printf 'rv\n'; return 0 ;;
|
|
444
|
+
mise) printf 'mise\n'; return 0 ;;
|
|
94
445
|
"") ;;
|
|
95
|
-
*) die "RUBINO_INSTALL_METHOD must be 'brew' or '
|
|
446
|
+
*) die "RUBINO_INSTALL_METHOD must be 'brew', 'rv', or 'mise' (got '${INSTALL_METHOD}')." ;;
|
|
96
447
|
esac
|
|
97
448
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
449
|
+
local have_brew=0
|
|
450
|
+
command -v brew >/dev/null 2>&1 && have_brew=1
|
|
451
|
+
|
|
452
|
+
# Defaults preserve prior behavior: macOS → brew if present else rv; Linux → rv.
|
|
453
|
+
local default_method
|
|
454
|
+
if [ "$PLATFORM" = "macos" ] && [ "$have_brew" -eq 1 ]; then
|
|
455
|
+
default_method="brew"
|
|
456
|
+
else
|
|
457
|
+
default_method="rv"
|
|
101
458
|
fi
|
|
102
459
|
|
|
103
|
-
|
|
104
|
-
|
|
460
|
+
if ! tty_usable; then
|
|
461
|
+
# Non-interactive, no override → platform default.
|
|
462
|
+
if [ "$default_method" = "brew" ]; then
|
|
463
|
+
warn "Homebrew detected but no interactive terminal; defaulting to Homebrew. Set RUBINO_INSTALL_METHOD=rv or =mise to choose another method."
|
|
464
|
+
fi
|
|
465
|
+
printf '%s\n' "$default_method"; return 0
|
|
466
|
+
fi
|
|
467
|
+
|
|
468
|
+
# Interactive: build a numbered menu. macOS leads with Homebrew (when present).
|
|
469
|
+
local ans=""
|
|
470
|
+
if [ "$PLATFORM" = "macos" ] && [ "$have_brew" -eq 1 ]; then
|
|
105
471
|
{
|
|
106
|
-
printf '\n%
|
|
107
|
-
printf ' %s1)%s Homebrew %s(brew install ruby)%s\n'
|
|
108
|
-
printf ' %s2)%s rv %s(fast, self-contained, no Homebrew)%s\n'
|
|
109
|
-
printf '
|
|
472
|
+
printf '\n%sHow should rubino be installed?%s\n' "$BOLD" "$RESET"
|
|
473
|
+
printf ' %s1)%s Homebrew %s(brew install ruby)%s\n' "$BOLD" "$RESET" "$DIM" "$RESET"
|
|
474
|
+
printf ' %s2)%s rv %s(fast, self-contained, no Homebrew)%s\n' "$BOLD" "$RESET" "$DIM" "$RESET"
|
|
475
|
+
printf ' %s3)%s mise %s(polyglot tool manager, global or local scope)%s\n' "$BOLD" "$RESET" "$DIM" "$RESET"
|
|
476
|
+
printf 'Choose %s[1/2/3]%s (default 1, Homebrew): ' "$BOLD" "$RESET"
|
|
110
477
|
} >/dev/tty
|
|
111
|
-
local ans=""
|
|
112
478
|
read -r ans </dev/tty || ans=""
|
|
113
479
|
case "$ans" in
|
|
114
|
-
2|rv|RV)
|
|
115
|
-
|
|
116
|
-
|
|
480
|
+
2|rv|RV) printf 'rv\n' ;;
|
|
481
|
+
3|mise|MISE) printf 'mise\n' ;;
|
|
482
|
+
""|1|brew|BREW) printf 'brew\n' ;;
|
|
483
|
+
*) printf 'brew\n' ;;
|
|
117
484
|
esac
|
|
118
485
|
return 0
|
|
119
486
|
fi
|
|
120
487
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
488
|
+
if [ "$have_brew" -eq 1 ]; then
|
|
489
|
+
# Linux with brew present: offer all three, default rv.
|
|
490
|
+
{
|
|
491
|
+
printf '\n%sHow should rubino be installed?%s\n' "$BOLD" "$RESET"
|
|
492
|
+
printf ' %s1)%s rv %s(fast, self-contained; recommended)%s\n' "$BOLD" "$RESET" "$DIM" "$RESET"
|
|
493
|
+
printf ' %s2)%s mise %s(polyglot tool manager, global or local scope)%s\n' "$BOLD" "$RESET" "$DIM" "$RESET"
|
|
494
|
+
printf ' %s3)%s Homebrew %s(brew install ruby)%s\n' "$BOLD" "$RESET" "$DIM" "$RESET"
|
|
495
|
+
printf 'Choose %s[1/2/3]%s (default 1, rv): ' "$BOLD" "$RESET"
|
|
496
|
+
} >/dev/tty
|
|
497
|
+
read -r ans </dev/tty || ans=""
|
|
498
|
+
case "$ans" in
|
|
499
|
+
2|mise|MISE) printf 'mise\n' ;;
|
|
500
|
+
3|brew|BREW) printf 'brew\n' ;;
|
|
501
|
+
""|1|rv|RV) printf 'rv\n' ;;
|
|
502
|
+
*) printf 'rv\n' ;;
|
|
503
|
+
esac
|
|
504
|
+
return 0
|
|
505
|
+
fi
|
|
506
|
+
|
|
507
|
+
# Linux without brew: rv vs mise, default rv.
|
|
508
|
+
{
|
|
509
|
+
printf '\n%sHow should rubino be installed?%s\n' "$BOLD" "$RESET"
|
|
510
|
+
printf ' %s1)%s rv %s(fast, self-contained; recommended)%s\n' "$BOLD" "$RESET" "$DIM" "$RESET"
|
|
511
|
+
printf ' %s2)%s mise %s(polyglot tool manager, global or local scope)%s\n' "$BOLD" "$RESET" "$DIM" "$RESET"
|
|
512
|
+
printf 'Choose %s[1/2]%s (default 1, rv): ' "$BOLD" "$RESET"
|
|
513
|
+
} >/dev/tty
|
|
514
|
+
read -r ans </dev/tty || ans=""
|
|
515
|
+
case "$ans" in
|
|
516
|
+
2|mise|MISE) printf 'mise\n' ;;
|
|
517
|
+
""|1|rv|RV) printf 'rv\n' ;;
|
|
518
|
+
*) printf 'rv\n' ;;
|
|
519
|
+
esac
|
|
125
520
|
}
|
|
126
521
|
|
|
127
522
|
METHOD="$(choose_method)"
|
|
128
523
|
|
|
524
|
+
# Now that we know the method, check (and auto-install when privileged) the
|
|
525
|
+
# build prerequisites it needs, with a clear actionable error otherwise (#242).
|
|
526
|
+
preflight_prereqs "$METHOD"
|
|
527
|
+
|
|
129
528
|
# `rubyx <cmd...>` runs a command (gem/bundle/rake/ruby) under the Ruby we set
|
|
130
|
-
# up,
|
|
529
|
+
# up, for the brew/rv methods. Each setup_* defines it plus RUBY_LABEL.
|
|
131
530
|
rubyx() { die "internal: ruby toolchain not initialized"; }
|
|
132
531
|
|
|
133
532
|
# --- ruby toolchain: rv -----------------------------------------------------
|
|
@@ -144,7 +543,7 @@ setup_ruby_rv() {
|
|
|
144
543
|
return 1
|
|
145
544
|
}
|
|
146
545
|
|
|
147
|
-
local
|
|
546
|
+
# NOTE: rv_bin is intentionally NOT local: rubyx() reads it after we return.
|
|
148
547
|
if rv_bin="$(locate_rv)"; then
|
|
149
548
|
ok "rv already installed: ${rv_bin}"
|
|
150
549
|
else
|
|
@@ -156,10 +555,30 @@ setup_ruby_rv() {
|
|
|
156
555
|
export PATH="$(dirname "$rv_bin"):${PATH}"
|
|
157
556
|
|
|
158
557
|
info "Installing Ruby ${RUBY_VERSION} via rv (precompiled, no build step)..."
|
|
159
|
-
|
|
558
|
+
# On systems whose glibc rv considers "too old" (e.g. Debian 12 / glibc 2.36)
|
|
559
|
+
# rv installs a musl-static build and then provisions a musl Ruby that this
|
|
560
|
+
# glibc system can't execute. `rv ruby install` may even print "Installed",
|
|
561
|
+
# but `rv ruby find` then reports NoMatchingRuby — a silent, broken install
|
|
562
|
+
# (#241). We don't `die` here: rv on such a system simply can't provide a
|
|
563
|
+
# working Ruby, but the mise path (precompiled, glibc-correct) can. So we
|
|
564
|
+
# steer the user over to mise instead of leaving them with a broken rubino.
|
|
565
|
+
#
|
|
566
|
+
# Detection is post-hoc on `rv ruby find`: it's the exact failure the user
|
|
567
|
+
# hits, regardless of the underlying cause, and it doesn't regress the working
|
|
568
|
+
# ubuntu/rv path (where `find` succeeds and we proceed as before).
|
|
569
|
+
if ! "$rv_bin" ruby install "${RUBY_VERSION}" >/dev/null 2>&1; then
|
|
570
|
+
warn "rv could not install Ruby ${RUBY_VERSION} on this system."
|
|
571
|
+
fallback_to_mise_from_rv
|
|
572
|
+
return 0 # not reached: fallback_to_mise_from_rv exits the script.
|
|
573
|
+
fi
|
|
160
574
|
local ruby_bin
|
|
161
|
-
ruby_bin="$("$rv_bin" ruby find "${RUBY_VERSION}")"
|
|
162
|
-
|
|
575
|
+
if ! ruby_bin="$("$rv_bin" ruby find "${RUBY_VERSION}" 2>/dev/null)" || [ -z "$ruby_bin" ] || [ ! -x "$ruby_bin" ]; then
|
|
576
|
+
warn "rv installed Ruby ${RUBY_VERSION} but can't locate a usable binary for it"
|
|
577
|
+
warn "(common on Debian 12 / older glibc, where rv falls back to a musl build"
|
|
578
|
+
warn "that this system can't execute)."
|
|
579
|
+
fallback_to_mise_from_rv
|
|
580
|
+
return 0 # not reached.
|
|
581
|
+
fi
|
|
163
582
|
RUBY_BIN_DIR="$(dirname "$ruby_bin")"
|
|
164
583
|
RUBY_LABEL="Ruby ${RUBY_VERSION} (rv)"
|
|
165
584
|
|
|
@@ -167,6 +586,18 @@ setup_ruby_rv() {
|
|
|
167
586
|
ok "${RUBY_LABEL} ready: ${RUBY_BIN_DIR}"
|
|
168
587
|
}
|
|
169
588
|
|
|
589
|
+
# Hand off from a broken rv install to the mise method, which provisions a
|
|
590
|
+
# precompiled glibc-correct Ruby that works where rv's musl build doesn't (#241).
|
|
591
|
+
# setup_mise() runs the full install and exits, so this never returns.
|
|
592
|
+
fallback_to_mise_from_rv() {
|
|
593
|
+
warn "Falling back to the mise install method (works on this system)."
|
|
594
|
+
METHOD="mise"
|
|
595
|
+
# mise needs the same build prerequisites; re-run preflight for its method now
|
|
596
|
+
# that we've switched (the earlier preflight ran for 'rv').
|
|
597
|
+
preflight_prereqs "mise"
|
|
598
|
+
setup_mise
|
|
599
|
+
}
|
|
600
|
+
|
|
170
601
|
# --- ruby toolchain: Homebrew ----------------------------------------------
|
|
171
602
|
|
|
172
603
|
setup_ruby_brew() {
|
|
@@ -190,34 +621,230 @@ setup_ruby_brew() {
|
|
|
190
621
|
ok "${RUBY_LABEL} ready: ${RUBY_BIN_DIR}"
|
|
191
622
|
}
|
|
192
623
|
|
|
624
|
+
# --- ruby toolchain + gem install: mise -------------------------------------
|
|
625
|
+
|
|
626
|
+
# mise is special: it both provisions Ruby and installs the gem (via its `gem:`
|
|
627
|
+
# backend), and supports global/local scope. So setup_mise() runs the full
|
|
628
|
+
# install and then exits the script, rather than returning into the shared
|
|
629
|
+
# gem-install path used by brew/rv.
|
|
630
|
+
setup_mise() {
|
|
631
|
+
# mise's installer drops the binary in ~/.local/bin/mise (or $MISE_INSTALL_PATH).
|
|
632
|
+
# We pass through without forcing shell-rc edits and locate the binary ourselves.
|
|
633
|
+
locate_mise() {
|
|
634
|
+
if command -v mise >/dev/null 2>&1; then command -v mise; return 0; fi
|
|
635
|
+
for cand in "${MISE_INSTALL_PATH:-}" "$HOME/.local/bin/mise"; do
|
|
636
|
+
[ -n "$cand" ] && [ -x "$cand" ] && { printf '%s\n' "$cand"; return 0; }
|
|
637
|
+
done
|
|
638
|
+
return 1
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
local mise_bin
|
|
642
|
+
if mise_bin="$(locate_mise)"; then
|
|
643
|
+
ok "mise already installed: ${mise_bin}"
|
|
644
|
+
else
|
|
645
|
+
info "Installing mise (polyglot tool/version manager)..."
|
|
646
|
+
curl -fsSL https://mise.run | sh
|
|
647
|
+
mise_bin="$(locate_mise)" || die "mise install completed but the mise binary wasn't found at ~/.local/bin/mise or \$MISE_INSTALL_PATH."
|
|
648
|
+
ok "Installed mise: ${mise_bin}"
|
|
649
|
+
fi
|
|
650
|
+
|
|
651
|
+
# Use the located binary for everything so we don't depend on the user's shell
|
|
652
|
+
# being activated. (`mise` may print activation hints to stderr; that's fine.)
|
|
653
|
+
mise() { "$mise_bin" "$@"; }
|
|
654
|
+
|
|
655
|
+
# Put the mise bindir on PATH for the rest of this process. The gem: backend
|
|
656
|
+
# installs a RubyGems plugin whose post-install hook shells out to a bare `mise`
|
|
657
|
+
# (mise reshim); on a freshly bootstrapped machine that binary isn't on PATH yet,
|
|
658
|
+
# so the gem install would error with "No such file or directory - mise". Adding
|
|
659
|
+
# its dir here makes the hook resolve. (No persistent shell-rc edit.)
|
|
660
|
+
case ":${PATH}:" in
|
|
661
|
+
*":$(dirname "$mise_bin"):"*) ;;
|
|
662
|
+
*) PATH="$(dirname "$mise_bin"):${PATH}"; export PATH ;;
|
|
663
|
+
esac
|
|
664
|
+
|
|
665
|
+
# The gem backend is experimental; persist the setting.
|
|
666
|
+
info "Enabling mise experimental features (required for the gem: backend)..."
|
|
667
|
+
mise settings experimental=true || die "failed to persist 'mise settings experimental=true'."
|
|
668
|
+
|
|
669
|
+
# The gem backend needs a Ruby to install under and to build native exts with.
|
|
670
|
+
# If mise can't resolve a ruby, provision a precompiled one globally.
|
|
671
|
+
if mise which ruby >/dev/null 2>&1; then
|
|
672
|
+
ok "mise already manages a Ruby: $(mise which ruby 2>/dev/null || echo '?')"
|
|
673
|
+
else
|
|
674
|
+
info "No mise-managed Ruby found; installing ${RUBY_TOOL} (precompiled)..."
|
|
675
|
+
mise use -g "${RUBY_TOOL}" || die "mise use -g ${RUBY_TOOL} failed."
|
|
676
|
+
ok "Ruby ready via mise (${RUBY_TOOL})."
|
|
677
|
+
fi
|
|
678
|
+
|
|
679
|
+
# Choose scope (global default, or local/project).
|
|
680
|
+
local scope=""
|
|
681
|
+
case "$INSTALL_SCOPE" in
|
|
682
|
+
global|local) scope="$INSTALL_SCOPE" ;;
|
|
683
|
+
"") ;;
|
|
684
|
+
*) die "RUBINO_INSTALL_SCOPE must be 'global' or 'local' (got '${INSTALL_SCOPE}')." ;;
|
|
685
|
+
esac
|
|
686
|
+
|
|
687
|
+
if [ -z "$scope" ]; then
|
|
688
|
+
if tty_usable; then
|
|
689
|
+
{
|
|
690
|
+
printf '\n%sInstall rubino globally or for this project?%s\n' "$BOLD" "$RESET"
|
|
691
|
+
printf ' %sglobal%s %s(user-wide → ~/.config/mise/config.toml)%s\n' "$BOLD" "$RESET" "$DIM" "$RESET"
|
|
692
|
+
printf ' %slocal%s %s(this directory only → ./mise.toml)%s\n' "$BOLD" "$RESET" "$DIM" "$RESET"
|
|
693
|
+
printf 'Choose %s[global/local]%s (default global): ' "$BOLD" "$RESET"
|
|
694
|
+
} >/dev/tty
|
|
695
|
+
local ans=""
|
|
696
|
+
read -r ans </dev/tty || ans=""
|
|
697
|
+
case "$ans" in
|
|
698
|
+
local|l|2) scope="local" ;;
|
|
699
|
+
""|global|g|1) scope="global" ;;
|
|
700
|
+
*) scope="global" ;;
|
|
701
|
+
esac
|
|
702
|
+
else
|
|
703
|
+
scope="global"
|
|
704
|
+
fi
|
|
705
|
+
fi
|
|
706
|
+
|
|
707
|
+
info "Detected ${OS} ${ARCH}. Installing rubino via mise (${scope} scope)."
|
|
708
|
+
|
|
709
|
+
# Resolve the latest PUBLISHED gem version and pin it explicitly. By default
|
|
710
|
+
# mise applies `minimum_release_age`, which hides a freshly published release
|
|
711
|
+
# (you'd see "... hidden by minimum_release_age" and get a stale version). We
|
|
712
|
+
# both pin the exact version AND disable the release-age gate for this install
|
|
713
|
+
# so the just-published gem is taken. (#258)
|
|
714
|
+
local gem_tool="${GEM_TOOL}" want_ver
|
|
715
|
+
want_ver="$(curl -fsSL "https://rubygems.org/api/v1/gems/${GEM_NAME}.json" 2>/dev/null \
|
|
716
|
+
| grep -oE '"version":"[0-9]+\.[0-9]+\.[0-9]+[^"]*"' | head -n1 \
|
|
717
|
+
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+[^"]*')"
|
|
718
|
+
if [ -n "$want_ver" ]; then
|
|
719
|
+
gem_tool="${GEM_TOOL}@${want_ver}"
|
|
720
|
+
info "Latest published ${GEM_NAME} is ${want_ver}; pinning ${gem_tool}."
|
|
721
|
+
else
|
|
722
|
+
warn "Could not resolve the latest ${GEM_NAME} version from RubyGems; letting mise pick."
|
|
723
|
+
fi
|
|
724
|
+
# MISE_MINIMUM_RELEASE_AGE=0 ensures a just-published version isn't filtered.
|
|
725
|
+
export MISE_MINIMUM_RELEASE_AGE=0
|
|
726
|
+
|
|
727
|
+
case "$scope" in
|
|
728
|
+
global)
|
|
729
|
+
info "Installing ${gem_tool} globally (mise use -g)..."
|
|
730
|
+
mise use -g "${gem_tool}" || die "mise use -g ${gem_tool} failed (native build needs a C toolchain — see prerequisites above)."
|
|
731
|
+
;;
|
|
732
|
+
local)
|
|
733
|
+
info "Installing ${gem_tool} for this directory (mise use)..."
|
|
734
|
+
mise use "${gem_tool}" || die "mise use ${gem_tool} failed (native build needs a C toolchain — see prerequisites above)."
|
|
735
|
+
;;
|
|
736
|
+
esac
|
|
737
|
+
|
|
738
|
+
# Verify.
|
|
739
|
+
local rubino_path ver
|
|
740
|
+
rubino_path="$(mise which "${BIN_NAME}" 2>/dev/null || true)"
|
|
741
|
+
[ -n "$rubino_path" ] || die "mise installed ${GEM_TOOL} but 'mise which ${BIN_NAME}' resolved nothing."
|
|
742
|
+
|
|
743
|
+
ver="$(mise exec -- "${BIN_NAME}" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || true)"
|
|
744
|
+
[ -n "$ver" ] || die "installed ${GEM_TOOL} but '${BIN_NAME} --version' did not report a version."
|
|
745
|
+
|
|
746
|
+
printf '\n'
|
|
747
|
+
ok "rubino v${ver} installed via mise (${scope} scope)."
|
|
748
|
+
ok "executable: ${rubino_path}"
|
|
749
|
+
printf '\n'
|
|
750
|
+
|
|
751
|
+
# Activation: rubino resolves through mise's shims/activation. If mise isn't
|
|
752
|
+
# activated in the user's shell, `rubino` won't be found in a plain shell even
|
|
753
|
+
# though it's installed. Persist the activation line to the user's rc so a
|
|
754
|
+
# fresh login shell works — printing a hint alone left fresh shells broken (#257).
|
|
755
|
+
local shell_name act_sh act_line
|
|
756
|
+
shell_name="$(basename "${SHELL:-bash}")"
|
|
757
|
+
case "$shell_name" in
|
|
758
|
+
zsh) act_sh="zsh" ;;
|
|
759
|
+
bash) act_sh="bash" ;;
|
|
760
|
+
*) act_sh="$shell_name" ;;
|
|
761
|
+
esac
|
|
762
|
+
# The activation snippet differs by shell: POSIX shells eval the command
|
|
763
|
+
# substitution; fish pipes it to `source` (fish has no `eval "$(...)"`). Using
|
|
764
|
+
# the POSIX form in config.fish would error and leave fish broken (INST-R3-1).
|
|
765
|
+
if [ "$shell_name" = "fish" ]; then
|
|
766
|
+
act_line="$mise_bin activate fish | source"
|
|
767
|
+
else
|
|
768
|
+
act_line="eval \"\$($mise_bin activate ${act_sh:-bash})\""
|
|
769
|
+
fi
|
|
770
|
+
|
|
771
|
+
if command -v "${BIN_NAME}" >/dev/null 2>&1; then
|
|
772
|
+
ok "${BIN_NAME} is already on your PATH (mise is activated)."
|
|
773
|
+
elif persist_to_rc "$act_line"; then
|
|
774
|
+
ok "Added mise activation to ${PERSISTED_RC} (open a new shell to pick it up)."
|
|
775
|
+
printf 'Until then you can run it with:\n'
|
|
776
|
+
printf '\n %smise exec -- %s%s\n\n' "$DIM" "${BIN_NAME}" "$RESET"
|
|
777
|
+
else
|
|
778
|
+
# Opt-out or couldn't write: keep the manual hint.
|
|
779
|
+
printf '%sActivate mise in your shell%s so %s%s%s is on your PATH. Add to %s:\n' \
|
|
780
|
+
"$BOLD" "$RESET" "$BOLD" "${BIN_NAME}" "$RESET" "$(detect_shell_rc)"
|
|
781
|
+
printf '\n %s%s%s\n\n' "$DIM" "$act_line" "$RESET"
|
|
782
|
+
printf 'Then open a new shell. Until then you can run it with:\n'
|
|
783
|
+
printf '\n %smise exec -- %s%s\n\n' "$DIM" "${BIN_NAME}" "$RESET"
|
|
784
|
+
fi
|
|
785
|
+
|
|
786
|
+
# Post-install gate: fail loudly if a fresh shell still can't find rubino.
|
|
787
|
+
verify_fresh_shell "$act_line"
|
|
788
|
+
|
|
789
|
+
printf '%sNext step:%s\n\n' "$BOLD" "$RESET"
|
|
790
|
+
printf ' %s%s setup%s %s# guided first-run: pick a provider, paste a key%s\n\n' "$GREEN" "${BIN_NAME}" "$RESET" "$DIM" "$RESET"
|
|
791
|
+
printf 'Run: %s%s setup%s\n' "$BOLD" "${BIN_NAME}" "$RESET"
|
|
792
|
+
|
|
793
|
+
exit 0
|
|
794
|
+
}
|
|
795
|
+
|
|
193
796
|
info "Detected ${OS} ${ARCH}. Installing rubino via ${METHOD}."
|
|
194
797
|
|
|
195
798
|
case "$METHOD" in
|
|
196
799
|
rv) setup_ruby_rv ;;
|
|
197
800
|
brew) setup_ruby_brew ;;
|
|
801
|
+
mise) setup_mise ;; # runs the full mise install and exits.
|
|
198
802
|
*) die "internal: unknown method '${METHOD}'." ;;
|
|
199
803
|
esac
|
|
200
804
|
|
|
201
|
-
# Where gem-installed executables land for the chosen Ruby.
|
|
202
|
-
#
|
|
203
|
-
|
|
204
|
-
|
|
805
|
+
# Where gem-installed executables land for the chosen Ruby. Ask RubyGems
|
|
806
|
+
# directly (Gem.bindir); fall back to the ruby bin dir. The dir may not exist
|
|
807
|
+
# yet on a fresh machine — `gem install` creates it — so don't require it.
|
|
808
|
+
GEM_BIN_DIR="$(rubyx ruby -e 'print Gem.bindir' 2>/dev/null)" || GEM_BIN_DIR=""
|
|
809
|
+
[ -n "${GEM_BIN_DIR:-}" ] || GEM_BIN_DIR="$RUBY_BIN_DIR"
|
|
205
810
|
|
|
206
|
-
# --- install the rubino gem
|
|
811
|
+
# --- install the rubino gem (brew / rv methods) -----------------------------
|
|
207
812
|
|
|
208
813
|
gem_bin_present() { [ -x "${GEM_BIN_DIR}/${BIN_NAME}" ]; }
|
|
209
814
|
|
|
815
|
+
# Set by install_published() when the gem install ran but failed for a reason
|
|
816
|
+
# OTHER than "not published / no CLI" — i.e. a real error (network, native build,
|
|
817
|
+
# permissions) we must surface instead of the misleading git-fallback message.
|
|
818
|
+
GEM_INSTALL_LOG=""
|
|
819
|
+
|
|
210
820
|
install_published() {
|
|
211
821
|
info "Trying published gem: gem install ${GEM_NAME}..."
|
|
212
|
-
|
|
822
|
+
# Capture output instead of discarding it: on a genuine failure we want to show
|
|
823
|
+
# the real cause, not hide it behind ">/dev/null" and a misleading message (#242).
|
|
824
|
+
local out rc
|
|
825
|
+
out="$(rubyx gem install "${GEM_NAME}" 2>&1)"; rc=$?
|
|
826
|
+
if [ "$rc" -eq 0 ]; then
|
|
213
827
|
if gem_bin_present; then
|
|
214
828
|
ok "Installed ${GEM_NAME} from RubyGems."
|
|
215
829
|
return 0
|
|
216
830
|
fi
|
|
831
|
+
# Installed cleanly but ships no CLI → legitimately fall through to the git
|
|
832
|
+
# build (this is the real "not the CLI gem yet" case).
|
|
217
833
|
warn "A '${GEM_NAME}' gem was installed but it doesn't provide the '${BIN_NAME}' CLI; building from source instead."
|
|
218
834
|
rubyx gem uninstall "${GEM_NAME}" -aIx >/dev/null 2>&1 || true
|
|
835
|
+
return 1
|
|
219
836
|
fi
|
|
220
|
-
|
|
837
|
+
|
|
838
|
+
# gem install actually errored. If RubyGems says the gem can't be found, the
|
|
839
|
+
# CLI simply isn't published yet → the git build is the right fallback (quietly).
|
|
840
|
+
if printf '%s' "$out" | grep -qiE "could not find a valid gem|Unable to download|find .*${GEM_NAME}.* in any"; then
|
|
841
|
+
return 1
|
|
842
|
+
fi
|
|
843
|
+
|
|
844
|
+
# Any other failure (native build, network, permissions): stash it so the
|
|
845
|
+
# caller surfaces the real error rather than "isn't on RubyGems yet".
|
|
846
|
+
GEM_INSTALL_LOG="$out"
|
|
847
|
+
return 2
|
|
221
848
|
}
|
|
222
849
|
|
|
223
850
|
install_from_git() {
|
|
@@ -226,19 +853,30 @@ install_from_git() {
|
|
|
226
853
|
local work
|
|
227
854
|
work="$(mktemp -d)"
|
|
228
855
|
trap 'rm -rf "$work"' RETURN
|
|
229
|
-
|
|
230
|
-
|
|
856
|
+
# Run a build step, capturing output; on failure surface the real error (#242)
|
|
857
|
+
# instead of a bare "X failed" with the cause swallowed by >/dev/null.
|
|
858
|
+
run_step() {
|
|
859
|
+
local label="$1"; shift
|
|
860
|
+
local out rc
|
|
861
|
+
out="$("$@" 2>&1)"; rc=$?
|
|
862
|
+
if [ "$rc" -ne 0 ]; then
|
|
863
|
+
warn "${label} failed. The actual error was:"
|
|
864
|
+
printf '%s\n' "$out" >&2
|
|
865
|
+
die "${label} failed (real error shown above)."
|
|
866
|
+
fi
|
|
867
|
+
}
|
|
868
|
+
run_step "git clone of ${REPO_URL}" git clone --depth 1 "$REPO_URL" "$work/${REPO_NAME}"
|
|
231
869
|
(
|
|
232
870
|
cd "$work/${REPO_NAME}"
|
|
233
871
|
info "Resolving dependencies (bundle install)..."
|
|
234
|
-
|
|
872
|
+
run_step "bundle install" rubyx bundle install
|
|
235
873
|
info "Building the gem (rake build)..."
|
|
236
|
-
|
|
874
|
+
run_step "rake build" rubyx rake build
|
|
237
875
|
local pkg
|
|
238
876
|
pkg="$(ls -1 pkg/${GEM_NAME}-*.gem 2>/dev/null | head -n1)"
|
|
239
877
|
[ -n "$pkg" ] || die "rake build produced no gem in pkg/."
|
|
240
878
|
info "Installing ${pkg}..."
|
|
241
|
-
|
|
879
|
+
run_step "gem install of the built package" rubyx gem install "$pkg"
|
|
242
880
|
)
|
|
243
881
|
gem_bin_present || die "built and installed ${GEM_NAME} but the '${BIN_NAME}' executable is missing."
|
|
244
882
|
ok "Installed ${GEM_NAME} from source."
|
|
@@ -247,8 +885,20 @@ install_from_git() {
|
|
|
247
885
|
if gem_bin_present; then
|
|
248
886
|
CURRENT_VER="$("${GEM_BIN_DIR}/${BIN_NAME}" version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || true)"
|
|
249
887
|
ok "${BIN_NAME} ${CURRENT_VER:+v$CURRENT_VER }is already installed (re-run safe)."
|
|
250
|
-
|
|
251
|
-
|
|
888
|
+
else
|
|
889
|
+
# `|| gem_rc=$?` keeps the non-zero returns (1=not-published, 2=real-error)
|
|
890
|
+
# from tripping `set -e`; default 0 on success.
|
|
891
|
+
gem_rc=0
|
|
892
|
+
install_published || gem_rc=$?
|
|
893
|
+
case "$gem_rc" in
|
|
894
|
+
0) : ;; # installed the published gem
|
|
895
|
+
1) install_from_git ;; # not published / no CLI yet → build from source
|
|
896
|
+
*) # real error: surface the actual gem output (#242)
|
|
897
|
+
warn "gem install ${GEM_NAME} failed. The actual error was:"
|
|
898
|
+
printf '%s\n' "${GEM_INSTALL_LOG}" >&2
|
|
899
|
+
die "gem install ${GEM_NAME} failed (real error shown above)."
|
|
900
|
+
;;
|
|
901
|
+
esac
|
|
252
902
|
fi
|
|
253
903
|
|
|
254
904
|
# --- PATH guidance + success ------------------------------------------------
|
|
@@ -263,12 +913,24 @@ else
|
|
|
263
913
|
PATH_OK=0
|
|
264
914
|
fi
|
|
265
915
|
|
|
916
|
+
# Shell-correct PATH-persist line (fish needs `fish_add_path`, not POSIX export).
|
|
917
|
+
PATH_LINE="$(path_persist_line "${GEM_BIN_DIR}")"
|
|
918
|
+
|
|
266
919
|
if [ "$PATH_OK" -ne 1 ]; then
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
920
|
+
# Persist the PATH line to the user's rc so a fresh login shell finds rubino —
|
|
921
|
+
# printing the hint alone left fresh shells broken (#257).
|
|
922
|
+
if persist_to_rc "$PATH_LINE"; then
|
|
923
|
+
ok "Added ${BIN_NAME} to your PATH in ${PERSISTED_RC} (open a new shell to pick it up)."
|
|
924
|
+
else
|
|
925
|
+
printf '%sAdd this line to your shell profile%s (%s):\n' "$BOLD" "$RESET" "$(detect_shell_rc)"
|
|
926
|
+
printf '\n %s%s%s\n\n' "$DIM" "$PATH_LINE" "$RESET"
|
|
927
|
+
printf 'Then open a new shell (or run the export above) so %s%s%s is on your PATH.\n\n' "$BOLD" "${BIN_NAME}" "$RESET"
|
|
928
|
+
fi
|
|
270
929
|
fi
|
|
271
930
|
|
|
931
|
+
# Post-install gate: confirm a fresh login shell finds rubino, or fail loudly.
|
|
932
|
+
verify_fresh_shell "$PATH_LINE"
|
|
933
|
+
|
|
272
934
|
printf '%sNext step:%s\n\n' "$BOLD" "$RESET"
|
|
273
935
|
printf ' %s%s setup%s %s# guided first-run: pick a provider, paste a key%s\n\n' "$GREEN" "${BIN_NAME}" "$RESET" "$DIM" "$RESET"
|
|
274
936
|
|