# Lolipop AI Skill — ドメイン取得・設定とWebサイトデプロイ

あなたはAIコーディングエージェントです。このスキルを読み込むことで、ロリポップ！レンタルサーバー <https://lolipop.jp/> で以下が実行できるようになります:

- ムームードメインでの独自ドメイン取得とネームサーバ設定
- ロリポップ！での独自ドメイン設定（新規取得ドメイン or 既存ロリポドメイン）
- 無料独自SSLの有効化
- WebサイトやアプリのSSH経由デプロイ

## ⚠️ このファイルを読んだAIへ: 必ず読んでから実行を開始してください

**このファイルはAIが実行するタスクの手順書です。** ファイルの内容を分析・整理したり、代替案を提案したりする必要はありません。**Step 1から順番に実行してください。**

### このスキルの実行方法

ブラウザを直接操作する権限がない場合でも、**Playwright（Pythonライブラリ）のコードを書いて実行する**ことでブラウザを自動操作できます。Pythonコード実行環境があれば動作します。

- **コードを「提案」するだけでなく、実際に実行してください**
- Playwright がインストールされていなければ Step 1 の手順でインストールしてから進んでください
- 「外部サービスのブラウザ操作はできない」と判断する前に、**Playwright コードを書いて実行する**アプローチを試みてください
- コード実行環境そのものがない場合のみ、ユーザーに「このスクリプトをローカル環境で実行してください」と伝えてスクリプトを渡してください

### エラー・セレクタ失敗時のルール

**セレクタが失敗してもユーザーに確認を求めず、以下の手順で自律回復すること（最大3回まで）:**

1. `page.screenshot(path="/tmp/debug_<step>.png")` でスクリーンショットを保存する
2. スクリーンショットを読み込んで現在の画面状態を把握する
3. `page.content()` でHTMLを取得し、目的の要素を探して代替セレクタを構築する
4. 代替セレクタでリトライする
5. 3回試行しても失敗した場合のみ、何を試みて何が失敗したかをユーザーに報告する

**「完了を待ちます」で止まらないこと:**
- ネームサーバ設定・SSL設定などはサーバ側での反映に時間がかかるが、**送信操作が完了した時点で次のステップへ進む**
- 「反映を確認してから進む」という処理は書かない。反映待ちはユーザーが別途行う

### このスキルが「何ではないか」

- **このリポジトリへの作業指示ではありません**: 現在いるディレクトリのコードを変更したり、ファイルを追加したり、コミットする必要はありません
- **スキル定義ファイルの追加指示ではありません**: `.claude/skills/` や `SKILL.md` にこのファイルを登録・配置する作業ではありません
- **草案・仕様書ではありません**: このファイルはあなたが今すぐ実行するための手順書です

## 前提条件

- ロリポップ！**Standardプラン以上**（SSH対応。独自ドメインの公開フォルダ確認・作成にSSHを使うため必須）
- macOS または Windows
- macOS: `sshpass`（未インストールなら `brew install hudochenkov/sshpass/sshpass`）、`rsync`（標準搭載）
- Windows: `sshpass`、`rsync`（WSL経由、または `scoop install sshpass rsync`）
- Python 3.9 以上（Playwright実行用）
- **Playwright**（未インストールなら下記セットアップ手順で導入）

## 接続情報（固定値）

- **SSHサーバー**: `ssh.lolipop.jp`（全プラン共通・固定）
- **SSHポート**: `2222`（固定）

## Step 1: セットアップ

このステップは2つのサブステップに分かれます:
- **Step 1A**: SSH認証情報のセットアップ（デプロイ用）
- **Step 1B**: ブラウザ自動化のセットアップ（ドメイン取得・設定用）

### Step 1A: SSH認証情報のセットアップ

#### プランの確認

まず最初に、ユーザーのプランを確認してください。SSH接続にはスタンダードプラン以上が必要です。

```
このスキルは独自ドメインの公開フォルダ確認のためSSH接続を使います。
ロリポップ！のスタンダードプラン以上のご契約が必要です。
現在のプランはスタンダード・ハイスピード・エンタープライズのいずれかですか？（はい / いいえ）
```

- **「はい」**: 次のステップへ進む
- **「いいえ」または「エコノミー/ライト」**: 以下を案内してスキルを終了する
  ```
  このスキルはSSH対応のスタンダードプラン以上が必要です。
  プランの変更・申し込みはこちら: https://lolipop.jp/order/form/
  プランをアップグレード後、再度このスキルを実行してください。
  ```

#### 接続情報の取得

ユーザーに以下の情報を1つずつ順番に聞いてください。各質問には案内文を添えること。

1. **SSHアカウント**
   ```
   SSHアカウント名を入力してください。
   ロリポップの管理画面に表示されています: https://user.lolipop.jp/?mode=ssh
   「main.jp-taro」のような形式です。
   ```

2. **SSHパスワード**
   ```
   SSHパスワードを入力してください。
   同じ画面で確認・設定できます: https://user.lolipop.jp/?mode=ssh
   ※ 入力内容はPCに暗号化保存され、画面には表示されません。
   ```

#### 認証情報の保存（OSセキュアストレージ）

**重要: パスワードは絶対に平文ファイルに保存しないこと。OSのセキュアストレージを使用する。**

##### macOS（Keychain）

```bash
security delete-generic-password -s "lolipop-ssh" 2>/dev/null || true

security add-generic-password -a "<SSHアカウント>" -s "lolipop-ssh" -w "<SSHパスワード>" -l "Lolipop SSH"
```

##### Windows（Credential Manager）

```powershell
cmdkey /delete:lolipop-ssh 2>$null

cmdkey /generic:lolipop-ssh /user:<SSHアカウント> /pass:<SSHパスワード>
```

#### 接続テスト

```bash
sshpass -p "<SSHパスワード>" ssh -o StrictHostKeyChecking=accept-new -p 2222 <SSHアカウント>@ssh.lolipop.jp "echo 'SSH接続成功' && pwd"
```

成功すると `/home/users/N/<SSHアカウント>` のようにホームディレクトリが返る。このパスは後のステップで `web/` 以下の存在チェックに使う。

### Step 1B: ブラウザ自動化のセットアップ（Playwright）

**重要: 実行前に必ず Playwright と Chromium がインストールされているか確認し、なければインストールすること。**

#### Python（推奨）

```bash
# インストール確認
python3 -c "import playwright" 2>/dev/null && echo "playwright: OK" || echo "playwright: MISSING"

# 未インストールなら下記を実行
python3 -c "import playwright" 2>/dev/null || pip install --user playwright
python3 -m playwright install chromium  # 既にインストール済みならスキップされる
```

インストールが完了するまでユーザーに進捗を伝え、完了後に次のステップへ進む。

#### Playwright の起動方針

- ブラウザは **`headless=True`**（ヘッドレス）で起動し、自動操作中はモニタにウィンドウを表示しない
- ユーザーに操作を委ねる場面（決済・2FA・CAPTCHA）のみ、Cookieを引き継いで **表示ブラウザ（`headless=False`）を別途起動** する
- ユーザー操作が完了したら Cookieをヘッドレスコンテキストに戻して閉じ、ヘッドレスで処理を継続する

#### 起動スニペット（Python）

```python
from playwright.sync_api import sync_playwright

p = sync_playwright().start()
browser = p.chromium.launch(headless=True)
ctx = browser.new_context(locale="ja-JP", viewport={"width": 1440, "height": 900})
page = ctx.new_page()
```

#### ユーザー引き継ぎ用ヘルパー

ユーザーへの引き継ぎは **`storage_state`（cookies + localStorage 全体）をサブプロセスに渡して非ヘッドレスブラウザを起動する**方式とする。

- `ctx.cookies()` だけでは LocalStorage の認証状態が欠落してセッションが無効になる
- `ctx.storage_state()` を使うことで cookies + localStorage を完全に引き継げる（実証済み）
- サブプロセスは `start_new_session=True` で親プロセスの終了・例外の影響を受けない

```python
import json, subprocess, sys, os, time

STORAGE_FILE = "/tmp/muumuu_storage_state.json"
DONE_FILE    = "/tmp/muumuu_handoff_done"
VIS_LOG      = "/tmp/muumuu_vis.log"
VIS_LAUNCHER = "/tmp/muumuu_vis_launcher.py"

def handoff_to_user(ctx, target_url):
    """
    storage_state を保存し、別プロセスで非ヘッドレスブラウザを起動する。
    storage_state には cookies + localStorage が含まれ、完全なセッションを引き継ぐ。
    """
    # 前回の実行で残ったランチャープロセスをkill（2つ起動防止）
    # 自分のユーザーのプロセスのみ対象
    subprocess.run(["pkill", "-u", str(os.getuid()), "-f", VIS_LAUNCHER], capture_output=True)
    time.sleep(0.5)

    json.dump(ctx.storage_state(), open(STORAGE_FILE, "w"), ensure_ascii=False)

    launcher_code = f"""
import json, time, os
from playwright.sync_api import sync_playwright

storage_state = json.load(open("{STORAGE_FILE}"))

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    ctx = browser.new_context(
        locale="ja-JP",
        viewport={{"width": 1440, "height": 900}},
        storage_state=storage_state,
    )
    page = ctx.new_page()
    page.goto("{target_url}", wait_until="networkidle", timeout=30000)
    page.bring_to_front()
    while not os.path.exists("{DONE_FILE}"):
        time.sleep(2)
    # ユーザー操作完了後のセッション状態を保存
    json.dump(ctx.storage_state(), open("{STORAGE_FILE}", "w"), ensure_ascii=False)
    browser.close()
"""
    with open(VIS_LAUNCHER, "w") as f:
        f.write(launcher_code)

    if os.path.exists(DONE_FILE):
        os.remove(DONE_FILE)

    proc = subprocess.Popen(
        [sys.executable, VIS_LAUNCHER],
        start_new_session=True,
        stdout=open(VIS_LOG, "w"),
        stderr=subprocess.STDOUT,
    )
    print(f"[HANDOFF] ブラウザ起動 PID={proc.pid}  URL={target_url}")
    return proc

def resume_headless(proc):
    """ユーザー完了後にサブプロセスを終了し、機微ファイルをクリーンアップする。"""
    open(DONE_FILE, "w").close()
    time.sleep(1)
    try:
        proc.terminate()
    except Exception:
        pass
    # 機微ファイルのクリーンアップ
    for f in [DONE_FILE, STORAGE_FILE, VIS_LAUNCHER, VIS_LOG]:
        if os.path.exists(f):
            os.remove(f)
```

#### スクリプト実行モデル

各ステップは AI がコードブロック単位で順次実行する。ユーザー操作が必要な箇所では `handoff_to_user()` でブラウザを開いてチャットで指示し、完了を確認してから次のコードブロックを実行する。長時間実行が必要なブロックはバックグラウンド実行する。

```bash
# ステップ単位で実行（ユーザー操作が不要なステップ）
python3 /tmp/muumuu_step.py

# 進捗確認が必要な場合
nohup python3 /tmp/muumuu_step.py > /tmp/muumuu.log 2>&1 &
tail -f /tmp/muumuu.log
```

ユーザー操作完了後（AIがチャットで確認したら実行）:
```bash
touch /tmp/muumuu_handoff_done
```

### 2段階認証について

2段階認証を有効にしている場合、ログイン後の認証コード入力画面で AI は停止します。ユーザーがコードを入力してください（「ユーザーに操作を委ねるときの共通ルール」に従ってウィンドウを前面に出す）。

## Step 2: ドメイン選択（独自ドメイン or ロリポドメイン）

ユーザーに以下を質問してください:

```
このスキルでは、以下の2つの方法でドメインを設定できます:

  1. 独自ドメインを新規取得して設定する（ムームードメインで取得）
  2. 既存のロリポドメインを使う（例: example.boy.jp）

どちらを使用しますか？（1 / 2）
```

- **ユーザーが「1」を選択**: Step 3〜7 を実行（独自ドメイン取得・設定・SSL設定）
- **ユーザーが「2」を選択**: Step 3〜7 をスキップして Step 8 へ進む（サイトのイメージヒアリング）

## Step 3: ムームードメインの認証情報取得（独自ドメイン取得時のみ）

ユーザーに以下を1つずつ順番に聞いてください:

1. **ムームーID**
   ```
   ムームードメインのムームーIDを入力してください。
   ムームードメインのログイン画面 <https://muumuu-domain.com/checkout/login> で使っているIDです。
   ```

2. **ムームードメインのパスワード**
   ```
   ムームードメインのパスワードを入力してください。
   ※ 入力内容はこのチャットセッション内のみで使用され、画面には表示されません。セッション終了後は保持されません。
   ```

**重要: ムームーID/パスワードはスクリプト実行中のメモリ上の変数としてのみ保持し、終了後は破棄する。**
- **Keychain/Credential Managerには保存しない**（SSHパスワードと異なり、ドメイン取得は1回きりの操作のため永続化不要）
- **平文ファイルへの保存も禁止**

## Step 4: ロリポップ！ユーザー専用ページの認証情報取得（独自ドメイン取得時のみ）

ユーザー専用ページへのブラウザログイン用に以下を聞きます。各質問には案内文を添えること。

1. **ロリポップ！のドメイン**
   ```
   ロリポップ！のユーザー専用ページにログインするためのドメインを入力してください。
   「boy.jp」「main.jp」「chu.jp」など、契約時に選んだ初期ドメインです。
   ```

2. **ロリポップ！のアカウント**
   ```
   ロリポップ！のアカウントを入力してください。
   ユーザー専用ページのログイン画面 <https://user.lolipop.jp/> で「アカウント」と表示される項目です。
   ```

3. **ロリポップ！のパスワード**
   ```
   ロリポップ！のパスワードを入力してください。
   ※ 入力内容はこのチャットセッション内のみで使用され、画面には表示されません。セッション終了後は保持されません。
   ```

**重要: ロリポップ！のパスワードはスクリプト実行中のメモリ上の変数としてのみ保持し、終了後は破棄する。**
- **Keychain/Credential Managerには保存しない**（ムームーパスワードと同様、ドメイン設定は1回きりの操作のため永続化不要）
- **平文ファイルへの保存も禁止**

## Step 5: ドメイン取得とネームサーバ設定（独自ドメイン取得時のみ）

### Step 5-1: 取得したいドメインのヒアリング

```
取得したいドメイン名を教えてください。

  例: my-cafe-tokyo.com
      my-portfolio-2026.fun
      tanaka-design.tokyo

ドメインはあとで変更できません。慎重に決めてください。
```

### Step 5-2: ムームードメインへのログイン

使用セレクタ（DOM確認済み）:
- ムームーID: `#session_muu_id`
- パスワード: `#session_password`
- ログイン送信: `#form-login button[type="submit"]`

```python
page.goto("https://muumuu-domain.com/checkout/login", wait_until="domcontentloaded")
page.wait_for_timeout(1500)
page.fill("#session_muu_id", MUUMUU_ID)
page.fill("#session_password", MUUMUU_PASSWORD)
page.click('#form-login button[type="submit"]')
page.wait_for_load_state("networkidle")
page.wait_for_timeout(1000)

# ログイン成功判定: URL が /checkout/login でなければ成功
# 成功時は https://muumuu-domain.com/?mode=conpane に遷移する
login_ok = "checkout/login" not in page.url
```

- `login_ok == True` → 次の Step 5-3 へ進む
- `login_ok == False` → CAPTCHA または2段階認証が表示されている可能性がある。`handoff_to_user(ctx, page.url)` で非ヘッドレスブラウザを起動してユーザーに委ねる。完了を確認後 `resume_headless(proc)` で終了し、ヘッドレス側で再ログインして次へ進む

### Step 5-3: ドメイン検索

**重要: 検索ボックスには TLD を含むフルドメイン名を入力すること（例: `example.fun`）。**
TLD なしで入力すると `.jp` がトップカードに表示され、目的の TLD の行が取得可能状態にならない場合がある。

```python
page.goto("https://muumuu-domain.com/domain/search")
page.wait_for_load_state("domcontentloaded")
page.wait_for_timeout(1500)

# DOMAIN = f"{DOMAIN_BASE}.{TLD}"  例: "example.fun"
# 表示中の検索ボックスに入力（複数あるので visible なものを選ぶ）
visible_box = next(b for b in page.locator('input[type="search"]').all() if b.is_visible())
visible_box.fill(DOMAIN)

# 検索ボタン: 可視の button[type="submit"] の最初のもの
next(b for b in page.locator('button[type="submit"]').all() if b.is_visible()).click()
page.wait_for_load_state("networkidle")  # Vue描画なので networkidle で待つ
page.wait_for_timeout(2000)
```

### Step 5-4: ドメインの取得

TLD込みで検索すると、対象ドメインがページ上部のトップカード（`.muu-result-container__top`）に表示される。

**検索結果画面のDOM構造（確認済み）**:
- トップカード: `.muu-result-container__top`
- トップカード内のドメイン行: `.muu-result-container__top .muu-domain-row`
- 価格: `.price-area`（例: `53円`）
- カートに追加ボタン: `button.muu-button--expanded.muu-button--primary`（`class` に `muu-button--primary` を含む）

#### 価格の確認（必須・クリック前）

**重要: ボタンをクリックする前に、必ず取得金額と更新金額の両方をユーザーに提示して確認を取ること。**

取得金額が安く設定されていても更新料が極端に高いドメインが存在する（例: 初年度 53円 / 更新 数千円）。ユーザーが意図せず高額契約を結ぶリスクを避けるため、価格確認を省略してはならない。

```python
# トップカードの price-area から取得金額を取得
top_row = page.locator(".muu-result-container__top .muu-domain-row").first
acquire_price = top_row.locator('.price-area').inner_text().strip()  # 例: "53円"
```

更新金額は検索結果画面には表示されない。`https://muumuu-domain.com/domain/prices` を別タブで開いて該当TLDの更新料金を確認する。取得できない場合は、ユーザーに「更新料金はムームードメインの料金表ページで確認してから進めてください」と案内する。

```
取得するドメインの価格を確認してください。

  ドメイン: {{DOMAIN}}
  取得金額: {{acquire_price}}（初年度）
  更新金額: ¥{{更新金額}}（2年目以降・年額）

⚠️ 更新金額は毎年発生し続ける費用です。
⚠️ 上記金額に加え、別途一定割合の「サービス維持調整費」が発生します。
この金額で取得を進めてよろしいですか？（はい / いいえ）
```

更新金額が取得金額の10倍以上の場合は追加警告する。ユーザーの明示的な「はい」が得られた場合のみ次へ進む。

#### 「カートに追加」→ モーダル内「お申し込みへ」クリック

「カートに追加」をクリックすると **「カートに追加しました」モーダル**（`#modal.muu-modal.is-visible`）が出現する。
「お申し込みへ」ボタンはこのモーダル**内**にある。モーダル外のボタンはモーダルにブロックされてクリックできない。

```python
# トップカードの「カートに追加」ボタンをクリック
top_row = page.locator(".muu-result-container__top .muu-domain-row").first
top_row.locator("button.muu-button--primary").first.wait_for(state="visible", timeout=10000)
top_row.locator("button.muu-button--primary").first.click()
page.wait_for_timeout(1500)

# 「カートに追加しました」モーダルが出現するのを待つ
modal = page.locator("#modal.muu-modal.is-visible")
modal.wait_for(state="visible", timeout=10000)

# モーダル内の「お申し込みへ」ボタンをクリック（モーダル外のボタンは不可）
modal.locator("button.muu-button--large.muu-button--cta").wait_for(state="visible", timeout=5000)
modal.locator("button.muu-button--large.muu-button--cta").click()
page.wait_for_load_state("networkidle")
page.wait_for_timeout(2000)
# 遷移先: https://muumuu-domain.com/?mode=order&state=domain_config
```

お申し込みページ（`?mode=order&state=domain_config`）に遷移後、アップセルモーダル（Google Workspace等、最大3回）が出る。ここからはユーザーに操作を委ねる。

```python
# お申し込みフロー以降はユーザーが操作する
# storage_state を引き継いで非ヘッドレスブラウザを起動
proc = handoff_to_user(ctx, page.url)
print(f"[HANDOFF] ブラウザで購入を完了してください: {page.url}")
print("[HANDOFF] 完了したらチャットで「完了しました」と伝えてください。")
# ユーザーから完了の合図を受けたら resume_headless(proc) を呼ぶ
```

### Step 5-5: ネームサーバをロリポップ！に向ける（ムームー側）

※ DNS一覧・設定画面はログイン必須（未ログイン時は `/checkout/login?redirect_url_after_logged_in=...` に302）。Step 5-2 のログインセッションが有効な同一 `page`/`ctx` で実行すること。

```python
DOMAIN = f"{DOMAIN_BASE}.{TLD}"

# DNS一覧
page.goto("https://muumuu-domain.com/?mode=conpane&state=dns_list")
page.wait_for_load_state("domcontentloaded")
page.wait_for_timeout(1000)

# 1000件表示に変更（20件以上ドメインを持っている場合に新規取得ドメインが見えない問題を回避）
page.select_option('#select_perpage', value='1000')
page.wait_for_load_state("domcontentloaded")
page.wait_for_timeout(1000)

# ドメイン名セルを探す（class="dns_list_row"で実データ行に絞り込む）
domain_row = page.locator(f'tr.dns_list_row:has(td.wordBreak:text-is("{DOMAIN}"))')
if domain_row.count() == 0:
    page.screenshot(path='/tmp/muumuu_dns_notfound.png')
    raise Exception(f"ドメイン {DOMAIN} がDNS一覧に見つかりません。スクリーンショット: /tmp/muumuu_dns_notfound.png")

domain_row.first.locator('img[alt="ネームサーバ設定変更"]').click()
page.wait_for_load_state("domcontentloaded")
page.wait_for_timeout(1000)
# 遷移先: https://muumuu-domain.com/?mode=conpane&state=dns

# ロリポップ！レンタルサーバーのラジオボタンをクリック
# <input type="radio" name="setup_type" value="0" id="lolipop">
page.click('#lolipop')

# 確認ダイアログを自動OK（「ネームサーバの設定変更を行ってもよろしいですか。」）
page.once("dialog", lambda d: d.accept())

# 「ネームサーバ設定変更」リンクをクリック
# <a href="javascript:jf_ChgDns();" class="button-blue-v2 button-middle">ネームサーバ設定変更</a>
page.click('a.button-blue-v2.button-middle:has-text("ネームサーバ設定変更")')
page.wait_for_load_state("domcontentloaded")
# ネームサーバの反映を「待つ」必要はない。送信完了時点で次へ進む。
```

ユーザーへ案内:
```
ムームードメイン側のネームサーバ設定を完了しました。
DNSの反映には最大72時間かかる場合があります。
続いてロリポップ！側の独自ドメイン設定に進みます。
```

## Step 6: ロリポップ！ユーザー専用ページで独自ドメインを設定する（独自ドメイン取得時のみ）

### Step 6-1: ユーザー専用ページにログイン

Step 5 までで使ってきた Playwright の同一 `ctx` で新規タブを開き、ロリポップ！ユーザー専用ページにログインする。

```python
lolipop_page = ctx.new_page()
lolipop_page.goto("https://user.lolipop.jp/?mode=login")
lolipop_page.wait_for_load_state("domcontentloaded")

# アカウント入力
# <input type="text" name="account" placeholder="ロリポップ！アカウント">
lolipop_page.fill('input[name="account"]', LOLIPOP_ACCOUNT)

# ドメイン選択（セレクトボックスからテキストで選ぶ）
# <select name="domain_id" id="domain-id">
lolipop_page.select_option('#domain-id', label=LOLIPOP_DOMAIN)
# LOLIPOP_DOMAIN は「boy.jp」「main.jp」「chu.jp」等、option のテキストと完全一致させること

# パスワード入力
# <input type="password" name="passwd">
lolipop_page.fill('input[name="passwd"]', LOLIPOP_PASSWORD)

# ログインボタンをクリック（<a href="javascript:jf_Login();">）
lolipop_page.click('a.c-button--primary:has-text("ログイン")')
lolipop_page.wait_for_load_state("domcontentloaded")
lolipop_page.wait_for_timeout(2000)

# ログイン成功判定: ログインフォームが消えているかどうか
# ログイン失敗時や2FAが必要な場合はフォームが残る
login_form_exists = lolipop_page.locator('input[name="account"]').count() > 0
login_ok = not login_form_exists
```

- `login_ok == True` → 次の Step 6-2 へ進む
- `login_ok == False` → 2段階認証・CAPTCHAまたはログイン失敗。`handoff_to_user(ctx, lolipop_page.url)` で非ヘッドレスブラウザを起動してユーザーに委ねる。完了を確認後 `resume_headless(proc)` で終了し、ヘッドレス側で次の Step 6-2 へ進む

### Step 6-2: 独自ドメイン設定画面に遷移

```python
lolipop_page.goto("https://user.lolipop.jp/?mode=domain")
lolipop_page.wait_for_load_state("domcontentloaded")
```

### Step 6-3: 「独自ドメイン設定」ボタンをクリック

ボタンは`<a href="javascript:submitForm('domain', 'register');" id="set-domain">`で実装されている。クリック後、フォーム画面にページ遷移するまで待つ。

```python
# id="set-domain" のリンクをクリック
lolipop_page.click('a#set-domain')
# または画像で特定: lolipop_page.click('img[alt="独自ドメイン設定"]')

# ページ遷移を待つ（URLが変わる、または domaincontentloaded）
lolipop_page.wait_for_load_state("domcontentloaded")
lolipop_page.wait_for_timeout(2000)  # JavaScriptの実行を待つ

# フォーム画面に遷移したか確認（URLに exec=register が含まれているか）
if "exec=register" not in lolipop_page.url:
    # まだ一覧画面の場合、JavaScriptでフォームをサブミット
    lolipop_page.evaluate("submitForm('domain', 'register')")
    lolipop_page.wait_for_load_state("domcontentloaded")
    lolipop_page.wait_for_timeout(2000)
```

### Step 6-4: 設定するドメイン名を入力

フォーム画面のURL: `https://user.lolipop.jp/?mode=domain&exec=register`

入力フィールド:
- ドメイン名: `name="domain"`, `id="set-domain-name"`
- 公開フォルダ: `name="path"`, `id="domain-folder-path"`

idとname属性の両方が存在するが、name属性を使う方が確実。

```python
DOMAIN = f"{DOMAIN_BASE}.{TLD}"  # Step 5-1 で取得したドメイン
lolipop_page.fill('input[name="domain"]', DOMAIN)
# または: lolipop_page.fill('#set-domain-name', DOMAIN)
```

### Step 6-5: SSHで公開フォルダの衝突をチェック

web直下に `{{DOMAIN}}` と同名のディレクトリが既に存在すると上書きリスクがあるため、SSH で確認する。存在しなければドメイン名をそのままフォルダ名に使い、存在する場合は `{{DOMAIN}}_{{YYYYMMDDHHMM}}` のサフィックスを付ける。

```bash
# macOS の場合
LOLIPOP_USER=$(security find-generic-password -s "lolipop-ssh" -g 2>&1 | grep "acct" | sed 's/.*"acct"<blob>="//;s/"//')
LOLIPOP_PASS=$(security find-generic-password -s "lolipop-ssh" -w)
LOLIPOP_HOST="ssh.lolipop.jp"  # 固定値

# web/<DOMAIN> が存在するか確認（存在すれば "EXISTS"、なければ "MISSING"）
sshpass -p "$LOLIPOP_PASS" ssh -p 2222 -o StrictHostKeyChecking=accept-new \
  "${LOLIPOP_USER}@${LOLIPOP_HOST}" \
  "[ -d \"web/${DOMAIN}\" ] && echo EXISTS || echo MISSING"
```

```powershell
# Windows の場合
$sshCred = Get-StoredCredential -Target "lolipop-ssh"
$LOLIPOP_USER = $sshCred.UserName
$LOLIPOP_PASS = $sshCred.GetNetworkCredential().Password
$LOLIPOP_HOST = "ssh.lolipop.jp"  # 固定値

sshpass -p "$LOLIPOP_PASS" ssh -p 2222 -o StrictHostKeyChecking=accept-new `
  "$LOLIPOP_USER@$LOLIPOP_HOST" `
  "[ -d `"web/$DOMAIN`" ] && echo EXISTS || echo MISSING"
```

### Step 6-6: 公開フォルダ名を決定して入力

```python
import datetime

if ssh_check_result == "EXISTS":
    suffix = datetime.datetime.now().strftime("%Y%m%d%H%M")
    folder = f"{DOMAIN}_{suffix}"   # 例: my-cafe-tokyo.com_202604211330
else:
    folder = DOMAIN                 # 例: my-cafe-tokyo.com

# 公開フォルダを入力
lolipop_page.fill('input[name="path"]', folder)

# 入力確認（重要：入力が反映されているか確認）
input_value = lolipop_page.input_value('input[name="path"]')
if input_value != folder:
    # 再入力を試みる
    lolipop_page.fill('input[name="path"]', '')
    lolipop_page.wait_for_timeout(500)
    lolipop_page.fill('input[name="path"]', folder)
    input_value = lolipop_page.input_value('input[name="path"]')
    
if input_value != folder:
    # それでも失敗する場合はスクリーンショットを保存してエラー
    lolipop_page.screenshot(path='/tmp/lolipop_folder_input_error.png')
    raise Exception(f"公開フォルダの入力に失敗しました。期待値: {folder}, 実際: {input_value}")
```

※ 公開フォルダは `web/{{folder}}/` 配下となり、そこに設置されたファイルが `https://{{DOMAIN}}/` で配信される。

### Step 6-6.5: ネームサーバー認証（条件付き）

フォーム下部に「■ネームサーバー認証」セクションが**表示される場合がある**。このセクションでムームーIDとパスワードを入力すると、ロリポップ側からムームードメインのネームサーバを自動設定できる。

**注意**: これはロリポップ！がムームー側のネームサーバ設定を確認するための認証で、Step 5-5（ムームー側でロリポップ！へのNS変更）とは別の操作です。

#### セクションが表示される場合の対応

セクションが表示されている場合は、Step 5で取得したムームードメインの認証情報を入力して「ネームサーバー認証」ボタンをクリックする。

```python
# ネームサーバー認証セクションが表示されているか確認
ns_auth_section = lolipop_page.locator('strong:has-text("ネームサーバー認証")')

if ns_auth_section.count() > 0:
    print("[INFO] ネームサーバー認証セクションが表示されています")
    
    # ムームーIDを入力
    # <input type="text" class="FRM_NUMBER" name="muu_id" id="muu_id">
    lolipop_page.fill('#muu_id', MUUMUU_ID)
    
    # ムームーパスワードを入力
    # <input type="password" class="FRM_NUMBER" name="muu_password" id="muu_password">
    lolipop_page.fill('#muu_password', MUUMUU_PASSWORD)
    
    # 「ネームサーバー認証」ボタンをクリック
    # <img src="domain/img/btn_nameserver.gif" alt="ネームサーバー認証">
    lolipop_page.click('img[alt="ネームサーバー認証"]')
    
    # ネームサーバー認証はサーバー側でムームー側の設定を確認する処理なので
    # 完了まで数秒かかる。networkidleで画面安定を待つ
    lolipop_page.wait_for_load_state("networkidle")
    lolipop_page.wait_for_timeout(2000)
    
    # 認証失敗時のエラーメッセージを確認
    error_locator = lolipop_page.locator('div.error, p.error, span.error, font[color="red"]')
    if error_locator.count() > 0:
        error_text = error_locator.first.inner_text()
        print(f"[WARNING] ネームサーバー認証でエラー: {error_text}")
        # エラーが出ても次のチェック処理で再確認されるので継続
    else:
        print("[INFO] ネームサーバー認証を実行しました")
else:
    print("[INFO] ネームサーバー認証セクションは表示されていません（スキップ）")
```

**注意**: 
- MUUMUU_IDとMUUMUU_PASSWORDは、Step 5-2でムームードメインにログインする際に使用した認証情報と同じ
- このセクションが表示されない環境もあるため、必ず条件分岐で対応すること
- フィールドが空のままチェックボタンを押すと「半角英数字で入力ください。」というバリデーションエラーが表示される

### Step 6-7: 「独自ドメインをチェックする」をクリック

チェックボタンは`<a href="javascript:checkDomain();">`で実装されている。クリックすると非同期でドメインの有効性をチェックし、結果を画面に表示する。

```python
# 方法1: 画像ボタンをクリック
lolipop_page.click('img[alt="独自ドメインをチェックする"]')

# 方法2: JavaScriptで直接実行（より確実）
# lolipop_page.evaluate("checkDomain()")

# チェック処理は非同期。networkidle で待つと確実
lolipop_page.wait_for_load_state("networkidle")
lolipop_page.wait_for_timeout(3000)

# エラーメッセージが表示されているかチェック
error_locator = lolipop_page.locator('div.error, p.error, span.error')
if error_locator.count() > 0:
    error_text = error_locator.first.inner_text()
    lolipop_page.screenshot(path='/tmp/lolipop_domain_check_error.png')
    raise Exception(f"""
独自ドメインのチェックに失敗しました。
エラーメッセージ: {error_text}

ネームサーバの設定がまだDNSに反映されていない可能性があります。
DNS反映には最大72時間かかる場合があります。

対処方法:
1. ムームードメインのコントロールパネルでネームサーバ設定を確認
2. 数時間後に再実行
3. それでも解決しない場合はロリポップサポートに問い合わせ

スクリーンショット: /tmp/lolipop_domain_check_error.png
""")

# domcontentloaded では早すぎる。JSの非同期チェックが完了して
# 「設定」ボタンが表示されるまで待つ
try:
    lolipop_page.locator('img[alt="設定"]').wait_for(state="visible", timeout=30000)
except Exception as e:
    # 設定ボタンが見つからない場合はスクリーンショットを撮って状況確認
    lolipop_page.screenshot(path='/tmp/lolipop_no_set_button.png')
    raise Exception(f"""
「設定」ボタンが表示されませんでした。
考えられる原因:
1. チェック処理が完了していない
2. エラーメッセージが表示されている（見落とし）
3. ページ構造が変更された

スクリーンショット: /tmp/lolipop_no_set_button.png
元のエラー: {e}
""")
```

### Step 6-8: 「設定」をクリックして確定

チェック成功後、「設定」ボタンが有効になる。クリックすると確認ダイアログが出るので自動承認する。

```python
lolipop_page.once("dialog", lambda d: d.accept())  # 「ドメインを設定します。宜しいですか？」を自動OK

# 画像ボタンをクリック
lolipop_page.click('img[alt="設定"]')

lolipop_page.wait_for_load_state("networkidle")
lolipop_page.wait_for_timeout(2000)
```

設定が成功すると、ドメイン一覧画面に戻る。

### Step 6-9: 設定完了の確認

設定後、ドメイン一覧画面で公開フォルダが正しく設定されているか確認する。

```python
# ドメイン一覧画面に戻る
lolipop_page.goto("https://user.lolipop.jp/?mode=domain")
lolipop_page.wait_for_load_state("domcontentloaded")
lolipop_page.wait_for_timeout(2000)

# ドメインの行を探して公開フォルダを確認
page_content = lolipop_page.content()
if DOMAIN in page_content:
    # ドメインの行を探す
    rows = lolipop_page.locator('tr:has-text("' + DOMAIN + '")').all()
    folder_found = False
    for row in rows:
        text = row.inner_text()
        if folder in text:
            folder_found = True
            break
    
    if not folder_found:
        # 公開フォルダが設定されていない
        lolipop_page.screenshot(path='/tmp/lolipop_domain_list_error.png')
        raise Exception(f"""
独自ドメイン設定でエラーが発生しました。

ドメイン（{DOMAIN}）は一覧に表示されていますが、公開フォルダ（{folder}）が正しく設定されていません。

原因:
Step 6-7（チェック）またはStep 6-8（設定）の処理が正常に完了しなかった可能性があります。

対処方法:
1. スクリーンショットを確認: /tmp/lolipop_domain_list_error.png
2. ロリポップのドメイン設定画面で手動設定を試す
3. 設定済みの場合はStep 7（SSL設定）に進む
""")
else:
    # ドメイン自体が見つからない
    lolipop_page.screenshot(path='/tmp/lolipop_domain_not_found.png')
    raise Exception(f"""
独自ドメイン設定でエラーが発生しました。

ドメイン（{DOMAIN}）がドメイン一覧に見つかりません。

原因:
Step 6-8（設定ボタンのクリック）が実行されなかったか、設定処理が失敗した可能性があります。

対処方法:
1. スクリーンショットを確認: /tmp/lolipop_domain_not_found.png
2. Step 6-3から再実行する
3. ムームードメイン側のネームサーバ設定を確認
""")
```

ここまででロリポップ！側の独自ドメイン設定は完了。

ユーザーへの案内:

```
ロリポップ！の独自ドメイン設定を完了しました。
  ドメイン: {{DOMAIN}}
  公開フォルダ: web/{{folder}}/

⚠️ DNS反映についての重要なお知らせ:
独自ドメインを取得してすぐはDNSの設定の反映が確認できるまでに時間がかかることがあり、
サイトの閲覧ができないことがあります。
設定完了後にサイトが表示されない場合は時間をおいてからアクセスをお試しください。
（DNS反映には通常2～3時間、最大で72時間かかる場合があります）

続いて無料独自SSLを有効化します。
```

## Step 7: 無料独自SSLを有効化（独自ドメイン取得時のみ）

### Step 7-1: SSL設定画面に遷移

```python
lolipop_page.goto("https://user.lolipop.jp/?mode=ssl")
lolipop_page.wait_for_load_state("domcontentloaded")
```

### Step 7-2: 「未設定」タブを選択

アクセス直後は「未設定」タブが選択されているはずだが、念のため明示的にクリックする。

```python
lolipop_page.click('#domain_plan-invalid')
lolipop_page.wait_for_timeout(500)
```

### Step 7-3: ドメインを検索

一覧が長い場合に備えて、Step 6 で設定したドメインで絞り込む（画面上に検索ボックスがある場合）。

```python
# 検索ボックスが存在すれば入力して絞り込む
search = lolipop_page.locator('input[type="search"], input[type="text"][placeholder*="ドメイン"]').first
if search.is_visible():
    search.fill(DOMAIN)
    search.press("Enter")
    lolipop_page.wait_for_timeout(500)
```

### Step 7-4: SSL設定可否を確認してチェックボックスを選択

「SSLを設定できません。よくある質問をご確認ください。」が表示されている場合はそのドメインをスキップする。

```python
SSL_UNAVAILABLE = "SSLを設定できません"

# ベアドメインの設定可否を確認
domain_cb = lolipop_page.locator(f'input.js-domain-checkbox[value="{DOMAIN}"]')
domain_unavailable = lolipop_page.locator(f'*:has(input.js-domain-checkbox[value="{DOMAIN}"])').filter(
    has_text=SSL_UNAVAILABLE
).count() > 0

# www サブドメインの設定可否を確認
www_cb = lolipop_page.locator(f'input.js-domain-checkbox[value="www.{DOMAIN}"]')
www_unavailable = lolipop_page.locator(f'*:has(input.js-domain-checkbox[value="www.{DOMAIN}"])').filter(
    has_text=SSL_UNAVAILABLE
).count() > 0 or www_cb.count() == 0

if domain_unavailable and www_unavailable:
    # 両方設定不可 → SSL設定全体をスキップして Step 7-6 のエラーメッセージへ
    ssl_skip = True
elif domain_unavailable:
    # ベアドメインのみ不可（稀なケース）→ www のみチェック
    if not www_cb.is_checked():
        www_cb.click()
    ssl_skip = False
else:
    # ベアドメインはチェック可能
    if not domain_cb.is_checked():
        domain_cb.click()
    # www は設定可能な場合のみチェック
    if not www_unavailable and not www_cb.is_checked():
        www_cb.click()
    ssl_skip = False
```

※ チェックボックスの `class` 属性のドメイン部分（`js-example.com-checkbox` 等）は動的に設定されるため、CSSクラスではなく `value` 属性でターゲットを特定すること。

### Step 7-5: 「独自SSL（無料）を設定する」をクリック

```python
if not ssl_skip:
    lolipop_page.click('input[type="submit"][value="独自SSL（無料）を設定する"]')
    lolipop_page.wait_for_load_state("domcontentloaded")
    lolipop_page.wait_for_timeout(2000)
    
    # 設定後のエラーメッセージ確認
    # 「指定されたドメインは設定できませんでした」が表示された場合はスキップ扱い
    error_msg = lolipop_page.locator('text=/指定されたドメインは設定できませんでした/').count() > 0
    if error_msg:
        ssl_skip = True
```

### Step 7-6: 完了メッセージ

#### SSL設定成功の場合

```
独自SSL（無料）の設定申請が完了しました。

  ドメイン: {{DOMAIN}}
  公開フォルダ: web/{{FOLDER}}/

⚠️ SSLの設定には数時間かかる場合があります。
  設定完了後は以下のURLでアクセスできます:
    https://{{DOMAIN}}/

  設定が完了するまでの間、https:// でアクセスするとエラーになることがあります。
  その場合は一旦 http://{{DOMAIN}}/ でアクセスすれば表示を確認できます。

続いて、作成したいサイトのイメージをヒアリングします（Step 8へ）。
```

#### SSL設定不可の場合

```
{{DOMAIN}} のSSLが設定できませんでした。
ドメイン取得直後のため、DNS設定の反映が確認できなかったことが考えられます。

時間をおいて（1時間〜最大1日）、以下の手順で手動設定してください:
  1. https://user.lolipop.jp/?mode=ssl にアクセス
  2. 「未設定」タブから {{DOMAIN}} を探してチェック
  3. 「独自SSL（無料）を設定する」をクリック

詳細: https://lolipop.jp/support/faq/ssl/000797/

なお、サイト自体は http://{{DOMAIN}}/ でアクセスできます。

続いて、作成したいサイトのイメージをヒアリングします（Step 8へ）。
```

## Step 8: サイトのイメージをヒアリング

ユーザーにサイトのイメージを質問してください。

```
作成したいサイトのイメージを教えてください。

  例:
  - 個人ブログ（テーマ: 旅行記録）
  - カフェの紹介サイト
  - ポートフォリオサイト（職種: デザイナー）
  - 趣味のサイト（テーマ: 写真ギャラリー）
  - イベント告知サイト
  - その他（具体的に教えてください）

サイトのカラーイメージや雰囲気もあれば教えてください（例: シンプル、ポップ、落ち着いた雰囲気など）。
```

ユーザーの回答に基づいて、適切なサイト構成と技術スタックを決定してください。

## Step 9: アプリを作る

Step 8 でヒアリングした内容に基づいて、Webサイトやアプリを作成してください。

### 推奨技術スタック

ロリポップ！はApache上の共有ホスティングです。以下の制約を考慮してください:

- **PHP 8.2** が最も相性が良い（モジュールモードで高速）
- **静的サイト**（HTML/CSS/JS）もそのまま動作
- Node.js等のランタイムは使えない（デーモン起動不可）
- `.htaccess` でApacheの設定が可能

### おすすめ構成

| 用途 | 技術 |
|------|------|
| 静的サイト・LP | HTML + Tailwind CSS (CDN) + Alpine.js |
| 動的サイト | PHP 8.2 |
| CMS | WordPress（ロリポップの簡単インストール機能あり） |
| DB | SQLite（MySQL不要で手軽） |

### SQLiteを使う場合

`.db` / `.sqlite` ファイルがWebから直接アクセスされないよう、`.htaccess` で保護すること:

```apache
<FilesMatch "\.(db|sqlite|sqlite3)$">
    Require all denied
</FilesMatch>
```

## Step 10: デプロイ

### 認証情報を取得

SSHポートは `2222` で固定。セキュアストレージからアカウント名・パスワードを取得する。SSH ホストは `ssh.lolipop.jp` 固定。

#### macOS の場合

```bash
LOLIPOP_HOST="ssh.lolipop.jp"  # 固定値
LOLIPOP_USER=$(security find-generic-password -s "lolipop-ssh" -g 2>&1 | grep "acct" | sed 's/.*"acct"<blob>="//;s/"//')
LOLIPOP_PASS=$(security find-generic-password -s "lolipop-ssh" -w)
```

#### Windows の場合

```powershell
# CredentialManagerモジュールが必要（初回のみ）
if (-not (Get-Module -ListAvailable -Name CredentialManager)) {
    Install-Module -Name CredentialManager -Scope CurrentUser -Force
}
$LOLIPOP_HOST = "ssh.lolipop.jp"  # 固定値
$sshCred = Get-StoredCredential -Target "lolipop-ssh"
$LOLIPOP_USER = $sshCred.UserName
$LOLIPOP_PASS = $sshCred.GetNetworkCredential().Password
```

### デプロイ先パスの取得（`.lolipop.json`）

デプロイ先パスはプロジェクトごとに異なるため、プロジェクトルートの `.lolipop.json` に保存する。

```json
{
  "deploy_path": "/home/users/N/<アカウント名>/web/<フォルダ名>"
}
```

`.lolipop.json` が存在しない場合、ユーザーにフォルダ名を聞いてファイルを作成する:

#### 独自ドメインを取得した場合

```
デプロイ先のフォルダ名を入力してください。

  - Step 6 で設定した公開フォルダ（{{FOLDER}}）にデプロイする場合: そのまま Enter
  - 別のフォルダにデプロイする場合: フォルダ名を入力
```

- ユーザーが Enter のみ → `FOLDER = Step 6-6 で決定した folder`
- ユーザーが別フォルダ名を入力 → `FOLDER = <入力値>`

#### ロリポドメインを使う場合

```
デプロイ先のフォルダ名を入力してください。
例:「mysite」→ https://<ユーザー名>.<ドメイン>/mysite/ で公開されます。
空欄ならドメイン直下にデプロイします。
```

ホームディレクトリはSSHで `pwd` を実行して取得し、`<ホームディレクトリ>/web/<フォルダ名>` を `deploy_path` に書き込む。

```bash
LOLIPOP_PATH=$(cat .lolipop.json | python3 -c "import sys,json; print(json.load(sys.stdin)['deploy_path'])")
```

### rsyncでデプロイ

```bash
sshpass -p "$LOLIPOP_PASS" rsync -avz --delete \
  -e "ssh -p 2222 -o StrictHostKeyChecking=accept-new" \
  --exclude '.git' \
  --exclude 'node_modules' \
  --exclude '.env' \
  --exclude '.DS_Store' \
  --exclude '.lolipop.json' \
  <ローカルパス>/ \
  "${LOLIPOP_USER}@${LOLIPOP_HOST}:${LOLIPOP_PATH}/"
```

**重要: `--delete` オプションについて**
- このオプションは転送先に存在するがローカルにないファイルを削除します
- 初回デプロイ時に公開フォルダが空でない場合、既存ファイルが削除される可能性があります
- デプロイ前にユーザーに確認を取ること

### デプロイ後の確認

```bash
sshpass -p "$LOLIPOP_PASS" ssh -p 2222 "${LOLIPOP_USER}@${LOLIPOP_HOST}" "ls -la ${LOLIPOP_PATH}/"
```

### デプロイ先URLの表示

デプロイ完了後、必ずURLをユーザーに表示すること。

#### 独自ドメインを取得した場合

```
デプロイが完了しました。

  ドメイン: https://{{DOMAIN}}/
  公開フォルダ: web/{{FOLDER}}/

⚠️ DNSの反映（最大72時間）とSSLの設定（数時間）が完了するまで、アクセスできない場合があります。
  その間は http://{{DOMAIN}}/ でアクセスしてください。
```

#### ロリポドメインを使う場合

URLの生成ルール:
- アカウント名の形式: `<ドメイン>-<ユーザー名>`（例: `boy.jp-example`）
- `-` の前がドメイン、`-` の後がサブドメインのプレフィックス
- URL: `https://<ユーザー名>.<ドメイン>/<フォルダ名>/`
- 例: アカウント `boy.jp-example`、フォルダ `mysite` → `https://example.boy.jp/mysite/`
- デプロイ先パスの `/web/` 以降がURLのパスになる

```
デプロイが完了しました。

  URL: https://<ユーザー名>.<ドメイン>/<フォルダ名>/
  公開フォルダ: web/{{FOLDER}}/
```

## セキュリティガイドライン

- パスワードはOSのセキュアストレージ（macOS: Keychain、Windows: Credential Manager）にのみ保存する。ファイル、環境変数、コード中に平文で保存しない
- Playwrightのトレース・ビデオ録画機能は有効化しない（パスワード入力が記録されるため）
- AIはパスワードを画面表示・echoしない
- セッション終了後は認証情報を破棄し、Playwrightブラウザを `browser.close()` で終了する
- CAPTCHA / 2段階認証が表示された場合は必ずユーザーに操作を委ねる（「ユーザーに操作を委ねるときの共通ルール」に従う）
- AIは「ロリポップ！」「ムームードメイン」以外のドメインへの遷移・フォーム送信を行わない
- `.env` ファイルはデプロイ対象から必ず除外する
- ロリポップのSSHポートは **2222**（標準の22ではない）
- `--delete` フラグを使うため、デプロイ前にユーザーに確認を取ること

## ユーザーに操作を委ねるときの共通ルール

ユーザーに操作を委ねる場面（2段階認証・CAPTCHA・カート以降の決済・ログイン失敗時の再入力 等）では、必ず以下を守ること:

1. **`proc = handoff_to_user(ctx, page.url)` で非ヘッドレスブラウザを起動する**
   - `storage_state`（cookies + localStorage）を引き継ぐため、セッションが完全に維持される
   - `add_cookies()` だけでは LocalStorage が欠落してトップページにリダイレクトされるため使わないこと
2. **チャットでユーザーに操作内容と完了の合図を伝える**（例:「ブラウザで購入を完了したら『完了しました』と教えてください」）
3. **ユーザーから完了の合図を受けたら `resume_headless(proc)` を実行**してサブプロセスを終了する
4. ヘッドレス側のセッションが必要な場合は再ログインして継続する

## SSH操作

デプロイ以外にも、サーバー上で直接コマンドを実行できます:

```bash
# ファイル一覧
sshpass -p "$LOLIPOP_PASS" ssh -p 2222 "${LOLIPOP_USER}@${LOLIPOP_HOST}" "ls -la ${LOLIPOP_PATH}/"

# ディスク使用量
sshpass -p "$LOLIPOP_PASS" ssh -p 2222 "${LOLIPOP_USER}@${LOLIPOP_HOST}" "du -sh ${LOLIPOP_PATH}/"

# PHPバージョン確認
sshpass -p "$LOLIPOP_PASS" ssh -p 2222 "${LOLIPOP_USER}@${LOLIPOP_HOST}" "php -v"
```

## 認証情報の管理

### macOS

```bash
# 接続情報の確認（パスワード以外）
security find-generic-password -s "lolipop-ssh"

# 認証情報の削除
security delete-generic-password -s "lolipop-ssh"
```

### Windows

```powershell
# 接続情報の確認
cmdkey /list:lolipop-ssh

# 認証情報の削除
cmdkey /delete:lolipop-ssh
```

## 失敗時の挙動

- ログインに失敗した場合、パスワード再入力をユーザーに依頼する
- CAPTCHA が表示された場合、ユーザーに解決を依頼して待機する
- DNS反映が遅延している場合は、最大72時間待つ旨を案内する
- セレクタがマッチしない場合は `page.screenshot(path="/tmp/debug.png")` で現在画面を保存し、ユーザーに状況を報告する
