diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index bb792ee..6fad2b8 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,7 +12,7 @@ "name": "token-saver", "source": "./", "description": "Automatically compresses verbose CLI output to save tokens. Supports git, docker, npm, terraform, kubectl, and 13+ other command families.", - "version": "2.0.0", + "version": "2.0.1", "author": { "name": "ppgranger" }, diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 01a0467..999c512 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "token-saver", "description": "Automatically compresses verbose CLI output (git, docker, npm, terraform, kubectl, etc.) to save tokens in Claude Code sessions. Supports 18+ command families with smart compression.", - "version": "2.0.0", + "version": "2.0.1", "author": { "name": "ppgranger", "url": "https://github.com/ppgranger" diff --git a/installers/claude.py b/installers/claude.py index 31488cd..9e7865c 100644 --- a/installers/claude.py +++ b/installers/claude.py @@ -72,6 +72,16 @@ def _plugin_cache_dir(version): ) +def _marketplace_dir(): + """Return the marketplace directory for token-saver. + + Claude Code reads .claude-plugin/marketplace.json from this path to + discover available plugins. This mirrors the layout produced by + ``/plugin marketplace add``. + """ + return os.path.join(_settings_dir(), "plugins", "marketplaces", _MARKETPLACE_NAME) + + def _settings_path(): """Return path to Claude Code settings.json.""" return os.path.join(_settings_dir(), "settings.json") @@ -110,12 +120,14 @@ def _iso_now(): return datetime.now(timezone.utc).isoformat(timespec="milliseconds") -def _register_plugin(target_dir, version): +def _register_plugin(marketplace_dir, cache_dir, version): """Register token-saver as a native Claude Code plugin. - Registers the GitHub repo as a known marketplace, writes the plugin - entry in installed_plugins.json (v2 format), and enables it in - settings.json. + Registers the GitHub repo as a known marketplace (pointing + ``installLocation`` at *marketplace_dir* so Claude Code can find + ``.claude-plugin/marketplace.json``), writes the plugin entry in + installed_plugins.json (v2 format, ``installPath`` → *cache_dir*), + and enables it in settings.json. """ plugins_dir = os.path.join(_settings_dir(), "plugins") os.makedirs(plugins_dir, exist_ok=True) @@ -135,8 +147,9 @@ def _register_plugin(target_dir, version): "source": { "source": "github", "repo": _GITHUB_REPO, + "ref": "production", }, - "installLocation": target_dir, + "installLocation": marketplace_dir, "lastUpdated": now, } @@ -161,7 +174,7 @@ def _register_plugin(target_dir, version): registry["plugins"][_PLUGIN_KEY] = [ { "scope": "user", - "installPath": target_dir, + "installPath": cache_dir, "version": version, "installedAt": now, "lastUpdated": now, @@ -371,21 +384,25 @@ def install(use_symlink=False): # 2. Install files to the versioned plugin cache directory version = _read_version() - target_dir = _plugin_cache_dir(version) - print(f"\n--- Claude Code ({target_dir}) ---") - install_files(target_dir, CLAUDE_FILES, use_symlink) - - # 3. Stamp version in BOTH plugin.json and marketplace.json - stamp_version( - target_dir, - [ - ".claude-plugin/plugin.json", - ".claude-plugin/marketplace.json", - ], - ) + cache_dir = _plugin_cache_dir(version) + print(f"\n--- Claude Code (cache: {cache_dir}) ---") + install_files(cache_dir, CLAUDE_FILES, use_symlink) + + # 3. Install files to the marketplace directory (for plugin discovery) + mkt_dir = _marketplace_dir() + print(f"--- Claude Code (marketplace: {mkt_dir}) ---") + install_files(mkt_dir, CLAUDE_FILES, use_symlink) + + # 4. Stamp version in BOTH plugin.json and marketplace.json (in both dirs) + version_files = [ + ".claude-plugin/plugin.json", + ".claude-plugin/marketplace.json", + ] + stamp_version(cache_dir, version_files) + stamp_version(mkt_dir, version_files) - # 4. Register marketplace + plugin - _register_plugin(target_dir, version) + # 5. Register marketplace + plugin (marketplace dir for discovery, cache for runtime) + _register_plugin(mkt_dir, cache_dir, version) print(" Plugin registered. Restart Claude Code, then /plugin to manage.") @@ -405,6 +422,11 @@ def uninstall(): if os.path.isdir(cache_root): uninstall_dir(cache_root) + # Remove marketplace discovery directory + mkt_dir = _marketplace_dir() + if os.path.isdir(mkt_dir): + uninstall_dir(mkt_dir) + # Also remove old v1 location if it still exists old_dir = _plugin_dir() if os.path.isdir(old_dir): diff --git a/src/__init__.py b/src/__init__.py index 5fdec9b..941397a 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,6 +1,6 @@ import os -__version__ = "2.0.0" +__version__ = "2.0.1" def data_dir() -> str: diff --git a/tests/test_installers.py b/tests/test_installers.py index 28d6eb4..72e3f9f 100644 --- a/tests/test_installers.py +++ b/tests/test_installers.py @@ -504,11 +504,13 @@ class TestRegisterPlugin: def setup_method(self): self.tmp_home = tempfile.mkdtemp() - self.tmp_target = tempfile.mkdtemp() + self.tmp_target = tempfile.mkdtemp() # cache dir (plugin runtime) + self.tmp_marketplace = tempfile.mkdtemp() # marketplace dir (discovery) def teardown_method(self): shutil.rmtree(self.tmp_home, ignore_errors=True) shutil.rmtree(self.tmp_target, ignore_errors=True) + shutil.rmtree(self.tmp_marketplace, ignore_errors=True) def _settings_dir(self): return os.path.join(self.tmp_home, ".claude") @@ -534,7 +536,7 @@ def test_registers_marketplace(self): from installers.claude import _register_plugin with mock.patch("installers.claude.home", return_value=self.tmp_home): - _register_plugin(self.tmp_target, "2.0.0") + _register_plugin(self.tmp_marketplace, self.tmp_target, "2.0.0") with open(self._known_marketplaces_path()) as f: known = json.load(f) @@ -542,13 +544,14 @@ def test_registers_marketplace(self): entry = known["token-saver-marketplace"] assert entry["source"]["source"] == "github" assert entry["source"]["repo"] == "ppgranger/token-saver" - assert entry["installLocation"] == self.tmp_target + assert entry["source"]["ref"] == "production" + assert entry["installLocation"] == self.tmp_marketplace def test_registers_in_installed_plugins_v2_format(self): from installers.claude import _register_plugin with mock.patch("installers.claude.home", return_value=self.tmp_home): - _register_plugin(self.tmp_target, "2.0.0") + _register_plugin(self.tmp_marketplace, self.tmp_target, "2.0.0") with open(self._installed_plugins_path()) as f: data = json.load(f) @@ -565,7 +568,7 @@ def test_enables_in_settings(self): from installers.claude import _register_plugin with mock.patch("installers.claude.home", return_value=self.tmp_home): - _register_plugin(self.tmp_target, "2.0.0") + _register_plugin(self.tmp_marketplace, self.tmp_target, "2.0.0") with open(self._settings_path()) as f: settings = json.load(f) @@ -576,8 +579,8 @@ def test_no_duplicates_on_reregistration(self): from installers.claude import _register_plugin with mock.patch("installers.claude.home", return_value=self.tmp_home): - _register_plugin(self.tmp_target, "2.0.0") - _register_plugin(self.tmp_target, "2.0.0") + _register_plugin(self.tmp_marketplace, self.tmp_target, "2.0.0") + _register_plugin(self.tmp_marketplace, self.tmp_target, "2.0.0") with open(self._installed_plugins_path()) as f: data = json.load(f) @@ -602,7 +605,7 @@ def test_preserves_existing_marketplaces(self): ) with mock.patch("installers.claude.home", return_value=self.tmp_home): - _register_plugin(self.tmp_target, "2.0.0") + _register_plugin(self.tmp_marketplace, self.tmp_target, "2.0.0") with open(km_path) as f: known = json.load(f) @@ -627,7 +630,7 @@ def test_preserves_existing_v2_plugins(self): ) with mock.patch("installers.claude.home", return_value=self.tmp_home): - _register_plugin(self.tmp_target, "2.0.0") + _register_plugin(self.tmp_marketplace, self.tmp_target, "2.0.0") with open(plugins_path) as f: data = json.load(f)