8Wrap ``pip install -e <target>`` with safety preflight checks that
9catch the most common operator-error patterns: running the install
10from the wrong terminal window with the wrong venv active, in the
11wrong CWD, or against a worktree that would shadow a sibling worktree.
15- Define the :func:`oct_install_cmd` Click command, mounted on the
16 top-level ``cli`` group.
17- Resolve and validate the install target (an Option C project root).
18- Verify the active venv matches the target's expected venv.
19- Detect existing-shadow installs from another worktree.
20- Refuse to install from inside a Claude Code worktree without an
22- Delegate to ``pip install -e <target>`` via :mod:`subprocess` after
23 preflights succeed; print post-install diagnostics so the user
24 sees exactly which ``oct`` binary will be invoked next.
30 L1 - errors (pip failure, preflight refusal)
31 L2 - lifecycle (preflight start/end, install start/end)
32 L3 - details (venv path, target path, shadow location)
33 L4 - deep trace (each preflight check)
37- All subprocess calls use ``shell=False`` and explicit argv lists.
38- No filesystem writes are performed by this command itself; only
39 ``pip`` (the delegated subprocess) writes to the venv's
41- Audit trail via the existing :func:`oct.git.audit.audited` decorator
42 is intentionally NOT applied here, because the audit module emits
43 to ``logs/git-audit-*.jsonl`` which lives under the project root -
44 but ``oct install`` may run before any project root exists. A
45 lightweight ``_dbg`` trace is kept instead.
49- oct.core.diagnostics (_dbg structured logger)
50- click (CLI framework)
53from __future__
import annotations
59from pathlib
import Path
63from oct.core.diagnostics
import _dbg
73_LOCAL_VENV_NAMES: tuple[str, ...] = (
".venv",
"venv")
77_PIP_TIMEOUT: int = 600
86 """Return True if *target* looks like an Option C project root.
88 Accept both legacy (``.octrc.json`` at root) and migrated
89 (``.option_c/`` directory) layouts. ``pyproject.toml`` must also
90 be present so ``pip install -e`` has something to install.
92 if not (target /
"pyproject.toml").is_file():
94 return (target /
".octrc.json").is_file() \
95 or (target /
".option_c").is_dir()
99 """Return the ``[project] name`` from *target*'s pyproject.toml."""
100 func =
"_read_project_name"
101 pyproject = target /
"pyproject.toml"
102 if not pyproject.is_file():
105 with pyproject.open(
"rb")
as fh:
106 data = tomllib.load(fh)
107 except (OSError, tomllib.TOMLDecodeError)
as exc:
108 _dbg(
"INSTALL", func, f
"pyproject parse failed: {exc}", 1, override=
True)
110 project = data.get(
"project")
111 if not isinstance(project, dict):
113 name = project.get(
"name")
114 return name
if isinstance(name, str)
else None
118 """Return the root of the currently active venv, or ``None``.
120 ``sys.prefix`` is the venv root for an active virtual environment,
121 ``sys.base_prefix`` is the system Python. When they differ, a venv
122 is active. If they match, no venv is active.
124 if sys.prefix != sys.base_prefix:
125 return Path(sys.prefix).resolve()
130 """Return the first existing local venv directory under *target*."""
131 target_resolved = target.resolve()
132 for name
in _LOCAL_VENV_NAMES:
133 candidate = target_resolved / name
134 if candidate.is_dir():
140 """Return True if *child* is at or below *parent*."""
142 child.resolve().relative_to(parent.resolve())
149 """Return the ``Location:`` from ``pip show <pkg>`` or ``None``.
151 Runs ``<venv>/Scripts/python -m pip show <pkg>`` (Windows) or
152 ``<venv>/bin/python -m pip show <pkg>`` (Unix) when *venv* is
153 given; falls back to ``sys.executable`` otherwise.
155 func =
"_pip_show_location"
156 python =
_venv_python(venv)
if venv
is not None else Path(sys.executable)
158 proc = subprocess.run(
159 [str(python),
"-m",
"pip",
"show", pkg],
160 shell=
False, capture_output=
True, text=
True, timeout=30,
162 except (subprocess.TimeoutExpired, FileNotFoundError)
as exc:
163 _dbg(
"INSTALL", func, f
"pip show failed: {exc}", 1, override=
True)
165 if proc.returncode != 0:
167 for line
in proc.stdout.splitlines():
168 if line.startswith(
"Location:"):
169 loc = line.split(
":", 1)[1].strip()
171 return Path(loc).resolve()
176 """Return the python executable path inside *venv*."""
177 if (venv /
"Scripts" /
"python.exe").is_file():
178 return (venv /
"Scripts" /
"python.exe").resolve()
179 if (venv /
"bin" /
"python").is_file():
180 return (venv /
"bin" /
"python").resolve()
181 return Path(sys.executable).resolve()
185 """Return the path to the ``oct`` script, preferring *venv*'s bin."""
187 for sub
in (
"Scripts/oct.exe",
"Scripts/oct",
"bin/oct"):
188 candidate = venv / sub
189 if candidate.is_file():
190 return candidate.resolve()
193 found = _sh.which(
"oct")
194 return Path(found).resolve()
if found
else None
198 """Return True if *target* lives under any ``.claude/worktrees/`` ancestor."""
199 target_resolved = target.resolve()
200 for parent
in [target_resolved, *target_resolved.parents]:
201 if parent.name ==
"worktrees" and parent.parent.name ==
".claude":
211@click.command(name="install")
213 "target", type=click.Path(file_okay=
False, dir_okay=
True),
217 "--editable",
"-e",
"editable", is_flag=
True,
218 help=
"Install in editable mode (pip install -e). Required for now.",
221 "--reinstall", is_flag=
True,
222 help=
"Allow re-install over an existing shadow install.",
225 "--use-active-venv", is_flag=
True,
226 help=
"Bypass the venv-match check and use whichever venv is active.",
229 "--allow-worktree", is_flag=
True,
230 help=
"Allow installing from inside a Claude Code worktree.",
233 "--allow-outside-project", is_flag=
True,
234 help=
"Allow running outside a recognised Option C project.",
237 "--json",
"json_mode", is_flag=
True, help=
"Emit JSON output.",
245 use_active_venv: bool,
246 allow_worktree: bool,
247 allow_outside_project: bool,
250 """Install an Option C project as an editable package, with safety checks.
252 Wraps ``pip install -e <target>`` after verifying:
254 1. The target is an Option C project (pyproject.toml + .octrc.json
256 2. The active venv matches the target's expected venv (refuse with
257 a precise diagnostic otherwise).
258 3. No existing shadow install from a different worktree is about
259 to be silently overwritten.
260 4. The target is not inside a Claude Code worktree (refusable
261 with --allow-worktree).
263 func =
"oct_install_cmd"
266 f
"target={target} editable={editable} reinstall={reinstall} "
267 f
"use_active_venv={use_active_venv} allow_worktree={allow_worktree}",
273 "Error: --editable / -e is required. Non-editable installs "
274 "are out of scope for `oct install`.",
280 target_path = Path(target).resolve()
281 refusals: list[str] = []
282 warnings: list[str] = []
287 f
"target {target_path} is not an Option C project "
288 f
"(missing pyproject.toml + (.octrc.json | .option_c/))"
294 f
"target {target_path} is inside a Claude Code worktree "
295 f
"(.claude/worktrees/...). Pass --allow-worktree to override."
299 cwd = Path.cwd().resolve()
303 if cwd != target_path:
305 f
"CWD {cwd} is not an Option C project root. "
306 f
"Continuing because target {target_path} is."
313 if not use_active_venv:
314 if expected_venv
is None:
316 f
"target has no local venv at {target_path}/.venv "
317 f
"or {target_path}/venv; skipping venv-match check"
319 elif active_venv
is None:
321 f
"no active venv detected; project expects {expected_venv}. "
322 f
"Activate the venv (.venv\\Scripts\\activate or "
323 f
"source .venv/bin/activate), or pass --use-active-venv."
327 same = active_venv.samefile(expected_venv)
329 same = active_venv == expected_venv
332 f
"active venv is at {active_venv}; project expects "
333 f
"{expected_venv}. Activate the right venv, or pass "
334 f
"--use-active-venv to override."
339 shadow_loc: Path |
None =
None
340 if not refusals
and pkg_name:
343 active_venv
if use_active_venv
or expected_venv
is None
346 if shadow_loc
is not None:
349 expected_loc = target_path
350 if not _is_under(shadow_loc, expected_loc):
353 f
"package '{pkg_name}' is already installed "
354 f
"with Location={shadow_loc}, which is NOT under "
355 f
"the install target {target_path}. This would "
356 f
"silently shadow another worktree's install. "
357 f
"Pass --reinstall to overwrite, or activate the "
362 f
"overwriting existing install at {shadow_loc} "
363 f
"(--reinstall acknowledged)"
369 click.echo(json.dumps({
370 "operation":
"install",
371 "target": str(target_path),
374 "warnings": warnings,
377 click.echo(
"oct install preflight FAILED:", err=
True)
379 click.echo(f
" - {r}", err=
True)
381 click.echo(f
" ! {w}", err=
True)
385 if warnings
and not json_mode:
387 click.echo(f
"Warning: {w}", err=
True)
390 pip_python =
_venv_python(active_venv)
if active_venv
else Path(sys.executable)
391 pip_argv = [str(pip_python),
"-m",
"pip",
"install",
"-e", str(target_path)]
392 _dbg(
"INSTALL", func, f
"running: {pip_argv}", 2)
395 proc = subprocess.run(
396 pip_argv, shell=
False, timeout=_PIP_TIMEOUT,
397 text=
True, capture_output=json_mode,
399 except subprocess.TimeoutExpired:
401 f
"pip install timed out after {_PIP_TIMEOUT}s.",
406 except FileNotFoundError
as exc:
407 click.echo(f
"pip executable not found: {exc}", err=
True)
411 if proc.returncode != 0:
413 click.echo(json.dumps({
414 "operation":
"install",
415 "target": str(target_path),
417 "errors": [f
"pip install failed (exit {proc.returncode})"],
418 "stdout": proc.stdout,
"stderr": proc.stderr,
422 f
"pip install failed (exit {proc.returncode}).",
433 click.echo(json.dumps({
434 "operation":
"install",
435 "target": str(target_path),
437 "venv": str(active_venv)
if active_venv
else None,
438 "installed_location": str(new_loc)
if new_loc
else None,
439 "oct_binary": str(oct_path)
if oct_path
else None,
441 "warnings": warnings,
444 click.echo(f
"\noct install: {pkg_name or '<pkg>'} installed.")
446 click.echo(f
" Location: {new_loc}")
448 click.echo(f
" oct binary: {oct_path}")
450 click.echo(f
" venv: {active_venv}")
Path|None _pip_show_location(str pkg, Path|None venv)
Path|None _expected_venv_under(Path target)
Path|None _active_venv_root()
Path _venv_python(Path venv)
str|None _read_project_name(Path target)
None oct_install_cmd(click.Context ctx, str target, bool editable, bool reinstall, bool use_active_venv, bool allow_worktree, bool allow_outside_project, bool json_mode)
bool _is_inside_worktree(Path target)
bool _is_under(Path child, Path parent)
bool _is_option_c_project(Path target)
Path|None _which_oct(Path|None venv)