npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@2tl/hangar

v0.2.0

Published

Hangar CLI — publish static sites to Hangar, activate them in preview and roll back, from a terminal

Readme

Hangar CLI (@2tl/hangar)

Hangar에 정적 사이트를 발행·전환·롤백하는 커맨드라인 도구다. 터미널에서 로그인 한 번으로 이후의 모든 명령이 그 워크스페이스·프로젝트·환경에 묶인다.

AI 코딩 도구(Claude Code, Cursor 등)에서 쓰려면 같은 기능을 MCP 서버로 제공하는 @2tl/hangar-mcp를 보라. 그 서버는 항상 버전을 고정해 npx -y @2tl/[email protected] 으로 등록하고, AI 도구를 프로젝트 폴더 안에서 실행하거나 HANGAR_ALLOWED_ROOTS 를 설정해야 한다(그 밖의 폴더는 열지 않는다). 키는 아래 hangar login --workspace-key --token-stdin 으로 CLI 프로필(0600 파일)에 두고 MCP 에는 --profile <이름> 만 넘기는 것을 권장한다.

설치

Node 22 이상이 필요하다. 버전은 항상 고정한다 — 업데이트는 버전을 올려서 직접 / update by bumping the version deliberately.

npm i -g @2tl/[email protected]
hangar --help

설치 없이 한 번만 쓰려면 npx -y @2tl/[email protected] <command>. 설치되는 명령 이름은 hangar 하나다. npx hangar·npx appctl 은 쓰지 않는다 — npm 의 hangar·appctl 은 다른 사람의 패키지다. 자격증명은 ~/.hangar/credentials.json(권한 0600)에 로컬로 저장된다.

발행할 때 빌드 산출물에서 .env*(.env.example/.env.sample 제외), 개인 키·인증서, .ssh·.aws 같은 자격증명 폴더, known_hosts·*.kdbx·*.tfstate·service-account*.json 등, 그리고 1 MB 이하 텍스트 파일 안의 PEM 개인 키·Hangar 토큰·service_role JWT 를 찾으면 발행을 거절하고 해당 경로를 모두 알려 준다. *.sqlite·*.sqlite3·*.db 는 기본 거절이며, 공개해도 되는 파일이면 --allow-database-files 로 허용한다.

API 주소는 기본값이 공개 API https://app-api.2tl.io이므로 --api-url은 생략해도 된다(아래 예시에는 명시적으로 적었다). 발행된 사이트는 https://<slug>.apps.2tl.io, preview는 https://<slug>-preview.apps.2tl.io에서 열린다.

로그인

hangar login --api-url https://app-api.2tl.io \
  --workspace <workspace-uuid> --project-id <project-uuid> \
  --environment production --token-stdin < token.txt

토큰은 인자나 환경변수로 받지 않는다. --token-stdin으로 표준입력에서만 읽으며, echo "$HANGAR_TOKEN" | hangar login ... --token-stdin 형태로도 쓸 수 있다. API에 검증을 마친 뒤에만 로컬에 저장한다.

  • --workspace, --project-id는 콘솔의 프로젝트 페이지에서 확인한다.
  • --environment는 preview 또는 production이다.
  • 토큰 자체는 콘솔에서 발급하는 CLI 키다. 콘솔 → Hosting → 워크스페이스 → "Access tokens" (/hosting/<workspaceId>/tokens)에서 프로젝트·환경·만료·범위(project:read, release:publish, release:activate)를 골라 발급하며, 값은 발급 직후 한 번만 표시된다. publish에는 release:publish, activate/rollback/pause에는 release:activate가 필요하고, 범위가 없는 명령은 API가 거부한다.

로그인 뒤에는 아래 두 명령으로 현재 세션을 확인·정리한다.

hangar whoami   # 저장된 토큰을 API로 재검증하고 워크스페이스/프로젝트/환경을 출력
hangar logout   # 로컬 자격증명만 제거한다. 서버의 토큰은 회수하지 않는다

프로필 — 프로젝트·환경마다 로그인 바꿔가며 쓰기

하나의 로그인은 워크스페이스·프로젝트·환경 하나에 묶인다. 같은 프로젝트의 preview 와 production 을 오가거나 프로젝트 두 개를 동시에 만지는 사람은 --profile <이름> 으로 로그인을 여러 개 이름 붙여 저장하고 바꿔가며 쓴다.

hangar login --profile game-preview --api-url https://app-api.2tl.io \
  --workspace <workspace-uuid> --project-id <project-uuid> \
  --environment preview --token-stdin < preview-token.txt
hangar login --profile game-prod --api-url https://app-api.2tl.io \
  --workspace <workspace-uuid> --project-id <project-uuid> \
  --environment production --token-stdin < prod-token.txt

hangar profiles           # 저장된 프로필 목록 — 이름, 프로젝트, 환경, api, 현재 표시(*)
hangar use game-prod      # 이후 명령이 기본으로 쓰는 프로필을 바꾼다
hangar whoami              # 현재 프로필의 로그인을 재검증하고, 프로필 이름도 함께 보여준다
hangar publish --profile game-preview --project . --idempotency-key <uuid>  # 바꾸지 않고 한 번만
hangar logout --profile game-preview   # 그 프로필만 로그아웃
hangar logout --all                    # 저장된 모든 프로필을 한 번에 로그아웃
  • --profile <이름> 은 로그인/조회 계열 명령 어디에나 붙일 수 있고, 현재 프로필을 바꾸지 않고 그 프로필로 한 번만 실행한다.
  • HANGAR_PROFILE 환경변수도 같은 효과다 — --profile 이 더 우선한다.
  • --profile 없이 쓰는 login/logout/whoami 는 이전과 똑같이 동작한다. 프로필을 한 번도 안 써봤다면 credentials.json 의 모양도 그대로다 — 프로필 개념은 "default" 라는 이름 뒤에 숨어 있을 뿐이다.
  • hangar profiles 는 저장된 토큰 값은 절대 보여주지 않는다.

자격증명 파일은 앱 CLI와 공유한다

~/.hangar/credentials.json 은 Hangar CLI 들이 슬롯으로 나눠 쓴다 — hangar 은 "web", 앱(OTA) 쪽 ota CLI 는 "app". 각 CLI 는 자기 슬롯만 읽고 쓰며 다른 최상위 키는 건드리지 않으므로, 한 컴퓨터에서 둘 다 로그인해 둘 수 있고 한쪽 logout 이 다른 쪽을 로그아웃시키지 않는다(파일은 마지막 슬롯이 없어질 때만 지워진다).

이전 형식은 자동으로 옮겨진다. 2026-09-25 이전의 hangar 은 다섯 필드를 파일 최상위에 평평하게 썼고 로그인할 때마다 파일 전체를 덮어써서, ota login 뒤에 hangar login 을 하면 앱 자격증명이 사라졌다. 그 형식의 파일은 hangar 이 처음 읽을 때 "web" 아래로 옮겨진다 — 이미 로그인해 둔 사람은 아무것도 할 필요가 없고, 다시 로그인하지 않아도 된다.

각 슬롯("web", "app") 안에서도 이제 프로필을 여러 개 담을 수 있다. --profile 을 한 번도 안 썼다면 슬롯 값은 예전처럼 자격증명 하나를 그대로 담고 있고("default" 프로필 하나뿐인 상태와 같다), --profile <이름> 으로 두 번째 프로필을 만든 순간부터만 슬롯이 { "current": "<이름>", "profiles": { "default": {...}, "<이름>": {...} } } 모양으로 바뀐다. 예:

{
  "web": {
    "current": "game-prod",
    "profiles": {
      "default": { "apiUrl": "...", "workspaceId": "...", "projectId": "...", "environment": "preview", "token": "..." },
      "game-prod": { "apiUrl": "...", "workspaceId": "...", "projectId": "...", "environment": "production", "token": "..." }
    }
  },
  "app": { "apiUrl": "...", "workspaceId": "...", "appId": "...", "environment": "preview", "token": "..." }
}

자동화 키 — 콘솔 없이 프로젝트 만들기부터 preview 까지

워크스페이스 owner 가 콘솔(워크스페이스 → 키 → 자동화 키)에서 발급하는 hgw_ 키 하나로, 프로젝트 생성·데이터 계층 켜기·발행·preview 활성화까지 CLI 에서 끝낸다. production 활성화는 발급 때 "production 활성화 허용"을 체크한 키만 가능하고(기본 꺼짐), service_role 키·preview 비밀번호·데이터 끄기/키 회전은 어떤 키로도 안 된다 — 콘솔 전용이다. 키는 최대 90일 뒤 만료되고, 키마다 만들 수 있는 프로젝트 수에 상한(기본 10)이 있다.

로그인

콘솔에서 복사한 키를 클립보드에서 바로 넘긴다 — 셸 히스토리에도, 프로젝트 안 파일에도 남지 않는다 (Linux: xclip -selection clipboard -o |, Windows PowerShell: Get-Clipboard |).

pbpaste | hangar login --workspace-key --api-url https://app-api.2tl.io \
  --workspace <workspace-uuid> --profile ws --token-stdin
hangar whoami --profile ws   # 키 이름, 만료(7일 이내면 경고), 생성 n/상한, production 허용 여부
  • --project-id/--environment 는 주지 않는다(주면 에러). 키 하나가 닿는 모든 프로젝트를 두 환경에서 쓴다 — 명령마다 고른다.
  • 검증은 GET …/automation-keys/self. 토큰은 여전히 표준입력으로만 받는다.

hangar project create <slug> [--data] [--name <text>] [--project <dir>]

cd my-vite-app
hangar project create demo-auto --data      # 1회차: 프로젝트·데이터·.env.local·hangar.json
npm run build
hangar project create demo-auto --data      # 같은 명령 다시: publish + preview 활성화

단계마다 이미 된 것은 건너뛰므로 중간에 끊기거나 빌드를 마친 뒤에는 같은 명령을 다시 치면 이어서 진행한다.

| 단계 | 하는 일 | 이미 돼 있으면 | | --- | --- | --- | | project | 키가 닿는 프로젝트 중 slug 로 찾고, 없으면 POST …/projects | 재사용(생성 수 안 늘어남) | | profile | 토큰 없는 파생 프로필 <slug>(충돌하면 <slug>@<ws>) 저장. 부모 키를 빌려 쓴다 | 그대로 | | data-enable | --data 면 preview·production 둘 다 켜기 | 건너뜀 | | data-ready | 둘 다 ready 까지 대기(2초→10초 간격, 5분 상한) | 건너뜀 | | env-file | preview anon 키를 .env.local 에(Vite 프로젝트면 VITE_HANGAR_* 도). 다른 줄은 보존 | 값이 같으면 안 씀 | | manifest | hangar.json 이 없으면 init(name=slug, --data 면 compatibility.profile) | 덮어쓰지 않음 | | publish | outputDir/entrypoint 가 있으면 발행 | 같은 빌드면 같은 릴리스(멱등) | | activate | preview 에 활성화 | 이미 serving 이면 건너뜀 |

  • 빌드는 하지 않는다. 산출물이 없으면 "빌드 후 같은 명령 다시"로 끝난다.
  • 이번 실행에서 .env.local 이 바뀌었는데 산출물이 이미 있으면, 그 빌드에는 키가 없을 수 있으므로 발행하지 않고 "다시 빌드" 를 안내한다(--publish-anyway 로 강행).
  • 끝나면 preview 주소(https://<slug>-preview.…)를 출력한다. preview 는 Basic 인증 뒤에 있고 비밀번호는 콘솔(프로젝트 → preview 접속, owner)에서만 본다.
  • production 키는 .env.local 에 쓰지 않는다 — Vite 에서 .env.production.local 이 이기므로 preview 빌드가 production DB 를 가리키게 된다. 대신 production 절차 (data keys --environment production --write-env .env.production.local → 빌드 → publish --environment production → target --environment production)를 명령으로 출력한다. production 활성화 명령은 출력하지 않는다 — 이 안내는 AI 에이전트가 그대로 실행하는 다음 단계이기 때문이다. 키가 허용하더라도 production 전환은 사람이 콘솔(https://console.apps.2tl.io, 프로젝트 → 릴리스)에서 한다.
  • --json 은 { ok, projectId, slug, profile, stage, previewUrl, releaseId, productionAllowed, steps: [{ step, status: done|skipped|failed, detail }], next }. 실패하면 에러 봉투의 details.step/details.steps 에 어디까지 갔는지 담긴다.
  • 실패 코드: slug_taken(409 HOSTING_PROJECT_SLUG_TAKEN/HOSTING_PROJECT_EXISTS — 키가 닿지 않는 곳의 slug), slug_reserved(400 HOSTING_PROJECT_SLUG_RESERVED — -preview 로 끝나거나 플랫폼 예약어), key_project_limit(409 WORKSPACE_KEY_PROJECT_LIMIT, n/상한 표시 — 콘솔에서 새 키), data_failed, data_timeout(같은 명령을 다시 치면 이어서 기다린다). 429 는 Retry-After 만큼(최대 60초) 기다린 뒤 한 번 재시도한다.

다른 명령을 워크스페이스 키로

모든 프로젝트 명령(publish·releases·target·activate·rollback·pause· data·jobs·secrets)은 프로필을 한 곳(hosting-credentials.ts)에서 프로젝트×환경× 토큰으로 풀어 받으므로 그대로 동작한다.

hangar target --profile demo-auto                         # 파생 프로필: preview
hangar target --profile demo-auto --environment production   # 이 호출만 production
hangar releases --profile ws --slug demo-auto              # 워크스페이스 프로필 + 일회성 지정
hangar releases --profile ws --project-id <uuid> --environment preview
  • --slug 는 GET …/projects?slug= 로 키가 닿는 프로젝트 중에서 찾는다.
  • 워크스페이스 프로필에 --slug/--project-id 가 없으면 project_required.
  • 프로젝트 토큰(hgr_) 프로필에 다른 환경·프로젝트를 주면 token_bound — 그 토큰은 서버에서 한 프로젝트·환경에 묶여 있다.
  • logout --profile ws 는 그 키를 빌려 쓰는 파생 프로필도 함께 지운다. hangar profiles 에 kind(project/workspace/workspace-project) 가 나온다.

저장 모양(기존 프로필은 kind 없이 그대로 — 마이그레이션 없음, "app" 슬롯 불변):

{
  "web": {
    "current": "ws",
    "profiles": {
      "ws": { "kind": "workspace", "apiUrl": "...", "workspaceId": "...", "keyId": "...", "token": "hgw_..." },
      "demo-auto": { "kind": "workspace-project", "parent": "ws", "projectId": "...", "slug": "demo-auto", "environment": "preview" }
    }
  }
}

에러·출력에 섞여 들어온 토큰 모양 값(hgr_/hgw_/hgp_)은 hgw_**** 처럼 가려서 출력한다.

프로젝트 준비

발행 대상 프로젝트의 루트에는 hangar.json이 있어야 한다.

| 필드 | 의미 | | --- | --- | | version | 매니페스트 스키마 버전. 현재 1 고정. | | name | 슬러그 형태의 프로젝트 이름. | | runtime | 이 CLI가 다루는 값은 static뿐이다(node, python은 선언만 기록되고 이 도구가 실행하지 않는다). | | outputDir | 발행할 빌드 산출물 디렉터리. 프로젝트 루트 기준 상대 경로. | | entrypoint | 선택 필드. HTML 진입점 경로. 생략하면 발행 시 index.html로 간주한다. |

hangar init [--project <dir>]

hangar.json을 프로젝트 레이아웃에서 관찰 가능한 값으로 채워 생성한다.

  • name: package.json의 name을 슬러그화(스코프 제거)하거나, 없으면 디렉터리 이름을 쓴다.
  • runtime: requirements.txt·pyproject.toml이 있으면 python, express·fastify·koa·hono·@nestjs/core·next 의존성이 있으면 node, 아니면 static.
  • outputDir: dist·build·out·public 중 실제로 존재하는 첫 디렉터리, 없으면 dist.

이 값들은 제안일 뿐이다. 발행 전에 파일을 직접 확인한다. 이미 hangar.json이 있으면 절대 덮어쓰지 않고 실패한다.

hangar doctor [--project <dir>] [--json]

아무것도 바꾸지 않고 점검만 한다. Node 버전(22 이상), hangar.json의 존재· 스키마 유효성, outputDir·entrypoint가 프로젝트 안에 머물고 심볼릭 링크가 아닌지, migrations/*.sql의 RLS 누락(아래), supabase/config.toml· supabase/migrations의 존재 여부를 확인한다. error 수준 항목이 하나라도 있으면 종료 코드가 0이 아니다.

RLS 누락 경고

데이터 계층의 유일한 조용한 실패는 RLS 를 켜지 않은 테이블이다. PostgREST 는 기본적으로 아무 역할에도 권한을 주지 않으므로, enable row level security 와 정책이 없는 테이블은 절반만 열린 게 아니라 anon·authenticated 양쪽에서 아예 보이지 않는다. 테이블 12개를 쓰는 프로젝트에서 하나만 빠뜨려도 그 화면만 런타임에 조용히 죽는다.

doctor는 migrations/*.sql 을 전부 읽어 텍스트로만 훑는다 — SQL 파서 의존성은 없고, 앞으로도 두지 않는다. 주석·문자열 리터럴·$$ … $$ 본문을 먼저 지운 뒤 create table [if not exists] <이름> 과 alter table <이름> enable row level security 를 모으고, 스키마 수식 (public.todos = todos)과 따옴표 식별자("Notes")를 같은 테이블로 본다. 다음 두 경우를 경고한다(막지 않는다 — 스키마를 쓰는 중인 프로젝트도 발행할 수 있어야 한다):

| 코드 | 언제 | | --- | --- | | migration_policy_without_rls | auth.uid() 를 쓰는 정책이 있는데 그 테이블에 RLS 를 켜는 줄이 마이그레이션 어디에도 없다 — 본인 행만 보이게 하려던 의도가 분명하므로 확실한 버그다 | | migration_table_without_rls | 테이블을 만들었고 RLS 를 켜는 줄이 없다 |

enable 은 마이그레이션 집합 전체에서 찾으므로 0001 에서 만들고 0005 에서 켜도 통과한다. 한 테이블에 두 경고가 같이 붙는 일은 없고, 메시지에 그대로 붙여 쓸 수 있는 줄이 들어 있다:

! migration_policy_without_rls: Table "public.book_records" has a policy using auth.uid()
  but no migration enables row level security on it, ... Add:
  alter table public.book_records enable row level security;   (migrations/0001_reader.sql)

임시 테이블(create temp table)은 PostgREST 가 볼 수 없으므로 대상이 아니다. 경고가 하나도 없으면 migration_rls_present(info)로 "봤고 문제 없었다"를 남긴다.

hangar inspect [--project <dir>] [--json]

발행이 무엇을 근거로 이뤄질지 보여준다: 매니페스트, outputDir의 파일 인벤토리(경로·바이트·sha256), 로컬 마이그레이션 체크섬, 이 전체에 대한 결정적 다이제스트. 아무것도 업로드하지 않는다.

세 도구 모두 공통 설계: 네트워크를 열지 않는다. init은 정확히 한 번, hangar.json 하나만 생성한다. 그 외에는 아무 파일도 만들거나 옮기거나 지우지 않는다. 프로젝트 코드를 실행하지 않는다 — package.json은 데이터로만 읽고 lifecycle 스크립트를 돌리지 않는다.

발행

hangar publish --project /absolute/path/to/project \
  --idempotency-key 11111111-1111-4111-8111-111111111111 --json

inspect가 만드는 것과 같은 검사(심볼릭 링크 거부, 경로 이탈 거부, .env류 자격증명 파일 거부 등)를 거친 뒤, outputDir 안의 파일들을 파일별로 업로드하고 그 결과로 불변 릴리스를 하나 만든다. 릴리스는 업로드된 파일들의 집합과 매니페스트 해시로 식별되는, 이후 절대 바뀌지 않는 스냅샷이다.

publish는 현재 서빙 중인 사이트를 절대 바꾸지 않는다. 릴리스를 만들 뿐이며, 그것을 실제로 트래픽에 노출하려면 아래 activate를 별도로 실행해야 한다.

전환·롤백·일시중지

hangar target --json      # 현재 desired 릴리스와 revision, paused 여부
hangar releases --json    # 이 프로젝트/환경에 쌓인 릴리스 목록 (첫 페이지 + nextCursor)

hangar activate --release <release-uuid> --expected-revision <n> \
  --idempotency-key <uuid> --reason '첫 production 배포' --json

hangar rollback --release <release-uuid> --expected-revision <n> \
  --idempotency-key <uuid> --reason '이전 릴리스로 복귀' --json

hangar pause --expected-revision <n> \
  --idempotency-key <uuid> --reason '점검' --json
  • --expected-revision: 지금 서버가 들고 있는 target의 revision 번호다. 실제 값보다 먼저 hangar target으로 확인한다. 서버는 이 값이 현재 revision과 일치할 때만 전환을 받아들인다 — 그 사이 다른 사람이 먼저 전환을 했다면 거부되므로, 내가 보고 있던 상태 위에 덮어쓰는 사고를 막는다(낙관적 동시성 제어).
  • --idempotency-key: 이 작업 시도 하나를 가리키는 UUID다. 네트워크 타임아웃 등으로 같은 명령을 같은 키로 재시도하면 서버는 두 번째 요청을 새 작업으로 만들지 않고 첫 시도의 결과를 그대로 돌려준다. 완전히 새로운 시도(다른 값으로 재시도하고 싶을 때)에는 새 키를 쓴다.

rollback으로 되돌릴 릴리스를 고르려면 과거에 실제로 선택했던 릴리스 id가 필요하다 — hangar releases와 과거 activate/rollback 기록을 참고한다. pause는 --release를 받지 않는다: 지금의 desired 릴리스는 그대로 두고 서빙만 멈춘다.

배포 진행 확인

activate(또는 rollback)는 desired 상태를 바꿀 뿐, 그 자리에서 트래픽을 전환하지 않는다. 클러스터에 상주하는 컨트롤러가 최대 30초 주기로 이 상태를 읽어 실제 배포를 집행하므로, 반영까지 수십 초의 지연이 있다. 실제로 서빙 중인 릴리스가 바뀌었는지는 콘솔에서 확인한다 — CLI에는 그 상태를 폴링하는 명령이 없다.

데이터 계층

프로젝트 전용 PostgreSQL/PostgREST 계층의 상태와 anon 키를 읽는다. 자세히 읽어보면 이 CLI가 갖고 있는 프로젝트 토큰(hgr_)으로 닿을 수 있는 라우트는 상태 조회 하나뿐이다 — enable/keys/rotate/service-key/disable는 서명 자재를 다루므로 전부 세션 전용이고, 프로젝트 토큰은 아예 거부된다. 그래서 hangar data enable이나 hangar data rotate-keys 같은 명령은 없다 — 만들 수 없는 걸 만든 척하지 않는다. 데이터 계층을 켜려면 콘솔을 쓰거나, 자동화 키(hgw_)로 hangar project create <slug> --data 를 쓴다(켜기만 열려 있다). 키 회전·끄기는 콘솔 전용이다.

hangar data status --json   # 상태, database 이름, REST/Auth base URL, 사용량
hangar data keys --json     # anon 키
hangar data keys --write-env .env.local   # 같은 값을 HANGAR_API_URL/HANGAR_ANON_KEY로 파일에도 쓴다
  • --write-env: 그 파일의 다른 줄은 보존하고 두 키만 교체(없으면 추가)한다. 파일이 있는 디렉터리가 Vite 프로젝트(package.json 에 vite)면 VITE_HANGAR_API_URL/VITE_HANGAR_ANON_KEY 도 쓴다 — Vite 는 VITE_* 만 브라우저 코드에 노출한다.

  • --env <preview|production>: 환경을 고르는 옵션이 아니다. login 하나가 워크스페이스·프로젝트·환경 하나에 묶이므로 저장된 토큰은 이미 한 환경에만 유효하다 — 이 플래그는 스크립트가 실수로 다른 환경을 향해 뛰지 않게 막는 확인용 가드다. 저장된 환경과 다르면 네트워크를 열기 전에 environment_mismatch로 실패한다. 자동화 키 프로필에서 환경을 고르는 것은 --environment 다.

  • --service-role: 이 플래그를 주면 무조건 service_role_console_only 로 실패한다. GET .../data/service-key는 세션만 받는 라우트라 프로젝트 토큰으로는 원천적으로 닿지 않는다 — service_role은 RLS를 우회하므로 워크스페이스 owner가 콘솔에서 직접 읽어야 한다. 브라우저 코드에는 절대 넣지 않는다.

  • 데이터 계층이 이 배포에서 꺼져 있으면(HOSTING_DATA_DISABLED) 두 명령 모두 분명한 에러로 실패한다. 이 프로젝트/환경이 아직 데이터 계층을 켜지 않은 경우 data status는 에러 없이 status: "disabled"를 돌려주고, data keys 는 data_not_ready로 실패한다 — 콘솔에서 켜고 프로비저닝이 끝나길 기다린다.

  • restUrl/authUrl은 PostgREST/GoTrue가 실제로 마운트된 경로 (/api/rest/v1, /api/auth/v1)다. supabase-js는 넘겨준 URL에 /rest/v1·/auth/v1을 직접 이어붙이므로, 그 URL을 그대로 createClient()에 주면 경로가 두 번 겹친다. data keys가 출력하는 HANGAR_API_URL은 이미 그 접미사를 뗀 base다:

    import { createClient } from '@supabase/supabase-js';
    
    const supabase = createClient(
      process.env.HANGAR_API_URL, // restUrl에서 "/rest/v1"을 뗀 값, 예: https://<slug>.apps.2tl.io/api
      process.env.HANGAR_ANON_KEY,
    );

스크립트 잡 (롤아웃 중)

hangar.json 의 jobs 가 있으면 publish 가 잡마다 entry 번들(단일 .mjs, outputDir 밖, ≤ 16MB)을 jobs/<name>.mjs 업로드로 올리고 릴리스 요청에 jobs 를 싣는다. 잡이 없으면 요청은 예전과 바이트 단위로 같다(jobs 키 없음). doctor 는 번들 존재·크기·번들되지 않은 import (playwright·Node 내장 외)를 점검한다 — 네트워크를 쓰지 않으므로 시크릿 값이 설정됐는지는 hangar secrets list 로 본다.

hangar jobs list [--json]                       # 스케줄(tz)·다음 실행·마지막 결과, 시각은 KST
hangar jobs runs <name> [--limit 20] [--json]
hangar jobs logs <run-id> | <name> --last       # 로그 꼬리(≤64KB, 주입 시크릿 마스킹)
hangar jobs run <name> --reason "<사유>" [--idempotency-key <uuid>]
hangar secrets list [--json]                    # 이름·크기·갱신 시각만
hangar secrets set <NAME> < value-file          # 값은 stdin 으로만, 출력·로그 안 함
hangar secrets unset <NAME> --reason "<사유>"

모두 --profile 과 --json 을 받는다. 조회는 project:read, jobs run·secrets set/unset 은 활성화와 같은 권한(release:activate + 발급자가 현재 owner, API 가 재확인)이다. 권한이 없으면 403 에 콘솔 안내가 붙는다. secrets set 은 인자나 --value 로 값을 받지 않고(거부하며 되풀이 출력하지 않음), 대화형 터미널이면 거부한다. 끝의 개행 하나는 버린다.

에러

모든 실패는 구조화된 형태다. --json 아래에서는 이렇게 나온다:

{ "ok": false, "error": { "code": "credential_file_rejected", "message": "...", "hint": "...", "path": ".env.local" } }

분기는 code 필드로 한다 — 이 값은 안정적으로 유지된다. message는 문구가 바뀔 수 있으므로 파싱하지 않는다. hint가 있으면 다음에 무엇을 할지 알려주고, path가 있으면 문제가 된 상대 경로를 가리킨다(파일 내용은 절대 담기지 않는다).

하지 않는 것

이 목록은 면책조항이 아니라 설계다. hangar은 방금 처음 보는 프로젝트를 상대로 개발자의 로컬 머신에서 실행되므로, 예측 가능한 것이 유용함의 전제조건이다.

  • init·doctor·inspect는 네트워크를 열지 않는다. 플랫폼이든 어디든 연결하지 않는다.
  • 설치도, 빌드도 대신 하지 않는다. 패키지 매니저, 번들러, Supabase CLI, 테스트 스위트를 실행하지 않는다.
  • 프로젝트 코드를 실행하지 않는다. package.json은 데이터로만 읽는다. lifecycle 스크립트는 돌리지 않는다.
  • 파일을 딱 하나, 한 번만 만든다. init이 hangar.json을 쓰고, 이미 있으면 덮어쓰기를 거부한다. 그 외에는 아무것도 만들거나 옮기거나 지우지 않는다. (project create 는 여기에 더해 .env.local 의 데이터 키 줄만 고친다.)
  • 배포하지 않고, 배포하는 척도 하지 않는다. inspect는 발행이 무엇을 근거로 만들어질지 보여줄 뿐이다. publish는 릴리스만 만들고 활성화하지 않는다. 시뮬레이션할 "성공"이 없다. 활성화까지 하는 것은 project create 뿐이고, 그것도 preview 에서만이다.