Skip to content
Merged
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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 <dir>` 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 <dir>` 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)

Expand Down
52 changes: 52 additions & 0 deletions aipass
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions src/aipass/aipass/.seedgo/bypass.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
203 changes: 203 additions & 0 deletions src/aipass/aipass/tests/test_launcher.py
Original file line number Diff line number Diff line change
@@ -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
Loading