axmap-cli 1.3.0 → 1.3.2
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-setup.md +9 -12
- package/README.ko.md +13 -7
- package/README.md +11 -7
- package/bin/axmap.mjs +56 -21
- package/mcp/README.md +6 -18
- package/mcp/SETUP-FOR-AI.md +2 -3
- package/mcp/install.ps1 +2 -1
- package/mcp/install.sh +2 -2
- package/mcp/server.mjs +9 -7
- package/package.json +1 -1
- package/src/identity.mjs +62 -0
- package/tools/bus.mjs +222 -89
- package/tools/mcp-register.mjs +44 -14
- package/tools/setup.mjs +67 -74
package/tools/setup.mjs
CHANGED
|
@@ -1,43 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* npx -y axmap-cli@latest setup --remote https://lab.ssafy.com/우리팀/우리저장소.git
|
|
8
|
-
*
|
|
9
|
-
* ── 이 파일이 메우는 구멍 ───────────────────────────────────────────────────
|
|
10
|
-
*
|
|
11
|
-
* `mcp/install.sh` · `mcp/install.ps1` 은 이미 "홈에 복사하고 이 PC 의 AI CLI 에
|
|
12
|
-
* 등록한다" 를 한다. 다만 **axMap 파일이 이미 손에 있어야** 시작할 수 있다 —
|
|
13
|
-
* 스크립트만 따로 내려받으면 대놓고 거절한다. 그래서 지금까지 설치의 첫 단계는
|
|
14
|
-
* 언제나 "저장소를 통째로 clone" 이었다.
|
|
15
|
-
*
|
|
16
|
-
* npm 꾸러미가 그 첫 단계를 대신한다. `npx` 가 꾸러미를 받아 풀어 주므로,
|
|
17
|
-
* 이 파일이 도는 시점에는 axMap 원본이 이미 옆에 있다. 여기서는 그 뒤를 잇는다.
|
|
18
|
-
*
|
|
19
|
-
* 1. 이 PC 에 프로그램을 둔다 → mcp/install.{sh,ps1} 에 맡긴다 (다시 안 짠다)
|
|
20
|
-
* 2. 이 PC 의 AI CLI 에 등록한다 → 위와 같음
|
|
21
|
-
* 3. 대상 저장소에 장부를 만든다 → axmap init
|
|
22
|
-
* 4. 커밋 훅을 심는다 → axmap hook install
|
|
23
|
-
* 5. 슬래시 명령을 놓는다 → ~/.claude/commands/ (또는 저장소 안)
|
|
24
|
-
*
|
|
25
|
-
* ── 🔴 git 을 떼어내지 않는다 ───────────────────────────────────────────────
|
|
26
|
-
*
|
|
27
|
-
* 없앤 것은 **"axMap 코드를 받는 수단으로서의 git"**(clone·벤더링·git pull)이지
|
|
28
|
-
* **"협업 백엔드로서의 git"** 이 아니다. 장부와 쪽지는 그대로 고아 브랜치
|
|
29
|
-
* (`axmap/claims` · `axmap/bus`)로 오간다. git push 의 ref CAS(Compare-And-Swap —
|
|
30
|
-
* "내가 읽은 뒤로 바뀐 게 없을 때만 쓴다") 가 직렬화를 공짜로 주기 때문이고,
|
|
31
|
-
* 그것을 버리면 락 서버를 따로 세워야 한다. 그건 다른 제품이다.
|
|
32
|
-
*
|
|
33
|
-
* 그래서 대상이 git 저장소가 아니면 **여기서 멈춘다.** 반쯤 되는 상태로 두면
|
|
34
|
-
* claim 이 아무에게도 안 가면서 조용히 성공한다 — 이 도구에서 제일 나쁜 실패다.
|
|
35
|
-
*
|
|
36
|
-
* ── 종료 코드 ───────────────────────────────────────────────────────────────
|
|
37
|
-
*
|
|
38
|
-
* 0 다 됐다
|
|
39
|
-
* 1 시작할 수 없다 (node 가 낮다 · git 저장소가 아니다 · 이름을 못 정했다)
|
|
40
|
-
* 2 일부가 안 됐다. 무엇이 안 됐는지 !! 줄에 있다
|
|
3
|
+
* Install axMap globally from any directory. Installation is machine state;
|
|
4
|
+
* repository ledgers are prepared at runtime in the caller's working repo.
|
|
5
|
+
* Only explicit project options (--repo / --remote / --commands project)
|
|
6
|
+
* select a repository during setup. Commit enforcement remains opt-in.
|
|
41
7
|
*/
|
|
42
8
|
|
|
43
9
|
import { spawnSync } from 'node:child_process'
|
|
@@ -45,7 +11,9 @@ import fs from 'node:fs'
|
|
|
45
11
|
import os from 'node:os'
|
|
46
12
|
import path from 'node:path'
|
|
47
13
|
import { fileURLToPath } from 'node:url'
|
|
14
|
+
import { createInterface } from 'node:readline/promises'
|
|
48
15
|
|
|
16
|
+
import { displayNameError, readProfile, writeProfile, profilePath } from '../src/identity.mjs'
|
|
49
17
|
import { agentNameError } from '../src/protocol.mjs'
|
|
50
18
|
|
|
51
19
|
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
@@ -84,21 +52,21 @@ const has = (n) => argv.includes(n)
|
|
|
84
52
|
|
|
85
53
|
if (has('-h') || has('--help')) {
|
|
86
54
|
say(`
|
|
87
|
-
axmap setup —
|
|
55
|
+
axmap setup — 어느 폴더에서든 이 PC에 axMap을 전역 설치·등록한다
|
|
88
56
|
|
|
89
57
|
npx -y axmap-cli@latest setup
|
|
90
58
|
npx -y axmap-cli@latest setup --remote <이름 또는 git 주소>
|
|
91
59
|
|
|
92
|
-
--repo <경로>
|
|
60
|
+
--repo <경로> 선택 사항: 이 저장소의 장부·커밋 훅도 명시적으로 준비한다
|
|
93
61
|
--remote <이름|URL> 장부를 둘 원격. git 주소를 주면, 그 주소를 가리키는 원격이
|
|
94
62
|
없을 때 'axmap' 이라는 이름으로 하나 더한다
|
|
95
|
-
--name <이름>
|
|
63
|
+
--name <이름> 쪽지 표시 이름을 저장한다 (대화형 실행에서는 직접 입력)
|
|
96
64
|
--dest <경로> 프로그램을 둘 자리 (기본: ~/.axmap/app)
|
|
97
65
|
--commands <곳> 슬래시 명령을 둘 자리: global(기본) · project · none
|
|
98
66
|
--hook 프롬프트마다 쪽지를 받는 하네스 훅을 settings.json 에 더한다
|
|
99
67
|
(기본은 안 더한다 — 매 턴 도는 명령이라 사람이 고를 일이다)
|
|
100
68
|
--no-register 이 PC 의 AI CLI 등록을 건너뛴다
|
|
101
|
-
--no-ledger
|
|
69
|
+
--no-ledger --repo를 지정했어도 장부·커밋 훅을 건드리지 않는다
|
|
102
70
|
--dry-run 아무것도 쓰지 않고 무엇을 할지만 찍는다
|
|
103
71
|
|
|
104
72
|
종료 코드: 0 성공 · 1 시작 불가 · 2 일부 실패
|
|
@@ -132,35 +100,32 @@ ok('node', `v${process.versions.node}`)
|
|
|
132
100
|
|
|
133
101
|
// ── 2. 대상 저장소 ──────────────────────────────────────────────────────────
|
|
134
102
|
//
|
|
135
|
-
//
|
|
136
|
-
const
|
|
137
|
-
if (!
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
` 먼저 작업할 저장소 안으로 들어간 뒤 다시 실행하세요:\n` +
|
|
148
|
-
` cd <저장소> && npx -y axmap-cli@latest setup`,
|
|
149
|
-
)
|
|
103
|
+
// Installation has no working repository. Only explicit project options use one.
|
|
104
|
+
const projectRequested = has('--repo') || has('--remote') || commandsWhere === 'project'
|
|
105
|
+
if (has('--repo') && !flag('--repo')) die('--repo 뒤에 저장소 경로를 지정하세요.')
|
|
106
|
+
let REPO = null
|
|
107
|
+
if (projectRequested) {
|
|
108
|
+
const where = path.resolve(flag('--repo') ?? process.cwd())
|
|
109
|
+
const top = git(['rev-parse', '--show-toplevel'], where)
|
|
110
|
+
if (top.code !== 0) die(`프로젝트 옵션의 대상이 Git 저장소가 아닙니다: ${where}\n전역 설치만 하려면 --repo, --remote, --commands project 없이 axmap setup을 실행하세요.`)
|
|
111
|
+
REPO = path.resolve(top.out)
|
|
112
|
+
ok('저장소', `${REPO} (명시적 프로젝트 설정)`)
|
|
113
|
+
} else {
|
|
114
|
+
ok('설치 범위', '이 PC 전체 — 작업 repo는 이후 CLI·MCP 실행 위치에서 결정합니다')
|
|
150
115
|
}
|
|
151
|
-
const REPO = path.resolve(top.out)
|
|
152
|
-
ok('저장소', REPO)
|
|
153
116
|
|
|
154
117
|
// ── 3. 이름 ─────────────────────────────────────────────────────────────────
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
// 판정은 자기 claim 을 겹침으로 보지 않으므로 팀 전원이 서로의 영역을 아무
|
|
158
|
-
// 경고 없이 덮어쓴다. 그래서 이 PC 에서 이미 사람마다 다른 값만 후보로 쓴다.
|
|
118
|
+
// Display labels are independent of claim ownership. The saved label is the
|
|
119
|
+
// default on upgrades; a first setup starts from this person's Git identity.
|
|
159
120
|
let name = flag('--name')
|
|
160
121
|
let nameFrom = '--name'
|
|
122
|
+
if (has('--name') && !name) die('--name 뒤에 표시 이름을 지정하세요.')
|
|
123
|
+
let saved
|
|
124
|
+
try { saved = readProfile() } catch (e) { die(e.message) }
|
|
125
|
+
if (!name && saved) { name = saved.name; nameFrom = '저장된 표시 이름' }
|
|
161
126
|
if (!name) {
|
|
162
|
-
const g = git(['config', 'user.name'], REPO)
|
|
163
|
-
if (g.code === 0 && g.out) { name = g.out; nameFrom = 'git config user.name' }
|
|
127
|
+
const g = git(REPO ? ['config', 'user.name'] : ['config', '--global', 'user.name'], REPO ?? os.homedir())
|
|
128
|
+
if (g.code === 0 && g.out) { name = g.out; nameFrom = REPO ? 'git config user.name' : 'git config --global user.name' }
|
|
164
129
|
}
|
|
165
130
|
if (!name) {
|
|
166
131
|
try { name = os.userInfo().username; nameFrom = 'OS 사용자 이름' } catch { /* 아래 */ }
|
|
@@ -168,10 +133,36 @@ if (!name) {
|
|
|
168
133
|
if (!name) { name = os.hostname(); nameFrom = '호스트 이름' }
|
|
169
134
|
if (!name) die('이 PC 에서 쓸 이름을 정하지 못했습니다. --name "홍길동" 으로 직접 주세요.')
|
|
170
135
|
|
|
136
|
+
if (!has('--name') && !DRY && process.stdin.isTTY && process.stdout.isTTY) {
|
|
137
|
+
const input = createInterface({ input: process.stdin, output: process.stdout })
|
|
138
|
+
try {
|
|
139
|
+
for (;;) {
|
|
140
|
+
const answer = (await input.question(`쪽지에 표시할 이름 [${name}]: `)).trim().normalize('NFC')
|
|
141
|
+
const candidate = answer || name
|
|
142
|
+
const error = displayNameError(candidate)
|
|
143
|
+
if (error) { say(error); continue }
|
|
144
|
+
name = candidate
|
|
145
|
+
nameFrom = '표시 이름 입력'
|
|
146
|
+
break
|
|
147
|
+
}
|
|
148
|
+
} finally { input.close() }
|
|
149
|
+
}
|
|
150
|
+
|
|
171
151
|
// 검사기를 다시 짜지 않는다. 여기서 통과시킨 이름을 서버가 거부하면 설치가 거짓말이 된다.
|
|
172
|
-
const nameErr =
|
|
173
|
-
if (nameErr) die(`이 이름은
|
|
174
|
-
|
|
152
|
+
const nameErr = displayNameError(name)
|
|
153
|
+
if (nameErr) die(`이 표시 이름은 쓸 수 없습니다: ${name}\n\n${nameErr}`)
|
|
154
|
+
if (!DRY) {
|
|
155
|
+
try { writeProfile(name) } catch (e) { die(e.message) }
|
|
156
|
+
}
|
|
157
|
+
ok('쪽지 이름', `${name} (${nameFrom}${DRY ? ', 저장 예정' : ''})`)
|
|
158
|
+
const email = git(REPO ? ['config', 'user.email'] : ['config', '--global', 'user.email'], REPO ?? os.homedir()).out
|
|
159
|
+
if (email) ok('Git 이메일', `${email} — 실제 쪽지 주소는 작업 repo의 Git 설정을 사용합니다`)
|
|
160
|
+
else hm('Git 이메일', '작업 repo의 git config user.email을 사용합니다. 이메일은 별도로 저장하지 않습니다.')
|
|
161
|
+
say(` 이름 변경: axmap setup --name "새 이름" (${profilePath()})`)
|
|
162
|
+
// Changing a message label must not change ownership of existing claims.
|
|
163
|
+
const agentIdentity = git(REPO ? ['config', 'user.name'] : ['config', '--global', 'user.name'], REPO ?? os.homedir()).out
|
|
164
|
+
|| (() => { try { return os.userInfo().username } catch { return os.hostname() } })()
|
|
165
|
+
if (agentNameError(agentIdentity)) die('Git 사용자 이름을 선점 이름으로 쓸 수 없습니다. git config user.name을 확인하세요.')
|
|
175
166
|
|
|
176
167
|
// ── 4. 장부를 둘 원격 ───────────────────────────────────────────────────────
|
|
177
168
|
//
|
|
@@ -230,7 +221,7 @@ function configureRemote(spec) {
|
|
|
230
221
|
else ok('장부 원격', `git config axmap.remote = ${picked}`)
|
|
231
222
|
}
|
|
232
223
|
|
|
233
|
-
configureRemote(flag('--remote'))
|
|
224
|
+
if (REPO) configureRemote(flag('--remote'))
|
|
234
225
|
|
|
235
226
|
// ── 5. 프로그램을 이 PC 에 둔다 + AI CLI 에 등록한다 ────────────────────────
|
|
236
227
|
//
|
|
@@ -250,8 +241,8 @@ function installAndRegister() {
|
|
|
250
241
|
|
|
251
242
|
say('')
|
|
252
243
|
const r = win
|
|
253
|
-
? spawnSync('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', script, '-Source', SOURCE_ROOT, '-Dest', DEST, '-Name',
|
|
254
|
-
: spawnSync('bash', [script, '--source', SOURCE_ROOT, '--dest', DEST, '--name',
|
|
244
|
+
? spawnSync('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', script, '-Source', SOURCE_ROOT, '-Dest', DEST, '-Name', agentIdentity], { cwd: os.homedir(), stdio: 'inherit', windowsHide: true })
|
|
245
|
+
: spawnSync('bash', [script, '--source', SOURCE_ROOT, '--dest', DEST, '--name', agentIdentity], { cwd: os.homedir(), stdio: 'inherit' })
|
|
255
246
|
say('')
|
|
256
247
|
|
|
257
248
|
if (r.status === 0) ok('설치·등록', DEST)
|
|
@@ -266,6 +257,7 @@ installAndRegister()
|
|
|
266
257
|
// 부르면 `hook install` 이 그 임시 경로를 훅에 박고, npx 캐시가 비워지는 순간
|
|
267
258
|
// 훅이 없는 파일을 가리킨다 — 커밋할 때마다 오류가 나는데 원인은 안 보인다.
|
|
268
259
|
function ledgerAndHook() {
|
|
260
|
+
if (!REPO) { hm('장부·커밋 훅', '작업 repo의 장부는 첫 사용 때 자동 준비합니다. 커밋 훅은 axmap hook install로 선택합니다.'); return }
|
|
269
261
|
if (has('--no-ledger')) { hm('장부', '--no-ledger — 건너뜁니다'); return }
|
|
270
262
|
|
|
271
263
|
const installed = path.join(DEST, 'bin', 'axmap.mjs')
|
|
@@ -279,7 +271,7 @@ function ledgerAndHook() {
|
|
|
279
271
|
stdio: 'inherit',
|
|
280
272
|
// 이름을 넘겨서 판정을 한 번으로 못 박는다. 여기서 통과한 이름과 나중에 CLI 가
|
|
281
273
|
// 스스로 고르는 이름이 다르면, 자기가 잡은 것을 자기가 반납하지 못한다.
|
|
282
|
-
env: { ...process.env, AXMAP_AGENT:
|
|
274
|
+
env: { ...process.env, AXMAP_AGENT: agentIdentity },
|
|
283
275
|
windowsHide: true,
|
|
284
276
|
})
|
|
285
277
|
|
|
@@ -482,9 +474,10 @@ say('설치가 끝났습니다.')
|
|
|
482
474
|
say('')
|
|
483
475
|
say('다음:')
|
|
484
476
|
say(' 1. AI CLI 를 완전히 껐다가 다시 켭니다')
|
|
485
|
-
say(
|
|
477
|
+
say(' 2. 원하는 Git repo에서 AI CLI를 실행합니다. 실행한 repo에 자동으로 연결됩니다.')
|
|
478
|
+
say(' 3. "ax_status로 지금 누가 뭘 잡고 있는지 보여줘"로 시작합니다. repo마다 setup이나 init을 반복하지 않습니다.')
|
|
486
479
|
say('')
|
|
487
480
|
say(`${C.d} 파일을 고치기 전에 claim → 끝나면 release. 그게 전부입니다.${C.x}`)
|
|
488
|
-
say(`${C.d}
|
|
481
|
+
say(`${C.d} 조회: 원하는 Git repo에서 axmap status${C.x}`)
|
|
489
482
|
say('')
|
|
490
483
|
process.exit(EXIT.OK)
|