#!/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 | 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.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 ""