@sanohiro/casty 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hironobu Sano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ja.md ADDED
@@ -0,0 +1,178 @@
1
+ # casty
2
+
3
+ Kitty graphics protocol を使った TTY Web ブラウザ。
4
+
5
+ **[English](README.md)**
6
+
7
+ ヘッドレス Chrome のレンダリングを Kitty 対応ターミナルに表示し、ターミナル上で完全な Web ブラウジングを実現します。
8
+
9
+ <video src="https://github.com/user-attachments/assets/330bf0c3-dd08-44a5-b627-b90c200d57fe" autoplay loop muted playsinline></video>
10
+
11
+ ```
12
+ Chrome (Headless Shell) casty Terminal
13
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
14
+ │ Web レンダリング │ ───→ │ 高解像度 │ ─→│ Kitty graphics │
15
+ │ JS 実行 │ │ キャプチャ │ │ 画面表示 │
16
+ │ フルブラウザ │ ←─── │ 入力ブリッジ │ ←─│ マウス/キーボード│
17
+ └─────────────────┘ └─────────────────┘ └─────────────────┘
18
+ ```
19
+
20
+ ## 機能
21
+
22
+ - ヘッドレス Chrome によるフル Web レンダリング (生 CDP、Playwright 不使用)
23
+ - ボット検出回避のステルスパッチ (Google ログイン可能)
24
+ - Kitty graphics protocol による画像表示
25
+ - マウス操作 (クリック、スクロール、ドラッグ)
26
+ - キーボード入力を Chrome にパススルー
27
+ - Vimium 風ヒントモード (Alt+F) でキーボードナビゲーション
28
+ - アドレスバー + 検索 (Alt+L)
29
+ - ブックマーク (アドレスバーで `/b` 検索)
30
+ - テキスト選択コピー / クリップボードペースト
31
+ - ターミナルのフォントサイズに基づく自動ズーム
32
+ - 動的リサイズ (SIGWINCH)
33
+ - キーバインド設定 (`~/.casty/keys.json`)
34
+ - 各種設定 (`~/.casty/config.json`)
35
+ - ファイルダウンロード (`~/Downloads/` に保存)
36
+ - ローディングインジケーター
37
+ - プロファイル自動クリーンアップによる高速起動
38
+
39
+ ## 必要環境
40
+
41
+ - **Kitty graphics protocol** 対応ターミナル
42
+ - Node.js >= 22
43
+
44
+ 動作確認済み: **bcon**, **Ghostty**, **kitty**
45
+
46
+ ## インストール
47
+
48
+ ```bash
49
+ npm install -g @sanohiro/casty
50
+ casty
51
+ ```
52
+
53
+ ソースからインストールする場合:
54
+
55
+ ```bash
56
+ git clone https://github.com/sanohiro/casty.git
57
+ cd casty
58
+ npm install
59
+ ./bin/casty
60
+ ```
61
+
62
+ 初回起動時に Chrome Headless Shell が `~/.casty/browsers/` に自動インストールされます。以降の起動時にバックグラウンドで更新チェックし、常に一世代だけ保持します。
63
+
64
+ ## 使い方
65
+
66
+ ```bash
67
+ casty https://google.com
68
+ casty https://youtube.com
69
+ casty # ホームページを開く (デフォルト: casty GitHub ページ)
70
+ ```
71
+
72
+ ### キーバインド
73
+
74
+ | キー | アクション |
75
+ |------|-----------|
76
+ | Alt+L | アドレスバーを開く |
77
+ | Alt+F | ヒントモード (Vimium 風リンク/ボタン選択) |
78
+ | Alt+Left | 戻る |
79
+ | Alt+Right | 進む |
80
+ | Alt+C | 選択テキストをコピー |
81
+ | Ctrl+V | クリップボードからペースト |
82
+ | Ctrl+Q | 終了 |
83
+ | Ctrl+C | 終了 (フォールバック) |
84
+
85
+ `~/.casty/keys.json` でカスタマイズ可能 (ファイルは自動生成されません):
86
+
87
+ ```json
88
+ {
89
+ "ctrl+q": "quit",
90
+ "alt+left": "back",
91
+ "alt+right": "forward",
92
+ "alt+l": "url_bar",
93
+ "alt+f": "hints",
94
+ "alt+c": "copy",
95
+ "ctrl+v": "paste"
96
+ }
97
+ ```
98
+
99
+ ### アドレスバー
100
+
101
+ - **Alt+L** または1行目クリックでフォーカス — URL が全選択状態になる
102
+ - **Enter** で移動 (URL) または検索 (Google)
103
+ - **`/b クエリ`** でブックマーク検索
104
+ - **Escape** でキャンセル
105
+ - **Ctrl+A** 全選択、**Ctrl+U** 全消去、**Ctrl+W** 単語削除
106
+
107
+ ### ヒントモード
108
+
109
+ **Alt+F** でクリック可能/フォーカス可能な要素にラベルを表示。ラベルの文字を入力するとリンク/ボタンのクリックや入力欄へのフォーカスができます。**Escape** でキャンセル。
110
+
111
+ ラベルはホームロウキー (`a`, `s`, `d`, `f`, `j`, `k`, `l`) を使用 — 7個以下なら1文字、それ以上は2文字 (最大49個)。
112
+
113
+ ### ブックマーク
114
+
115
+ `~/.casty/bookmarks.json` を手動で作成:
116
+
117
+ ```json
118
+ {
119
+ "GitHub": "https://github.com",
120
+ "Google": "https://google.com",
121
+ "YouTube": "https://youtube.com"
122
+ }
123
+ ```
124
+
125
+ アドレスバーで `/b クエリ` と入力して検索 (名前・URL の部分一致、大文字小文字無視)。
126
+
127
+ ### 設定
128
+
129
+ `~/.casty/config.json` でカスタマイズ可能 (ファイルは自動生成されません):
130
+
131
+ ```json
132
+ {
133
+ "homeUrl": "https://github.com/sanohiro/casty",
134
+ "searchUrl": "https://www.google.com/search?q=",
135
+ "transport": "auto",
136
+ "format": "auto"
137
+ }
138
+ ```
139
+
140
+ | キー | 説明 | デフォルト |
141
+ |------|------|-----------|
142
+ | `homeUrl` | URL 未指定時に開くページ | `https://github.com/sanohiro/casty` |
143
+ | `searchUrl` | 検索エンジン URL (クエリが末尾に付加される) | `https://www.google.com/search?q=` |
144
+ | `transport` | Kitty 画像転送方式: `auto`, `file`, `inline` | `auto` (bcon→file、他→inline) |
145
+ | `format` | スクリーンショット形式: `auto`, `png`, `jpeg` | `auto` (file→jpeg、inline→png) |
146
+
147
+ ## アーキテクチャ
148
+
149
+ ```
150
+ bin/
151
+ casty # シェルラッパー (Chrome インストール/更新)
152
+ casty.js # エントリポイント (ターミナル検出、ズーム、リサイズ)
153
+ lib/
154
+ browser.js # CDP ブラウザ制御 (起動、Screencast、キャプチャ)
155
+ cdp.js # 軽量 CDP WebSocket クライアント
156
+ chrome.js # Chrome バイナリ検出、起動、プロファイルクリーンアップ
157
+ kitty.js # Kitty graphics protocol 出力 (file/inline)
158
+ input.js # マウス/キーボード処理、アクション
159
+ hints.js # Vimium 風ヒントモード
160
+ urlbar.js # アドレス/検索バー
161
+ bookmarks.js # ブックマーク検索
162
+ keys.js # キーバインド設定
163
+ config.js # ユーザー設定
164
+ ```
165
+
166
+ ## 仕組み
167
+
168
+ 1. 生 CDP で Chrome Headless Shell を起動 (Playwright 不使用、`Runtime.enable` も送信しない)
169
+ 2. ページロード前にステルスパッチを注入してボット検出を回避
170
+ 3. ハイブリッドフレーム取得: 低解像度 Screencast を変更検知トリガーとして使い、`Page.captureScreenshot` で高解像度フレームを取得
171
+ 4. Kitty graphics protocol でフレームをターミナルに描画
172
+ 5. ターミナル入力 (raw mode) をキャプチャし、CDP 経由で Chrome に送信
173
+ 6. CSI 14t でターミナルのピクセルサイズを自動検出し、ズームを計算
174
+ 7. 起動時にプロファイルをクリーンアップ (Cookie/LocalStorage を保持、キャッシュ類を削除) して高速起動を維持
175
+
176
+ ## ライセンス
177
+
178
+ MIT
package/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # casty
2
+
3
+ A TTY web browser powered by Kitty graphics protocol.
4
+
5
+ **[日本語](README.ja.md)**
6
+
7
+ casty renders full web pages in your terminal using Chrome's headless rendering, bridging the gap between a headless browser and your Kitty-compatible terminal.
8
+
9
+ <video src="https://github.com/user-attachments/assets/330bf0c3-dd08-44a5-b627-b90c200d57fe" autoplay loop muted playsinline></video>
10
+
11
+ ```
12
+ Chrome (Headless Shell) casty Terminal
13
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
14
+ │ Web rendering │ ───→ │ High-res │ ─→│ Kitty graphics │
15
+ │ JS execution │ │ capture │ │ display │
16
+ │ Full browser │ ←─── │ Input bridge │ ←─│ Mouse/Keyboard │
17
+ └─────────────────┘ └─────────────────┘ └─────────────────┘
18
+ ```
19
+
20
+ ## Features
21
+
22
+ - Full web rendering via headless Chrome (raw CDP, no Playwright)
23
+ - Stealth patches to avoid bot detection (Google login works)
24
+ - Kitty graphics protocol for image display
25
+ - Mouse support (click, scroll, drag)
26
+ - Keyboard passthrough to Chrome
27
+ - Vimium-style hint mode (Alt+F) for keyboard navigation
28
+ - Address bar with search (Alt+L)
29
+ - Bookmarks (`/b` search in address bar)
30
+ - Copy selected text / paste from clipboard
31
+ - Auto-zoom based on terminal font size
32
+ - Dynamic resize (SIGWINCH)
33
+ - Configurable keybindings (`~/.casty/keys.json`)
34
+ - Configurable settings (`~/.casty/config.json`)
35
+ - File downloads to `~/Downloads/`
36
+ - Loading indicator
37
+ - Fast startup with automatic profile cleanup
38
+
39
+ ## Requirements
40
+
41
+ - **Kitty graphics protocol** compatible terminal
42
+ - Node.js >= 22
43
+
44
+ Tested on: **bcon**, **Ghostty**, **kitty**
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ npm install -g @sanohiro/casty
50
+ casty
51
+ ```
52
+
53
+ Or install from source:
54
+
55
+ ```bash
56
+ git clone https://github.com/sanohiro/casty.git
57
+ cd casty
58
+ npm install
59
+ ./bin/casty
60
+ ```
61
+
62
+ Chrome Headless Shell is automatically installed to `~/.casty/browsers/` on first run and kept up to date on subsequent launches. Only one version is kept at a time.
63
+
64
+ ## Usage
65
+
66
+ ```bash
67
+ casty https://google.com
68
+ casty https://youtube.com
69
+ casty # opens home page (default: casty GitHub page)
70
+ ```
71
+
72
+ ### Keybindings
73
+
74
+ | Key | Action |
75
+ |-----|--------|
76
+ | Alt+L | Open address bar |
77
+ | Alt+F | Hint mode (Vimium-style link/button selection) |
78
+ | Alt+Left | Back |
79
+ | Alt+Right | Forward |
80
+ | Alt+C | Copy selected text |
81
+ | Ctrl+V | Paste from clipboard |
82
+ | Ctrl+Q | Quit |
83
+ | Ctrl+C | Quit (fallback) |
84
+
85
+ Customize via `~/.casty/keys.json` (file is not created automatically):
86
+
87
+ ```json
88
+ {
89
+ "ctrl+q": "quit",
90
+ "alt+left": "back",
91
+ "alt+right": "forward",
92
+ "alt+l": "url_bar",
93
+ "alt+f": "hints",
94
+ "alt+c": "copy",
95
+ "ctrl+v": "paste"
96
+ }
97
+ ```
98
+
99
+ ### Address Bar
100
+
101
+ - **Alt+L** or click row 1 to focus — URL is selected, type to replace
102
+ - **Enter** to navigate (URLs) or search (Google)
103
+ - **`/b query`** to search bookmarks
104
+ - **Escape** to cancel
105
+ - **Ctrl+A** select all, **Ctrl+U** clear, **Ctrl+W** delete word
106
+
107
+ ### Hint Mode
108
+
109
+ Press **Alt+F** to show labels on clickable and focusable elements. Type the label characters to click a link/button or focus an input field. Press **Escape** to cancel.
110
+
111
+ Labels use home-row keys (`a`, `s`, `d`, `f`, `j`, `k`, `l`) — single character for ≤7 elements, two characters for more (up to 49).
112
+
113
+ ### Bookmarks
114
+
115
+ Create `~/.casty/bookmarks.json` manually:
116
+
117
+ ```json
118
+ {
119
+ "GitHub": "https://github.com",
120
+ "Google": "https://google.com",
121
+ "YouTube": "https://youtube.com"
122
+ }
123
+ ```
124
+
125
+ Search from the address bar with `/b query` (matches name or URL, case-insensitive).
126
+
127
+ ### Configuration
128
+
129
+ Customize via `~/.casty/config.json` (file is not created automatically):
130
+
131
+ ```json
132
+ {
133
+ "homeUrl": "https://github.com/sanohiro/casty",
134
+ "searchUrl": "https://www.google.com/search?q=",
135
+ "transport": "auto",
136
+ "format": "auto"
137
+ }
138
+ ```
139
+
140
+ | Key | Description | Default |
141
+ |-----|-------------|---------|
142
+ | `homeUrl` | Page opened when no URL is given | `https://github.com/sanohiro/casty` |
143
+ | `searchUrl` | Search engine URL (query appended) | `https://www.google.com/search?q=` |
144
+ | `transport` | Kitty image transfer: `auto`, `file`, or `inline` | `auto` (bcon→file, others→inline) |
145
+ | `format` | Screenshot format: `auto`, `png`, or `jpeg` | `auto` (file→jpeg, inline→png) |
146
+
147
+ ## Architecture
148
+
149
+ ```
150
+ bin/
151
+ casty # Shell wrapper (Chrome install/update)
152
+ casty.js # Entry point (terminal detection, zoom, resize)
153
+ lib/
154
+ browser.js # CDP browser control (launch, screencast, capture)
155
+ cdp.js # Lightweight CDP WebSocket client
156
+ chrome.js # Chrome binary detection, launch, profile cleanup
157
+ kitty.js # Kitty graphics protocol output (file/inline)
158
+ input.js # Mouse/keyboard handling, actions
159
+ hints.js # Vimium-style hint mode
160
+ urlbar.js # Address/search bar
161
+ bookmarks.js # Bookmark search
162
+ keys.js # Configurable keybindings
163
+ config.js # User configuration
164
+ ```
165
+
166
+ ## How It Works
167
+
168
+ 1. Launches Chrome Headless Shell via raw CDP (no Playwright, no `Runtime.enable`)
169
+ 2. Injects stealth patches before page load to avoid bot detection
170
+ 3. Uses hybrid frame capture: low-res Screencast as change detection trigger, `Page.captureScreenshot` for high-res frames
171
+ 4. Renders frames to terminal via Kitty graphics protocol
172
+ 5. Captures terminal input (raw mode) and dispatches to Chrome via CDP
173
+ 6. Auto-detects terminal pixel size (CSI 14t) for zoom calculation
174
+ 7. Cleans up profile on startup (keeps cookies/storage, removes caches) for fast launch
175
+
176
+ ## License
177
+
178
+ MIT
package/bin/casty ADDED
@@ -0,0 +1,200 @@
1
+ #!/bin/bash
2
+ # casty - TTY web browser
3
+ # シェルラッパー: Chrome for Testing 管理 + 本体起動
4
+
5
+ # シンボリックリンクを解決して実際のパスを取得
6
+ SOURCE="${BASH_SOURCE[0]}"
7
+ while [ -L "$SOURCE" ]; do
8
+ DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
9
+ SOURCE="$(readlink "$SOURCE")"
10
+ [[ "$SOURCE" != /* ]] && SOURCE="$DIR/$SOURCE"
11
+ done
12
+ CASTY_DIR="$(cd -P "$(dirname "$SOURCE")/.." && pwd)"
13
+
14
+ BROWSERS_DIR="$HOME/.casty/browsers"
15
+ STAMP="$HOME/.casty/.update-check"
16
+
17
+ # プラットフォーム自動検出
18
+ detect_platform() {
19
+ local os=$(uname -s)
20
+ local arch=$(uname -m)
21
+ case "$os" in
22
+ Linux)
23
+ case "$arch" in
24
+ aarch64|arm64) echo "linux-arm64" ;; # Chrome for Testing にはないが検出用
25
+ *) echo "linux64" ;;
26
+ esac ;;
27
+ Darwin)
28
+ case "$arch" in
29
+ arm64) echo "mac-arm64" ;;
30
+ *) echo "mac-x64" ;;
31
+ esac ;;
32
+ *) echo "linux64" ;;
33
+ esac
34
+ }
35
+
36
+ # ARM64 Linux か判定 (Chrome for Testing にはバイナリがない)
37
+ is_arm64_linux() {
38
+ [ "$(detect_platform)" = "linux-arm64" ]
39
+ }
40
+
41
+ # Playwright 経由で chromium-headless-shell をインストール (ARM64 Linux 用)
42
+ install_chromium_playwright() {
43
+ echo "casty: Installing Chrome Headless Shell via Playwright..." >&2
44
+ if PLAYWRIGHT_BROWSERS_PATH="$BROWSERS_DIR" npx -y playwright install chromium-headless-shell 2>&1 >&2; then
45
+ touch "$STAMP"
46
+ # 古いバージョンを削除 (最新1つだけ残す)
47
+ local dirs=$(ls -t -d "$BROWSERS_DIR"/chromium_headless_shell-*/ 2>/dev/null)
48
+ local count=$(echo "$dirs" | wc -l)
49
+ if [ "$count" -gt 1 ]; then
50
+ echo "$dirs" | tail -n +2 | while read -r d; do rm -rf "$d"; done
51
+ fi
52
+ # Playwright が入れる ffmpeg も古いのを削除
53
+ local ffdirs=$(ls -t -d "$BROWSERS_DIR"/ffmpeg-*/ 2>/dev/null)
54
+ local ffcount=$(echo "$ffdirs" | wc -l)
55
+ if [ "$ffcount" -gt 1 ]; then
56
+ echo "$ffdirs" | tail -n +2 | while read -r d; do rm -rf "$d"; done
57
+ fi
58
+ fi
59
+ }
60
+
61
+ # Chrome for Testing API から chrome-headless-shell をダウンロード・インストール
62
+ install_chromium() {
63
+ local platform=$(detect_platform)
64
+ local json_url="https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json"
65
+
66
+ # JSON API から最新バージョンの URL を取得
67
+ local json=$(curl -fsSL "$json_url" 2>/dev/null)
68
+ if [ -z "$json" ]; then
69
+ echo "casty: Failed to fetch Chrome version info" >&2
70
+ return 1
71
+ fi
72
+
73
+ # jq がなくても動くよう node で JSON パース
74
+ local download_url=$(node -e "
75
+ const d = JSON.parse(process.argv[1]);
76
+ const hs = d.channels.Stable.downloads['chrome-headless-shell'];
77
+ const e = hs && hs.find(x => x.platform === '$platform');
78
+ if (e) console.log(e.url);
79
+ " "$json" 2>/dev/null)
80
+
81
+ if [ -z "$download_url" ]; then
82
+ echo "casty: No chrome-headless-shell for $platform" >&2
83
+ return 1
84
+ fi
85
+
86
+ # バージョン番号を抽出
87
+ local version=$(node -e "
88
+ const d = JSON.parse(process.argv[1]);
89
+ console.log(d.channels.Stable.version);
90
+ " "$json" 2>/dev/null)
91
+
92
+ local dest_dir="$BROWSERS_DIR/chrome-headless-shell-${version}"
93
+
94
+ # 既にインストール済みならスキップ
95
+ if [ -f "$dest_dir/chrome-headless-shell" ]; then
96
+ touch "$STAMP"
97
+ return 0
98
+ fi
99
+
100
+ echo "casty: Installing Chrome Headless Shell ${version}..." >&2
101
+
102
+ local tmp_zip=$(mktemp /tmp/casty-chrome-XXXXXX.zip)
103
+ # プログレスバー付きダウンロード (TTY ならバー表示、パイプなら非表示)
104
+ if [ -t 2 ]; then
105
+ curl -fL --progress-bar "$download_url" -o "$tmp_zip" 2>&1 >&2
106
+ else
107
+ curl -fsSL "$download_url" -o "$tmp_zip" 2>/dev/null
108
+ fi
109
+ if [ $? -ne 0 ]; then
110
+ rm -f "$tmp_zip"
111
+ echo "casty: Download failed" >&2
112
+ return 1
113
+ fi
114
+
115
+ mkdir -p "$BROWSERS_DIR"
116
+ # zip 内は chrome-headless-shell-<platform>/ ディレクトリに入っている
117
+ local tmp_dir=$(mktemp -d /tmp/casty-extract-XXXXXX)
118
+ unzip -q "$tmp_zip" -d "$tmp_dir" 2>/dev/null
119
+ rm -f "$tmp_zip"
120
+
121
+ # 展開されたディレクトリを見つけてリネーム
122
+ local extracted=$(ls -d "$tmp_dir"/chrome-headless-shell-* 2>/dev/null | head -1)
123
+ if [ -z "$extracted" ] || [ ! -f "$extracted/chrome-headless-shell" ]; then
124
+ rm -rf "$tmp_dir"
125
+ echo "casty: Extraction failed" >&2
126
+ return 1
127
+ fi
128
+
129
+ mv "$extracted" "$dest_dir"
130
+ rm -rf "$tmp_dir"
131
+ chmod +x "$dest_dir/chrome-headless-shell"
132
+
133
+ touch "$STAMP"
134
+ echo "casty: Installed Chrome Headless Shell ${version}" >&2
135
+ }
136
+
137
+ # 古いブラウザバージョンを削除 (最新1つだけ残す)
138
+ cleanup_old_browsers() {
139
+ local count=$(ls -d "$BROWSERS_DIR"/chrome-headless-shell-*/ 2>/dev/null | wc -l)
140
+ [ "$count" -le 1 ] && return
141
+ # 最も古いディレクトリを削除
142
+ local oldest=$(ls -t -d "$BROWSERS_DIR"/chrome-headless-shell-*/ 2>/dev/null | tail -1)
143
+ [ -n "$oldest" ] && rm -rf "$oldest"
144
+ }
145
+
146
+ # 初回: ブラウザがなければインストール (ブロッキング)
147
+ has_chrome() {
148
+ # Chrome for Testing 形式 (ARM64 Linux はスキップ — x86_64 バイナリなので動かない)
149
+ if ! is_arm64_linux; then
150
+ ls "$BROWSERS_DIR"/chrome-headless-shell-*/chrome-headless-shell &>/dev/null && return 0
151
+ fi
152
+ # Playwright 形式 — 再帰的に探索 (バイナリ名: chrome-headless-shell or headless_shell)
153
+ find "$BROWSERS_DIR"/chromium_headless_shell-* \( -name 'chrome-headless-shell' -o -name 'headless_shell' \) -type f 2>/dev/null | grep -q . && return 0
154
+ return 1
155
+ }
156
+
157
+ # システム Chrome/Chromium があるか
158
+ has_system_chrome() {
159
+ command -v chromium-browser >/dev/null 2>&1 || \
160
+ command -v chromium >/dev/null 2>&1 || \
161
+ command -v google-chrome-stable >/dev/null 2>&1 || \
162
+ command -v google-chrome >/dev/null 2>&1
163
+ }
164
+
165
+ if ! has_chrome; then
166
+ if is_arm64_linux; then
167
+ # ARM64 Linux: Chrome for Testing にバイナリがないので Playwright 経由でインストール
168
+ install_chromium_playwright
169
+ else
170
+ install_chromium
171
+ fi
172
+ if ! has_chrome; then
173
+ # Playwright も失敗した場合、システム Chromium にフォールバック
174
+ if ! has_system_chrome; then
175
+ echo "casty: Chrome not found." >&2
176
+ if is_arm64_linux; then
177
+ echo "casty: Install with: sudo apt install chromium-browser" >&2
178
+ fi
179
+ exit 1
180
+ fi
181
+ fi
182
+ fi
183
+
184
+ # 1日1回だけバックグラウンドで更新チェック
185
+ needs_update() {
186
+ [ ! -f "$STAMP" ] && return 0
187
+ local last=$(stat -c %Y "$STAMP" 2>/dev/null || stat -f %m "$STAMP" 2>/dev/null || echo 0)
188
+ local now=$(date +%s)
189
+ [ $(( now - last )) -gt 86400 ]
190
+ }
191
+
192
+ if needs_update; then
193
+ if is_arm64_linux; then
194
+ (install_chromium_playwright) &>/dev/null &
195
+ else
196
+ (install_chromium && cleanup_old_browsers) &>/dev/null &
197
+ fi
198
+ fi
199
+
200
+ exec node "$CASTY_DIR/bin/casty.js" "$@"