@elyun/bylane 1.26.0 → 1.27.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elyun/bylane",
3
- "version": "1.26.0",
3
+ "version": "1.27.0",
4
4
  "description": "Frontend development harness for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -223,7 +223,7 @@ if (command === 'install') {
223
223
  }
224
224
  } else if (command === 'loop') {
225
225
  const subCmd = args[1] || 'start'
226
- const { resolveLoopMode, startTmuxLoops, stopTmuxLoops, isTmuxSessionAlive } = await import('./loop-utils.js')
226
+ const { resolveLoopMode, startTmuxLoops, stopTmuxLoops, isTmuxSessionAlive, findLoopPid } = await import('./loop-utils.js')
227
227
  const { loadConfig } = await import('./config.js')
228
228
  const config = loadConfig()
229
229
  const sessionName = config.loop?.sessionName ?? 'bylane-loops'
@@ -264,42 +264,50 @@ if (command === 'install') {
264
264
  stopped = true
265
265
  }
266
266
 
267
- // 2) process 모드 PID 종료 시도 (모드 무관)
267
+ // 2) PID 기반 종료 (state → pgrep fallback)
268
268
  const { readState, writeState } = await import('./state.js')
269
269
  for (const loopName of ['review-loop', 'respond-loop']) {
270
- const state = readState(loopName)
271
- if (!state?.pid) continue
272
- const pid = Number(state.pid)
273
- let alive = false
274
- try { process.kill(pid, 0); alive = true } catch {}
275
-
276
- if (alive) {
277
- try {
278
- process.kill(pid, 'SIGTERM')
279
- console.log(` ${loopName} (PID: ${pid}) 종료`)
280
- } catch {
281
- console.log(` ${loopName} (PID: ${pid}) 종료 실패`)
270
+ const found = findLoopPid(loopName)
271
+ if (!found) {
272
+ // state가 running이면 정리
273
+ const state = readState(loopName)
274
+ if (state?.status === 'running') {
275
+ writeState(loopName, { ...state, status: 'stopped', stoppedAt: new Date().toISOString() })
276
+ console.log(` ${loopName}: 프로세스를 찾을 수 없음 — 상태 정리`)
277
+ stopped = true
282
278
  }
283
- } else {
284
- console.log(` ${loopName} (PID: ${pid}) 이미 종료됨 — 상태 정리`)
279
+ continue
280
+ }
281
+
282
+ const { pid, source } = found
283
+ try {
284
+ process.kill(pid, 'SIGTERM')
285
+ const state = readState(loopName)
286
+ if (state) writeState(loopName, { ...state, status: 'stopped', stoppedAt: new Date().toISOString() })
287
+ console.log(` ${loopName} (PID: ${pid}, 출처: ${source}) 종료`)
288
+ stopped = true
289
+ } catch (err) {
290
+ console.log(` ${loopName} (PID: ${pid}) 종료 실패: ${err.message}`)
291
+ console.log(` 수동 종료: kill -9 ${pid}`)
285
292
  }
286
- writeState(loopName, { ...state, status: 'stopped', stoppedAt: new Date().toISOString() })
287
- stopped = true
288
293
  }
289
294
 
290
295
  if (!stopped) {
291
296
  console.log(' 실행 중인 루프가 없습니다.')
292
297
  }
293
298
  } else if (subCmd === 'status') {
294
- const alive = isTmuxSessionAlive(sessionName)
299
+ const tmuxAlive = isTmuxSessionAlive(sessionName)
295
300
  const { readState } = await import('./state.js')
296
- const reviewState = readState('review-loop')
297
- const respondState = readState('respond-loop')
298
301
  const mode = resolveLoopMode()
299
302
  console.log(`\n 모드: ${mode}`)
300
- console.log(` tmux 세션 (${sessionName}): ${alive ? '실행 중' : '없음'}`)
301
- console.log(` review-loop: ${reviewState?.status ?? 'unknown'}${reviewState?.pid ? ` (PID: ${reviewState.pid})` : ''}`)
302
- console.log(` respond-loop: ${respondState?.status ?? 'unknown'}${respondState?.pid ? ` (PID: ${respondState.pid})` : ''}\n`)
303
+ console.log(` tmux 세션 (${sessionName}): ${tmuxAlive ? '실행 중' : '없음'}`)
304
+ for (const loopName of ['review-loop', 'respond-loop']) {
305
+ const state = readState(loopName)
306
+ const found = findLoopPid(loopName)
307
+ const pidInfo = found ? `PID: ${found.pid} (${found.source}, 실행 중)` : (state?.pid ? `PID: ${state.pid} (사망)` : '프로세스 없음')
308
+ console.log(` ${loopName}: ${state?.status ?? 'unknown'} — ${pidInfo}`)
309
+ }
310
+ console.log('')
303
311
  } else {
304
312
  console.error('사용법: bylane loop <start|stop|status>')
305
313
  process.exit(1)
package/src/loop-utils.js CHANGED
@@ -47,6 +47,42 @@ export function killExistingLoop(loopName, stateDir = '.bylane/state') {
47
47
  }
48
48
  }
49
49
 
50
+ /**
51
+ * 루프 프로세스의 PID를 반환한다.
52
+ * state 파일 → pgrep fallback 순서로 탐색.
53
+ * @param {string} loopName e.g. 'review-loop'
54
+ * @param {string} stateDir
55
+ * @returns {{ pid: number, source: 'state' | 'pgrep' } | null}
56
+ */
57
+ export function findLoopPid(loopName, stateDir = '.bylane/state') {
58
+ // 1) state 파일에서 PID 확인
59
+ const state = readState(loopName, stateDir)
60
+ if (state?.pid) {
61
+ const pid = Number(state.pid)
62
+ try {
63
+ process.kill(pid, 0)
64
+ return { pid, source: 'state' }
65
+ } catch {
66
+ // PID가 있지만 이미 죽었으면 pgrep으로 재시도
67
+ }
68
+ }
69
+
70
+ // 2) pgrep으로 프로세스명 검색
71
+ const scriptFile = `${loopName}.js`
72
+ try {
73
+ const result = execSync(`pgrep -f "${scriptFile}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
74
+ if (result) {
75
+ // 여러 PID가 있을 수 있음 — 첫 번째 사용
76
+ const pid = Number(result.split('\n')[0])
77
+ if (pid && pid !== process.pid) return { pid, source: 'pgrep' }
78
+ }
79
+ } catch {
80
+ // pgrep 결과 없음
81
+ }
82
+
83
+ return null
84
+ }
85
+
50
86
  /**
51
87
  * tmux 세션이 살아있는지 확인
52
88
  * @param {string} sessionName