From 421a123fdf41a6038e999928ddf67610f0a99b90 Mon Sep 17 00:00:00 2001 From: rinjugatla Date: Sat, 8 Aug 2026 16:27:44 +0900 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Windows=20=E3=81=A7=20plugin=20i?= =?UTF-8?q?nstall=20=E3=81=8C=20ENOTDIR=20=E3=81=AB=E3=81=AA=E3=82=8B=20sy?= =?UTF-8?q?mlink=20=E4=BE=9D=E5=AD=98=E3=82=92=E5=A4=96=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows で marketplace を登録した後の `claude plugin install` が `ENOTDIR: not a directory, scandir '...\plugins\reviewable-html-workbench'` で失敗していた。 原因は marketplace の source が指す plugins/reviewable-html-workbench が repo root (..) への symlink だったこと。Windows の git は既定で core.symlinks=false (Git for Windows のインストーラが system gitconfig へ 書く) のため、symlink をリンク先文字列 ".." が入った 2 バイトの通常ファイル として checkout する。そこを plugin root として走査した時点で落ちる。 symlink の解決先は元々 repo root なので、source を repo root 自身へ向ければ 経路が 1 段短くなるだけで解決先は変わらない。Claude 側 (.claude-plugin/marketplace.json の source) と Codex 側 (.agents/plugins/marketplace.json の source.path) を "./" にし、symlink は 廃止した。tracked file に symlink を持ち込まないことを test で固定する。 symlink 前提だった test_codex_marketplace_entry_points_to_plugin_root は 新実装の期待値へ更新し、Claude 側 source の検査を新規に追加。 検証: Windows では symlink が壊れたままの copy から marketplace add と plugin install が通り、cache に skill 3 件が展開されることを確認。Linux コンテナでは修正前の構成 (symlink を復元し source を旧パスへ戻したもの) と 修正後の構成を両方 install し、生成された cache が plugins/ と marketplace.json の 1 行を除いて完全に一致することを diff -r で確認した。 unittest は .sh を exec できない Windows 環境固有の既存失敗 10 件を除き pass (修正前は symlink 起因の 1 件を加えた 11 件)。Codex CLI が無いため Codex 側の 実機検証は未了。 core.symlinks=true での回避も成立はするが、symlink 作成権限に依存し、かつ global 由来の設定は clone 先の .git/config に記録されないため、false へ 戻すと次の checkout で再発する。symlink 自体を廃止すれば依存が消える。 version は bump していない。`claude plugin install` のローカル導入手順の修正を 別 PR で並行して出す予定で、双方が version を上げると衝突するため、bump の 要否と値の判断は作者に委ねる (AGENTS.md の規約に対する意図的な逸脱)。 --- .agents/plugins/marketplace.json | 2 +- .claude-plugin/marketplace.json | 2 +- docs/codex-plugin-packaging.md | 4 +++- docs/design.html | 2 +- docs/development-plan.html | 2 +- plugins/reviewable-html-workbench | 1 - tests/test_project_layout.py | 26 ++++++++++++++++++++++---- 7 files changed, 29 insertions(+), 10 deletions(-) delete mode 120000 plugins/reviewable-html-workbench diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index b14ec97..eefcf31 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -8,7 +8,7 @@ "name": "reviewable-html-workbench", "source": { "source": "local", - "path": "./plugins/reviewable-html-workbench" + "path": "./" }, "policy": { "installation": "INSTALLED_BY_DEFAULT", diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ed794aa..f284c58 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -15,7 +15,7 @@ "author": { "name": "u1" }, - "source": "./plugins/reviewable-html-workbench" + "source": "./" } ] } diff --git a/docs/codex-plugin-packaging.md b/docs/codex-plugin-packaging.md index 1bc9615..54a7344 100644 --- a/docs/codex-plugin-packaging.md +++ b/docs/codex-plugin-packaging.md @@ -4,7 +4,9 @@ `reviewable-html-workbench` は Codex plugin repo を管理元にする。base repo の `home/skills` / `home/generated/codex-skills` には同名 skill を置かず、必要になった場合だけ薄い adapter skill を追加する。 -この repo は plugin root と marketplace root を兼ねる。Codex marketplace loader の既存パターンに合わせるため、`plugins/reviewable-html-workbench` は repo root への symlink として置く。`codex plugin marketplace add ` で `.agents/plugins/marketplace.json` が読まれ、`reviewable-html-workbench` plugin は `source.path: "./plugins/reviewable-html-workbench"` から読み込まれる。 +この repo は plugin root と marketplace root を兼ねる。`codex plugin marketplace add ` で `.agents/plugins/marketplace.json` が読まれ、`reviewable-html-workbench` plugin は `source.path: "./"`、つまり marketplace root 自体から読み込まれる。 + +以前は Codex loader の既存パターンに合わせて `plugins/reviewable-html-workbench` を repo root (`..`) への symlink として置き、`source.path` をそこへ向けていた。Windows の git は既定 (`core.symlinks=false`) で symlink をリンク先文字列を書いた通常ファイルとして checkout するため、その path を plugin root として走査した Claude Code / Codex が `ENOTDIR` で install に失敗する。symlink の解決先は元々 repo root なので、`source.path` を `"./"` に直せば POSIX 側の解決結果は変わらないまま Windows でも成立する。この理由から symlink は廃止し、tracked file に symlink を置かないことを `tests/test_project_layout.py` で検証する。 ## ローカル登録手順 diff --git a/docs/design.html b/docs/design.html index 98e1b18..3911c6d 100644 --- a/docs/design.html +++ b/docs/design.html @@ -851,7 +851,7 @@

plugin化の判断

- 採用方針は、専用repoを plugin repo の管理元にし、visual-html-renderer と reviewable-design-doc を同梱する形。Codex側は .agents/plugins/marketplace.json を持つ repo-local marketplace として登録し、plugins/reviewable-html-workbench symlink から plugin root を解決する。base repo の home/skills には同名skillを置かない。 + 採用方針は、専用repoを plugin repo の管理元にし、visual-html-renderer と reviewable-design-doc を同梱する形。Codex側は .agents/plugins/marketplace.json を持つ repo-local marketplace として登録し、source.path: "./" つまり marketplace root 自体を plugin root として解決する。base repo の home/skills には同名skillを置かない。

base repoとの衝突回避

base repoの bin/build-codex-skills.sh は home/skills から home/generated/codex-skills を生成する。visual-html-renderer と reviewable-design-doc はこのplugin repoで管理し、base repo側には同名skillを追加しないため、生成skill経路とは分離される。

diff --git a/docs/development-plan.html b/docs/development-plan.html index 2065236..7b1450a 100644 --- a/docs/development-plan.html +++ b/docs/development-plan.html @@ -480,7 +480,7 @@

統合時の確認項目

  • 既存skillからこのpluginを呼ぶ場合は、直接script呼び出しではなくplugin skill境界経由にする。
  • 採用する登録方式

    -

    このrepoは plugin root と repo-local marketplace root を兼ねる。.agents/plugins/marketplace.json は reviewable-html-workbench-local marketplace を定義し、plugin entry は source.path: "./plugins/reviewable-html-workbench" を参照する。plugins/reviewable-html-workbench はrepo rootへのsymlinkで、Codex marketplace loaderの既存パターンに合わせるために置く。導入は codex plugin marketplace add /path/to/reviewable-html-workbench で行う。

    +

    このrepoは plugin root と repo-local marketplace root を兼ねる。.agents/plugins/marketplace.json は reviewable-html-workbench-local marketplace を定義し、plugin entry は source.path: "./" を参照し、marketplace root 自体を plugin root として解決する。以前は repo root への symlink plugins/reviewable-html-workbench を経由していたが、Windows の git が既定で symlink を通常ファイルとして checkout し install が ENOTDIR で失敗するため廃止した。導入は codex plugin marketplace add /path/to/reviewable-html-workbench で行う。

    marketplace policy は INSTALLED_BY_DEFAULT とする。AVAILABLE のままだと marketplace 登録後もplugin cacheが作られず、skill discovery に載らない。

    repo path に空白が含まれる環境では、Codex CLIのsource判定がgit source側へ倒れる場合がある。その場合は空白を含まないsymlinkを作り、codex plugin marketplace add /private/tmp/reviewable-html-workbench のようにsymlink pathを渡す。

    base repoの install.sh には含めない。Codex全プロジェクトに強制配布すると同名skillの二重登録リスクが高いため、まずは個別導入とし、必要になった場合だけbase repo側へ薄いadapterを追加する。

    diff --git a/plugins/reviewable-html-workbench b/plugins/reviewable-html-workbench deleted file mode 120000 index a96aa0e..0000000 --- a/plugins/reviewable-html-workbench +++ /dev/null @@ -1 +0,0 @@ -.. \ No newline at end of file diff --git a/tests/test_project_layout.py b/tests/test_project_layout.py index 18a4de6..6199919 100644 --- a/tests/test_project_layout.py +++ b/tests/test_project_layout.py @@ -2,6 +2,7 @@ import json import os +import subprocess import unittest from pathlib import Path @@ -46,14 +47,31 @@ def test_codex_marketplace_entry_points_to_plugin_root(self) -> None: self.assertEqual(len(plugins), 1) plugin = plugins[0] self.assertEqual(plugin["name"], "reviewable-html-workbench") - self.assertEqual(plugin["source"], {"source": "local", "path": "./plugins/reviewable-html-workbench"}) + self.assertEqual(plugin["source"], {"source": "local", "path": "./"}) self.assertEqual(plugin["policy"]["installation"], "INSTALLED_BY_DEFAULT") self.assertEqual(plugin["policy"]["authentication"], "ON_INSTALL") self.assertEqual(plugin["category"], "Productivity") - plugin_link = ROOT / "plugins" / "reviewable-html-workbench" - self.assertTrue(plugin_link.exists()) - self.assertEqual(plugin_link.resolve(), ROOT) + def test_claude_marketplace_entry_points_to_plugin_root(self) -> None: + payload = json.loads((ROOT / ".claude-plugin" / "marketplace.json").read_text(encoding="utf-8")) + plugins = payload["plugins"] + self.assertEqual(len(plugins), 1) + self.assertEqual(plugins[0]["name"], "reviewable-html-workbench") + self.assertEqual(plugins[0]["source"], "./") + + def test_repo_contains_no_symlinks(self) -> None: + """Windows の git は既定 (core.symlinks=false) で symlink を通常ファイルとして + checkout する。marketplace source がそこを指すと install が ENOTDIR で失敗するため、 + tracked file に symlink を持ち込まない。""" + listing = subprocess.run( + ["git", "ls-files", "-s"], + cwd=ROOT, + capture_output=True, + text=True, + check=True, + ).stdout + symlinks = [line.split("\t", 1)[1] for line in listing.splitlines() if line.startswith("120000 ")] + self.assertEqual(symlinks, [], f"tracked symlinks break Windows checkouts: {symlinks}") def test_codex_manifest_documents_required_interface(self) -> None: payload = json.loads((ROOT / ".codex-plugin" / "plugin.json").read_text(encoding="utf-8"))