diff --git a/apps/mobile/modules/t3-device-runtime/android/src/main/assets/device-runtime-setup.sh b/apps/mobile/modules/t3-device-runtime/android/src/main/assets/device-runtime-setup.sh index e3307e47120f..f8ead04a7e3d 100644 --- a/apps/mobile/modules/t3-device-runtime/android/src/main/assets/device-runtime-setup.sh +++ b/apps/mobile/modules/t3-device-runtime/android/src/main/assets/device-runtime-setup.sh @@ -4,7 +4,8 @@ set -euo pipefail export DEBIAN_FRONTEND=noninteractive export PATH="$HOME/.local/bin:$PATH" NODE_CHANNEL=https://nodejs.org/dist/latest-v24.x -CLAUDE_LIST=/etc/apt/sources.list.d/claude-code.list +CLAUDE_LIST=${CLAUDE_LIST:-/etc/apt/sources.list.d/claude-code.list} +CLAUDE_MANAGED=${CLAUDE_MANAGED:-/etc/claude-code} # The fork whose releases carry a Linux arm64 server (FORK.md). RELEASE_REPO=tebiggs/t3code T3_LINK="$HOME/.local/bin/t3" @@ -55,20 +56,41 @@ step_node() { done } -# Anthropic's stable channel. The app checks the signing key's fingerprint and writes it -# first. One copy of the binary, updated only through apt. +# Anthropic's apt repository for the channel Claude Code's settings pick. The app checks the +# signing key's fingerprint and writes it first. One copy of the binary, updated only through apt. step_claude_code() { [ -s /etc/apt/keyrings/claude-code.asc ] || { echo "Claude Code's signing key is missing." >&2; exit 1; } - echo "deb [signed-by=/etc/apt/keyrings/claude-code.asc] https://downloads.claude.ai/claude-code/apt/stable stable main" > "$CLAUDE_LIST" + write_claude_list update_claude_repo apt_install claude-code } +# Moving to stable keeps a newer build until stable passes it. step_upgrade_claude_code() { + write_claude_list update_claude_repo apt-get install -y --only-upgrade claude-code } +write_claude_list() { + channel=$(claude_channel) + echo "deb [signed-by=/etc/apt/keyrings/claude-code.asc] https://downloads.claude.ai/claude-code/apt/$channel $channel main" > "$CLAUDE_LIST" + echo "Claude Code channel: $channel" +} + +# autoUpdatesChannel, latest by default as in Claude Code. Later files win, as in the server's +# ClaudeMaintenance.readUpdateChannel. +claude_channel() { + channel=latest + for file in "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" "$CLAUDE_MANAGED/managed-settings.json" \ + "$CLAUDE_MANAGED"/managed-settings.d/*.json; do + [ -f "$file" ] || continue + value=$(sed -n 's/.*"autoUpdatesChannel"[[:space:]]*:[[:space:]]*"\(latest\|stable\)".*/\1/p' "$file" | tail -n 1) + if [ -n "$value" ]; then channel=$value; fi + done + echo "$channel" +} + update_claude_repo() { apt-get update -o Dir::Etc::SourceList="$CLAUDE_LIST" -o Dir::Etc::SourceParts=- -o APT::Get::List-Cleanup=0 } diff --git a/apps/mobile/src/features/device-runtime/device-runtime-script.test.ts b/apps/mobile/src/features/device-runtime/device-runtime-script.test.ts index 8e0dcd2be89d..e1c5e512c44a 100644 --- a/apps/mobile/src/features/device-runtime/device-runtime-script.test.ts +++ b/apps/mobile/src/features/device-runtime/device-runtime-script.test.ts @@ -30,6 +30,7 @@ ln -sfn "$HOME/.t3/runtime/versions/$T3CODE_VERSION/t3" "$HOME/.local/bin/t3" `; interface Guest { + readonly root: string; readonly home: string; readonly log: string; run(step: string): string; @@ -64,6 +65,7 @@ esac `, ); writeExecutable(NodePath.join(bin, "node"), "#!/bin/sh\necho v24.22.1\n"); + writeExecutable(NodePath.join(bin, "apt-get"), `#!/bin/sh\necho "apt-get $*" >> "$STUB_LOG"\n`); writeExecutable( NodePath.join(bin, "claude"), `#!/bin/sh @@ -75,13 +77,21 @@ esac ); const versions = NodePath.join(home, ".t3/runtime/versions"); return { + root, home, log, run(step) { return NodeChildProcess.execFileSync("bash", ["-s", "--", step], { input: SCRIPT, encoding: "utf8", - env: { NODE_ENV: "test", HOME: home, PATH: `${bin}:/usr/bin:/bin`, STUB_LOG: log }, + env: { + NODE_ENV: "test", + HOME: home, + PATH: `${bin}:/usr/bin:/bin`, + STUB_LOG: log, + CLAUDE_LIST: NodePath.join(root, "claude-code.list"), + CLAUDE_MANAGED: NodePath.join(root, "claude-code"), + }, }); }, version(name) { @@ -132,6 +142,36 @@ describe("device-runtime-setup.sh", () => { expect(NodeFS.readFileSync(guest.log, "utf8")).toBe(""); }); + it.each([ + { settings: {}, channel: "latest" }, + { + settings: { ".claude/settings.json": '{ "autoUpdatesChannel": "stable" }' }, + channel: "stable", + }, + { + settings: { + ".claude/settings.json": '{"autoUpdatesChannel":"stable"}', + "claude-code/managed-settings.json": "{}", + "claude-code/managed-settings.d/10-channel.json": '{\n "autoUpdatesChannel": "latest"\n}', + }, + channel: "latest", + }, + ])("upgrades Claude Code from the $channel apt repository its settings pick", (row) => { + using guest = makeGuest(); + for (const [file, text] of Object.entries(row.settings)) { + const path = NodePath.join(file.startsWith(".claude") ? guest.home : guest.root, file); + NodeFS.mkdirSync(NodePath.dirname(path), { recursive: true }); + NodeFS.writeFileSync(path, text); + } + guest.run("upgrade-claude-code"); + expect(NodeFS.readFileSync(NodePath.join(guest.root, "claude-code.list"), "utf8")).toBe( + `deb [signed-by=/etc/apt/keyrings/claude-code.asc] https://downloads.claude.ai/claude-code/apt/${row.channel} ${row.channel} main\n`, + ); + expect(NodeFS.readFileSync(guest.log, "utf8")).toContain( + "apt-get install -y --only-upgrade claude-code", + ); + }); + it("reports versions and the Claude sign-in for the app", () => { using guest = makeGuest(); guest.link("0.1.3");