@elyun/bylane 1.25.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.25.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,20 +264,31 @@ 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)
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
278
+ }
279
+ continue
280
+ }
281
+
282
+ const { pid, source } = found
273
283
  try {
274
- process.kill(pid, 0) // PID 살아있는지 확인
275
284
  process.kill(pid, 'SIGTERM')
276
- writeState(loopName, { ...state, status: 'stopped', stoppedAt: new Date().toISOString() })
277
- console.log(` ${loopName} (PID: ${pid}) 종료`)
285
+ const state = readState(loopName)
286
+ if (state) writeState(loopName, { ...state, status: 'stopped', stoppedAt: new Date().toISOString() })
287
+ console.log(` ${loopName} (PID: ${pid}, 출처: ${source}) 종료`)
278
288
  stopped = true
279
- } catch {
280
- console.log(` ${loopName} (PID: ${pid}) 이미 종료됨`)
289
+ } catch (err) {
290
+ console.log(` ${loopName} (PID: ${pid}) 종료 실패: ${err.message}`)
291
+ console.log(` 수동 종료: kill -9 ${pid}`)
281
292
  }
282
293
  }
283
294
 
@@ -285,15 +296,18 @@ if (command === 'install') {
285
296
  console.log(' 실행 중인 루프가 없습니다.')
286
297
  }
287
298
  } else if (subCmd === 'status') {
288
- const alive = isTmuxSessionAlive(sessionName)
299
+ const tmuxAlive = isTmuxSessionAlive(sessionName)
289
300
  const { readState } = await import('./state.js')
290
- const reviewState = readState('review-loop')
291
- const respondState = readState('respond-loop')
292
301
  const mode = resolveLoopMode()
293
302
  console.log(`\n 모드: ${mode}`)
294
- console.log(` tmux 세션 (${sessionName}): ${alive ? '실행 중' : '없음'}`)
295
- console.log(` review-loop: ${reviewState?.status ?? 'unknown'}${reviewState?.pid ? ` (PID: ${reviewState.pid})` : ''}`)
296
- 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('')
297
311
  } else {
298
312
  console.error('사용법: bylane loop <start|stop|status>')
299
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