feat: split ClaudeAlt into independent package

This commit is contained in:
Александр
2026-07-25 01:18:17 +10:00
parent 1801be3549
commit f3af93802f
14 changed files with 361 additions and 256 deletions
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
set -euo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
exec "${root}/tools/package_updates.sh" check claude-alt
+134
View File
@@ -0,0 +1,134 @@
name='claude-alt'
version=1.24012.0
release=1
summary='Independent secondary Claude desktop application'
summary_ru='Независимое второе приложение Claude'
group='Development/Tools'
desc='A fully independent secondary Claude Desktop installation with its own runtime, profile, desktop identity and Nivora icons.'
desc_ru='Полностью независимая вторая установка Claude Desktop с собственным runtime, профилем, desktop-идентичностью и иконками Nivora.'
homepage='https://github.com/Cheviiot/Nivora'
maintainer='Nivora <https://github.com/Cheviiot/Nivora>'
architectures=('amd64' 'arm64')
license=('Custom')
nonfree=1
nonfree_msg='ClaudeAlt использует проприетарный Claude Desktop и условия Anthropic.'
nonfree_url='https://www.anthropic.com/legal/consumer-terms'
provides=()
replaces=('claude-alt')
conflicts=()
auto_reqprov_method="dirty"
auto_req=0
auto_prov=0
deps=()
deps_debian=('libgtk-3-0' 'libnotify4' 'libnss3' 'xdg-utils' 'libatspi2.0-0' 'libdrm2' 'libgbm1' 'libxcb-dri3-0' 'libsecret-1-0' 'libc6' 'libxtst6' 'libuuid1' 'xdg-desktop-portal')
deps_ubuntu=("${deps_debian[@]}")
deps_fedora=('gtk3' 'libnotify' 'nss' 'xdg-utils' 'at-spi2-core' 'libdrm' 'mesa-libgbm' 'libxcb' 'libsecret' 'glibc' 'libXtst' 'libuuid' 'xdg-desktop-portal')
deps_arch=('gtk3' 'libnotify' 'nss' 'xdg-utils' 'at-spi2-core' 'libdrm' 'libgbm' 'libxcb' 'libsecret' 'glibc' 'libxtst' 'libutil-linux' 'xdg-desktop-portal')
deps_opensuse=('gtk3' 'libnotify4' 'mozilla-nss' 'xdg-utils' 'at-spi2-core' 'libdrm2' 'libgbm1' 'libxcb-dri3-0' 'libsecret-1-0' 'glibc' 'libXtst6' 'libuuid1' 'xdg-desktop-portal')
deps_altlinux=('libgtk+3' 'libnotify' 'libnss' 'xdg-utils' 'libat-spi2-core' 'libdrm' 'libgbm' 'libxcb' 'libsecret' 'glibc' 'libXtst' 'libuuid' 'xdg-desktop-portal')
build_deps=('binutils')
build_deps_debian=('binutils' 'python3')
build_deps_ubuntu=("${build_deps_debian[@]}")
build_deps_fedora=('binutils' 'python3')
build_deps_arch=('binutils' 'python')
build_deps_opensuse=('binutils' 'python3')
build_deps_altlinux=('binutils' 'python3')
sources=(
"https://downloads.claude.ai/claude-desktop/apt/stable/pool/main/c/claude-desktop/claude-desktop_${version}_amd64.deb?~archive=false&~name=claude-desktop.deb"
"local:///claude-alt"
"https://raw.githubusercontent.com/Cheviiot/Nivora/main/claude-desktop/claude-alt.png?~archive=false&~name=claude-alt.png"
"https://raw.githubusercontent.com/Cheviiot/Nivora/main/claude-desktop/claude-alt-tray-turquoise.png?~archive=false&~name=claude-alt-tray.png"
"local:///patch-asar.py"
)
sources_arm64=(
"https://downloads.claude.ai/claude-desktop/apt/stable/pool/main/c/claude-desktop/claude-desktop_${version}_arm64.deb?~archive=false&~name=claude-desktop.deb"
"local:///claude-alt"
"https://raw.githubusercontent.com/Cheviiot/Nivora/main/claude-desktop/claude-alt.png?~archive=false&~name=claude-alt.png"
"https://raw.githubusercontent.com/Cheviiot/Nivora/main/claude-desktop/claude-alt-tray-turquoise.png?~archive=false&~name=claude-alt-tray.png"
"local:///patch-asar.py"
)
checksums=(
'sha256:1096a8063956f8430fdbe504bb1c5e02985f31d0f45d082e4966e5d30ea7e3ac'
'sha256:b5264918d7cc3436bde057057e5790f617e42ccfcd1237d9170d26aea12799b8'
'sha256:f23de36a74c3f6ddae92511fc0dc659816853bf1d28520a6e0232fe2606fd0b4'
'sha256:21d1db80c30921346252ed405fa36cb5c48c287afdd8ebe474073389fd88e85e'
'sha256:14e0a5ce4da6afb25b13b3cee0b0264fbd896430dae85ade1f3394d8ca999e7d'
)
checksums_arm64=(
'sha256:37dd5dc1b0bfcb3192561acbd33cb59037806fe9b95221a3ddf38244203196f1'
'sha256:b5264918d7cc3436bde057057e5790f617e42ccfcd1237d9170d26aea12799b8'
'sha256:f23de36a74c3f6ddae92511fc0dc659816853bf1d28520a6e0232fe2606fd0b4'
'sha256:21d1db80c30921346252ed405fa36cb5c48c287afdd8ebe474073389fd88e85e'
'sha256:14e0a5ce4da6afb25b13b3cee0b0264fbd896430dae85ade1f3394d8ca999e7d'
)
scripts=(
['postinstall']='postinstall.sh'
['postremove']='postremove.sh'
)
package() {
cd "${srcdir}"
mkdir -p extracted
ar x claude-desktop.deb
tar -xf data.tar.* -C extracted
install -dm755 "${pkgdir}/usr/lib"
cp -a extracted/usr/lib/claude-desktop "${pkgdir}/usr/lib/claude-alt"
mv "${pkgdir}/usr/lib/claude-alt/claude-desktop" \
"${pkgdir}/usr/lib/claude-alt/claude-alt-bin"
chmod 0755 "${pkgdir}/usr/lib/claude-alt/claude-alt-bin" \
"${pkgdir}/usr/lib/claude-alt/chrome-sandbox"
python3 patch-asar.py "${pkgdir}/usr/lib/claude-alt/resources/app.asar"
install -Dm755 claude-alt "${pkgdir}/usr/lib/claude-alt/claude-alt"
install -Dm644 claude-alt.png "${pkgdir}/usr/lib/claude-alt/resources/icon.png"
for icon in TrayIconLinux.png TrayIconLinux-Dark.png; do
install -Dm644 claude-alt-tray.png \
"${pkgdir}/usr/lib/claude-alt/resources/${icon}"
done
install -dm755 "${pkgdir}/usr/bin"
ln -s /usr/lib/claude-alt/claude-alt "${pkgdir}/usr/bin/claude-alt"
ln -s claude-alt "${pkgdir}/usr/bin/claude-code-alt"
install -Dm644 claude-alt.png \
"${pkgdir}/usr/share/icons/hicolor/512x512/apps/claude-alt.png"
install -Dm644 /dev/stdin \
"${pkgdir}/usr/share/applications/com.anthropic.ClaudeAlt.desktop" <<'EOF'
[Desktop Entry]
Name=ClaudeAlt
Name[ru]=ClaudeAlt
Comment=Independent secondary Claude desktop application
Comment[ru]=Независимое второе приложение Claude
GenericName=AI Assistant
Keywords=AI;Chat;Assistant;Claude;Code;LLM;
Exec=/usr/bin/claude-alt %U
Icon=claude-alt
Type=Application
StartupNotify=true
StartupWMClass=com.anthropic.ClaudeAlt
SingleMainWindow=true
Categories=Development;
Actions=NewChat;NewCode;
[Desktop Action NewChat]
Name=New chat
Name[ru]=Новый чат
Exec=/usr/bin/claude-alt claude://claude.ai/new
[Desktop Action NewCode]
Name=New Claude Code session
Name[ru]=Новая сессия Claude Code
Exec=/usr/bin/claude-alt claude://code/new
EOF
install-license extracted/usr/share/doc/claude-desktop/copyright \
"claude-alt/copyright"
}
files() {
files-find "/usr/lib/claude-alt/**/*"
files-find-binary
files-find-desktop
files-find-license
files-find "/usr/share/icons/hicolor/*/apps/claude-alt.png"
}
+36
View File
@@ -0,0 +1,36 @@
#!/bin/sh
set -eu
if [ -z "${HOME:-}" ] && [ -z "${XDG_CONFIG_HOME:-}" ]; then
echo "claude-alt: HOME or XDG_CONFIG_HOME is required" >&2
exit 1
fi
config_home="${XDG_CONFIG_HOME:-${HOME}/.config}"
default_data_dir="${config_home}/ClaudeAlt"
legacy_data_dir="${CLAUDE_DESKTOP_ACCOUNT2_DIR:-${config_home}/Claude-Account-2}"
claude_alt_executable="${CLAUDE_ALT_EXECUTABLE:-/usr/lib/claude-alt/claude-alt-bin}"
if [ -n "${CLAUDE_ALT_DATA_DIR:-}" ]; then
data_dir="${CLAUDE_ALT_DATA_DIR}"
elif [ -d "${default_data_dir}" ]; then
data_dir="${default_data_dir}"
elif [ -d "${legacy_data_dir}" ]; then
data_dir="${legacy_data_dir}"
else
data_dir="${default_data_dir}"
fi
if [ ! -x "${claude_alt_executable}" ]; then
echo "claude-alt: executable not found: ${claude_alt_executable}" >&2
exit 1
fi
mkdir -p "${data_dir}"
CHROME_DESKTOP='com.anthropic.ClaudeAlt.desktop'
export CHROME_DESKTOP
exec "${claude_alt_executable}" \
--user-data-dir="${data_dir}" \
--class=com.anthropic.ClaudeAlt \
"$@"
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""Give ClaudeAlt an independent Electron identity without changing its protocol."""
import copy
import hashlib
import json
import struct
import sys
from pathlib import Path
path = Path(sys.argv[1])
data = path.read_bytes()
if len(data) < 16:
raise SystemExit("ClaudeAlt app.asar is too small")
_, _, _, header_size = struct.unpack("<IIII", data[:16])
header = json.loads(data[16 : 16 + header_size])
base_offset = 16 + ((header_size + 3) & ~3)
output = bytearray()
counts = {"package": 0, "socket": 0, "user_agent": 0}
def integrity(content: bytes, block_size: int) -> dict:
blocks = [
hashlib.sha256(content[index : index + block_size]).hexdigest()
for index in range(0, len(content), block_size)
] or [hashlib.sha256(b"").hexdigest()]
return {
"algorithm": "SHA256",
"hash": hashlib.sha256(content).hexdigest(),
"blockSize": block_size,
"blocks": blocks,
}
def rebuild(node: dict, prefix: str = "") -> dict:
result = {}
for name, child in node.get("files", {}).items():
item_path = f"{prefix}/{name}" if prefix else name
if "files" in child:
result[name] = {
**{key: value for key, value in child.items() if key != "files"},
"files": rebuild(child, item_path),
}
continue
entry = copy.deepcopy(child)
if entry.get("unpacked"):
result[name] = entry
continue
offset, size = int(entry["offset"]), int(entry["size"])
content = data[base_offset + offset : base_offset + offset + size]
changed = False
if item_path == "package.json":
package = json.loads(content)
expected = {
"productName": "Claude",
"desktopName": "com.anthropic.Claude.desktop",
}
for field, value in expected.items():
if package.get(field) != value:
raise SystemExit(
f"unexpected Claude package.json {field}: "
f"{package.get(field)!r} != {value!r}"
)
package["productName"] = "ClaudeAlt"
package["desktopName"] = "com.anthropic.ClaudeAlt.desktop"
content = (json.dumps(package, indent=2) + "\n").encode()
counts["package"] += 1
changed = True
old_socket = b"claude-cowork-vm.sock"
socket_count = content.count(old_socket)
if socket_count:
content = content.replace(old_socket, b"claude-alt-cowork-vm.sock")
counts["socket"] += socket_count
changed = True
marker = b'P.app.isPackaged||P.app.setName("Claude");RRn();'
marker_count = content.count(marker)
if marker_count:
content = content.replace(
marker,
b'P.app.isPackaged||P.app.setName("Claude");'
b'P.app.userAgentFallback=P.app.userAgentFallback'
b'.replaceAll("ClaudeAlt","Claude");RRn();',
)
counts["user_agent"] += marker_count
changed = True
entry["offset"] = str(len(output))
if changed:
entry["size"] = len(content)
block_size = entry.get("integrity", {}).get("blockSize", 4194304)
entry["integrity"] = integrity(content, block_size)
output.extend(content)
result[name] = entry
return result
rebuilt_header = {"files": rebuild(header)}
if counts != {"package": 1, "socket": 1, "user_agent": 1}:
raise SystemExit(f"unexpected ClaudeAlt patch counts: {counts}")
header_bytes = json.dumps(rebuilt_header, separators=(",", ":")).encode()
padded_size = (len(header_bytes) + 3) & ~3
patched = (
struct.pack("<IIII", 4, padded_size + 8, padded_size + 4, len(header_bytes))
+ header_bytes
+ b"\0" * (padded_size - len(header_bytes))
+ bytes(output)
)
required = (
b"com.anthropic.ClaudeAlt.desktop",
b'"productName": "ClaudeAlt"',
b'.replaceAll("ClaudeAlt","Claude")',
b"claude-alt-cowork-vm.sock",
)
if any(patched.count(value) != 1 for value in required):
raise SystemExit("ClaudeAlt identity verification failed")
path.write_bytes(patched)
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
set -euo pipefail
sandbox=/usr/lib/claude-alt/chrome-sandbox
if [[ ! -f "$sandbox" ]]; then
echo "Ошибка: не найден Electron sandbox: ${sandbox}" >&2
exit 1
fi
if [[ -L /proc/self/ns/user ]] && command -v unshare >/dev/null 2>&1 \
&& unshare --user true 2>/dev/null; then
chmod 0755 "$sandbox"
else
chmod 4755 "$sandbox"
fi
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database -q /usr/share/applications || true
fi
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache -f -q /usr/share/icons/hicolor || true
fi
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
set -euo pipefail
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database -q /usr/share/applications || true
fi
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache -f -q /usr/share/icons/hicolor || true
fi
+1
View File
@@ -0,0 +1 @@
include = "../stapler-repo.toml"