From b2b6ce480eadd881ac07614834003ea6556de2c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Feb 2026 20:28:29 +0000 Subject: [PATCH 1/3] Add deprecated install/sync CLI commands for plugin migration DeepWork is now distributed as a Claude Code plugin rather than a standalone CLI tool. These hidden back-compat commands auto-install the plugin config into .claude/settings.json and display uninstall instructions for brew/uv. Both commands are scheduled for removal after June 1st, 2026. https://claude.ai/code/session_01WGRUea9sYZkwvxi4cta5gu --- src/deepwork/cli/install.py | 95 +++++++++++++++++++++++++++++++++++++ src/deepwork/cli/main.py | 7 +++ 2 files changed, 102 insertions(+) create mode 100644 src/deepwork/cli/install.py diff --git a/src/deepwork/cli/install.py b/src/deepwork/cli/install.py new file mode 100644 index 00000000..bd7a3339 --- /dev/null +++ b/src/deepwork/cli/install.py @@ -0,0 +1,95 @@ +"""Install/sync deprecation commands for DeepWork CLI. + +# DEPRECATION NOTICE: Remove after June 1st, 2026. +# These commands exist only for backwards compatibility with users who +# have DeepWork installed globally via brew or uv. By June 1st, 2026, +# all users should have migrated to the Claude plugin distribution model +# and these commands can be safely deleted. +""" + +import json +from pathlib import Path + +import click + +_DEPRECATION_MESSAGE = """ +╔══════════════════════════════════════════════════════════════════════╗ +║ DEEPWORK DISTRIBUTION CHANGE ║ +╚══════════════════════════════════════════════════════════════════════╝ + +DeepWork is no longer installed as a standalone CLI tool — it is now +distributed as a Claude Code plugin. + +I have auto-installed the DeepWork plugin in this project for you, +but you must now uninstall the global CLI install you have previously done. + +To uninstall, try BOTH of the following to make sure it is fully removed: + + brew uninstall deepwork + uv tool uninstall deepwork + +(It's safe to run both — one will succeed and the other will simply +report that it's not installed.) + +Once uninstalled, DeepWork will continue to work automatically as a +Claude Code plugin whenever you use Claude in a project with DeepWork +configured. +""" + + +def _install_plugin_config() -> None: + """Write marketplace and plugin configuration to .claude/settings.json. + + Merges the DeepWork plugin marketplace and plugin entries into the + project's existing Claude settings without overwriting other config. + """ + settings_path = Path(".claude") / "settings.json" + settings_path.parent.mkdir(parents=True, exist_ok=True) + + # Load existing settings or start fresh + if settings_path.exists(): + try: + settings = json.loads(settings_path.read_text()) + except (json.JSONDecodeError, OSError): + settings = {} + else: + settings = {} + + # Merge extraKnownMarketplaces + marketplaces = settings.setdefault("extraKnownMarketplaces", {}) + marketplaces["deepwork-plugins"] = { + "source": { + "source": "github", + "repo": "Unsupervisedcom/deepwork", + } + } + + # Merge enabledPlugins + plugins = settings.setdefault("enabledPlugins", {}) + plugins["deepwork@deepwork-plugins"] = True + plugins["learning-agents@deepwork-plugins"] = True + + settings_path.write_text(json.dumps(settings, indent=2) + "\n") + + +def _run_install_deprecation() -> None: + """Shared implementation for both install and sync commands. + + # DEPRECATION NOTICE: Remove after June 1st, 2026. + """ + _install_plugin_config() + click.echo(_DEPRECATION_MESSAGE) + + +# DEPRECATION NOTICE: Remove after June 1st, 2026. +@click.command(hidden=True) +def install() -> None: + """(Deprecated) Install DeepWork — now distributed as a Claude plugin.""" + _run_install_deprecation() + + +# DEPRECATION NOTICE: Remove after June 1st, 2026. +@click.command(hidden=True) +def sync() -> None: + """(Deprecated) Sync DeepWork — now distributed as a Claude plugin.""" + _run_install_deprecation() diff --git a/src/deepwork/cli/main.py b/src/deepwork/cli/main.py index f0f23aec..f193b9ad 100644 --- a/src/deepwork/cli/main.py +++ b/src/deepwork/cli/main.py @@ -12,11 +12,18 @@ def cli() -> None: # Import commands from deepwork.cli.hook import hook # noqa: E402 +from deepwork.cli.install import install, sync # noqa: E402 from deepwork.cli.serve import serve # noqa: E402 cli.add_command(hook) cli.add_command(serve) +# DEPRECATION NOTICE: Remove after June 1st, 2026. +# install and sync are hidden back-compat commands that tell users +# to migrate to the Claude plugin distribution model. +cli.add_command(install) +cli.add_command(sync) + if __name__ == "__main__": cli() From 5bb48b4dcad15ab1249931b959fb241fd0325caf Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Feb 2026 20:35:18 +0000 Subject: [PATCH 2/3] Add requirements and tests for deprecated install/sync commands Adds REQ-005.4 to the CLI commands spec documenting the deprecated install and sync commands, with a scheduled removal note for June 1st 2026. Includes 13 unit tests covering deprecation message output, hidden-from-help behavior, plugin config creation, settings merge, and invalid JSON handling. https://claude.ai/code/session_01WGRUea9sYZkwvxi4cta5gu --- specs/deepwork/REQ-005-cli-commands.md | 21 ++- tests/unit/test_install_cli.py | 220 +++++++++++++++++++++++++ 2 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_install_cli.py diff --git a/specs/deepwork/REQ-005-cli-commands.md b/specs/deepwork/REQ-005-cli-commands.md index bcd1df42..d4dbe4ba 100644 --- a/specs/deepwork/REQ-005-cli-commands.md +++ b/specs/deepwork/REQ-005-cli-commands.md @@ -2,7 +2,7 @@ ## Overview -The DeepWork CLI provides two commands: `serve` (starts the MCP server) and `hook` (runs hook scripts). The CLI is built with Click and serves as the entry point for `deepwork` executable. The `serve` command is the primary runtime entry point, while `hook` provides a generic mechanism for running Python hook modules. +The DeepWork CLI provides two primary commands: `serve` (starts the MCP server) and `hook` (runs hook scripts), plus two deprecated back-compat commands: `install` and `sync`. The CLI is built with Click and serves as the entry point for `deepwork` executable. The `serve` command is the primary runtime entry point, while `hook` provides a generic mechanism for running Python hook modules. ## Requirements @@ -10,7 +10,7 @@ The DeepWork CLI provides two commands: `serve` (starts the MCP server) and `hoo 1. The CLI MUST be a Click group command named `cli`. 2. The CLI MUST provide a `--version` option sourced from the `deepwork` package version. -3. The CLI MUST register both `serve` and `hook` as subcommands. +3. The CLI MUST register `serve`, `hook`, `install`, and `sync` as subcommands. 4. The CLI MUST be callable as `deepwork` from the command line (via package entry point). ### REQ-005.2: serve Command @@ -41,3 +41,20 @@ The DeepWork CLI provides two commands: `serve` (starts the MCP server) and `hoo 9. If the module does not have a `main()` function, the system MUST raise `HookError`. 10. `HookError` exceptions MUST be caught and printed to stderr with exit code 1. 11. Other unexpected exceptions MUST be caught and printed to stderr with exit code 1. + +### REQ-005.4: Deprecated install and sync Commands + +> **SCHEDULED REMOVAL: June 1st, 2026.** These commands exist only for +> backwards compatibility with users who installed DeepWork globally via +> `brew` or `uv`. Once all users have migrated to the Claude Code plugin +> distribution model, this entire section and all associated code and tests +> SHOULD be deleted. + +1. Both `install` and `sync` MUST be registered as Click commands with `hidden=True` so they do not appear in `--help` output. +2. Both commands MUST execute the same shared implementation (`_run_install_deprecation`). +3. The shared implementation MUST write plugin configuration to `.claude/settings.json`, creating the `.claude/` directory if it does not exist. +4. The configuration MUST merge into existing settings without overwriting unrelated keys (e.g., `permissions`). +5. The written `extraKnownMarketplaces` MUST contain a `deepwork-plugins` entry with source `{"source": "github", "repo": "Unsupervisedcom/deepwork"}`. +6. The written `enabledPlugins` MUST set `deepwork@deepwork-plugins` and `learning-agents@deepwork-plugins` to `true`. +7. If `.claude/settings.json` exists but contains invalid JSON, the command MUST treat it as an empty settings object (not crash). +8. Both commands MUST print a deprecation message that includes `brew uninstall deepwork` and `uv tool uninstall deepwork` instructions. diff --git a/tests/unit/test_install_cli.py b/tests/unit/test_install_cli.py new file mode 100644 index 00000000..5201cb0d --- /dev/null +++ b/tests/unit/test_install_cli.py @@ -0,0 +1,220 @@ +"""Tests for deprecated install/sync CLI commands -- validates REQ-005.4. + +SCHEDULED REMOVAL: June 1st, 2026. +Delete this entire file when REQ-005.4 and the install/sync commands are removed. +""" + +import json +from pathlib import Path + +from click.testing import CliRunner + +from deepwork.cli.install import install, sync +from deepwork.cli.main import cli + + +class TestInstallCommandOutput: + """Tests for the install command's deprecation message.""" + + # THIS TEST VALIDATES A HARD REQUIREMENT (REQ-005.4.8). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + def test_install_prints_deprecation_message(self, tmp_path: str) -> None: + """install must print a deprecation message with uninstall instructions.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + result = runner.invoke(install) + + assert result.exit_code == 0 + assert "no longer installed" in result.output.lower() + assert "brew uninstall deepwork" in result.output + assert "uv tool uninstall deepwork" in result.output + + # THIS TEST VALIDATES A HARD REQUIREMENT (REQ-005.4.8). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + def test_sync_prints_deprecation_message(self, tmp_path: str) -> None: + """sync must print the same deprecation message as install.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + result = runner.invoke(sync) + + assert result.exit_code == 0 + assert "brew uninstall deepwork" in result.output + assert "uv tool uninstall deepwork" in result.output + + # THIS TEST VALIDATES A HARD REQUIREMENT (REQ-005.4.2). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + def test_install_and_sync_produce_identical_output(self, tmp_path: str) -> None: + """Both commands must execute the same shared implementation.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + install_result = runner.invoke(install) + with runner.isolated_filesystem(temp_dir=tmp_path): + sync_result = runner.invoke(sync) + + assert install_result.output == sync_result.output + + +class TestInstallHiddenFromHelp: + """Tests that install and sync are hidden from CLI help.""" + + # THIS TEST VALIDATES A HARD REQUIREMENT (REQ-005.4.1). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + def test_install_not_in_help(self) -> None: + """install must not appear in --help output.""" + runner = CliRunner() + result = runner.invoke(cli, ["--help"]) + + assert result.exit_code == 0 + lines = result.output.splitlines() + command_lines = [] + in_commands = False + for line in lines: + if line.strip().lower().startswith("commands:"): + in_commands = True + continue + if in_commands and line.strip(): + command_lines.append(line) + + command_names = [line.split()[0] for line in command_lines if line.strip()] + assert "install" not in command_names + + # THIS TEST VALIDATES A HARD REQUIREMENT (REQ-005.4.1). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + def test_sync_not_in_help(self) -> None: + """sync must not appear in --help output.""" + runner = CliRunner() + result = runner.invoke(cli, ["--help"]) + + assert result.exit_code == 0 + lines = result.output.splitlines() + command_lines = [] + in_commands = False + for line in lines: + if line.strip().lower().startswith("commands:"): + in_commands = True + continue + if in_commands and line.strip(): + command_lines.append(line) + + command_names = [line.split()[0] for line in command_lines if line.strip()] + assert "sync" not in command_names + + # THIS TEST VALIDATES A HARD REQUIREMENT (REQ-005.4.1). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + def test_install_still_invocable(self, tmp_path: str) -> None: + """Hidden commands must still be invocable directly.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + result = runner.invoke(cli, ["install"]) + + assert result.exit_code == 0 + + # THIS TEST VALIDATES A HARD REQUIREMENT (REQ-005.4.1). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + def test_sync_still_invocable(self, tmp_path: str) -> None: + """Hidden commands must still be invocable directly.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + result = runner.invoke(cli, ["sync"]) + + assert result.exit_code == 0 + + +class TestPluginConfigCreation: + """Tests for auto-installing plugin config into .claude/settings.json.""" + + @staticmethod + def _read_settings() -> dict: + """Read .claude/settings.json relative to CWD.""" + return json.loads(Path(".claude/settings.json").read_text()) + + # THIS TEST VALIDATES A HARD REQUIREMENT (REQ-005.4.3). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + def test_creates_settings_file_from_scratch(self, tmp_path: str) -> None: + """install must create .claude/settings.json when it does not exist.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + result = runner.invoke(install) + assert result.exit_code == 0 + settings = self._read_settings() + + assert "extraKnownMarketplaces" in settings + assert "enabledPlugins" in settings + + # THIS TEST VALIDATES A HARD REQUIREMENT (REQ-005.4.5). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + def test_marketplace_entry_is_correct(self, tmp_path: str) -> None: + """extraKnownMarketplaces must contain deepwork-plugins with correct source.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + runner.invoke(install) + settings = self._read_settings() + + mp = settings["extraKnownMarketplaces"]["deepwork-plugins"] + assert mp == { + "source": { + "source": "github", + "repo": "Unsupervisedcom/deepwork", + } + } + + # THIS TEST VALIDATES A HARD REQUIREMENT (REQ-005.4.6). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + def test_enabled_plugins_are_correct(self, tmp_path: str) -> None: + """enabledPlugins must include both deepwork and learning-agents plugins.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + runner.invoke(install) + settings = self._read_settings() + + assert settings["enabledPlugins"]["deepwork@deepwork-plugins"] is True + assert settings["enabledPlugins"]["learning-agents@deepwork-plugins"] is True + + # THIS TEST VALIDATES A HARD REQUIREMENT (REQ-005.4.4). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + def test_merges_with_existing_settings(self, tmp_path: str) -> None: + """install must preserve existing keys in settings.json.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + Path(".claude").mkdir() + existing = { + "permissions": {"allow": ["Bash(git:*)"]}, + "enabledPlugins": {}, + } + Path(".claude/settings.json").write_text(json.dumps(existing)) + + runner.invoke(install) + settings = self._read_settings() + + # Original permissions must still be present + assert settings["permissions"] == {"allow": ["Bash(git:*)"]} + # New plugin config must be added + assert "deepwork-plugins" in settings["extraKnownMarketplaces"] + assert settings["enabledPlugins"]["deepwork@deepwork-plugins"] is True + + # THIS TEST VALIDATES A HARD REQUIREMENT (REQ-005.4.7). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + def test_handles_invalid_json_gracefully(self, tmp_path: str) -> None: + """install must not crash if settings.json contains invalid JSON.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + Path(".claude").mkdir() + Path(".claude/settings.json").write_text("{invalid json!!") + + result = runner.invoke(install) + assert result.exit_code == 0 + settings = self._read_settings() + + assert "extraKnownMarketplaces" in settings + assert "enabledPlugins" in settings + + # THIS TEST VALIDATES A HARD REQUIREMENT (REQ-005.4.3). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + def test_creates_claude_directory_if_missing(self, tmp_path: str) -> None: + """install must create the .claude/ directory if it does not exist.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + assert not Path(".claude").exists() + result = runner.invoke(install) + assert result.exit_code == 0 + assert Path(".claude/settings.json").exists() From 473f62c75e419abb3a141dba5be2657f37932dbe Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Feb 2026 20:46:18 +0000 Subject: [PATCH 3/3] Add PR #227 reference to all deprecation comments for bulk searchability https://claude.ai/code/session_01WGRUea9sYZkwvxi4cta5gu --- specs/deepwork/REQ-005-cli-commands.md | 2 +- src/deepwork/cli/install.py | 8 ++++---- src/deepwork/cli/main.py | 2 +- tests/unit/test_install_cli.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/specs/deepwork/REQ-005-cli-commands.md b/specs/deepwork/REQ-005-cli-commands.md index d4dbe4ba..d4f32c92 100644 --- a/specs/deepwork/REQ-005-cli-commands.md +++ b/specs/deepwork/REQ-005-cli-commands.md @@ -44,7 +44,7 @@ The DeepWork CLI provides two primary commands: `serve` (starts the MCP server) ### REQ-005.4: Deprecated install and sync Commands -> **SCHEDULED REMOVAL: June 1st, 2026.** These commands exist only for +> **SCHEDULED REMOVAL: June 1st, 2026; details in PR https://github.com/Unsupervisedcom/deepwork/pull/227.** These commands exist only for > backwards compatibility with users who installed DeepWork globally via > `brew` or `uv`. Once all users have migrated to the Claude Code plugin > distribution model, this entire section and all associated code and tests diff --git a/src/deepwork/cli/install.py b/src/deepwork/cli/install.py index bd7a3339..e0c64d37 100644 --- a/src/deepwork/cli/install.py +++ b/src/deepwork/cli/install.py @@ -1,6 +1,6 @@ """Install/sync deprecation commands for DeepWork CLI. -# DEPRECATION NOTICE: Remove after June 1st, 2026. +# DEPRECATION NOTICE: Remove after June 1st, 2026; details in PR https://github.com/Unsupervisedcom/deepwork/pull/227 # These commands exist only for backwards compatibility with users who # have DeepWork installed globally via brew or uv. By June 1st, 2026, # all users should have migrated to the Claude plugin distribution model @@ -75,20 +75,20 @@ def _install_plugin_config() -> None: def _run_install_deprecation() -> None: """Shared implementation for both install and sync commands. - # DEPRECATION NOTICE: Remove after June 1st, 2026. + # DEPRECATION NOTICE: Remove after June 1st, 2026; details in PR https://github.com/Unsupervisedcom/deepwork/pull/227 """ _install_plugin_config() click.echo(_DEPRECATION_MESSAGE) -# DEPRECATION NOTICE: Remove after June 1st, 2026. +# DEPRECATION NOTICE: Remove after June 1st, 2026; details in PR https://github.com/Unsupervisedcom/deepwork/pull/227 @click.command(hidden=True) def install() -> None: """(Deprecated) Install DeepWork — now distributed as a Claude plugin.""" _run_install_deprecation() -# DEPRECATION NOTICE: Remove after June 1st, 2026. +# DEPRECATION NOTICE: Remove after June 1st, 2026; details in PR https://github.com/Unsupervisedcom/deepwork/pull/227 @click.command(hidden=True) def sync() -> None: """(Deprecated) Sync DeepWork — now distributed as a Claude plugin.""" diff --git a/src/deepwork/cli/main.py b/src/deepwork/cli/main.py index f193b9ad..e031381f 100644 --- a/src/deepwork/cli/main.py +++ b/src/deepwork/cli/main.py @@ -18,7 +18,7 @@ def cli() -> None: cli.add_command(hook) cli.add_command(serve) -# DEPRECATION NOTICE: Remove after June 1st, 2026. +# DEPRECATION NOTICE: Remove after June 1st, 2026; details in PR https://github.com/Unsupervisedcom/deepwork/pull/227 # install and sync are hidden back-compat commands that tell users # to migrate to the Claude plugin distribution model. cli.add_command(install) diff --git a/tests/unit/test_install_cli.py b/tests/unit/test_install_cli.py index 5201cb0d..c3235f3b 100644 --- a/tests/unit/test_install_cli.py +++ b/tests/unit/test_install_cli.py @@ -1,6 +1,6 @@ """Tests for deprecated install/sync CLI commands -- validates REQ-005.4. -SCHEDULED REMOVAL: June 1st, 2026. +SCHEDULED REMOVAL: June 1st, 2026; details in PR https://github.com/Unsupervisedcom/deepwork/pull/227 Delete this entire file when REQ-005.4 and the install/sync commands are removed. """