rtk: an agent left with the hook of an rtk that isn't installed (never was, or removed since) is shown with RTK missing — its shell commands fail — and can still be switched off: magpie takes out just what rtk's installer put in (the hook in settings.json / hooks.json, RTK.md and the @RTK.md line, Gemini's hook script, Copilot's instructions block, the OpenCode / Pi / OMP extension, Hermes' plugin) itself, needing no rtk, the files kept aside first; rtk's own uninstaller is no longer used, which deleted Gemini's GEMINI.md whole and took Claude Code, OpenCode and Cursor away together. Gemini is given rtk hook-only, so the user's GEMINI.md isn't written over. Install RTK on the page (and magpie library rtk install) installs rtk with Homebrew, winget on Windows, or rtk's own script into ~/.local/bin, only when clicked
This commit is contained in:
7 files changed
+555
-99
No files matched your search
@@ -821,7 +821,12 @@ const I18N = {
|
||||
"RTK rewrites the shell commands an agent runs — git status, cargo test, ls… — to print only what the model needs, so they cost fewer tokens. Switch it on for an agent and magpie runs RTK's own installer for it.": "RTK 会改写 Agent 执行的命令行命令(git status、cargo test、ls……),只输出模型需要的部分,从而省 token。为某个 Agent 打开后,magpie 会替它运行 RTK 自带的安装程序。",
|
||||
"{saved} tokens saved over {n} commands — {pct}% on average": "{n} 条命令共节省 {saved} token,平均省 {pct}%",
|
||||
"Nothing saved yet: the agents' commands go through RTK once it's switched on and the agent is restarted.": "还没有节省记录:打开后重启 Agent,它的命令就会经过 RTK。",
|
||||
"RTK isn't installed. Install it (brew install rtk, or see its site), then come back here.": "尚未安装 RTK。先安装(brew install rtk,或见其官网),再回到这里。",
|
||||
"RTK isn't installed. magpie can install it for you, or get it from its site.": "尚未安装 RTK。magpie 可以帮你安装,也可以去官网获取。",
|
||||
"Installing RTK… this can take a minute.": "正在安装 RTK…可能需要一分钟。",
|
||||
"Install RTK": "安装 RTK",
|
||||
"RTK is installed — switch it on for your agents below": "RTK 已安装 — 在下方为你的 agent 打开",
|
||||
"RTK missing": "RTK 缺失",
|
||||
"{agent}'s hook calls rtk, which isn't installed, so its shell commands fail. Install RTK, or switch this off.": "{agent} 的 hook 会调用 rtk,但 rtk 没有安装,它的 shell 命令会失败。请安装 RTK,或关掉这个开关。",
|
||||
"Get RTK": "获取 RTK",
|
||||
"Check again": "重新检测",
|
||||
"restart an agent after switching it": "切换后需重启对应 Agent",
|
||||
|
||||
@@ -275,3 +275,4 @@ body.window .lib-head { top: -2px; margin: -2px -16px 0; padding: 8px 16px 4px;
|
||||
.mk-sheet .bar .mk-id { margin-left: 2px; }
|
||||
.lib-rtk-gain { margin: 6px 12px 10px; font-size: 12px; line-height: 1.5; color: var(--muted); }
|
||||
.lib-card > .lib-acts { padding: 0 12px 12px; }
|
||||
.lib-rtk-cmd { margin: -4px 12px 10px; font: 11.5px var(--code); color: var(--muted); user-select: text; }
|
||||
@@ -276,7 +276,8 @@
|
||||
|
||||
// RTK (rtk-ai.app) cuts down what the shell commands an agent runs print,
|
||||
// so their output costs fewer tokens. magpie runs rtk's own installer for
|
||||
// each agent switched on, and reads the agents' files for which have it.
|
||||
// each agent switched on, takes out what it wrote for one switched off
|
||||
// (with or without rtk), and reads the agents' files for which have it.
|
||||
let rtk = null; // /api/library/rtk
|
||||
const rtkBusy = new Set(); // agents being switched
|
||||
let rtkLoading = false;
|
||||
@@ -310,10 +311,17 @@
|
||||
: t("Nothing saved yet: the agents' commands go through RTK once it's switched on and the agent is restarted.")));
|
||||
} else {
|
||||
card.append(ttl);
|
||||
const p = el("p", "lib-rtk-gain", t("RTK isn't installed. Install it (brew install rtk, or see its site), then come back here."));
|
||||
card.append(p);
|
||||
card.append(el("p", "lib-rtk-gain", rtkInstalling
|
||||
? t("Installing RTK… this can take a minute.")
|
||||
: t("RTK isn't installed. magpie can install it for you, or get it from its site.")));
|
||||
if (rtk.install) card.append(el("p", "lib-rtk-cmd", rtk.install));
|
||||
const acts = el("div", "lib-acts");
|
||||
acts.append(button(t("Get RTK"), "action", () => browse(rtk.url)), button(t("Check again"), "", () => { rtk = null; render(); }));
|
||||
if (rtk.install) {
|
||||
const ib = button(rtkInstalling ? t("Installing…") : t("Install RTK"), "action", installRTK);
|
||||
ib.disabled = rtkInstalling;
|
||||
acts.append(ib);
|
||||
}
|
||||
acts.append(button(t("Get RTK"), rtk.install ? "" : "action", () => browse(rtk.url)), button(t("Check again"), "", () => { rtk = null; render(); }));
|
||||
card.append(acts);
|
||||
}
|
||||
body.append(card);
|
||||
@@ -327,8 +335,10 @@
|
||||
const who = el("div", "who");
|
||||
who.append(el("div", "name", a.name));
|
||||
row.append(icon(a.icon), who, el("span", "grow"));
|
||||
// its hook calls an rtk that isn't there: switching it off still works
|
||||
if (a.on && !rtk.path) row.append(tag(t("RTK missing"), "warn", t("{agent}'s hook calls rtk, which isn't installed, so its shell commands fail. Install RTK, or switch this off.", { agent: a.name })));
|
||||
const sw = toggle(a.on, t("{agent} runs its commands through RTK", { agent: a.name }), (on) => setRTK(a, on));
|
||||
if (!rtk.path || rtkBusy.has(a.id)) sw.disabled = true;
|
||||
if ((!rtk.path && !a.on) || rtkBusy.has(a.id) || rtkInstalling) sw.disabled = true;
|
||||
row.append(sw);
|
||||
list.append(row);
|
||||
}
|
||||
@@ -348,6 +358,19 @@
|
||||
rtkBusy.delete(a.id);
|
||||
render();
|
||||
}
|
||||
let rtkInstalling = false;
|
||||
async function installRTK() {
|
||||
rtkInstalling = true;
|
||||
render();
|
||||
try {
|
||||
rtk = await api("library/rtk/install", {});
|
||||
status(t("RTK is installed — switch it on for your agents below"), "ok", 6000);
|
||||
} catch (e) {
|
||||
status(e.message, "err", 10000);
|
||||
}
|
||||
rtkInstalling = false;
|
||||
render();
|
||||
}
|
||||
function tokens(n) {
|
||||
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
|
||||
if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
|
||||
|
||||
+10
-1
@@ -131,7 +131,8 @@ func libraryRoutes(mux *http.ServeMux, w Windows) {
|
||||
}
|
||||
writeJSON(rw, p)
|
||||
})
|
||||
// RTK: which agents have its hook, and switching one on or off
|
||||
// RTK: which agents have its hook, switching one on or off, and
|
||||
// installing rtk when the page is asked to
|
||||
mux.HandleFunc("GET /api/library/rtk", func(rw http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(rw, library.ReadRTK())
|
||||
})
|
||||
@@ -151,6 +152,14 @@ func libraryRoutes(mux *http.ServeMux, w Windows) {
|
||||
}
|
||||
writeJSON(rw, v)
|
||||
})
|
||||
mux.HandleFunc("POST /api/library/rtk/install", func(rw http.ResponseWriter, r *http.Request) {
|
||||
v, err := library.InstallRTK()
|
||||
if err != nil {
|
||||
fail(rw, err)
|
||||
return
|
||||
}
|
||||
writeJSON(rw, v)
|
||||
})
|
||||
mux.HandleFunc("POST /api/library/reveal", func(rw http.ResponseWriter, r *http.Request) {
|
||||
var in struct{ Path string }
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil || in.Path == "" {
|
||||
|
||||
+356
-82
@@ -4,25 +4,30 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/agent"
|
||||
"github.com/yetone/magpie/internal/edit"
|
||||
"github.com/yetone/magpie/internal/proc"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// RTK (rtk-ai.app) is a CLI that the shell commands an agent runs go
|
||||
// through — git status, cargo test, ls — cut down to what the model needs,
|
||||
// so their output costs fewer tokens. Each agent gets it through a hook its
|
||||
// own installer writes (rtk init -g …): magpie runs that installer for the
|
||||
// agents switched on, and reads each one's files to say which have it, since
|
||||
// rtk init --show knows only of Claude Code, OpenCode and Cursor.
|
||||
// agents switched on, takes out what it wrote for those switched off, and
|
||||
// reads each one's files to say which have it, since rtk init --show knows
|
||||
// only of Claude Code, OpenCode and Cursor.
|
||||
|
||||
// RTKURL is where rtk is installed from.
|
||||
const RTKURL = "https://www.rtk-ai.app"
|
||||
@@ -34,9 +39,15 @@ type rtkSpec struct {
|
||||
patch bool
|
||||
// has says whether the agent has rtk's hook, by its files
|
||||
has func(a *agent.Agent) bool
|
||||
// touches are the files the installer edits (not those it only adds),
|
||||
// kept aside by magpie first
|
||||
touches func(a *agent.Agent) []string
|
||||
// files are those the installer writes and remove takes back, kept
|
||||
// aside by magpie first
|
||||
files func(a *agent.Agent) []string
|
||||
// remove takes out what the installer put in, and only that. It is
|
||||
// magpie's own, not rtk init --uninstall: it works with rtk gone (its
|
||||
// hooks left behind then fail every command), and rtk's uninstaller
|
||||
// deletes Gemini's GEMINI.md whole and takes Claude Code, OpenCode and
|
||||
// Cursor away together.
|
||||
remove func(a *agent.Agent) error
|
||||
// dir is the agent's folder, made first: rtk writes nothing for an
|
||||
// agent whose folder isn't there yet
|
||||
dir func(a *agent.Agent) string
|
||||
@@ -55,82 +66,288 @@ func exists(path string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// rtkRef is the line rtk adds to an instructions file to pull its own in:
|
||||
// @RTK.md, or @ and RTK.md's whole path.
|
||||
func rtkRef(line string) bool {
|
||||
t := strings.TrimSpace(line)
|
||||
return strings.HasPrefix(t, "@") && (t == "@RTK.md" || strings.HasSuffix(t, "/RTK.md") || strings.HasSuffix(t, `\RTK.md`))
|
||||
}
|
||||
|
||||
var rtkSpecs = map[string]rtkSpec{
|
||||
"claude": {
|
||||
patch: true,
|
||||
has: func(*agent.Agent) bool {
|
||||
return contains(filepath.Join(claudeDir(), "settings.json"), "rtk hook claude")
|
||||
},
|
||||
touches: func(*agent.Agent) []string {
|
||||
return []string{filepath.Join(claudeDir(), "settings.json"), filepath.Join(claudeDir(), "CLAUDE.md")}
|
||||
files: func(*agent.Agent) []string {
|
||||
d := claudeDir()
|
||||
return []string{filepath.Join(d, "settings.json"), filepath.Join(d, "CLAUDE.md"), filepath.Join(d, "RTK.md")}
|
||||
},
|
||||
remove: func(*agent.Agent) error {
|
||||
d := claudeDir()
|
||||
return errors.Join(
|
||||
dropHook(filepath.Join(d, "settings.json"), "hooks.PreToolUse", "rtk hook claude"),
|
||||
dropLines(filepath.Join(d, "CLAUDE.md"), rtkRef),
|
||||
rm(filepath.Join(d, "RTK.md")))
|
||||
},
|
||||
dir: func(*agent.Agent) string { return claudeDir() },
|
||||
},
|
||||
"codex": {
|
||||
flags: []string{"--codex"},
|
||||
has: func(*agent.Agent) bool { return contains(filepath.Join(codexDir(), "hooks.json"), "rtk hook codex") },
|
||||
touches: func(*agent.Agent) []string {
|
||||
return []string{filepath.Join(codexDir(), "hooks.json"), filepath.Join(codexDir(), "AGENTS.md")}
|
||||
files: func(*agent.Agent) []string {
|
||||
d := codexDir()
|
||||
return []string{filepath.Join(d, "hooks.json"), filepath.Join(d, "AGENTS.md"), filepath.Join(d, "RTK.md")}
|
||||
},
|
||||
remove: func(*agent.Agent) error {
|
||||
d := codexDir()
|
||||
return errors.Join(
|
||||
dropHook(filepath.Join(d, "hooks.json"), "hooks.PreToolUse", "rtk hook codex"),
|
||||
dropLines(filepath.Join(d, "AGENTS.md"), rtkRef),
|
||||
rm(filepath.Join(d, "RTK.md")))
|
||||
},
|
||||
dir: func(*agent.Agent) string { return codexDir() },
|
||||
},
|
||||
"gemini": {
|
||||
flags: []string{"--gemini"}, patch: true,
|
||||
// --hook-only: without it the installer writes its own GEMINI.md
|
||||
// over the user's
|
||||
flags: []string{"--gemini", "--hook-only"}, patch: true,
|
||||
has: func(*agent.Agent) bool {
|
||||
return contains(filepath.Join(home(), ".gemini", "settings.json"), "rtk-hook-gemini")
|
||||
},
|
||||
touches: func(*agent.Agent) []string {
|
||||
return []string{filepath.Join(home(), ".gemini", "settings.json"), filepath.Join(home(), ".gemini", "GEMINI.md")}
|
||||
files: func(*agent.Agent) []string {
|
||||
d := filepath.Join(home(), ".gemini")
|
||||
return []string{filepath.Join(d, "settings.json"), filepath.Join(d, "hooks", "rtk-hook-gemini.sh"), filepath.Join(d, "hooks", ".rtk-hook.sha256")}
|
||||
},
|
||||
remove: func(*agent.Agent) error {
|
||||
d := filepath.Join(home(), ".gemini")
|
||||
err := errors.Join(
|
||||
dropHook(filepath.Join(d, "settings.json"), "hooks.BeforeTool", "rtk-hook-gemini"),
|
||||
rm(filepath.Join(d, "hooks", "rtk-hook-gemini.sh"), filepath.Join(d, "hooks", ".rtk-hook.sha256")))
|
||||
os.Remove(filepath.Join(d, "hooks")) // when nothing else is in it
|
||||
return err
|
||||
},
|
||||
dir: func(*agent.Agent) string { return filepath.Join(home(), ".gemini") },
|
||||
},
|
||||
"opencode": {
|
||||
flags: []string{"--opencode"}, patch: true, withClaude: true,
|
||||
has: func(a *agent.Agent) bool { return exists(filepath.Join(filepath.Dir(a.Path), "plugins", "rtk.ts")) },
|
||||
touches: func(*agent.Agent) []string { return nil },
|
||||
dir: func(a *agent.Agent) string { return filepath.Dir(a.Path) },
|
||||
has: func(a *agent.Agent) bool { return exists(opencodePlugin(a)) },
|
||||
files: func(a *agent.Agent) []string { return []string{opencodePlugin(a)} },
|
||||
remove: func(a *agent.Agent) error { return rm(opencodePlugin(a)) },
|
||||
dir: func(a *agent.Agent) string { return filepath.Dir(a.Path) },
|
||||
},
|
||||
"cursor": {
|
||||
flags: []string{"--agent", "cursor"}, patch: true, withClaude: true,
|
||||
has: func(*agent.Agent) bool {
|
||||
return contains(filepath.Join(home(), ".cursor", "hooks.json"), "rtk hook cursor")
|
||||
},
|
||||
touches: func(*agent.Agent) []string { return []string{filepath.Join(home(), ".cursor", "hooks.json")} },
|
||||
dir: func(*agent.Agent) string { return filepath.Join(home(), ".cursor") },
|
||||
files: func(*agent.Agent) []string { return []string{filepath.Join(home(), ".cursor", "hooks.json")} },
|
||||
remove: func(*agent.Agent) error {
|
||||
return dropHook(filepath.Join(home(), ".cursor", "hooks.json"), "hooks.preToolUse", "rtk hook cursor")
|
||||
},
|
||||
dir: func(*agent.Agent) string { return filepath.Join(home(), ".cursor") },
|
||||
},
|
||||
"copilot": {
|
||||
flags: []string{"--copilot"}, patch: true,
|
||||
has: func(*agent.Agent) bool { return exists(filepath.Join(home(), ".copilot", "hooks", "rtk-rewrite.json")) },
|
||||
touches: func(*agent.Agent) []string {
|
||||
return []string{filepath.Join(home(), ".copilot", "copilot-instructions.md")}
|
||||
files: func(*agent.Agent) []string {
|
||||
d := filepath.Join(home(), ".copilot")
|
||||
return []string{filepath.Join(d, "hooks", "rtk-rewrite.json"), filepath.Join(d, "copilot-instructions.md")}
|
||||
},
|
||||
remove: func(*agent.Agent) error {
|
||||
d := filepath.Join(home(), ".copilot")
|
||||
return errors.Join(
|
||||
rm(filepath.Join(d, "hooks", "rtk-rewrite.json")),
|
||||
dropBlock(filepath.Join(d, "copilot-instructions.md"), "<!-- rtk-instructions", "<!-- /rtk-instructions -->"))
|
||||
},
|
||||
dir: func(*agent.Agent) string { return filepath.Join(home(), ".copilot") },
|
||||
},
|
||||
"pi": {
|
||||
flags: []string{"--agent", "pi"}, patch: true,
|
||||
has: func(*agent.Agent) bool { return exists(filepath.Join(home(), ".pi", "agent", "extensions", "rtk.ts")) },
|
||||
touches: func(*agent.Agent) []string { return nil },
|
||||
dir: func(*agent.Agent) string { return filepath.Join(home(), ".pi", "agent") },
|
||||
},
|
||||
"omp": {
|
||||
flags: []string{"--agent", "omp"}, patch: true,
|
||||
has: func(*agent.Agent) bool { return exists(filepath.Join(home(), ".omp", "agent", "extensions", "rtk.ts")) },
|
||||
touches: func(*agent.Agent) []string { return nil },
|
||||
dir: func(*agent.Agent) string { return filepath.Join(home(), ".omp", "agent") },
|
||||
},
|
||||
"pi": extensionSpec("pi"),
|
||||
"omp": extensionSpec("omp"),
|
||||
"hermes": {
|
||||
flags: []string{"--agent", "hermes"}, patch: true,
|
||||
has: func(*agent.Agent) bool {
|
||||
return exists(filepath.Join(home(), ".hermes", "plugins", "rtk-rewrite", "plugin.yaml"))
|
||||
has: func(*agent.Agent) bool { return exists(filepath.Join(hermesPlugin(), "plugin.yaml")) },
|
||||
files: func(*agent.Agent) []string {
|
||||
return []string{filepath.Join(home(), ".hermes", "config.yaml"), filepath.Join(hermesPlugin(), "plugin.yaml"), filepath.Join(hermesPlugin(), "__init__.py")}
|
||||
},
|
||||
touches: func(*agent.Agent) []string { return []string{filepath.Join(home(), ".hermes", "config.yaml")} },
|
||||
dir: func(*agent.Agent) string { return filepath.Join(home(), ".hermes") },
|
||||
remove: func(*agent.Agent) error {
|
||||
return errors.Join(os.RemoveAll(hermesPlugin()), dropHermesPlugin(filepath.Join(home(), ".hermes", "config.yaml")))
|
||||
},
|
||||
dir: func(*agent.Agent) string { return filepath.Join(home(), ".hermes") },
|
||||
},
|
||||
}
|
||||
|
||||
// rtkBundle: rtk's uninstaller for Claude Code or OpenCode takes it out of
|
||||
// all three of these at once, so the others are put back after.
|
||||
var rtkBundle = []string{"claude", "opencode", "cursor"}
|
||||
func opencodePlugin(a *agent.Agent) string {
|
||||
return filepath.Join(filepath.Dir(a.Path), "plugins", "rtk.ts")
|
||||
}
|
||||
|
||||
func hermesPlugin() string { return filepath.Join(home(), ".hermes", "plugins", "rtk-rewrite") }
|
||||
|
||||
// extensionSpec is pi's and omp's: an extension of rtk's own, and nothing else.
|
||||
func extensionSpec(id string) rtkSpec {
|
||||
ext := func() string { return filepath.Join(home(), "."+id, "agent", "extensions", "rtk.ts") }
|
||||
return rtkSpec{
|
||||
flags: []string{"--agent", id}, patch: true,
|
||||
has: func(*agent.Agent) bool { return exists(ext()) },
|
||||
files: func(*agent.Agent) []string { return []string{ext()} },
|
||||
remove: func(*agent.Agent) error { return rm(ext()) },
|
||||
dir: func(*agent.Agent) string { return filepath.Join(home(), "."+id, "agent") },
|
||||
}
|
||||
}
|
||||
|
||||
func rm(paths ...string) error {
|
||||
var errs []error
|
||||
for _, p := range paths {
|
||||
if err := os.Remove(p); err != nil && !os.IsNotExist(err) {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// dropHook takes the hooks whose command has mark out of the array at key:
|
||||
// Claude Code's, Codex's and Gemini's are groups, each with its own hooks,
|
||||
// a group left with none going too; Cursor's are the hooks themselves. An
|
||||
// array left empty goes, and hooks with it when that leaves it empty.
|
||||
func dropHook(path, key, mark string) error {
|
||||
raw, ok := edit.GetJSON(path, key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var list []json.RawMessage
|
||||
if json.Unmarshal([]byte(raw), &list) != nil {
|
||||
return fmt.Errorf("%s: %s isn't a list", path, key)
|
||||
}
|
||||
var keep []json.RawMessage
|
||||
changed := false
|
||||
for _, e := range list {
|
||||
var h struct {
|
||||
Command string `json:"command"`
|
||||
Hooks []json.RawMessage `json:"hooks"`
|
||||
}
|
||||
json.Unmarshal(e, &h)
|
||||
if strings.Contains(h.Command, mark) {
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
if h.Hooks != nil && bytes.Contains(e, []byte(mark)) {
|
||||
var inner []json.RawMessage
|
||||
for _, x := range h.Hooks {
|
||||
var c struct {
|
||||
Command string `json:"command"`
|
||||
}
|
||||
json.Unmarshal(x, &c)
|
||||
if !strings.Contains(c.Command, mark) {
|
||||
inner = append(inner, x)
|
||||
}
|
||||
}
|
||||
if len(inner) < len(h.Hooks) {
|
||||
changed = true
|
||||
if len(inner) == 0 {
|
||||
continue
|
||||
}
|
||||
var g map[string]json.RawMessage
|
||||
json.Unmarshal(e, &g)
|
||||
g["hooks"], _ = json.Marshal(inner)
|
||||
e, _ = json.Marshal(g)
|
||||
}
|
||||
}
|
||||
keep = append(keep, e)
|
||||
}
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
if len(keep) > 0 {
|
||||
return edit.SetJSON(path, edit.KV{Path: key, Value: keep})
|
||||
}
|
||||
if err := edit.DelJSON(path, key); err != nil {
|
||||
return err
|
||||
}
|
||||
parent := key[:strings.LastIndex(key, ".")]
|
||||
if v, ok := edit.GetJSON(path, parent); ok && strings.TrimSpace(v) == "{}" {
|
||||
return edit.DelJSON(path, parent)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dropLines takes out the lines match says are rtk's, with the blank line
|
||||
// rtk put before one; a file left with nothing in it goes.
|
||||
func dropLines(path string, match func(string) bool) error {
|
||||
b, err := os.ReadFile(path)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ls := strings.Split(string(b), "\n")
|
||||
var out []string
|
||||
for i, l := range ls {
|
||||
if !match(l) {
|
||||
out = append(out, l)
|
||||
continue
|
||||
}
|
||||
next := ""
|
||||
if i+1 < len(ls) {
|
||||
next = ls[i+1]
|
||||
}
|
||||
if n := len(out); n > 0 && strings.TrimSpace(out[n-1]) == "" && strings.TrimSpace(next) == "" {
|
||||
out = out[:n-1]
|
||||
}
|
||||
}
|
||||
if len(out) == len(ls) {
|
||||
return nil
|
||||
}
|
||||
return writeOrRemove(path, strings.Join(out, "\n"))
|
||||
}
|
||||
|
||||
// dropBlock takes out the lines from the one with open to the one with
|
||||
// close, as dropLines does.
|
||||
func dropBlock(path, open, close string) error {
|
||||
in := false
|
||||
return dropLines(path, func(l string) bool {
|
||||
switch {
|
||||
case strings.Contains(l, open):
|
||||
in = true
|
||||
case in && strings.Contains(l, close):
|
||||
in = false
|
||||
return true
|
||||
}
|
||||
return in
|
||||
})
|
||||
}
|
||||
|
||||
func writeOrRemove(path, s string) error {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return os.Remove(path)
|
||||
}
|
||||
return edit.WriteAtomic(path, []byte(s))
|
||||
}
|
||||
|
||||
// dropHermesPlugin takes rtk-rewrite out of Hermes' plugins.enabled, and
|
||||
// the keys rtk added for it when that leaves them empty.
|
||||
func dropHermesPlugin(path string) error {
|
||||
if err := dropLines(path, func(l string) bool { return strings.TrimSpace(l) == "- rtk-rewrite" }); err != nil {
|
||||
return err
|
||||
}
|
||||
var c struct {
|
||||
Plugins map[string]any `yaml:"plugins"`
|
||||
}
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil || yaml.Unmarshal(b, &c) != nil || c.Plugins == nil {
|
||||
return nil
|
||||
}
|
||||
if l, ok := c.Plugins["enabled"]; ok && (l == nil || fmt.Sprint(l) == "[]") {
|
||||
if err := edit.DelYAML(path, "plugins.enabled"); err != nil {
|
||||
return err
|
||||
}
|
||||
delete(c.Plugins, "enabled")
|
||||
}
|
||||
if len(c.Plugins) == 0 {
|
||||
return edit.DelYAML(path, "plugins")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RTKAgent is one agent rtk can be given to, and whether it has it.
|
||||
type RTKAgent struct {
|
||||
@@ -155,6 +372,8 @@ type RTKView struct {
|
||||
Gain *RTKGain `json:"gain,omitempty"`
|
||||
Agents []RTKAgent `json:"agents"`
|
||||
URL string `json:"url"`
|
||||
// Install is the command Install RTK runs, shown before it is clicked
|
||||
Install string `json:"install,omitempty"`
|
||||
// Restart are the agents a change reached, to be restarted to see it
|
||||
Restart []string `json:"restart,omitempty"`
|
||||
Backup string `json:"backup,omitempty"`
|
||||
@@ -162,12 +381,23 @@ type RTKView struct {
|
||||
|
||||
var rtkMu sync.Mutex
|
||||
|
||||
// rtkPath is rtk's, "" when it isn't installed; one installed since magpie
|
||||
// started may not be on its PATH yet, so where the installers put it is
|
||||
// looked in too (Homebrew's are on it from the start: proc.UserPath).
|
||||
func rtkPath() string {
|
||||
p, err := exec.LookPath("rtk")
|
||||
if err != nil {
|
||||
return ""
|
||||
if p, err := exec.LookPath("rtk"); err == nil {
|
||||
return p
|
||||
}
|
||||
return p
|
||||
name, dirs := "rtk", []string{filepath.Join(home(), ".local", "bin"), filepath.Join(home(), ".cargo", "bin")}
|
||||
if runtime.GOOS == "windows" {
|
||||
name, dirs = "rtk.exe", []string{filepath.Join(os.Getenv("LOCALAPPDATA"), "Microsoft", "WinGet", "Links"), filepath.Join(home(), ".cargo", "bin")}
|
||||
}
|
||||
for _, d := range dirs {
|
||||
if p := filepath.Join(d, name); exists(p) {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func rtkRun(bin string, args ...string) (string, error) { return rtkRunEnv(bin, nil, args...) }
|
||||
@@ -215,6 +445,14 @@ func rtkAgents() []*agent.Agent {
|
||||
// have it.
|
||||
func ReadRTK() *RTKView {
|
||||
v := &RTKView{Agents: []RTKAgent{}, URL: RTKURL, Path: rtkPath()}
|
||||
if v.Path == "" {
|
||||
if c := rtkInstaller(); c != nil {
|
||||
v.Install = strings.Join(c, " ")
|
||||
if c[0] == "sh" {
|
||||
v.Install = c[2]
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, a := range rtkAgents() {
|
||||
v.Agents = append(v.Agents, RTKAgent{ID: a.ID, Name: a.Name, Icon: a.Icon, On: rtkSpecs[a.ID].has(a)})
|
||||
}
|
||||
@@ -241,45 +479,35 @@ func ReadRTK() *RTKView {
|
||||
return v
|
||||
}
|
||||
|
||||
// SetRTK gives rtk to an agent, or takes it away, with rtk's own installer.
|
||||
// SetRTK gives rtk to an agent with rtk's own installer, or takes it away
|
||||
// by taking out what that put in — which needs no rtk, so an agent left
|
||||
// with the hook of an rtk since removed can be put right.
|
||||
func SetRTK(id string, on bool) (*RTKView, error) {
|
||||
rtkMu.Lock()
|
||||
defer rtkMu.Unlock()
|
||||
bin := rtkPath()
|
||||
if bin == "" {
|
||||
return nil, fmt.Errorf("rtk isn't installed — get it from %s", RTKURL)
|
||||
}
|
||||
agents := map[string]*agent.Agent{}
|
||||
for _, a := range rtkAgents() {
|
||||
agents[a.ID] = a
|
||||
}
|
||||
a := agents[id]
|
||||
if a == nil {
|
||||
if _, ok := rtkSpecs[id]; !ok {
|
||||
return nil, fmt.Errorf("rtk has no hook for %s", id)
|
||||
var a *agent.Agent
|
||||
for _, x := range rtkAgents() {
|
||||
if x.ID == id {
|
||||
a = x
|
||||
}
|
||||
}
|
||||
sp, ok := rtkSpecs[id]
|
||||
switch {
|
||||
case !ok:
|
||||
return nil, fmt.Errorf("rtk has no hook for %s", id)
|
||||
case a == nil:
|
||||
return nil, fmt.Errorf("%s isn't installed", id)
|
||||
case sp.has(a) == on:
|
||||
return ReadRTK(), nil
|
||||
}
|
||||
sp := rtkSpecs[id]
|
||||
if sp.has(a) == on {
|
||||
v := ReadRTK()
|
||||
return v, nil
|
||||
}
|
||||
// what else the uninstaller takes, to put back
|
||||
var back []*agent.Agent
|
||||
if !on && (id == "claude" || id == "opencode") {
|
||||
for _, o := range rtkBundle {
|
||||
if b := agents[o]; b != nil && o != id && rtkSpecs[o].has(b) {
|
||||
back = append(back, b)
|
||||
}
|
||||
}
|
||||
bin := rtkPath()
|
||||
if on && bin == "" {
|
||||
return nil, fmt.Errorf("rtk isn't installed — install it first (%s)", RTKURL)
|
||||
}
|
||||
b := newBackups()
|
||||
for _, x := range append([]*agent.Agent{a}, back...) {
|
||||
for _, p := range rtkSpecs[x.ID].touches(x) {
|
||||
if err := b.keep(x.ID, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, p := range sp.files(a) {
|
||||
if err := b.keep(a.ID, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if on {
|
||||
@@ -287,23 +515,17 @@ func SetRTK(id string, on bool) (*RTKView, error) {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if _, err := rtkRun(bin, append(append([]string{"init", "-g"}, sp.flags...), "--uninstall")...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, x := range back {
|
||||
if err := rtkInstall(bin, x); err != nil {
|
||||
return nil, fmt.Errorf("rtk was taken out of %s, but putting it back into %s failed: %w", a.Name, x.Name, err)
|
||||
}
|
||||
if err := sp.remove(a); err != nil {
|
||||
return nil, fmt.Errorf("taking rtk out of %s: %w", a.Name, err)
|
||||
}
|
||||
if sp.has(a) {
|
||||
return nil, fmt.Errorf("rtk's uninstaller left its hook in %s", a.Name)
|
||||
return nil, fmt.Errorf("rtk's hook is still in %s", a.Name)
|
||||
}
|
||||
}
|
||||
if b.dir != "" {
|
||||
pruneBackups()
|
||||
}
|
||||
v := ReadRTK()
|
||||
// those put back are as they were: only this one has anything new
|
||||
v.Restart, v.Backup = []string{a.ID}, b.dir
|
||||
return v, nil
|
||||
}
|
||||
@@ -336,6 +558,58 @@ func rtkInstall(bin string, a *agent.Agent) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// rtkScript is rtk's own installer, which puts it in ~/.local/bin.
|
||||
const rtkScript = "https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh"
|
||||
|
||||
// rtkInstaller is how rtk is installed here: with Homebrew when there is
|
||||
// one, winget on Windows, else rtk's own script; nil when there's no way.
|
||||
func rtkInstaller() []string {
|
||||
if runtime.GOOS == "windows" {
|
||||
if _, err := exec.LookPath("winget"); err == nil {
|
||||
return []string{"winget", "install", "--id", "rtk-ai.rtk", "--exact", "--silent", "--accept-package-agreements", "--accept-source-agreements", "--disable-interactivity"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if _, err := exec.LookPath("brew"); err == nil {
|
||||
return []string{"brew", "install", "rtk"}
|
||||
}
|
||||
if _, err := exec.LookPath("curl"); err == nil {
|
||||
return []string{"sh", "-c", "curl -fsSL " + rtkScript + " | sh"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InstallRTK installs rtk, when the user asks for it.
|
||||
func InstallRTK() (*RTKView, error) {
|
||||
rtkMu.Lock()
|
||||
defer rtkMu.Unlock()
|
||||
if rtkPath() != "" {
|
||||
return ReadRTK(), nil
|
||||
}
|
||||
c := rtkInstaller()
|
||||
if c == nil {
|
||||
return nil, fmt.Errorf("no way to install rtk here — get it from %s", RTKURL)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
cmd := proc.CommandContext(ctx, c[0], c[1:]...)
|
||||
cmd.Stdin = nil
|
||||
cmd.Env = append(os.Environ(), "HOMEBREW_NO_AUTO_UPDATE=1", "NONINTERACTIVE=1")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
text := strings.TrimSpace(string(out))
|
||||
if text == "" {
|
||||
text = err.Error()
|
||||
}
|
||||
return nil, fmt.Errorf("%s: %s", strings.Join(c, " "), lastLines(text, 4))
|
||||
}
|
||||
v := ReadRTK()
|
||||
if v.Path == "" {
|
||||
return nil, fmt.Errorf("%s ran, but rtk still isn't found: %s", strings.Join(c, " "), lastLines(string(out), 3))
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// RTKTakes is the id of the agent q names when rtk can be given to it.
|
||||
func RTKTakes(q string) (string, error) {
|
||||
a, err := agent.Find(q)
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
package library
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fakeRTK is an rtk on PATH that acts as rtk 0.50's installer does for
|
||||
// Claude Code, OpenCode and Cursor: installing OpenCode or Cursor gives it to
|
||||
// Claude Code as well, and uninstalling Claude Code or OpenCode takes all
|
||||
// three away.
|
||||
// Claude Code as well. Its uninstaller isn't there: magpie doesn't use it.
|
||||
const fakeRTK = `#!/bin/sh
|
||||
c="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
||||
claude() { [ -d "$c" ] && echo '{"hooks":{"PreToolUse":[{"hooks":[{"command":"rtk hook claude"}]}]}}' > "$c/settings.json"; }
|
||||
@@ -23,9 +24,6 @@ case "$*" in
|
||||
"--auto-patch") claude ;;
|
||||
"--opencode --auto-patch") claude; /bin/mkdir -p "$XDG_CONFIG_HOME/opencode/plugins"; echo x > "$XDG_CONFIG_HOME/opencode/plugins/rtk.ts" ;;
|
||||
"--agent cursor --auto-patch") claude; echo '{"hooks":{"preToolUse":[{"command":"rtk hook cursor"}]}}' > "$HOME/.cursor/hooks.json" ;;
|
||||
"--agent cursor --uninstall") echo '{}' > "$HOME/.cursor/hooks.json" ;;
|
||||
"--uninstall"|"--opencode --uninstall")
|
||||
echo '{}' > "$c/settings.json"; /bin/rm -f "$XDG_CONFIG_HOME/opencode/plugins/rtk.ts"; echo '{}' > "$HOME/.cursor/hooks.json" ;;
|
||||
*) echo "unknown: $*" >&2; exit 2 ;;
|
||||
esac
|
||||
`
|
||||
@@ -73,7 +71,7 @@ func TestRTK(t *testing.T) {
|
||||
want(false, true, true)
|
||||
set("claude", true)
|
||||
want(true, true, true)
|
||||
// Claude Code's uninstaller takes the other two with it: they're put back
|
||||
// taking it out of Claude Code leaves the other two as they are
|
||||
set("claude", false)
|
||||
want(false, true, true)
|
||||
set("claude", true)
|
||||
@@ -89,7 +87,139 @@ func TestRTK(t *testing.T) {
|
||||
if v := ReadRTK(); v.Path != "" {
|
||||
t.Fatalf("rtk found at %s with nothing on PATH", v.Path)
|
||||
}
|
||||
if _, err := SetRTK("claude", false); err == nil {
|
||||
t.Fatal("switched without rtk installed")
|
||||
if _, err := SetRTK("opencode", true); err == nil {
|
||||
t.Fatal("switched on without rtk installed")
|
||||
}
|
||||
// the hook of an rtk since removed can still be taken out
|
||||
set("claude", false)
|
||||
want(false, false, false)
|
||||
}
|
||||
|
||||
// TestRTKRemove: switching an agent off takes out just what rtk's installer
|
||||
// put in, as rtk 0.50 writes it, with no rtk to do it.
|
||||
func TestRTKRemove(t *testing.T) {
|
||||
h := sandbox(t)
|
||||
hook := func(key, matcher, cmd string) string {
|
||||
return `"hooks": {
|
||||
"` + key + `": [
|
||||
{
|
||||
"matcher": "` + matcher + `",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "` + cmd + `"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`
|
||||
}
|
||||
claude := filepath.Join(h, ".claude")
|
||||
write(t, filepath.Join(claude, "settings.json"), `{
|
||||
"model": "opus",
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Write",
|
||||
"hooks": [{"type": "command", "command": "lint"}]
|
||||
},
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "rtk hook claude"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`)
|
||||
write(t, filepath.Join(claude, "CLAUDE.md"), "# mine\n\n@RTK.md\n")
|
||||
write(t, filepath.Join(claude, "RTK.md"), "# RTK\n")
|
||||
codex := filepath.Join(h, ".codex")
|
||||
write(t, filepath.Join(codex, "hooks.json"), "{\n "+hook("PreToolUse", "Bash", "rtk hook codex")+"\n}\n")
|
||||
write(t, filepath.Join(codex, "AGENTS.md"), "@"+filepath.Join(codex, "RTK.md")+"\n")
|
||||
write(t, filepath.Join(codex, "RTK.md"), "# RTK\n")
|
||||
gemini := filepath.Join(h, ".gemini")
|
||||
sh := filepath.Join(gemini, "hooks", "rtk-hook-gemini.sh")
|
||||
write(t, filepath.Join(gemini, "settings.json"), "{\n \"theme\": \"dark\",\n "+hook("BeforeTool", "run_shell_command", sh)+"\n}\n")
|
||||
write(t, sh, "#!/bin/sh\n")
|
||||
write(t, filepath.Join(gemini, "hooks", ".rtk-hook.sha256"), "x\n")
|
||||
write(t, filepath.Join(gemini, "GEMINI.md"), "# g\n")
|
||||
write(t, filepath.Join(h, ".cursor", "hooks.json"), `{"version":1,"hooks":{"preToolUse":[{"command":"rtk hook cursor","matcher":"Shell"}]}}`)
|
||||
copilot := filepath.Join(h, ".copilot")
|
||||
write(t, filepath.Join(copilot, "hooks", "rtk-rewrite.json"), "{}")
|
||||
write(t, filepath.Join(copilot, "copilot-instructions.md"), "# c\n\n<!-- rtk-instructions v2 -->\nAlways prefix shell commands with rtk\n<!-- /rtk-instructions -->\n")
|
||||
write(t, filepath.Join(h, ".pi", "agent", "extensions", "rtk.ts"), "x")
|
||||
write(t, filepath.Join(h, ".config", "opencode", "plugins", "rtk.ts"), "x")
|
||||
|
||||
ids := []string{"claude", "codex", "gemini", "cursor", "copilot", "pi", "opencode"}
|
||||
v := ReadRTK()
|
||||
if v.Path != "" {
|
||||
t.Fatalf("rtk found at %s", v.Path)
|
||||
}
|
||||
on := map[string]bool{}
|
||||
for _, a := range v.Agents {
|
||||
on[a.ID] = a.On
|
||||
}
|
||||
for _, id := range ids {
|
||||
if !on[id] {
|
||||
t.Fatalf("%s's hook isn't seen: %v", id, on)
|
||||
}
|
||||
if _, err := SetRTK(id, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, a := range ReadRTK().Agents {
|
||||
if a.On {
|
||||
t.Fatalf("%s still has rtk", a.ID)
|
||||
}
|
||||
}
|
||||
jsonIs := func(p, want string) {
|
||||
t.Helper()
|
||||
var got, w any
|
||||
if err := json.Unmarshal([]byte(read(t, p)), &got); err != nil {
|
||||
t.Fatalf("%s: %v\n%s", p, err, read(t, p))
|
||||
}
|
||||
json.Unmarshal([]byte(want), &w)
|
||||
if !reflect.DeepEqual(got, w) {
|
||||
t.Fatalf("%s:\n%s\nwant %s", p, read(t, p), want)
|
||||
}
|
||||
}
|
||||
jsonIs(filepath.Join(claude, "settings.json"), `{"model":"opus","hooks":{"PreToolUse":[{"matcher":"Write","hooks":[{"type":"command","command":"lint"}]}]}}`)
|
||||
jsonIs(filepath.Join(codex, "hooks.json"), `{}`)
|
||||
jsonIs(filepath.Join(gemini, "settings.json"), `{"theme":"dark"}`)
|
||||
jsonIs(filepath.Join(h, ".cursor", "hooks.json"), `{"version":1}`)
|
||||
for p, want := range map[string]string{
|
||||
filepath.Join(claude, "CLAUDE.md"): "# mine\n",
|
||||
filepath.Join(copilot, "copilot-instructions.md"): "# c\n",
|
||||
filepath.Join(gemini, "GEMINI.md"): "# g\n",
|
||||
filepath.Join(claude, "RTK.md"): "",
|
||||
filepath.Join(codex, "AGENTS.md"): "",
|
||||
filepath.Join(codex, "RTK.md"): "",
|
||||
sh: "",
|
||||
filepath.Join(copilot, "hooks", "rtk-rewrite.json"): "",
|
||||
} {
|
||||
if got := read(t, p); got != want {
|
||||
t.Errorf("%s = %q, want %q", p, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDropHermesPlugin(t *testing.T) {
|
||||
p := filepath.Join(t.TempDir(), "config.yaml")
|
||||
for in, want := range map[string]string{
|
||||
"model: x\nplugins:\n enabled:\n - rtk-rewrite\n": "model: x\n",
|
||||
"model: x\nplugins:\n enabled:\n - a\n - rtk-rewrite\n": "model: x\nplugins:\n enabled:\n - a\n",
|
||||
} {
|
||||
write(t, p, in)
|
||||
if err := dropHermesPlugin(p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := read(t, p); got != want {
|
||||
t.Errorf("%q → %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-2
@@ -20,7 +20,8 @@ const libraryUsage = `magpie library what the library gives
|
||||
magpie library skill agents <name> <a,b…|none>
|
||||
magpie library skill rm <name> (skills are installed from the app's Library page)
|
||||
magpie library rtk which agents run their shell commands through RTK (rtk-ai.app), to save tokens
|
||||
magpie library rtk on|off <agent> switch it, with RTK's own installer
|
||||
magpie library rtk on|off <agent> switch it (on with RTK's own installer; off works with RTK gone)
|
||||
magpie library rtk install install RTK (Homebrew, winget, or RTK's own script)
|
||||
`
|
||||
|
||||
// libraryCmd is magpie library …: the instructions, MCP servers and skills
|
||||
@@ -250,6 +251,12 @@ func rtkCmd(args []string) error {
|
||||
switch {
|
||||
case len(args) == 0:
|
||||
v = library.ReadRTK()
|
||||
case len(args) == 1 && args[0] == "install":
|
||||
fmt.Println(muted.Render("installing rtk…"))
|
||||
var err error
|
||||
if v, err = library.InstallRTK(); err != nil {
|
||||
return err
|
||||
}
|
||||
case len(args) == 2 && (args[0] == "on" || args[0] == "off"):
|
||||
id, err := library.RTKTakes(args[1])
|
||||
if err != nil {
|
||||
@@ -263,6 +270,9 @@ func rtkCmd(args []string) error {
|
||||
}
|
||||
if v.Path == "" {
|
||||
fmt.Println(amber.Render("!"), "rtk isn't installed —", v.URL)
|
||||
if v.Install != "" {
|
||||
fmt.Println(muted.Render(" magpie library rtk install runs: " + v.Install))
|
||||
}
|
||||
} else {
|
||||
fmt.Println(bold.Render("RTK"), muted.Render(v.Version+" · "+v.Path))
|
||||
if g := v.Gain; g != nil {
|
||||
@@ -271,10 +281,14 @@ func rtkCmd(args []string) error {
|
||||
}
|
||||
for _, a := range v.Agents {
|
||||
mark := muted.Render("off")
|
||||
note := ""
|
||||
if a.On {
|
||||
mark = green.Render("on ")
|
||||
if v.Path == "" {
|
||||
mark, note = amber.Render("on "), muted.Render(" — its hook calls rtk, which isn't installed: install it, or switch this off")
|
||||
}
|
||||
}
|
||||
fmt.Println(" ", mark, a.Name)
|
||||
fmt.Println(" ", mark, a.Name+note)
|
||||
}
|
||||
if len(v.Agents) == 0 {
|
||||
fmt.Println(" ", muted.Render("none of the agents here is one RTK has a hook for"))
|
||||
|
||||
Reference in new issue
Block a user