diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 0fa3d14..6e81c99 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -12,7 +12,7 @@ body: id: version attributes: label: ipcheck version - placeholder: ipcheck 0.6.1 + placeholder: ipcheck 0.7.0 validations: required: true - type: dropdown diff --git a/CHANGELOG.md b/CHANGELOG.md index 35d7709..0926f7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to ipcheck are documented here. The project follows [Semantic Versioning](https://semver.org/). +## [0.7.0] - 2026-07-22 + +### Added + +- `--explain-score` to show the service-path base score and the separate + download/upload adjustments in human and Markdown reports. +- Machine-readable `developer_readiness.score_breakdown` details in JSON. +- A reproducible animated terminal demo and ready-to-post launch copy. + ## [0.6.1] - 2026-07-22 ### Changed @@ -87,6 +96,7 @@ All notable changes to ipcheck are documented here. The project follows - Homebrew and direct-download packaging. - Median/P95 TTFB, jitter, reference bandwidth, and macOS `networkQuality`. +[0.7.0]: https://github.com/jacklv-coder/ipcheck/compare/v0.6.1...v0.7.0 [0.6.1]: https://github.com/jacklv-coder/ipcheck/compare/v0.6.0...v0.6.1 [0.6.0]: https://github.com/jacklv-coder/ipcheck/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/jacklv-coder/ipcheck/compare/v0.4.0...v0.5.0 diff --git a/README.md b/README.md index b357d9d..9225fb1 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,11 @@ time-to-first-byte (TTFB), jitter, reference bandwidth, and a clear color-coded progress, can be cancelled cleanly with `Ctrl+C`, and finish with a direct answer plus a rule-based readiness score. +![Animated ipcheck terminal demo](assets/ipcheck-demo.gif) + ```text $ ipcheck -ipcheck v0.6.1 — AI coding network diagnostics +ipcheck v0.7.0 — AI coding network diagnostics Developer verdict Ready to code? YES @@ -74,7 +76,7 @@ brew install ipcheck ```bash mkdir -p "$HOME/.local/bin" -curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.6.1/bin/ipcheck \ +curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.7.0/bin/ipcheck \ -o "$HOME/.local/bin/ipcheck" chmod +x "$HOME/.local/bin/ipcheck" ``` @@ -106,12 +108,19 @@ ipcheck --json > ipcheck-report.json ipcheck --markdown > ipcheck-report.md ``` +Explain every score component: + +```bash +ipcheck --explain-score +``` + Tune or override a check: ```bash ipcheck --samples 10 ipcheck --timeout 30 ipcheck --system +ipcheck --explain-score ipcheck --endpoint https://your-gateway.example.com/health ipcheck --lang zh ipcheck --no-progress --no-color @@ -148,7 +157,8 @@ percentiles. Rule v1 starts from the service-path verdict (`90` healthy, `70` usable but delayed, `45` reachable but temporarily unavailable, `35` poor, or `0` blocked). Completed bandwidth ratings can then add up to 5 points or deduct up to 10; partial samples deduct 2 points each. JSON exposes `score`, -`score_label`, and `score_method` for automation. +`score_label`, `score_method`, and the per-component `score_breakdown` for +automation. Run `ipcheck --help` for the complete option and environment-variable list. diff --git a/README.zh-CN.md b/README.zh-CN.md index e490168..f85f25f 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -15,9 +15,11 @@ 实时进度,支持通过 `Ctrl+C` 友好取消,并直接告诉你“现在是否适合开发”以及 规则化的开发适配分。 +![ipcheck 终端动态演示](assets/ipcheck-demo.gif) + ```text $ ipcheck -ipcheck v0.6.1 — AI 编程网络诊断 +ipcheck v0.7.0 — AI 编程网络诊断 开发建议 现在适合开发吗?适合 @@ -68,7 +70,7 @@ brew install ipcheck ```bash mkdir -p "$HOME/.local/bin" -curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.6.1/bin/ipcheck \ +curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.7.0/bin/ipcheck \ -o "$HOME/.local/bin/ipcheck" chmod +x "$HOME/.local/bin/ipcheck" ``` @@ -98,12 +100,19 @@ ipcheck --json > ipcheck-report.json ipcheck --markdown > ipcheck-report.md ``` +查看每一项评分依据: + +```bash +ipcheck --explain-score +``` + 其他常用选项: ```bash ipcheck --samples 10 ipcheck --timeout 30 ipcheck --system +ipcheck --explain-score ipcheck --endpoint https://your-gateway.example.com/health ipcheck --lang en ipcheck --no-progress --no-color @@ -132,7 +141,8 @@ ipcheck --no-upload 开发适配分是透明的规则评分,不代表“超过多少用户”的百分位。规则 v1 根据 服务链路结论从 90(健康)、70(可用但偏慢)、45(可达但服务暂不可用)、 35(较差)或 0(阻断)起算;完整的上下行测速最多加 5 分或扣 10 分,部分 -样本每项扣 2 分。JSON 会输出 `score`、`score_label` 和 `score_method`。 +样本每项扣 2 分。JSON 会输出 `score`、`score_label`、`score_method`,以及逐项的 +`score_breakdown`。 运行 `ipcheck --help` 可以查看完整参数。 diff --git a/assets/ipcheck-demo.gif b/assets/ipcheck-demo.gif new file mode 100644 index 0000000..4ab92d1 Binary files /dev/null and b/assets/ipcheck-demo.gif differ diff --git a/bin/ipcheck b/bin/ipcheck index 710bd73..6218974 100755 --- a/bin/ipcheck +++ b/bin/ipcheck @@ -10,7 +10,7 @@ unset LC_ALL LC_NUMERIC=C export LC_NUMERIC -VERSION="0.6.1" +VERSION="0.7.0" SCHEMA_VERSION=1 SAMPLES=3 TIMEOUT=15 @@ -27,6 +27,7 @@ RUN_SYSTEM=0 COLOR_MODE="auto" PROGRESS_MODE="${IPCHECK_PROGRESS:-auto}" LANG_MODE="${IPCHECK_LANG:-auto}" +EXPLAIN_SCORE=0 usage() { cat <<'EOF' @@ -51,6 +52,7 @@ Options: --progress Always emit human-readable progress to stderr. --no-progress Disable progress output. --lang LANG Output language: auto, en, or zh (default: auto). + --explain-score Show how the readiness score was calculated. -v, --version Show the ipcheck version. -h, --help Show this help. @@ -151,6 +153,7 @@ while [ "$#" -gt 0 ]; do LANG_MODE="$2" shift ;; + --explain-score) EXPLAIN_SCORE=1 ;; -v|--version) printf 'ipcheck %s\n' "$VERSION"; exit 0 ;; -h|--help) usage; exit 0 ;; *) die "unknown option: $1 (use --help)" ;; @@ -1276,24 +1279,28 @@ esac # A transparent heuristic, not a user percentile. Service-path quality carries # most of the score; completed bandwidth samples can move it by at most 10. case "$overall" in - good) readiness_score=90 ;; + good) readiness_score_base=90 ;; fair) - if [ "$usable_services" -gt 0 ]; then readiness_score=70; else readiness_score=45; fi + if [ "$usable_services" -gt 0 ]; then readiness_score_base=70; else readiness_score_base=45; fi ;; - poor) readiness_score=35 ;; - *) readiness_score=0 ;; + poor) readiness_score_base=35 ;; + *) readiness_score_base=0 ;; esac +download_score_adjustment=0 +upload_score_adjustment=0 +readiness_score="$readiness_score_base" if [ "$readiness_score" -gt 0 ]; then case "$download_rating" in - fast) readiness_score=$((readiness_score + 2)) ;; - slow) readiness_score=$((readiness_score - 5)) ;; - partial) readiness_score=$((readiness_score - 2)) ;; + fast) download_score_adjustment=2 ;; + slow) download_score_adjustment=-5 ;; + partial) download_score_adjustment=-2 ;; esac case "$upload_rating" in - fast) readiness_score=$((readiness_score + 3)) ;; - slow) readiness_score=$((readiness_score - 5)) ;; - partial) readiness_score=$((readiness_score - 2)) ;; + fast) upload_score_adjustment=3 ;; + slow) upload_score_adjustment=-5 ;; + partial) upload_score_adjustment=-2 ;; esac + readiness_score=$((readiness_score + download_score_adjustment + upload_score_adjustment)) fi [ "$readiness_score" -ge 0 ] || readiness_score=0 [ "$readiness_score" -le 100 ] || readiness_score=100 @@ -1336,6 +1343,24 @@ service_display() { awk -F '\t' -v service="$1" '$1 == service { print $2; exit }' "$SERVICES_FILE" } +signed_score() { + if [ "$1" -ge 0 ]; then printf '+%s' "$1"; else printf '%s' "$1"; fi +} + +render_score_breakdown() { + download_score_label=$(localized_bandwidth_rating "$download_rating") + upload_score_label=$(localized_bandwidth_rating "$upload_rating") + if [ "$UI_LANG" = "zh" ]; then + printf ' 评分依据:服务链路 %s;下载 %s %s;上传 %s %s;总分 %s。\n' \ + "$readiness_score_base" "$download_score_label" "$(signed_score "$download_score_adjustment")" \ + "$upload_score_label" "$(signed_score "$upload_score_adjustment")" "$readiness_score" + else + printf ' Score breakdown: service path %s; download %s %s; upload %s %s; total %s.\n' \ + "$readiness_score_base" "$download_score_label" "$(signed_score "$download_score_adjustment")" \ + "$upload_score_label" "$(signed_score "$upload_score_adjustment")" "$readiness_score" + fi +} + render_human() { if [ "$UI_LANG" = "zh" ]; then printf '%sipcheck v%s — AI 编程网络诊断%s\n' "$C_BOLD$C_CYAN" "$VERSION" "$C_RESET" @@ -1343,12 +1368,14 @@ render_human() { printf '\n%s开发建议%s\n' "$C_BOLD" "$C_RESET" printf ' 现在适合开发吗?%s%s%s\n' "$C_BOLD" "$readiness_answer" "$C_RESET" printf ' 开发适配分:%s%s/100 · %s%s\n' "$C_BOLD" "$readiness_score" "$readiness_score_label" "$C_RESET" + [ "$EXPLAIN_SCORE" -eq 0 ] || render_score_breakdown else printf '%sipcheck v%s — AI coding network diagnostics%s\n' "$C_BOLD$C_CYAN" "$VERSION" "$C_RESET" printf 'Time (UTC): %s\nNetwork path: %s\n' "$timestamp" "$proxy_text" printf '\n%sDeveloper verdict%s\n' "$C_BOLD" "$C_RESET" printf ' Ready to code? %s%s%s\n' "$C_BOLD" "$readiness_answer" "$C_RESET" printf ' Readiness score: %s%s/100 · %s%s\n' "$C_BOLD" "$readiness_score" "$readiness_score_label" "$C_RESET" + [ "$EXPLAIN_SCORE" -eq 0 ] || render_score_breakdown fi case "$overall" in good) verdict_color="$C_GREEN" ;; @@ -1449,6 +1476,11 @@ render_markdown() { printf -- '- 结果:**%s** — %s\n' "$(printf '%s' "$overall" | tr '[:lower:]' '[:upper:]')" "$localized_overall_reason" printf -- '- 现在适合开发吗?**%s** — %s\n\n' "$readiness_answer" "$readiness_summary" printf -- '- 开发适配分:**%s/100 · %s**(规则评分,不是用户百分位)\n\n' "$readiness_score" "$readiness_score_label" + if [ "$EXPLAIN_SCORE" -eq 1 ]; then + printf -- '- 评分依据:服务链路 %s;下载 %s %s;上传 %s %s;总分 %s。\n\n' \ + "$readiness_score_base" "$(localized_bandwidth_rating "$download_rating")" "$(signed_score "$download_score_adjustment")" \ + "$(localized_bandwidth_rating "$upload_rating")" "$(signed_score "$upload_score_adjustment")" "$readiness_score" + fi printf '## 检测到的客户端\n\n' printf '| 客户端 | 模型 | 路由 | 配置来源 | 结果 |\n|---|---|---|---|---|\n' else @@ -1458,6 +1490,11 @@ render_markdown() { printf -- '- Result: **%s** — %s\n' "$(printf '%s' "$overall" | tr '[:lower:]' '[:upper:]')" "$localized_overall_reason" printf -- '- Ready to code? **%s** — %s\n\n' "$readiness_answer" "$readiness_summary" printf -- '- Readiness score: **%s/100 · %s** (rule-based, not a user percentile)\n\n' "$readiness_score" "$readiness_score_label" + if [ "$EXPLAIN_SCORE" -eq 1 ]; then + printf -- '- Score breakdown: service path %s; download %s %s; upload %s %s; total %s.\n\n' \ + "$readiness_score_base" "$(localized_bandwidth_rating "$download_rating")" "$(signed_score "$download_score_adjustment")" \ + "$(localized_bandwidth_rating "$upload_rating")" "$(signed_score "$upload_score_adjustment")" "$readiness_score" + fi printf '## Detected clients\n\n' printf '| Client | Model | Route | Source | Result |\n|---|---|---|---|---|\n' fi @@ -1534,9 +1571,9 @@ render_json() { warnings_json="$warnings_json$separator\"$(json_escape "$warning")\"" separator="," done < "$WARNINGS_FILE" - printf '{"schema_version":%s,"version":"%s","timestamp":"%s","proxy":"%s","result":"%s","reason":"%s","developer_readiness":{"ready":%s,"level":"%s","score":%s,"score_label":"%s","score_method":"rule_v1","summary":"%s","recommendation":"%s"},"reachable_endpoints":%s,"primary_success_rate_pct":%s,"primary_ttfb_median_ms":%s,"primary_jitter_ms":%s,"privacy":{"credentials_used":false,"billable_requests":false,"upload_payload":"zero-filled"},"warnings":[%s],"bandwidth":{"enabled":%s,"available":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s,"path":"%s","download":{"enabled":%s,"available":%s,"complete":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s,"mbps":%s,"rating":"%s"},"upload":{"enabled":%s,"available":%s,"complete":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s,"mbps":%s,"rating":"%s"}},"system":{"enabled":%s,"interface":"%s","base_rtt_ms":%s,"download_bits_per_second":%s,"upload_bits_per_second":%s,"proxy_state":"%s"},"services":[%s],"endpoints":[%s]}\n' \ + printf '{"schema_version":%s,"version":"%s","timestamp":"%s","proxy":"%s","result":"%s","reason":"%s","developer_readiness":{"ready":%s,"level":"%s","score":%s,"score_label":"%s","score_method":"rule_v1","score_breakdown":{"service_path":%s,"download":%s,"upload":%s},"summary":"%s","recommendation":"%s"},"reachable_endpoints":%s,"primary_success_rate_pct":%s,"primary_ttfb_median_ms":%s,"primary_jitter_ms":%s,"privacy":{"credentials_used":false,"billable_requests":false,"upload_payload":"zero-filled"},"warnings":[%s],"bandwidth":{"enabled":%s,"available":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s,"path":"%s","download":{"enabled":%s,"available":%s,"complete":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s,"mbps":%s,"rating":"%s"},"upload":{"enabled":%s,"available":%s,"complete":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s,"mbps":%s,"rating":"%s"}},"system":{"enabled":%s,"interface":"%s","base_rtt_ms":%s,"download_bits_per_second":%s,"upload_bits_per_second":%s,"proxy_state":"%s"},"services":[%s],"endpoints":[%s]}\n' \ "$SCHEMA_VERSION" "$VERSION" "$timestamp" "$(json_escape "$proxy_text")" "$overall" "$(json_escape "$overall_reason")" \ - "$readiness_ready" "$readiness_level" "$readiness_score" "$(json_escape "$readiness_score_label_en")" "$(json_escape "$readiness_summary_en")" "$(json_escape "$readiness_recommendation_en")" \ + "$readiness_ready" "$readiness_level" "$readiness_score" "$(json_escape "$readiness_score_label_en")" "$readiness_score_base" "$download_score_adjustment" "$upload_score_adjustment" "$(json_escape "$readiness_summary_en")" "$(json_escape "$readiness_recommendation_en")" \ "$reachable_count" "$primary_success_rate" "$primary_ttfb" "$primary_jitter" "$warnings_json" \ "$([ "$RUN_BANDWIDTH" -eq 1 ] && printf true || printf false)" "$legacy_bandwidth_available" "$legacy_bandwidth_code" "$legacy_bandwidth_bytes" "$legacy_bandwidth_speed" "$(json_escape "$proxy_text")" \ "$([ "$RUN_BANDWIDTH" -eq 1 ] && printf true || printf false)" "$([ "$bandwidth_valid" -eq 1 ] && printf true || printf false)" "$bandwidth_complete" "$bandwidth_code" "$bandwidth_bytes" "$bandwidth_speed" "$download_mbps" "$download_rating" \ diff --git a/demo/render_demo.py b/demo/render_demo.py new file mode 100644 index 0000000..577d00e --- /dev/null +++ b/demo/render_demo.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +"""Render the README GIF from deterministic output of the real ipcheck CLI. + +Requires Python 3 and Pillow. No live service or credential is used. +""" + +from __future__ import annotations + +import os +import re +import subprocess +import tempfile +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + + +ROOT = Path(__file__).resolve().parents[1] +OUTPUT = ROOT / "assets" / "ipcheck-demo.gif" +WIDTH, HEIGHT = 1200, 720 +BACKGROUND = "#0d1117" +PANEL = "#161b22" +TEXT = "#c9d1d9" +MUTED = "#8b949e" +GREEN = "#3fb950" +YELLOW = "#d29922" +CYAN = "#58a6ff" +RED = "#f85149" +ANSI_RE = re.compile(r"\x1b\[[0-9;]*m") + + +def font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont: + candidates = [ + Path("/System/Library/Fonts/Menlo.ttc"), + Path("/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf" if bold else "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"), + ] + for candidate in candidates: + if candidate.exists(): + return ImageFont.truetype(str(candidate), size=size, index=1 if bold and candidate.suffix == ".ttc" else 0) + return ImageFont.load_default() + + +BODY_FONT = font(21) +BODY_BOLD = font(21, bold=True) +TITLE_FONT = font(17, bold=True) + + +def demo_output() -> list[str]: + with tempfile.TemporaryDirectory(prefix="ipcheck-demo-") as directory: + fixture = Path(directory) + home = fixture / "home" + codex = home / ".codex" + claude = home / ".claude" + codex.mkdir(parents=True) + claude.mkdir(parents=True) + (codex / "config.toml").write_text('model = "gpt-5.6-sol"\n', encoding="utf-8") + (claude / "settings.json").write_text( + '{"env":{"ANTHROPIC_BASE_URL":"https://dashscope.aliyuncs.com/apps/anthropic",' + '"ANTHROPIC_MODEL":"deepseek-v4-flash"}}\n', + encoding="utf-8", + ) + curl = fixture / "curl" + curl.write_text( + """#!/usr/bin/env bash +is_download=0 +is_upload=0 +is_claude=0 +for argument in "$@"; do + case "$argument" in + *__down*) is_download=1 ;; + *__up*) is_upload=1 ;; + */v1/messages) is_claude=1 ;; + esac +done +if [ "$is_upload" -eq 1 ]; then + printf '200\\t1000000\\t2000000' +elif [ "$is_download" -eq 1 ]; then + printf '200\\t2000000\\t10000000' +elif [ "$is_claude" -eq 1 ]; then + printf '403\\t0.020\\t0.040\\t0.060\\t0.220\\t0.240\\t151\\t1000' +else + printf '401\\t0.020\\t0.040\\t0.060\\t0.260\\t0.280\\t151\\t1000' +fi +""", + encoding="utf-8", + ) + curl.chmod(0o755) + environment = os.environ.copy() + for key in ( + "ANTHROPIC_AUTH_TOKEN", + "ANTHROPIC_API_KEY", + "ANTHROPIC_BASE_URL", + "ANTHROPIC_MODEL", + "OPENAI_API_KEY", + "CODEX_API_KEY", + "CODEX_NETWORK_ENDPOINTS", + "CLAUDE_NETWORK_ENDPOINTS", + "IPCHECK_ENDPOINTS", + "IPCHECK_SERVICES", + "HTTP_PROXY", + "http_proxy", + "https_proxy", + "ALL_PROXY", + "all_proxy", + ): + environment.pop(key, None) + environment.update( + { + "PATH": f"{fixture}{os.pathsep}{environment['PATH']}", + "HOME": str(home), + "CODEX_HOME": str(codex), + "CLAUDE_CONFIG_DIR": str(claude), + "HTTPS_PROXY": "http://127.0.0.1:1080", + "IPCHECK_LANG": "en", + "IPCHECK_PROGRESS": "always", + } + ) + process = subprocess.run( + [str(ROOT / "bin" / "ipcheck"), "all", "--samples", "1", "--no-color", "--explain-score"], + cwd=ROOT, + env=environment, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + ) + progress = [line for line in process.stderr.splitlines() if "Ctrl+C" in line or line.startswith("Checking")] + report_prefixes = ( + "ipcheck v", + "Developer verdict", + " Ready to code?", + " Readiness score:", + " Score breakdown:", + " This network", + "Detected clients", + " Codex", + " Claude Code", + "AI service latency", + " Time to first byte", + " OK", + "AI service results", + "Network bandwidth", + " Download", + " Upload", + " Advice", + "Result:", + "Interpretation:", + ) + report = [line for line in process.stdout.splitlines() if line.startswith(report_prefixes)] + return [ANSI_RE.sub("", line).removeprefix("⌨ ") for line in progress + report] + + +def line_color(line: str) -> str: + if any(token in line for token in ("95/100", "GOOD", "FAST", "Ready to code? YES")): + return GREEN + if line.startswith(("Developer verdict", "Detected clients", "AI service latency", "Network bandwidth")): + return CYAN + if line.startswith(("Checking", "Press Ctrl+C")): + return MUTED + if "!" in line or "WARNING" in line: + return YELLOW + if "BLOCKED" in line or "POOR" in line: + return RED + return TEXT + + +def frame(command: str, lines: list[str]) -> Image.Image: + image = Image.new("RGB", (WIDTH, HEIGHT), BACKGROUND) + draw = ImageDraw.Draw(image) + draw.rounded_rectangle((22, 18, WIDTH - 22, HEIGHT - 18), radius=16, fill=PANEL, outline="#30363d", width=2) + draw.ellipse((48, 42, 66, 60), fill="#ff5f56") + draw.ellipse((76, 42, 94, 60), fill="#ffbd2e") + draw.ellipse((104, 42, 122, 60), fill="#27c93f") + draw.text((WIDTH // 2, 51), "ipcheck — terminal", font=TITLE_FONT, fill=MUTED, anchor="mm") + draw.line((44, 78, WIDTH - 44, 78), fill="#30363d", width=1) + draw.text((48, 98), "$", font=BODY_BOLD, fill=GREEN) + draw.text((76, 98), command, font=BODY_FONT, fill=TEXT) + + max_lines = 24 + visible = lines[-max_lines:] + y = 138 + for line in visible: + clipped = line if len(line) <= 91 else line[:88] + "..." + draw.text((48, y), clipped, font=BODY_FONT, fill=line_color(clipped)) + y += 23 + return image + + +def main() -> None: + output = demo_output() + command = "ipcheck --explain-score" + frames: list[Image.Image] = [] + durations: list[int] = [] + + for index in range(0, len(command) + 1, 2): + frames.append(frame(command[:index] + ("▋" if index < len(command) else ""), [])) + durations.append(80) + frames.append(frame(command, [])) + durations.append(350) + + for index in range(1, len(output) + 1): + frames.append(frame(command, output[:index])) + durations.append(95 if output[index - 1] else 45) + durations[-1] = 3200 + + OUTPUT.parent.mkdir(parents=True, exist_ok=True) + frames[0].save( + OUTPUT, + save_all=True, + append_images=frames[1:], + duration=durations, + loop=0, + optimize=True, + disposal=2, + ) + print(f"Rendered {OUTPUT} ({OUTPUT.stat().st_size / 1024:.0f} KiB, {len(frames)} frames)") + + +if __name__ == "__main__": + main() diff --git a/docs/launch-kit.md b/docs/launch-kit.md new file mode 100644 index 0000000..a4a827a --- /dev/null +++ b/docs/launch-kit.md @@ -0,0 +1,81 @@ +# ipcheck launch kit + +Use these as starting points, then answer comments in your own voice. Do not add +percentile claims: the readiness score is deliberately rule-based. + +## Show HN + +**Title** + +Show HN: ipcheck – Diagnose the network path used by Codex and Claude Code + +**Post** + +I built ipcheck after repeatedly wondering whether an AI coding CLI was slow +because of the model, my proxy, or the configured gateway. + +It is a zero-dependency Bash CLI that detects Codex and Claude Code routes, then +measures reachability, TTFB median/P95, jitter, and small reference download and +upload samples. It gives each client a separate verdict and a transparent +readiness score. HTTP 401/403 is treated as reachable, so no API key or billable +model request is needed. + +Install: + +```sh +brew tap jacklv-coder/tap +brew install ipcheck +ipcheck +``` + +GitHub: https://github.com/jacklv-coder/ipcheck + +I would especially value feedback from developers using proxies, VPNs, or +Anthropic-compatible gateways. + +## Reddit + +**Title** + +I made a CLI that tells you whether Codex/Claude Code is slow because of your network path + +**Post** + +When an AI coding session feels slow, a normal speed test is often misleading: +TTFB, jitter, failures, proxy routing, and the actual API gateway matter more +than peak download speed. + +ipcheck tests the real routes configured for Codex and Claude Code without +reading or sending credentials. It reports per-client latency and reachability, +small reference download/upload measurements, and a plain-language “ready to +code?” verdict. It is MIT licensed and runs with Bash + curl on macOS and Linux. + +Repo and demo: https://github.com/jacklv-coder/ipcheck + +## V2EX / 掘金 + +**标题** + +做了一个 ipcheck:一条命令判断 Codex / Claude Code 的代理网络是否适合开发 + +**正文** + +用 Codex 或 Claude Code 时,卡顿到底是模型慢、代理节点慢,还是网关配置错了, +普通测速很难回答。 + +我做了一个开源命令行工具 ipcheck。它会自动识别 Codex 和 Claude Code 的真实 +网络路径,检测可达性、TTFB 中位数/P95、抖动,并用少量数据测试参考下载和 +上传速度,最后直接给出“现在是否适合开发”和透明的规则评分。HTTP 401/403 +只代表链路可达;工具不会读取或发送 API Key,也不会产生模型调用费用。 + +安装: + +```sh +brew tap jacklv-coder/tap +brew install ipcheck +ipcheck +``` + +GitHub(MIT):https://github.com/jacklv-coder/ipcheck + +如果你使用代理、VPN、国内网关或 Anthropic 兼容接口,欢迎贴匿名报告反馈。 diff --git a/package.json b/package.json index b890e50..74b05de 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@jacklv-coder/ipcheck", "private": true, - "version": "0.6.1", + "version": "0.7.0", "description": "Diagnose proxy, latency, and API routes used by Codex and Claude Code.", "license": "MIT", "author": "Jintao", diff --git a/test/smoke.sh b/test/smoke.sh index ab53c95..1b4ac60 100755 --- a/test/smoke.sh +++ b/test/smoke.sh @@ -185,7 +185,8 @@ run_ipcheck_direct() { bash -n "$PROJECT_DIR/bin/ipcheck" "$PROJECT_DIR/bin/ipcheck" --help | grep -q '^ipcheck - diagnose Codex and Claude Code' -[ "$("$PROJECT_DIR/bin/ipcheck" --version)" = "ipcheck 0.6.1" ] +[ "$("$PROJECT_DIR/bin/ipcheck" --version)" = "ipcheck 0.7.0" ] +"$PROJECT_DIR/bin/ipcheck" --help | grep -q -- '--explain-score' : > "$CURL_LOG" report=$(ANTHROPIC_AUTH_TOKEN="runtime-secret-must-never-appear" run_ipcheck --samples 3 --no-bandwidth --json) @@ -472,6 +473,7 @@ grep -q '^env:ALL_PROXY=$' "$CURL_LOG" bandwidth_report=$(run_ipcheck --samples 1 --json) printf '%s\n' "$bandwidth_report" | grep -q '"score":95,"score_label":"COMFORTABLE","score_method":"rule_v1"' +printf '%s\n' "$bandwidth_report" | grep -q '"score_breakdown":{"service_path":90,"download":2,"upload":3}' printf '%s\n' "$bandwidth_report" | grep -q '"bandwidth":{"enabled":true,"available":true,"http_code":"200"' printf '%s\n' "$bandwidth_report" | grep -q '"download":{"enabled":true,"available":true,"complete":true,"http_code":"200","bytes":2000000,"bytes_per_second":10000000,"mbps":80.0,"rating":"fast"}' printf '%s\n' "$bandwidth_report" | grep -q '"upload":{"enabled":true,"available":true,"complete":true,"http_code":"200","bytes":1000000,"bytes_per_second":2000000,"mbps":16.0,"rating":"fast"}' @@ -482,10 +484,17 @@ printf '%s\n' "$bandwidth_human" | grep -q '开发适配分:95/100 · 舒适' printf '%s\n' "$bandwidth_human" | grep -q '下载 80.0 Mbps.*快.*Cloudflare,最多 2 MB' printf '%s\n' "$bandwidth_human" | grep -q '上传 16.0 Mbps.*快.*Cloudflare,最多 1 MB 零字节' printf '%s\n' "$bandwidth_human" | grep -q '路径 HTTPS_PROXY=http://127.0.0.1:1080' - -slow_upload_human=$(IPCHECK_TEST_UPLOAD_SPEED=100000 IPCHECK_LANG=en run_ipcheck --samples 1) +score_explanation=$(IPCHECK_LANG=en run_ipcheck --samples 1 --explain-score) +printf '%s\n' "$score_explanation" | grep -q 'Score breakdown: service path 90; download FAST +2; upload FAST +3; total 95.' +score_explanation_zh=$(IPCHECK_LANG=zh run_ipcheck --samples 1 --markdown --explain-score) +printf '%s\n' "$score_explanation_zh" | grep -q '评分依据:服务链路 90;下载 快 +2;上传 快 +3;总分 95。' +score_explanation_without_bandwidth=$(IPCHECK_LANG=en run_ipcheck --samples 1 --no-bandwidth --explain-score) +printf '%s\n' "$score_explanation_without_bandwidth" | grep -q 'Score breakdown: service path 90; download UNAVAILABLE +0; upload UNAVAILABLE +0; total 90.' + +slow_upload_human=$(IPCHECK_TEST_UPLOAD_SPEED=100000 IPCHECK_LANG=en run_ipcheck --samples 1 --explain-score) printf '%s\n' "$slow_upload_human" | grep -q 'Upload.*0.8 Mbps.*SLOW' printf '%s\n' "$slow_upload_human" | grep -q 'Readiness score: 87/100 · GOOD' +printf '%s\n' "$slow_upload_human" | grep -q 'upload SLOW -5; total 87.' printf '%s\n' "$slow_upload_human" | grep -q 'Upload is slow; sending large code contexts may take longer.' invalid_bandwidth_report=$(IPCHECK_TEST_BANDWIDTH_CODE=407 run_ipcheck --samples 1 --json) printf '%s\n' "$invalid_bandwidth_report" | grep -q '"bandwidth":{"enabled":true,"available":false,"http_code":"407","bytes":0,"bytes_per_second":0,'