diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5d1877a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,21 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + verify-statusline: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + + - name: Verify statusline generation + run: python3 scripts/verify_context.py + shell: bash diff --git a/README.ja.md b/README.ja.md index c2a0b9f..611651a 100644 --- a/README.ja.md +++ b/README.ja.md @@ -107,7 +107,7 @@ npx skills add webup/skills-cc -s webup-statusline -g **effort アイコンの上書き**は `--effort-icon` で。プリセット:`arrow`(`↯`、デフォルト)、`bolt`(`ϟ`)、`flash`(`⚡`)、`reason`(`∴`)、`dot`(`◉`)、`none`(非表示)。任意の文字も受け付けます。 -> ⚠️ **注意:** 生成されたスクリプトは JSON 解析に `jq` が必要です。スキルは `~/.claude/scripts/statusline.sh` を自動生成し `~/.claude/settings.json` を更新します。Claude Code を再起動すると反映されます。 +> ⚠️ **注意:** 生成されたスクリプトは JSON 解析に `jq` が必要です。Windows では WinGet や scoop でインストールされた jq のパスを自動検出します。それでも jq が見つからない場合は、手動でディレクトリを PATH に追加してください。スキルは `~/.claude/scripts/statusline.sh` を自動生成し `~/.claude/settings.json` を更新します。Claude Code を再起動すると反映されます。 ### 🎰 webup-buddy-reroll diff --git a/README.md b/README.md index 30087a6..39f7a3d 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ The `context` column intentionally has no prefix icon in any theme — the color **Override the effort icon** via `--effort-icon`. Presets: `arrow` (`↯`, default), `bolt` (`ϟ`), `flash` (`⚡`), `reason` (`∴`), `dot` (`◉`), `none` (hide). A raw character is also accepted. -> ⚠️ **Note:** The generated script requires `jq` for JSON parsing. The skill writes to `~/.claude/scripts/statusline.sh` and updates `~/.claude/settings.json` — restart Claude Code to see it. +> ⚠️ **Note:** The generated script requires `jq` for JSON parsing. On Windows, it auto-detects jq installed via WinGet or scoop; if jq is still not found, add its directory to your PATH manually. The skill writes to `~/.claude/scripts/statusline.sh` and updates `~/.claude/settings.json` — restart Claude Code to see it. ### 🎰 webup-buddy-reroll diff --git a/README.zh.md b/README.zh.md index f012886..4d71114 100644 --- a/README.zh.md +++ b/README.zh.md @@ -107,7 +107,7 @@ npx skills add webup/skills-cc -s webup-statusline -g **替换 effort 图标** 用 `--effort-icon`。预设值:`arrow`(`↯`,默认)、`bolt`(`ϟ`)、`flash`(`⚡`)、`reason`(`∴`)、`dot`(`◉`)、`none`(隐藏)。也可直接传入任意字符。 -> ⚠️ **注意:** 生成的脚本需要 `jq` 解析 JSON。本技能会自动写入 `~/.claude/scripts/statusline.sh` 并更新 `~/.claude/settings.json` —— 重启 Claude Code 即可生效。 +> ⚠️ **注意:** 生成的脚本需要 `jq` 解析 JSON。在 Windows 下,脚本会自动检测 WinGet 和 scoop 安装的 jq 路径;若仍无法找到 jq,请手动将其目录添加到 PATH。本技能会自动写入 `~/.claude/scripts/statusline.sh` 并更新 `~/.claude/settings.json` —— 重启 Claude Code 即可生效。 ### 🎰 webup-buddy-reroll diff --git a/scripts/verify_context.py b/scripts/verify_context.py new file mode 100644 index 0000000..43e7a54 --- /dev/null +++ b/scripts/verify_context.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Verify that every statusline theme+element combo generates valid bash.""" + +import subprocess +import sys +import tempfile +import os + +THEMES = ["gruvbox", "dracula", "robbyrussell", "minimal"] +ELEMENT_SETS = [ + "model", + "model,context,effort,git,dir", + "model,context,cost,effort,style,git,dir,worktree,vim", +] +GENERATOR = "skills/webup-statusline/scripts/generate.mjs" + +IS_WINDOWS = sys.platform == "win32" + + +def run(cmd, input=None): + return subprocess.run(cmd, capture_output=True, encoding="utf-8", errors="replace", input=input) + + +def bash_syntax_ok(script): + """Check bash syntax via ``bash -n``. + + Skipped on Windows: Git Bash's ``bash -n`` on GitHub Actions runners + returns exit code 1 with empty stderr for all scripts, making it + unreliable. Linux and macOS CI already cover this check. + + Returns (ok: bool, detail: str). + """ + if IS_WINDOWS: + return True, "(skipped on Windows — Linux/macOS CI covers bash -n)" + + fd, path = tempfile.mkstemp(suffix=".sh") + try: + with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as f: + f.write(script) + br = subprocess.run(["bash", "-n", path], capture_output=True, text=True) + if br.returncode == 0: + return True, "" + return False, br.stderr.strip() + finally: + os.unlink(path) + + +def main(): + errors = [] + for theme in THEMES: + for elements in ELEMENT_SETS: + label = f"{theme} / {elements}" + r = run(["bun", GENERATOR, "--elements", elements, "--theme", theme]) + if r.returncode != 0: + errors.append(f"FAIL generate {label}: {r.stderr.strip()}") + continue + + script = r.stdout + + # 1. bash syntax check (non-Windows only; Windows runners skip this) + ok, detail = bash_syntax_ok(script) + if not ok: + errors.append(f"FAIL bash -n {label}: {detail}") + + # 2. must contain jq auto-detect block + if "command -v jq" not in script: + errors.append(f"FAIL missing jq detect block in {label}") + + # 3. must contain shebang + if not script.startswith("#!/bin/bash"): + errors.append(f"FAIL missing shebang in {label}") + + # 4. must contain at least one jq invocation per requested element + for el in elements.split(","): + el = el.strip() + if el == "model" and "model.display_name" not in script: + errors.append(f"FAIL missing model field in {label}") + if el == "context" and "remaining_percentage" not in script: + errors.append(f"FAIL missing context field in {label}") + if el == "effort" and "effortLevel" not in script: + errors.append(f"FAIL missing effort field in {label}") + if el == "git" and "branch --show-current" not in script: + errors.append(f"FAIL missing git field in {label}") + if el == "dir" and "short_dir" not in script: + errors.append(f"FAIL missing dir field in {label}") + if el == "cost" and "total_cost_usd" not in script: + errors.append(f"FAIL missing cost field in {label}") + if el == "style" and "output_style" not in script: + errors.append(f"FAIL missing style field in {label}") + if el == "worktree" and "is_worktree" not in script: + errors.append(f"FAIL missing worktree field in {label}") + if el == "vim" and "vim.mode" not in script: + errors.append(f"FAIL missing vim field in {label}") + + if errors: + for e in errors: + print(e, file=sys.stderr) + print(f"\n{len(errors)} check(s) failed", file=sys.stderr) + sys.exit(1) + + combos = len(THEMES) * len(ELEMENT_SETS) + print(f"All {combos} theme×element combos passed") + + +if __name__ == "__main__": + main() diff --git a/skills/webup-statusline/SKILL.md b/skills/webup-statusline/SKILL.md index 2456a70..76cc9a3 100644 --- a/skills/webup-statusline/SKILL.md +++ b/skills/webup-statusline/SKILL.md @@ -29,7 +29,7 @@ This skill generates a bash script tailored to your preferences and installs it ## Prerequisites -- **jq** — required by the generated status line script to parse JSON input from Claude Code +- **jq** — required by the generated status line script to parse JSON input from Claude Code. On Windows, the script auto-detects jq installed via WinGet or scoop; if jq is still not found, add its directory to your PATH manually. - **Bun** — required to run the generator. Use `npx -y bun` if not installed globally. ## Usage @@ -186,5 +186,5 @@ Claude Opus 4.7 · low · skills-cc · main - Generated script is saved to `~/.claude/scripts/statusline.sh` - Running the skill again overwrites the existing script — just re-run to change theme or columns -- The script uses `jq` to parse JSON input — make sure it's installed +- The script uses `jq` to parse JSON input — make sure it's installed. On Windows, the script auto-detects WinGet and scoop jq paths; if jq is still not found, add it to PATH manually. - Git dirty detection uses `--no-optional-locks` to avoid interfering with other git operations diff --git a/skills/webup-statusline/scripts/generate.mjs b/skills/webup-statusline/scripts/generate.mjs index 8fd107b..23c3cbd 100644 --- a/skills/webup-statusline/scripts/generate.mjs +++ b/skills/webup-statusline/scripts/generate.mjs @@ -159,6 +159,25 @@ function buildScript() { p(`# Claude Code status line — ${t.name} theme`) p(`# Generated by webup-statusline skill`) p('') + + // Auto-detect jq on Windows (WinGet/scoop paths may not be on PATH) + const jqPathBlock = [ + '# Ensure jq is available (Windows: WinGet/scoop installs may not be on PATH)', + 'if ! command -v jq >/dev/null 2>&1; then', + ' for _jq_dir in \\', + ' "/c/Users/$USERNAME/AppData/Local/Microsoft/WinGet/Links" \\', + ' "/c/Users/$USERNAME/AppData/Local/Microsoft/WinGet/Packages/jqlang.jq_Microsoft.Winget.Source_8wekyb3d8bbwe" \\', + ' "$HOME/scoop/shims" \\', + ' ; do', + ' if [ -d "$_jq_dir" ] && { [ -x "$_jq_dir/jq" ] || [ -x "$_jq_dir/jq.exe" ]; }; then', + ' export PATH="$PATH:$_jq_dir"', + ' break', + ' fi', + ' done', + 'fi', + ] + for (const line of jqPathBlock) p(line) + p('# Colors') p("readonly RST='\\033[0m'") p(`readonly C_MODEL='${t.model}'`)