Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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");
Expand Down
Loading