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.
@@ -0,0 +1,18 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.egg
9
+ .venv/
10
+ venv/
11
+ .env
12
+ *.duckdb
13
+ *.duckdb.wal
14
+ *.db-journal
15
+ .DS_Store
16
+ CLAUDE.local.md
17
+ .serena/
18
+ .playwright-mcp/
@@ -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,3 @@
1
+ """MissionCache installer - bootstrap package for MissionCache on Claude Code."""
2
+
3
+ __version__ = "1.0.0"
@@ -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())