./aipass repo-root launcher — cold-clone entry (DPLAN-0234 Strand B, built by @aipass)
- 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
This commit is contained in:
@@ -13,6 +13,20 @@ PyPI version — not the changelog header.
|
|||||||
|
|
||||||
### Added
|
### 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
|
- **`./setup.sh` chains into `aipass init run` — clone-first one-command install
|
||||||
(DPLAN-0234 Strand A).** Distribution is git-clone, not pip: the framework
|
(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
|
changes constantly, so a PyPI snapshot goes stale while a clone is always
|
||||||
|
|||||||
@@ -32,10 +32,11 @@ AIPass is a CLI-native scaffold that adds **persistent memory, identity, and coo
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/AIOSAI/AIPass.git
|
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.
|
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
|
```bash
|
||||||
git clone https://github.com/AIOSAI/AIPass.git
|
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)
|
### 2. Your own project (if you skipped the chain)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -101,6 +101,11 @@
|
|||||||
"standard": "encapsulation",
|
"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."
|
"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",
|
"file": "tests/test_ping_sweep.py",
|
||||||
"standard": "architecture",
|
"standard": "architecture",
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user