missioncache-install 1.0.0__py3-none-any.whl
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/__init__.py +3 -0
- missioncache_install/__main__.py +424 -0
- missioncache_install/bundled/commands/done.md +137 -0
- missioncache_install/bundled/commands/load.md +326 -0
- missioncache_install/bundled/commands/mode.md +186 -0
- missioncache_install/bundled/commands/new.md +281 -0
- missioncache_install/bundled/commands/prompts.md +364 -0
- missioncache_install/bundled/commands/rename.md +156 -0
- missioncache_install/bundled/commands/save.md +174 -0
- missioncache_install/bundled/rules/missioncache.md +136 -0
- missioncache_install/bundled/user_commands/optimize-prompt.md +293 -0
- missioncache_install/bundled/user_commands/whats-new.md +204 -0
- missioncache_install/command_clients.py +752 -0
- missioncache_install/installers.py +681 -0
- missioncache_install/mcp_clients.py +426 -0
- missioncache_install/prereqs.py +147 -0
- missioncache_install/settings.py +129 -0
- missioncache_install/state.py +105 -0
- missioncache_install/subprocess_utils.py +83 -0
- missioncache_install/ui.py +192 -0
- missioncache_install/wizard.py +271 -0
- missioncache_install-1.0.0.dist-info/METADATA +87 -0
- missioncache_install-1.0.0.dist-info/RECORD +25 -0
- missioncache_install-1.0.0.dist-info/WHEEL +4 -0
- missioncache_install-1.0.0.dist-info/entry_points.txt +2 -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())
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Mark an active project as completed and archive files"
|
|
3
|
+
argument-hint: "[project-name]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Complete Project
|
|
7
|
+
|
|
8
|
+
Mark a project as completed and optionally move MissionCache files to the completed folder.
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
1. **If project name provided:**
|
|
13
|
+
```
|
|
14
|
+
mcp__plugin_missioncache_pm__complete_task(project_name="<name>", move_files=true)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
2. **If no project name, list active projects:**
|
|
18
|
+
```
|
|
19
|
+
mcp__plugin_missioncache_pm__list_active_tasks()
|
|
20
|
+
```
|
|
21
|
+
Then ask user to select one.
|
|
22
|
+
|
|
23
|
+
## Workflow
|
|
24
|
+
|
|
25
|
+
### Step 1: Confirm Project
|
|
26
|
+
|
|
27
|
+
If project name not provided, list active projects and ask user to select.
|
|
28
|
+
|
|
29
|
+
### Step 2: Show Summary
|
|
30
|
+
|
|
31
|
+
Before completing, show the user:
|
|
32
|
+
- Total time invested
|
|
33
|
+
- Progress (should be 100%)
|
|
34
|
+
- What will happen (files moved, status changed)
|
|
35
|
+
|
|
36
|
+
### Step 3: Complete
|
|
37
|
+
|
|
38
|
+
Call `mcp__plugin_missioncache_pm__complete_task` which:
|
|
39
|
+
1. Updates project status to "completed" in database
|
|
40
|
+
2. Moves files from `~/.missioncache/active/<name>/` to `~/.missioncache/completed/<name>/`
|
|
41
|
+
3. Records completion timestamp
|
|
42
|
+
|
|
43
|
+
### Step 4: Process Time Tracking
|
|
44
|
+
|
|
45
|
+
Call `mcp__plugin_missioncache_pm__process_heartbeats()` to finalize time tracking.
|
|
46
|
+
|
|
47
|
+
### Step 5: Clear Statusline
|
|
48
|
+
|
|
49
|
+
Remove the project pointer so the statusline stops showing the completed project name. Mirrors the resolver in `/missioncache:new` / `/missioncache:load` (filesystem primary, term-env fallback) and uses direct SQL because the dashboard has no DELETE endpoint for project_state. Silently no-ops on quick-install setups without `hooks-state.db`.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Primary: env var set by Claude Code 2.1.132+ in every Bash tool subprocess.
|
|
53
|
+
SESSION_ID="$CLAUDE_CODE_SESSION_ID"
|
|
54
|
+
|
|
55
|
+
# Fallback for older Claude Code versions. SessionStart hook writes the
|
|
56
|
+
# authoritative current-session pointer at ~/.claude/hooks/state/cwd-session/
|
|
57
|
+
# <sanitized-cwd>.json; transcript mtime walk catches sessions that started
|
|
58
|
+
# before the pointer mechanism landed.
|
|
59
|
+
if [ -z "$SESSION_ID" ]; then
|
|
60
|
+
CWD_KEY=$(pwd | sed 's|/|-|g')
|
|
61
|
+
POINTER_FILE="$HOME/.claude/hooks/state/cwd-session/${CWD_KEY}.json"
|
|
62
|
+
if [ -r "$POINTER_FILE" ]; then
|
|
63
|
+
SESSION_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin)['sessionId'])" < "$POINTER_FILE" 2>/dev/null)
|
|
64
|
+
fi
|
|
65
|
+
[ -z "$SESSION_ID" ] && SESSION_ID=$(ls -t "$HOME/.claude/projects/${CWD_KEY}"/*.jsonl 2>/dev/null | head -1 | xargs -I{} basename {} .jsonl)
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# Delete project_state row. String concatenation, not f-strings, because
|
|
69
|
+
# Python <=3.11 rejects backslashes inside f-string expressions and the
|
|
70
|
+
# outer single-quoted bash heredoc forces escaped double quotes inside.
|
|
71
|
+
if [ -n "$SESSION_ID" ]; then
|
|
72
|
+
SESSION_ID="$SESSION_ID" python3 -c '
|
|
73
|
+
import os, sqlite3
|
|
74
|
+
sid = os.environ["SESSION_ID"]
|
|
75
|
+
conn = sqlite3.connect(os.path.expanduser("~/.claude/hooks-state.db"))
|
|
76
|
+
cur = conn.execute("DELETE FROM project_state WHERE session_id = ?", (sid,))
|
|
77
|
+
conn.commit()
|
|
78
|
+
print("Cleared project_state for session " + sid + " (rows: " + str(cur.rowcount) + ")")
|
|
79
|
+
' 2>/dev/null
|
|
80
|
+
|
|
81
|
+
# Also delete the per-session project pointer written by /missioncache:load and /missioncache:new.
|
|
82
|
+
# Read by find_task_for_cwd (missioncache-db); leaving it in place would make /missioncache:save
|
|
83
|
+
# still find this task after completion.
|
|
84
|
+
rm -f "$HOME/.claude/hooks/state/projects/${SESSION_ID}.json" 2>/dev/null
|
|
85
|
+
fi
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Step 6: Share Dashboard Link (if running)
|
|
89
|
+
|
|
90
|
+
Probe the dashboard and, if reachable, include a deep link in the completion summary so the user can jump straight to the archived project view. Skip silently if the dashboard is not installed or not running - dead links train users to ignore the dashboard entirely.
|
|
91
|
+
|
|
92
|
+
Replace `<project-name>` with the kebab-case project name, then run:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
PROJECT_NAME='<project-name>'
|
|
96
|
+
DASHBOARD_URL="${MISSIONCACHE_DASHBOARD_URL:-http://localhost:8787}"
|
|
97
|
+
if curl -sf -o /dev/null --max-time 1 "${DASHBOARD_URL}/health" 2>/dev/null; then
|
|
98
|
+
echo "Dashboard: ${DASHBOARD_URL}/#projects?task=$PROJECT_NAME"
|
|
99
|
+
fi
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
If the probe succeeds, include the emitted URL as a "Dashboard" line in the completion summary shown below. If it emits nothing, omit the line entirely.
|
|
103
|
+
|
|
104
|
+
## Example Output
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
## Completing Project: kafka-consumer-fix
|
|
108
|
+
|
|
109
|
+
**Time Invested:** 4h 30m
|
|
110
|
+
**Progress:** 8/8 tasks (100%)
|
|
111
|
+
**Status:** active -> completed
|
|
112
|
+
|
|
113
|
+
Moving files:
|
|
114
|
+
~/.missioncache/active/kafka-consumer-fix/ -> ~/.missioncache/completed/kafka-consumer-fix/
|
|
115
|
+
|
|
116
|
+
Project completed successfully!
|
|
117
|
+
|
|
118
|
+
Summary:
|
|
119
|
+
- Total time: 4h 30m
|
|
120
|
+
- Sessions: 12
|
|
121
|
+
- Completed at: 2026-01-20 15:30
|
|
122
|
+
- Dashboard: http://localhost:8787/#projects?task=kafka-consumer-fix
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Options
|
|
126
|
+
|
|
127
|
+
- `move_files=true` (default): Move MissionCache files to completed/
|
|
128
|
+
- `move_files=false`: Keep files in active/ (useful for reference)
|
|
129
|
+
|
|
130
|
+
## MCP Tools Used
|
|
131
|
+
|
|
132
|
+
| Tool | Purpose |
|
|
133
|
+
|------|---------|
|
|
134
|
+
| `mcp__plugin_missioncache_pm__list_active_tasks` | List projects if none specified |
|
|
135
|
+
| `mcp__plugin_missioncache_pm__get_task` | Get project details for summary |
|
|
136
|
+
| `mcp__plugin_missioncache_pm__complete_task` | Mark complete and move files |
|
|
137
|
+
| `mcp__plugin_missioncache_pm__process_heartbeats` | Finalize time tracking |
|