missioncache-install 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- missioncache_install-1.0.0/.gitignore +18 -0
- missioncache_install-1.0.0/PKG-INFO +87 -0
- missioncache_install-1.0.0/README.md +59 -0
- missioncache_install-1.0.0/missioncache_install/__init__.py +3 -0
- missioncache_install-1.0.0/missioncache_install/__main__.py +424 -0
- missioncache_install-1.0.0/missioncache_install/command_clients.py +752 -0
- missioncache_install-1.0.0/missioncache_install/installers.py +681 -0
- missioncache_install-1.0.0/missioncache_install/mcp_clients.py +426 -0
- missioncache_install-1.0.0/missioncache_install/prereqs.py +147 -0
- missioncache_install-1.0.0/missioncache_install/settings.py +129 -0
- missioncache_install-1.0.0/missioncache_install/state.py +105 -0
- missioncache_install-1.0.0/missioncache_install/subprocess_utils.py +83 -0
- missioncache_install-1.0.0/missioncache_install/ui.py +192 -0
- missioncache_install-1.0.0/missioncache_install/wizard.py +271 -0
- missioncache_install-1.0.0/pyproject.toml +82 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: missioncache-install
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Bootstrap installer for MissionCache - the project manager for Claude Code
|
|
5
|
+
Project-URL: Homepage, https://github.com/missioncache/missioncache
|
|
6
|
+
Project-URL: Repository, https://github.com/missioncache/missioncache
|
|
7
|
+
Project-URL: Issues, https://github.com/missioncache/missioncache/issues
|
|
8
|
+
Author-email: Tomer Brami <tomerbrami@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: bootstrap,claude,installer,missioncache,plugin
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: System :: Installation/Setup
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: json5>=0.9.0
|
|
22
|
+
Requires-Dist: pyfiglet>=1.0.0
|
|
23
|
+
Requires-Dist: rich>=13.7.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# missioncache-install
|
|
30
|
+
|
|
31
|
+
Bootstrap installer for [MissionCache](https://github.com/missioncache/missioncache), the project manager for Claude Code.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uvx missioncache-install
|
|
37
|
+
# or
|
|
38
|
+
pipx run missioncache-install
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The interactive wizard asks which components to install. Default is all:
|
|
42
|
+
|
|
43
|
+
| Component | What it does |
|
|
44
|
+
|----------------|------------------------------------------------------------------------|
|
|
45
|
+
| Plugin | Registers the MissionCache plugin with Claude Code (slash commands, MCP, hooks) |
|
|
46
|
+
| Dashboard | Installs `missioncache-dashboard` pip package + launchd/systemd service on port 8787 |
|
|
47
|
+
| missioncache-auto CLI | Installs `missioncache-auto` for autonomous task execution |
|
|
48
|
+
| Statusline | Wires `~/.claude/settings.json` to run `missioncache-statusline` on every prompt |
|
|
49
|
+
| Rules | Copies rule files into `~/.claude/rules/` |
|
|
50
|
+
| User commands | Copies `/whats-new` and `/optimize-prompt` into `~/.claude/commands/` |
|
|
51
|
+
|
|
52
|
+
## Non-interactive
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uvx missioncache-install --all # install everything
|
|
56
|
+
uvx missioncache-install --dashboard --statusline # install a subset
|
|
57
|
+
uvx missioncache-install --update # refresh everything
|
|
58
|
+
uvx missioncache-install --uninstall # remove everything (preserves user data)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Maintainer mode
|
|
62
|
+
|
|
63
|
+
From a clone of `missioncache`:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
git clone https://github.com/missioncache/missioncache.git
|
|
67
|
+
cd missioncache
|
|
68
|
+
uvx missioncache-install --local
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`--local` swaps PyPI installs for editable ones and registers the plugin via a local marketplace. Edit files in the clone and see changes live.
|
|
72
|
+
|
|
73
|
+
## Windows
|
|
74
|
+
|
|
75
|
+
Windows service registration is not yet supported. The installer will register the plugin, pip-install missioncache-auto, and print manual instructions for running the dashboard.
|
|
76
|
+
|
|
77
|
+
## Uninstall
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
uvx missioncache-install --uninstall
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Removes: plugin registration, pip packages, service units, settings.json entries. Preserves: `~/.missioncache/` (projects and task history).
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# missioncache-install
|
|
2
|
+
|
|
3
|
+
Bootstrap installer for [MissionCache](https://github.com/missioncache/missioncache), the project manager for Claude Code.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uvx missioncache-install
|
|
9
|
+
# or
|
|
10
|
+
pipx run missioncache-install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The interactive wizard asks which components to install. Default is all:
|
|
14
|
+
|
|
15
|
+
| Component | What it does |
|
|
16
|
+
|----------------|------------------------------------------------------------------------|
|
|
17
|
+
| Plugin | Registers the MissionCache plugin with Claude Code (slash commands, MCP, hooks) |
|
|
18
|
+
| Dashboard | Installs `missioncache-dashboard` pip package + launchd/systemd service on port 8787 |
|
|
19
|
+
| missioncache-auto CLI | Installs `missioncache-auto` for autonomous task execution |
|
|
20
|
+
| Statusline | Wires `~/.claude/settings.json` to run `missioncache-statusline` on every prompt |
|
|
21
|
+
| Rules | Copies rule files into `~/.claude/rules/` |
|
|
22
|
+
| User commands | Copies `/whats-new` and `/optimize-prompt` into `~/.claude/commands/` |
|
|
23
|
+
|
|
24
|
+
## Non-interactive
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uvx missioncache-install --all # install everything
|
|
28
|
+
uvx missioncache-install --dashboard --statusline # install a subset
|
|
29
|
+
uvx missioncache-install --update # refresh everything
|
|
30
|
+
uvx missioncache-install --uninstall # remove everything (preserves user data)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Maintainer mode
|
|
34
|
+
|
|
35
|
+
From a clone of `missioncache`:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
git clone https://github.com/missioncache/missioncache.git
|
|
39
|
+
cd missioncache
|
|
40
|
+
uvx missioncache-install --local
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`--local` swaps PyPI installs for editable ones and registers the plugin via a local marketplace. Edit files in the clone and see changes live.
|
|
44
|
+
|
|
45
|
+
## Windows
|
|
46
|
+
|
|
47
|
+
Windows service registration is not yet supported. The installer will register the plugin, pip-install missioncache-auto, and print manual instructions for running the dashboard.
|
|
48
|
+
|
|
49
|
+
## Uninstall
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uvx missioncache-install --uninstall
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Removes: plugin registration, pip packages, service units, settings.json entries. Preserves: `~/.missioncache/` (projects and task history).
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
MIT
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""Entry point for `uvx missioncache-install` / `pipx run missioncache-install`.
|
|
2
|
+
|
|
3
|
+
Invocation patterns:
|
|
4
|
+
uvx missioncache-install # interactive install wizard
|
|
5
|
+
uvx missioncache-install --all # install all components non-interactively
|
|
6
|
+
uvx missioncache-install --dashboard # install only the dashboard
|
|
7
|
+
uvx missioncache-install --all --no-statusline # install everything except the statusline
|
|
8
|
+
uvx missioncache-install --update # refresh whatever is in state.json
|
|
9
|
+
uvx missioncache-install --uninstall # interactive uninstall wizard (TTY only)
|
|
10
|
+
uvx missioncache-install --uninstall --all # uninstall every tracked component
|
|
11
|
+
uvx missioncache-install --uninstall codex,vscode # uninstall a specific list
|
|
12
|
+
uvx missioncache-install --local # maintainer mode: editable installs from clone
|
|
13
|
+
|
|
14
|
+
Project data and DBs at `~/.missioncache/` are never touched by any uninstall flow.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from . import __version__, installers, state, ui, wizard
|
|
24
|
+
from .wizard import COMMAND_IMPLIES
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
DEFAULT_PORT = 8787
|
|
28
|
+
|
|
29
|
+
# Sentinel for bare `--uninstall` (no value supplied). A unique object so
|
|
30
|
+
# `is` comparison distinguishes it from `--uninstall ""` (empty list, e.g.
|
|
31
|
+
# unset shell var) and from `--uninstall foo,bar` (positive list). Cannot
|
|
32
|
+
# collide with any string a user could pass on the CLI.
|
|
33
|
+
INTERACTIVE_WIZARD = object()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
37
|
+
p = argparse.ArgumentParser(
|
|
38
|
+
prog="missioncache-install",
|
|
39
|
+
description="Bootstrap installer for MissionCache (project manager for Claude Code).",
|
|
40
|
+
)
|
|
41
|
+
p.add_argument(
|
|
42
|
+
"--version", action="version", version=f"%(prog)s {__version__}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# `--all` and `--update` are install-time verbs and remain mutually
|
|
46
|
+
# exclusive. `--uninstall` is a different verb and lives outside the
|
|
47
|
+
# group so it can compose with `--all` (uninstall everything tracked,
|
|
48
|
+
# bypass wizard) and accept an optional positive list.
|
|
49
|
+
action = p.add_mutually_exclusive_group()
|
|
50
|
+
action.add_argument(
|
|
51
|
+
"--all", action="store_true",
|
|
52
|
+
help="With no other verb: install all components non-interactively. "
|
|
53
|
+
"With --uninstall: remove every tracked component, bypassing the "
|
|
54
|
+
"interactive wizard.",
|
|
55
|
+
)
|
|
56
|
+
action.add_argument(
|
|
57
|
+
"--update", action="store_true",
|
|
58
|
+
help="Update installed components in place (reads state).",
|
|
59
|
+
)
|
|
60
|
+
p.add_argument(
|
|
61
|
+
"--uninstall",
|
|
62
|
+
nargs="?",
|
|
63
|
+
const=INTERACTIVE_WIZARD,
|
|
64
|
+
default=None,
|
|
65
|
+
metavar="COMP1,COMP2",
|
|
66
|
+
help="Uninstall components. Bare flag opens the interactive wizard "
|
|
67
|
+
"(requires TTY). Pass a comma-separated component list (e.g. "
|
|
68
|
+
"`--uninstall codex,vscode`) for non-interactive removal of "
|
|
69
|
+
"specific components. Combine with `--all` to remove everything "
|
|
70
|
+
"tracked. Project data at ~/.missioncache/ is never touched.",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Per-component opt-in flags. Any of these triggers non-interactive mode
|
|
74
|
+
# for exactly the components listed (in combination with --no-* opt-outs).
|
|
75
|
+
opt_in = p.add_argument_group(
|
|
76
|
+
"component opt-in (non-interactive)",
|
|
77
|
+
"Install only the components listed. Can be combined with --no-* to "
|
|
78
|
+
"exclude specific ones from --all.",
|
|
79
|
+
)
|
|
80
|
+
for flag, dest in (
|
|
81
|
+
("--plugin", "plugin"),
|
|
82
|
+
("--dashboard", "dashboard"),
|
|
83
|
+
("--missioncache-auto", "missioncache_auto"),
|
|
84
|
+
("--statusline", "statusline"),
|
|
85
|
+
("--rules", "rules"),
|
|
86
|
+
("--user-commands", "user_commands"),
|
|
87
|
+
("--missioncache-db", "missioncache_db"),
|
|
88
|
+
("--codex", "codex"),
|
|
89
|
+
("--codex-commands", "codex_commands"),
|
|
90
|
+
("--opencode", "opencode"),
|
|
91
|
+
("--opencode-commands", "opencode_commands"),
|
|
92
|
+
("--vscode", "vscode"),
|
|
93
|
+
("--vscode-commands", "vscode_commands"),
|
|
94
|
+
):
|
|
95
|
+
opt_in.add_argument(flag, dest=dest, action="store_true")
|
|
96
|
+
|
|
97
|
+
opt_out = p.add_argument_group(
|
|
98
|
+
"component opt-out",
|
|
99
|
+
"Exclude specific components from --all (e.g. `--all --no-statusline`). "
|
|
100
|
+
"`--no-codex-commands` keeps the Codex MCP server but skips the slash "
|
|
101
|
+
"command plugin (same for opencode / vscode).",
|
|
102
|
+
)
|
|
103
|
+
for flag, dest in (
|
|
104
|
+
("--no-plugin", "no_plugin"),
|
|
105
|
+
("--no-dashboard", "no_dashboard"),
|
|
106
|
+
("--no-missioncache-auto", "no_missioncache_auto"),
|
|
107
|
+
("--no-statusline", "no_statusline"),
|
|
108
|
+
("--no-rules", "no_rules"),
|
|
109
|
+
("--no-user-commands", "no_user_commands"),
|
|
110
|
+
("--no-missioncache-db", "no_missioncache_db"),
|
|
111
|
+
("--no-codex", "no_codex"),
|
|
112
|
+
("--no-codex-commands", "no_codex_commands"),
|
|
113
|
+
("--no-opencode", "no_opencode"),
|
|
114
|
+
("--no-opencode-commands", "no_opencode_commands"),
|
|
115
|
+
("--no-vscode", "no_vscode"),
|
|
116
|
+
("--no-vscode-commands", "no_vscode_commands"),
|
|
117
|
+
):
|
|
118
|
+
opt_out.add_argument(flag, dest=dest, action="store_true")
|
|
119
|
+
|
|
120
|
+
p.add_argument(
|
|
121
|
+
"--local", action="store_true",
|
|
122
|
+
help="Maintainer mode: editable installs + local marketplace from the "
|
|
123
|
+
"current clone. Auto-detected when run from a repo root.",
|
|
124
|
+
)
|
|
125
|
+
p.add_argument(
|
|
126
|
+
"--no-service", action="store_true",
|
|
127
|
+
help="Skip launchd/systemd service registration (dashboard will not auto-start).",
|
|
128
|
+
)
|
|
129
|
+
p.add_argument(
|
|
130
|
+
"--port", type=int, default=DEFAULT_PORT,
|
|
131
|
+
help=f"Dashboard port (default: {DEFAULT_PORT}).",
|
|
132
|
+
)
|
|
133
|
+
p.add_argument(
|
|
134
|
+
"--yes", "-y", action="store_true",
|
|
135
|
+
help="Skip per-file confirmations (still honors --no-* component opt-outs).",
|
|
136
|
+
)
|
|
137
|
+
return p
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _explicit_components(args: argparse.Namespace) -> list[str]:
|
|
141
|
+
"""Components explicitly opted in via --plugin / --dashboard / etc."""
|
|
142
|
+
return [
|
|
143
|
+
c for c in installers.ALL_COMPONENTS
|
|
144
|
+
if getattr(args, c, False)
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _excluded_components(args: argparse.Namespace) -> set[str]:
|
|
149
|
+
"""Components explicitly opted out via --no-*.
|
|
150
|
+
|
|
151
|
+
Also auto-excludes a slash command companion when its parent MCP component
|
|
152
|
+
is excluded - slash commands need the MCP server to function, so installing
|
|
153
|
+
`codex_commands` without `codex` is a foot-gun. Users who really want that
|
|
154
|
+
asymmetry can override by passing `--codex-commands` explicitly.
|
|
155
|
+
"""
|
|
156
|
+
excluded = {
|
|
157
|
+
c for c in installers.ALL_COMPONENTS
|
|
158
|
+
if getattr(args, f"no_{c}", False)
|
|
159
|
+
}
|
|
160
|
+
for parent, child in COMMAND_IMPLIES.items():
|
|
161
|
+
if parent in excluded and not getattr(args, child, False):
|
|
162
|
+
excluded.add(child)
|
|
163
|
+
return excluded
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _expand_implies(selected: list[str], excluded: set[str]) -> list[str]:
|
|
167
|
+
"""Auto-add slash command companions for selected MCP integration parents.
|
|
168
|
+
|
|
169
|
+
`--codex` (or any `--all` that includes codex) implicitly turns on
|
|
170
|
+
`codex_commands` so that opting in to the Codex integration delivers the
|
|
171
|
+
full parity experience by default. Use `--no-codex-commands` to install
|
|
172
|
+
MCP without slash commands. Same pattern for opencode and vscode.
|
|
173
|
+
|
|
174
|
+
No-op if the child is already in `selected` (e.g. user passed both
|
|
175
|
+
`--codex` and `--codex-commands`) or explicitly excluded.
|
|
176
|
+
"""
|
|
177
|
+
out = list(selected)
|
|
178
|
+
for parent, child in COMMAND_IMPLIES.items():
|
|
179
|
+
if parent in out and child not in out and child not in excluded:
|
|
180
|
+
out.append(child)
|
|
181
|
+
return out
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _resolve_mode_and_repo(args: argparse.Namespace) -> tuple[str, Path | None]:
|
|
185
|
+
"""Decide pypi vs local mode and locate the repo root if local."""
|
|
186
|
+
cwd = Path.cwd()
|
|
187
|
+
marker = cwd / ".claude-plugin" / "plugin.json"
|
|
188
|
+
if args.local:
|
|
189
|
+
if not marker.exists():
|
|
190
|
+
ui.fail(
|
|
191
|
+
f"--local requires running from a orbit-pm clone "
|
|
192
|
+
f"(expected {marker} to exist)."
|
|
193
|
+
)
|
|
194
|
+
return "local", cwd
|
|
195
|
+
if marker.exists():
|
|
196
|
+
# Silent auto-detect: if they're in a clone, assume maintainer workflow.
|
|
197
|
+
return "local", cwd
|
|
198
|
+
return "pypi", None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _expand_command_pairs(requested: list[str], installed: list[str]) -> list[str]:
|
|
202
|
+
"""Auto-add `<tool>_commands` when uninstalling its parent `<tool>`.
|
|
203
|
+
|
|
204
|
+
Symmetric counterpart to the wizard's COMMAND_IMPLIES install-side pairing:
|
|
205
|
+
install pairs codex+codex_commands, so uninstall should too. Only adds the
|
|
206
|
+
child if it's still in the tracked-installed list (user may have already
|
|
207
|
+
removed it independently).
|
|
208
|
+
|
|
209
|
+
Asymmetric on purpose: removing `codex_commands` does NOT also remove
|
|
210
|
+
`codex` - users may want MCP without slash commands.
|
|
211
|
+
"""
|
|
212
|
+
out = list(requested)
|
|
213
|
+
for parent, child in COMMAND_IMPLIES.items():
|
|
214
|
+
if parent in out and child in installed and child not in out:
|
|
215
|
+
out.append(child)
|
|
216
|
+
return out
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _filter_known_state(tracked: list[str]) -> list[str]:
|
|
220
|
+
"""Drop state.json keys that are no longer in `ALL_COMPONENTS`.
|
|
221
|
+
|
|
222
|
+
Schema-evolution defense: if a future release deletes a component, an
|
|
223
|
+
older state.json may still name it. Filter and warn so the user sees
|
|
224
|
+
the orphan instead of a silent no-op or KeyError downstream.
|
|
225
|
+
"""
|
|
226
|
+
valid = [c for c in tracked if c in installers.ALL_COMPONENTS]
|
|
227
|
+
unknown = [c for c in tracked if c not in installers.ALL_COMPONENTS]
|
|
228
|
+
if unknown:
|
|
229
|
+
ui.warn(
|
|
230
|
+
f"State file references unknown components: {', '.join(unknown)}.\n"
|
|
231
|
+
" These are not in this missioncache-install version's ALL_COMPONENTS list. "
|
|
232
|
+
"Skipping them."
|
|
233
|
+
)
|
|
234
|
+
return valid
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _run_uninstall(args: argparse.Namespace, ctx: installers.InstallContext) -> int:
|
|
238
|
+
"""Dispatch the three uninstall patterns.
|
|
239
|
+
|
|
240
|
+
Patterns:
|
|
241
|
+
- `--uninstall --all` -> remove every tracked component. Refuses (warn +
|
|
242
|
+
no-op) if no state is tracked, matching `update_all`'s safer pattern.
|
|
243
|
+
- `--uninstall comp1,comp2` -> remove the listed components. Errors if
|
|
244
|
+
state is empty, list is empty after parsing, components are unknown,
|
|
245
|
+
or components aren't currently installed. Auto-expands `<tool>` to
|
|
246
|
+
include `<tool>_commands` if the latter is still tracked.
|
|
247
|
+
- `--uninstall` (bare, sentinel `INTERACTIVE_WIZARD`) -> interactive
|
|
248
|
+
wizard. Errors on non-TTY shells or empty state.
|
|
249
|
+
|
|
250
|
+
Combining `--all` with a positive list (e.g. `--uninstall foo --all`) is
|
|
251
|
+
an ambiguous error. Empty-string input (e.g. unset shell var) is rejected.
|
|
252
|
+
"""
|
|
253
|
+
uninstall_arg = args.uninstall
|
|
254
|
+
bypass_wizard = args.all
|
|
255
|
+
|
|
256
|
+
if isinstance(uninstall_arg, str) and bypass_wizard:
|
|
257
|
+
# `--uninstall foo --all` is ambiguous: positive list AND --all both
|
|
258
|
+
# specified. (`--uninstall --all` alone has uninstall_arg=sentinel.)
|
|
259
|
+
ui.fail(
|
|
260
|
+
"Pass either `--uninstall --all` (everything tracked) OR "
|
|
261
|
+
"`--uninstall <list>` (specific components), not both."
|
|
262
|
+
)
|
|
263
|
+
raise AssertionError("unreachable") # ui.fail exits
|
|
264
|
+
|
|
265
|
+
if bypass_wizard:
|
|
266
|
+
tracked = _filter_known_state(state.installed_components())
|
|
267
|
+
if not tracked:
|
|
268
|
+
ui.warn(
|
|
269
|
+
"No tracked components to uninstall. State file is empty or "
|
|
270
|
+
"missing.\n"
|
|
271
|
+
" If you installed missioncache manually outside the installer, "
|
|
272
|
+
"remove components by hand or restore the state file at "
|
|
273
|
+
f"{state.STATE_FILE}."
|
|
274
|
+
)
|
|
275
|
+
return 0
|
|
276
|
+
installers.uninstall_components(tracked, ctx)
|
|
277
|
+
return 0
|
|
278
|
+
|
|
279
|
+
if isinstance(uninstall_arg, str):
|
|
280
|
+
# Empty-string from unset shell var (`--uninstall "$EMPTY"`) lands
|
|
281
|
+
# here as `""` and is rejected loudly. Bare flag would have been
|
|
282
|
+
# the sentinel, never `""`.
|
|
283
|
+
if not uninstall_arg.strip():
|
|
284
|
+
ui.fail(
|
|
285
|
+
f"Empty `--uninstall` argument: {uninstall_arg!r}.\n"
|
|
286
|
+
" Pass a comma-separated component list, `--all`, or invoke "
|
|
287
|
+
"without a value to open the interactive wizard."
|
|
288
|
+
)
|
|
289
|
+
raise AssertionError("unreachable") # ui.fail exits
|
|
290
|
+
|
|
291
|
+
requested = [
|
|
292
|
+
c.strip().lower().replace("-", "_")
|
|
293
|
+
for c in uninstall_arg.split(",")
|
|
294
|
+
if c.strip()
|
|
295
|
+
]
|
|
296
|
+
# Dedup while preserving first-occurrence order.
|
|
297
|
+
requested = list(dict.fromkeys(requested))
|
|
298
|
+
|
|
299
|
+
# Separator-only input (`,`, ` , , `, etc.) bypasses the whitespace
|
|
300
|
+
# guard above (",".strip() == ",") but yields an empty list after
|
|
301
|
+
# the if-c.strip() filter. Without this check we'd silently no-op
|
|
302
|
+
# via uninstall_components([]) - same failure mode the empty-string
|
|
303
|
+
# guard prevents.
|
|
304
|
+
if not requested:
|
|
305
|
+
ui.fail(
|
|
306
|
+
f"No component names found in `--uninstall {uninstall_arg!r}`.\n"
|
|
307
|
+
" Input contained only commas/whitespace. Pass a real "
|
|
308
|
+
"component list, `--all`, or invoke without a value for the "
|
|
309
|
+
"interactive wizard."
|
|
310
|
+
)
|
|
311
|
+
raise AssertionError("unreachable") # ui.fail exits
|
|
312
|
+
|
|
313
|
+
unknown = [c for c in requested if c not in installers.ALL_COMPONENTS]
|
|
314
|
+
if unknown:
|
|
315
|
+
ui.fail(
|
|
316
|
+
f"Unknown components: {', '.join(unknown)}.\n"
|
|
317
|
+
f" Valid components: {', '.join(installers.ALL_COMPONENTS)}"
|
|
318
|
+
)
|
|
319
|
+
raise AssertionError("unreachable") # ui.fail exits
|
|
320
|
+
|
|
321
|
+
installed = _filter_known_state(state.installed_components())
|
|
322
|
+
if not installed:
|
|
323
|
+
ui.fail(
|
|
324
|
+
"No prior missioncache-install was tracked.\n"
|
|
325
|
+
" Use `--uninstall --all` to attempt a best-effort uninstall."
|
|
326
|
+
)
|
|
327
|
+
raise AssertionError("unreachable") # ui.fail exits
|
|
328
|
+
|
|
329
|
+
# Auto-expand <tool> to include <tool>_commands. Inform the user
|
|
330
|
+
# so they don't think we're going off-script. Re-dedupe in case
|
|
331
|
+
# they explicitly passed both parent and child.
|
|
332
|
+
expanded = list(dict.fromkeys(_expand_command_pairs(requested, installed)))
|
|
333
|
+
added = [c for c in expanded if c not in requested]
|
|
334
|
+
if added:
|
|
335
|
+
ui.detail(
|
|
336
|
+
f"Auto-adding paired components: {', '.join(added)} "
|
|
337
|
+
"(parent install pairs them; uninstall keeps the pairing)."
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
not_installed = [c for c in expanded if c not in installed]
|
|
341
|
+
if not_installed:
|
|
342
|
+
ui.fail(
|
|
343
|
+
f"Not currently installed: {', '.join(not_installed)}.\n"
|
|
344
|
+
f" Currently installed: {', '.join(installed)}"
|
|
345
|
+
)
|
|
346
|
+
raise AssertionError("unreachable") # ui.fail exits
|
|
347
|
+
|
|
348
|
+
installers.uninstall_components(expanded, ctx)
|
|
349
|
+
return 0
|
|
350
|
+
|
|
351
|
+
# Bare `--uninstall` -> interactive wizard.
|
|
352
|
+
components = wizard.run_uninstall_wizard()
|
|
353
|
+
if components is None:
|
|
354
|
+
# Wizard either reported an error itself (already exited) or the
|
|
355
|
+
# user cancelled. Either way, no work to do.
|
|
356
|
+
return 0
|
|
357
|
+
|
|
358
|
+
# Mirror the positive-list path: auto-expand <tool> -> <tool>_commands
|
|
359
|
+
# so picking `codex` from the wizard menu also removes its paired
|
|
360
|
+
# slash-command plugin. Without this, the wizard path would leave
|
|
361
|
+
# orphaned `/missioncache-*` commands pointing at a removed MCP integration.
|
|
362
|
+
installed = _filter_known_state(state.installed_components())
|
|
363
|
+
expanded = list(dict.fromkeys(_expand_command_pairs(components, installed)))
|
|
364
|
+
added = [c for c in expanded if c not in components]
|
|
365
|
+
if added:
|
|
366
|
+
ui.detail(
|
|
367
|
+
f"Auto-adding paired components: {', '.join(added)} "
|
|
368
|
+
"(parent install pairs them; uninstall keeps the pairing)."
|
|
369
|
+
)
|
|
370
|
+
installers.uninstall_components(expanded, ctx)
|
|
371
|
+
return 0
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def main() -> int:
|
|
375
|
+
args = build_parser().parse_args()
|
|
376
|
+
if args.uninstall is not None and args.update:
|
|
377
|
+
ui.fail("--uninstall and --update cannot be combined (different verbs).")
|
|
378
|
+
raise AssertionError("unreachable") # ui.fail exits
|
|
379
|
+
mode, repo_root = _resolve_mode_and_repo(args)
|
|
380
|
+
state.set_mode(mode)
|
|
381
|
+
ctx = installers.InstallContext(
|
|
382
|
+
mode=mode,
|
|
383
|
+
repo_root=repo_root,
|
|
384
|
+
skip_service=args.no_service,
|
|
385
|
+
port=args.port,
|
|
386
|
+
assume_yes=args.yes,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
if args.uninstall is not None:
|
|
390
|
+
return _run_uninstall(args, ctx)
|
|
391
|
+
|
|
392
|
+
if args.update:
|
|
393
|
+
installers.update_all(ctx)
|
|
394
|
+
return 0
|
|
395
|
+
|
|
396
|
+
explicit = _explicit_components(args)
|
|
397
|
+
excluded = _excluded_components(args)
|
|
398
|
+
|
|
399
|
+
if args.all or explicit:
|
|
400
|
+
base = list(installers.ALL_COMPONENTS) if args.all else explicit
|
|
401
|
+
selected = [c for c in base if c not in excluded]
|
|
402
|
+
selected = _expand_implies(selected, excluded)
|
|
403
|
+
# statusline needs the missioncache-statusline entry point, which ships in the
|
|
404
|
+
# missioncache-dashboard package. Installing statusline without dashboard wires
|
|
405
|
+
# settings.json to a command that won't resolve. Auto-add dashboard.
|
|
406
|
+
if "statusline" in selected and "dashboard" not in selected and "dashboard" not in excluded:
|
|
407
|
+
ui.warn("statusline depends on missioncache-dashboard (provides the missioncache-statusline entry point). Adding dashboard to the install.")
|
|
408
|
+
selected.insert(selected.index("statusline"), "dashboard")
|
|
409
|
+
if not selected:
|
|
410
|
+
ui.warn("Component selection is empty after applying --no-* flags.")
|
|
411
|
+
return 0
|
|
412
|
+
ui.banner()
|
|
413
|
+
ui.info(f"Installing: {', '.join(c.replace('_', '-') for c in selected)}")
|
|
414
|
+
installers.install_components(selected, ctx)
|
|
415
|
+
ui.success_banner(selected, dashboard_port=ctx.port)
|
|
416
|
+
return 0
|
|
417
|
+
|
|
418
|
+
# Default: interactive wizard.
|
|
419
|
+
wizard.run(ctx)
|
|
420
|
+
return 0
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
if __name__ == "__main__":
|
|
424
|
+
sys.exit(main())
|