@elyun/bylane 1.33.0 → 1.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/bylane-review-agent.md +11 -3
- package/commands/bylane-setup.md +2 -1
- package/package.json +1 -1
- package/src/cli.js +33 -3
- package/src/config.js +1 -1
- package/src/loop-utils.js +71 -4
- package/src/monitor/index.js +1 -1
- package/src/monitor/panels/status.js +32 -2
- package/src/preflight.js +12 -1
|
@@ -18,17 +18,25 @@ description: PR의 diff를 파일별로 분석하여 코드 라인별 인라인
|
|
|
18
18
|
|
|
19
19
|
## GitHub 리뷰 템플릿 탐지
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
다음 순서로 탐색한다. 먼저 발견된 파일을 **최우선**으로 따른다:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
+
# 1순위: 프로젝트 .github/ 커스텀 템플릿
|
|
24
25
|
ls .github/REVIEW_TEMPLATE.md \
|
|
25
26
|
.github/review_template.md \
|
|
26
27
|
.github/CODE_REVIEW_TEMPLATE.md \
|
|
27
28
|
docs/REVIEW_TEMPLATE.md 2>/dev/null | head -1
|
|
29
|
+
|
|
30
|
+
# 2순위: 프로젝트 .bylane/ 설치 템플릿 (bylane setup 시 설치됨)
|
|
31
|
+
# .bylane/templates/review-template.md
|
|
32
|
+
|
|
33
|
+
# 3순위: 글로벌 fallback (~/.claude/templates/bylane/review-template.md)
|
|
28
34
|
```
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
우선순위 요약:
|
|
37
|
+
1. `.github/` 내 커스텀 템플릿 (프로젝트별 재정의)
|
|
38
|
+
2. `.bylane/templates/review-template.md` (프로젝트 기본값)
|
|
39
|
+
3. `~/.claude/templates/bylane/review-template.md` (글로벌 기본값)
|
|
32
40
|
|
|
33
41
|
## 검사 항목 선택
|
|
34
42
|
|
package/commands/bylane-setup.md
CHANGED
|
@@ -10,9 +10,10 @@ ALWAYS complete all 8 steps before saving. NEVER skip steps.
|
|
|
10
10
|
|
|
11
11
|
## 실행 전 준비
|
|
12
12
|
|
|
13
|
-
1. `.bylane/` 디렉토리가 없으면
|
|
13
|
+
1. `.bylane/` 디렉토리가 없으면 생성하고 기본 템플릿을 설치:
|
|
14
14
|
```bash
|
|
15
15
|
mkdir -p .bylane/state
|
|
16
|
+
npx @elyun/bylane templates install
|
|
16
17
|
```
|
|
17
18
|
|
|
18
19
|
2. 기존 bylane.json 있으면 현재 설정을 로드해 기본값으로 사용.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -18,8 +18,9 @@ const USER_CONFIG_FILES = [
|
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
const TARGETS = [
|
|
21
|
-
{ src: join(ROOT, 'commands'),
|
|
22
|
-
{ src: join(ROOT, 'hooks'),
|
|
21
|
+
{ src: join(ROOT, 'commands'), dest: join(CLAUDE_DIR, 'commands'), label: 'Commands' },
|
|
22
|
+
{ src: join(ROOT, 'hooks'), dest: join(CLAUDE_DIR, 'hooks'), label: 'Hooks' },
|
|
23
|
+
{ src: join(ROOT, 'templates'), dest: join(CLAUDE_DIR, 'templates', 'bylane'), label: 'Templates' },
|
|
23
24
|
]
|
|
24
25
|
|
|
25
26
|
function backupAndCopy(src, dest, file, label) {
|
|
@@ -369,6 +370,35 @@ if (command === 'install') {
|
|
|
369
370
|
console.error('사용법: bylane loop <start|stop|status>')
|
|
370
371
|
process.exit(1)
|
|
371
372
|
}
|
|
373
|
+
} else if (command === 'templates') {
|
|
374
|
+
const subCmd = args[1] || 'install'
|
|
375
|
+
if (subCmd === 'install') {
|
|
376
|
+
const globalTemplatesDir = join(CLAUDE_DIR, 'templates', 'bylane')
|
|
377
|
+
const projectTemplatesDir = join('.bylane', 'templates')
|
|
378
|
+
if (!existsSync(globalTemplatesDir)) {
|
|
379
|
+
console.error(' 글로벌 템플릿이 없습니다. 먼저 npx @elyun/bylane install 을 실행하세요.')
|
|
380
|
+
process.exit(1)
|
|
381
|
+
}
|
|
382
|
+
mkdirSync(projectTemplatesDir, { recursive: true })
|
|
383
|
+
const files = readdirSync(globalTemplatesDir)
|
|
384
|
+
let copied = 0
|
|
385
|
+
for (const file of files) {
|
|
386
|
+
const dest = join(projectTemplatesDir, file)
|
|
387
|
+
if (!existsSync(dest)) {
|
|
388
|
+
copyFileSync(join(globalTemplatesDir, file), dest)
|
|
389
|
+
console.log(` + Templates: ${file}`)
|
|
390
|
+
copied++
|
|
391
|
+
} else {
|
|
392
|
+
console.log(` = Templates: ${file} (이미 존재, 건너뜀)`)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
console.log(copied > 0
|
|
396
|
+
? `\n 템플릿 ${copied}개가 .bylane/templates/ 에 설치되었습니다.`
|
|
397
|
+
: '\n 모든 템플릿이 이미 설치되어 있습니다.')
|
|
398
|
+
} else {
|
|
399
|
+
console.error('사용법: bylane templates install')
|
|
400
|
+
process.exit(1)
|
|
401
|
+
}
|
|
372
402
|
} else if (command === 'monitor') {
|
|
373
403
|
// 항상 현재 패키지의 모니터 실행 (버전 일치 보장)
|
|
374
404
|
const monitorPath = join(__dirname, 'monitor', 'index.js')
|
|
@@ -377,6 +407,6 @@ if (command === 'install') {
|
|
|
377
407
|
child.on('exit', code => process.exit(code ?? 0))
|
|
378
408
|
} else {
|
|
379
409
|
console.error(`알 수 없는 명령: ${command}`)
|
|
380
|
-
console.error('사용법: npx @elyun/bylane [install|uninstall|loop|monitor|preflight|state|memory|cleanup] [--symlink]')
|
|
410
|
+
console.error('사용법: npx @elyun/bylane [install|uninstall|loop|monitor|preflight|state|memory|cleanup|templates] [--symlink]')
|
|
381
411
|
process.exit(1)
|
|
382
412
|
}
|
package/src/config.js
CHANGED
package/src/loop-utils.js
CHANGED
|
@@ -97,11 +97,64 @@ export function isTmuxSessionAlive(sessionName) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* tmux 세션의 두 루프가 정상 기동됐는지 검증한다.
|
|
102
|
+
* state 파일에 running 상태가 기록될 때까지 최대 timeoutMs 대기.
|
|
103
|
+
* @param {string} sessionName
|
|
104
|
+
* @param {string} stateDir
|
|
105
|
+
* @param {number} timeoutMs
|
|
106
|
+
* @returns {{ ok: boolean, review: object, respond: object }}
|
|
107
|
+
*/
|
|
108
|
+
export function verifyTmuxLoops(sessionName = 'bylane-loops', stateDir = '.bylane/state', timeoutMs = 8000) {
|
|
109
|
+
const loops = ['review-loop', 'respond-loop']
|
|
110
|
+
const result = {}
|
|
111
|
+
|
|
112
|
+
// 1. tmux 세션 및 윈도우 존재 확인
|
|
113
|
+
if (!isTmuxSessionAlive(sessionName)) {
|
|
114
|
+
return { ok: false, review: { alive: false }, respond: { alive: false } }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let windows = []
|
|
118
|
+
try {
|
|
119
|
+
const out = execSync(
|
|
120
|
+
`tmux list-windows -t ${sessionName} -F '#{window_name}'`,
|
|
121
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
122
|
+
).trim()
|
|
123
|
+
windows = out.split('\n').map(w => w.trim())
|
|
124
|
+
} catch { /* 세션이 막 종료됐을 수 있음 */ }
|
|
125
|
+
|
|
126
|
+
// 2. 각 루프의 state 파일이 running 상태가 될 때까지 폴링
|
|
127
|
+
const deadline = Date.now() + timeoutMs
|
|
128
|
+
const pending = new Set(loops)
|
|
129
|
+
|
|
130
|
+
while (pending.size > 0 && Date.now() < deadline) {
|
|
131
|
+
for (const loop of [...pending]) {
|
|
132
|
+
const state = readState(loop, stateDir)
|
|
133
|
+
if (state?.status === 'running' && state?.pid) {
|
|
134
|
+
result[loop] = { alive: true, pid: state.pid, startedAt: state.startedAt }
|
|
135
|
+
pending.delete(loop)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (pending.size > 0) execSync('sleep 0.5')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 타임아웃된 루프는 실패로 처리
|
|
142
|
+
for (const loop of pending) {
|
|
143
|
+
const windowName = loop.replace('-loop', '')
|
|
144
|
+
const windowAlive = windows.includes(windowName)
|
|
145
|
+
result[loop] = { alive: false, windowAlive }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const ok = loops.every(l => result[l]?.alive)
|
|
149
|
+
return { ok, review: result['review-loop'], respond: result['respond-loop'] }
|
|
150
|
+
}
|
|
151
|
+
|
|
100
152
|
/**
|
|
101
153
|
* tmux 세션에서 review-loop + respond-loop을 실행
|
|
102
154
|
* @param {string} sessionName
|
|
155
|
+
* @param {string} stateDir
|
|
103
156
|
*/
|
|
104
|
-
export function startTmuxLoops(sessionName = 'bylane-loops') {
|
|
157
|
+
export function startTmuxLoops(sessionName = 'bylane-loops', stateDir = '.bylane/state') {
|
|
105
158
|
if (isTmuxSessionAlive(sessionName)) {
|
|
106
159
|
console.log(`[tmux] 세션 '${sessionName}'이 이미 실행 중입니다.`)
|
|
107
160
|
return { started: false, reason: 'already_running' }
|
|
@@ -118,8 +171,21 @@ export function startTmuxLoops(sessionName = 'bylane-loops') {
|
|
|
118
171
|
{ stdio: 'inherit' }
|
|
119
172
|
)
|
|
120
173
|
|
|
121
|
-
console.log(`[tmux] 세션 '${sessionName}' 시작
|
|
122
|
-
|
|
174
|
+
console.log(`[tmux] 세션 '${sessionName}' 시작 — 루프 기동 확인 중...`)
|
|
175
|
+
const verification = verifyTmuxLoops(sessionName, stateDir)
|
|
176
|
+
|
|
177
|
+
if (verification.ok) {
|
|
178
|
+
console.log(`[tmux] review-loop (PID: ${verification.review.pid}) ✓`)
|
|
179
|
+
console.log(`[tmux] respond-loop (PID: ${verification.respond.pid}) ✓`)
|
|
180
|
+
} else {
|
|
181
|
+
const reviewStatus = verification.review?.alive ? `✓ PID: ${verification.review.pid}` : `✗ 기동 실패 (창: ${verification.review?.windowAlive ? '있음' : '없음'})`
|
|
182
|
+
const respondStatus = verification.respond?.alive ? `✓ PID: ${verification.respond.pid}` : `✗ 기동 실패 (창: ${verification.respond?.windowAlive ? '있음' : '없음'})`
|
|
183
|
+
console.log(`[tmux] review-loop ${reviewStatus}`)
|
|
184
|
+
console.log(`[tmux] respond-loop ${respondStatus}`)
|
|
185
|
+
console.log(`[tmux] 일부 루프가 정상 기동되지 않았습니다. tmux attach -t ${sessionName} 으로 확인하세요.`)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { started: true, sessionName, verified: verification.ok, verification }
|
|
123
189
|
}
|
|
124
190
|
|
|
125
191
|
/**
|
|
@@ -174,9 +240,10 @@ export function resolveLoopMode() {
|
|
|
174
240
|
if (mode === 'tmux') {
|
|
175
241
|
try {
|
|
176
242
|
execSync('which tmux', { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
243
|
+
execSync('tmux -V', { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
177
244
|
return 'tmux'
|
|
178
245
|
} catch {
|
|
179
|
-
console.log('[loop] tmux
|
|
246
|
+
console.log('[loop] tmux를 사용할 수 없음 — process 모드로 fallback합니다.')
|
|
180
247
|
return 'process'
|
|
181
248
|
}
|
|
182
249
|
}
|
package/src/monitor/index.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import blessed from 'blessed'
|
|
2
|
+
import { execSync } from 'child_process'
|
|
2
3
|
import { loadConfig } from '../../config.js'
|
|
3
4
|
|
|
5
|
+
function isTmuxAlive(sessionName) {
|
|
6
|
+
try {
|
|
7
|
+
execSync(`tmux has-session -t ${sessionName}`, { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
8
|
+
return true
|
|
9
|
+
} catch { return false }
|
|
10
|
+
}
|
|
11
|
+
|
|
4
12
|
export function createStatusPanel(screen) {
|
|
5
13
|
const box = blessed.box({
|
|
6
14
|
top: '60%',
|
|
@@ -15,13 +23,35 @@ export function createStatusPanel(screen) {
|
|
|
15
23
|
screen.append(box)
|
|
16
24
|
|
|
17
25
|
return {
|
|
18
|
-
update() {
|
|
26
|
+
update(states = {}) {
|
|
19
27
|
const config = loadConfig()
|
|
20
28
|
const check = (v) => v ? '{green-fg}OK{/green-fg}' : '{red-fg}--{/red-fg}'
|
|
29
|
+
|
|
30
|
+
// 루프 상태
|
|
31
|
+
const reviewState = states['review-loop']
|
|
32
|
+
const respondState = states['respond-loop']
|
|
33
|
+
const loopMode = config.loop?.mode ?? 'tmux'
|
|
34
|
+
const sessionName = config.loop?.sessionName ?? 'bylane-loops'
|
|
35
|
+
const tmuxAlive = loopMode === 'tmux' ? isTmuxAlive(sessionName) : null
|
|
36
|
+
|
|
37
|
+
const loopStatus = (state) => {
|
|
38
|
+
if (!state) return '{red-fg}미실행{/red-fg}'
|
|
39
|
+
if (state.status === 'running') return `{green-fg}실행중{/green-fg} PID:${state.pid ?? '?'}`
|
|
40
|
+
if (state.status === 'stopped') return '{yellow-fg}중지됨{/yellow-fg}'
|
|
41
|
+
return `{grey-fg}${state.status ?? '알 수 없음'}{/grey-fg}`
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
const lines = [
|
|
45
|
+
` ── 루프 ──`,
|
|
46
|
+
` review-loop ${loopStatus(reviewState)}`,
|
|
47
|
+
` respond-loop ${loopStatus(respondState)}`,
|
|
48
|
+
tmuxAlive !== null
|
|
49
|
+
? ` tmux [${sessionName}] ${tmuxAlive ? '{green-fg}세션 유지{/green-fg}' : '{red-fg}세션 없음{/red-fg}'}`
|
|
50
|
+
: ` 모드: process`,
|
|
51
|
+
``,
|
|
52
|
+
` ── 연동 ──`,
|
|
22
53
|
` GitHub ${check(true)} 연결됨`,
|
|
23
54
|
` Linear ${check(config.trackers.linear.enabled)} ${config.trackers.linear.enabled ? '활성' : '비활성'}`,
|
|
24
|
-
` Figma MCP ${check(config.extensions.figma.enabled)} ${config.extensions.figma.enabled ? '활성' : '비활성'}`,
|
|
25
55
|
` Slack ${check(config.notifications.slack.enabled)} ${config.notifications.slack.channel || '미설정'}`,
|
|
26
56
|
` Telegram ${check(config.notifications.telegram.enabled)} ${config.notifications.telegram.chatId || '미설정'}`,
|
|
27
57
|
``,
|
package/src/preflight.js
CHANGED
|
@@ -20,7 +20,18 @@ function run(cmd) {
|
|
|
20
20
|
export function checkTmux() {
|
|
21
21
|
const which = run('which tmux')
|
|
22
22
|
if (!which.ok) return { ok: false, reason: 'tmux 미설치 — loop을 process 모드로 실행합니다', fix: 'brew install tmux # 또는 https://github.com/tmux/tmux' }
|
|
23
|
-
|
|
23
|
+
|
|
24
|
+
const version = run('tmux -V')
|
|
25
|
+
if (!version.ok) return { ok: false, reason: 'tmux 바이너리가 있으나 실행 불가', fix: 'tmux 재설치 또는 PATH 확인' }
|
|
26
|
+
|
|
27
|
+
// 실제 세션 생성/삭제로 동작 검증
|
|
28
|
+
const testSession = `bylane-preflight-test-${Date.now()}`
|
|
29
|
+
const create = run(`tmux new-session -d -s ${testSession}`)
|
|
30
|
+
if (!create.ok) return { ok: false, reason: `tmux 세션 생성 실패: ${create.out || '알 수 없는 오류'}`, fix: 'tmux 서버 상태 확인: tmux kill-server && tmux new-session -d -s test' }
|
|
31
|
+
|
|
32
|
+
run(`tmux kill-session -t ${testSession}`)
|
|
33
|
+
|
|
34
|
+
return { ok: true, detail: version.out }
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
function checkGhCli() {
|