replace shell update scripts with Go tooling

This commit is contained in:
Josh Palmer 2026-01-04 18:06:45 +01:00
parent df368945bc
commit 2f252345d9
11 changed files with 397 additions and 244 deletions

View File

@ -18,7 +18,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Sync skills from clawdbot - name: Sync skills from clawdbot
run: scripts/sync-skills.sh run: go run ./cmd/sync-skills
- name: Commit & push if changed - name: Commit & push if changed
run: | run: |

View File

@ -23,7 +23,7 @@ jobs:
- name: Update tool versions and hashes - name: Update tool versions and hashes
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
run: scripts/update-tools.sh run: go run ./cmd/update-tools
- name: Commit & push if changed - name: Commit & push if changed
run: | run: |

View File

@ -65,7 +65,7 @@ inputs.nix-steipete-tools.packages.aarch64-darwin.peekaboo
Skills are vendored from [clawdbot/clawdbot](https://github.com/clawdbot/clawdbot) main branch. No pinning - we track latest. Skills are vendored from [clawdbot/clawdbot](https://github.com/clawdbot/clawdbot) main branch. No pinning - we track latest.
```bash ```bash
scripts/sync-skills.sh go run ./cmd/sync-skills
``` ```
Pulls latest main via sparse checkout, only updates files when contents actually change. Pulls latest main via sparse checkout, only updates files when contents actually change.
@ -75,7 +75,7 @@ Pulls latest main via sparse checkout, only updates files when contents actually
Tools track upstream GitHub releases directly (not Homebrew). Tools track upstream GitHub releases directly (not Homebrew).
```bash ```bash
scripts/update-tools.sh go run ./cmd/update-tools
``` ```
Fetches latest release versions/URLs/hashes and updates the Nix expressions. Oracle uses pnpm and auto-derives its hash via build mismatch. Fetches latest release versions/URLs/hashes and updates the Nix expressions. Oracle uses pnpm and auto-derives its hash via build mismatch.

112
cmd/sync-skills/main.go Normal file
View File

@ -0,0 +1,112 @@
package main
import (
"bytes"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
)
type Mapping struct {
Tool string
Up string
}
func run(dir string, name string, args ...string) error {
cmd := exec.Command(name, args...)
cmd.Dir = dir
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
if err := cmd.Run(); err != nil {
return fmt.Errorf("%s %v: %v: %s", name, args, err, out.String())
}
return nil
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return err
}
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
return out.Close()
}
func main() {
repoRoot, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
workdir, err := os.MkdirTemp("", "clawdbot-skills-")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(workdir)
mappings := []Mapping{
{"summarize", "skills/summarize"},
{"gogcli", "skills/gog"},
{"camsnap", "skills/camsnap"},
{"sonoscli", "skills/sonoscli"},
{"bird", "skills/bird"},
{"peekaboo", "skills/peekaboo"},
{"sag", "skills/sag"},
{"imsg", "skills/imsg"},
{"oracle", "skills/oracle"},
}
log.Printf("[sync-skills] cloning clawdbot main")
if err := run("", "git", "clone", "--depth", "1", "--filter=blob:none", "--sparse", "https://github.com/clawdbot/clawdbot.git", workdir); err != nil {
log.Fatal(err)
}
paths := []string{}
for _, m := range mappings {
paths = append(paths, m.Up)
}
args := append([]string{"sparse-checkout", "set"}, paths...)
if err := run(workdir, "git", args...); err != nil {
log.Fatal(err)
}
updated := false
for _, m := range mappings {
src := filepath.Join(workdir, m.Up, "SKILL.md")
dest := filepath.Join(repoRoot, "tools", m.Tool, "skills", filepath.Base(m.Up), "SKILL.md")
if _, err := os.Stat(src); err != nil {
log.Printf("[sync-skills] missing %s", src)
continue
}
same := false
if b1, err1 := os.ReadFile(src); err1 == nil {
if b2, err2 := os.ReadFile(dest); err2 == nil && bytes.Equal(b1, b2) {
same = true
}
}
if !same {
if err := copyFile(src, dest); err != nil {
log.Fatalf("copy %s -> %s: %v", src, dest, err)
}
updated = true
log.Printf("[sync-skills] updated %s", m.Tool)
}
}
if !updated {
log.Printf("[sync-skills] no changes")
}
}

147
cmd/update-tools/main.go Normal file
View File

@ -0,0 +1,147 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/clawdbot/nix-stepiete-tools/internal"
)
type Tool struct {
Name string
Repo string
AssetRegex *regexp.Regexp
NixFile string
}
func updateTool(tool Tool) error {
log.Printf("[update-tools] %s", tool.Name)
rel, err := internal.LatestRelease(tool.Repo)
if err != nil {
return err
}
version := strings.TrimPrefix(rel.TagName, "v")
var assetURL string
for _, a := range rel.Assets {
if tool.AssetRegex.MatchString(a.Name) {
assetURL = a.BrowserDownloadURL
break
}
}
if assetURL == "" {
return fmt.Errorf("no asset matched for %s", tool.Name)
}
hash, err := internal.PrefetchHash(assetURL)
if err != nil {
return err
}
if err := internal.ReplaceOnce(tool.NixFile, regexp.MustCompile(`version = "[^"]+";`), fmt.Sprintf(`version = "%s";`, version)); err != nil {
return err
}
if err := internal.ReplaceOnce(tool.NixFile, regexp.MustCompile(`url = "[^"]+";`), fmt.Sprintf(`url = "%s";`, assetURL)); err != nil {
return err
}
if err := internal.ReplaceOnce(tool.NixFile, regexp.MustCompile(`hash = "sha256-[^"]+";`), fmt.Sprintf(`hash = "%s";`, hash)); err != nil {
return err
}
return nil
}
func updateOracle(repoRoot string) error {
log.Printf("[update-tools] oracle")
rel, err := internal.LatestRelease("steipete/oracle")
if err != nil {
return err
}
version := strings.TrimPrefix(rel.TagName, "v")
var assetURL string
for _, a := range rel.Assets {
if matched, _ := regexp.MatchString(`oracle-[0-9.]+\.tgz`, a.Name); matched {
assetURL = a.BrowserDownloadURL
break
}
}
if assetURL == "" {
return fmt.Errorf("no asset matched for oracle")
}
assetHash, err := internal.PrefetchHash(assetURL)
if err != nil {
return err
}
lockURL := fmt.Sprintf("https://github.com/steipete/oracle/archive/refs/tags/%s.tar.gz", rel.TagName)
lockHash, err := internal.PrefetchHash(lockURL)
if err != nil {
return err
}
oracleFile := filepath.Join(repoRoot, "nix", "pkgs", "oracle.nix")
if err := internal.ReplaceOnce(oracleFile, regexp.MustCompile(`version = "[^"]+";`), fmt.Sprintf(`version = "%s";`, version)); err != nil {
return err
}
if err := internal.ReplaceOnce(oracleFile, regexp.MustCompile(`url = "[^"]+";`), fmt.Sprintf(`url = "%s";`, assetURL)); err != nil {
return err
}
if err := internal.ReplaceOnce(oracleFile, regexp.MustCompile(`hash = "sha256-[^"]+";`), fmt.Sprintf(`hash = "%s";`, assetHash)); err != nil {
return err
}
lockRe := regexp.MustCompile(`lockSrc = fetchFromGitHub \{[^}]*?hash = "sha256-[^"]+";`)
if err := internal.ReplaceOnceFunc(oracleFile, lockRe, func(s string) string {
return regexp.MustCompile(`hash = "sha256-[^"]+";`).ReplaceAllString(s, fmt.Sprintf(`hash = "%s";`, lockHash))
}); err != nil {
return err
}
pnpmRe := regexp.MustCompile(`pnpmDeps.*?hash = "sha256-[^"]+";`)
if err := internal.ReplaceOnceFunc(oracleFile, pnpmRe, func(s string) string {
return regexp.MustCompile(`hash = "sha256-[^"]+";`).ReplaceAllString(s, `hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";`)
}); err != nil {
return err
}
log.Printf("[update-tools] oracle: deriving pnpm hash")
logText, _ := internal.NixBuildOracle()
pnpmHash := internal.ExtractGotHash(logText)
if pnpmHash == "" {
return fmt.Errorf("oracle pnpm hash not found in build output")
}
if err := internal.ReplaceOnceFunc(oracleFile, pnpmRe, func(s string) string {
return regexp.MustCompile(`hash = "sha256-[^"]+";`).ReplaceAllString(s, fmt.Sprintf(`hash = "%s";`, pnpmHash))
}); err != nil {
return err
}
return nil
}
func main() {
repoRoot, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
tools := []Tool{
{"summarize", "steipete/summarize", regexp.MustCompile(`summarize-macos-arm64-v[0-9.]+\.tar\.gz`), filepath.Join(repoRoot, "nix", "pkgs", "summarize.nix")},
{"gogcli", "steipete/gogcli", regexp.MustCompile(`gogcli_[0-9.]+_darwin_arm64\.tar\.gz`), filepath.Join(repoRoot, "nix", "pkgs", "gogcli.nix")},
{"camsnap", "steipete/camsnap", regexp.MustCompile(`camsnap-macos-arm64\.tar\.gz`), filepath.Join(repoRoot, "nix", "pkgs", "camsnap.nix")},
{"sonoscli", "steipete/sonoscli", regexp.MustCompile(`sonoscli-macos-arm64\.tar\.gz`), filepath.Join(repoRoot, "nix", "pkgs", "sonoscli.nix")},
{"bird", "steipete/bird", regexp.MustCompile(`bird-macos-universal-v[0-9.]+\.tar\.gz`), filepath.Join(repoRoot, "nix", "pkgs", "bird.nix")},
{"peekaboo", "steipete/peekaboo", regexp.MustCompile(`peekaboo-macos-universal\.tar\.gz`), filepath.Join(repoRoot, "nix", "pkgs", "peekaboo.nix")},
{"poltergeist", "steipete/poltergeist", regexp.MustCompile(`poltergeist-macos-universal-v[0-9.]+\.tar\.gz`), filepath.Join(repoRoot, "nix", "pkgs", "poltergeist.nix")},
{"sag", "steipete/sag", regexp.MustCompile(`sag_[0-9.]+_darwin_universal\.tar\.gz`), filepath.Join(repoRoot, "nix", "pkgs", "sag.nix")},
{"imsg", "steipete/imsg", regexp.MustCompile(`imsg-macos\.zip`), filepath.Join(repoRoot, "nix", "pkgs", "imsg.nix")},
}
for _, tool := range tools {
if err := updateTool(tool); err != nil {
log.Fatalf("update %s failed: %v", tool.Name, err)
}
}
if err := updateOracle(repoRoot); err != nil {
log.Fatalf("update oracle failed: %v", err)
}
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module github.com/clawdbot/nix-stepiete-tools
go 1.22

33
internal/fs.go Normal file
View File

@ -0,0 +1,33 @@
package internal
import (
"fmt"
"os"
"regexp"
)
func ReplaceOnce(path string, re *regexp.Regexp, replace string) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
orig := string(data)
out := re.ReplaceAllString(orig, replace)
if out == orig {
return fmt.Errorf("pattern not found in %s", path)
}
return os.WriteFile(path, []byte(out), 0644)
}
func ReplaceOnceFunc(path string, re *regexp.Regexp, fn func(string) string) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
orig := string(data)
out := re.ReplaceAllStringFunc(orig, fn)
if out == orig {
return fmt.Errorf("pattern not found in %s", path)
}
return os.WriteFile(path, []byte(out), 0644)
}

49
internal/github.go Normal file
View File

@ -0,0 +1,49 @@
package internal
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
)
type Release struct {
TagName string `json:"tag_name"`
Assets []Asset `json:"assets"`
}
type Asset struct {
Name string `json:"name"`
BrowserDownloadURL string `json:"browser_download_url"`
}
func LatestRelease(repo string) (*Release, error) {
url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", repo)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/vnd.github+json")
if token := os.Getenv("GH_TOKEN"); token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("github api %s: %s", resp.Status, string(body))
}
var rel Release
if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil {
return nil, err
}
if rel.TagName == "" {
return nil, errors.New("missing tag_name in release")
}
return &rel, nil
}

49
internal/nix.go Normal file
View File

@ -0,0 +1,49 @@
package internal
import (
"bytes"
"encoding/json"
"fmt"
"os/exec"
"strings"
)
type PrefetchResult struct {
Hash string `json:"hash"`
}
func PrefetchHash(url string) (string, error) {
cmd := exec.Command("nix", "store", "prefetch-file", "--json", url)
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("prefetch failed: %v: %s", err, out.String())
}
var res PrefetchResult
if err := json.Unmarshal(out.Bytes(), &res); err != nil {
return "", err
}
if res.Hash == "" {
return "", fmt.Errorf("empty hash for %s", url)
}
return res.Hash, nil
}
func NixBuildOracle() (string, error) {
cmd := exec.Command("nix", "build", ".#oracle")
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
err := cmd.Run()
return out.String(), err
}
func ExtractGotHash(log string) string {
for _, line := range strings.Split(log, "\n") {
if idx := strings.Index(line, "got: sha256-"); idx != -1 {
return strings.TrimSpace(line[idx+5:])
}
}
return ""
}

View File

@ -1,58 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
workdir="$(mktemp -d)"
trap 'rm -rf "${workdir}"' EXIT
# Mapping: tool -> upstream skill dir (relative to clawdbot repo)
declare -A SKILLS
SKILLS[summarize]="skills/summarize"
SKILLS[gogcli]="skills/gog"
SKILLS[camsnap]="skills/camsnap"
SKILLS[sonoscli]="skills/sonoscli"
SKILLS[bird]="skills/bird"
SKILLS[peekaboo]="skills/peekaboo"
SKILLS[sag]="skills/sag"
SKILLS[imsg]="skills/imsg"
SKILLS[oracle]="skills/oracle"
# Clone only the needed paths from latest main.
# Note: no pinning; this is explicitly "latest main".
git -c advice.detachedHead=false clone --depth 1 --filter=blob:none --sparse \
https://github.com/clawdbot/clawdbot.git "${workdir}/clawdbot" >/dev/null
pushd "${workdir}/clawdbot" >/dev/null
git sparse-checkout set "${SKILLS[@]}" >/dev/null
popd >/dev/null
updated=0
for tool in "${!SKILLS[@]}"; do
src_dir="${workdir}/clawdbot/${SKILLS[$tool]}"
dest_dir="${repo_root}/tools/${tool}/skills/$(basename "${SKILLS[$tool]}")"
if [[ ! -d "${src_dir}" ]]; then
echo "[sync-skills] missing upstream skill: ${SKILLS[$tool]} (skip)" >&2
continue
fi
mkdir -p "${dest_dir}"
# Copy only if content changed (avoid noisy commits).
if ! cmp -s "${src_dir}/SKILL.md" "${dest_dir}/SKILL.md" 2>/dev/null; then
cp "${src_dir}/SKILL.md" "${dest_dir}/SKILL.md"
updated=1
echo "[sync-skills] updated ${tool}" >&2
fi
# If upstream has extra files in the skill dir, sync them too.
rsync -a --delete --exclude SKILL.md "${src_dir}/" "${dest_dir}/" >/dev/null
if [[ -n "$(git -C "${repo_root}" status --porcelain "${dest_dir}" 2>/dev/null)" ]]; then
updated=1
fi
done
if [[ $updated -eq 0 ]]; then
echo "[sync-skills] no changes" >&2
fi

View File

@ -1,182 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
if ! command -v jq >/dev/null 2>&1; then
echo "[update-tools] jq is required" >&2
exit 1
fi
if ! command -v nix >/dev/null 2>&1; then
echo "[update-tools] nix is required" >&2
exit 1
fi
prefetch_hash() {
local url="$1"
nix store prefetch-file --json "$url" | jq -r .hash
}
latest_release() {
local repo="$1"
gh api "repos/${repo}/releases/latest"
}
update_nix_file() {
local file="$1"
local version="$2"
local url="$3"
local hash="$4"
VERSION="$version" URL="$url" HASH="$hash" FILE="$file" python3 - <<'PY'
from pathlib import Path
import os
import re
path = Path(os.environ["FILE"])
version = os.environ["VERSION"]
url = os.environ["URL"]
hash_ = os.environ["HASH"]
text = path.read_text()
text, n1 = re.subn(r'version = "[^"]+";', f'version = "{version}";', text, count=1)
text, n2 = re.subn(r'url = "[^"]+";', f'url = "{url}";', text, count=1)
text, n3 = re.subn(r'hash = "sha256-[^"]+";', f'hash = "{hash_}";', text, count=1)
if n1 == 0 or n2 == 0 or n3 == 0:
raise SystemExit(f"update failed for {path}: version/url/hash not found")
path.write_text(text)
PY
}
update_tool() {
local tool="$1"
local repo="$2"
local asset_regex="$3"
local nix_file="$4"
echo "[update-tools] ${tool}" >&2
local json
json=$(latest_release "$repo")
local tag
tag=$(echo "$json" | jq -r .tag_name)
local version
version="${tag#v}"
local asset
asset=$(echo "$json" | jq -r --arg re "$asset_regex" '.assets[] | select(.name|test($re)) | .browser_download_url' | head -1)
if [[ -z "$asset" ]]; then
echo "[update-tools] no asset matched for ${tool} (${asset_regex})" >&2
return 1
fi
local hash
hash=$(prefetch_hash "$asset")
update_nix_file "$nix_file" "$version" "$asset" "$hash"
}
update_oracle() {
local tool="oracle"
local repo="steipete/oracle"
local nix_file="$repo_root/nix/pkgs/oracle.nix"
echo "[update-tools] ${tool}" >&2
local json
json=$(latest_release "$repo")
local tag
tag=$(echo "$json" | jq -r .tag_name)
local version
version="${tag#v}"
local asset
asset=$(echo "$json" | jq -r '.assets[] | select(.name|test("oracle-[0-9.]+\\.tgz")) | .browser_download_url' | head -1)
if [[ -z "$asset" ]]; then
echo "[update-tools] no asset matched for oracle" >&2
return 1
fi
local asset_hash
asset_hash=$(prefetch_hash "$asset")
local lock_url
lock_url="https://github.com/steipete/oracle/archive/refs/tags/${tag}.tar.gz"
local lock_hash
lock_hash=$(prefetch_hash "$lock_url")
VERSION="$version" URL="$asset" HASH="$asset_hash" LOCK_HASH="$lock_hash" FILE="$nix_file" python3 - <<'PY'
from pathlib import Path
import os
import re
path = Path(os.environ["FILE"])
version = os.environ["VERSION"]
url = os.environ["URL"]
hash_ = os.environ["HASH"]
lock_hash = os.environ["LOCK_HASH"]
text = path.read_text()
text, n1 = re.subn(r'version = "[^"]+";', f'version = "{version}";', text, count=1)
text, n2 = re.subn(r'url = "[^"]+";', f'url = "{url}";', text, count=1)
text, n3 = re.subn(r'hash = "sha256-[^"]+";', f'hash = "{hash_}";', text, count=1)
text, n4 = re.subn(r'lockSrc = fetchFromGitHub \{[^}]*?hash = "sha256-[^"]+";',
lambda m: re.sub(r'hash = "sha256-[^"]+";', f'hash = "{lock_hash}";', m.group(0)),
text, count=1, flags=re.S)
text, n5 = re.subn(r'pnpmDeps.*?hash = "sha256-[^"]+";',
lambda m: re.sub(r'hash = "sha256-[^"]+";', 'hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";', m.group(0)),
text, count=1, flags=re.S)
if n1 == 0 or n2 == 0 or n3 == 0 or n4 == 0 or n5 == 0:
raise SystemExit(f"update failed for {path}: fields not found")
path.write_text(text)
PY
# Derive pnpmDeps hash from a build mismatch
set +e
build_log=$(nix build .#oracle 2>&1)
set -e
if echo "$build_log" | grep -q "got: sha256-"; then
pnpm_hash=$(echo "$build_log" | sed -n 's/.*got: \(sha256-[A-Za-z0-9+/=]*\).*/\1/p' | head -1)
if [[ -n "$pnpm_hash" ]]; then
PNPM_HASH="$pnpm_hash" FILE="$nix_file" python3 - <<'PY'
from pathlib import Path
import os
import re
path = Path(os.environ["FILE"])
pnpm_hash = os.environ["PNPM_HASH"]
text = path.read_text()
text, n = re.subn(r'pnpmDeps.*?hash = "sha256-[^"]+";',
lambda m: re.sub(r'hash = "sha256-[^"]+";', f'hash = "{pnpm_hash}";', m.group(0)),
text, count=1, flags=re.S)
if n == 0:
raise SystemExit(f"update failed for {path}: pnpmDeps hash not found")
path.write_text(text)
PY
else
echo "[update-tools] failed to extract pnpmDeps hash" >&2
return 1
fi
else
echo "[update-tools] no pnpmDeps hash mismatch found" >&2
return 1
fi
}
update_tool summarize "steipete/summarize" "summarize-macos-arm64-v[0-9.]+\\.tar\\.gz" "$repo_root/nix/pkgs/summarize.nix"
update_tool gogcli "steipete/gogcli" "gogcli_[0-9.]+_darwin_arm64\\.tar\\.gz" "$repo_root/nix/pkgs/gogcli.nix"
update_tool camsnap "steipete/camsnap" "camsnap-macos-arm64\\.tar\\.gz" "$repo_root/nix/pkgs/camsnap.nix"
update_tool sonoscli "steipete/sonoscli" "sonoscli-macos-arm64\\.tar\\.gz" "$repo_root/nix/pkgs/sonoscli.nix"
update_tool bird "steipete/bird" "bird-macos-universal-v[0-9.]+\\.tar\\.gz" "$repo_root/nix/pkgs/bird.nix"
update_tool peekaboo "steipete/peekaboo" "peekaboo-macos-universal\\.tar\\.gz" "$repo_root/nix/pkgs/peekaboo.nix"
update_tool poltergeist "steipete/poltergeist" "poltergeist-macos-universal-v[0-9.]+\\.tar\\.gz" "$repo_root/nix/pkgs/poltergeist.nix"
update_tool sag "steipete/sag" "sag_[0-9.]+_darwin_universal\\.tar\\.gz" "$repo_root/nix/pkgs/sag.nix"
update_tool imsg "steipete/imsg" "imsg-macos\\.zip" "$repo_root/nix/pkgs/imsg.nix"
update_oracle