axmap-cli 0.0.1
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/ax-done.md +10 -0
- package/.claude/commands/ax-setup.md +31 -0
- package/.claude/commands/ax-start.md +15 -0
- package/.claude/commands/ax-tell.md +14 -0
- package/.claude/commands/ax-update.md +19 -0
- package/.claude/commands/ax.md +13 -0
- package/CLAUDE.md +309 -0
- package/LICENSE +20 -0
- package/README.md +207 -0
- package/app/README.md +366 -0
- package/app/eval/edges.mjs +242 -0
- package/app/lib/adjacent.mjs +125 -0
- package/app/lib/agentcli.mjs +153 -0
- package/app/lib/analyze.mjs +1159 -0
- package/app/lib/cochange.mjs +421 -0
- package/app/lib/datanodes.mjs +127 -0
- package/app/lib/entry.mjs +192 -0
- package/app/lib/featuregraph.mjs +389 -0
- package/app/lib/features.mjs +645 -0
- package/app/lib/fetchrepo-run.mjs +37 -0
- package/app/lib/fetchrepo.mjs +164 -0
- package/app/lib/flow.mjs +1089 -0
- package/app/lib/ladder.mjs +387 -0
- package/app/lib/langs.mjs +630 -0
- package/app/lib/live.mjs +346 -0
- package/app/lib/llm.mjs +594 -0
- package/app/lib/newfile.mjs +126 -0
- package/app/lib/prdiff.mjs +651 -0
- package/app/lib/reveal.mjs +316 -0
- package/app/lib/roots.mjs +186 -0
- package/app/lib/scope.mjs +342 -0
- package/app/lib/session.mjs +389 -0
- package/app/lib/slots.mjs +233 -0
- package/app/lib/ssot.mjs +277 -0
- package/app/lib/teamview.mjs +962 -0
- package/app/lib/terms.ko.mjs +169 -0
- package/app/server.mjs +1959 -0
- package/app/web/shell.css +538 -0
- package/app/web/shell.html +197 -0
- package/app/web/shell.js +638 -0
- package/app/web/stage.js +347 -0
- package/app/web/words.js +85 -0
- package/bin/axmap.mjs +1918 -0
- package/governance/GOVERNANCE.md +433 -0
- package/governance/gate.mjs +526 -0
- package/governance/vote.mjs +501 -0
- package/mcp/README.md +254 -0
- package/mcp/SETUP-FOR-AI.md +186 -0
- package/mcp/install.ps1 +341 -0
- package/mcp/install.sh +339 -0
- package/mcp/server.mjs +969 -0
- package/package.json +48 -0
- package/src/closure.mjs +343 -0
- package/src/governance.mjs +839 -0
- package/src/invariants.mjs +226 -0
- package/src/mrtarget.mjs +284 -0
- package/src/promote.mjs +177 -0
- package/src/protocol.mjs +423 -0
- package/src/repotarget.mjs +81 -0
- package/src/update.mjs +177 -0
- package/src/version.mjs +186 -0
- package/tools/bus.mjs +520 -0
- package/tools/cluster-experiment.mjs +256 -0
- package/tools/cluster-sweep.mjs +226 -0
- package/tools/make-icon.mjs +108 -0
- package/tools/mcp-register.mjs +269 -0
- package/tools/mr-target.mjs +49 -0
- package/tools/persona-bench.mjs +362 -0
- package/tools/pick-repo.mjs +229 -0
- package/tools/promote.mjs +550 -0
- package/tools/reveal-demo.mjs +158 -0
- package/tools/run-tests.mjs +42 -0
- package/tools/setup.mjs +490 -0
- package/tools/shortcut.mjs +121 -0
- package/tools/smoke.mjs +166 -0
- package/tools/topicgraph.py +154 -0
- package/tools/vendor.mjs +382 -0
- package/tools/version.mjs +115 -0
package/app/lib/live.mjs
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 실시간 계층 — 지금 AI 가 어디를 건드리고 있고 무엇을 하려는지.
|
|
3
|
+
*
|
|
4
|
+
* 두 개를 겹친다.
|
|
5
|
+
* 선언 axMap 장부의 claim — 어디를 건드리겠다고 했나 + 왜 (intent)
|
|
6
|
+
* 실제 git status + 파일 감시 — 실제로 지금 뭐가 바뀌었나
|
|
7
|
+
*
|
|
8
|
+
* 둘의 차이가 docs/DECISIONS.md 의 D3 다. PR 시점이 아니라 작업 중에 보면
|
|
9
|
+
* 몇 시간을 번다. 선언 밖 수정은 그 자리에서 경고가 된다.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'node:fs'
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import { spawnSync } from 'node:child_process'
|
|
15
|
+
import { activeClaims, coversPath, claimExpiresAt, normalizePath } from '../../src/protocol.mjs'
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// 선언 — axMap 장부
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 이 저장소에서 '나'는 누구인가. 팀원과 구분하는 기준이다.
|
|
23
|
+
*
|
|
24
|
+
* 이름을 정하는 순서는 CLI·MCP 와 같아야 한다 (SPEC "에이전트 이름을 정하는 순서").
|
|
25
|
+
* 여기만 다르면 CLI 는 `A` 로 claim 했는데 화면은 그 claim 을 **남의 것**으로
|
|
26
|
+
* 칠해 보여준다. 순서가 갈리는 것 자체가 버그이므로 세 곳을 함께 고친다.
|
|
27
|
+
*/
|
|
28
|
+
export function whoAmI(repoRoot) {
|
|
29
|
+
if (process.env.AXMAP_AGENT) return process.env.AXMAP_AGENT
|
|
30
|
+
const r = spawnSync('git', ['config', 'user.name'], {
|
|
31
|
+
cwd: repoRoot, encoding: 'utf8', windowsHide: true,
|
|
32
|
+
})
|
|
33
|
+
return (r.stdout ?? '').trim() || null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function readClaims(repoRoot, now = Date.now(), me = null) {
|
|
37
|
+
const dir = path.join(repoRoot, '.axmap', 'ledger', 'claims')
|
|
38
|
+
if (!fs.existsSync(dir)) return { available: false, claims: [] }
|
|
39
|
+
const claims = []
|
|
40
|
+
for (const f of fs.readdirSync(dir)) {
|
|
41
|
+
if (!f.endsWith('.json')) continue
|
|
42
|
+
try {
|
|
43
|
+
claims.push(JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')))
|
|
44
|
+
} catch {
|
|
45
|
+
// 장부가 손상됐으면 조용히 넘기지 않고 표시한다 (axMap 본체는 중단하지만
|
|
46
|
+
// 뷰어는 읽기 전용이므로 경고만 남긴다)
|
|
47
|
+
claims.push({ agent: `(손상: ${f})`, paths: [], since: new Date(0).toISOString(), ttlMs: 0, broken: true })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
available: true,
|
|
52
|
+
claims: activeClaims(
|
|
53
|
+
claims.filter((c) => !c.broken),
|
|
54
|
+
now,
|
|
55
|
+
).map((c) => ({
|
|
56
|
+
agent: c.agent,
|
|
57
|
+
task: c.task ?? null,
|
|
58
|
+
intent: c.intent ?? null,
|
|
59
|
+
// 표시용 — 사람 / 대화형 AI / 백그라운드 에이전트 / 다른 팀원
|
|
60
|
+
actor: c.actor ?? (c.agent === me ? 'human' : 'team'),
|
|
61
|
+
paths: c.paths.map(normalizePath),
|
|
62
|
+
since: c.since,
|
|
63
|
+
expiresAt: new Date(claimExpiresAt(c)).toISOString(),
|
|
64
|
+
})),
|
|
65
|
+
broken: claims.filter((c) => c.broken).map((c) => c.agent),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// 실제 — git 이 보는 변경
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
export function modifiedFiles(repoRoot) {
|
|
74
|
+
const r = spawnSync('git', ['status', '--porcelain=v1', '-uall'], {
|
|
75
|
+
cwd: repoRoot,
|
|
76
|
+
encoding: 'utf8',
|
|
77
|
+
windowsHide: true,
|
|
78
|
+
})
|
|
79
|
+
if (r.status !== 0) return { available: false, files: [] }
|
|
80
|
+
const files = []
|
|
81
|
+
for (const line of (r.stdout ?? '').split('\n')) {
|
|
82
|
+
if (line.length < 4) continue
|
|
83
|
+
const code = line.slice(0, 2).trim()
|
|
84
|
+
// 이름 변경은 "old -> new" 형태
|
|
85
|
+
const raw = line.slice(3).split(' -> ').pop().replace(/^"|"$/g, '')
|
|
86
|
+
files.push({ path: normalizePath(raw), code })
|
|
87
|
+
}
|
|
88
|
+
return { available: true, files }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// 겹치기 — 파일마다의 상태
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @returns Map<path, {state, agent, intent}>
|
|
97
|
+
* declared 선언했지만 아직 안 건드림
|
|
98
|
+
* working 선언했고 실제로 건드리는 중
|
|
99
|
+
* undeclared 선언 안 했는데 건드림 ← 경고
|
|
100
|
+
*/
|
|
101
|
+
export function overlay(claims, modified, knownPaths = []) {
|
|
102
|
+
const out = new Map()
|
|
103
|
+
|
|
104
|
+
for (const c of claims) {
|
|
105
|
+
for (const p of c.paths) {
|
|
106
|
+
const mark = { state: 'declared', agent: c.agent, actor: c.actor, intent: c.intent, task: c.task, claimed: p }
|
|
107
|
+
out.set(p, mark)
|
|
108
|
+
// 디렉터리를 claim 하면 그 아래 파일도 선점된 것이다.
|
|
109
|
+
// 그래프 노드는 파일 단위이므로 전파하지 않으면 화면에서 회색으로 남아
|
|
110
|
+
// "아무도 안 잡은 곳"처럼 보인다.
|
|
111
|
+
for (const f of knownPaths) {
|
|
112
|
+
if (f !== p && coversPath([p], f)) out.set(f, { ...mark })
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const m of modified) {
|
|
118
|
+
const owner = claims.find((c) => coversPath(c.paths, m.path))
|
|
119
|
+
if (owner) {
|
|
120
|
+
const covering = owner.paths.find((p) => coversPath([p], m.path))
|
|
121
|
+
out.set(m.path, {
|
|
122
|
+
state: 'working',
|
|
123
|
+
agent: owner.agent,
|
|
124
|
+
actor: owner.actor,
|
|
125
|
+
intent: owner.intent,
|
|
126
|
+
task: owner.task,
|
|
127
|
+
claimed: covering,
|
|
128
|
+
code: m.code,
|
|
129
|
+
})
|
|
130
|
+
// 디렉터리 claim 이면 그 디렉터리 자체도 '작업중'으로 물들인다
|
|
131
|
+
if (covering !== m.path && out.get(covering)?.state === 'declared') {
|
|
132
|
+
out.set(covering, { ...out.get(covering), state: 'working' })
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
out.set(m.path, { state: 'undeclared', agent: null, intent: null, code: m.code })
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return out
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 화면 상단에 띄울 한 줄 요약. */
|
|
143
|
+
/**
|
|
144
|
+
* 지금 누가 무엇을 잡고 있나 — 한 줄 요약.
|
|
145
|
+
*
|
|
146
|
+
* 🔴 장부를 **못 읽었으면 숫자를 내지 않는다.**
|
|
147
|
+
*
|
|
148
|
+
* 벤치마크에서 잡혔다. `/api/watch` 가 `ledgerAvailable: false` 를 보내면서
|
|
149
|
+
* 동시에 `claims: []` 와 `summary.agents: 0` 을 함께 보냈다. 텍스트만 보는
|
|
150
|
+
* 신입이 그 0 을 읽고 **"아무도 안 잡고 있어 부딪힐 대상 없음"** 이라고
|
|
151
|
+
* 확신 있게 틀린 답을 냈다.
|
|
152
|
+
*
|
|
153
|
+
* 같은 화면을 그림으로 본 신입은 맞게 "못 함" 이라고 했다 — 화면은 경고를
|
|
154
|
+
* 크게 그렸기 때문이다. **화면이 API 보다 정직했다.**
|
|
155
|
+
*
|
|
156
|
+
* 플래그는 있었다. 문제는 **0 이 더 크게 말한다**는 것이다. 읽는 쪽이
|
|
157
|
+
* 플래그를 안 보면 그건 우리 잘못이다 — 애초에 0 을 주지 말아야 한다.
|
|
158
|
+
*
|
|
159
|
+
* 이 저장소가 락에서 뿌리뽑은 fail-open 이고, 오늘 아침 SSOT 서버에서
|
|
160
|
+
* 고친 `recorded: 0` 과 정확히 같은 유형이다. 세 번째다.
|
|
161
|
+
*
|
|
162
|
+
* @param {boolean} available 장부를 실제로 읽었는가
|
|
163
|
+
*/
|
|
164
|
+
export function summarize(claims, modified, ov, available = true) {
|
|
165
|
+
if (!available) {
|
|
166
|
+
return {
|
|
167
|
+
// 🔴 null 은 "모른다" 이고 0 은 "없다" 다. 둘을 같은 값으로 말하지 않는다.
|
|
168
|
+
agents: null,
|
|
169
|
+
declaredPaths: null,
|
|
170
|
+
declared: null,
|
|
171
|
+
working: null,
|
|
172
|
+
undeclared: null,
|
|
173
|
+
// 수정 파일은 git 에서 오므로 장부와 무관하게 알 수 있다.
|
|
174
|
+
modified: modified.length,
|
|
175
|
+
unknown: true,
|
|
176
|
+
why: '장부를 읽지 못했습니다 — 0 이 아니라 모르는 것입니다',
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return summarizeKnown(claims, modified, ov)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function summarizeKnown(claims, modified, ov) {
|
|
183
|
+
const undeclared = [...ov.values()].filter((v) => v.state === 'undeclared').length
|
|
184
|
+
const working = [...ov.values()].filter((v) => v.state === 'working').length
|
|
185
|
+
/**
|
|
186
|
+
* 🔴 세 색깔에는 세 숫자가 있어야 한다.
|
|
187
|
+
*
|
|
188
|
+
* 범례는 색을 셋으로 나눠 설명하는데(선점만·작업 중·선언 밖) 여기서는 둘만
|
|
189
|
+
* 셌다. 화면은 없는 `claimed` 필드를 읽어 `?? 0` 으로 떨어졌고, 그래서
|
|
190
|
+
* **장부에 세 명이 잡고 있어도 "선점 0" 이라고 썼다.**
|
|
191
|
+
*
|
|
192
|
+
* 장부가 없는 환경에서만 돌리는 동안에는 이 버그가 안 보였다 — 0 이 맞는
|
|
193
|
+
* 답처럼 보였기 때문이다. 4회차에서 실제 claim 을 넣고서야 드러났다.
|
|
194
|
+
* 검증하지 않은 축은 조용히 썩는다.
|
|
195
|
+
*/
|
|
196
|
+
const declared = [...ov.values()].filter((v) => v.state === 'declared').length
|
|
197
|
+
return {
|
|
198
|
+
agents: claims.length,
|
|
199
|
+
declaredPaths: claims.reduce((a, c) => a + c.paths.length, 0),
|
|
200
|
+
modified: modified.length,
|
|
201
|
+
// 잡았지만 아직 안 건드린 것. 디렉터리 claim 은 그 아래 파일까지 물들므로
|
|
202
|
+
// 이것은 **파일 수**이지 claim 건수가 아니다 (화면 라벨도 그렇게 쓴다).
|
|
203
|
+
declared,
|
|
204
|
+
working,
|
|
205
|
+
undeclared, // D3 — 선언 ⊂ 실제. 이 숫자가 0 이 아니면 볼 이유가 있다
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// git 작성자별 작업 이력
|
|
211
|
+
//
|
|
212
|
+
// "이 사람이 뭘 건드렸나" 를 그래프에서 한번에 보여주기 위한 것.
|
|
213
|
+
// 장부(claim)는 지금 무슨 일이 일어나는지를, 이것은 지금까지 누가 무엇을 했는지를 말한다.
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
export function authorMap(repoRoot, { since = '3 months ago', maxCommits = 4000 } = {}) {
|
|
217
|
+
const r = spawnSync(
|
|
218
|
+
'git',
|
|
219
|
+
// --relative 가 반드시 필요하다. 없으면 git 저장소 루트 기준 경로가 나오는데,
|
|
220
|
+
// 뷰어가 저장소의 하위 디렉터리를 보고 있으면 그래프 노드 ID 와 하나도 맞지 않는다.
|
|
221
|
+
// (e101 은 저장소 루트가 e101_hj 이고 뷰어는 그 아래 BE_robot 을 본다)
|
|
222
|
+
['log', `--since=${since}`, `-n${maxCommits}`, '--no-merges', '--relative',
|
|
223
|
+
'--format=\x01%an\x02%ae\x02%ct\x02%h', '--name-only'],
|
|
224
|
+
{ cwd: repoRoot, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, windowsHide: true },
|
|
225
|
+
)
|
|
226
|
+
if (r.status !== 0) return { available: false, authors: [] }
|
|
227
|
+
|
|
228
|
+
const byAuthor = new Map()
|
|
229
|
+
let cur = null
|
|
230
|
+
for (const line of (r.stdout ?? '').split('\n')) {
|
|
231
|
+
if (line.startsWith('\x01')) {
|
|
232
|
+
const [name, email, ct, sha] = line.slice(1).split('\x02')
|
|
233
|
+
if (!byAuthor.has(name)) byAuthor.set(name, { name, email, commits: 0, last: 0, files: new Map() })
|
|
234
|
+
cur = byAuthor.get(name)
|
|
235
|
+
cur.commits++
|
|
236
|
+
cur.last = Math.max(cur.last, Number(ct) * 1000)
|
|
237
|
+
cur.lastSha = cur.lastSha ?? sha
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
const p = line.trim()
|
|
241
|
+
if (!cur || !p) continue
|
|
242
|
+
const n = normalizePath(p)
|
|
243
|
+
cur.files.set(n, (cur.files.get(n) ?? 0) + 1)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
available: true,
|
|
248
|
+
authors: [...byAuthor.values()]
|
|
249
|
+
.map((a) => ({
|
|
250
|
+
name: a.name,
|
|
251
|
+
email: a.email,
|
|
252
|
+
commits: a.commits,
|
|
253
|
+
lastAt: new Date(a.last).toISOString(),
|
|
254
|
+
fileCount: a.files.size,
|
|
255
|
+
files: [...a.files.entries()].sort((x, y) => y[1] - x[1]).map(([p, n]) => ({ path: p, touches: n })),
|
|
256
|
+
}))
|
|
257
|
+
.sort((x, y) => y.commits - x.commits),
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 커밋마다 바뀐 파일 목록. 기능 클러스터의 재료다 (features.mjs).
|
|
263
|
+
*
|
|
264
|
+
* `authorMap` 과 같은 git 호출을 쓰지만 결과가 다르다 — 저쪽은 사람별로
|
|
265
|
+
* 접고, 여기는 **커밋 단위 묶음이 그대로 필요하다.** 무엇이 무엇과 함께
|
|
266
|
+
* 바뀌었는지가 신호이므로 커밋 경계를 잃으면 안 된다.
|
|
267
|
+
*
|
|
268
|
+
* `--relative` 가 필요한 이유는 authorMap 주석과 같다. 뷰어가 저장소의
|
|
269
|
+
* 하위 디렉터리를 보고 있으면 루트 기준 경로는 노드 ID 와 하나도 안 맞는다.
|
|
270
|
+
*
|
|
271
|
+
* 코드 파일만 남긴다. 문서·설정이 섞이면 "릴리스 커밋"처럼 온갖 것이 함께
|
|
272
|
+
* 바뀐 커밋이 기능 신호를 뭉갠다.
|
|
273
|
+
*/
|
|
274
|
+
const CODE_EXT = /\.(py|mjs|cjs|js|jsx|ts|tsx|java|kt|go|rs|rb|php|cs|swift|c|cc|cpp|h|hpp)$/i
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @param {string} ref 어디서부터 거슬러 올라갈지. 기본은 `HEAD`.
|
|
278
|
+
*
|
|
279
|
+
* 🔴 PR 화면(prdiff.mjs Q4)이 `merge-base` 를 넘긴다. "평소 이 파일들이
|
|
280
|
+
* 함께 바뀌던 범위" 를 재는데 **이번 변경 자체가 그 표본에 들어가면**
|
|
281
|
+
* 비교가 무의미해진다 — 이번 커밋을 포함한 평균과 이번 커밋을 비교하게 된다.
|
|
282
|
+
* 기본값을 HEAD 로 두었으므로 기존 호출자의 동작은 그대로다.
|
|
283
|
+
*/
|
|
284
|
+
export function commitFiles(repoRoot, { since = '1 year ago', maxCommits = 4000, ref = 'HEAD' } = {}) {
|
|
285
|
+
const r = spawnSync(
|
|
286
|
+
'git',
|
|
287
|
+
// `--format=%x01` 이지 `--format=\x01` 이 아니다. 값에 `%` 가 없으면 git 은
|
|
288
|
+
// 그것을 **형식 이름**으로 보고 `invalid --pretty format` 으로 거부한다.
|
|
289
|
+
// (authorMap 은 `%an` 이 들어 있어 우연히 문제가 없었다)
|
|
290
|
+
//
|
|
291
|
+
// 제목(`%s`)도 함께 가져온다. 기능 이름을 지을 때 쓴다 —
|
|
292
|
+
// 단, **명단은 제목과 무관하다.** 제목이 `wip` 뿐인 저장소에서도
|
|
293
|
+
// 파일 묶음은 그대로 나오고 이름만 파일명 추출로 떨어진다.
|
|
294
|
+
['log', ref, `--since=${since}`, `-n${maxCommits}`, '--no-merges', '--relative',
|
|
295
|
+
'--format=%x01%s', '--name-only'],
|
|
296
|
+
{ cwd: repoRoot, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, windowsHide: true },
|
|
297
|
+
)
|
|
298
|
+
if (r.status !== 0) return { available: false, commits: [] }
|
|
299
|
+
|
|
300
|
+
const commits = []
|
|
301
|
+
let cur = null
|
|
302
|
+
for (const line of (r.stdout ?? '').split('\n')) {
|
|
303
|
+
if (line.startsWith('\x01')) {
|
|
304
|
+
cur = { subject: line.slice(1).trim(), files: [] }
|
|
305
|
+
commits.push(cur)
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
308
|
+
const p = line.trim()
|
|
309
|
+
if (!cur || !p || !CODE_EXT.test(p)) continue
|
|
310
|
+
// 하위 디렉터리를 보고 있으면 그 밖의 파일은 `../` 로 나온다. 그래프에 없는 것이다.
|
|
311
|
+
if (p.startsWith('..')) continue
|
|
312
|
+
cur.files.push(normalizePath(p))
|
|
313
|
+
}
|
|
314
|
+
return { available: true, commits: commits.filter((c) => c.files.length) }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// 감시
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
const SKIP = /[\\/](\.git|node_modules|__pycache__|\.axmap[\\/]ledger[\\/]\.git)[\\/]/
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 변경이 멎은 뒤 한 번만 알린다.
|
|
325
|
+
* AI 는 파일을 연달아 여러 개 쓰므로 매 이벤트마다 다시 계산하면 낭비다.
|
|
326
|
+
*/
|
|
327
|
+
export function watch(repoRoot, onChange, { debounceMs = 400 } = {}) {
|
|
328
|
+
let timer = null
|
|
329
|
+
let watcher
|
|
330
|
+
try {
|
|
331
|
+
watcher = fs.watch(repoRoot, { recursive: true }, (_type, filename) => {
|
|
332
|
+
if (filename && SKIP.test(`/${filename}/`)) return
|
|
333
|
+
clearTimeout(timer)
|
|
334
|
+
timer = setTimeout(onChange, debounceMs)
|
|
335
|
+
})
|
|
336
|
+
} catch (e) {
|
|
337
|
+
return { ok: false, error: e.message, close() {} }
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
ok: true,
|
|
341
|
+
close() {
|
|
342
|
+
clearTimeout(timer)
|
|
343
|
+
watcher.close()
|
|
344
|
+
},
|
|
345
|
+
}
|
|
346
|
+
}
|