Ctrl K
ComunidadGuías…

install.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/bin/sh
# tiguclaw 한 줄 설치 (macOS · Linux)
#
#   curl -fsSL https://raw.githubusercontent.com/tigu77/tiguclaw/main/install.sh | sh
#
# 하는 일: 전제 확인 → clone → npm ci → onboard 로 넘김.
# 하지 않는 일: 설정을 대신 정하지 않는다(대화형 onboard 가 그 자리다).
#
# ★`| sh` 는 stdin 이 **파이프**다 — 그대로 onboard 를 부르면 질문을 읽을 수 없어
#  마법사가 조용히 헛돈다. 그래서 아래에서 /dev/tty 를 되찾아 넘긴다(없으면 다음
#  명령을 안내하고 멈춘다 — 반쯤 설치된 상태로 두지 않는다).
set -eu

REPO_URL="https://github.com/tigu77/tiguclaw.git"
DIR="${TIGUCLAW_DIR:-$HOME/tiguclaw}"
MIN_NODE=20
# 자동 설치할 때 고를 LTS 계열 — 네이티브 미리빌드가 LTS 를 따라간다(README 와 같은 근거).
LTS_MAJOR="${TIGUCLAW_NODE_MAJOR:-22}"
NEED_NODE=0

say() { printf '%s\n' "$*"; }
die() { printf '\n🔴 %s\n' "$*" >&2; exit 1; }
# 붙여넣는 명령에 싣는 경로 — 작은따옴표로 감싼다(공백·`$`·백틱·`"` 가 있어도 그대로). 안의 `'` 는 `'\''` 로.
shq() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; }

say ""
say "=== Installing tiguclaw ==="
say "Install location: $DIR   (to change it: TIGUCLAW_DIR=/your/path)"
say ""

# ── 전제 ────────────────────────────────────────────────────────────────────
command -v git >/dev/null 2>&1 || die "git is not installed. Please install it first."

# ── 이미 있으면 덮지 않는다 — 업데이트는 update 의 일이다 ────────────────────
if [ -e "$DIR" ]; then
  if [ -d "$DIR/.git" ]; then
    # 전용 Node 로 설치했으면 그 셸엔 npx 가 없다 — 명령 안에 PATH 를 싣는다(아래 재시도 안내와 같은 이유, 2026-10-16 재검토 2차 P5).
    UPDATE_CMD="cd $(shq "$DIR") && npx tiguclaw update"
    if [ -x "$DIR/.node/bin/node" ]; then
      UPDATE_CMD="cd $(shq "$DIR") && PATH=$(shq "$DIR/.node/bin"):\"\$PATH\" npx tiguclaw update"
    fi
    die "tiguclaw is already installed in $DIR.
   To update:  $UPDATE_CMD
   (It stops the daemon, updates dependencies, rebuilds, and starts it again.)"
  fi
  die "$DIR already exists but is not a tiguclaw install. Use another path:
   TIGUCLAW_DIR=~/tiguclaw2 curl -fsSL <the URL above> | sh"
fi

# ★**Node 가 없으면 여기서 멈추지 않는다** (2026-09-09 정태님: *"일반 사용자들도 설치하기
#  시작했거든"*). 이 제품의 정체성은 «당신의 상시 AI 비서» 이고 그 대상은 개발자가 아니다.
#  «먼저 Node 20 이상을 설치하세요» 는 거기서 벽이다.
#
# ★그래도 **시스템은 안 건드린다.** `brew install node`·`apt install`·`winget install` 은
#  전역 변경이고 관리자 권한이 필요할 수 있으며, 무엇보다 **지울 때 같이 안 지워진다.**
#  대신 앱 폴더 안에 **이 설치본 전용 Node** 를 둔다:  <설치폴더>/.node
#  판정 3줄을 그대로 만족한다 — 지울 때 같이 지워지고 · 깨질 때 혼자 깨지고 ·
#  폴더만 보면 누구 것인지 안다. PATH 오염 0, 관리자 권한 0, 제거는 폴더 삭제.
#
# ★서비스 등록이 저절로 따라온다: `bin/daemon.mjs` 가 유닛에 굽는 것은 `process.execPath`
#  이므로, 아래에서 PATH 를 앞세워 onboard 를 돌리면 launchd/systemd 가 이 Node 를 가리킨다.
#  (배선을 새로 만들 필요가 없다 — 이미 그렇게 돼 있다.)
NODE_DIR="$DIR/.node"

node_ok() {
  command -v node >/dev/null 2>&1 || return 1
  command -v npm  >/dev/null 2>&1 || return 1
  _maj=$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null) || return 1
  [ "${_maj:-0}" -ge "$MIN_NODE" ] 2>/dev/null
}

# 공식 배포본을 받아 <설치폴더>/.node 에 푼다. **체크섬을 검증한다** — 실행 파일이다.
fetch_node() {
  _os=$(uname -s); _arch=$(uname -m)
  case "$_os" in
    Darwin) _o=darwin ;;
    Linux)  _o=linux ;;
    *) die "Automatic Node install isn't available for this OS ($_os) — please install Node ${MIN_NODE} or newer yourself (https://nodejs.org)." ;;
  esac
  case "$_arch" in
    arm64|aarch64) _a=arm64 ;;
    x86_64|amd64)  _a=x64 ;;
    *) die "Automatic Node install isn't available for this architecture ($_arch) — please download Node from https://nodejs.org." ;;
  esac

  # ★버전을 **손으로 박지 않는다.** 박아두면 낡고, 낡은 줄은 아무도 안 고친다.
  #  `latest-v<major>.x/SHASUMS256.txt` 는 그 LTS 계열의 **현재 파일 이름과 해시**를
  #  한 번에 준다 — 목록에서 우리 것을 골라내면 버전은 저절로 따라온다.
  _base="https://nodejs.org/dist/latest-v${LTS_MAJOR}.x"
  _tmp=$(mktemp -d) || die "Could not create a temporary folder."
  # ★`.tar.gz` 를 쓴다 — `.tar.xz` 는 리눅스 기본 tar 에 xz-utils 가 있어야 풀린다.
  curl -fsSL "$_base/SHASUMS256.txt" -o "$_tmp/SHASUMS256.txt" \
    || die "Could not fetch the Node release list — check your network ($_base)."
  _file=$(grep -E "node-v[0-9.]+-${_o}-${_a}\.tar\.gz\$" "$_tmp/SHASUMS256.txt" | awk '{print $2}' | head -1)
  [ -n "$_file" ] || die "No Node release found for this platform (${_o}-${_a})."
  _want=$(grep " $_file\$" "$_tmp/SHASUMS256.txt" | awk '{print $1}' | head -1)

  say "   Downloading: $_file"
  curl -fsSL "$_base/$_file" -o "$_tmp/$_file" || die "Node download failed ($_base/$_file)."

  if command -v sha256sum >/dev/null 2>&1; then _got=$(sha256sum "$_tmp/$_file" | awk '{print $1}')
  elif command -v shasum   >/dev/null 2>&1; then _got=$(shasum -a 256 "$_tmp/$_file" | awk '{print $1}')
  else die "No sha256 tool found, so the download can't be verified — unverified binaries are not installed."
  fi
  # ★검증 실패는 «다시 시도» 가 아니라 **중단**이다. 실행 파일을 받는 중이다.
  [ "$_got" = "$_want" ] || die "Node checksum mismatch — installation stopped.
   expected: $_want
   actual:   $_got"

  # ★**임시 자리에 푼다** (2026-09-09, 적대 검토 P3). 종전엔 clone 뒤에 받아서, 받기가
  #  어떤 이유로든 실패하면(목록 404·다운로드 끊김·체크섬 불일치·미지원 OS/arch·tar 실패)
  #  **clone 된 폴더만 남았다.** 그 사람은 Node 가 없어서 온 사람이라 안내하던 탈출구
  #  (`npx tiguclaw update`)를 실행할 수도 없었고, 재실행하면 «이미 설치돼 있습니다» 로
  #  막혔다 — 유일한 길 `rm -rf` 는 어디에도 안 적혀 있었다. 이 파일 머리말이 *"반쯤
  #  설치된 상태로 두지 않는다"* 고 적어둔 바로 그 상태다.
  #  ★그래서 **clone 보다 먼저** 받는다. 실패하면 아직 아무것도 안 만들었으니 그냥 끝난다.
  NODE_STAGE="$_tmp/node"
  mkdir -p "$NODE_STAGE" || die "Could not create a temporary folder."
  tar -xzf "$_tmp/$_file" -C "$NODE_STAGE" --strip-components=1 \
    || die "Could not extract Node."
  rm -f "$_tmp/$_file"
  "$NODE_STAGE/bin/node" -v >/dev/null 2>&1 \
    || die "The downloaded Node does not run on this machine ($_os-$_a)."
  say "✓ Private node $("$NODE_STAGE/bin/node" -v) ready (moving it into the install folder)"
}

# 받아둔 것을 설치 폴더 안으로 옮기고 PATH 를 앞세운다 — clone 뒤에 부른다.
place_node() {
  mv "$NODE_STAGE" "$NODE_DIR" || die "Could not move it to $NODE_DIR."
  rm -rf "$(dirname "$NODE_STAGE")"
  PATH="$NODE_DIR/bin:$PATH"; export PATH
  node_ok || die "The private Node was installed but does not run ($NODE_DIR/bin/node)."
  say "✓ Private node $(node -v) — $NODE_DIR (your system was not touched)"
}

if node_ok; then
  say "✓ node $(node -v) · git $(git --version | awk '{print $3}')"
else
  if command -v node >/dev/null 2>&1; then
    say "! Your node is $(node -v), but ${MIN_NODE} or newer is required."
  else
    say "! Node.js is not installed (${MIN_NODE} or newer is required)."
  fi
  say ""
  say "  tiguclaw can download its own Node into this install folder only:"
  # ★숫자를 정직하게 (적대 검토 P8): 내려받기 ≈50MB 지만 **푼 뒤 디스크는 ≈200MB**
  #  (실측 darwin-arm64 187MB · 파일 4,750개). 받는 양만 말하면 절반만 말한 것이다.
  say "    $NODE_DIR   (about 50 MB to download · about 200 MB installed · no admin rights · system PATH untouched)"
  say "  Deleting the install folder removes it as well."
  say ""
  # ★**묻는다.** 런타임을 받아 까는 일을 조용히 하지 않는다. 비대화형(파이프·CI)에서는
  #  묻지 못하므로, 그때는 명시 동의(TIGUCLAW_AUTO_NODE=1)가 있을 때만 진행한다.
  # ★**값을 본다** (적대 검토 P7). 종전엔 «설정됐나» 만 봐서 `TIGUCLAW_AUTO_NODE=0`·
  #  `false`·`no` 가 전부 «묻지 말고 받아라» 가 됐다 — 끄려고 0 을 넣은 사람이 정확히
  #  반대를 얻는다.
  _auto=$(printf '%s' "${TIGUCLAW_AUTO_NODE:-}" | tr 'A-Z' 'a-z')
  case "$_auto" in 0|false|no|off|n) _auto="" ;; esac
  if [ -z "$_auto" ]; then
    if [ -t 0 ]; then
      printf '  Download it? [Y/n] '; read -r _ans || _ans=""
    elif { : < /dev/tty; } 2>/dev/null; then
      printf '  Download it? [Y/n] '; read -r _ans < /dev/tty || _ans=""
    else
      die "Node ${MIN_NODE} or newer is required.
   Install it yourself: https://nodejs.org
   Or rerun with TIGUCLAW_AUTO_NODE=1 to download a private Node automatically."
    fi
    # ★거절을 **넓게** 받는다 (2026-09-09, 적대 검토 P6). 종전 `n|N|no|NO` 는 실측으로
    #  `No`·`nO`·`nope`·`아니오`·`아니요` 를 전부 **승낙**으로 읽었다 — 질문이 한국어이고
    #  대상이 비개발자인데 거절만 ASCII 4형태였다. 같은 판단을 하는 `install.ps1` 은
    #  `-match` 가 대소문자를 무시해 `No` 를 제대로 막았다(두 곳이 다르게 구현돼 있었다).
    #  ★애매하면 **안 받는 쪽**이 맞다: 잘못 멈추면 다시 돌리면 되고, 잘못 받으면 원치
    #   않은 50MB 다운로드가 이미 끝나 있다.
    _lower=$(printf '%s' "${_ans:-y}" | tr 'A-Z' 'a-z')
    case "$_lower" in
      n|no|nope|nah|q|quit|0|false|아니|아니오|아니요|싫어|취소)
        die "Installation stopped. Install Node ${MIN_NODE} or newer (https://nodejs.org) and run this again." ;;
    esac
  fi
  say ""
  say "→ Preparing a private Node…"
  fetch_node          # ★clone 전에 받는다 — 실패해도 아무것도 안 남는다(P3).
  NEED_NODE=1
fi

# ── 받기 · 설치 ─────────────────────────────────────────────────────────────
say ""
say "→ Downloading the code…"
git clone --quiet "$REPO_URL" "$DIR" || die "git clone failed — check your network or access."
cd "$DIR"

# 받아둔 전용 Node 를 설치 폴더 안으로 옮긴다(폴더가 먼저 있으면 clone 이 실패하므로 여기서).
# 여기서 PATH 를 앞세우면 아래 npm·onboard·서비스 등록이 전부 이 Node 를 쓴다.
if [ "$NEED_NODE" = "1" ]; then place_node; fi

# ★**안내하는 명령이 그 사람 손에서 실제로 돌아야 한다** (2026-09-09, 적대 검토 P5).
#  전용 Node 는 이 스크립트 프로세스 안에서만 PATH 에 오른다(프로필·setx 어디에도 안 남긴다
#  — 시스템을 안 건드린다는 약속이 그것이다). 그래서 아래 안내가 `npm run onboard` 라고
#  적으면 **그 사람 셸엔 npm 이 없다.** 문자열은 남는데 실행이 안 되는 상태였다.
# ★npm 절대경로만으로도 안 된다 (2026-10-16 아스트라 «초보자 설치 검토» 2번) — npm 의 첫 줄이
#  `#!/usr/bin/env node` 라 새 셸에선 `env: node: No such file or directory`(127)로 죽는다(실측).
#  그래서 붙여넣는 명령 **안에** 전용 Node 를 PATH 로 싣는다 — 프로필은 여전히 안 건드린다.
#  경로는 `shq` 로 감싼다(설치 폴더에 공백·`$` 가 있을 수 있다).
RETRY_CD="cd $(shq "$DIR")"
if [ "$NEED_NODE" = "1" ]; then
  NPM_SHOW="PATH=$(shq "$NODE_DIR/bin"):\"\$PATH\" npm"
  HOWTO_TAIL="
   (This install uses a private Node — to keep using it from your terminal, add this to your PATH:
      export PATH=\"$NODE_DIR/bin:\$PATH\"
    Even without it, you can update from chat with /update.)"
else
  NPM_SHOW="npm"
  HOWTO_TAIL=""
fi

say "→ Installing dependencies… (building native modules can take 1–2 minutes)"
# ★`--ignore-scripts=false` 를 **명시**한다 (2026-08-19 실사고). 사내 정책으로 npm 설정에
#  ignore-scripts=true 가 켜져 있으면 `npm ci` 는 **성공하는데** 네이티브 빌드가 아예 안 돌아
#  better_sqlite3.node 가 안 생긴다 → 데몬이 부팅마다 죽는다. 전역 정책은 안 건드리고
#  이 호출에만 붙인다(사용자가 설치를 직접 시작했고, 이 제품은 네이티브 없이는 못 뜬다).
if ! npm ci --no-audit --no-fund --ignore-scripts=false; then
  die "Dependency install failed.
   You may need build tools — Linux: build-essential + python3 / macOS: xcode-select --install
   Then retry:  $RETRY_CD && $NPM_SHOW ci --ignore-scripts=false"
fi

# ★설치가 "성공" 해도 **쓸 수 있는지는 별개다** (2026-08-19 실사고 — 윈도우).
#  `ignore-scripts=true`(사내 정책 등)면 `npm ci` 는 성공하는데 네이티브 빌드가 안 돌아
#  바인딩이 안 생긴다. 설치는 끝난 것처럼 보이고 데몬은 부팅마다 죽는다(실측 6회 연속).
#  종료코드는 "명령이 실패했나" 지 "결과가 쓸 만한가" 가 아니다 — 열어봐야 안다.
say "→ Checking native modules…"
if ! node bin/check-native.cjs >/dev/null 2>&1; then
  # ★알려주고 끝내지 않는다 — **스스로 한 번 고쳐본다**(사용자가 명령을 외우게 하지 않는다).
  say "   A native module failed to load — rebuilding…"
  npm rebuild better-sqlite3 --ignore-scripts=false >/dev/null 2>&1 || true
fi
if ! node bin/check-native.cjs >/dev/null 2>&1; then
  die "Cannot load the SQLite native module — the daemon would crash on every start.
   Build tools are required — Linux: build-essential + python3 / macOS: xcode-select --install
   Then:  $RETRY_CD && $NPM_SHOW rebuild better-sqlite3 --ignore-scripts=false"
fi

# ── onboard 로 넘김 (대화형) ────────────────────────────────────────────────
#
# ★터미널이 **있는지**가 아니라 **열리는지**로 판정한다 (실측 2026-08-11).
#  종전엔 `[ -r /dev/tty ]` 로 갈랐는데, 제어 터미널이 없는 환경(CI·컨테이너·
#  일부 SSH)에서 그 검사는 **true 를 주고 실제 열기는 실패**한다
#  (`/dev/tty: Device not configured`). 그러면 exec 가 죽으면서 안내문도 못 뿌리고
#  **종료코드 0 으로 조용히** 끝났다 — 의존성은 깔렸는데 다음 할 일을 아무도 모르는
#  상태. 존재 확인은 판정이 아니다. 열어보고 갈라야 한다.
#
# 그래서 `exec` 도 쓰지 않는다 — 껍데기를 남겨 둬야 실패했을 때 안내를 낼 수 있다.
say ""
RAN_ONBOARD=0
if [ -t 0 ]; then
  RAN_ONBOARD=1
  npm run onboard && exit 0
elif { : < /dev/tty; } 2>/dev/null; then
  RAN_ONBOARD=1
  npm run onboard < /dev/tty && exit 0
fi

# onboard 가 돌다가 멈췄으면(서비스가 안 떴거나 점검에 막는 문제) «설정만 남았다» 가 아니다 — 위에 적힌 문제를 고치고 다시 돌리면 된다.
if [ "$RAN_ONBOARD" = 1 ]; then
  say ""
  say "⚠️  Setup stopped before finishing — see the messages above, fix the problem, then run it again (your settings are kept):"
  say ""
  say "   $RETRY_CD && $NPM_SHOW run onboard"
  say ""
  exit 1
fi

say ""
say "✅ Code and dependencies are ready — only setup is left."
say ""
say "   $RETRY_CD && $NPM_SHOW run onboard"
say ""
say "   (The setup wizard walks you through choosing an LLM, entering keys, registering the service, and checking that it works.)$HOWTO_TAIL"
say ""