# tiguclaw 한 줄 설치 (Windows / PowerShell) # # irm https://raw.githubusercontent.com/tigu77/tiguclaw/main/install.ps1 | iex # # 하는 일: 전제 확인 → clone → npm ci → onboard 로 넘김. # 하지 않는 일: 설정을 대신 정하지 않는다(대화형 onboard 가 그 자리다). # # ★`irm | iex` 는 현재 콘솔에서 실행되므로 stdin 은 살아 있다(sh 판과 다른 점). # 대신 여기선 **관리자 권한을 요구하지 않는다** — 데몬 등록이 *사용자 수준* 예약작업 # (`Register-ScheduledTask … -RunLevel Limited`)이라서다. 2026-08-22 이전엔 HKCU Run # 키였는데, 그건 로그온 1회라 supervisor 가 없었다(죽으면 그대로 멈춤). $ErrorActionPreference = 'Stop' $RepoUrl = 'https://github.com/tigu77/tiguclaw.git' $Dir = if ($env:TIGUCLAW_DIR) { $env:TIGUCLAW_DIR } else { Join-Path $env:USERPROFILE 'tiguclaw' } $MinNode = 20 # 자동 설치할 때 고를 LTS 계열 — 네이티브 미리빌드가 LTS 를 따라간다(sh 판과 같은 값). $LtsMajor = if ($env:TIGUCLAW_NODE_MAJOR) { $env:TIGUCLAW_NODE_MAJOR } else { '22' } # ★`exit` 를 쓰지 않는다 (2026-10-18, 깨끗한 윈도우 실기). `irm | iex` 는 사용자 창 안에서 돌아서, `exit 1` 이 # **사용자의 PowerShell 창 자체를 닫았다** — 빨간 안내를 읽기도 전에 창이 사라졌다. 던져서 맨 아래 catch 가 받는다. function Die($msg) { Write-Host "`n[X] $msg" -ForegroundColor Red; throw 'TIGUCLAW_DIE' } try { Write-Host "" Write-Host "=== Installing tiguclaw ===" -ForegroundColor Cyan Write-Host "Install location: $Dir (to change it: `$env:TIGUCLAW_DIR='D:\tiguclaw')" Write-Host "" # Existing directory != finished installation. Do not overwrite a partial/unrelated checkout. $ExistingInstall = Test-Path -LiteralPath $Dir if ($ExistingInstall) { $packageFile = Join-Path $Dir 'package.json' if (!(Test-Path -LiteralPath $packageFile) -or !(Test-Path -LiteralPath (Join-Path $Dir '.git'))) { Die "Existing folder is not a complete tiguclaw checkout: $Dir. Keep it for recovery; inspect/rename it yourself or choose another TIGUCLAW_DIR, then rerun. Nothing was overwritten." } $existingPackage = Get-Content -LiteralPath $packageFile -Raw | ConvertFrom-Json if ($existingPackage.name -ne 'tiguclaw' -or !(Test-Path -LiteralPath (Join-Path $Dir 'bin\tiguclaw.mjs'))) { Die 'This is not a tiguclaw install. Choose another TIGUCLAW_DIR; the folder was not changed.' } # npm ci/build must not replace files held by a running daemon. Unknown is not stopped. try { $nodes = @(Get-CimInstance Win32_Process -Filter "Name = 'node.exe'" -ErrorAction Stop) $needle = [IO.Path]::GetFullPath($Dir).Replace('/', '\').ToLowerInvariant() foreach ($proc in $nodes) { # WMI does not expose cwd. A relative tiguclaw daemon entry cannot be proved unrelated. $unscopedDaemon = $proc.CommandLine -match '(?i)(?:^|[\s"])(?:[^\\/\s":]+[\\/])*(?:(?:dist[\\/])?src[\\/]index\.(?:js|ts)|bin[\\/]daemon\.mjs)(?:\s|$|")' if (!$proc.CommandLine -or $unscopedDaemon -or $proc.CommandLine.Replace('/', '\').ToLowerInvariant().Contains($needle) -or ($proc.ExecutablePath -and $proc.ExecutablePath.Replace('/', '\').ToLowerInvariant().StartsWith($needle + '\'))) { throw 'Running or uninspectable Node process' } } } catch { Die "Cannot establish that this install is stopped. No dependencies were changed. Use node.exe `"$Dir\bin\tiguclaw.mjs`" status / stop, then rerun. For a normal update use node.exe `"$Dir\bin\tiguclaw.mjs`" update. ($($_.Exception.Message))" } } # System Git first; opt-in app-private PortableGit includes Bash (MinGit does not). $GitRoot = Join-Path $env:LOCALAPPDATA 'tiguclaw\tools\git' $PrivateGit = Join-Path $GitRoot 'cmd\git.exe' $PrivateBash = Join-Path $GitRoot 'bin\bash.exe' function Enable-PrivateGit { $env:Path = (Join-Path $GitRoot 'cmd') + ";" + $env:Path # Bash is discovered by the CLI after the home .env has had its say. } $GitCmd = Get-Command git.exe -ErrorAction SilentlyContinue if (!$GitCmd) { if ((Test-Path -LiteralPath $PrivateGit) -and (Test-Path -LiteralPath $PrivateBash) -and (Test-Path -LiteralPath (Join-Path $GitRoot 'tiguclaw-portable-git.json'))) { & $PrivateGit --version | Out-Null if ($LASTEXITCODE -ne 0) { Die 'App-private Git is damaged. Inspect/rename it or install Git for Windows yourself, then rerun.' } & $PrivateBash --version | Out-Null if ($LASTEXITCODE -ne 0) { Die 'App-private Bash is damaged. Inspect/rename it or install Git for Windows yourself, then rerun.' } Enable-PrivateGit } else { if (Test-Path -LiteralPath $GitRoot) { Die "Incomplete private Git at $GitRoot. It was not overwritten. Inspect/rename it or install Git for Windows yourself, then rerun." } Write-Host "Git is missing. Prepare official PortableGit (Git + Bash) in $GitRoot?" Write-Host 'Only this app/process uses it; machine/user PATH and execution policy are not changed.' $consent = Read-Host 'Download, verify SHA256 and run the official PortableGit extractor? [y/N]' if ($consent -notmatch '^(?i:y|yes)$') { Die 'Git preparation declined. Install Git for Windows from https://git-scm.com/download/win and rerun. Settings are kept.' } $gitArch = $(if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }).ToLowerInvariant() $assetArch = switch ($gitArch) { 'arm64' { 'arm64' } 'amd64' { '64-bit' } default { Die "Unsupported PortableGit architecture: $gitArch" } } $release = Invoke-RestMethod 'https://api.github.com/repos/git-for-windows/git/releases/latest' -Headers @{ 'User-Agent' = 'tiguclaw-installer' } $assets = @($release.assets | Where-Object { $_.name -match "^PortableGit-[0-9.]+-$assetArch\.7z\.exe$" }) if ($assets.Count -ne 1) { Die 'Official PortableGit asset is missing/ambiguous. Retry later or install Git for Windows manually.' } $asset = $assets[0] if ($asset.digest -notmatch '^sha256:[a-fA-F0-9]{64}$' -or $asset.browser_download_url -notmatch '^https://github\.com/git-for-windows/git/releases/download/[a-zA-Z0-9._-]+/PortableGit-[0-9.]+-(64-bit|arm64)\.7z\.exe$') { Die 'Official PortableGit URL/SHA256 metadata is unavailable. Nothing will be executed; retry later.' } $download = Join-Path ([IO.Path]::GetTempPath()) ('tiguclaw-portable-git-' + [guid]::NewGuid().ToString('N') + '.exe') $stage = Join-Path (Split-Path $GitRoot -Parent) ('git-partial-' + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $stage -Force | Out-Null Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $download -UseBasicParsing $actual = (Get-FileHash -LiteralPath $download -Algorithm SHA256).Hash if ($actual -ine $asset.digest.Substring(7)) { Die 'PortableGit SHA256 mismatch. Extractor was NOT run. Rerun to download a fresh copy.' } $extract = Start-Process -FilePath $download -ArgumentList @('-y', ('-o"' + $stage + '"')) -Wait -PassThru if ($extract.ExitCode -ne 0) { Die "PortableGit extraction failed ($($extract.ExitCode)); rerun. Incomplete staging was not reused." } $stageGit = Join-Path $stage 'cmd\git.exe' $stageBash = Join-Path $stage 'bin\bash.exe' if (!(Test-Path -LiteralPath $stageGit) -or !(Test-Path -LiteralPath $stageBash)) { Die 'PortableGit extraction is incomplete; no tool path was enabled.' } & $stageGit --version | Out-Null if ($LASTEXITCODE -ne 0) { Die 'Extracted Git failed its version check.' } & $stageBash --version | Out-Null if ($LASTEXITCODE -ne 0) { Die 'Extracted Bash failed its version check.' } @{ asset = $asset.name; sha256 = $actual; source = $asset.browser_download_url } | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $stage 'tiguclaw-portable-git.json') -Encoding UTF8 [IO.Directory]::Move($stage, $GitRoot) Enable-PrivateGit Write-Host ' SHA256 verified; app-private Git + Bash ready.' } $GitCmd = Get-Command git.exe -ErrorAction Stop } $GitExe = $GitCmd.Source if ($ExistingInstall) { $origin = & $GitExe -C $Dir remote get-url origin if ($LASTEXITCODE -ne 0 -or $origin -notmatch '^(https://github\.com/tigu77/tiguclaw(?:\.git)?|git@github\.com:tigu77/tiguclaw(?:\.git)?)$') { Die 'Existing folder is not the official tiguclaw checkout. It was not changed; choose another TIGUCLAW_DIR.' } } Write-Host " git: $GitExe" # ★**Node 가 없으면 여기서 멈추지 않는다** — sh 판과 같은 이유·같은 방식이다(2026-09-09). # `winget install` 을 부르지 않는다: 전역 변경이고, 무엇보다 **지울 때 같이 안 지워진다.** # 대신 앱 폴더 안에 이 설치본 전용 Node 를 둔다: <설치폴더>\.node # ★서비스 등록(사용자 수준 예약작업)은 `bin/daemon.mjs` 가 `process.execPath` 를 굽기 # 때문에 저절로 이 Node 를 가리킨다 — 배선을 새로 만들 필요가 없다. $NodeDir = Join-Path $Dir '.node' $NeedNode = $false function Test-NodeOk { if (-not (Get-Command node.exe -ErrorAction SilentlyContinue)) { return $false } if (-not (Get-Command npm.cmd -ErrorAction SilentlyContinue)) { return $false } try { $maj = [int]((((node.exe --version) -replace '^v','') -split '\.')[0]) } catch { return $false } return ($maj -ge $MinNode) } function Install-PrivateNode { $arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'x64' } # 버전을 손으로 박지 않는다 — `latest-v.x/SHASUMS256.txt` 가 파일 이름과 해시를 같이 준다. $base = "https://nodejs.org/dist/latest-v$LtsMajor.x" $tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("tiguclaw-node-" + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $tmp -Force | Out-Null try { $sums = (Invoke-WebRequest -UseBasicParsing "$base/SHASUMS256.txt").Content -split "`n" $line = $sums | Where-Object { $_ -match "node-v[\d.]+-win-$arch\.zip\s*$" } | Select-Object -First 1 if (-not $line) { Die "No Node release found for this platform (win-$arch)." } $parts = ($line -split '\s+') | Where-Object { $_ -ne '' } $want = $parts[0]; $file = $parts[1] Write-Host " Downloading: $file" $zip = Join-Path $tmp $file Invoke-WebRequest -UseBasicParsing "$base/$file" -OutFile $zip # ★검증 실패는 «다시 시도» 가 아니라 중단이다 — 실행 파일을 받는 중이다. $got = (Get-FileHash -Algorithm SHA256 $zip).Hash.ToLower() if ($got -ne $want.ToLower()) { Die "Node checksum mismatch — installation stopped.`n expected: $want`n actual: $got" } Expand-Archive -Path $zip -DestinationPath $tmp -Force $inner = Get-ChildItem -Path $tmp -Directory | Where-Object { $_.Name -like 'node-v*' } | Select-Object -First 1 if (-not $inner) { Die "Could not find the folder inside the Node archive." } # ★**임시 자리에 둔다** (2026-09-09, 코드 리뷰). `install.sh` 는 같은 날 이 문제를 # 고쳤는데 이 파일만 그대로였다 — clone 뒤에 받으면 받기가 어떤 이유로든 실패할 때 # (목록 404·다운로드 끊김·체크섬 불일치·압축 폴더 없음·실행 실패) **clone 된 폴더만 # 남고**, 재실행은 «이미 설치돼 있습니다 → npx tiguclaw update» 로 막힌다. 그 사람은 # 정의상 node·npx 가 없어서 온 사람이라 그 안내를 실행할 수 없다. # ★clone 보다 먼저 받으면 실패해도 아직 아무것도 안 만들었으니 그냥 끝난다. $script:NodeStage = $inner.FullName $script:NodeStageTmp = $tmp if (-not (Test-Path (Join-Path $script:NodeStage 'node.exe'))) { Die "The downloaded Node has no node.exe." } } catch { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue throw } Write-Host "[v] Private node ready (moving it into the install folder)" -ForegroundColor Green } # 받아둔 것을 설치 폴더 안으로 옮기고 PATH 를 앞세운다 — clone 뒤에 부른다. function Move-PrivateNode { New-Item -ItemType Directory -Path $NodeDir -Force | Out-Null Copy-Item -Path (Join-Path $script:NodeStage '*') -Destination $NodeDir -Recurse -Force Remove-Item -Recurse -Force $script:NodeStageTmp -ErrorAction SilentlyContinue $env:PATH = "$NodeDir;$env:PATH" if (-not (Test-NodeOk)) { Die "The private Node was installed but does not run ($NodeDir\node.exe)." } Write-Host "[v] Private node $(node.exe -v) — $NodeDir (your system was not touched)" -ForegroundColor Green } # A fresh PowerShell does not inherit the previous installer's process PATH. if ($ExistingInstall -and (Test-Path -LiteralPath (Join-Path $NodeDir 'node.exe'))) { $env:PATH = "$NodeDir;$env:PATH" } if (Test-NodeOk) { Write-Host "[v] node $(node.exe -v) · git $((& $GitExe --version).Split(' ')[2])" } else { if (Get-Command node.exe -ErrorAction SilentlyContinue) { Write-Host "! Your node is $(node.exe -v), but $MinNode or newer is required." -ForegroundColor Yellow } else { Write-Host "! Node.js is not installed ($MinNode or newer is required)." -ForegroundColor Yellow } Write-Host "" Write-Host " tiguclaw can download its own Node into this install folder only:" Write-Host " $NodeDir (about 50 MB to download · about 200 MB installed · no admin rights · system PATH untouched)" Write-Host " Deleting the install folder removes it as well." Write-Host "" # ★묻는다 — 런타임을 받아 까는 일을 조용히 하지 않는다. # ★**값을 본다**(2026-09-09, 코드 리뷰). 종전 `-not $env:…` 는 «설정됐나» 만 봤고, # PowerShell 은 비어 있지 않은 문자열을 전부 참으로 보므로 `0`·`false`·`no` 가 모두 # «묻지 말고 받아라» 였다 — 끄려고 0 을 넣은 사람이 정확히 반대를 얻는다. # `install.sh` 는 같은 날 고쳤는데 이 파일만 그대로였다. if ($env:TIGUCLAW_AUTO_NODE -notmatch '^(1|true|yes|on|y)$') { $ans = Read-Host " Download it? [Y/n]" # ★거절을 **넓게** 받는다 — 질문이 한국어인데 거절만 ASCII 2형태였다. # 애매하면 안 받는 쪽이 맞다: 잘못 멈추면 다시 돌리면 되고, 잘못 받으면 200MB 가 # 이미 내려와 있다. if ($ans -match '^\s*(n|no|nope|nah|q|quit|0|false|아니|아니오|아니요|싫어|취소)\s*$') { Die "Installation stopped. Install Node $MinNode or newer (winget install OpenJS.NodeJS.LTS) and run this again." } } $NeedNode = $true Write-Host "" Write-Host "-> Downloading a private Node..." -ForegroundColor Cyan Install-PrivateNode # ★clone 전에 받는다 — 실패해도 아무것도 안 남는다. } # ★npm 은 **npm.cmd** 로 부른다 (2026-08-19 실사고). # PowerShell 에서 `npm` 을 부르면 `npm.ps1` 이 잡히는데, 실행 정책이 기본 잠금인 윈도우에서는 # 그 파일을 **로드하지 못해** 설치가 통째로 멈춘다("이 시스템에서 스크립트를 실행할 수 # 없으므로 npm.ps1 파일을 로드할 수 없습니다"). `npm.cmd` 는 배치 파일이라 정책 대상이 아니다. # ★사용자에게 `Set-ExecutionPolicy` 를 시키지 않는다 — 설치 하나 하려고 시스템 보안 설정을 # 바꾸게 하는 건 우리가 할 말이 아니고, 우리가 부르는 방식만 바꾸면 되는 일이다. # ★해석은 **전용 Node 를 깐 뒤**에 한다 (적대 검토 P2). 여기서 정하면 그 시점엔 전용 Node 가 # 아직 없어 `$Npm = "npm"` 으로 굳고, 나중에 Node 를 깔아도 그대로라 `npm.ps1` 을 잡는다 — # 바로 위 주석이 막겠다고 적어둔 2026-08-19 사고가 그대로 재발한다. # ★**여기 있던 버전 검사 3줄을 지웠다** (2026-09-09, 적대 검토 P1). 위 `Test-NodeOk` 가 # 이미 같은 판정을 하는데 옛 검사가 남아 **두 벌**이 됐고, 그게 치명적이었다: 이 줄에 # 도달하는 사람은 정의상 **node 가 없어서** 온 사람인데 `node --version` 을 맨몸으로 불렀다. # `$ErrorActionPreference='Stop'` 이라 CommandNotFound 로 죽고, 넘어가도 `[int]('')` 가 던지고, # 0 이면 "지금 " 이라는 빈 자기모순 메시지로 죽는다 — **세 갈래 전부 clone 전에 중단**이라 # 윈도우에서 전용 Node 기능이 **0% 작동**했다. # ★함정을 몰라서가 아니다 — 42행의 같은 호출은 `try{}catch{}` 안에 있다. **새 갈래에서만** # 빠뜨렸다. 같은 판단을 두 곳에 두면 한쪽이 낡는다. # (원래 이 자리가 막던 PowerShell 5.1 인용 함정은 `Test-NodeOk` 안으로 옮겨가 있다.) # A verified stopped checkout resumes without pull/reset. A folder is not a completion marker. Write-Host "" if ($ExistingInstall) { Write-Host "-> Resuming existing checkout (settings are kept): $Dir" } else { Write-Host "-> Downloading the code..." & $GitExe clone --quiet $RepoUrl $Dir if ($LASTEXITCODE -ne 0) { Die "git clone failed. Keep any partial folder for recovery or choose another TIGUCLAW_DIR and rerun." } } Set-Location $Dir # ★전용 Node 는 clone 뒤에 받는다 — 폴더가 먼저 있으면 `git clone` 이 실패한다. # 여기서 PATH 를 앞세우면 아래 npm·onboard·서비스 등록이 전부 이 Node 를 쓴다. if ($NeedNode) { Move-PrivateNode } # ★**안내하는 명령이 그 사람 손에서 실제로 돌아야 한다** (2026-09-09, 코드 리뷰). # 전용 Node 는 이 프로세스 안에서만 PATH 에 오른다(프로필·setx 어디에도 안 남긴다 — # 시스템을 안 건드린다는 약속이 그것이다). 그래서 아래 안내가 `npm ci` 라고 적으면 # **그 사람 셸엔 npm 이 없다.** 문자열은 남는데 실행이 안 되는 상태였다. # `install.sh` 는 같은 날 고쳤는데 이 파일만 그대로였다. if ($NeedNode) { $NpmShow = Join-Path $NodeDir 'npm.cmd' $NodeDirLiteral = "'" + $NodeDir.Replace("'", "''") + "'" $HowtoTail = "`n (This install uses a private Node — to keep using it from your terminal, add this to your PATH:`n `$env:PATH = $NodeDirLiteral + [IO.Path]::PathSeparator + `$env:PATH`n Even without it, you can update from chat with /update.)" } else { $NpmShow = 'npm.cmd' $HowtoTail = '' } # Copyable in a fresh PowerShell: literal paths, plus private Node for npm's children. $RetryBase = "Set-Location -LiteralPath '" + $Dir.Replace("'", "''") + "'; " if ($NeedNode) { $RetryBase += "`$env:PATH = $NodeDirLiteral + [IO.Path]::PathSeparator + `$env:PATH; " } $RetryBase += "& '" + $NpmShow.Replace("'", "''") + "'" # ★이제서야 npm 을 해석한다 — 전용 Node 를 깔았으면 그쪽 `npm.cmd` 가 잡혀야 한다(P2). $Npm = (Get-Command npm.cmd -ErrorAction Stop).Source Write-Host "-> 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 가 안 생긴다 -> 데몬이 부팅마다 죽는다. 전역 정책은 안 건드리고 # 이 호출에만 붙인다(사용자가 설치를 직접 시작했고, 이 제품은 네이티브 없이는 못 뜬다). & $Npm ci --no-audit --no-fund --ignore-scripts=false if ($LASTEXITCODE -ne 0) { Die @" Dependency install failed. You may need the C++ build tools: winget install Microsoft.VisualStudio.2022.BuildTools --override "--wait --quiet --add Microsoft.VisualStudio.Workload.VCTools" Then retry: $RetryBase ci --ignore-scripts=false "@ } # ★설치가 "성공" 해도 **쓸 수 있는지는 별개다** (2026-08-19 실사고). # 사내 정책으로 `ignore-scripts=true` 가 켜진 머신에서 `npm ci` 는 멀쩡히 성공하는데 # 네이티브 빌드 스크립트가 아예 안 돌아 `better_sqlite3.node` 가 안 생긴다. 그러면 # 설치는 끝난 것처럼 보이고 데몬은 **부팅할 때마다 죽는다**(실측: 6회 연속 크래시). # 종료코드는 "명령이 실패했나" 지 "결과가 쓸 만한가" 가 아니다 — 열어봐야 안다. # ★우리 클린룸 검증(sync 스킬 §7)은 이미 이 확인을 하고 있었다. 정작 **사용자가 돌리는 # 스크립트**에만 없었다 — 우리 설치는 검증하고 사용자 설치는 안 하고 있었던 셈이다. Write-Host "-> Checking native modules..." # ★이 구간만 Continue (2026-10-16 재검토 2차 P4) - Windows PowerShell 5.1 은 Stop 아래서 **리디렉션한 네이티브 stderr**(`2>$null`)를 # 종료 오류로 바꾼다. 점검이 실제로 DB 를 열게 되면서 실패 때 stderr 가 나오고, 그러면 아래 rebuild·안내에 닿기 전에 스크립트가 죽는다. # 판정은 종료코드($LASTEXITCODE)로 한다. $ErrorActionPreference = 'Continue' node.exe bin/check-native.cjs 2>$null if ($LASTEXITCODE -ne 0) { # ★알려주고 끝내지 않는다 - **스스로 한 번 고쳐본다**(사용자가 명령을 외우게 하지 않는다). Write-Host " A native module failed to load - rebuilding..." & $Npm rebuild better-sqlite3 --ignore-scripts=false 2>$null | Out-Null node.exe bin/check-native.cjs 2>$null if ($LASTEXITCODE -ne 0) { Die @" Cannot load the SQLite native module - the daemon would crash on every start. The C++ build tools are required: winget install Microsoft.VisualStudio.2022.BuildTools --override "--wait --quiet --add Microsoft.VisualStudio.Workload.VCTools" Then: $RetryBase rebuild better-sqlite3 "@ } Write-Host " Native module repaired." } $ErrorActionPreference = 'Stop' # ── onboard 로 넘김 (대화형) ──────────────────────────────────────────────── Write-Host "" & $Npm run onboard if ($LASTEXITCODE -ne 0) { Die "Setup is not finished. Settings are kept; rerun this installer after fixing the problem above." } } catch { # Die 는 이미 안내를 찍었다. 그 밖의 오류(네트워크 끊김 등)는 날것의 PowerShell 오류 대신 한 줄로. if ("$_" -ne 'TIGUCLAW_DIE') { Write-Host "`n[X] Installation stopped: $($_.Exception.Message)" -ForegroundColor Red Write-Host " If this is a network problem, check your connection and run the same command again." } # 파일로 실행했을 때만 종료 코드를 남긴다 — `irm | iex` 에서 exit 는 사용자 창을 닫는다. if ($PSCommandPath) { exit 1 } }