#!/bin/sh # TokenJoy / macOS / Claude Code. Never run with sudo. # A function wrapper prevents a truncated curl | sh download from making changes. tokenjoy_main() { set +x set -eu umask 077 fail() { printf '\nTokenJoy: %s\n' "$1" >&2; exit 1; } [ "$(uname -s)" = Darwin ] || fail '此脚本仅支持 macOS。' [ "$(id -u)" -ne 0 ] || fail '请使用普通用户运行,不要加 sudo。' mac_major=$(sw_vers -productVersion | cut -d . -f 1) [ "$mac_major" -ge 13 ] || fail '需要 macOS 13 或更新版本。' case "${1:-}" in ''|--key-stdin) ;; *) fail '用法:sh install.sh [--key-stdin]' ;; esac [ "$#" -le 1 ] || fail '参数过多。' config_dir=${CLAUDE_CONFIG_DIR:-"$HOME/.claude"} case "$config_dir" in /*) ;; *) fail 'CLAUDE_CONFIG_DIR 必须是绝对路径。' ;; esac [ ! -L "$config_dir" ] || fail '配置目录是符号链接,请先检查后手动配置。' settings="$config_dir/settings.json" [ ! -L "$settings" ] || fail '配置文件是符号链接,请先检查后手动配置。' [ ! -e "$settings" ] || [ -f "$settings" ] || fail '配置文件路径不是普通文件。' work_dir=$(mktemp -d "${TMPDIR:-/tmp}/tokenjoy-setup.XXXXXX") tty_state='' cleanup() { if [ -n "$tty_state" ]; then stty "$tty_state" /dev/null || true; fi rm -rf "$work_dir" } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM HUP printf '\nTokenJoy · Claude Code 配置\n' if [ "${1:-}" = --key-stdin ]; then IFS= read -r tokenjoy_key || fail '无法读取 SK。' else tty_state=$(stty -g /dev/tty stty -echo /dev/tty fi case "$tokenjoy_key" in sk-*) ;; *) fail 'SK 必须以 sk- 开头。' ;; esac case "$tokenjoy_key" in *[!A-Za-z0-9_-]*) fail 'SK 含无效字符。' ;; esac [ "${#tokenjoy_key}" -ge 11 ] && [ "${#tokenjoy_key}" -le 512 ] || fail 'SK 长度无效。' printf '%s' "$tokenjoy_key" >"$work_dir/key" printf 'Authorization: Bearer %s\n' "$tokenjoy_key" >"$work_dir/headers" unset tokenjoy_key printf '检查密钥与网关连接…\n' http_status=$(curl --proto '=https' --tlsv1.2 -sS --connect-timeout 15 --max-time 45 \ --header "@$work_dir/headers" -o "$work_dir/models.json" -w '%{http_code}' \ https://tokenjoy.org/v1/models) || fail '无法连接 TokenJoy,请检查网络后重试。' case "$http_status" in 200) ;; 401|403) fail '密钥无效或无权限;尚未更改本机配置。' ;; *) fail "网关暂时不可用(HTTP ${http_status});尚未更改本机配置。" ;; esac # macOS ships JavaScript for Automation. Foundation is used only for local # JSON file IO; no app automation and no Python/Node/Homebrew dependency. cat >"$work_dir/configure.js" <<'TOKENJOY_JS' ObjC.import('Foundation'); function run(args) { const fm = $.NSFileManager.defaultManager; const read = p => { const error = Ref(); const value = $.NSString.stringWithContentsOfFileEncodingError(p, $.NSUTF8StringEncoding, error); if (!value || value.isNil()) throw new Error('无法读取本地文件。'); return ObjC.unwrap(value); }; const object = value => value && typeof value === 'object' && !Array.isArray(value); const models = JSON.parse(read(args[2])); if (!object(models) || !Array.isArray(models.data)) throw new Error('网关返回了非预期的模型列表;配置未修改。'); let config = {}; if (fm.fileExistsAtPath(args[0])) config = JSON.parse(read(args[0])); if (!object(config)) throw new Error('settings.json 必须是 JSON 对象;配置未修改。'); if (config.env !== undefined && !object(config.env)) throw new Error('env 必须是 JSON 对象;配置未修改。'); config.env = config.env || {}; config.env.ANTHROPIC_BASE_URL = 'https://tokenjoy.org'; config.env.ANTHROPIC_AUTH_TOKEN = read(args[1]); config.env.ANTHROPIC_API_KEY = ''; config.env.CLAUDE_CODE_OAUTH_TOKEN = ''; for (const name of ['CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_USE_FOUNDRY']) config.env[name] = '0'; const output = $(JSON.stringify(config, null, 2) + '\n'); if (!output.writeToFileAtomicallyEncodingError(args[3], true, $.NSUTF8StringEncoding, Ref())) throw new Error('无法写入配置临时文件。'); } TOKENJOY_JS /usr/bin/osascript -l JavaScript "$work_dir/configure.js" "$settings" "$work_dir/key" "$work_dir/models.json" "$work_dir/settings.json" >/dev/null || fail '无法合并配置,请检查原 settings.json;原配置未修改。' if ! command -v claude >/dev/null 2>&1 && [ ! -x "$HOME/.local/bin/claude" ]; then printf '从 Claude 官方安装 Claude Code…\n' curl --proto '=https' --proto-redir '=https' --tlsv1.2 -fsSL --connect-timeout 15 --max-time 120 \ https://claude.ai/install.sh -o "$work_dir/claude-install.sh" || fail '下载安装程序失败。' bash "$work_dir/claude-install.sh" || fail 'Claude Code 安装失败;原配置未修改。' fi command -v claude >/dev/null 2>&1 || [ -x "$HOME/.local/bin/claude" ] || fail '未找到 Claude Code,原配置未修改。' mkdir -p "$config_dir" backup='' if [ -f "$settings" ]; then backup=$(mktemp "$config_dir/settings.json.tokenjoy-backup.XXXXXX") cp "$settings" "$backup" chmod 600 "$backup" fi staged=$(mktemp "$config_dir/.tokenjoy-settings.XXXXXX") cp "$work_dir/settings.json" "$staged" chmod 600 "$staged" mv -f "$staged" "$settings" if ! command -v claude >/dev/null 2>&1; then case "${SHELL:-/bin/zsh}" in */zsh) profile="$HOME/.zshrc" ;; */bash) profile="$HOME/.bash_profile" ;; *) profile='' ;; esac if [ -n "$profile" ] && [ ! -L "$profile" ]; then if [ -f "$profile" ]; then profile_backup=$(mktemp "$profile.tokenjoy-backup.XXXXXX") cp "$profile" "$profile_backup" chmod 600 "$profile_backup" fi printf '\n# TokenJoy: Claude Code native install\nexport PATH="$HOME/.local/bin:$PATH"\n' >>"$profile" else printf '请将 ~/.local/bin 加入 PATH,或直接运行 ~/.local/bin/claude。\n' fi fi printf '\n配置完成:%s\n' "$settings" if [ -n "$backup" ]; then printf '原配置备份:%s\n' "$backup"; fi printf '重新打开终端,进入项目目录后运行 claude。\n使用 /status 检查 TokenJoy 地址,再发送一条消息验证模型调用。\n如果使用了专属安装文件,请在成功后删除该文件。\n' } tokenjoy_main "$@"