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
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git 주소를 받아 로컬로 가져온다 — 서사 1단계 (D14).
|
|
3
|
+
*
|
|
4
|
+
* "이해하고 싶은 저장소 주소를 넣는다" 가 사용자의 첫 동작인데, 그동안 뷰어는
|
|
5
|
+
* **로컬 경로만** 받았다. 첫 걸음이 아예 없었다.
|
|
6
|
+
*
|
|
7
|
+
* 🔴 blobless 클론을 쓴다 (`--filter=blob:none`).
|
|
8
|
+
*
|
|
9
|
+
* 공변경에는 커밋과 트리만 있으면 되고 파일 내용은 HEAD 것만 있으면 된다.
|
|
10
|
+
* 실측(immich): full clone 대비 받는 양이 크게 줄고, 커밋 수에 비례하므로
|
|
11
|
+
* 저장소가 커도 히스토리가 짧으면 금방 끝난다.
|
|
12
|
+
*
|
|
13
|
+
* 대신 rename 탐지가 네트워크를 타게 되는데, 그건 cochange.mjs 가
|
|
14
|
+
* promisor 설정을 보고 `--no-renames` 로 우회한다 (거기 주석 참조).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { execFileSync, spawnSync } from 'node:child_process'
|
|
18
|
+
import fs from 'node:fs'
|
|
19
|
+
import os from 'node:os'
|
|
20
|
+
import path from 'node:path'
|
|
21
|
+
|
|
22
|
+
/** 캐시 위치. 같은 주소를 다시 넣으면 다시 안 받는다. */
|
|
23
|
+
export const CACHE_DIR = path.join(os.tmpdir(), 'axmap-repos')
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 이게 원격 주소인가.
|
|
27
|
+
*
|
|
28
|
+
* 로컬 경로와 헷갈리지 않게 **명시적인 형태만** 받는다.
|
|
29
|
+
* `github.com/x/y` 처럼 스킴이 없는 것도 흔하므로 함께 인정한다.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* 스킴이 없는 주소도 받는다 — `lab.ssafy.com/팀/저장소` 처럼 붙여넣는 사람이 많다.
|
|
33
|
+
*
|
|
34
|
+
* 🔴 호스트를 **목록으로 두지 않는다.**
|
|
35
|
+
*
|
|
36
|
+
* 예전에는 `github|gitlab|bitbucket.com` 만 받았다. 그래서 사내·학교 GitLab
|
|
37
|
+
* (SSAFY 의 `lab.ssafy.com` 같은)을 쓰는 사람이 주소를 그대로 붙여넣으면
|
|
38
|
+
* **로컬 경로로 오인해서 "그런 폴더가 없습니다"** 를 봤다. 자기 저장소 주소가
|
|
39
|
+
* 틀렸다고 생각하게 된다. 목록은 반드시 누군가를 빠뜨린다.
|
|
40
|
+
*
|
|
41
|
+
* 대신 **모양**으로 가른다 — 점이 든 호스트 + 경로 두 칸 이상.
|
|
42
|
+
* 로컬과 헷갈리지 않게: `.`·`/` 로 시작하면 로컬이고, 윈도우 드라이브(`C:/…`)는
|
|
43
|
+
* 점이 없어서 자연히 걸러진다. GitLab 의 중첩 그룹은 받는다.
|
|
44
|
+
*
|
|
45
|
+
* 🔴 스킴 없는 **포트(`host:8080/a/b`)는 일부러 안 받는다.** git 의 scp 문법
|
|
46
|
+
* (`git@host:owner/repo`)과 모양이 겹쳐서, 받아 주면 어느 쪽인지 우리가 정해야
|
|
47
|
+
* 한다. 애매하면 거부한다(CLAUDE.md) — 포트가 필요하면 스킴을 붙이면 되고,
|
|
48
|
+
* 그때는 위의 스킴 갈래가 받는다.
|
|
49
|
+
*/
|
|
50
|
+
const HOSTISH = /^[A-Za-z0-9][\w-]*(\.[\w-]+)+\/[^/\s]+\/[^\s]+$/
|
|
51
|
+
|
|
52
|
+
export function isRemote(s) {
|
|
53
|
+
const t = String(s ?? '').trim()
|
|
54
|
+
if (!t || t.startsWith('.') || t.startsWith('/') || t.startsWith('\\')) return false
|
|
55
|
+
return /^(https?:\/\/|git@|ssh:\/\/|git:\/\/)/.test(t) || HOSTISH.test(t)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 주소를 정규화한다. 스킴이 없으면 https 를 붙인다. */
|
|
59
|
+
export function normalizeUrl(s) {
|
|
60
|
+
const t = s.trim().replace(/\/+$/, '')
|
|
61
|
+
if (/^(https?:\/\/|git@|ssh:\/\/|git:\/\/)/.test(t)) return t
|
|
62
|
+
return `https://${t}`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 캐시 폴더 이름.
|
|
67
|
+
*
|
|
68
|
+
* 🔴 `owner/repo` 를 그대로 폴더명으로 쓰면 서로 다른 두 저장소가 같은 자리에
|
|
69
|
+
* 떨어진다 (`a/util` 과 `b/util` 이 둘 다 `util`). 소유자까지 넣고,
|
|
70
|
+
* 경로에 못 쓰는 글자는 거부하지 말고 바꾸되 **원본을 함께 적어둔다** —
|
|
71
|
+
* 치환은 서로 다른 둘을 같게 만들 수 있어서, 어느 주소였는지 남겨야 한다.
|
|
72
|
+
*/
|
|
73
|
+
export function cacheName(url) {
|
|
74
|
+
/**
|
|
75
|
+
* 🔴 마지막 두 칸만 쓰면 GitLab 의 **중첩 그룹**에서 서로 다른 저장소가 같은
|
|
76
|
+
* 폴더에 떨어진다 — `lab.ssafy.com/A조/sub/proj` 와 `.../B조/sub/proj` 가 둘 다
|
|
77
|
+
* `sub__proj` 가 된다. SSAFY 처럼 그룹을 겹쳐 쓰는 곳에서는 흔한 모양이라
|
|
78
|
+
* **호스트와 경로를 통째로** 넣는다. 폴더 이름이 길어지는 것이 섞이는 것보다 낫다.
|
|
79
|
+
*/
|
|
80
|
+
const t = String(url)
|
|
81
|
+
.replace(/^[a-z+]+:\/\//i, '') // 스킴
|
|
82
|
+
.replace(/^[^@/]+@/, '') // git@ 같은 사용자 부분
|
|
83
|
+
.replace(/\.git$/, '')
|
|
84
|
+
.replace(/\/+$/, '')
|
|
85
|
+
return t.replace(/[^\w.-]/g, '_').slice(0, 120)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 원격 주소를 로컬로 가져온다.
|
|
90
|
+
*
|
|
91
|
+
* @returns {{dir: string, url: string, cached: boolean, commits: number}}
|
|
92
|
+
* @throws 클론이 실패하면 이유를 담아 던진다. 조용히 빈 폴더를 돌려주지 않는다 —
|
|
93
|
+
* 그러면 "저장소가 비었다" 는 틀린 결론이 화면에 뜬다.
|
|
94
|
+
*/
|
|
95
|
+
export function fetchRepo(input, { onLog = () => {} } = {}) {
|
|
96
|
+
const url = normalizeUrl(input)
|
|
97
|
+
const dir = path.join(CACHE_DIR, cacheName(url))
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 🔴 `.git` 이 있다고 멀쩡한 저장소는 아니다.
|
|
101
|
+
*
|
|
102
|
+
* 클론 **도중에 프로세스가 죽으면**(강제 종료·전원·타임아웃) 아래의 정리 코드가
|
|
103
|
+
* 못 돌아서 반쯤 받은 폴더가 그대로 남는다. 그 뒤로는 "캐시 있음" 으로 오인해
|
|
104
|
+
* **커밋 0개짜리 저장소를 정상인 것처럼** 돌려준다 — 화면에는 빈 그래프가 뜨고
|
|
105
|
+
* 아무 오류도 안 난다. 실제로 그렇게 됐다.
|
|
106
|
+
*
|
|
107
|
+
* HEAD 가 실제 커밋을 가리키는지로 판정한다. 안 가리키면 버리고 다시 받는다.
|
|
108
|
+
*/
|
|
109
|
+
if (fs.existsSync(path.join(dir, '.git')) && !hasHead(dir)) {
|
|
110
|
+
onLog(' (받다 만 캐시입니다 — 지우고 다시 받습니다)')
|
|
111
|
+
try { fs.rmSync(dir, { recursive: true, force: true }) } catch { /* 못 지우면 아래 클론이 실패로 말한다 */ }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
115
|
+
onLog(`캐시 사용: ${dir}`)
|
|
116
|
+
// 이미 받아둔 것은 최신으로만 맞춘다. 실패해도 있는 것으로 진행한다 —
|
|
117
|
+
// 네트워크가 끊겨도 이미 받은 저장소는 볼 수 있어야 한다.
|
|
118
|
+
const r = spawnSync('git', ['-C', dir, 'fetch', '--quiet', '--filter=blob:none', 'origin'], {
|
|
119
|
+
encoding: 'utf8', windowsHide: true,
|
|
120
|
+
})
|
|
121
|
+
if (r.status !== 0) onLog(' (최신화 실패 — 받아둔 상태로 엽니다)')
|
|
122
|
+
return { dir, url, cached: true, commits: countCommits(dir) }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true })
|
|
126
|
+
onLog(`클론: ${url}`)
|
|
127
|
+
onLog(` → ${dir}`)
|
|
128
|
+
const r = spawnSync(
|
|
129
|
+
'git',
|
|
130
|
+
['clone', '--filter=blob:none', '--single-branch', url, dir],
|
|
131
|
+
{ encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] },
|
|
132
|
+
)
|
|
133
|
+
if (r.status !== 0) {
|
|
134
|
+
// 반쯤 받다 만 폴더는 지운다. 남겨두면 다음 실행이 "캐시 있음" 으로 오인한다.
|
|
135
|
+
try { fs.rmSync(dir, { recursive: true, force: true }) } catch { /* 지울 수 없으면 그대로 둔다 */ }
|
|
136
|
+
const why = (r.stderr ?? '').trim().split('\n').filter(Boolean).slice(-2).join(' / ')
|
|
137
|
+
throw new Error(`클론 실패: ${url}\n ${why || '원인을 알 수 없습니다'}`)
|
|
138
|
+
}
|
|
139
|
+
return { dir, url, cached: false, commits: countCommits(dir) }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* HEAD 가 실제 커밋을 가리키는가.
|
|
144
|
+
*
|
|
145
|
+
* 받다 만 저장소와 `git init` 만 한 빈 폴더를 함께 가려낸다. 둘 다 `.git` 은
|
|
146
|
+
* 있지만 읽을 것이 없고, 그 상태로 분석하면 **빈 그래프를 정상 결과처럼** 보여준다.
|
|
147
|
+
*/
|
|
148
|
+
export function hasHead(dir) {
|
|
149
|
+
const r = spawnSync('git', ['-C', dir, 'rev-parse', '--verify', '--quiet', 'HEAD'],
|
|
150
|
+
{ encoding: 'utf8', windowsHide: true })
|
|
151
|
+
return r.status === 0 && !!r.stdout?.trim()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function countCommits(dir) {
|
|
155
|
+
try {
|
|
156
|
+
return Number(
|
|
157
|
+
execFileSync('git', ['-C', dir, 'rev-list', '--count', '--no-merges', 'HEAD'], {
|
|
158
|
+
encoding: 'utf8', windowsHide: true,
|
|
159
|
+
}).trim(),
|
|
160
|
+
)
|
|
161
|
+
} catch {
|
|
162
|
+
return 0
|
|
163
|
+
}
|
|
164
|
+
}
|