#!/usr/bin/env sh set -e if [ -t 1 ]; then RESET="\033[0m" GREEN="\033[32m" CYAN="\033[36m" YELLOW="\033[33m" GREY="\033[90m" else RESET="" GREEN="" CYAN="" YELLOW="" GREY="" fi say() { printf "%b\n" "$*"; } is_local_dev_base_url() { python3 - "$1" <<'PY' from urllib.parse import urlparse import sys host = (urlparse(sys.argv[1]).hostname or '').strip().lower() local_hosts = {'localhost', '127.0.0.1', '::1', '0.0.0.0', 'host.docker.internal'} print('1' if host in local_hosts or host.endswith('.localhost') else '0') PY } print_logo() { if [ -t 1 ]; then COLS="" if command -v tput >/dev/null 2>&1; then COLS=$(tput cols 2>/dev/null || true) fi if [ -z "$COLS" ]; then COLS=$(stty size 2>/dev/null | awk '{print $2}' || true) fi if [ -n "$COLS" ] && [ "$COLS" -ge 70 ]; then say " ██████╗ ███████╗ ███████╗ ██████╗ ██╗ ██╗ ███╗ ██╗ ███████╗" say " ██╔══██╗ ██╔════╝ ██╔════╝ ██╔══██╗ ██║ ██║ ████╗ ██║ ██╔════╝" say " ██║ ██║ █████╗ █████╗ ██████╔╝ ██║ ██║ ██╔██╗ ██║ █████╗" say " ██║ ██║ ██╔══╝ ██╔══╝ ██╔═══╝ ██║ ██║ ██║╚██╗██║ ██╔══╝" say " ██████╔╝ ███████╗ ███████╗ ██║ ███████╗ ██║ ██║ ╚████║ ███████╗" say " ╚═════╝ ╚══════╝ ╚══════╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═══╝ ╚══════╝" else say "${CYAN}DEEPLINE${RESET}" fi else say "DEEPLINE" fi } BASE_URL="https://code.deepline.com" REQUEST_BASE_URL="https://code.deepline.com" HOST_SLUG="code-deepline-com" CLI_INSTALL_ROOT="$HOME/.local/deepline/code-deepline-com" CLI_INSTALL_DIR="$HOME/.local/deepline/code-deepline-com/bin" CLI_INSTALL_STATE_DIR="$HOME/.local/deepline/code-deepline-com/cli" CLI_VERSION="0471c524a2aa805f558d2453252c120f01d6cac90822811c18965ea800382471" SKILLS_INSTALL_DIR="$HOME/.local/deepline/code-deepline-com/skills" SKILLS_VERSION="bab44a266baa365a9a4f24219065ef1b1e2c36136b61297b376543ea3fd85c1e" INSTALLER_MODE="$(printf "%s" "${DEEPLINE_INSTALLER_MODE:-}" | tr '[:upper:]' '[:lower:]')" # Resolve install directory: env override > localhost detection > default if [ -n "$DEEPLINE_INSTALL_DIR" ]; then INSTALL_DIR="$DEEPLINE_INSTALL_DIR" elif [ "$(is_local_dev_base_url "$BASE_URL")" = "1" ]; then INSTALL_DIR="$CLI_INSTALL_DIR" else INSTALL_DIR="$HOME/.local/bin" fi install_cli_python() { if command -v python3 >/dev/null 2>&1; then PYTHON_BIN="python3" elif command -v python >/dev/null 2>&1; then PYTHON_BIN="python" else return 1 fi TMP_BIN="$(mktemp "${TMPDIR:-/tmp}/deepline-python.XXXXXX")" if ! curl -fsSL "$BASE_URL/api/v2/cli/python" -o "$TMP_BIN"; then rm -f "$TMP_BIN" return 1 fi chmod +x "$TMP_BIN" mkdir -p "$INSTALL_DIR" CLI_REAL_BIN="$INSTALL_DIR/deepline-real" mv "$TMP_BIN" "$CLI_REAL_BIN" { printf '%s\n' '#!/usr/bin/env sh' printf '%s\n' 'export DEEPLINE_API_BASE_URL="https://code.deepline.com"' printf '%s\n' 'export DEEPLINE_CONFIG_SCOPE="code-deepline-com"' printf '%s\n' "export DEEPLINE_REAL_BINARY=\"$CLI_REAL_BIN\"" printf '%s\n' 'export DEEPLINE_INSTALL_METHOD="shiv"' printf '%s\n' 'exec "$DEEPLINE_REAL_BINARY" "$@"' } > "$INSTALL_DIR/deepline" chmod +x "$INSTALL_DIR/deepline" CLI_BIN="$INSTALL_DIR/deepline" mkdir -p "$CLI_INSTALL_STATE_DIR" printf "%s\n" "shiv" > "$CLI_INSTALL_STATE_DIR/.install-method" if [ -n "$CLI_VERSION" ]; then printf "%s\n" "$CLI_VERSION" > "$CLI_INSTALL_STATE_DIR/.version" else say "${YELLOW}CLI version unavailable in install script output; local CLI metadata was not written.${RESET}" fi return 0 } install_cli() { if ! install_cli_python; then say "${YELLOW}Python-based Deepline install failed.${RESET}" say "${YELLOW}This installer now requires python3 (or python) and /api/v2/cli/python to be reachable.${RESET}" exit 1 fi } install_cli # On Windows (Git Bash / MSYS2 / Cygwin), the bare zip cannot be executed # directly — no shebang support. Create .cmd wrappers so both cmd.exe and # the playground backend can find and invoke the CLI. create_windows_wrappers() { case "$(uname -s 2>/dev/null)" in MINGW*|MSYS*|CYGWIN*) ;; *) return 0 ;; esac # Resolve the real Python path (avoid WindowsApps App Execution Alias) WIN_PY="" for candidate in "$PYTHON_BIN" python3 python; do resolved="$(command -v "$candidate" 2>/dev/null || true)" case "$resolved" in *WindowsApps*) continue ;; "") continue ;; esac WIN_PY="$resolved" break done if [ -z "$WIN_PY" ]; then say "${YELLOW}Warning: could not resolve a non-alias Python path for .cmd wrapper.${RESET}" return 0 fi # Convert MSYS/Git Bash path to Windows path for the .cmd wrapper if command -v cygpath >/dev/null 2>&1; then WIN_PY_NATIVE="$(cygpath -w "$WIN_PY")" WIN_ZIP_NATIVE="$(cygpath -w "$CLI_BIN")" else WIN_PY_NATIVE="$WIN_PY" WIN_ZIP_NATIVE="$CLI_BIN" fi # 1. Wrapper next to the zip at ~/.local/bin/deepline.cmd printf "@\"%s\" \"%%~dp0deepline\" %%*\r\n" "$WIN_PY_NATIVE" > "$INSTALL_DIR/deepline.cmd" say "${GREEN}Created .cmd wrapper:${RESET} $INSTALL_DIR/deepline.cmd" # 2. Also create the layout the playground backend searches first: # %LOCALAPPDATA%\Deepline\bin\deepline.cmd if [ -n "$LOCALAPPDATA" ]; then if command -v cygpath >/dev/null 2>&1; then DL_BIN_DIR="$(cygpath "$LOCALAPPDATA")/Deepline/bin" else DL_BIN_DIR="$LOCALAPPDATA/Deepline/bin" fi mkdir -p "$DL_BIN_DIR" if command -v cygpath >/dev/null 2>&1; then DL_BIN_DIR_NATIVE="$(cygpath -w "$DL_BIN_DIR")" else DL_BIN_DIR_NATIVE="$DL_BIN_DIR" fi printf "@\"%s\" \"%s\" %%*\r\n" "$WIN_PY_NATIVE" "$WIN_ZIP_NATIVE" > "$DL_BIN_DIR/deepline.cmd" say "${GREEN}Created .cmd wrapper:${RESET} $DL_BIN_DIR/deepline.cmd" fi # Point CLI_BIN at the wrapper so auth register works in this session CLI_BIN="$INSTALL_DIR/deepline.cmd" } create_windows_wrappers say "${GREEN}✓ Deepline CLI installed${RESET} at $CLI_BIN" PATH_MARKER="# deepline-cli-path" detect_login_shell() { DETECTED_SHELL="" if command -v getent >/dev/null 2>&1; then DETECTED_SHELL=$(getent passwd "$(id -un)" 2>/dev/null | awk -F: '{print $7}') fi if [ -z "$DETECTED_SHELL" ] && command -v dscl >/dev/null 2>&1; then DETECTED_SHELL=$(dscl . -read "/Users/$(id -un)" UserShell 2>/dev/null | awk '{print $2}') fi if [ -z "$DETECTED_SHELL" ] && [ -n "$SHELL" ]; then DETECTED_SHELL="$SHELL" fi if [ -z "$DETECTED_SHELL" ]; then DETECTED_SHELL="/bin/sh" fi printf "%s" "${DETECTED_SHELL##*/}" } ensure_path() { # Already in PATH? Skip silently. case ":$PATH:" in *":$INSTALL_DIR:"*) return 0 ;; esac UPDATED_FILES="" SKIPPED_FILES="" ADDED_ANY="false" FOUND_MARKER_ANY="false" PRIMARY_RC="" SHELL_PATH_LINE="" TARGET_SHELL=$(detect_login_shell) add_path_line() { TARGET="$1" LINE="$2" TARGET_DIR="$(dirname "$TARGET")" if [ ! -f "$TARGET" ]; then if ! mkdir -p "$TARGET_DIR" 2>/dev/null; then if [ -z "$SKIPPED_FILES" ]; then SKIPPED_FILES="$TARGET" else SKIPPED_FILES="$SKIPPED_FILES, $TARGET" fi return 0 fi if ! touch "$TARGET" 2>/dev/null; then if [ -z "$SKIPPED_FILES" ]; then SKIPPED_FILES="$TARGET" else SKIPPED_FILES="$SKIPPED_FILES, $TARGET" fi return 0 fi fi if [ ! -r "$TARGET" ]; then say "${YELLOW} Could not verify PATH in shell profile $TARGET (not readable).${RESET}" if [ -z "$SKIPPED_FILES" ]; then SKIPPED_FILES="$TARGET" else SKIPPED_FILES="$SKIPPED_FILES, $TARGET" fi return 0 fi if grep -Fqs "$PATH_MARKER" "$TARGET"; then FOUND_MARKER_ANY="true" return 0 fi if [ ! -w "$TARGET" ]; then say "${YELLOW} Could not update shell profile (permission denied): $TARGET${RESET}" if [ -z "$SKIPPED_FILES" ]; then SKIPPED_FILES="$TARGET" else SKIPPED_FILES="$SKIPPED_FILES, $TARGET" fi return 0 fi if ! { echo "$PATH_MARKER" echo "$LINE" } >> "$TARGET" 2>/dev/null; then if [ -z "$SKIPPED_FILES" ]; then SKIPPED_FILES="$TARGET" else SKIPPED_FILES="$SKIPPED_FILES, $TARGET" fi return 0 fi ADDED_ANY="true" if [ -z "$UPDATED_FILES" ]; then UPDATED_FILES="$TARGET" else UPDATED_FILES="$UPDATED_FILES, $TARGET" fi } case "$TARGET_SHELL" in zsh) PRIMARY_RC="$HOME/.zshenv" SHELL_PATH_LINE="export PATH=\"$INSTALL_DIR:\$PATH\"" ;; bash) PRIMARY_RC="$HOME/.bash_profile" SHELL_PATH_LINE="export PATH=\"$INSTALL_DIR:\$PATH\"" ;; fish) PRIMARY_RC="$HOME/.config/fish/config.fish" SHELL_PATH_LINE="set -gx PATH \"$INSTALL_DIR\" \$PATH" ;; *) PRIMARY_RC="$HOME/.profile" SHELL_PATH_LINE="export PATH=\"$INSTALL_DIR:\$PATH\"" ;; esac add_path_line "$PRIMARY_RC" "$SHELL_PATH_LINE" export PATH="$INSTALL_DIR:$PATH" if [ "$ADDED_ANY" = "true" ]; then say " Added to PATH in ${CYAN}$UPDATED_FILES${RESET} — restart your shell to persist." elif [ "$FOUND_MARKER_ANY" != "true" ]; then say "${YELLOW}Could not verify PATH in shell profile.${RESET}" if [ -n "$SKIPPED_FILES" ]; then say "${YELLOW}Could not update shell profile (permission denied): $SKIPPED_FILES${RESET}" say "To persist PATH manually:" say " $SHELL_PATH_LINE" fi fi } install_claude_local_permissions() { if [ ! -t 1 ]; then return 0 fi if [ -z "$PYTHON_BIN" ]; then if command -v python3 >/dev/null 2>&1; then PYTHON_BIN="python3" elif command -v python >/dev/null 2>&1; then PYTHON_BIN="python" else return 0 fi fi say "" say " (only updates ~/.claude/settings.json)" say " Press Enter to keep current Claude settings, or type y then Enter to allow." printf "⏳ ${CYAN}Quick setup:${RESET} allow Claude to run common Deepline setup commands? [y/N]: " REPLY="" if [ -r /dev/tty ]; then IFS= read -r REPLY "$CLI_ENV_FILE" fi if grep -Eq "^DEEPLINE_SHARE_SESSION_USER_PROMPTS=" "$CLI_ENV_FILE"; then sed -i.bak "s/^DEEPLINE_SHARE_SESSION_USER_PROMPTS=.*/DEEPLINE_SHARE_SESSION_USER_PROMPTS=$PROMPT_SHARE_VALUE/" "$CLI_ENV_FILE" && rm -f "$CLI_ENV_FILE.bak" else printf "\nDEEPLINE_SHARE_SESSION_USER_PROMPTS=%s\n" "$PROMPT_SHARE_VALUE" >> "$CLI_ENV_FILE" fi if [ "$PROMPT_SHARE_VALUE" = "true" ]; then say "${GREEN}✓ Deepline prompt sharing enabled.${RESET}" else say "${CYAN}Deepline prompt sharing disabled.${RESET}" fi } # Skip shell profile modification for local-dev installs — use cli-env.sh instead if [ "$(is_local_dev_base_url "$BASE_URL")" != "1" ]; then ensure_path else export PATH="$INSTALL_DIR:$PATH" say " Activate in this shell: ${CYAN}source cli-env dev${RESET}" fi # Background: sync playground runtime say "${GREY} Syncing playground...${RESET}" ( if env DEEPLINE_SKIP_SELF_UPDATE=true DEEPLINE_INSTALLER_MODE=true "$CLI_BIN" backend refresh-runtime >/dev/null 2>&1; then say "${GREEN}✓ Playground synced.${RESET}" else say "${YELLOW} Playground sync failed. Retry: deepline backend refresh-runtime${RESET}" fi ) & BG_PG=$! # Background: install agent skills AGENTS="codex claude-code cursor" SKILLS_PACKAGE_URL="https://code.deepline.com/.well-known/skills/index.json" if command -v npx >/dev/null 2>&1; then say "${GREY} Installing agent skills...${RESET}" ( SKILLS_LOG="$(mktemp -t deepline-skills.XXXXXX)" if npx skills add "$SKILLS_PACKAGE_URL" --agents $AGENTS --global --yes --skill 'build-tam' --skill 'clay-to-deepline' --skill 'deepline-feedback' --skill 'deepline-gtm' --skill 'deepline-quickstart' --skill 'gtm-meta-skill' --skill 'linkedin-url-lookup' --skill 'niche-signal-discovery' --skill 'portfolio-prospecting' --skill 'workflow-hello-world' --full-depth >"$SKILLS_LOG" 2>&1 "$SKILLS_INSTALL_DIR/.version" else say "${YELLOW} Skills version unavailable; local skills metadata was not written.${RESET}" fi say "${GREEN}✓ Agent skills installed.${RESET}" else say "${YELLOW} Skill install failed. Log: $SKILLS_LOG${RESET}" fi ) & BG_SK=$! else BG_SK="" say "${YELLOW} npx not found — skipping skill install.${RESET}" say " • Install Node.js (includes npx): https://nodejs.org/" say " • Then run:" say " • npx skills add https://code.deepline.com/.well-known/skills/index.json --agents --global --yes --skill 'build-tam' --skill 'clay-to-deepline' --skill 'deepline-feedback' --skill 'deepline-gtm' --skill 'deepline-quickstart' --skill 'gtm-meta-skill' --skill 'linkedin-url-lookup' --skill 'niche-signal-discovery' --skill 'portfolio-prospecting' --skill 'workflow-hello-world' --full-depth" fi say "" say "⏳ ${CYAN}Connecting your account...${RESET}" export DEEPLINE_SKIP_SELF_UPDATE=true if [ "$INSTALLER_MODE" = "true" ]; then if ! "$CLI_BIN" auth register --no-wait /dev/null || true [ -n "$BG_SK" ] && { wait $BG_SK 2>/dev/null || true; } if [ "$INSTALLER_MODE" != "true" ]; then install_claude_local_permissions fi if [ -t 1 ]; then say "" say "${GREY} Launching quickstart next. Pick a workflow in the browser, or press Ctrl+C to skip.${RESET}" "$CLI_BIN" quickstart