8Provide the main command-line interface for the OCT (Option C Tools) ecosystem.
12- Define top-level commands: oct lint, oct format, oct new, oct scaffold, oct docs,
13 oct test, oct export-source, oct clean, oct health.
14- Delegate work to the appropriate OCT modules.
15- Scaffold new Option C projects with a correct ``oc_diagnostics/debug_config.json``
16 pre-configured for use with ``oc_diagnostics``.
17- Set up Sphinx documentation templates in ``docs/``.
18- Ensure consistent behavior across all projects.
30- Must be installed as the console entry point for the OCT package.
31- Must not contain business logic; only dispatching.
32- Scaffolded ``debug_config.json`` must be a valid ``oc_diagnostics`` configuration.
36- oct (package version)
37- oct.core.project_root (project root detection)
38- oct.linter.oct_lint (linter command implementation)
39- oct.formatter.oct_format (formatter command implementation)
40- oct.health.oct_health (health dashboard command)
41- oct.generator.new_python_file (new file scaffolding)
42- oct.diag.oct_diag (diagnostics config commands)
43- oct.deps.oct_deps (dependency graph command)
44- oct.hooks.oct_hooks (pre-commit hooks generator)
45- oct.typecheck.oct_typecheck (type-checking wrapper)
46- click (CLI framework)
47- oct.complexity.command (complexity command)
48- oct.mutation.command (mutate command)
49- oct.git (git_group - the git command group)
50- oct.migrate (migrate_group - the migrate command group)
51- oct.audit (audit_group - the audit command)
52- oct.install.oct_install (oct_install_cmd - the install command)
53- oct.core.diagnostics (_dbg structured logger)
54- oc_diagnostics (lazy; init with file-only output before each command)
55- oct.timing.ledger (lazy; the timing stage around each command)
56- oct.core.compat (lazy; oc_diagnostics compatibility warning for scaffold)
57- oct.scaffold (lazy; run_scaffold for the scaffold command)
58- oct.scaffold.oct_scaffold (lazy; Claude Code skills copy for scaffold --with-claude)
59- oct.core.workspace (lazy; manifest subproject name for the Doxyfile PROJECT_NAME)
60- oct.docs.callgraph_command (lazy; docs --callgraph)
61- oct.docs.doxyfile_init (lazy; Doxyfile writer for docs --init-doxygen)
62- oct.docs.oct_docs (lazy; Sphinx, Doxygen, all-tier and clean docs runs, project name fallback)
63- oct.docs.typedoc (lazy; docs --typedoc)
64- oct.testing.cache (lazy; pytest cache plugin flags and the cached run's file count)
65- oct.core.git (lazy; changed-file discovery for test --changed)
66- oct.tools.source_exporter (lazy; export-source, and source export and pycache removal for clean)
67- oct.tools.skeleton_exporter (lazy; export-skeleton, and skeleton export removal for clean)
68- oct.tools.build_artifacts (lazy; build and dist tree removal for clean)
69- oct.git.oct_git (lazy; enhanced hooks template for the deprecated install-hooks)
70- oct.tools.install_mcp (lazy; install-mcp command)
71- oc_chat.registry (not imported; run as python -m oc_chat.registry by the agent command)
72- pytest (not imported; run as python -m pytest by the test command)
79from pathlib
import Path
81from oct
import __version__
83from oct.core.project_root
import find_project_root, get_project_root
88from oct.generator.new_python_file
import create_new_file
103 (
"--fix-headers",
"Automatically fix shebang, encoding, and path headers"),
104 (
"--dry-run",
"Preview changes without modifying files"),
105 (
"--json",
"Emit machine-readable JSON to stdout"),
106 (
"--jobs N, -j N",
"Parallel workers (default: 1)"),
107 (
"--changed",
"Lint only git-modified Python files"),
108 (
"--profile P",
"Lint profile: proto, compact, strict"),
109 (
"--secret-match MODE",
"Secret-name matcher: substring (default) | word"),
110 (
"--code-entropy-threshold N",
"Shannon entropy threshold for code strings (default: 4.5)"),
111 (
"--no-entropy",
"Disable entropy-based secret detection"),
112 (
"--no-cache",
"Bypass lint cache reads (still writes)"),
113 (
"--baseline",
"Save current violations as the baseline"),
114 (
"--against-baseline",
"Report only new/resolved vs saved baseline"),
115 (
"--explain RULE",
"Show rule documentation (rationale, pattern, spec ref)"),
116 (
"PATH ...",
"Files or directories to lint (default: entire project)"),
119 (
"--fix",
"Apply formatting changes (default: dry-run)"),
120 (
"--dry-run",
"Report what would change without modifying"),
121 (
"--json",
"Output machine-readable JSON"),
122 (
"--verbose",
"Show per-file details"),
123 (
"--changed",
"Format only git-modified Python files"),
126 (
"SUBCOMMAND ...",
"Forwarded verbatim to 'python -m oc_chat.registry' "
127 "(init, doctor, queue, plan, round, roadmap, "
128 "regenerate, checkpoint, ...)"),
129 (
"(passthrough)",
"The registry JSON envelope, --json flags, and exit "
130 "codes (0/1/2/3/5) are preserved by the child CLI"),
133 (
"PATH",
"Target file path (required)"),
134 (
"--force",
"Overwrite existing file"),
135 (
"--test",
"Also generate companion test file in tests/"),
136 (
"--dry-run",
"Show what would be created without writing"),
137 (
"--verbose",
"Show generated file content"),
140 (
"NAME",
"Project name (required)"),
141 (
"--dry-run",
"Show what would be created without writing"),
142 (
"--no-venv",
"Skip .venv/ creation (FS-503: on by default)"),
143 (
"--with-claude",
"Copy bundled Claude Code skills into .claude/skills/"),
146 (
"--sphinx",
"Generate Sphinx HTML documentation"),
147 (
"--doxygen",
"Generate Doxygen HTML documentation"),
148 (
"--typedoc",
"Generate TypeDoc HTML documentation (needs typedoc.json)"),
149 (
"--all",
"Generate every declared tier: Sphinx, Doxygen, TypeDoc"),
150 (
"--clean",
"Remove generated documentation artifacts"),
151 (
"--dry-run",
"Preview what --clean would remove"),
152 (
"--no-workspace",
"Force single-project mode at a workspace root (FS-WS-DOCS)"),
153 (
"--init-doxygen",
"Write docs/doxygen_work_dir/Doxyfile from the bundled template"),
154 (
"--force",
"With --init-doxygen: overwrite an existing Doxyfile"),
155 (
"--callgraph QUALNAME",
"Full call graph of one function (DOT + SVG, docs/_build/callgraph/)"),
156 (
"--depth N",
"With --callgraph: stop N calls below the root"),
157 (
"--trace -- ARGS",
"With --callgraph: run 'oct ARGS' for real and compare what ran"),
160 (
"--json",
"Emit machine-readable JSON summary to stdout"),
161 (
"--changed",
"Run tests for git-modified files only"),
162 (
"--cached",
"Skip test files unchanged since their last green run"),
163 (
"--oct-cache-clear",
"Empty the test cache before running"),
166 (
"--verbose",
"Show per-file statistics"),
167 (
"--clean",
"[DEPRECATED: use 'oct clean'] Remove _source_code-* dirs"),
168 (
"--single-dir",
"Write all output into one root-level directory"),
169 (
"--diff REF",
"Export only files changed since REF"),
170 (
"--warn-secrets",
"Warn on suspected secrets in exported files"),
171 (
"--block-secrets",
"Abort export when secrets are detected"),
172 (
"--allow-outside-project",
"Allow export targets outside the project root"),
175 (
"--json",
"Emit machine-readable JSON summary to stdout"),
176 (
"--verbose",
"Show per-file statistics"),
177 (
"--single-dir",
"Write all output into one root-level directory"),
178 (
"--no-diagnostics",
"Omit Diagnostics section from docstrings"),
179 (
"--clean",
"Remove previously exported skeletons"),
180 (
"--allow-outside-project",
"Allow skeleton targets outside the project root"),
183 (
"--dry-run",
"Show what would be deleted without deleting"),
184 (
"--allow-outside-project",
"Allow cleaning paths outside the project root"),
187 (
"--tests",
"Run the project test suite"),
188 (
"--timing",
"Show only the timing report"),
189 (
"--timing-last N",
"Recent records per stage (default: 20)"),
190 (
"--write-baseline",
"Write this host's timing medians"),
191 (
"--force-host",
"Allow adding another baseline host"),
192 (
"--session PATH",
"Read tool latency and tokens from session events"),
193 (
"--json",
"Machine-readable JSON output"),
194 (
"--verbose",
"Detailed per-file breakdown"),
195 (
"--timeout N",
"Per-check timeout in seconds (default: 300)"),
198 (
"--coverage-data PATH",
"Map an existing coverage.py data file"),
199 (
"--json",
"Emit the full machine-readable report"),
200 (
"--top N",
"Limit human output to the worst N functions"),
201 (
"--max-cc N",
"Exit 2 when a function exceeds CC N (advisory: 15)"),
202 (
"--max-crap M",
"Exit 2 when a function exceeds CRAP M (advisory: 30)"),
203 (
"--changed",
"Measure only git-modified Python files"),
204 (
"--baseline PATH",
"Ratchet against recorded debt; new or worse fails"),
205 (
"--write-baseline PATH",
"Record this run's measurements as a baseline"),
206 (
"PATH ...",
"Python files or directories (default: project root)"),
209 (
"--target FILE",
"Python file to mutate (required)"),
210 (
"--tests PATH",
"Tests run once per mutant (required for a run)"),
211 (
"--timeout N",
"Seconds per test run before a mutant times out"),
212 (
"--json",
"Emit the full machine-readable report"),
213 (
"--list",
"Enumerate the mutants without running anything"),
216 (
"--json",
"JSON adjacency list output"),
217 (
"--mermaid",
"Mermaid diagram output"),
218 (
"--dot",
"DOT/Graphviz output"),
221 (
"TARGET",
"Project root to install (default: current directory)"),
222 (
"--editable, -e",
"Install in editable mode (required)"),
223 (
"--reinstall",
"Allow re-install over an existing shadow"),
224 (
"--use-active-venv",
"Bypass venv-match check"),
225 (
"--allow-worktree",
"Allow installing from a Claude Code worktree"),
226 (
"--allow-outside-project",
"Allow running outside an Option C project"),
227 (
"--json",
"Emit JSON output"),
230 (
"--force",
"Overwrite existing .pre-commit-config.yaml"),
233 (
"status",
"Show git status with Option C compliance annotations"),
234 (
"check",
"Run the unified quality gate (lint + format + secrets)"),
235 (
"init",
"Initialise or enhance a git repo with Option C conventions"),
236 (
"commit",
"Commit with quality gate and secrets enforcement"),
237 (
"branch",
"Create or switch branches with naming validation"),
238 (
"add",
"Stage files for commit (whole-file or interactive)"),
239 (
"restore",
"Restore working-tree edits or unstage paths"),
240 (
"ignore",
"Manage .gitignore patterns within the OCT-managed block"),
241 (
"commit-msg",
"Validate a commit message file (pre-commit stage)"),
242 (
"hooks",
"Manage Option C git hooks"),
243 (
"changelog",
"Generate changelog from Conventional Commits history"),
246 (
"--json",
"Emit machine-readable JSON output"),
247 (
"--no-check",
"Skip compliance annotations (plain git status)"),
248 (
"--scope SCOPE",
"Scope: project (default), repo, auto"),
251 (
"PATHS...",
"Files or directories to stage"),
252 (
"--patch, -p",
"Interactive hunk-level staging"),
253 (
"--update, -u",
"Stage all already-tracked modified files"),
254 (
"--all, -A",
"Stage all changes within project scope"),
255 (
"--dry-run",
"Show what would be staged without staging"),
256 (
"--json",
"Emit JSON output"),
259 (
"PATTERNS...",
"Glob patterns to add to .gitignore"),
260 (
"--remove PATTERN",
"Remove a pattern from the OCT-managed block"),
261 (
"--list",
"List currently active ignore patterns"),
262 (
"--scope SCOPE",
"Target .gitignore: workspace, project, nearest (default)"),
263 (
"--json",
"Emit JSON output"),
266 (
"PATHS...",
"Files to restore (default: scope by --all)"),
267 (
"--staged",
"Unstage paths from the index without touching working tree"),
268 (
"--all, -A",
"Restore all in-scope paths"),
269 (
"--yes",
"Skip confirmation prompt for --staged --all"),
270 (
"--json",
"Emit JSON output"),
273 (
"--dry-run",
"Show what would be created without writing"),
274 (
"--github",
"Also generate GitHub Actions workflow"),
275 (
"--no-hooks",
"Skip hook installation"),
278 (
"--create NAME",
"Create a new branch from HEAD"),
279 (
"--switch NAME",
"Switch to an existing branch"),
280 (
"--json",
"Emit JSON output"),
283 (
"-m MESSAGE, --message MESSAGE",
"Commit message (Conventional Commits format)"),
284 (
"--force",
"Skip lint/format checks (secrets still checked)"),
285 (
"--no-verify",
"Forward --no-verify to git (hooks skipped)"),
286 (
"--profile P",
"Override lint profile: proto, compact, strict"),
287 (
"--json",
"Emit JSON output"),
288 (
"--ignore-unstaged-mutation",
"Downgrade B1.0 mutation detection to warn for this run"),
291 (
"MESSAGE_FILE",
"Path to the commit message file (required)"),
292 (
"--json",
"Emit machine-readable JSON output"),
295 (
"--staged-only",
"Check only staged (index) files"),
296 (
"--all",
"Check every Python file in the project"),
297 (
"--include-tests",
"Also run `oct test` (for pre-push use)"),
298 (
"--fix",
"Attempt auto-fix of violations, then re-check"),
299 (
"--profile P",
"Override lint profile: proto, compact, strict"),
300 (
"--json",
"Emit machine-readable JSON output"),
301 (
"--sandbox",
"Run pytest under the MCP sandbox environment"),
304 (
"install",
"Generate .pre-commit-config.yaml with all hooks"),
305 (
"status",
"Show installed hook state"),
306 (
"remove",
"Remove Option C hooks (preserves non-OCT hooks)"),
309 (
"--release V",
"Release version tag (e.g. v0.13.0)"),
310 (
"--since REF",
"Git ref to start from (default: last tag)"),
311 (
"--dry-run",
"Print to stdout without writing"),
312 (
"--json",
"Emit structured JSON"),
314 "git hooks install": [
315 (
"--force",
"Overwrite existing configuration"),
318 (
"--json",
"Emit machine-readable JSON summary to stdout"),
319 (
"ARGS ...",
"Arguments forwarded to pyright/mypy"),
322 (
"validate-config",
"Validate debug_config.json against schema"),
323 (
" --strict",
"Treat validation warnings as errors"),
324 (
"list-domains",
"List configured domains with levels and colors"),
325 (
"set-level DOMAIN N",
"Set a domain's debug level (0-9)"),
326 (
" --dry-run",
"Preview edit without writing (use with set-level)"),
327 (
"restore",
"Restore debug_config.json from .bk backup"),
328 (
"migrate-config [PATH]",
"Upgrade legacy v1 schema to v2.0"),
329 (
" --dry-run",
"Preview migration without writing (use with migrate-config)"),
332 (
"--host HOST",
"Target AI host: claude-code, cursor, vscode, auto (default: auto)"),
333 (
"--dry-run / --no-dry-run",
"Preview without writing (default: --dry-run)"),
336 (
"option-c",
"Migrate to canonical .option_c/ dotdir layout (FS-539)"),
338 "migrate option-c": [
339 (
"--dry-run",
"Show plan without writing anything"),
340 (
"--no-backup",
"Skip .bk backup when overwriting"),
343 (
"--json-output",
"Emit JSON instead of human-readable text"),
351TIMING_SAMPLE_KEY =
"oct.timing.sample"
357NO_GLOB_EXPANSION_ATTR: str =
"no_glob_expansion"
361 group: click.Group, args: list[str],
362) -> click.Command |
None:
363 """Return the command *args* would dispatch to, parsing nothing else.
365 Walks the group tree with ``resilient_parsing`` contexts, so option
366 callbacks never run and a malformed command line resolves to ``None``
367 rather than raising - the real parse that follows reports the error
368 properly. Used before Click's Windows argument expansion to find out
369 whether the target command wants that expansion at all.
371 cmd: click.Command = group
372 remaining = list(args)
374 while isinstance(cmd, click.Group):
375 with cmd.make_context(
376 cmd.name
or "oct", remaining, resilient_parsing=
True,
381 *getattr(ctx,
"_protected_args", ()), *ctx.args,
385 name, remaining = remaining[0], remaining[1:]
386 nxt = cmd.get_command(ctx, name)
390 except click.ClickException:
396 """Apply Click's Windows shell-expansion unless the target opts out.
398 Click's ``Command.main`` runs ``_expand_args`` over the whole argv on
399 Windows - ``~``, ``$VAR`` and globs - because cmd and PowerShell do
400 none of that themselves, and ``oct lint oct/*.py`` relies on it. But
401 the expansion cannot tell a path from a pattern: ``oct git ignore
402 "**/docs/html/"`` had its one argument replaced by the fourteen
403 directories it happened to match, every one of which the pattern
404 validator then rejected (2026-09-27). A command that takes patterns
405 sets :data:`NO_GLOB_EXPANSION_ATTR` and receives its argv verbatim;
406 every other command keeps the expansion it always had.
408 from click.utils
import _expand_args
410 if leaf
is not None and getattr(leaf, NO_GLOB_EXPANSION_ATTR,
False):
412 return _expand_args(args)
416 """Start oc_diagnostics for this process: exception hook, no stream capture.
418 Output goes to the log file only, so oct's terminal output stays its
419 own. The file's FORMAT is left to the config's ``log_format`` (text, or
420 ``jsonl`` for machine reading; oc_diagnostics >= 2.2.0 keeps the two
421 independent). Idempotent; a no-op without oc_diagnostics.
424 from oc_diagnostics
import init, set_global_setting
425 init(intercept_streams=
False)
426 set_global_setting(
"output_mode",
"file")
432 """Custom group that shows per-command flags in the main --help output."""
434 def main(self, args: list[str] |
None =
None, *rest: object,
435 windows_expand_args: bool =
True, **kwargs: object) -> object:
436 """Click's ``main`` with the Windows expansion made command-aware.
438 Click only expands when it reads ``sys.argv`` itself, so the argv
439 is taken here, expanded (or not) per the target command, and
440 handed over with expansion switched off so it is not done twice.
444 if os.name ==
"nt" and windows_expand_args:
446 return super().
main(args, *rest, windows_expand_args=
False, **kwargs)
448 def invoke(self, ctx: click.Context) -> object:
449 """Time the registered command path without recording its arguments."""
458 root = ctx.params.get(
"root_dir")
461 root = find_project_root(Path.cwd())
465 Path(root
or Path.cwd()), command_stage(ctx), outcome=0,
470 ctx.meta[TIMING_SAMPLE_KEY] = sample
471 return super().
invoke(ctx)
476 formatter: click.HelpFormatter,
478 formatter.write(f
"oct {__version__}\n")
479 formatter.write_paragraph()
480 self.format_usage(ctx, formatter)
483 formatter.write_paragraph()
484 with formatter.indentation():
485 formatter.write(self.
help)
486 formatter.write_paragraph()
490 for param
in self.get_params(ctx):
491 rv = param.get_help_record(ctx)
495 with formatter.section(
"Global Options"):
496 formatter.write_dl(opts)
500 for name
in self.list_commands(ctx):
501 cmd = self.get_command(ctx, name)
502 if cmd
is None or cmd.hidden:
504 commands.append((name, cmd))
507 with formatter.section(
"Commands"):
508 for name, cmd
in commands:
509 short_help = cmd.get_short_help_str(limit=60)
510 formatter.write(f
" {name:<16} {short_help}\n")
511 flags = _COMMAND_FLAGS.get(name, [])
512 for flag, desc
in flags:
513 formatter.write(f
" {flag:<22} {desc}\n")
515 formatter.write(
"\n")
518@click.group(cls=OctGroup)
519@click.version_option(__version__,
"-V",
"--version", prog_name=
"oct",
520 message=
"%(prog)s %(version)s")
521@click.option("--root-dir", default=None, type=click.Path(exists=True, file_okay=False),
522 help=
"Override project root directory (skip auto-detection).")
523@click.option(
"--allow-outside-project", is_flag=
True,
524 help=
"Allow operations outside auto-detected project root (OI-405).")
525@click.option(
"--strict-config", is_flag=
True,
526 help=
"Fail on malformed .octrc.json or schema errors (OI-411).")
527@click.option(
"--log",
"log_output", is_flag=
True,
528 help=
"Persist command output as JSON to .option_c/logs/.")
532 root_dir: str |
None,
533 allow_outside_project: bool,
537 """OCT - Option C Tools"""
541 _dbg(
"OCT-CLI", func,
542 f
"cli start: root_dir={root_dir} strict_config={strict_config}", 3)
543 ctx.ensure_object(dict)
544 ctx.obj[
"root_dir"] = root_dir
545 ctx.obj[
"allow_outside_project"] = allow_outside_project
546 ctx.obj[
"strict_config"] = strict_config
547 ctx.obj[
"log_output"] = log_output
549 os.environ[
"OCT_STRICT_CONFIG"] =
"1"
552cli.add_command(complexity)
553cli.add_command(mutate)
556@cli.command(context_settings=dict(ignore_unknown_options=True))
557@click.argument("args", nargs=-1, type=click.UNPROCESSED)
559def lint(ctx: click.Context, args: tuple[str, ...]) ->
None:
560 """Run the Option C linter."""
561 project_root = get_project_root(ctx)
562 exit_code = run_linter(project_root, list(args))
564 raise SystemExit(exit_code)
567@cli.command(context_settings=dict(ignore_unknown_options=True))
568@click.argument("args", nargs=-1, type=click.UNPROCESSED)
570def format(ctx: click.Context, args: tuple[str, ...]) ->
None:
571 """Auto-fix Option C compliance violations in Python files."""
572 project_root = get_project_root(ctx)
573 run_formatter(project_root, list(args))
576@cli.command(name="agent", context_settings=dict(ignore_unknown_options=True))
577@click.argument("args", nargs=-1, type=click.UNPROCESSED)
578def agent(args: tuple[str, ...]) ->
None:
579 """Delegate to the OC Chat registry agent CLI.
581 A thin subprocess facade over ``python -m oc_chat.registry`` (Plan 151
582 T6). oct NEVER imports oc_chat - the CLI-boundary rule; all arguments,
583 stdin/stdout/stderr, the JSON envelope, and the exit-code contract
584 (0 ok, 1 source-validation, 2 build/refusal/scope, 3 not-byte-stable,
585 5 stale schema) are preserved verbatim by delegating to the child
586 process. Requires oc_chat to be importable on the interpreter running
587 the facade (the monorepo workspace install).
599 result = subprocess.run(
600 [sys.executable,
"-P",
"-m",
"oc_chat.registry", *args]
602 except FileNotFoundError
as exc:
603 raise click.ClickException(f
"could not launch the registry CLI: {exc}")
604 if result.returncode != 0:
605 raise SystemExit(result.returncode)
609@click.argument("path")
610@click.option("--force", is_flag=True, help="Overwrite existing file")
611@click.option("--test", is_flag=True, help="Also generate companion test file")
612@click.option("--dry-run", is_flag=True, help="Show what would be created without writing anything")
613@click.option("--verbose", is_flag=True, help="Show generated file content")
623 """Create a new Option C-compliant Python file."""
624 project_root = get_project_root(ctx)
625 create_new_file(project_root, path, force, dry_run=dry_run, test=test, verbose=verbose)
629@click.argument("name")
630@click.option(
"--dry-run", is_flag=
True,
631 help=
"Show what would be created without writing anything")
632@click.option(
"--no-venv", is_flag=
True, default=
False,
633 help=
"Skip .venv/ creation (FS-503: on by default).")
634@click.option(
"--with-claude", is_flag=
True, default=
False,
635 help=
"Copy bundled Claude Code skills into .claude/skills/.")
642 """Create a new Option C project."""
643 from oct.core.compat
import check_oc_diagnostics_compat
646 compat_msg = check_oc_diagnostics_compat()
648 click.echo(compat_msg, err=
True)
650 result = run_scaffold(
654 include_venv=
not no_venv,
659 _copy_claude_skills(Path.cwd() / name, result, dry_run=dry_run)
661 for msg
in result.messages:
664 click.echo(f
"Created new Option C project: {Path.cwd() / name}")
668 """Return the manifest name of the subproject at *project_root*, if any.
670 ``None`` when *project_root* is not inside a workspace, the manifest
671 does not load, or no subproject entry contains it.
673 from oct.core.workspace
import (
674 WorkspaceError, assign_to_subproject, find_workspace_root, load_workspace,
676 ws_root = find_workspace_root(project_root)
680 workspace = load_workspace(ws_root)
681 except WorkspaceError:
683 sp = assign_to_subproject(project_root, workspace)
684 return sp.name
if sp
is not None else None
688@click.option(
"--sphinx",
"sphinx_mode", is_flag=
True,
689 help=
"Generate Sphinx HTML documentation")
690@click.option(
"--doxygen",
"doxygen_mode", is_flag=
True,
691 help=
"Generate Doxygen HTML documentation")
692@click.option(
"--typedoc",
"typedoc_mode", is_flag=
True,
693 help=
"Generate TypeDoc HTML documentation (TypeScript projects "
694 "declaring a typedoc.json)")
695@click.option(
"--all",
"all_mode", is_flag=
True,
696 help=
"Generate every declared tier: Sphinx, Doxygen, TypeDoc")
697@click.option(
"--clean",
"clean_mode", is_flag=
True,
698 help=
"Remove generated documentation artifacts")
699@click.option(
"--dry-run",
"dry_run", is_flag=
True,
700 help=
"Preview what --clean would remove")
701@click.option(
"--no-workspace",
"no_workspace", is_flag=
True,
702 help=
"Force single-project mode even at a workspace root "
703 "(escape hatch for the FS-WS-DOCS workspace fan-out)")
704@click.option(
"--init-doxygen",
"init_doxygen", is_flag=
True,
705 help=
"Write docs/doxygen_work_dir/Doxyfile from the bundled "
706 "template, declaring this project for Doxygen builds. "
707 "Keeps an existing Doxyfile unless --force is given.")
708@click.option(
"--force",
"force", is_flag=
True,
709 help=
"With --init-doxygen: overwrite an existing Doxyfile.")
710@click.option(
"--callgraph",
"callgraph", metavar=
"QUALNAME", default=
None,
711 help=
"Draw the full call graph of one function (dotted name, or "
712 "a unique suffix) to docs/_build/callgraph/ as DOT and SVG, "
713 "and report what could and could not be resolved.")
714@click.option("--depth", "depth", type=click.IntRange(min=0), default=
None,
715 help=
"With --callgraph: stop this many calls below the root.")
716@click.option(
"--trace",
"trace_mode", is_flag=
True,
717 help=
"With --callgraph: run 'oct ARGS' (given after --) in this "
718 "process, FOR REAL, record what ran, and draw it against the "
719 "static graph. Run it in a throwaway project for commands "
720 "with side effects, such as git commit.")
721@click.argument("trace_args", nargs=-1, type=click.UNPROCESSED)
734 callgraph: str |
None,
737 trace_args: tuple[str, ...],
739 """Validate or build Option C documentation."""
740 project_root = get_project_root(ctx)
742 if callgraph
is None and (depth
is not None or trace_mode):
743 raise click.UsageError(
"--depth and --trace need --callgraph QUALNAME.")
744 if trace_args
and not trace_mode:
745 raise click.UsageError(
746 f
"Unexpected arguments {' '.join(trace_args)!r}; arguments after "
747 "-- are the oct command that --trace runs.")
748 if callgraph
is not None:
750 exit_code = run_docs_callgraph(
751 project_root, callgraph, depth=depth,
752 trace_args=list(trace_args)
if trace_mode
else None,
755 raise SystemExit(exit_code)
767 name = _load_project_info(project_root).get(
"name")
or project_root.name
769 path, written = write_doxyfile(project_root, name, force=force)
770 except DoxyfileTemplateMissing
as exc:
771 click.echo(f
"Error: {exc}", err=
True)
774 click.echo(f
"Wrote {path} (PROJECT_NAME = {name!r}).")
775 click.echo(
"This project now builds under 'oct docs --doxygen' "
776 "and the workspace fan-out, and gets a Doxygen card "
777 "on the docs hub, unless its workspace manifest entry "
778 "sets html_docs.doxygen to false.")
780 click.echo(f
"Kept existing {path}; pass --force to overwrite it.")
785 exit_code = run_docs_clean(project_root, dry_run=dry_run, no_workspace=no_workspace)
787 raise SystemExit(exit_code)
792 exit_code = run_docs_all(project_root, no_workspace=no_workspace)
794 raise SystemExit(exit_code)
799 exit_code = run_docs_sphinx(project_root, no_workspace=no_workspace)
801 raise SystemExit(exit_code)
806 exit_code = run_docs_doxygen(project_root, no_workspace=no_workspace)
808 raise SystemExit(exit_code)
813 exit_code = run_docs_typedoc(project_root, no_workspace=no_workspace)
815 raise SystemExit(exit_code)
818 _oc_diag = project_root /
"oc_diagnostics"
819 diag_dir = _oc_diag
if _oc_diag.exists()
else project_root /
"diagnostics"
823 if diag_dir.name ==
"diagnostics" and diag_dir.is_dir():
825 "Warning: 'diagnostics/' is deprecated and will be removed in v1.0.0. "
826 "Rename to 'oc_diagnostics/'.",
830 project_root /
"docs" /
"ARCHITECTURE.md",
831 project_root /
"tests" /
"Tests_README.md",
832 diag_dir /
"debug_config.json",
835 missing = [p
for p
in required
if not p.exists()]
838 click.echo(
"Missing required documentation files:")
840 click.echo(f
" - {m}")
842 click.echo(
"All required Option C documentation is present.")
846setattr(docs, NO_GLOB_EXPANSION_ATTR,
True)
848_PYTEST_INSTALL_HINT = (
849 "pytest is not installed. Install it with one of:\n"
850 " pip install oct[test] # installs OCT's test extras\n"
851 " pip install pytest # install pytest directly"
856 """Detect the 'No module named pytest' signature in combined output."""
857 return "No module named pytest" in combined_output
860def _run_pytest(project_root: Path, args: tuple, json_mode: bool =
False) ->
None:
861 """Run pytest in *project_root* and exit on failure.
863 When *json_mode* is True, captures pytest output, parses the summary
864 line, and emits a machine-readable JSON object to stdout:
865 ``{"tool": "test", "passed": N, "failed": N, "errors": N,
866 "warnings": N, "output": "...", "exit_code": N}``
868 OI-511: pytest is now declared in the ``[project.optional-dependencies].test``
869 extra rather than as a runtime dependency; if ``python -m pytest`` reports
870 it is not importable, emit the install hint above and exit 1.
874 import subprocess, sys
877 result = subprocess.run(
878 [sys.executable,
"-m",
"pytest",
"--tb=short",
"-q"] + list(args),
885 except FileNotFoundError:
886 click.echo(_json.dumps({
887 "tool":
"test",
"passed": 0,
"failed": 0,
"errors": 1,
888 "warnings": 0,
"output": _PYTEST_INSTALL_HINT,
892 combined = (result.stdout
or "") + (result.stderr
or "")
894 click.echo(_json.dumps({
895 "tool":
"test",
"passed": 0,
"failed": 0,
"errors": 1,
896 "warnings": 0,
"output": _PYTEST_INSTALL_HINT,
901 passed = failed = errors = warnings = 0
902 for m
in _re.finditer(
903 r"(\d+)\s+(passed|failed|error(?:s)?|warning(?:s)?)",
906 n, kind = int(m.group(1)), m.group(2)
909 elif kind ==
"failed":
911 elif kind.startswith(
"error"):
913 elif kind.startswith(
"warning"):
915 click.echo(_json.dumps({
920 "warnings": warnings,
922 "exit_code": result.returncode,
924 if result.returncode != 0:
925 raise SystemExit(result.returncode)
928 result = subprocess.run(
929 [sys.executable,
"-m",
"pytest"] + list(args),
936 except FileNotFoundError:
937 raise click.ClickException(_PYTEST_INSTALL_HINT)
938 combined = (result.stdout
or "") + (result.stderr
or "")
940 raise click.ClickException(_PYTEST_INSTALL_HINT)
943 click.echo(result.stdout, nl=
False)
945 click.echo(result.stderr, nl=
False, err=
True)
946 if result.returncode == 0:
947 click.echo(
"All tests passed.")
949 click.echo(
"Some tests failed.")
950 raise SystemExit(result.returncode)
954 """Heuristic: map a source .py file to its likely test file.
956 Tries several common patterns and returns the first existing match,
957 or ``None`` if no test file is found.
960 rel = source.relative_to(project_root)
964 parts = list(rel.parts)
968 if len(parts) >= 3
and parts[0] ==
"oct":
970 test_path = project_root /
"tests" / f
"tests_{pkg}" / f
"test_{stem}.py"
971 if test_path.is_file():
975 if len(parts) >= 2
and parts[0] ==
"src":
976 test_path = project_root /
"tests" / f
"test_{stem}.py"
977 if test_path.is_file():
981 test_path = project_root /
"tests" / f
"test_{stem}.py"
982 if test_path.is_file():
988def _cache_args(cached: bool, cache_clear: bool) -> tuple[str, ...]:
989 """Pytest arguments that drive the Plan 249 content-hashed cache.
991 ``-p oct.testing.cache`` loads the plugin for ANY project, so the
992 flags work whether or not the project's root conftest wires the
993 seams itself; the plugin registers its options at most once.
995 if not (cached
or cache_clear):
999 flags = [
"-p",
"oct.testing.cache"]
1001 flags.append(CACHE_OPTION)
1003 flags.append(CACHE_CLEAR_OPTION)
1008 """Put the cached run's file count in the timing ledger (P5).
1010 ``files`` is the number of files that actually RAN, so the saving
1011 a cache produced is visible in ``oct health --timing`` beside the
1012 uncached runs of the same stage.
1016 sample = ctx.meta.get(TIMING_SAMPLE_KEY)
1019 sample.scope =
"cached"
1020 last_run = read_last_run(project_root)
1021 ran = last_run.get(
"ran")
1022 if isinstance(ran, int)
and ran >= 0:
1027@click.option(
"--json",
"json_mode", is_flag=
True,
1028 help=
"Emit machine-readable JSON summary to stdout.")
1029@click.option(
"--changed", is_flag=
True,
1030 help=
"Run tests for git-modified files only.")
1031@click.option(
"--cached", is_flag=
True,
1032 help=
"Skip test files unchanged since their last green run.")
1033@click.option(
"--oct-cache-clear",
"cache_clear", is_flag=
True,
1034 help=
"Empty the Option C test cache before running.")
1035@click.argument("args", nargs=-1)
1043 args: tuple[str, ...],
1045 """Run the project's test suite."""
1046 project_root = get_project_root(ctx)
1053 _PYTEST_CFG_FILES = (
"pytest.ini",
"pyproject.toml",
"setup.cfg",
"tox.ini")
1058 from oct.core.git
import git_changed_files
1059 changed_py = git_changed_files(project_root, extensions={
".py"})
1060 test_files: list[Path] = []
1061 for src
in changed_py:
1063 if mapped
and mapped
not in test_files:
1064 test_files.append(mapped)
1066 rel_paths = [str(t.relative_to(project_root))
for t
in test_files]
1068 click.echo(f
"Running {len(test_files)} test file(s) for changed sources.")
1069 _run_pytest(project_root, tuple(rel_paths) + args, json_mode=json_mode)
1073 "No test files found for changed sources. Running all tests.",
1076 except Exception
as exc:
1078 f
"Warning: --changed failed ({exc}). Running all tests.",
1082 has_tests_dir = (project_root /
"tests").is_dir()
1083 has_pytest_cfg = any((project_root / f).is_file()
for f
in _PYTEST_CFG_FILES)
1085 if has_tests_dir
or has_pytest_cfg:
1087 _run_pytest(project_root, args, json_mode=json_mode)
1092 for child
in sorted(project_root.iterdir()):
1093 if not child.is_dir()
or child.name.startswith(
"."):
1095 if (child /
"tests").is_dir()
and any(
1096 (child / f).is_file()
for f
in _PYTEST_CFG_FILES
1098 sub_projects.append(child)
1100 if not sub_projects:
1101 raise click.ClickException(
1102 f
"No tests/ directory or pytest configuration found in {project_root}, "
1103 f
"and no testable sub-projects detected. "
1104 f
"Run 'oct test' from a project that has a test suite."
1107 import subprocess, sys
1109 click.echo(f
"No test suite at root -- found {len(sub_projects)} "
1110 f
"sub-project(s) with tests:")
1111 for sp
in sub_projects:
1112 click.echo(f
" - {sp.name}/")
1116 for sp
in sub_projects:
1117 click.echo(f
"{'=' * 60}")
1118 click.echo(f
"Running tests in {sp.name}/")
1119 click.echo(f
"{'=' * 60}")
1121 result = subprocess.run(
1122 [sys.executable,
"-m",
"pytest"] + list(args),
1125 if result.returncode != 0:
1126 failed.append(sp.name)
1127 except FileNotFoundError:
1128 raise click.ClickException(
1129 "pytest is not installed. Install it with: pip install pytest"
1134 click.echo(f
"Tests failed in: {', '.join(failed)}")
1137 click.echo(
"All sub-project tests passed.")
1139@cli.command(name="export-source")
1140@click.option("--verbose", is_flag=True, help="Show per-file statistics.")
1141@click.option("--clean", is_flag=True, help="[DEPRECATED: use 'oct clean'] Remove _source_code-* dirs.")
1142@click.option("--single-dir", is_flag=True, help="Write all output into one root-level directory.")
1143@click.option(
"--allow-outside-project", is_flag=
True,
1144 help=
"Allow PATH outside the auto-detected project root (OI-405).")
1145@click.option(
"--warn-secrets/--no-warn-secrets", default=
True,
1146 help=
"Scan file contents for potential secrets (OI-416).")
1147@click.option(
"--block-secrets", is_flag=
True,
1148 help=
"Skip files with potential secrets (implies --warn-secrets, OI-416).")
1149@click.option(
"--diff",
"diff_ref", default=
None,
1150 help=
"Export only files changed since REF (git ref).")
1151@click.argument("path", required=False)
1158 allow_outside_project: bool,
1160 block_secrets: bool,
1161 diff_ref: str |
None,
1164 """Export consolidated source-code files for inspection."""
1165 from oct.core.project_root
import validate_path_containment
1166 from oct.tools.source_exporter
import run_exporter
1169 global_allow = ctx.obj.get(
"allow_outside_project",
False)
if ctx.obj
else False
1170 allow_outside_project = allow_outside_project
or global_allow
1174 ctx.obj[
"allow_outside_project"] = allow_outside_project
1177 if allow_outside_project:
1178 project_root = Path(path).resolve()
1180 auto_root = get_project_root(ctx)
1181 project_root = validate_path_containment(
1182 Path(path), auto_root, allow_outside=
False
1185 project_root = get_project_root(ctx)
1189 args.append(
"--verbose")
1192 "Warning: 'export-source --clean' is deprecated. "
1193 "Use 'oct clean' (with optional --dry-run) instead.",
1196 args.append(
"--clean")
1198 args.append(
"--single-dir")
1200 args.append(
"--warn-secrets")
1202 args.append(
"--block-secrets")
1204 args.append(f
"--diff={diff_ref}")
1207 run_exporter(project_root, args)
1208 except Exception
as e:
1209 click.echo(f
"Error: export-source failed unexpectedly: {e}", err=
True)
1213@cli.command(name="export-skeleton")
1214@click.option(
"--json",
"json_mode", is_flag=
True,
1215 help=
"Emit machine-readable JSON summary to stdout.")
1216@click.option("--verbose", is_flag=True, help="Show per-file statistics.")
1217@click.option("--single-dir", is_flag=True, help="Write all output into one root-level directory.")
1218@click.option("--no-diagnostics", is_flag=True, help="Omit Diagnostics section from docstrings.")
1219@click.option("--clean", is_flag=True, help="Remove _skeleton_code-* dirs.")
1220@click.option(
"--allow-outside-project", is_flag=
True,
1221 help=
"Allow PATH outside the auto-detected project root (OI-405).")
1222@click.argument("path", required=False)
1229 no_diagnostics: bool,
1231 allow_outside_project: bool,
1234 """Export structural skeletons of source files for AI inspection."""
1235 from oct.core.project_root
import validate_path_containment
1236 from oct.tools.skeleton_exporter
import run_skeleton_exporter
1239 if allow_outside_project:
1240 project_root = Path(path).resolve()
1242 auto_root = get_project_root(ctx)
1243 project_root = validate_path_containment(
1244 Path(path), auto_root, allow_outside=
False
1247 project_root = get_project_root(ctx)
1251 args.append(
"--verbose")
1253 args.append(
"--single-dir")
1255 args.append(
"--no-diagnostics")
1257 args.append(
"--clean")
1260 run_skeleton_exporter(project_root, args, json_mode=json_mode)
1261 except Exception
as e:
1262 click.echo(f
"Error: export-skeleton failed unexpectedly: {e}", err=
True)
1267@click.argument("path", required=False)
1268@click.option("--dry-run", is_flag=True, help="Show what would be deleted without deleting anything")
1269@click.option(
"--allow-outside-project", is_flag=
True,
1270 help=
"Allow PATH outside the auto-detected project root (OI-405).")
1276 allow_outside_project: bool,
1278 """Remove _source_code-*, _skeleton_code-* export dirs, __pycache__ dirs
1279 and packaging build/ and dist/ trees.
1281 A build/ or dist/ directory is only removed when it sits beside a
1282 pyproject.toml / setup.py / setup.cfg AND contains nothing but
1283 packaging output. One that fails either test is kept and the reason
1284 printed, because both are ordinary words and a project is entitled to
1285 a source directory called either.
1287 from oct.core.project_root
import validate_path_containment
1288 from oct.tools.build_artifacts
import clean_build_dirs
1289 from oct.tools.source_exporter
import clean_source_dirs, clean_pycache_dirs
1290 from oct.tools.skeleton_exporter
import clean_skeleton_dirs
1293 if allow_outside_project:
1294 target = Path(path).resolve()
1297 auto_root = get_project_root(ctx)
1298 except click.ClickException:
1299 auto_root = Path.cwd()
1300 target = validate_path_containment(
1301 Path(path), auto_root, allow_outside=
False
1306 for cleaner
in (clean_source_dirs, clean_skeleton_dirs, clean_pycache_dirs,
1308 _removed, failed = cleaner(target, dry_run=dry_run)
1309 failed_total += len(failed)
1312 f
"oct clean: {failed_total} directories could not be removed; "
1313 "see the list above.",
1320@click.option("--json", "json_mode", is_flag=True, help="Machine-readable JSON output")
1321@click.option("--verbose", is_flag=True, help="Detailed per-file breakdown")
1322@click.option("--tests", "run_tests", is_flag=True, help="Run the project test suite")
1326 type=click.IntRange(min=1),
1328 help=
"Test execution timeout in seconds (default: 300, or health.test_timeout in .octrc.json).",
1330@click.option("--timing", is_flag=True, help="Show only timing statistics")
1331@click.option("--timing-last", type=click.IntRange(min=1), default=20, help=
"Recent records per stage")
1332@click.option("--write-baseline", is_flag=True, help="Write this host's timing baseline")
1333@click.option("--force-host", is_flag=True, help="Allow adding a different baseline host")
1334@click.option("--session", type=click.Path(exists=True, dir_okay=False, path_type=Path), help=
"Session events JSONL")
1341 test_timeout: int |
None,
1344 write_baseline: bool,
1346 session: Path |
None,
1348 """Display project compliance dashboard."""
1349 project_root = get_project_root(ctx)
1352 json_mode=json_mode,
1354 run_tests=run_tests,
1355 test_timeout=test_timeout,
1356 log_output=ctx.obj.get(
"log_output",
False),
1357 **({
"timing": timing,
"timing_last": timing_last,
1358 "write_baseline": write_baseline,
"force_host": force_host,
1359 "session": session}
if timing
or write_baseline
or session
or force_host
else {}),
1363@cli.command(context_settings=dict(ignore_unknown_options=True))
1364@click.argument("args", nargs=-1, type=click.UNPROCESSED)
1366def deps(ctx: click.Context, args: tuple[str, ...]) ->
None:
1367 """Analyse inter-module dependencies."""
1368 project_root = get_project_root(ctx)
1369 exit_code = run_deps(project_root, list(args))
1371 raise SystemExit(exit_code)
1374@cli.command(name="install-hooks")
1375@click.option("--force", is_flag=True, help="Overwrite existing .pre-commit-config.yaml")
1378 """[DEPRECATED] Use 'oct git hooks install' instead."""
1380 "Notice: 'oct install-hooks' is deprecated. "
1381 "Use 'oct git hooks install' instead.",
1384 project_root = get_project_root(ctx)
1387 config_path = project_root /
".pre-commit-config.yaml"
1388 if config_path.exists()
and not force:
1389 click.echo(f
"Error: {config_path} already exists. Use --force to overwrite.")
1391 config_path.write_text(_ENHANCED_HOOKS_TEMPLATE, encoding=
"utf-8")
1392 click.echo(f
"Created {config_path}")
1393 click.echo(
"Run 'pre-commit install' to activate the hooks.")
1396@cli.command(context_settings=dict(ignore_unknown_options=True))
1397@click.option(
"--json",
"json_mode", is_flag=
True,
1398 help=
"Emit machine-readable JSON summary to stdout.")
1399@click.argument("args", nargs=-1, type=click.UNPROCESSED)
1404 args: tuple[str, ...],
1406 """Run type checking (pyright or mypy)."""
1407 project_root = get_project_root(ctx)
1408 exit_code = run_typecheck(project_root, list(args), json_mode=json_mode)
1410 raise SystemExit(exit_code)
1414from oct.git import git_group
as _git_group
1415cli.add_command(_git_group, name=
"git")
1418from oct.migrate import migrate_group
as _migrate_group
1419cli.add_command(_migrate_group, name=
"migrate")
1422from oct.audit import audit_group
as _audit_cmd
1423cli.add_command(_audit_cmd, name=
"audit")
1427from oct.core.diagnostics
import _dbg
1428cli.add_command(_oct_install_cmd, name=
"install")
1433def diag(ctx: click.Context) ->
None:
1434 """oc_diagnostics configuration tools."""
1438@diag.command(name="validate-config")
1443 help=
"OI-534: escalate WARNING to failure; exit 2 on any ERROR or WARNING.",
1447 """Validate debug_config.json against the expected schema."""
1448 project_root = get_project_root(ctx)
1449 issues = validate_config(project_root)
1451 click.echo(
"debug_config.json is valid.")
1453 for issue
in issues:
1455 has_error = any(i.startswith(
"ERROR")
for i
in issues)
1456 has_warning = any(i.startswith(
"WARNING")
for i
in issues)
1460 if strict
and (has_error
or has_warning):
1466@diag.command(name="list-domains")
1469 """List configured domains with their levels and colors."""
1470 project_root = get_project_root(ctx)
1472 domains = list_domains(project_root)
1473 except FileNotFoundError
as e:
1474 raise click.ClickException(str(e))
1477 click.echo(
"No domains configured.")
1480 click.echo(f
"{'Domain':<24} {'Level':<8} {'Color'}")
1481 click.echo(
"-" * 44)
1483 click.echo(f
"{d['name']:<24} {d['level']:<8} {d['color']}")
1486@diag.command(name="set-level")
1487@click.argument("domain")
1488@click.argument("level", type=click.IntRange(0, 9))
1493 help=
"Preview the edit without writing the config or its .bk backup.",
1502 """Set a domain's debug level (0-9).
1504 OI-528: the write is always preceded by a ``<path>.bk`` backup so
1505 ``oct diag restore`` can undo a bad edit. ``--dry-run`` prints the
1506 would-be post-edit config to stdout and skips both writes.
1508 project_root = get_project_root(ctx)
1510 new_config = set_level(project_root, domain, level, dry_run=dry_run)
1511 except (FileNotFoundError, ValueError, KeyError)
as e:
1512 raise click.ClickException(str(e))
1514 click.echo(json.dumps(new_config, indent=2))
1516 f
"[DRY RUN] Would set {domain} level to {level} (no write).",
1520 click.echo(f
"Set {domain} level to {level}.")
1523@diag.command(name="restore")
1526 """OI-528: restore debug_config.json from its .bk backup."""
1527 project_root = get_project_root(ctx)
1529 restored = restore_config(project_root)
1530 except FileNotFoundError
as e:
1531 raise click.ClickException(str(e))
1532 click.echo(f
"Restored {restored} from backup.")
1535@diag.command(name="migrate-config")
1539 type=click.Path(dir_okay=
False, path_type=Path),
1545 help=
"Show the migrated config on stdout without writing to disk.",
1550 config_path: Path |
None,
1553 """OI-508 / FS-515: upgrade a legacy v1 debug_config.json to v2.0.
1555 When PATH is omitted, migrates the project's own debug_config.json.
1556 Writes ``<path>.bk`` before overwriting; idempotent - running twice
1557 over an already-v2 file is a no-op.
1560 if config_path
is None:
1561 project_root = get_project_root(ctx)
1562 config_path = _find_config(project_root)
1564 new_config = migrate_config(config_path, dry_run=dry_run)
1565 except (FileNotFoundError, ValueError)
as e:
1566 raise click.ClickException(str(e))
1568 click.echo(json.dumps(new_config, indent=2))
1569 click.echo(f
"[DRY RUN] Would write v2 schema to {config_path}", err=
True)
1571 backup_path = config_path.with_suffix(config_path.suffix +
".bk")
1572 click.echo(f
"Migrated {config_path} -> v{new_config.get('_schema_version')} "
1573 f
"(backup: {backup_path.name})")
1576@cli.command(name="install-mcp")
1579 type=click.Choice([
"claude-code",
"cursor",
"vscode",
"auto"], case_sensitive=
False),
1581 help=
"Target AI host (default: auto-detect).",
1584 "--dry-run/--no-dry-run",
1586 help=
"Preview without writing (default: --dry-run).",
1590 """Install oct-mcp into an AI host's MCP config (Claude Code, Cursor, VS Code)."""
1591 from oct.tools.install_mcp
import run_install_mcp
1592 project_root = get_project_root(ctx)
1593 exit_code = run_install_mcp(host=host, project_root=project_root, dry_run=dry_run)
1595 raise SystemExit(exit_code)
1598if __name__ ==
"__main__":
None format_help(self, click.Context ctx, click.HelpFormatter formatter)
object invoke(self, click.Context ctx)
object main(self, list[str]|None args=None, *object rest, bool windows_expand_args=True, **object kwargs)
None export_skeleton(click.Context ctx, bool json_mode, bool verbose, bool single_dir, bool no_diagnostics, bool clean, bool allow_outside_project, str|None path)
bool _pytest_missing(str combined_output)
None _record_cached_run(click.Context ctx, Path project_root)
None format(click.Context ctx, tuple[str,...] args)
None diag_validate_config(click.Context ctx, bool strict)
None _run_pytest(Path project_root, tuple args, bool json_mode=False)
None agent(tuple[str,...] args)
list[str] _expand_windows_args(click.Group group, list[str] args)
None diag_list_domains(click.Context ctx)
None diag_migrate_config(click.Context ctx, Path|None config_path, bool dry_run)
None lint(click.Context ctx, tuple[str,...] args)
tuple[str,...] _cache_args(bool cached, bool cache_clear)
None _start_diagnostics()
None clean(click.Context ctx, str|None path, bool dry_run, bool allow_outside_project)
None export_source(click.Context ctx, bool verbose, bool clean, bool single_dir, bool allow_outside_project, bool warn_secrets, bool block_secrets, str|None diff_ref, str|None path)
str|None _workspace_subproject_name(Path project_root)
None test(click.Context ctx, bool json_mode, bool changed, bool cached, bool cache_clear, tuple[str,...] args)
Path|None _map_source_to_test(Path source, Path project_root)
None diag_restore(click.Context ctx)
None new(click.Context ctx, str path, bool force, bool test, bool dry_run, bool verbose)
None install_hooks_cmd(click.Context ctx, bool force)
None diag_set_level(click.Context ctx, str domain, int level, bool dry_run)
click.Command|None _resolve_leaf_command(click.Group group, list[str] args)