From d38b42ccda6dec14cfc2b453c71dbe85f157f16d Mon Sep 17 00:00:00 2001 From: Yuan <20144414+baskduf@users.noreply.github.com> Date: Thu, 18 Jun 2026 19:58:17 +0900 Subject: [PATCH] feat: add version and update commands --- README.ja.md | 4 + README.ko.md | 4 + README.md | 4 + README.zh-CN.md | 4 + README.zh-TW.md | 4 + plugins/codex-fable5/bin/codex-fable5 | 139 ++++++++++++++++++++++ tests/test_ci_release.py | 163 ++++++++++++++++++++++++++ tests/test_manifests_docs.py | 2 + 8 files changed, 324 insertions(+) diff --git a/README.ja.md b/README.ja.md index 802e8d5..eb1ff83 100644 --- a/README.ja.md +++ b/README.ja.md @@ -196,6 +196,8 @@ codex-fable5 findings gate | コマンド | 目的 | | --- | --- | | `codex-fable5 status` | findings と goal の進捗を表示します。 | +| `codex-fable5 version` | インストール済み plugin version、パス、git checkout 状態を表示します。 | +| `codex-fable5 update` | FableCodex checkout/plugin package を最新の安定版 `v*` tag に更新します。 | | `codex-fable5 goals create` | ローカルの multi-step goal ledger を作成します。 | | `codex-fable5 goals next` | 次の goal を開始または再開します。 | | `codex-fable5 goals checkpoint` | evidence 付きで goal を complete、failed、blocked にします。 | @@ -210,6 +212,8 @@ codex-fable5 findings gate plugins/codex-fable5/bin/codex-fable5 status ``` +`codex-fable5 update` が変更するのは FableCodex checkout/plugin package だけです。model access、provider credential、hidden runtime behavior は変更しません。最新の安定版ではなく開発 branch を意図的に使う場合だけ、`codex-fable5 update --ref main` を使用してください。 + ## インストール方法 安定版: diff --git a/README.ko.md b/README.ko.md index de0e6c9..2806203 100644 --- a/README.ko.md +++ b/README.ko.md @@ -196,6 +196,8 @@ codex-fable5 findings gate | 명령어 | 용도 | | --- | --- | | `codex-fable5 status` | findings와 goal 진행 상태를 봅니다. | +| `codex-fable5 version` | 설치된 plugin version, 경로, git checkout 상태를 봅니다. | +| `codex-fable5 update` | FableCodex checkout/plugin package를 최신 안정 `v*` tag로 업데이트합니다. | | `codex-fable5 goals create` | 로컬 multi-step goal ledger를 만듭니다. | | `codex-fable5 goals next` | 다음 goal을 시작하거나 재개합니다. | | `codex-fable5 goals checkpoint` | goal을 evidence와 함께 complete, failed, blocked로 표시합니다. | @@ -210,6 +212,8 @@ codex-fable5 findings gate plugins/codex-fable5/bin/codex-fable5 status ``` +`codex-fable5 update`는 FableCodex checkout/plugin package만 변경합니다. model access, provider credential, hidden runtime behavior는 바꾸지 않습니다. 최신 안정 release가 아니라 개발 branch를 의도적으로 쓰려면 `codex-fable5 update --ref main`을 사용하세요. + ## 설치 옵션 안정 버전: diff --git a/README.md b/README.md index 66991bf..833fd81 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,8 @@ The gate fails while `open` or `blocked` findings remain. Final goal completion | Command | Purpose | | --- | --- | | `codex-fable5 status` | Show findings and goal progress. | +| `codex-fable5 version` | Show the installed plugin version, paths, and git checkout state. | +| `codex-fable5 update` | Update the FableCodex checkout/plugin package to the latest stable `v*` tag. | | `codex-fable5 goals create` | Create a local multi-step goal ledger. | | `codex-fable5 goals next` | Start or resume the next goal. | | `codex-fable5 goals checkpoint` | Mark a goal complete, failed, or blocked with evidence. | @@ -210,6 +212,8 @@ Without changing `PATH`, run the checkout helper directly: plugins/codex-fable5/bin/codex-fable5 status ``` +`codex-fable5 update` changes only the FableCodex checkout/plugin package. It does not change model access, provider credentials, or hidden runtime behavior. Use `codex-fable5 update --ref main` only when you intentionally want the development branch instead of the latest stable release. + ## Install Options Stable release: diff --git a/README.zh-CN.md b/README.zh-CN.md index d7b40d8..f8f91a8 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -196,6 +196,8 @@ codex-fable5 findings gate | 命令 | 用途 | | --- | --- | | `codex-fable5 status` | 查看 findings 和 goal 进度。 | +| `codex-fable5 version` | 显示已安装的 plugin version、路径和 git checkout 状态。 | +| `codex-fable5 update` | 将 FableCodex checkout/plugin package 更新到最新稳定 `v*` tag。 | | `codex-fable5 goals create` | 创建本地 multi-step goal ledger。 | | `codex-fable5 goals next` | 开始或继续下一个 goal。 | | `codex-fable5 goals checkpoint` | 带 evidence 将 goal 标记为 complete、failed 或 blocked。 | @@ -210,6 +212,8 @@ codex-fable5 findings gate plugins/codex-fable5/bin/codex-fable5 status ``` +`codex-fable5 update` 只会更改 FableCodex checkout/plugin package。它不会更改 model access、provider credential 或 hidden runtime behavior。只有在你明确想使用开发 branch 而不是最新稳定版时,才使用 `codex-fable5 update --ref main`。 + ## 安装选项 稳定版: diff --git a/README.zh-TW.md b/README.zh-TW.md index dc91b43..6d43a99 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -196,6 +196,8 @@ codex-fable5 findings gate | 指令 | 用途 | | --- | --- | | `codex-fable5 status` | 查看 findings 和 goal 進度。 | +| `codex-fable5 version` | 顯示已安裝的 plugin version、路徑和 git checkout 狀態。 | +| `codex-fable5 update` | 將 FableCodex checkout/plugin package 更新到最新穩定 `v*` tag。 | | `codex-fable5 goals create` | 建立本地 multi-step goal ledger。 | | `codex-fable5 goals next` | 開始或繼續下一個 goal。 | | `codex-fable5 goals checkpoint` | 帶 evidence 將 goal 標記為 complete、failed 或 blocked。 | @@ -210,6 +212,8 @@ codex-fable5 findings gate plugins/codex-fable5/bin/codex-fable5 status ``` +`codex-fable5 update` 只會變更 FableCodex checkout/plugin package。它不會變更 model access、provider credential 或 hidden runtime behavior。只有在你明確想使用開發 branch 而不是最新穩定版時,才使用 `codex-fable5 update --ref main`。 + ## 安裝選項 穩定版: diff --git a/plugins/codex-fable5/bin/codex-fable5 b/plugins/codex-fable5/bin/codex-fable5 index cc89156..b371ab7 100755 --- a/plugins/codex-fable5/bin/codex-fable5 +++ b/plugins/codex-fable5/bin/codex-fable5 @@ -2,24 +2,156 @@ set -eu SCRIPT_DIR=$(CDPATH= cd "$(dirname "$0")" && pwd) +PLUGIN_ROOT=$(CDPATH= cd "$SCRIPT_DIR/.." && pwd) +REPO_ROOT=$(CDPATH= cd "$PLUGIN_ROOT/../.." && pwd) +MANIFEST="$PLUGIN_ROOT/.codex-plugin/plugin.json" GOALS="$SCRIPT_DIR/../skills/codex-fable5/scripts/codex_goals.py" FINDINGS="$SCRIPT_DIR/../skills/codex-fable5/scripts/codex_findings.py" +SKILL_ROOT=$(CDPATH= cd "$SCRIPT_DIR/../skills/codex-fable5" && pwd) +SKILL="$SKILL_ROOT/SKILL.md" usage() { cat <<'EOF' Usage: codex-fable5 status + codex-fable5 version + codex-fable5 update [--ref ] [--no-fetch] codex-fable5 goals [args...] codex-fable5 findings [args...] Examples: codex-fable5 status + codex-fable5 version + codex-fable5 update + codex-fable5 update --ref main codex-fable5 findings add --title "Missing verification" --evidence "No test evidence" codex-fable5 findings gate codex-fable5 goals next EOF } +manifest_value() { + key=$1 + python3 - "$MANIFEST" "$key" <<'PY' +import json +import sys +path, key = sys.argv[1:] +try: + data = json.loads(open(path, encoding="utf-8").read()) +except OSError as exc: + raise SystemExit(f"codex-fable5: could not read manifest: {exc}") +value = data.get(key, "") +print(value if isinstance(value, str) else "") +PY +} + +cmd_version() { + name=$(manifest_value name) + version=$(manifest_value version) + printf '%s %s\n' "$name" "$version" + printf 'plugin: %s\n' "$PLUGIN_ROOT" + printf 'skill: %s\n' "$SKILL" + printf 'wrapper: %s\n' "$0" + if git -C "$REPO_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + branch=$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || printf 'unknown') + commit=$(git -C "$REPO_ROOT" rev-parse --short HEAD 2>/dev/null || printf 'unknown') + if [ -n "$(git -C "$REPO_ROOT" status --porcelain 2>/dev/null || true)" ]; then + state=dirty + else + state=clean + fi + printf 'git: %s %s %s\n' "$branch" "$commit" "$state" + fi +} + +latest_stable_tag() { + git -C "$REPO_ROOT" tag --list 'v[0-9]*' --sort=-v:refname | grep -E '^v[0-9]+(\.[0-9]+)*$' | sed -n '1p' +} + +cmd_update() { + ref="" + fetch=1 + while [ "$#" -gt 0 ]; do + case "$1" in + --ref) + shift + if [ "$#" -eq 0 ]; then + printf '%s\n' "codex-fable5: --ref requires a value" >&2 + exit 2 + fi + ref=$1 + ;; + --no-fetch) + fetch=0 + ;; + -h|--help|help) + printf '%s\n' "Usage: codex-fable5 update [--ref ] [--no-fetch]" + printf '%s\n' "Default updates the FableCodex checkout/plugin package to the latest stable v* tag." + return 0 + ;; + *) + printf '%s\n' "codex-fable5: unknown update option '$1'" >&2 + exit 2 + ;; + esac + shift + done + + current=$(manifest_value version) + printf 'codex-fable5: current version %s\n' "$current" + printf '%s\n' "codex-fable5: updates the FableCodex checkout/plugin package only; it does not change model/provider access." + + if ! git -C "$REPO_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + target=${ref:-''} + printf '%s\n' "codex-fable5: this installation is not inside a git checkout; update with Codex plugin commands:" + printf ' codex plugin marketplace add baskduf/FableCodex --ref %s\n' "$target" + printf '%s\n' " codex plugin add codex-fable5@fablecodex" + printf '%s\n' "codex-fable5: restart Codex after updating." + return 0 + fi + + if [ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]; then + printf '%s\n' "codex-fable5: refusing to update a dirty checkout. Commit, stash, or clean local changes first." >&2 + exit 1 + fi + + if [ "$fetch" -eq 1 ]; then + git -C "$REPO_ROOT" fetch --tags --prune --quiet + fi + + if [ -z "$ref" ]; then + ref=$(latest_stable_tag) + if [ -z "$ref" ]; then + printf '%s\n' "codex-fable5: no stable v* tags found." >&2 + exit 1 + fi + fi + + if [ "$ref" = "main" ]; then + current_ref=$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD) + if [ "$current_ref" != "main" ]; then + git -C "$REPO_ROOT" checkout main + fi + git -C "$REPO_ROOT" pull --ff-only + printf '%s\n' "codex-fable5: updated to main." + else + if ! git -C "$REPO_ROOT" rev-parse --verify --quiet "$ref^{commit}" >/dev/null; then + printf 'codex-fable5: ref not found: %s\n' "$ref" >&2 + exit 1 + fi + current_commit=$(git -C "$REPO_ROOT" rev-parse HEAD) + target_commit=$(git -C "$REPO_ROOT" rev-parse "$ref^{commit}") + if [ "$current_commit" = "$target_commit" ]; then + printf 'codex-fable5: already up to date at %s\n' "$ref" + else + git -C "$REPO_ROOT" checkout "$ref" + printf 'codex-fable5: updated to %s\n' "$ref" + fi + fi + + printf '%s\n' "codex-fable5: restart Codex to reload the plugin." +} + if [ "$#" -eq 0 ]; then usage exit 0 @@ -37,6 +169,13 @@ case "$1" in printf '%s\n' "codex-fable5: no goal plan" fi ;; + version|--version|-v) + cmd_version + ;; + update) + shift + cmd_update "$@" + ;; findings|finding|f) shift exec python3 "$FINDINGS" "$@" diff --git a/tests/test_ci_release.py b/tests/test_ci_release.py index fcaaf90..d2f38c8 100644 --- a/tests/test_ci_release.py +++ b/tests/test_ci_release.py @@ -1,5 +1,7 @@ from __future__ import annotations +import shutil + try: from tests.support import ( BIN, @@ -228,3 +230,164 @@ def test_user_facing_wrappers_run_from_path(self) -> None: self.assertEqual(status_with_plan.returncode, 0, status_with_plan.stderr) self.assertIn("1 open", status_with_plan.stdout) self.assertIn("0/1 complete", status_with_plan.stdout) + + def test_version_command_reports_manifest_and_paths(self) -> None: + env = {**os.environ, "PATH": f"{BIN}{os.pathsep}{os.environ['PATH']}"} + plugin = json.loads((ROOT / "plugins" / "codex-fable5" / ".codex-plugin" / "plugin.json").read_text()) + + result = subprocess.run( + ["codex-fable5", "version"], + cwd=ROOT, + env=env, + text=True, + capture_output=True, + check=False, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn(f"codex-fable5 {plugin['version']}", result.stdout) + self.assertIn("plugin:", result.stdout) + self.assertIn("skill:", result.stdout) + self.assertIn("wrapper:", result.stdout) + self.assertIn("git:", result.stdout) + + def test_update_command_updates_clean_checkout_to_requested_ref(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + clone = Path(tmp) / "FableCodex" + shutil.copytree(ROOT / "plugins", clone / "plugins") + for command in [ + ["git", "init"], + ["git", "config", "user.email", "test@example.invalid"], + ["git", "config", "user.name", "Test User"], + ["git", "add", "plugins"], + ["git", "commit", "-m", "release"], + ["git", "tag", "v0.4.4"], + ]: + result = subprocess.run(command, cwd=clone, text=True, capture_output=True, check=False) + self.assertEqual(result.returncode, 0, result.stderr) + (clone / "NEXT.txt").write_text("next\n", encoding="utf-8") + for command in [["git", "add", "NEXT.txt"], ["git", "commit", "-m", "next"]]: + result = subprocess.run(command, cwd=clone, text=True, capture_output=True, check=False) + self.assertEqual(result.returncode, 0, result.stderr) + + update = subprocess.run( + [ + str(clone / "plugins" / "codex-fable5" / "bin" / "codex-fable5"), + "update", + "--ref", + "v0.4.4", + "--no-fetch", + ], + cwd=clone, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(update.returncode, 0, update.stderr) + self.assertIn("updates the FableCodex checkout/plugin package only", update.stdout) + self.assertIn("restart Codex", update.stdout) + + head = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=clone, + text=True, + capture_output=True, + check=False, + ) + tag = subprocess.run( + ["git", "rev-parse", "v0.4.4^{commit}"], + cwd=clone, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(head.returncode, 0, head.stderr) + self.assertEqual(tag.returncode, 0, tag.stderr) + self.assertEqual(head.stdout.strip(), tag.stdout.strip()) + + def test_update_command_refuses_dirty_checkout(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + clone = Path(tmp) / "FableCodex" + shutil.copytree(ROOT / "plugins", clone / "plugins") + for command in [ + ["git", "init"], + ["git", "config", "user.email", "test@example.invalid"], + ["git", "config", "user.name", "Test User"], + ["git", "add", "plugins"], + ["git", "commit", "-m", "release"], + ["git", "tag", "v0.4.4"], + ]: + result = subprocess.run(command, cwd=clone, text=True, capture_output=True, check=False) + self.assertEqual(result.returncode, 0, result.stderr) + (clone / "DIRTY.txt").write_text("dirty\n", encoding="utf-8") + + update = subprocess.run( + [ + str(clone / "plugins" / "codex-fable5" / "bin" / "codex-fable5"), + "update", + "--ref", + "v0.4.4", + "--no-fetch", + ], + cwd=clone, + text=True, + capture_output=True, + check=False, + ) + + self.assertNotEqual(update.returncode, 0) + self.assertIn("refusing to update a dirty checkout", update.stderr) + + def test_update_default_ignores_prerelease_tags(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + clone = Path(tmp) / "FableCodex" + shutil.copytree(ROOT / "plugins", clone / "plugins") + for command in [ + ["git", "init"], + ["git", "config", "user.email", "test@example.invalid"], + ["git", "config", "user.name", "Test User"], + ["git", "add", "plugins"], + ["git", "commit", "-m", "stable release"], + ["git", "tag", "v1.0.0"], + ]: + result = subprocess.run(command, cwd=clone, text=True, capture_output=True, check=False) + self.assertEqual(result.returncode, 0, result.stderr) + (clone / "RC.txt").write_text("rc\n", encoding="utf-8") + for command in [ + ["git", "add", "RC.txt"], + ["git", "commit", "-m", "release candidate"], + ["git", "tag", "v1.1.0-rc1"], + ["git", "checkout", "-b", "work"], + ]: + result = subprocess.run(command, cwd=clone, text=True, capture_output=True, check=False) + self.assertEqual(result.returncode, 0, result.stderr) + + update = subprocess.run( + [ + str(clone / "plugins" / "codex-fable5" / "bin" / "codex-fable5"), + "update", + "--no-fetch", + ], + cwd=clone, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(update.returncode, 0, update.stderr) + self.assertIn("updated to v1.0.0", update.stdout) + + head = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=clone, + text=True, + capture_output=True, + check=False, + ) + stable = subprocess.run( + ["git", "rev-parse", "v1.0.0^{commit}"], + cwd=clone, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(head.stdout.strip(), stable.stdout.strip()) diff --git a/tests/test_manifests_docs.py b/tests/test_manifests_docs.py index eb4524a..15b90dc 100644 --- a/tests/test_manifests_docs.py +++ b/tests/test_manifests_docs.py @@ -106,6 +106,8 @@ def test_readme_localizations_cover_core_workflow(self) -> None: ] required_snippets = [ "codex-fable5 goals create", + "codex-fable5 version", + "codex-fable5 update", "codex-fable5 findings gate", ".codex-fable5/", "AGPL-3.0-or-later",