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)
52from pathlib
import Path
54from oct
import __version__
55from oct.core.project_root
import find_project_root, get_project_root
59from oct.generator.new_python_file
import create_new_file
74 (
"--fix-headers",
"Automatically fix shebang, encoding, and path headers"),
75 (
"--dry-run",
"Preview changes without modifying files"),
76 (
"--json",
"Emit machine-readable JSON to stdout"),
77 (
"--jobs N, -j N",
"Parallel workers (default: 1)"),
78 (
"--changed",
"Lint only git-modified Python files"),
79 (
"--profile P",
"Lint profile: proto, compact, strict"),
80 (
"--secret-match MODE",
"Secret-name matcher: substring (default) | word"),
81 (
"--code-entropy-threshold N",
"Shannon entropy threshold for code strings (default: 4.5)"),
82 (
"--no-entropy",
"Disable entropy-based secret detection"),
83 (
"--no-cache",
"Bypass lint cache reads (still writes)"),
84 (
"--baseline",
"Save current violations as the baseline"),
85 (
"--against-baseline",
"Report only new/resolved vs saved baseline"),
86 (
"--explain RULE",
"Show rule documentation (rationale, pattern, spec ref)"),
87 (
"PATH ...",
"Files or directories to lint (default: entire project)"),
90 (
"--fix",
"Apply formatting changes (default: dry-run)"),
91 (
"--dry-run",
"Report what would change without modifying"),
92 (
"--json",
"Output machine-readable JSON"),
93 (
"--verbose",
"Show per-file details"),
94 (
"--changed",
"Format only git-modified Python files"),
97 (
"PATH",
"Target file path (required)"),
98 (
"--force",
"Overwrite existing file"),
99 (
"--test",
"Also generate companion test file in tests/"),
100 (
"--dry-run",
"Show what would be created without writing"),
101 (
"--verbose",
"Show generated file content"),
104 (
"NAME",
"Project name (required)"),
105 (
"--dry-run",
"Show what would be created without writing"),
106 (
"--no-venv",
"Skip .venv/ creation (FS-503: on by default)"),
107 (
"--with-claude",
"Copy bundled Claude Code skills into .claude/skills/"),
110 (
"--sphinx",
"Generate Sphinx HTML documentation"),
111 (
"--doxygen",
"Generate Doxygen HTML documentation"),
112 (
"--all",
"Generate both Sphinx and Doxygen documentation"),
113 (
"--clean",
"Remove generated documentation artifacts"),
114 (
"--dry-run",
"Preview what --clean would remove"),
117 (
"--json",
"Emit machine-readable JSON summary to stdout"),
118 (
"--changed",
"Run tests for git-modified files only"),
121 (
"--verbose",
"Show per-file statistics"),
122 (
"--clean",
"[DEPRECATED: use 'oct clean'] Remove _source_code-* dirs"),
123 (
"--single-dir",
"Write all output into one root-level directory"),
124 (
"--diff REF",
"Export only files changed since REF"),
125 (
"--warn-secrets",
"Warn on suspected secrets in exported files"),
126 (
"--block-secrets",
"Abort export when secrets are detected"),
127 (
"--allow-outside-project",
"Allow export targets outside the project root"),
130 (
"--json",
"Emit machine-readable JSON summary to stdout"),
131 (
"--verbose",
"Show per-file statistics"),
132 (
"--single-dir",
"Write all output into one root-level directory"),
133 (
"--no-diagnostics",
"Omit Diagnostics section from docstrings"),
134 (
"--clean",
"Remove previously exported skeletons"),
135 (
"--allow-outside-project",
"Allow skeleton targets outside the project root"),
138 (
"--dry-run",
"Show what would be deleted without deleting"),
139 (
"--allow-outside-project",
"Allow cleaning paths outside the project root"),
142 (
"--json",
"Machine-readable JSON output"),
143 (
"--verbose",
"Detailed per-file breakdown"),
144 (
"--timeout N",
"Per-check timeout in seconds (default: 300)"),
147 (
"--json",
"JSON adjacency list output"),
148 (
"--mermaid",
"Mermaid diagram output"),
149 (
"--dot",
"DOT/Graphviz output"),
152 (
"TARGET",
"Project root to install (default: current directory)"),
153 (
"--editable, -e",
"Install in editable mode (required)"),
154 (
"--reinstall",
"Allow re-install over an existing shadow"),
155 (
"--use-active-venv",
"Bypass venv-match check"),
156 (
"--allow-worktree",
"Allow installing from a Claude Code worktree"),
157 (
"--allow-outside-project",
"Allow running outside an Option C project"),
158 (
"--json",
"Emit JSON output"),
161 (
"--force",
"Overwrite existing .pre-commit-config.yaml"),
164 (
"status",
"Show git status with Option C compliance annotations"),
165 (
"check",
"Run the unified quality gate (lint + format + secrets)"),
166 (
"init",
"Initialise or enhance a git repo with Option C conventions"),
167 (
"commit",
"Commit with quality gate and secrets enforcement"),
168 (
"branch",
"Create or switch branches with naming validation"),
169 (
"add",
"Stage files for commit (whole-file or interactive)"),
170 (
"restore",
"Restore working-tree edits or unstage paths"),
171 (
"ignore",
"Manage .gitignore patterns within the OCT-managed block"),
172 (
"commit-msg",
"Validate a commit message file (pre-commit stage)"),
173 (
"hooks",
"Manage Option C git hooks"),
174 (
"changelog",
"Generate changelog from Conventional Commits history"),
177 (
"--json",
"Emit machine-readable JSON output"),
178 (
"--no-check",
"Skip compliance annotations (plain git status)"),
179 (
"--scope SCOPE",
"Scope: project (default), repo, auto"),
182 (
"PATHS...",
"Files or directories to stage"),
183 (
"--patch, -p",
"Interactive hunk-level staging"),
184 (
"--update, -u",
"Stage all already-tracked modified files"),
185 (
"--all, -A",
"Stage all changes within project scope"),
186 (
"--dry-run",
"Show what would be staged without staging"),
187 (
"--json",
"Emit JSON output"),
190 (
"PATTERNS...",
"Glob patterns to add to .gitignore"),
191 (
"--remove PATTERN",
"Remove a pattern from the OCT-managed block"),
192 (
"--list",
"List currently active ignore patterns"),
193 (
"--scope SCOPE",
"Target .gitignore: workspace, project, nearest (default)"),
194 (
"--json",
"Emit JSON output"),
197 (
"PATHS...",
"Files to restore (default: scope by --all)"),
198 (
"--staged",
"Unstage paths from the index without touching working tree"),
199 (
"--all, -A",
"Restore all in-scope paths"),
200 (
"--yes",
"Skip confirmation prompt for --staged --all"),
201 (
"--json",
"Emit JSON output"),
204 (
"--dry-run",
"Show what would be created without writing"),
205 (
"--github",
"Also generate GitHub Actions workflow"),
206 (
"--no-hooks",
"Skip hook installation"),
209 (
"--create NAME",
"Create a new branch from HEAD"),
210 (
"--switch NAME",
"Switch to an existing branch"),
211 (
"--json",
"Emit JSON output"),
214 (
"-m MESSAGE, --message MESSAGE",
"Commit message (Conventional Commits format)"),
215 (
"--force",
"Skip lint/format checks (secrets still checked)"),
216 (
"--no-verify",
"Forward --no-verify to git (hooks skipped)"),
217 (
"--profile P",
"Override lint profile: proto, compact, strict"),
218 (
"--json",
"Emit JSON output"),
219 (
"--ignore-unstaged-mutation",
"Downgrade B1.0 mutation detection to warn for this run"),
222 (
"MESSAGE_FILE",
"Path to the commit message file (required)"),
223 (
"--json",
"Emit machine-readable JSON output"),
226 (
"--staged-only",
"Check only staged (index) files"),
227 (
"--all",
"Check every Python file in the project"),
228 (
"--include-tests",
"Also run `oct test` (for pre-push use)"),
229 (
"--fix",
"Attempt auto-fix of violations, then re-check"),
230 (
"--profile P",
"Override lint profile: proto, compact, strict"),
231 (
"--json",
"Emit machine-readable JSON output"),
232 (
"--sandbox",
"Run pytest under the MCP sandbox environment"),
235 (
"install",
"Generate .pre-commit-config.yaml with all hooks"),
236 (
"status",
"Show installed hook state"),
237 (
"remove",
"Remove Option C hooks (preserves non-OCT hooks)"),
240 (
"--release V",
"Release version tag (e.g. v0.13.0)"),
241 (
"--since REF",
"Git ref to start from (default: last tag)"),
242 (
"--dry-run",
"Print to stdout without writing"),
243 (
"--json",
"Emit structured JSON"),
245 "git hooks install": [
246 (
"--force",
"Overwrite existing configuration"),
249 (
"--json",
"Emit machine-readable JSON summary to stdout"),
250 (
"ARGS ...",
"Arguments forwarded to pyright/mypy"),
253 (
"validate-config",
"Validate debug_config.json against schema"),
254 (
" --strict",
"Treat validation warnings as errors"),
255 (
"list-domains",
"List configured domains with levels and colors"),
256 (
"set-level DOMAIN N",
"Set a domain's debug level (0-4)"),
257 (
" --dry-run",
"Preview edit without writing (use with set-level)"),
258 (
"restore",
"Restore debug_config.json from .bk backup"),
259 (
"migrate-config [PATH]",
"Upgrade legacy v1 schema to v2.0"),
260 (
" --dry-run",
"Preview migration without writing (use with migrate-config)"),
263 (
"--host HOST",
"Target AI host: claude-code, cursor, vscode, auto (default: auto)"),
264 (
"--dry-run / --no-dry-run",
"Preview without writing (default: --dry-run)"),
267 (
"option-c",
"Migrate to canonical .option_c/ dotdir layout (FS-539)"),
269 "migrate option-c": [
270 (
"--dry-run",
"Show plan without writing anything"),
271 (
"--no-backup",
"Skip .bk backup when overwriting"),
274 (
"--json-output",
"Emit JSON instead of human-readable text"),
280 """Custom group that shows per-command flags in the main --help output."""
283 formatter.write(f
"oct {__version__}\n")
284 formatter.write_paragraph()
285 self.format_usage(ctx, formatter)
288 formatter.write_paragraph()
289 with formatter.indentation():
290 formatter.write(self.
help)
291 formatter.write_paragraph()
295 for param
in self.get_params(ctx):
296 rv = param.get_help_record(ctx)
300 with formatter.section(
"Global Options"):
301 formatter.write_dl(opts)
305 for name
in self.list_commands(ctx):
306 cmd = self.get_command(ctx, name)
307 if cmd
is None or cmd.hidden:
309 commands.append((name, cmd))
312 with formatter.section(
"Commands"):
313 for name, cmd
in commands:
314 short_help = cmd.get_short_help_str(limit=60)
315 formatter.write(f
" {name:<16} {short_help}\n")
316 flags = _COMMAND_FLAGS.get(name, [])
317 for flag, desc
in flags:
318 formatter.write(f
" {flag:<22} {desc}\n")
320 formatter.write(
"\n")
323@click.group(cls=OctGroup)
324@click.version_option(__version__,
"-V",
"--version", prog_name=
"oct",
325 message=
"%(prog)s %(version)s")
326@click.option("--root-dir", default=None, type=click.Path(exists=True, file_okay=False),
327 help=
"Override project root directory (skip auto-detection).")
328@click.option(
"--allow-outside-project", is_flag=
True,
329 help=
"Allow operations outside auto-detected project root (OI-405).")
330@click.option(
"--strict-config", is_flag=
True,
331 help=
"Fail on malformed .octrc.json or schema errors (OI-411).")
332@click.option(
"--log",
"log_output", is_flag=
True,
333 help=
"Persist command output as JSON to .option_c/logs/.")
335def cli(ctx, root_dir, allow_outside_project, strict_config, log_output):
336 """OCT — Option C Tools"""
338 from oc_diagnostics
import init, set_global_setting
339 init(intercept_streams=
False)
340 set_global_setting(
"output_mode",
"file")
343 ctx.ensure_object(dict)
344 ctx.obj[
"root_dir"] = root_dir
345 ctx.obj[
"allow_outside_project"] = allow_outside_project
346 ctx.obj[
"strict_config"] = strict_config
347 ctx.obj[
"log_output"] = log_output
349 os.environ[
"OCT_STRICT_CONFIG"] =
"1"
352@cli.command(context_settings=dict(ignore_unknown_options=True))
353@click.argument("args", nargs=-1, type=click.UNPROCESSED)
356 """Run the Option C linter."""
357 project_root = get_project_root(ctx)
358 exit_code = run_linter(project_root, list(args))
360 raise SystemExit(exit_code)
363@cli.command(context_settings=dict(ignore_unknown_options=True))
364@click.argument("args", nargs=-1, type=click.UNPROCESSED)
367 """Auto-fix Option C compliance violations in Python files."""
368 project_root = get_project_root(ctx)
369 run_formatter(project_root, list(args))
373@click.argument("path")
374@click.option("--force", is_flag=True, help="Overwrite existing file")
375@click.option("--test", is_flag=True, help="Also generate companion test file")
376@click.option("--dry-run", is_flag=True, help="Show what would be created without writing anything")
377@click.option("--verbose", is_flag=True, help="Show generated file content")
379def new(ctx, path, force, test, dry_run, verbose):
380 """Create a new Option C-compliant Python file."""
381 project_root = get_project_root(ctx)
382 create_new_file(project_root, path, force, dry_run=dry_run, test=test, verbose=verbose)
386@click.argument("name")
387@click.option(
"--dry-run", is_flag=
True,
388 help=
"Show what would be created without writing anything")
389@click.option(
"--no-venv", is_flag=
True, default=
False,
390 help=
"Skip .venv/ creation (FS-503: on by default).")
391@click.option(
"--with-claude", is_flag=
True, default=
False,
392 help=
"Copy bundled Claude Code skills into .claude/skills/.")
393def scaffold(name, dry_run, no_venv, with_claude):
394 """Create a new Option C project."""
395 from oct.core.compat
import check_oc_diagnostics_compat
398 compat_msg = check_oc_diagnostics_compat()
400 click.echo(compat_msg, err=
True)
402 result = run_scaffold(
406 include_venv=
not no_venv,
411 _copy_claude_skills(Path.cwd() / name, result, dry_run=dry_run)
413 for msg
in result.messages:
416 click.echo(f
"Created new Option C project: {Path.cwd() / name}")
420@click.option(
"--sphinx",
"sphinx_mode", is_flag=
True,
421 help=
"Generate Sphinx HTML documentation")
422@click.option(
"--doxygen",
"doxygen_mode", is_flag=
True,
423 help=
"Generate Doxygen HTML documentation")
424@click.option(
"--all",
"all_mode", is_flag=
True,
425 help=
"Generate both Sphinx and Doxygen documentation")
426@click.option(
"--clean",
"clean_mode", is_flag=
True,
427 help=
"Remove generated documentation artifacts")
428@click.option(
"--dry-run",
"dry_run", is_flag=
True,
429 help=
"Preview what --clean would remove")
431def docs(ctx, sphinx_mode, doxygen_mode, all_mode, clean_mode, dry_run):
432 """Validate or build Option C documentation."""
433 project_root = get_project_root(ctx)
437 exit_code = run_docs_clean(project_root, dry_run=dry_run)
439 raise SystemExit(exit_code)
444 exit_code = run_docs_all(project_root)
446 raise SystemExit(exit_code)
451 exit_code = run_docs_sphinx(project_root)
453 raise SystemExit(exit_code)
458 exit_code = run_docs_doxygen(project_root)
460 raise SystemExit(exit_code)
463 _oc_diag = project_root /
"oc_diagnostics"
464 diag_dir = _oc_diag
if _oc_diag.exists()
else project_root /
"diagnostics"
465 if diag_dir.name ==
"diagnostics":
467 "Warning: 'diagnostics/' is deprecated and will be removed in v1.0.0. "
468 "Rename to 'oc_diagnostics/'.",
472 project_root /
"docs" /
"ARCHITECTURE.md",
473 project_root /
"tests" /
"Tests_README.md",
474 diag_dir /
"debug_config.json",
477 missing = [p
for p
in required
if not p.exists()]
480 click.echo(
"Missing required documentation files:")
482 click.echo(f
" - {m}")
484 click.echo(
"All required Option C documentation is present.")
486_PYTEST_INSTALL_HINT = (
487 "pytest is not installed. Install it with one of:\n"
488 " pip install oct[test] # installs OCT's test extras\n"
489 " pip install pytest # install pytest directly"
494 """Detect the 'No module named pytest' signature in combined output."""
495 return "No module named pytest" in combined_output
498def _run_pytest(project_root: Path, args: tuple, json_mode: bool =
False) ->
None:
499 """Run pytest in *project_root* and exit on failure.
501 When *json_mode* is True, captures pytest output, parses the summary
502 line, and emits a machine-readable JSON object to stdout:
503 ``{"tool": "test", "passed": N, "failed": N, "errors": N,
504 "warnings": N, "output": "...", "exit_code": N}``
506 OI-511: pytest is now declared in the ``[project.optional-dependencies].test``
507 extra rather than as a runtime dependency; if ``python -m pytest`` reports
508 it is not importable, emit the install hint above and exit 1.
512 import subprocess, sys
515 result = subprocess.run(
516 [sys.executable,
"-m",
"pytest",
"--tb=short",
"-q"] + list(args),
523 except FileNotFoundError:
524 click.echo(_json.dumps({
525 "tool":
"test",
"passed": 0,
"failed": 0,
"errors": 1,
526 "warnings": 0,
"output": _PYTEST_INSTALL_HINT,
530 combined = (result.stdout
or "") + (result.stderr
or "")
532 click.echo(_json.dumps({
533 "tool":
"test",
"passed": 0,
"failed": 0,
"errors": 1,
534 "warnings": 0,
"output": _PYTEST_INSTALL_HINT,
539 passed = failed = errors = warnings = 0
540 for m
in _re.finditer(
541 r"(\d+)\s+(passed|failed|error(?:s)?|warning(?:s)?)",
544 n, kind = int(m.group(1)), m.group(2)
547 elif kind ==
"failed":
549 elif kind.startswith(
"error"):
551 elif kind.startswith(
"warning"):
553 click.echo(_json.dumps({
558 "warnings": warnings,
560 "exit_code": result.returncode,
562 if result.returncode != 0:
563 raise SystemExit(result.returncode)
566 result = subprocess.run(
567 [sys.executable,
"-m",
"pytest"] + list(args),
574 except FileNotFoundError:
575 raise click.ClickException(_PYTEST_INSTALL_HINT)
576 combined = (result.stdout
or "") + (result.stderr
or "")
578 raise click.ClickException(_PYTEST_INSTALL_HINT)
581 click.echo(result.stdout, nl=
False)
583 click.echo(result.stderr, nl=
False, err=
True)
584 if result.returncode == 0:
585 click.echo(
"All tests passed.")
587 click.echo(
"Some tests failed.")
588 raise SystemExit(result.returncode)
592 """Heuristic: map a source .py file to its likely test file.
594 Tries several common patterns and returns the first existing match,
595 or ``None`` if no test file is found.
598 rel = source.relative_to(project_root)
602 parts = list(rel.parts)
606 if len(parts) >= 3
and parts[0] ==
"oct":
608 test_path = project_root /
"tests" / f
"tests_{pkg}" / f
"test_{stem}.py"
609 if test_path.is_file():
613 if len(parts) >= 2
and parts[0] ==
"src":
614 test_path = project_root /
"tests" / f
"test_{stem}.py"
615 if test_path.is_file():
619 test_path = project_root /
"tests" / f
"test_{stem}.py"
620 if test_path.is_file():
627@click.option(
"--json",
"json_mode", is_flag=
True,
628 help=
"Emit machine-readable JSON summary to stdout.")
629@click.option(
"--changed", is_flag=
True,
630 help=
"Run tests for git-modified files only.")
631@click.argument("args", nargs=-1)
633def test(ctx, json_mode, changed, args):
634 """Run the project's test suite."""
635 project_root = get_project_root(ctx)
637 _PYTEST_CFG_FILES = (
"pytest.ini",
"pyproject.toml",
"setup.cfg",
"tox.ini")
642 from oct.core.git
import git_changed_files
643 changed_py = git_changed_files(project_root, extensions={
".py"})
644 test_files: list[Path] = []
645 for src
in changed_py:
647 if mapped
and mapped
not in test_files:
648 test_files.append(mapped)
650 rel_paths = [str(t.relative_to(project_root))
for t
in test_files]
652 click.echo(f
"Running {len(test_files)} test file(s) for changed sources.")
653 _run_pytest(project_root, tuple(rel_paths) + args, json_mode=json_mode)
657 "No test files found for changed sources. Running all tests.",
660 except Exception
as exc:
662 f
"Warning: --changed failed ({exc}). Running all tests.",
666 has_tests_dir = (project_root /
"tests").is_dir()
667 has_pytest_cfg = any((project_root / f).is_file()
for f
in _PYTEST_CFG_FILES)
669 if has_tests_dir
or has_pytest_cfg:
671 _run_pytest(project_root, args, json_mode=json_mode)
676 for child
in sorted(project_root.iterdir()):
677 if not child.is_dir()
or child.name.startswith(
"."):
679 if (child /
"tests").is_dir()
and any(
680 (child / f).is_file()
for f
in _PYTEST_CFG_FILES
682 sub_projects.append(child)
685 raise click.ClickException(
686 f
"No tests/ directory or pytest configuration found in {project_root}, "
687 f
"and no testable sub-projects detected. "
688 f
"Run 'oct test' from a project that has a test suite."
691 import subprocess, sys
693 click.echo(f
"No test suite at root -- found {len(sub_projects)} "
694 f
"sub-project(s) with tests:")
695 for sp
in sub_projects:
696 click.echo(f
" - {sp.name}/")
700 for sp
in sub_projects:
701 click.echo(f
"{'=' * 60}")
702 click.echo(f
"Running tests in {sp.name}/")
703 click.echo(f
"{'=' * 60}")
705 result = subprocess.run(
706 [sys.executable,
"-m",
"pytest"] + list(args),
709 if result.returncode != 0:
710 failed.append(sp.name)
711 except FileNotFoundError:
712 raise click.ClickException(
713 "pytest is not installed. Install it with: pip install pytest"
718 click.echo(f
"Tests failed in: {', '.join(failed)}")
721 click.echo(
"All sub-project tests passed.")
723@cli.command(name="export-source")
724@click.option("--verbose", is_flag=True, help="Show per-file statistics.")
725@click.option("--clean", is_flag=True, help="[DEPRECATED: use 'oct clean'] Remove _source_code-* dirs.")
726@click.option("--single-dir", is_flag=True, help="Write all output into one root-level directory.")
727@click.option(
"--allow-outside-project", is_flag=
True,
728 help=
"Allow PATH outside the auto-detected project root (OI-405).")
729@click.option(
"--warn-secrets/--no-warn-secrets", default=
True,
730 help=
"Scan file contents for potential secrets (OI-416).")
731@click.option(
"--block-secrets", is_flag=
True,
732 help=
"Skip files with potential secrets (implies --warn-secrets, OI-416).")
733@click.option(
"--diff",
"diff_ref", default=
None,
734 help=
"Export only files changed since REF (git ref).")
735@click.argument("path", required=False)
738 warn_secrets, block_secrets, diff_ref, path):
739 """Export consolidated source-code files for inspection."""
740 from oct.core.project_root
import validate_path_containment
741 from oct.tools.source_exporter
import run_exporter
744 global_allow = ctx.obj.get(
"allow_outside_project",
False)
if ctx.obj
else False
745 allow_outside_project = allow_outside_project
or global_allow
749 ctx.obj[
"allow_outside_project"] = allow_outside_project
752 if allow_outside_project:
753 project_root = Path(path).resolve()
755 auto_root = get_project_root(ctx)
756 project_root = validate_path_containment(
757 Path(path), auto_root, allow_outside=
False
760 project_root = get_project_root(ctx)
764 args.append(
"--verbose")
767 "Warning: 'export-source --clean' is deprecated. "
768 "Use 'oct clean' (with optional --dry-run) instead.",
771 args.append(
"--clean")
773 args.append(
"--single-dir")
775 args.append(
"--warn-secrets")
777 args.append(
"--block-secrets")
779 args.append(f
"--diff={diff_ref}")
782 run_exporter(project_root, args)
783 except Exception
as e:
784 click.echo(f
"Error: export-source failed unexpectedly: {e}", err=
True)
788@cli.command(name="export-skeleton")
789@click.option(
"--json",
"json_mode", is_flag=
True,
790 help=
"Emit machine-readable JSON summary to stdout.")
791@click.option("--verbose", is_flag=True, help="Show per-file statistics.")
792@click.option("--single-dir", is_flag=True, help="Write all output into one root-level directory.")
793@click.option("--no-diagnostics", is_flag=True, help="Omit Diagnostics section from docstrings.")
794@click.option("--clean", is_flag=True, help="Remove _skeleton_code-* dirs.")
795@click.option(
"--allow-outside-project", is_flag=
True,
796 help=
"Allow PATH outside the auto-detected project root (OI-405).")
797@click.argument("path", required=False)
800 allow_outside_project, path):
801 """Export structural skeletons of source files for AI inspection."""
802 from oct.core.project_root
import validate_path_containment
803 from oct.tools.skeleton_exporter
import run_skeleton_exporter
806 if allow_outside_project:
807 project_root = Path(path).resolve()
809 auto_root = get_project_root(ctx)
810 project_root = validate_path_containment(
811 Path(path), auto_root, allow_outside=
False
814 project_root = get_project_root(ctx)
818 args.append(
"--verbose")
820 args.append(
"--single-dir")
822 args.append(
"--no-diagnostics")
824 args.append(
"--clean")
827 run_skeleton_exporter(project_root, args, json_mode=json_mode)
828 except Exception
as e:
829 click.echo(f
"Error: export-skeleton failed unexpectedly: {e}", err=
True)
834@click.argument("path", required=False)
835@click.option("--dry-run", is_flag=True, help="Show what would be deleted without deleting anything")
836@click.option(
"--allow-outside-project", is_flag=
True,
837 help=
"Allow PATH outside the auto-detected project root (OI-405).")
839def clean(ctx, path, dry_run, allow_outside_project):
840 """Remove _source_code-*, _skeleton_code-* export dirs and __pycache__ dirs."""
841 from oct.core.project_root
import validate_path_containment
842 from oct.tools.source_exporter
import clean_source_dirs, clean_pycache_dirs
843 from oct.tools.skeleton_exporter
import clean_skeleton_dirs
846 if allow_outside_project:
847 target = Path(path).resolve()
850 auto_root = get_project_root(ctx)
851 except click.ClickException:
852 auto_root = Path.cwd()
853 target = validate_path_containment(
854 Path(path), auto_root, allow_outside=
False
858 clean_source_dirs(target, dry_run=dry_run)
859 clean_skeleton_dirs(target, dry_run=dry_run)
860 clean_pycache_dirs(target, dry_run=dry_run)
864@click.option("--json", "json_mode", is_flag=True, help="Machine-readable JSON output")
865@click.option("--verbose", is_flag=True, help="Detailed per-file breakdown")
869 type=click.IntRange(min=1),
871 help=
"Test execution timeout in seconds (default: 300, or health.test_timeout in .octrc.json).",
874def health(ctx, json_mode, verbose, test_timeout):
875 """Display project compliance dashboard."""
876 project_root = get_project_root(ctx)
881 test_timeout=test_timeout,
882 log_output=ctx.obj.get(
"log_output",
False),
886@cli.command(context_settings=dict(ignore_unknown_options=True))
887@click.argument("args", nargs=-1, type=click.UNPROCESSED)
890 """Analyse inter-module dependencies."""
891 project_root = get_project_root(ctx)
892 exit_code = run_deps(project_root, list(args))
894 raise SystemExit(exit_code)
897@cli.command(name="install-hooks")
898@click.option("--force", is_flag=True, help="Overwrite existing .pre-commit-config.yaml")
901 """[DEPRECATED] Use 'oct git hooks install' instead."""
903 "Notice: 'oct install-hooks' is deprecated. "
904 "Use 'oct git hooks install' instead.",
907 project_root = get_project_root(ctx)
910 config_path = project_root /
".pre-commit-config.yaml"
911 if config_path.exists()
and not force:
912 click.echo(f
"Error: {config_path} already exists. Use --force to overwrite.")
914 config_path.write_text(_ENHANCED_HOOKS_TEMPLATE, encoding=
"utf-8")
915 click.echo(f
"Created {config_path}")
916 click.echo(
"Run 'pre-commit install' to activate the hooks.")
919@cli.command(context_settings=dict(ignore_unknown_options=True))
920@click.option(
"--json",
"json_mode", is_flag=
True,
921 help=
"Emit machine-readable JSON summary to stdout.")
922@click.argument("args", nargs=-1, type=click.UNPROCESSED)
924def typecheck(ctx, json_mode, args):
925 """Run type checking (pyright or mypy)."""
926 project_root = get_project_root(ctx)
927 exit_code = run_typecheck(project_root, list(args), json_mode=json_mode)
929 raise SystemExit(exit_code)
933from oct.git import git_group
as _git_group
934cli.add_command(_git_group, name=
"git")
937from oct.migrate import migrate_group
as _migrate_group
938cli.add_command(_migrate_group, name=
"migrate")
941from oct.audit import audit_group
as _audit_cmd
942cli.add_command(_audit_cmd, name=
"audit")
946cli.add_command(_oct_install_cmd, name=
"install")
952 """oc_diagnostics configuration tools."""
956@diag.command(name=
"validate-config")
961 help=
"OI-534: escalate WARNING to failure; exit 2 on any ERROR or WARNING.",
965 """Validate debug_config.json against the expected schema."""
966 project_root = get_project_root(ctx)
967 issues = validate_config(project_root)
969 click.echo(
"debug_config.json is valid.")
973 has_error = any(i.startswith(
"ERROR")
for i
in issues)
974 has_warning = any(i.startswith(
"WARNING")
for i
in issues)
978 if strict
and (has_error
or has_warning):
984@diag.command(name="list-domains")
987 """List configured domains with their levels and colors."""
988 project_root = get_project_root(ctx)
990 domains = list_domains(project_root)
991 except FileNotFoundError
as e:
992 raise click.ClickException(str(e))
995 click.echo(
"No domains configured.")
998 click.echo(f
"{'Domain':<24} {'Level':<8} {'Color'}")
1001 click.echo(f
"{d['name']:<24} {d['level']:<8} {d['color']}")
1004@diag.command(name="set-level")
1005@click.argument("domain")
1006@click.argument("level", type=click.IntRange(0, 9))
1011 help=
"Preview the edit without writing the config or its .bk backup.",
1015 """Set a domain's debug level (0-4).
1017 OI-528: the write is always preceded by a ``<path>.bk`` backup so
1018 ``oct diag restore`` can undo a bad edit. ``--dry-run`` prints the
1019 would-be post-edit config to stdout and skips both writes.
1021 project_root = get_project_root(ctx)
1023 new_config = set_level(project_root, domain, level, dry_run=dry_run)
1024 except (FileNotFoundError, ValueError, KeyError)
as e:
1025 raise click.ClickException(str(e))
1027 click.echo(json.dumps(new_config, indent=2))
1029 f
"[DRY RUN] Would set {domain} level to {level} (no write).",
1033 click.echo(f
"Set {domain} level to {level}.")
1036@diag.command(name="restore")
1039 """OI-528: restore debug_config.json from its .bk backup."""
1040 project_root = get_project_root(ctx)
1042 restored = restore_config(project_root)
1043 except FileNotFoundError
as e:
1044 raise click.ClickException(str(e))
1045 click.echo(f
"Restored {restored} from backup.")
1048@diag.command(name="migrate-config")
1052 type=click.Path(dir_okay=
False, path_type=Path),
1058 help=
"Show the migrated config on stdout without writing to disk.",
1062 """OI-508 / FS-515: upgrade a legacy v1 debug_config.json to v2.0.
1064 When PATH is omitted, migrates the project's own debug_config.json.
1065 Writes ``<path>.bk`` before overwriting; idempotent — running twice
1066 over an already-v2 file is a no-op.
1069 if config_path
is None:
1070 project_root = get_project_root(ctx)
1071 config_path = _find_config(project_root)
1073 new_config = migrate_config(config_path, dry_run=dry_run)
1074 except (FileNotFoundError, ValueError)
as e:
1075 raise click.ClickException(str(e))
1077 click.echo(json.dumps(new_config, indent=2))
1078 click.echo(f
"[DRY RUN] Would write v2 schema to {config_path}", err=
True)
1080 backup_path = config_path.with_suffix(config_path.suffix +
".bk")
1081 click.echo(f
"Migrated {config_path} -> v{new_config.get('_schema_version')} "
1082 f
"(backup: {backup_path.name})")
1085@cli.command(name="install-mcp")
1088 type=click.Choice([
"claude-code",
"cursor",
"vscode",
"auto"], case_sensitive=
False),
1090 help=
"Target AI host (default: auto-detect).",
1093 "--dry-run/--no-dry-run",
1095 help=
"Preview without writing (default: --dry-run).",
1099 """Install oct-mcp into an AI host's MCP config (Claude Code, Cursor, VS Code)."""
1100 from oct.tools.install_mcp
import run_install_mcp
1101 project_root = get_project_root(ctx)
1102 exit_code = run_install_mcp(host=host, project_root=project_root, dry_run=dry_run)
1104 raise SystemExit(exit_code)
1107if __name__ ==
"__main__":
format_help(self, ctx, formatter)
new(ctx, path, force, test, dry_run, verbose)
bool _pytest_missing(str combined_output)
export_skeleton(ctx, json_mode, verbose, single_dir, no_diagnostics, clean, allow_outside_project, path)
clean(ctx, path, dry_run, allow_outside_project)
None _run_pytest(Path project_root, tuple args, bool json_mode=False)
diag_validate_config(ctx, bool strict)
install_hooks_cmd(ctx, force)
diag_set_level(ctx, domain, level, bool dry_run)
export_source(ctx, verbose, clean, single_dir, allow_outside_project, warn_secrets, block_secrets, diff_ref, path)
test(ctx, json_mode, changed, args)
Path|None _map_source_to_test(Path source, Path project_root)
diag_migrate_config(ctx, Path|None config_path, bool dry_run)