diff --git a/CHANGELOG.md b/CHANGELOG.md index c93f3453..75b7312d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,24 @@ PyPI version — not the changelog header. ### Added +- **`./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 + current HEAD. Now `git clone && cd AIPass && ./setup.sh` takes you from cold + clone to a working first project in one command: on interactive terminals, + setup ends by launching the guided `aipass init run` in a sibling directory + (default `~/aipass-project`, prompt to choose — init refuses to run inside the + engine tree). New flags mirror `aipass install`'s handoff rules: `--no-init` + skips, `--with-init` forces even headless (init chains `--non-interactive`), + `--project ` picks the target. CI and piped shells skip automatically + (`CI` env or no tty), so the windows/macos-test workflows that run bare + `bash setup.sh` are untouched. `install.py` now calls `setup.sh --no-init` + since install owns its own init handoff — no double-scaffold. Proven in + clean-room Docker, both runs exit 0: bare headless run skips init with a + hint; `--with-init` run chains init to `✓ Project initialized.` with the + project dir scaffolded. 39/39 install tests, seedgo 30/30. README Quick + Start updated to the one-command flow. + - **`aipass install` — one-command framework bootstrap.** The missing half of `pip install aipass`: a single command resolves the install home (default `~/AIPass`), git-clones the public repo, runs `setup.sh` (venv, editable diff --git a/README.md b/README.md index 20febaf5..20794d93 100644 --- a/README.md +++ b/README.md @@ -32,13 +32,10 @@ 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 the `aipass` + `drone` commands on your PATH - -cd ~ && mkdir my-project && cd my-project -aipass init run +cd AIPass && ./setup.sh # installs everything, then walks you into your first project ``` -A guided setup 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. +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. This is the base framework. It gives your agents the infrastructure to persist, communicate, and organize — everything else you build on top. @@ -87,7 +84,9 @@ git clone https://github.com/AIOSAI/AIPass.git cd AIPass && ./setup.sh # Creates venv, installs, puts `aipass` + `drone` on your PATH, bootstraps 17 agents ``` -### 2. Your own project +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). + +### 2. Your own project (if you skipped the chain) ```bash cd ~ && mkdir my-project && cd my-project diff --git a/setup.sh b/setup.sh index 83afe50d..d58a5267 100755 --- a/setup.sh +++ b/setup.sh @@ -2,6 +2,13 @@ # # AIPass setup script # Creates a venv, installs the package in editable mode, and verifies CLI entry points. +# On interactive terminals it then chains into `aipass init run` to scaffold a first +# project (DPLAN-0234: one command does setup + init). +# +# Usage: ./setup.sh [--no-init] [--with-init] [--project ] +# --no-init skip the first-project init chain +# --with-init force the init chain even headless (init runs --non-interactive) +# --project first-project directory (default: ~/aipass-project) # set -euo pipefail @@ -27,6 +34,27 @@ case "${OSTYPE:-}" in ;; esac +# --- Args --- +# Mirrors the `aipass install` handoff rules: --no-init wins, --with-init forces, +# default (auto) chains into init on interactive terminals only — CI/headless skip. +RUN_INIT="auto" +INIT_PROJECT="" +PREV_ARG="" +for arg in "$@"; do + if [ "$PREV_ARG" = "--project" ]; then + INIT_PROJECT="$arg" + PREV_ARG="" + continue + fi + case "$arg" in + --no-init) RUN_INIT="no" ;; + --with-init) RUN_INIT="yes" ;; + --project=*) INIT_PROJECT="${arg#--project=}" ;; + --project) PREV_ARG="--project" ;; + *) echo "WARN: unknown argument '$arg' (ignored)" ;; + esac +done + echo "=== AIPass Setup ===" echo "Repo root: $SCRIPT_DIR" echo "" @@ -982,6 +1010,77 @@ if [ "$FAIL" -eq 0 ]; then echo " Claude Code: hooks installed to ~/.claude/settings.json" command -v codex &>/dev/null && echo " Codex CLI: hooks at .codex/hooks.json + config at ~/.codex/config.toml" echo "" + + # --- Chain into first-project init (DPLAN-0234: one command does setup + init) --- + # `aipass init` refuses to run inside the engine tree, so it always targets a + # sibling directory — never the repo itself. Decision mirrors install.py: + # --no-init wins, --with-init forces (headless chains --non-interactive), + # default = interactive terminals only (CI and piped shells skip). + AIPASS_BIN="" + if [ "$IS_WINDOWS" -eq 1 ] && [ -f "$SCRIPT_DIR/.venv/Scripts/aipass.exe" ]; then + AIPASS_BIN="$SCRIPT_DIR/.venv/Scripts/aipass.exe" + elif [ "$IS_WINDOWS" -eq 1 ] && [ -f "$SCRIPT_DIR/.venv/Scripts/aipass" ]; then + AIPASS_BIN="$SCRIPT_DIR/.venv/Scripts/aipass" + elif [ -f "$SCRIPT_DIR/.venv/bin/aipass" ]; then + AIPASS_BIN="$SCRIPT_DIR/.venv/bin/aipass" + elif command -v aipass &>/dev/null; then + AIPASS_BIN="$(command -v aipass)" + fi + + LAUNCH_INIT=0 + INIT_HEADLESS=0 + if [ "$RUN_INIT" = "no" ]; then + echo "Skipping first-project init (--no-init). Run 'aipass init run' in a fresh directory when ready." + elif [ "$RUN_INIT" = "yes" ]; then + LAUNCH_INIT=1 + if [ ! -t 0 ]; then + INIT_HEADLESS=1 + fi + elif [ -t 0 ] && [ -z "${CI:-}" ]; then + LAUNCH_INIT=1 + else + echo "Non-interactive shell — skipping first-project init. Run 'aipass init run' in a fresh directory when ready." + fi + + if [ "$LAUNCH_INIT" -eq 1 ]; then + if [ -z "$AIPASS_BIN" ]; then + echo "WARN: aipass binary not found — open a new terminal and run 'aipass init run' in a fresh directory." + else + DEFAULT_PROJECT_DIR="$HOME/aipass-project" + PROJECT_DIR="$INIT_PROJECT" + if [ -z "$PROJECT_DIR" ] && [ "$INIT_HEADLESS" -eq 0 ] && [ -t 0 ]; then + echo "AIPass projects live in their own directory, never inside the engine repo." + read -r -p "Set up your first project now? [Y/n]: " INIT_REPLY + case "$INIT_REPLY" in + [nN]*) + PROJECT_DIR="-" + echo " Skipped. Run 'aipass init run' in a fresh directory when ready." + ;; + *) + read -r -p " Project directory [$DEFAULT_PROJECT_DIR]: " INIT_INPUT + PROJECT_DIR="${INIT_INPUT:-$DEFAULT_PROJECT_DIR}" + ;; + esac + elif [ -z "$PROJECT_DIR" ]; then + PROJECT_DIR="$DEFAULT_PROJECT_DIR" + fi + + if [ "$PROJECT_DIR" != "-" ]; then + mkdir -p "$PROJECT_DIR" + INIT_CMD=("$AIPASS_BIN" init run) + if [ "$INIT_HEADLESS" -eq 1 ]; then + INIT_CMD+=(--non-interactive) + fi + echo "" + echo "Launching guided setup in $PROJECT_DIR ..." + if (cd "$PROJECT_DIR" && "${INIT_CMD[@]}"); then + echo "First project initialized at $PROJECT_DIR" + else + echo "Init didn't complete — run 'aipass init run' in $PROJECT_DIR when ready." + fi + fi + fi + fi else echo "=== Setup finished with errors ===" echo "The venv was created and the package was installed, but one or more" diff --git a/src/aipass/aipass/apps/modules/install.py b/src/aipass/aipass/apps/modules/install.py index efcc6ad5..6e225738 100644 --- a/src/aipass/aipass/apps/modules/install.py +++ b/src/aipass/aipass/apps/modules/install.py @@ -131,7 +131,9 @@ def _run_setup(home: Path, dry_run: bool) -> bool: return False console.print("[cyan]Building environment[/cyan] [dim](venv, dependencies, hook wiring)…[/dim]") try: - proc = subprocess.run(["bash", str(setup)], cwd=str(home), timeout=_SETUP_TIMEOUT) + # --no-init: install owns the init handoff (_handoff_to_init) — without it, + # setup.sh's own init chain (DPLAN-0234) would scaffold the project twice. + proc = subprocess.run(["bash", str(setup), "--no-init"], cwd=str(home), timeout=_SETUP_TIMEOUT) if proc.returncode == 0: return True logger.warning("[install] setup.sh exited %s", proc.returncode)