diff --git a/.flow/epics/fn-5.json b/.flow/epics/fn-5.json new file mode 100644 index 00000000..40de4bbe --- /dev/null +++ b/.flow/epics/fn-5.json @@ -0,0 +1,13 @@ +{ + "branch_name": "pr-22-user-flag", + "created_at": "2026-01-13T05:56:37.540724Z", + "depends_on_epics": [], + "id": "fn-5", + "next_task": 1, + "plan_review_status": "unknown", + "plan_reviewed_at": null, + "spec_path": ".flow/specs/fn-5.md", + "status": "open", + "title": "PR 22: User-level storage (--user flag)", + "updated_at": "2026-01-13T05:56:37.540740Z" +} diff --git a/.flow/specs/fn-5.md b/.flow/specs/fn-5.md new file mode 100644 index 00000000..7409386b --- /dev/null +++ b/.flow/specs/fn-5.md @@ -0,0 +1,20 @@ +# fn-5 PR 22: User-level storage (--user flag) + +## Overview +TBD + +## Scope +TBD + +## Approach +TBD + +## Quick commands + +- `# e.g., npm test, bun test, make test` + +## Acceptance +- [ ] TBD + +## References +- TBD diff --git a/.gitignore b/.gitignore index bea5ab6c..250122fc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ plans/ todos/ /docs/ !plugins/*/docs/ +plugins/flow-next/scripts/__pycache__/ diff --git a/plugins/flow-next/VERSION b/plugins/flow-next/VERSION new file mode 100644 index 00000000..1d0ba9ea --- /dev/null +++ b/plugins/flow-next/VERSION @@ -0,0 +1 @@ +0.4.0 diff --git a/plugins/flow-next/scripts/flowctl.py b/plugins/flow-next/scripts/flowctl.py index e1561aa4..1d4b6d45 100755 --- a/plugins/flow-next/scripts/flowctl.py +++ b/plugins/flow-next/scripts/flowctl.py @@ -66,6 +66,11 @@ def get_flow_dir() -> Path: return get_repo_root() / FLOW_DIR +def get_user_config_dir() -> Path: + """Get user-level config directory (~/.config/flow-next/).""" + return Path.home() / ".config" / "flow-next" + + def ensure_flow_exists() -> bool: """Check if .flow/ exists.""" return get_flow_dir().exists() @@ -76,17 +81,48 @@ def get_default_config() -> dict: return {"memory": {"enabled": False}} -def load_flow_config() -> dict: - """Load .flow/config.json, returning defaults if missing.""" - config_path = get_flow_dir() / CONFIG_FILE - defaults = get_default_config() +def deep_merge(base: dict, override: dict) -> dict: + """Deep merge two dicts. Override values take precedence.""" + result = base.copy() + for key, value in override.items(): + if key in result and isinstance(result[key], dict) and isinstance(value, dict): + result[key] = deep_merge(result[key], value) + else: + result[key] = value + return result + + +def load_user_config() -> dict: + """Load user-level config from ~/.config/flow-next/config.json.""" + config_path = get_user_config_dir() / CONFIG_FILE if not config_path.exists(): - return defaults + return {} try: data = json.loads(config_path.read_text(encoding="utf-8")) - return data if isinstance(data, dict) else defaults + return data if isinstance(data, dict) else {} except (json.JSONDecodeError, Exception): - return defaults + return {} + + +def load_flow_config() -> dict: + """Load config, merging user-level with project-level (project overrides user).""" + defaults = get_default_config() + user_config = load_user_config() + + # Start with defaults, overlay user config + config = deep_merge(defaults, user_config) + + # Overlay project config if .flow/ exists + config_path = get_flow_dir() / CONFIG_FILE + if config_path.exists(): + try: + project_config = json.loads(config_path.read_text(encoding="utf-8")) + if isinstance(project_config, dict): + config = deep_merge(config, project_config) + except (json.JSONDecodeError, Exception): + pass + + return config def get_config(key: str, default=None): @@ -101,16 +137,28 @@ def get_config(key: str, default=None): return config if config != {} else default -def set_config(key: str, value) -> dict: - """Set nested config value and return updated config.""" - config_path = get_flow_dir() / CONFIG_FILE +def set_config(key: str, value, user_level: bool = False) -> dict: + """Set nested config value and return updated config. + + Args: + key: Config key (e.g., 'memory.enabled') + value: Value to set + user_level: If True, set in ~/.config/flow-next/config.json instead of .flow/config.json + """ + if user_level: + config_dir = get_user_config_dir() + config_dir.mkdir(parents=True, exist_ok=True) + config_path = config_dir / CONFIG_FILE + else: + config_path = get_flow_dir() / CONFIG_FILE + if config_path.exists(): try: config = json.loads(config_path.read_text(encoding="utf-8")) except (json.JSONDecodeError, Exception): - config = get_default_config() + config = {} else: - config = get_default_config() + config = {} # Navigate/create nested path parts = key.split(".") @@ -1077,8 +1125,8 @@ def cmd_init(args: argparse.Namespace) -> None: meta = {"schema_version": SCHEMA_VERSION, "next_epic": 1} atomic_write_json(flow_dir / META_FILE, meta) - # Create config.json with defaults - atomic_write_json(flow_dir / CONFIG_FILE, get_default_config()) + # Create empty config.json (inherits from user config) + atomic_write_json(flow_dir / CONFIG_FILE, {}) if args.json: json_output({"message": ".flow/ initialized", "path": str(flow_dir)}) @@ -1135,12 +1183,8 @@ def cmd_detect(args: argparse.Namespace) -> None: def cmd_config_get(args: argparse.Namespace) -> None: - """Get a config value.""" - if not ensure_flow_exists(): - error_exit( - ".flow/ does not exist. Run 'flowctl init' first.", use_json=args.json - ) - + """Get a config value (merges user-level + project-level).""" + # Config get works even without .flow/ (reads user config) value = get_config(args.key) if args.json: json_output({"key": args.key, "value": value}) @@ -1154,19 +1198,32 @@ def cmd_config_get(args: argparse.Namespace) -> None: def cmd_config_set(args: argparse.Namespace) -> None: - """Set a config value.""" - if not ensure_flow_exists(): + """Set a config value (use --user for user-level config).""" + user_level = getattr(args, "user", False) + + if not user_level and not ensure_flow_exists(): error_exit( - ".flow/ does not exist. Run 'flowctl init' first.", use_json=args.json + ".flow/ does not exist. Use --user for user-level config or run 'flowctl init' first.", + use_json=args.json, ) - set_config(args.key, args.value) - new_value = get_config(args.key) + set_config(args.key, args.value, user_level=user_level) + + # Parse the value the same way set_config does for display + display_value = args.value + if isinstance(display_value, str): + if display_value.lower() == "true": + display_value = True + elif display_value.lower() == "false": + display_value = False + elif display_value.isdigit(): + display_value = int(display_value) + location = "user-level (~/.config/flow-next/)" if user_level else "project (.flow/)" if args.json: - json_output({"key": args.key, "value": new_value, "message": f"{args.key} set"}) + json_output({"key": args.key, "value": display_value, "location": location, "message": f"{args.key} set"}) else: - print(f"{args.key} set to {new_value}") + print(f"{args.key} = {display_value} ({location})") MEMORY_TEMPLATES = { @@ -3617,6 +3674,7 @@ def main() -> None: p_config_set = config_sub.add_parser("set", help="Set config value") p_config_set.add_argument("key", help="Config key (e.g., memory.enabled)") p_config_set.add_argument("value", help="Config value") + p_config_set.add_argument("--user", action="store_true", help="Set in user-level config (~/.config/flow-next/)") p_config_set.add_argument("--json", action="store_true", help="JSON output") p_config_set.set_defaults(func=cmd_config_set) diff --git a/plugins/flow-next/skills/flow-next-ralph-init/SKILL.md b/plugins/flow-next/skills/flow-next-ralph-init/SKILL.md index 847ece5d..6d915106 100644 --- a/plugins/flow-next/skills/flow-next-ralph-init/SKILL.md +++ b/plugins/flow-next/skills/flow-next-ralph-init/SKILL.md @@ -1,24 +1,49 @@ --- name: flow-next-ralph-init -description: Scaffold repo-local Ralph autonomous harness under scripts/ralph/. Use when user runs /flow-next:ralph-init. +description: Scaffold Ralph autonomous harness. Supports project-local (scripts/ralph/) or user-level (~/.config/flow-next/ralph/) modes. Use when user runs /flow-next:ralph-init. --- # Ralph init -Scaffold repo-local Ralph harness. Opt-in only. +Scaffold Ralph autonomous harness. Opt-in only. Safe to re-run for updates. + +## Installation Modes + +### Project-local (default) +- Everything in `scripts/ralph/` in the current repo +- Scripts, config, and runs all in one place +- Good for: single project, team sharing via git + +### User-level (`--user` flag) +- Scripts in `~/.config/flow-next/ralph/` (shared across projects) +- User config in `~/.config/flow-next/ralph/config.env` (defaults) +- Project config in `scripts/ralph/config.env` (overrides) +- Runs stay in project `scripts/ralph/runs/` +- Good for: multiple projects, personal workflow, easy updates ## Rules -- Only create `scripts/ralph/` in the current repo. -- If `scripts/ralph/` already exists, stop and ask the user to remove it first. -- Copy templates from `templates/` into `scripts/ralph/`. -- Copy `flowctl` and `flowctl.py` from `${CLAUDE_PLUGIN_ROOT}/scripts/` into `scripts/ralph/`. -- Set executable bit on `scripts/ralph/ralph.sh`, `scripts/ralph/ralph_once.sh`, and `scripts/ralph/flowctl`. +### Project-local mode +- Create `scripts/ralph/` in the current repo +- If exists, ask user if they want to update or skip +- Copy all templates from `templates/` into `scripts/ralph/` +- Copy `flowctl` and `flowctl.py` from `${CLAUDE_PLUGIN_ROOT}/scripts/` +- Set executable bits + +### User-level mode (`--user`) +- Create `~/.config/flow-next/ralph/` if not exists +- If exists, backup modified files then update from plugin +- Copy scripts to user dir: `ralph.sh`, `ralph_once.sh`, `flowctl`, `flowctl.py`, `watch-filter.py`, `prompt_*.md`, `config.env` +- Write VERSION file to track plugin version +- In project, create `scripts/ralph/` with: + - `config.env` (project-specific overrides) + - Symlinks: `ralph.sh -> ~/.config/flow-next/ralph/ralph.sh` etc. + - `runs/` directory for run logs ## Workflow -1. Resolve repo root: `git rev-parse --show-toplevel` -2. Check `scripts/ralph/` does not exist. +1. Parse arguments: check for `--user` flag +2. Resolve repo root: `git rev-parse --show-toplevel` 3. Detect available review backends: ```bash HAVE_RP=$(which rp-cli >/dev/null 2>&1 && echo 1 || echo 0) @@ -37,12 +62,39 @@ Scaffold repo-local Ralph harness. Opt-in only. - If only rp-cli available: use `rp` - If only codex available: use `codex` - If neither available: use `none` -5. Write `scripts/ralph/config.env` with: - - `PLAN_REVIEW=` and `WORK_REVIEW=` - - replace `{{PLAN_REVIEW}}` and `{{WORK_REVIEW}}` placeholders in the template -6. Copy templates and flowctl files. -7. Print next steps (run from terminal, NOT inside Claude Code): - - Edit `scripts/ralph/config.env` to customize settings - - `./scripts/ralph/ralph_once.sh` (one iteration, observe) - - `./scripts/ralph/ralph.sh` (full loop, AFK) - - Uninstall: `rm -rf scripts/ralph/` + +### If project-local mode (no --user): +5. Check `scripts/ralph/` - if exists, ask "Update existing? (y/n)" +6. Copy templates to `scripts/ralph/` +7. Copy flowctl files +8. Replace `{{PLAN_REVIEW}}` and `{{WORK_REVIEW}}` in config.env (only on first install) +9. Set executable bits + +### If user-level mode (--user): +5. Check `~/.config/flow-next/ralph/VERSION` for existing version +6. If exists: backup modified files to `~/.config/flow-next/ralph/backups//` +7. Copy scripts to user dir +8. Update VERSION file with plugin version +9. In project `scripts/ralph/`: + - Create config.env if not exists (project overrides only) + - Create/update symlinks to user scripts + - Create `runs/` directory +10. Set executable bits on user scripts + +## Print next steps + +### Project-local: +- Edit `scripts/ralph/config.env` to customize settings +- `./scripts/ralph/ralph_once.sh` (one iteration, observe) +- `./scripts/ralph/ralph.sh` (full loop, AFK) +- Update: re-run `/flow-next:ralph-init` +- Uninstall: `rm -rf scripts/ralph/` + +### User-level: +- Edit `~/.config/flow-next/ralph/config.env` for user defaults +- Edit `scripts/ralph/config.env` for project-specific overrides +- `./scripts/ralph/ralph.sh` (runs from user-level scripts) +- Update: re-run `/flow-next:ralph-init --user` after plugin updates +- Backups: `~/.config/flow-next/ralph/backups/` (if files were modified) +- Uninstall project: `rm -rf scripts/ralph/` +- Uninstall user-level: `rm -rf ~/.config/flow-next/ralph/` diff --git a/plugins/flow-next/skills/flow-next-ralph-init/templates/ralph.sh b/plugins/flow-next/skills/flow-next-ralph-init/templates/ralph.sh index 6bba5d73..1f0f3773 100644 --- a/plugins/flow-next/skills/flow-next-ralph-init/templates/ralph.sh +++ b/plugins/flow-next/skills/flow-next-ralph-init/templates/ralph.sh @@ -1,11 +1,99 @@ #!/usr/bin/env bash set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" -CONFIG="$SCRIPT_DIR/config.env" +# ───────────────────────────────────────────────────────────────────────────── +# Path resolution: supports user-level or project-local installation +# ───────────────────────────────────────────────────────────────────────────── +# User-level mode: +# - Scripts in ~/.config/flow-next/ralph/ +# - User config in ~/.config/flow-next/ralph/config.env (defaults) +# - Project config in scripts/ralph/config.env (overrides) +# - Runs in scripts/ralph/runs/ (always project-local) +# +# Project-local mode: everything in scripts/ralph/ (original behavior) +# +# Detection order: +# 1. RALPH_USER_DIR env var (explicit user-level path) +# 2. ~/.config/flow-next/ralph/ if exists +# 3. Fall back to project-local (script's directory) +# ───────────────────────────────────────────────────────────────────────────── + +_portable_realpath() { + # Portable realpath: resolve symlinks to absolute path + # Works on Linux (readlink -f) and macOS (greadlink/python fallback) + local path="$1" + if readlink -f "$path" 2>/dev/null; then + return + elif command -v greadlink >/dev/null 2>&1; then + greadlink -f "$path" + elif command -v realpath >/dev/null 2>&1; then + realpath "$path" + else + python3 -c "import os; print(os.path.realpath('$path'))" + fi +} + +_resolve_script_dir() { + local self_dir + self_dir="$(cd "$(dirname "$0")" && pwd)" + + # Check if we're a symlink to user-level scripts + if [[ -L "$0" ]]; then + local target + target="$(_portable_realpath "$0")" + self_dir="$(dirname "$target")" + fi + + echo "$self_dir" +} + +_resolve_user_dir() { + # Explicit env var takes precedence + if [[ -n "${RALPH_USER_DIR:-}" ]]; then + echo "$RALPH_USER_DIR" + return + fi + # Default user-level location + local default_user_dir="${HOME}/.config/flow-next/ralph" + if [[ -d "$default_user_dir" ]]; then + echo "$default_user_dir" + return + fi + echo "" +} + +_resolve_project_dir() { + # Project scripts/ralph/ directory (for config and runs) + local root="$1" + echo "$root/scripts/ralph" +} + +# Determine actual script source (may be user-level) +SCRIPT_DIR="$(_resolve_script_dir)" +USER_DIR="$(_resolve_user_dir)" +ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null)" || ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +PROJECT_DIR="$(_resolve_project_dir "$ROOT_DIR")" + +# Scripts come from user-level if available, else project-local +if [[ -n "$USER_DIR" && -f "$USER_DIR/ralph.sh" ]]; then + SCRIPT_DIR="$USER_DIR" +fi + +# Config: user-level defaults + project overrides +# Both are sourced: user first (defaults), then project (overrides) +USER_CONFIG="" +PROJECT_CONFIG="" +if [[ -n "$USER_DIR" && -f "$USER_DIR/config.env" ]]; then + USER_CONFIG="$USER_DIR/config.env" +fi +if [[ -f "$PROJECT_DIR/config.env" ]]; then + PROJECT_CONFIG="$PROJECT_DIR/config.env" +fi FLOWCTL="$SCRIPT_DIR/flowctl" +# Ensure runs directory exists in project +mkdir -p "$PROJECT_DIR/runs" 2>/dev/null || true + fail() { echo "ralph: $*" >&2; exit 1; } log() { # Machine-readable logs: only show when UI disabled @@ -264,12 +352,15 @@ ui_waiting() { ui " ${C_DIM}⏳ Claude working...${C_RESET}" } -[[ -f "$CONFIG" ]] || fail "missing config.env" +# Require at least one config file +[[ -n "$USER_CONFIG" || -n "$PROJECT_CONFIG" ]] || fail "missing config.env (user or project)" [[ -x "$FLOWCTL" ]] || fail "missing flowctl" +# Load config: user-level first (defaults), then project (overrides) # shellcheck disable=SC1090 set -a -source "$CONFIG" +[[ -n "$USER_CONFIG" && -f "$USER_CONFIG" ]] && source "$USER_CONFIG" +[[ -n "$PROJECT_CONFIG" && -f "$PROJECT_CONFIG" ]] && source "$PROJECT_CONFIG" set +a MAX_ITERATIONS="${MAX_ITERATIONS:-25}" @@ -421,7 +512,7 @@ PY } RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)-$(hostname -s 2>/dev/null || hostname)-$(sanitize_id "$(get_actor)")-$$-$(rand4)" -RUN_DIR="$SCRIPT_DIR/runs/$RUN_ID" +RUN_DIR="$PROJECT_DIR/runs/$RUN_ID" mkdir -p "$RUN_DIR" ATTEMPTS_FILE="$RUN_DIR/attempts.json" ensure_attempts_file "$ATTEMPTS_FILE" diff --git a/plugins/flow-next/skills/flow-next-ralph-init/templates/ralph_once.sh b/plugins/flow-next/skills/flow-next-ralph-init/templates/ralph_once.sh index 2ce3112e..43f230d2 100644 --- a/plugins/flow-next/skills/flow-next-ralph-init/templates/ralph_once.sh +++ b/plugins/flow-next/skills/flow-next-ralph-init/templates/ralph_once.sh @@ -3,7 +3,26 @@ # Use this to observe behavior before going fully autonomous set -euo pipefail + +# Portable realpath (same as ralph.sh) +_portable_realpath() { + local path="$1" + if readlink -f "$path" 2>/dev/null; then + return + elif command -v greadlink >/dev/null 2>&1; then + greadlink -f "$path" + elif command -v realpath >/dev/null 2>&1; then + realpath "$path" + else + python3 -c "import os; print(os.path.realpath('$path'))" + fi +} + +# Resolve script directory (follows symlinks for user-level mode) SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +if [[ -L "$0" ]]; then + SCRIPT_DIR="$(dirname "$(_portable_realpath "$0")")" +fi export MAX_ITERATIONS=1 exec "$SCRIPT_DIR/ralph.sh" "$@" diff --git a/plugins/flow-next/skills/flow-next-setup/SKILL.md b/plugins/flow-next/skills/flow-next-setup/SKILL.md index bd115a9b..84e07aa0 100644 --- a/plugins/flow-next/skills/flow-next-setup/SKILL.md +++ b/plugins/flow-next/skills/flow-next-setup/SKILL.md @@ -1,24 +1,39 @@ --- name: flow-next-setup -description: Optional local install of flowctl CLI and CLAUDE.md/AGENTS.md instructions. Use when user runs /flow-next:setup. +description: Optional local install of flowctl CLI and CLAUDE.md/AGENTS.md instructions. Supports --user for user-level install. Use when user runs /flow-next:setup. --- # Flow-Next Setup (Optional) Install flowctl locally and add instructions to project docs. **Fully optional** - flow-next works without this via the plugin. +## Installation Modes + +### Project-local (default) +- Installs to `.flow/bin/` in current project +- Good for: team sharing via git, portable projects + +### User-level (`--user` flag) +- Installs to `~/.config/flow-next/` (shared across projects) +- Project gets symlinks to user-level scripts +- Good for: multiple projects, personal workflow, easy updates + ## Benefits -- `flowctl` accessible from command line (add `.flow/bin` to PATH) +- `flowctl` accessible from command line - Other AI agents (Codex, Cursor, etc.) can read instructions from CLAUDE.md/AGENTS.md - Works without Claude Code plugin installed ## Workflow -Read [workflow.md](workflow.md) and follow each step in order. +1. Parse arguments: check for `--user` flag +2. Read [workflow.md](workflow.md) and follow each step in order +3. Adapt paths based on mode: + - Project-local: `.flow/bin/` + - User-level: `~/.config/flow-next/bin/` with symlinks in `.flow/bin/` ## Notes - **Fully optional** - standard plugin usage works without local setup -- Copies scripts (not symlinks) for portability across environments - Safe to re-run - will detect existing setup and offer to update +- User-level mode creates backups before updating modified files diff --git a/plugins/flow-next/skills/flow-next-setup/templates/usage.md b/plugins/flow-next/skills/flow-next-setup/templates/usage.md index f77848cf..e5189e7b 100644 --- a/plugins/flow-next/skills/flow-next-setup/templates/usage.md +++ b/plugins/flow-next/skills/flow-next-setup/templates/usage.md @@ -70,6 +70,23 @@ Task tracking for AI agents. All state lives in `.flow/`. {"commits": ["abc123"], "tests": ["npm test"], "prs": []} ``` +## Configuration + +Config hierarchy (project overrides user): +1. `~/.config/flow-next/config.json` - user-level defaults +2. `.flow/config.json` - project-level overrides + +```bash +# Get config (reads merged user + project) +.flow/bin/flowctl config get memory.enabled + +# Set project-level config +.flow/bin/flowctl config set memory.enabled true + +# Set user-level config (applies to all projects) +.flow/bin/flowctl config set memory.enabled true --user +``` + ## More Info - Human docs: https://github.com/gmickel/gmickel-claude-marketplace/blob/main/plugins/flow-next/docs/flowctl.md diff --git a/plugins/flow-next/skills/flow-next-setup/workflow.md b/plugins/flow-next/skills/flow-next-setup/workflow.md index 335a2276..4627863d 100644 --- a/plugins/flow-next/skills/flow-next-setup/workflow.md +++ b/plugins/flow-next/skills/flow-next-setup/workflow.md @@ -2,7 +2,9 @@ Follow these steps in order. This workflow is **idempotent** - safe to re-run. -## Step 0: Resolve plugin path +## Step 0: Parse arguments and resolve paths + +Check if `--user` flag was passed. Set `USER_MODE=true` if so. The plugin root is the parent of this skill's directory. From this SKILL.md location, go up to find `scripts/` and `.claude-plugin/`. @@ -10,6 +12,10 @@ Example: if this file is at `~/.claude/plugins/cache/.../flow-next/0.3.12/skills Store this as `PLUGIN_ROOT` for use in later steps. +Set paths based on mode: +- **Project-local mode**: `BIN_DIR=".flow/bin"` +- **User-level mode**: `USER_BIN_DIR="$HOME/.config/flow-next/bin"`, `BIN_DIR=".flow/bin"` (for symlinks) + ## Step 1: Check .flow/ exists Check if `.flow/` directory exists (use Bash `ls .flow/` or check for `.flow/meta.json`). @@ -20,39 +26,91 @@ Check if `.flow/` directory exists (use Bash `ls .flow/` or check for `.flow/met {"schema_version": 2, "next_epic": 1} ``` -Also ensure `.flow/config.json` exists with defaults: +Also ensure `.flow/config.json` exists (empty to inherit user config): ```bash if [ ! -f .flow/config.json ]; then - echo '{"memory":{"enabled":false}}' > .flow/config.json + echo '{}' > .flow/config.json fi ``` ## Step 2: Check existing setup -Read `.flow/meta.json` and check for `setup_version` field. +Read `${PLUGIN_ROOT}/.claude-plugin/plugin.json` to get current plugin version. -Also read `${PLUGIN_ROOT}/.claude-plugin/plugin.json` to get current plugin version. +**For user-level mode:** +- Check `~/.config/flow-next/VERSION` for existing version +- If exists and same version: "Already up to date (v). Update anyway? (y/n)" +- If exists and different: "Updating from v to v" -**If `setup_version` exists (already set up):** -- If **same version**: tell user "Already set up with v. Re-run to update docs only? (y/n)" - - If yes: skip to Step 6 (docs) - - If no: done -- If **older version**: tell user "Updating from v to v" and continue +**For project-local mode:** +- Read `.flow/meta.json` and check for `setup_version` field +- If `setup_version` exists (already set up): + - If **same version**: tell user "Already set up with v. Re-run to update docs only? (y/n)" + - If yes: skip to Step 6 (docs) + - If no: done + - If **older version**: tell user "Updating from v to v" and continue +- If no `setup_version`: continue (first-time setup) -**If no `setup_version`:** continue (first-time setup) +## Step 3: Create directories -## Step 3: Create .flow/bin/ +**User-level mode:** +```bash +mkdir -p ~/.config/flow-next/bin +mkdir -p .flow/bin +``` +**Project-local mode:** ```bash mkdir -p .flow/bin ``` -## Step 4: Copy files +## Step 4: Copy/link files **IMPORTANT: Do NOT read flowctl.py - it's too large. Just copy it.** -Copy using Bash `cp` with absolute paths: +### User-level mode + +First, backup any modified files: +```bash +if [[ -d ~/.config/flow-next/bin ]]; then + BACKUP_DIR="$HOME/.config/flow-next/backups/$(date +%Y%m%dT%H%M%S)" + # Check if files differ from plugin + for f in flowctl flowctl.py; do + if [[ -f ~/.config/flow-next/bin/$f ]]; then + PLUGIN_HASH=$(md5sum "${PLUGIN_ROOT}/scripts/$f" | cut -d' ' -f1) + USER_HASH=$(md5sum ~/.config/flow-next/bin/$f | cut -d' ' -f1) + if [[ "$PLUGIN_HASH" != "$USER_HASH" ]]; then + mkdir -p "$BACKUP_DIR" + cp ~/.config/flow-next/bin/$f "$BACKUP_DIR/" + diff -u "${PLUGIN_ROOT}/scripts/$f" ~/.config/flow-next/bin/$f > "$BACKUP_DIR/$f.diff" 2>/dev/null || true + echo "Backed up: $f (modified)" + fi + fi + done +fi +``` + +Copy to user-level: +```bash +cp "${PLUGIN_ROOT}/scripts/flowctl" ~/.config/flow-next/bin/flowctl +cp "${PLUGIN_ROOT}/scripts/flowctl.py" ~/.config/flow-next/bin/flowctl.py +chmod +x ~/.config/flow-next/bin/flowctl +``` + +Create symlinks in project: +```bash +ln -sf ~/.config/flow-next/bin/flowctl .flow/bin/flowctl +ln -sf ~/.config/flow-next/bin/flowctl.py .flow/bin/flowctl.py +``` + +Update user-level VERSION: +```bash +echo "" > ~/.config/flow-next/VERSION +``` + +### Project-local mode +Copy using Bash `cp` with absolute paths: ```bash cp "${PLUGIN_ROOT}/scripts/flowctl" .flow/bin/flowctl cp "${PLUGIN_ROOT}/scripts/flowctl.py" .flow/bin/flowctl.py @@ -68,7 +126,8 @@ Read current `.flow/meta.json`, add/update these fields (preserve all others): ```json { "setup_version": "", - "setup_date": "" + "setup_date": "", + "setup_mode": "" } ``` @@ -118,6 +177,37 @@ Wait for response, then for each chosen file: ## Step 7: Print summary +**User-level mode:** +``` +Flow-Next setup complete! (user-level) + +Installed to ~/.config/flow-next/bin/: +- flowctl (v) +- flowctl.py + +Project symlinks in .flow/bin/ + +To use from command line: + export PATH="$HOME/.config/flow-next/bin:$PATH" + flowctl --help + +Documentation updated: +- + +Config hierarchy (project overrides user): + 1. ~/.config/flow-next/config.json (user defaults) + 2. .flow/config.json (project overrides) + +Set user-level defaults: + flowctl config set memory.enabled true --user + +Notes: +- Re-run /flow-next:setup --user after plugin updates +- Scripts shared across all projects +- Backups: ~/.config/flow-next/backups/ (if files were modified) +``` + +**Project-local mode:** ``` Flow-Next setup complete! @@ -134,7 +224,8 @@ Documentation updated: - Memory system: disabled by default -Enable with: flowctl config set memory.enabled true +Enable for this project: flowctl config set memory.enabled true +Enable for all projects: flowctl config set memory.enabled true --user Notes: - Re-run /flow-next:setup after plugin updates to refresh scripts