From c8e1f07fdb3dcc0c97f6ae836b23986d6ebad85d Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Sun, 5 Jul 2026 15:50:01 -0700 Subject: [PATCH] =?UTF-8?q?./aipass=20repo-root=20launcher=20=E2=80=94=20c?= =?UTF-8?q?old-clone=20entry=20(DPLAN-0234=20Strand=20B,=20built=20by=20@a?= =?UTF-8?q?ipass)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New stdlib-only bash launcher at repo root: pre-setup only 'install' works (delegates to setup.sh, full flag pass-through); post-setup execs the venv aipass binary transparently - 13 launcher tests (tests/test_launcher.py), bypass.json architecture entry for the test file - README Quick Start leads with ./aipass install; CHANGELOG entry --- CHANGELOG.md | 14 ++ README.md | 10 +- aipass | 52 ++++++ src/aipass/aipass/.seedgo/bypass.json | 5 + src/aipass/aipass/tests/test_launcher.py | 203 +++++++++++++++++++++++ 5 files changed, 280 insertions(+), 4 deletions(-) create mode 100755 aipass create mode 100644 src/aipass/aipass/tests/test_launcher.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 75b7312d..658d640e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,20 @@ PyPI version — not the changelog header. ### Added +- **`./aipass` — repo-root cold-clone launcher (DPLAN-0234 Strand B).** The + branded entry point for the clone-first flow: `git clone`, `cd AIPass`, + `./aipass install` — three commands to a working AIPass. Stdlib-only bash + (zero deps, runs before anything is installed): pre-setup, only the `install` + verb exists and delegates to `setup.sh` with full flag pass-through + (`--no-init` / `--with-init` / `--project`); any other verb prints help + pointing at `./aipass install`. Post-setup the launcher turns transparent — + it execs the venv `aipass` binary for everything, so `./aipass doctor` just + works. Bare `aipass` always resolves to the PATH binary; the launcher only + ever runs as an explicit `./aipass`. 13 launcher tests (file properties, + pre-setup help, install delegation, post-setup forwarding), @aipass suite + 622 green, seedgo 100%. README Quick Start now leads with `./aipass install`. + (built by @aipass) + - **`./setup.sh` chains into `aipass init run` — clone-first one-command install (DPLAN-0234 Strand A).** Distribution is git-clone, not pip: the framework changes constantly, so a PyPI snapshot goes stale while a clone is always diff --git a/README.md b/README.md index 20794d93..e29f8694 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,11 @@ AIPass is a CLI-native scaffold that adds **persistent memory, identity, and coo ```bash git clone https://github.com/AIOSAI/AIPass.git -cd AIPass && ./setup.sh # installs everything, then walks you into your first project +cd AIPass +./aipass install # installs everything, then walks you into your first project ``` -Setup builds the environment, puts `aipass` + `drone` on your PATH, then chains straight into a guided init that creates your project, your first agent, and opens a terminal where that agent is already running. Say "hi" — it knows who it is. Come back tomorrow — it remembers. +One command does it all: builds the environment, puts `aipass` + `drone` on your PATH, then chains straight into a guided init that creates your project, your first agent, and opens a terminal where that agent is already running. Say "hi" — it knows who it is. Come back tomorrow — it remembers. This is the base framework. It gives your agents the infrastructure to persist, communicate, and organize — everything else you build on top. @@ -81,10 +82,11 @@ aipass init agent my-agent # Full agent: apps, mail, memory, identity ```bash git clone https://github.com/AIOSAI/AIPass.git -cd AIPass && ./setup.sh # Creates venv, installs, puts `aipass` + `drone` on your PATH, bootstraps 17 agents +cd AIPass +./aipass install # Creates venv, installs, puts `aipass` + `drone` on your PATH, bootstraps 17 agents ``` -On an interactive terminal, setup ends by chaining into `aipass init run` — one command takes you from clone to a working first project. Pass `--no-init` to skip the chain, `--project ` to pick where the project lands (CI and piped shells skip automatically). +On an interactive terminal, install ends by chaining into `aipass init run` — one command takes you from clone to a working first project. Pass `--no-init` to skip the chain, `--project ` to pick where the project lands (CI and piped shells skip automatically). `./aipass` is a thin repo-root launcher over `setup.sh`; after setup it simply forwards to the installed `aipass` binary. ### 2. Your own project (if you skipped the chain) diff --git a/aipass b/aipass new file mode 100755 index 00000000..f495934a --- /dev/null +++ b/aipass @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# aipass — cold-clone entry point (pre-venv launcher) +# +# The first command after cloning: +# git clone https://github.com/AIOSAI/AIPass.git +# cd AIPass +# ./aipass install +# +# Pre-setup: only `install` is available — delegates to setup.sh. +# Post-setup: forwards everything to the venv-installed aipass binary. +# +# Stdlib-only, zero AIPass imports, zero third-party deps. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# --- Post-setup: forward to the real binary --- +if [[ -x "$SCRIPT_DIR/.venv/bin/aipass" ]]; then + exec "$SCRIPT_DIR/.venv/bin/aipass" "$@" +fi +if [[ -x "$SCRIPT_DIR/.venv/Scripts/aipass.exe" ]]; then + exec "$SCRIPT_DIR/.venv/Scripts/aipass.exe" "$@" +fi +if [[ -x "$SCRIPT_DIR/.venv/Scripts/aipass" ]]; then + exec "$SCRIPT_DIR/.venv/Scripts/aipass" "$@" +fi + +# --- Pre-setup: only 'install' works --- +if [[ "${1:-}" == "install" ]]; then + shift + exec bash "$SCRIPT_DIR/setup.sh" "$@" +fi + +# --- Help (no venv, no 'install' verb) --- +cat <<'HELP' +AIPass is not set up yet. + + ./aipass install # set up AIPass (venv + deps + hooks) + ./aipass install --no-init # set up without the guided first-project setup + ./aipass install --project DIR # set the first-project directory + +After setup, all aipass commands become available: + ./aipass doctor # system health + ./aipass help # how-does-X-work Q&A + ./aipass init run # scaffold a new project + +Quick start: + git clone https://github.com/AIOSAI/AIPass.git + cd AIPass + ./aipass install +HELP diff --git a/src/aipass/aipass/.seedgo/bypass.json b/src/aipass/aipass/.seedgo/bypass.json index f40d11af..26d8d28f 100644 --- a/src/aipass/aipass/.seedgo/bypass.json +++ b/src/aipass/aipass/.seedgo/bypass.json @@ -101,6 +101,11 @@ "standard": "encapsulation", "reason": "Unit tests must import the install module directly to test home resolution, clone, and setup orchestration in isolation. Entry-point imports would defeat the purpose of unit testing." }, + { + "file": "tests/test_launcher.py", + "standard": "architecture", + "reason": "Test file lives in tests/ by convention — not in apps/. Standard 3-layer structure applies to production code only." + }, { "file": "tests/test_ping_sweep.py", "standard": "architecture", diff --git a/src/aipass/aipass/tests/test_launcher.py b/src/aipass/aipass/tests/test_launcher.py new file mode 100644 index 00000000..570740bd --- /dev/null +++ b/src/aipass/aipass/tests/test_launcher.py @@ -0,0 +1,203 @@ +# =================== AIPass ==================== +# Name: test_launcher.py +# Description: Tests for the repo-root ./aipass cold-clone launcher +# Version: 1.0.0 +# Created: 2026-07-05 +# Modified: 2026-07-05 +# ============================================= + +"""Tests for the repo-root ./aipass cold-clone launcher (bash script).""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="bash launcher requires Unix") + +REPO_ROOT = Path(__file__).resolve().parents[4] +LAUNCHER = REPO_ROOT / "aipass" + +_EXEC_BITS = 0o111 + + +def _make_executable(path: Path) -> None: + """Set executable bits on a file.""" + path.chmod(path.stat().st_mode | _EXEC_BITS) + + +def _isolated_launcher(tmp_path: Path) -> Path: + """Copy the launcher to a temp dir (isolated from real .venv).""" + dest = tmp_path / "aipass" + shutil.copy2(LAUNCHER, dest) + _make_executable(dest) + return dest + + +def _fake_venv_binary(tmp_path: Path, body: str) -> Path: + """Create a fake .venv/bin/aipass that runs the given bash body.""" + venv_bin = tmp_path / ".venv" / "bin" + venv_bin.mkdir(parents=True) + fake = venv_bin / "aipass" + fake.write_text(f"#!/usr/bin/env bash\n{body}\n") + _make_executable(fake) + return fake + + +class TestLauncherProperties: + """Verify the launcher file's basic properties.""" + + def test_exists(self): + """Launcher file exists at repo root.""" + assert LAUNCHER.is_file() + + def test_executable(self): + """Launcher has executable permission.""" + assert os.access(LAUNCHER, os.X_OK) + + def test_shebang(self): + """Launcher starts with bash shebang.""" + first_line = LAUNCHER.read_text().splitlines()[0] + assert first_line == "#!/usr/bin/env bash" + + def test_no_python_imports(self): + """Launcher contains no Python imports (stdlib-only bash).""" + content = LAUNCHER.read_text() + assert "import " not in content + assert "from " not in content + + +class TestPreSetupHelp: + """Pre-setup (no venv): bare ./aipass prints help text.""" + + def test_no_args_shows_help(self, tmp_path): + """No args and no venv prints setup instructions.""" + launcher = _isolated_launcher(tmp_path) + result = subprocess.run( + ["bash", str(launcher)], + capture_output=True, + text=True, + timeout=5, + ) + assert "not set up yet" in result.stdout + assert "./aipass install" in result.stdout + + def test_unknown_verb_shows_help(self, tmp_path): + """Unknown verb pre-setup prints help, not an error.""" + launcher = _isolated_launcher(tmp_path) + result = subprocess.run( + ["bash", str(launcher), "doctor"], + capture_output=True, + text=True, + timeout=5, + ) + assert "not set up yet" in result.stdout + + def test_help_mentions_quick_start(self, tmp_path): + """Help text includes the Quick start snippet.""" + launcher = _isolated_launcher(tmp_path) + result = subprocess.run( + ["bash", str(launcher)], + capture_output=True, + text=True, + timeout=5, + ) + assert "Quick start" in result.stdout + assert "git clone" in result.stdout + + +class TestPreSetupInstall: + """Pre-setup: ./aipass install delegates to setup.sh.""" + + def test_install_calls_setup_sh(self, tmp_path): + """./aipass install execs setup.sh in the same directory.""" + launcher = _isolated_launcher(tmp_path) + setup = tmp_path / "setup.sh" + setup.write_text('#!/usr/bin/env bash\necho "SETUP_CALLED $@"\n') + _make_executable(setup) + + result = subprocess.run( + ["bash", str(launcher), "install"], + capture_output=True, + text=True, + timeout=5, + ) + assert "SETUP_CALLED" in result.stdout + + def test_install_passes_flags(self, tmp_path): + """Flags after 'install' pass through to setup.sh.""" + launcher = _isolated_launcher(tmp_path) + setup = tmp_path / "setup.sh" + setup.write_text('#!/usr/bin/env bash\necho "FLAGS:$@"\n') + _make_executable(setup) + + result = subprocess.run( + ["bash", str(launcher), "install", "--no-init"], + capture_output=True, + text=True, + timeout=5, + ) + assert "--no-init" in result.stdout + + def test_install_passes_project_flag(self, tmp_path): + """--project and its value pass through to setup.sh.""" + launcher = _isolated_launcher(tmp_path) + setup = tmp_path / "setup.sh" + setup.write_text('#!/usr/bin/env bash\necho "FLAGS:$@"\n') + _make_executable(setup) + + result = subprocess.run( + ["bash", str(launcher), "install", "--project", str(tmp_path / "proj")], + capture_output=True, + text=True, + timeout=5, + ) + assert "--project" in result.stdout + + +class TestPostSetupForwarding: + """Post-setup: forwards to the venv binary.""" + + def test_forwards_to_venv_binary(self, tmp_path): + """With venv binary present, verbs forward to it.""" + launcher = _isolated_launcher(tmp_path) + _fake_venv_binary(tmp_path, 'echo "VENV_AIPASS $@"') + + result = subprocess.run( + ["bash", str(launcher), "doctor"], + capture_output=True, + text=True, + timeout=5, + ) + assert "VENV_AIPASS doctor" in result.stdout + + def test_forwards_install_post_setup(self, tmp_path): + """Post-setup ./aipass install goes to venv binary, not setup.sh.""" + launcher = _isolated_launcher(tmp_path) + _fake_venv_binary(tmp_path, 'echo "VENV_AIPASS $@"') + + result = subprocess.run( + ["bash", str(launcher), "install"], + capture_output=True, + text=True, + timeout=5, + ) + assert "VENV_AIPASS install" in result.stdout + + def test_no_args_forwards_post_setup(self, tmp_path): + """Post-setup bare ./aipass forwards to venv binary.""" + launcher = _isolated_launcher(tmp_path) + _fake_venv_binary(tmp_path, 'echo "VENV_AIPASS_NOARGS"') + + result = subprocess.run( + ["bash", str(launcher)], + capture_output=True, + text=True, + timeout=5, + ) + assert "VENV_AIPASS_NOARGS" in result.stdout