Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,27 @@ jobs:
- name: Setup uv virtual environment
run: uv venv

- name: Install dependencies
- name: Install lean dependencies
run: uv pip install -e .

- name: Verify lean install keeps native extras optional
run: |
uv run python - <<'PY'
import importlib.util

optional_modules = ["PIL", "playwright", "regex", "ripgrep", "tiktoken"]
installed = [name for name in optional_modules if importlib.util.find_spec(name)]
if installed:
raise SystemExit(f"Lean install unexpectedly included optional modules: {installed}")
print("Lean install optional-native smoke check passed")
PY

- name: Install CI test extras
# The full historical test suite exercises image/search behavior.
# macOS CI can use the PyPI ripgrep [search] extra; Android/Termux uses
# system rg from `pkg install ripgrep` instead.
run: uv pip install -e ".[images,search]"

- name: Install pexpect for integration tests
run: uv pip install pexpect>=4.9.0

Expand Down
20 changes: 19 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,29 @@ jobs:
- name: Setup uv virtual environment
run: uv venv

- name: Install dependencies
- name: Install lean durable dependencies
# [durable] extras pulls in dbos, required by integration test
# tests/integration/test_dbos_enabled.py
run: uv pip install -e ".[durable]"

- name: Verify lean install keeps native extras optional
run: |
uv run python - <<'PY'
import importlib.util

optional_modules = ["PIL", "playwright", "regex", "ripgrep", "tiktoken"]
installed = [name for name in optional_modules if importlib.util.find_spec(name)]
if installed:
raise SystemExit(f"Lean install unexpectedly included optional modules: {installed}")
print("Lean install optional-native smoke check passed")
PY

- name: Install CI test extras
# The full historical test suite exercises image/search behavior.
# macOS CI can use the PyPI ripgrep [search] extra; Android/Termux uses
# system rg from `pkg install ripgrep` instead.
run: uv pip install -e ".[durable,images,search]"

- name: Install pexpect for integration tests
run: uv pip install pexpect>=4.9.0

Expand Down
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,73 @@ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | ie
uvx code-puppy
```

#### Android / Termux

Code Puppy installs lean on Android/Termux. Read the full guide at
[`docs/ANDROID_INSTALL.md`](docs/ANDROID_INSTALL.md). Android's Python wheel
availability is different from desktop Linux, so the installer keeps optional
native-heavy features detached and makes the required Termux build toolchain
explicit.

**The one-command path (recommended):** the interactive bootstrap *wizard*
detects the device and walks you through every remaining step (native helper
packages, Code Puppy) with a `[Y/n]` confirmation before each one, then verifies
the install. Install `uv rust clang` first because `uvx --from code-puppy ...`
must resolve Code Puppy's required Python dependencies before the wizard can run:

```bash
pkg update
pkg install python git uv rust clang
uvx --from code-puppy code-puppy-bootstrap wizard
```

The wizard is stdlib-only, gates every state-changing step behind a prompt, and
finishes with a verification + reconciliation summary. Add `--yes` to
auto-confirm for scripted installs, or `--dry-run` to preview without executing
anything. You can also inspect the plan without running it:

```bash
uvx --from code-puppy code-puppy-bootstrap detect # what the wizard sees
uvx --from code-puppy code-puppy-bootstrap plan # the lean install plan
```

<details>
<summary>Prefer to drive it by hand? Here is the manual flow the wizard runs.</summary>

```bash
# In Termux
pkg update
pkg install python git uv rust clang ripgrep proot
uv tool install --refresh code-puppy
code-puppy -i
```

</details>

Install `ripgrep` from Termux (`pkg install ripgrep`) so the file-search and
recursive-listing tools work — Code Puppy uses the system `rg` binary. Android
support does not remove search support; it only prevents the PyPI `ripgrep`
package from being installed on Android/Termux, where it is known to fail.
Desktop and CI installs using the optional `[search]` extra remain unchanged.

#### Optional: Browser automation

Browser tools are powered by [Playwright](https://playwright.dev/), which has no
wheels for some platforms (e.g. Android/Termux). It is therefore an optional
extra and is **not** part of the base install. On supported platforms, opt in:

```bash
pip install "code-puppy[browser]"
# or
uv tool install "code-puppy[browser]"

# then download the browser binaries
playwright install chromium
```

Without this extra, Code Puppy runs normally; browser tools simply report that
the optional `browser` dependency is missing when they are invoked.

#### Optional: DBOS durable execution

Code Puppy ships with an optional [DBOS](https://github.com/dbos-inc/dbos-transact-py)-backed
Expand Down
153 changes: 153 additions & 0 deletions code_puppy/bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
from __future__ import annotations

import argparse
import json
import sys
from typing import Any

from code_puppy.bootstrap_profiles import (
available_profiles,
build_install_plan,
detect_environment,
)


def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Code Puppy bootstrap planner for lean environment-aware installs"
)
subparsers = parser.add_subparsers(dest="command")

detect_parser = subparsers.add_parser(
"detect",
help="Inspect the current environment without importing the runtime CLI",
)
detect_parser.add_argument("--json", action="store_true", help="Print JSON output")

plan_parser = subparsers.add_parser(
"plan",
help="Build a lean install/reattach plan from a builtin profile and optional manifest",
)
plan_parser.add_argument(
"--profile",
default="auto",
choices=["auto", *available_profiles()],
help="Builtin install profile to use",
)
plan_parser.add_argument(
"--manifest-json",
default="",
help="Inline JSON manifest with extras_add/extras_remove/notes overrides",
)
plan_parser.add_argument(
"--manifest-file",
default="",
help="Path to a JSON manifest file with plan overrides",
)
plan_parser.add_argument("--json", action="store_true", help="Print JSON output")

wizard_parser = subparsers.add_parser(
"wizard",
help="Interactively install Code Puppy step-by-step with confirmation",
)
wizard_parser.add_argument(
"--profile",
default="auto",
choices=["auto", *available_profiles()],
help="Builtin install profile to use",
)
wizard_parser.add_argument(
"--yes",
action="store_true",
help="Auto-confirm every step (non-interactive automation)",
)
wizard_parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would run without executing anything",
)

return parser


def _human_detect(environment: dict[str, Any]) -> str:
lines = [
"Code Puppy bootstrap environment",
f"- python: {environment['python_executable']}",
f"- version: {environment['python_version']}",
f"- platform: {environment['platform_system']} {environment['platform_release']} ({environment['platform_machine']})",
f"- termux: {'yes' if environment['is_termux'] else 'no'}",
f"- android: {'yes' if environment['is_android'] else 'no'}",
f"- uv: {'yes' if environment['has_uv'] else 'no'}",
f"- uvx: {'yes' if environment['has_uvx'] else 'no'}",
f"- proot: {'yes' if environment['has_proot'] else 'no'}",
f"- ripgrep: {'yes' if environment['has_ripgrep'] else 'no'}",
f"- rust: {'yes' if environment['has_rust'] else 'no'}",
f"- clang: {'yes' if environment['has_clang'] else 'no'}",
]
return "\n".join(lines)


def _human_plan(plan: dict[str, Any]) -> str:
lines = [
f"Profile: {plan['profile']}",
f"Package spec: {plan['package_spec']}",
f"Install: {plan['install_command']}",
f"Reattach: {plan['reattach_command']}",
f"Run: {plan['run_command']}",
]
if plan["extras"]:
lines.append(f"Extras: {', '.join(plan['extras'])}")
if plan["degraded_capabilities"]:
lines.append("Degraded until reattach:")
lines.extend(f"- {item}" for item in plan["degraded_capabilities"])
if plan["system_packages"]:
lines.append("Suggested system packages:")
lines.extend(f"- {item}" for item in plan["system_packages"])
if plan["notes"]:
lines.append("Notes:")
lines.extend(f"- {item}" for item in plan["notes"])
return "\n".join(lines)


def main(argv: list[str] | None = None) -> int:
parser = _build_parser()
args = parser.parse_args(argv)

command = args.command or "plan"

try:
if command == "wizard":
from code_puppy.bootstrap_wizard import run_wizard

return run_wizard(
profile=getattr(args, "profile", "auto"),
assume_yes=getattr(args, "yes", False),
dry_run=getattr(args, "dry_run", False),
)

if command == "detect":
environment = detect_environment()
if args.json:
print(json.dumps(environment, indent=2, sort_keys=True))
else:
print(_human_detect(environment))
return 0

plan = build_install_plan(
requested_profile=getattr(args, "profile", "auto"),
manifest_json=getattr(args, "manifest_json", ""),
manifest_file=getattr(args, "manifest_file", ""),
)
if getattr(args, "json", False):
print(json.dumps(plan, indent=2, sort_keys=True))
else:
print(_human_plan(plan))
return 0
except ValueError as exc:
parser.exit(status=2, message=f"error: {exc}\n")
return 2


if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
Loading