axmap-cli 1.6.0 → 1.8.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/.claude/commands/axmap-view.md +9 -0
- package/.claude/commands/pipeline.md +9 -0
- package/README.ko.md +19 -0
- package/README.md +19 -0
- package/bin/axmap.mjs +3 -0
- package/docs/VIEW.md +19 -0
- package/mcp/README.md +3 -0
- package/mcp/pipeline.html +34 -7
- package/mcp/server.mjs +34 -3
- package/package.json +3 -1
- package/skills/axmap-view/SKILL.md +8 -0
- package/skills/pipeline/SKILL.md +14 -0
- package/src/bridge.mjs +1 -0
- package/src/bridge.mts +1 -0
- package/src/codex-connection.mjs +151 -0
- package/src/codex-connection.mts +83 -0
- package/src/managed-session.mjs +76 -14
- package/src/managed-session.mts +57 -13
- package/src/monitor-cli.mjs +8 -4
- package/src/monitor-cli.mts +8 -4
- package/src/monitor.mjs +5 -0
- package/src/monitor.mts +4 -0
- package/src/pipeline-graph.mjs +55 -0
- package/src/pipeline-graph.mts +51 -0
- package/src/pipeline-mcp.mjs +3 -1
- package/src/pipeline-mcp.mts +3 -1
- package/src/pipeline-web.mjs +56 -0
- package/src/pipeline-web.mts +31 -0
- package/src/session-mailbox.mjs +190 -0
- package/src/session-mailbox.mts +157 -0
- package/src/session-types.mts +2 -1
- package/src/view.mjs +129 -0
- package/src/view.mts +115 -0
- package/tools/bridge.mjs +32 -0
- package/tools/pipeline.mjs +10 -2
- package/tools/run-tests.mjs +3 -1
- package/tools/view.mjs +10 -0
package/src/view.mts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/** Read-only terminal view of observed sessions, claims, mail and events. */
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { bridgeHome } from './bridge.mjs'
|
|
5
|
+
import { createMonitor, fitTerminal, terminalText } from './monitor.mjs'
|
|
6
|
+
import type { MonitorSnapshot } from './monitor.mjs'
|
|
7
|
+
|
|
8
|
+
interface ViewMail { to: string; from: string; topic: string; text: string }
|
|
9
|
+
export interface ViewSnapshot extends MonitorSnapshot { mail: ViewMail[]; mailIssues: string[] }
|
|
10
|
+
const uuid = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i
|
|
11
|
+
|
|
12
|
+
export function createView(cwd: string, options: Parameters<typeof createMonitor>[1] = {}): () => Promise<ViewSnapshot> {
|
|
13
|
+
const collect = createMonitor(cwd, options)
|
|
14
|
+
return async () => {
|
|
15
|
+
const snapshot = await collect(), mail: ViewMail[] = [], mailIssues: string[] = []
|
|
16
|
+
if (snapshot.issues.some(issue => issue.startsWith('세션 확인:'))) mailIssues.push('받은 편지함: 세션 목록을 확인하지 못함')
|
|
17
|
+
// Reading does not create mailboxes, expire records, collect messages or wake a model.
|
|
18
|
+
for (const session of snapshot.sessions.slice(0, 100)) {
|
|
19
|
+
if (!uuid.test(session.id)) { mailIssues.push('받은 편지함: 세션 ID 확인 필요'); continue }
|
|
20
|
+
const file = path.join(bridgeHome(), 'mailboxes', `${session.id}.json`)
|
|
21
|
+
try {
|
|
22
|
+
if (fs.statSync(file).size > 1024 * 1024) throw new Error('파일 크기 초과')
|
|
23
|
+
const box: unknown = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
24
|
+
if (!box || typeof box !== 'object' || (box as { version?: unknown }).version !== 1 || !Array.isArray((box as { messages?: unknown }).messages)) throw new Error('형식 오류')
|
|
25
|
+
for (const m of (box as { messages: unknown[] }).messages) {
|
|
26
|
+
if (!m || typeof m !== 'object') throw new Error('메시지 형식 오류')
|
|
27
|
+
const entry = m as Record<string, unknown>
|
|
28
|
+
if (entry.repo !== snapshot.repo || entry.state !== 'queued') continue
|
|
29
|
+
if (entry.to !== session.id || typeof entry.from !== 'string' || typeof entry.text !== 'string' || typeof entry.expiresAt !== 'string' || !Number.isFinite(Date.parse(entry.expiresAt))) throw new Error('메시지 형식 오류')
|
|
30
|
+
if (Date.parse(entry.expiresAt) <= Date.now()) continue
|
|
31
|
+
mail.push({ to: session.id, from: entry.from, topic: typeof entry.topic === 'string' ? entry.topic : '', text: entry.text })
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') mailIssues.push(`받은 편지함 ${session.id.slice(0, 8)}: 읽기 실패`)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (snapshot.sessions.length > 100) mailIssues.push('받은 편지함: 앞의 세션 100개만 확인')
|
|
38
|
+
return { ...snapshot, mail, mailIssues }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const labels: Record<string, string> = { idle: '대기', running: '작업 중', closed: '종료', awaiting_user: '승인 대기', unknown: '상태 미확인', starting: '시작 중', settling: '대기 전환 중' }
|
|
43
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
|
44
|
+
function cells(value: string): number {
|
|
45
|
+
return [...segmenter.segment(value)].reduce((n, { segment }) => {
|
|
46
|
+
const cp = segment.codePointAt(0) || 0
|
|
47
|
+
return n + (/\p{Extended_Pictographic}/u.test(segment) || cp >= 0x1100 && (cp <= 0x115f || cp >= 0x2e80 && cp <= 0xa4cf || cp >= 0xac00 && cp <= 0xd7a3 || cp >= 0xf900 && cp <= 0xfaff || cp >= 0xfe10 && cp <= 0xfe6f || cp >= 0xff01 && cp <= 0xff60 || cp >= 0x20000) ? 2 : /^\p{Mark}+$/u.test(segment) ? 0 : 1)
|
|
48
|
+
}, 0)
|
|
49
|
+
}
|
|
50
|
+
function pad(value: unknown, width: number): string {
|
|
51
|
+
const text = fitTerminal(value, Math.max(0, width))
|
|
52
|
+
return text + ' '.repeat(Math.max(0, width - cells(text)))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function renderView(snapshot: MonitorSnapshot, { columns = 100, rows = 36, color = false, frame = 0 } = {}): string {
|
|
56
|
+
const width = Math.max(1, Math.floor(columns)), height = Math.max(1, Math.floor(rows)), lines: string[] = []
|
|
57
|
+
if (width < 20) return [fitTerminal('axMap · 화면이 좁습니다', width), fitTerminal('q 종료', width)].slice(0, height).join('\n')
|
|
58
|
+
const view = snapshot as Partial<ViewSnapshot>
|
|
59
|
+
const palette = {
|
|
60
|
+
text: '194;197;205', muted: '137;140;150', border: '78;82;94', accent: '172;158;199',
|
|
61
|
+
claude: '207;157;134', codex: '143;174;214', agy: '139;187;165', warning: '212;171;125',
|
|
62
|
+
}
|
|
63
|
+
type Tone = keyof typeof palette
|
|
64
|
+
const paint = (text: string, tone: Tone) => color ? `\x1b[38;2;${palette[tone]}m${text}\x1b[0m` : text
|
|
65
|
+
const line = (text: string, tone: Tone = 'text') => lines.push(paint(fitTerminal(text, width), tone))
|
|
66
|
+
const paintBoxLine = (text: string, index: number, last: number, tone: Tone, colorStatus = false) => {
|
|
67
|
+
if (index === 0 || index === last) return paint(text, 'border')
|
|
68
|
+
return paint(text.slice(0, 1), 'border') + paint(text.slice(1, -1), index === 1 || colorStatus && index === 3 ? tone : 'text') + paint(text.slice(-1), 'border')
|
|
69
|
+
}
|
|
70
|
+
const box = (title: string, content: string[], w: number): string[] => {
|
|
71
|
+
const n = Math.max(1, w - 2)
|
|
72
|
+
return [`┌${'─'.repeat(n)}┐`, ...[title, ...content].map(s => `│${pad(' ' + terminalText(s), n)}│`), `└${'─'.repeat(n)}┘`]
|
|
73
|
+
}
|
|
74
|
+
line('axMap VIEW · 실제 로컬 관측', 'accent')
|
|
75
|
+
line(`${snapshot.cwd} · ${snapshot.checkedAt}`, 'muted')
|
|
76
|
+
const sessions = [...snapshot.sessions].sort((a, b) => Number(b.connection === 'connected') - Number(a.connection === 'connected')).slice(0, 3)
|
|
77
|
+
const cols = width >= 90 ? 3 : width >= 58 ? 2 : 1
|
|
78
|
+
const cardWidth = Math.max(3, Math.floor((width - (cols - 1) * 2) / cols))
|
|
79
|
+
const cards = sessions.map(s => {
|
|
80
|
+
const active = snapshot.requests.some(r => r.to === s.id && ['sending', 'submitted'].includes(r.state))
|
|
81
|
+
const connection: Record<string, string> = { closed: '연결 종료', unreachable: '연결 불가', unverified: '연결 미확인' }
|
|
82
|
+
const state = s.connection === 'connected' ? labels[s.state] || '상태 미확인' : connection[s.connection] || '연결 미확인'
|
|
83
|
+
return box(`${s.provider.toUpperCase()} · ${s.id.slice(0, 8)}`, [s.role || '역할 미확인', `${active ? ['◐', '◓', '◑', '◒'][frame % 4] : '○'} ${state}`, s.model || '모델 미확인'], cardWidth)
|
|
84
|
+
})
|
|
85
|
+
for (let start = 0; start < cards.length; start += cols) {
|
|
86
|
+
const group = cards.slice(start, start + cols)
|
|
87
|
+
for (let i = 0; i < group[0]!.length; i++) lines.push(group.map((card, k) => {
|
|
88
|
+
const provider = sessions[start + k]!.provider.toLowerCase()
|
|
89
|
+
const tone: Tone = provider === 'codex' ? 'codex' : provider === 'agy' ? 'agy' : provider === 'claude' ? 'claude' : 'accent'
|
|
90
|
+
return paintBoxLine(card[i]!, i, card.length - 1, tone, true)
|
|
91
|
+
}).join(' '))
|
|
92
|
+
}
|
|
93
|
+
if (!sessions.length) line('등록된 세션 없음 · bridge 연결 후 표시', 'muted')
|
|
94
|
+
const claims = snapshot.claims.flatMap(c => c.paths.map(p => `${terminalText(c.agent)} · ${terminalText(p)}`))
|
|
95
|
+
const mails = view.mail?.map(m => `${terminalText(m.from)} → ${m.to.slice(0, 8)} · ${terminalText(m.topic || m.text)}`) || []
|
|
96
|
+
const left = [`LEDGER · 현재 선점 ${snapshot.claims.length}건`, ...(claims.slice(0, 4).length ? claims.slice(0, 4) : ['현재 선점 없음'])]
|
|
97
|
+
const right = [`MAILBOX · ${view.mailIssues?.length ? '일부 확인 실패' : `대기 ${view.mail?.length ?? 0}건`}`, ...(mails.slice(0, 4).length ? mails.slice(0, 4) : [view.mailIssues?.length ? '대기 건수 미확인' : '대기 메시지 없음'])]
|
|
98
|
+
if (width >= 58) {
|
|
99
|
+
const lw = Math.floor((width - 2) / 2), rw = width - lw - 2, count = Math.max(left.length, right.length)
|
|
100
|
+
const a = box(left[0]!, Array.from({ length: count - 1 }, (_, i) => left[i + 1] || ''), lw)
|
|
101
|
+
const b = box(right[0]!, Array.from({ length: count - 1 }, (_, i) => right[i + 1] || ''), rw)
|
|
102
|
+
a.forEach((v, i) => lines.push(paintBoxLine(v, i, a.length - 1, 'accent') + ' ' + paintBoxLine(b[i]!, i, b.length - 1, 'accent')))
|
|
103
|
+
} else {
|
|
104
|
+
const a = box(left[0]!, left.slice(1), Math.max(3, width))
|
|
105
|
+
const b = box(right[0]!, right.slice(1), Math.max(3, width))
|
|
106
|
+
lines.push(...a.map((x, i) => paintBoxLine(x, i, a.length - 1, 'accent')))
|
|
107
|
+
lines.push(...b.map((x, i) => paintBoxLine(x, i, b.length - 1, 'accent')))
|
|
108
|
+
}
|
|
109
|
+
line('── 최근 관측 이벤트 ──', 'muted')
|
|
110
|
+
for (const e of snapshot.events.slice(0, 4)) line(`${terminalText(e.at.slice(11, 19))} ${terminalText(e.label)}`)
|
|
111
|
+
if (!snapshot.events.length) line('기록된 이벤트 없음', 'muted')
|
|
112
|
+
for (const issue of [...snapshot.issues, ...(view.mailIssues || [])].slice(0, 2)) line(`확인 필요: ${issue}`, 'warning')
|
|
113
|
+
const footer = paint(fitTerminal('q / Esc 종료 · 작업은 계속됨 · 모델 호출 없음', width), 'muted')
|
|
114
|
+
return [...lines.slice(0, Math.max(0, height - 1)), footer].join('\n')
|
|
115
|
+
}
|
package/tools/bridge.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { runDialogue, startBackgroundDialogue, dialogueResult, stopDialogue } fr
|
|
|
8
8
|
import { sendTerminal } from '../src/terminal-bridge.mjs'
|
|
9
9
|
import { sessionStatus, formatSessionStatus } from '../src/session-status.mjs'
|
|
10
10
|
import { attachConsole } from '../src/external-console.mjs'
|
|
11
|
+
import { postMail, collectMail, flushMail } from '../src/session-mailbox.mjs'
|
|
11
12
|
|
|
12
13
|
const args = process.argv.slice(2)
|
|
13
14
|
const [command] = args
|
|
@@ -15,14 +16,31 @@ const option = (name) => { const i = args.indexOf(`--${name}`); return i < 0 ? u
|
|
|
15
16
|
const output = (value) => console.log(JSON.stringify(value, null, 2))
|
|
16
17
|
try {
|
|
17
18
|
switch (command) {
|
|
19
|
+
case 'message': {
|
|
20
|
+
const r = postMail({ to: option('to'), cwd: process.cwd(), text: fs.readFileSync(0, 'utf8'),
|
|
21
|
+
priority: option('priority') || 'boundary', reason: option('reason'), topic: option('topic'), key: option('key'),
|
|
22
|
+
ttlSeconds: Number(option('ttl-seconds') || 3600) })
|
|
23
|
+
output(r)
|
|
24
|
+
if (r.delivery && !['submitted', 'empty'].includes(r.delivery.state)) process.exitCode = 2
|
|
25
|
+
break
|
|
26
|
+
}
|
|
27
|
+
case 'mailbox': output(collectMail({ to: option('to'), cwd: process.cwd(), at: option('at'), take: args.includes('--take'), all: args.includes('--all') })); break
|
|
28
|
+
case 'flush': {
|
|
29
|
+
const r = flushMail({ to: option('to'), cwd: process.cwd(), at: option('at') }); output(r)
|
|
30
|
+
if (['unknown', 'deferred'].includes(r.state)) process.exitCode = 2
|
|
31
|
+
break
|
|
32
|
+
}
|
|
18
33
|
case 'attach': output(attachConsole({ pid: Number(option('pid')), cwd: process.cwd() })); break
|
|
34
|
+
case 'connect':
|
|
19
35
|
case 'serve': {
|
|
20
36
|
const provider = args[1]
|
|
21
37
|
if (!['codex', 'agy'].includes(provider)) throw new Error('axmap bridge serve codex|agy를 사용하세요.')
|
|
22
38
|
const binary = resolveBin(provider)
|
|
23
39
|
if (!binary) throw new Error(`${provider} 실행 파일을 찾지 못했습니다.`)
|
|
24
40
|
const [executable, ...prefix] = provider === 'codex' ? codexInvocation(binary) : [binary]
|
|
41
|
+
if (command === 'connect' && (!option('thread') || !option('socket'))) throw new Error('bridge connect codex --thread <기존 UUID> --socket <로컬 경로>가 필요합니다.')
|
|
25
42
|
const session = await startManagedSession({ provider, executable, prefix, model: option('model'), role: option('role'),
|
|
43
|
+
...(command === 'connect' ? { existingThreadId: option('thread'), socketPath: option('socket'), observeActive: args.includes('--observe') } : {}),
|
|
26
44
|
maxTokens: Number(option('max-tokens') || 100000), onEvent: output })
|
|
27
45
|
output({ event: 'connected', ...session, exited: undefined, close: undefined })
|
|
28
46
|
process.once('SIGINT', session.close); process.once('SIGTERM', session.close)
|
|
@@ -41,6 +59,7 @@ try {
|
|
|
41
59
|
break
|
|
42
60
|
}
|
|
43
61
|
case 'dialogue': output(dialogueResult(args[1])); break
|
|
62
|
+
case 'notice': output(await sendTerminal(option('to'), { action: 'notice', text: fs.readFileSync(0, 'utf8') })); break
|
|
44
63
|
case 'stop': output(stopDialogue(args[1])); break
|
|
45
64
|
case 'answer': output(await sendTerminal(option('to'), { action: 'answer', id: option('request'), decision: option('decision') })); break
|
|
46
65
|
case 'install':
|
|
@@ -78,6 +97,9 @@ try {
|
|
|
78
97
|
console.log(`axMap 로컬 세션 연결 — 관리형 Codex/agy와 제한된 자동 대화
|
|
79
98
|
axmap bridge serve codex|agy [--model 이름] [--role 역할] [--max-tokens 100000]
|
|
80
99
|
명시적으로 새 관리형 CLI를 시작합니다. 기존 창에 임의로 붙지 않습니다.
|
|
100
|
+
axmap bridge connect codex --thread <기존 UUID> --socket <로컬 socket> [--role 역할] [--observe]
|
|
101
|
+
실행 중인 공식 Codex daemon의 기존 idle 대화에 연결합니다. 새 모델/대화를 만들지 않습니다.
|
|
102
|
+
--observe는 active 대화에도 관측만 연결하며 idle 전환 전에는 새 요청을 거절합니다.
|
|
81
103
|
axmap bridge chat --left <UUID> --right <UUID> [--max-turns 4] [--max-tokens 50000]
|
|
82
104
|
stdin 작업을 자동 중계합니다. 기본 메시지 한도 2000자, 대기 중 모델 호출 없음.
|
|
83
105
|
axmap bridge answer --to <UUID> --request <ID> --decision accept|decline|cancel
|
|
@@ -88,9 +110,19 @@ axmap bridge stop <대화 UUID> 추가 중계 중단 (이
|
|
|
88
110
|
axmap bridge install --experimental --executable <agy 절대 경로> 연결 훅 실험
|
|
89
111
|
axmap bridge uninstall 추가한 연결 훅 해제
|
|
90
112
|
axmap bridge sessions 이 repo에 연결된 세션
|
|
113
|
+
axmap bridge notice --to <UUID> stdin 안내를 기존 Codex 문맥에 기록 (모델 호출 없음)
|
|
91
114
|
axmap bridge attach --pid <PID> 이미 실행한 Windows agy 콘솔을 현재 repo에 연결
|
|
92
115
|
axmap bridge status [--json] 로컬 연결·작업·승인 대기·종료 상태 조회 (모델 호출 없음)
|
|
93
116
|
axmap bridge send --to <세션 UUID> [--wait-ms 30000] 본문은 stdin. 전달 후 지정 시간까지 로컬에서 회신 대기
|
|
117
|
+
send는 실행용 저수준 명령입니다. 일반 협업 알림에는 message를 사용하세요.
|
|
118
|
+
axmap bridge message --to <UUID> [--topic <갱신 주제>] [--key <중복 방지 키>] [--ttl-seconds 3600]
|
|
119
|
+
stdin 일반 메시지를 큐에 저장합니다. 모델 호출·새 턴 없음. 같은 발신자의 대기 중 topic만 최신으로 합칩니다.
|
|
120
|
+
axmap bridge message --to <UUID> --priority immediate --reason stop|data-loss|work-invalidated
|
|
121
|
+
긴급 사유를 보존하고 즉시 전달을 시도합니다. busy면 deferred, unknown이면 재전송 금지.
|
|
122
|
+
axmap bridge mailbox --to <UUID> [--all]
|
|
123
|
+
읽기만 합니다. --take --at checkpoint로 작업 경계에서 묶음 회수 (모델 호출 없음).
|
|
124
|
+
axmap bridge flush --to <UUID> --at task-complete|test-complete|experiment-complete|checkpoint
|
|
125
|
+
필요할 때만 idle 수신자에게 큐를 한 요청으로 전달. 자동 감시·중단·재전송 없음.
|
|
94
126
|
axmap bridge result <요청 UUID> [--wait-ms 30000] 제출/완료/불명 상태와 결과. 대기 중 모델 호출 없음
|
|
95
127
|
관리형 stdio: Codex App Server / agy stream-json. PTY와 agentapi는 별도 실험 경로입니다.`)
|
|
96
128
|
break
|
package/tools/pipeline.mjs
CHANGED
|
@@ -3,14 +3,22 @@ import fs from 'node:fs'
|
|
|
3
3
|
import { runPipelineOperation } from '../src/pipeline-mcp.mjs'
|
|
4
4
|
import { errorMessage } from '../src/session-types.mjs'
|
|
5
5
|
import { installPipelineStatusline, pipelineStatusline } from '../src/pipeline-statusline.mjs'
|
|
6
|
+
import { runMonitor } from '../src/monitor-cli.mjs'
|
|
7
|
+
import { startPipelineWeb } from '../src/pipeline-web.mjs'
|
|
6
8
|
const output = value => process.stdout.write(JSON.stringify(value) + '\n')
|
|
7
9
|
let cancelled = false
|
|
8
10
|
process.on('message', message => { if (message?.type === 'cancel-observation') cancelled = true })
|
|
9
11
|
try {
|
|
10
12
|
const action = process.argv[2]
|
|
11
13
|
const option = name => { const at = process.argv.indexOf(`--${name}`); return at < 0 ? undefined : process.argv[at + 1] }
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
+
if (process.argv.includes('--web')) {
|
|
15
|
+
const web = await startPipelineWeb(option('cwd') || process.cwd(), Number(option('port') || 0))
|
|
16
|
+
console.log(JSON.stringify({ url: web.url, mode: 'read-only-local-pipeline' }))
|
|
17
|
+
const close = () => { void web.close() }; process.once('SIGINT', close); process.once('SIGTERM', close)
|
|
18
|
+
} else if (!action || action === 'view' || action.startsWith('--') && !['--help', '-h'].includes(action)) {
|
|
19
|
+
await runMonitor(['--graph', ...process.argv.slice(action === 'view' ? 3 : 2)])
|
|
20
|
+
} else if (action === '--help' || action === '-h') {
|
|
21
|
+
console.log('axmap pipeline [view] [--cwd <repo>] [--once | --json]\naxmap pipeline --web [--cwd <repo>] [--port <local-port>]\n세션 그래프 · 역할 · 실제 요청/응답 · 토큰 · 이벤트를 표시합니다. TTY에서는 실시간 갱신합니다.\naxmap pipeline install-statusline --provider claude|agy\naxmap pipeline uninstall-statusline --provider claude|agy\n상태줄 설치는 기존 명령을 보존하며 새 창은 열지 않습니다.\n대화 안 패널은 MCP ax_pipeline, 진행 알림은 ax_pipeline_watch를 사용하세요.')
|
|
14
22
|
} else if (action === 'install-statusline' || action === 'uninstall-statusline') {
|
|
15
23
|
console.log(JSON.stringify(installPipelineStatusline(option('provider'), option('home'), action === 'uninstall-statusline'), null, 2))
|
|
16
24
|
} else if (action === 'statusline') {
|
package/tools/run-tests.mjs
CHANGED
|
@@ -40,7 +40,9 @@ if (files.length === 0) {
|
|
|
40
40
|
// Windows ConPTY owns native console workers. Exercise it after other test
|
|
41
41
|
// processes have exited so temporary console teardown cannot race the suite's
|
|
42
42
|
// many concurrent subprocess fixtures. Both batches remain mandatory.
|
|
43
|
-
|
|
43
|
+
// The lock test also asserts an elapsed-time ceiling, so concurrent fixtures
|
|
44
|
+
// can make it fail even when the lock retry path was never entered.
|
|
45
|
+
const nativeNames = new Set(['terminal-bridge.test.mjs', 'monitor-cli.test.mjs', 'view.test.mjs', 'ledgerlock.test.mjs'])
|
|
44
46
|
const native = process.platform === 'win32' ? files.filter(f => nativeNames.has(path.basename(f))) : []
|
|
45
47
|
const ordinary = files.filter(f => !native.includes(f))
|
|
46
48
|
for (const batch of [ordinary, ...native.map(f => [f])].filter(batch => batch.length)) {
|
package/tools/view.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runMonitor } from '../src/monitor-cli.mjs'
|
|
3
|
+
import { errorMessage } from '../src/session-types.mjs'
|
|
4
|
+
|
|
5
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
6
|
+
console.log('axmap view [--cwd <repo>] [--interval 1000] [--once | --json] [--no-color]\n실제 세션·선점·대기 메시지·최근 이벤트를 터미널에서 표시합니다. q / Esc / Ctrl+C 종료.')
|
|
7
|
+
} else {
|
|
8
|
+
try { await runMonitor(['--view', ...process.argv.slice(2)]) }
|
|
9
|
+
catch (e) { console.error(`axMap view: ${errorMessage(e)}`); process.exitCode = 1 }
|
|
10
|
+
}
|