@elyun/bylane 1.14.0 → 1.16.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.
|
@@ -19,14 +19,11 @@ Claude가 직접 실행하지 않는다. 사용자에게 아래 명령을 안내
|
|
|
19
19
|
모니터 대시보드는 터미널에서 직접 실행하세요:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
|
|
22
|
+
npx @elyun/bylane monitor
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
npm run monitor --prefix ~/.claude/bylane
|
|
29
|
-
```
|
|
25
|
+
> `npx`를 사용하면 항상 최신 버전 모니터가 실행됩니다.
|
|
26
|
+
> 로컬 node_modules의 구버전이 실행되지 않습니다.
|
|
30
27
|
|
|
31
28
|
**종료**: `q` 또는 `Ctrl+C`
|
|
32
29
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -12,6 +12,11 @@ const args = process.argv.slice(2)
|
|
|
12
12
|
const command = args[0] || 'install'
|
|
13
13
|
const useSymlink = args.includes('--symlink')
|
|
14
14
|
|
|
15
|
+
// 사용자 설정 파일 — 절대 덮어쓰지 않는다
|
|
16
|
+
const USER_CONFIG_FILES = [
|
|
17
|
+
'.bylane/bylane.json',
|
|
18
|
+
]
|
|
19
|
+
|
|
15
20
|
const TARGETS = [
|
|
16
21
|
{ src: join(ROOT, 'commands'), dest: join(CLAUDE_DIR, 'commands'), label: 'Commands' },
|
|
17
22
|
{ src: join(ROOT, 'hooks'), dest: join(CLAUDE_DIR, 'hooks'), label: 'Hooks' },
|
|
@@ -31,7 +36,7 @@ function backupAndCopy(src, dest, file, label) {
|
|
|
31
36
|
const backupPath = `${destFile}.bak`
|
|
32
37
|
renameSync(destFile, backupPath)
|
|
33
38
|
copyFileSync(srcFile, destFile)
|
|
34
|
-
console.log(` ~ ${label}: ${file} (기존 파일 -> ${file}.bak)`)
|
|
39
|
+
console.log(` ~ ${label}: ${file} (업데이트됨, 기존 파일 -> ${file}.bak)`)
|
|
35
40
|
} else {
|
|
36
41
|
copyFileSync(srcFile, destFile)
|
|
37
42
|
console.log(` + ${label}: ${file}`)
|
|
@@ -48,41 +53,41 @@ function registerHooks() {
|
|
|
48
53
|
const hookScript = join(CLAUDE_DIR, 'hooks', 'bylane-agent-tracker.js')
|
|
49
54
|
settings.hooks = settings.hooks ?? {}
|
|
50
55
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
h.hooks?.some(hh => hh.command?.includes('bylane-agent-tracker'))
|
|
55
|
-
)
|
|
56
|
-
if (!preExists) {
|
|
57
|
-
settings.hooks.PreToolUse.push({
|
|
58
|
-
matcher: 'Agent',
|
|
59
|
-
hooks: [{ type: 'command', command: `node ${hookScript} pre` }]
|
|
60
|
-
})
|
|
61
|
-
console.log(' + Hook: PreToolUse/Agent → bylane-agent-tracker')
|
|
62
|
-
} else {
|
|
63
|
-
console.log(' = Hook: PreToolUse/Agent (이미 등록됨)')
|
|
64
|
-
}
|
|
56
|
+
// 기존 bylane 훅 제거 후 재등록 (버전 업 시 경로 변경 대응)
|
|
57
|
+
const stripBylane = (arr) =>
|
|
58
|
+
(arr ?? []).filter(h => !h.hooks?.some(hh => hh.command?.includes('bylane-agent-tracker')))
|
|
65
59
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
hooks: [{ type: 'command', command: `node ${hookScript} post` }]
|
|
75
|
-
})
|
|
76
|
-
console.log(' + Hook: PostToolUse/Agent → bylane-agent-tracker')
|
|
77
|
-
} else {
|
|
78
|
-
console.log(' = Hook: PostToolUse/Agent (이미 등록됨)')
|
|
79
|
-
}
|
|
60
|
+
settings.hooks.PreToolUse = [
|
|
61
|
+
...stripBylane(settings.hooks.PreToolUse),
|
|
62
|
+
{ matcher: 'Agent', hooks: [{ type: 'command', command: `node ${hookScript} pre` }] }
|
|
63
|
+
]
|
|
64
|
+
settings.hooks.PostToolUse = [
|
|
65
|
+
...stripBylane(settings.hooks.PostToolUse),
|
|
66
|
+
{ matcher: 'Agent', hooks: [{ type: 'command', command: `node ${hookScript} post` }] }
|
|
67
|
+
]
|
|
80
68
|
|
|
81
69
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
|
|
70
|
+
console.log(' ~ Hook: bylane-agent-tracker 등록 (최신 경로로 갱신)')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function preservedConfigs() {
|
|
74
|
+
const found = USER_CONFIG_FILES.filter(f => existsSync(f))
|
|
75
|
+
if (found.length > 0) {
|
|
76
|
+
console.log('\n [보존된 사용자 설정]')
|
|
77
|
+
found.forEach(f => console.log(` * ${f}`))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isUpdateMode() {
|
|
82
|
+
return existsSync(join(CLAUDE_DIR, 'commands', 'bylane.md'))
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
function install() {
|
|
85
|
-
|
|
86
|
+
const updating = isUpdateMode()
|
|
87
|
+
console.log(updating
|
|
88
|
+
? '\n byLane 업데이트 중...\n'
|
|
89
|
+
: '\n byLane 설치 중...\n'
|
|
90
|
+
)
|
|
86
91
|
|
|
87
92
|
for (const { src, dest, label } of TARGETS) {
|
|
88
93
|
mkdirSync(dest, { recursive: true })
|
|
@@ -108,8 +113,17 @@ function install() {
|
|
|
108
113
|
|
|
109
114
|
console.log('')
|
|
110
115
|
registerHooks()
|
|
116
|
+
preservedConfigs()
|
|
111
117
|
|
|
112
|
-
|
|
118
|
+
if (updating) {
|
|
119
|
+
console.log(`
|
|
120
|
+
byLane 업데이트 완료!
|
|
121
|
+
|
|
122
|
+
사용자 설정(.bylane/bylane.json)은 그대로 유지됩니다.
|
|
123
|
+
Claude Code를 재시작하면 변경사항이 적용됩니다.
|
|
124
|
+
`)
|
|
125
|
+
} else {
|
|
126
|
+
console.log(`
|
|
113
127
|
byLane 설치 완료!
|
|
114
128
|
|
|
115
129
|
다음 단계:
|
|
@@ -122,12 +136,19 @@ function install() {
|
|
|
122
136
|
|
|
123
137
|
/bylane 다크모드 토글 추가해줘
|
|
124
138
|
`)
|
|
139
|
+
}
|
|
125
140
|
}
|
|
126
141
|
|
|
127
142
|
if (command === 'install') {
|
|
128
143
|
install()
|
|
144
|
+
} else if (command === 'monitor') {
|
|
145
|
+
// 항상 현재 패키지의 모니터 실행 (버전 일치 보장)
|
|
146
|
+
const monitorPath = join(__dirname, 'monitor', 'index.js')
|
|
147
|
+
const { spawn } = await import('child_process')
|
|
148
|
+
const child = spawn(process.execPath, [monitorPath], { stdio: 'inherit' })
|
|
149
|
+
child.on('exit', code => process.exit(code ?? 0))
|
|
129
150
|
} else {
|
|
130
151
|
console.error(`알 수 없는 명령: ${command}`)
|
|
131
|
-
console.error('사용법: npx bylane [install] [--symlink]')
|
|
152
|
+
console.error('사용법: npx @elyun/bylane [install|monitor] [--symlink]')
|
|
132
153
|
process.exit(1)
|
|
133
154
|
}
|
package/src/monitor/layout.js
CHANGED
|
@@ -9,9 +9,48 @@ export function createLayout() {
|
|
|
9
9
|
const screen = blessed.screen({
|
|
10
10
|
smartCSR: true,
|
|
11
11
|
fullUnicode: true,
|
|
12
|
+
dockBorders: true,
|
|
12
13
|
title: 'byLane Monitor'
|
|
13
14
|
})
|
|
14
15
|
|
|
16
|
+
// 최소 크기 경고 오버레이
|
|
17
|
+
const tooSmall = blessed.box({
|
|
18
|
+
top: 'center',
|
|
19
|
+
left: 'center',
|
|
20
|
+
width: 40,
|
|
21
|
+
height: 5,
|
|
22
|
+
content: '\n 터미널이 너무 작습니다.\n 최소 80×24 이상으로 키워주세요.',
|
|
23
|
+
align: 'center',
|
|
24
|
+
tags: true,
|
|
25
|
+
border: { type: 'line' },
|
|
26
|
+
style: { fg: 'white', bg: 'red', border: { fg: 'white' } },
|
|
27
|
+
hidden: true
|
|
28
|
+
})
|
|
29
|
+
screen.append(tooSmall)
|
|
30
|
+
|
|
31
|
+
function checkSize() {
|
|
32
|
+
const cols = screen.width
|
|
33
|
+
const rows = screen.height
|
|
34
|
+
if (cols < 80 || rows < 24) {
|
|
35
|
+
tooSmall.show()
|
|
36
|
+
} else {
|
|
37
|
+
tooSmall.hide()
|
|
38
|
+
}
|
|
39
|
+
screen.render()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
screen.on('resize', () => {
|
|
43
|
+
// 약간의 지연으로 tmux/pane 리사이즈 완료 후 렌더링
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
screen.alloc()
|
|
46
|
+
screen.render()
|
|
47
|
+
checkSize()
|
|
48
|
+
}, 50)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// 초기 크기 체크
|
|
52
|
+
process.nextTick(checkSize)
|
|
53
|
+
|
|
15
54
|
const header = createHeader(screen)
|
|
16
55
|
const pipeline = createPipelinePanel(screen)
|
|
17
56
|
const log = createLogPanel(screen)
|
|
@@ -5,10 +5,15 @@ const AGENTS = [
|
|
|
5
5
|
'commit-agent', 'pr-agent', 'review-agent', 'respond-agent', 'notify-agent'
|
|
6
6
|
]
|
|
7
7
|
|
|
8
|
+
// 루프는 상태 파일에서 동적으로 감지 (-loop 접미사)
|
|
9
|
+
const KNOWN_LOOPS = ['review-loop', 'respond-loop']
|
|
10
|
+
|
|
8
11
|
const STATUS_ICON = {
|
|
9
12
|
idle: '[ ]',
|
|
10
13
|
in_progress: '[>]',
|
|
14
|
+
running: '[>]',
|
|
11
15
|
completed: '[v]',
|
|
16
|
+
stopped: '[-]',
|
|
12
17
|
failed: '[x]',
|
|
13
18
|
escalated: '[!]'
|
|
14
19
|
}
|
|
@@ -45,6 +50,23 @@ export function createPipelinePanel(screen) {
|
|
|
45
50
|
const maxRetries = states['orchestrator']?.maxRetries ?? 3
|
|
46
51
|
lines.push('', ` Retries: ${retries}/${maxRetries}`)
|
|
47
52
|
|
|
53
|
+
// 루프 프로세스 섹션 — 상태 파일에서 *-loop 동적 감지 + KNOWN_LOOPS 항상 표시
|
|
54
|
+
const activeLoopNames = Object.keys(states).filter(n => n.endsWith('-loop'))
|
|
55
|
+
const allLoops = [...new Set([...KNOWN_LOOPS, ...activeLoopNames])]
|
|
56
|
+
lines.push('', ' LOOPS')
|
|
57
|
+
for (const name of allLoops) {
|
|
58
|
+
const s = states[name]
|
|
59
|
+
if (!s) {
|
|
60
|
+
lines.push(` [-] ${name.padEnd(16)} 미실행`)
|
|
61
|
+
} else {
|
|
62
|
+
const icon = STATUS_ICON[s.status] ?? '[-]'
|
|
63
|
+
const elapsed = s.startedAt
|
|
64
|
+
? `${Math.floor((Date.now() - new Date(s.startedAt)) / 1000)}s`
|
|
65
|
+
: ''
|
|
66
|
+
lines.push(` ${icon} ${name.padEnd(16)} ${elapsed}`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
48
70
|
// 하위 에이전트 섹션
|
|
49
71
|
lines.push('', ' SUBAGENTS')
|
|
50
72
|
if (subagents.active.length === 0) {
|