axmap-cli 1.2.4 → 1.3.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/ax-start.md +2 -1
- package/CLAUDE.md +20 -73
- package/README.ko.md +124 -0
- package/README.md +81 -174
- package/app/lib/ssot.mjs +1 -1
- package/bin/axmap.mjs +62 -33
- package/governance/GOVERNANCE.md +7 -668
- package/governance/policy.example.json +31 -0
- package/mcp/README.md +13 -48
- package/mcp/SETUP-FOR-AI.md +14 -175
- package/mcp/server.mjs +123 -100
- package/package.json +6 -2
- package/src/overlap.mjs +1 -1
- package/src/repolock.mjs +31 -0
- package/src/repotarget.mjs +3 -0
- package/src/update.mjs +20 -2
- package/tools/smoke.mjs +9 -1
package/bin/axmap.mjs
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
import { stateDir } from '../src/repotarget.mjs'
|
|
47
47
|
import { homeRegistrations } from '../src/mcpstate.mjs'
|
|
48
48
|
import { compareBranchFor, formatOverlap } from '../src/overlap.mjs'
|
|
49
|
+
import { withRepoLock } from '../src/repolock.mjs'
|
|
49
50
|
|
|
50
51
|
/** 이 CLI 가 들어 있는 axMap 폴더. 갱신 확인과 setup 이 자기 위치를 알아야 한다. */
|
|
51
52
|
const SELF_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
@@ -666,8 +667,8 @@ function repoRoot() {
|
|
|
666
667
|
* 같은 명령을 쓰고 있었다 — 훅은 공통이라는 사실을 알면서 장부는 몰랐던 것이다.
|
|
667
668
|
*
|
|
668
669
|
* 🔴 **"못 찾음" 과 "없음" 을 뭉개지 않는다.** 여기서 하는 일은 *어디를 볼지*를
|
|
669
|
-
* 바로잡는 것뿐이다.
|
|
670
|
-
*
|
|
670
|
+
* 바로잡는 것뿐이다. 1.3.0부터 그 자리에 장부가 없으면 자동 준비한다.
|
|
671
|
+
* 준비 후에도 실제 선점이 없는 staged 파일은 verify가 거부한다.
|
|
671
672
|
*
|
|
672
673
|
* 판정이 안 서면 `root` 를 그대로 돌려준다(= 지금까지의 동작). 공통 디렉터리가
|
|
673
674
|
* `.git` 이라는 이름이 아닌 배치 — bare 저장소의 worktree 나 `--git-dir` 을 손으로
|
|
@@ -711,6 +712,12 @@ function ledgerDir(root) {
|
|
|
711
712
|
return path.join(axmapHome(root), LEDGER_REL)
|
|
712
713
|
}
|
|
713
714
|
|
|
715
|
+
function commonDir(root) {
|
|
716
|
+
const candidate = path.join(axmapHome(root), '.git')
|
|
717
|
+
try { if (fs.statSync(candidate).isDirectory()) return candidate } catch { /* nonstandard layout */ }
|
|
718
|
+
return path.resolve(root, gitOrDie(['rev-parse', '--git-common-dir'], { cwd: root }).out)
|
|
719
|
+
}
|
|
720
|
+
|
|
714
721
|
function claimsDir(root) {
|
|
715
722
|
return path.join(ledgerDir(root), 'claims')
|
|
716
723
|
}
|
|
@@ -989,18 +996,18 @@ function warnRetiredTakeover(flags, command) {
|
|
|
989
996
|
* 모두가 반납해 claim 파일이 하나도 없으면 reset --hard 가 그 디렉터리를 지운다.
|
|
990
997
|
* 그러면 "정상적으로 다 반납한 상태"가 "장부가 없음"으로 오진된다.
|
|
991
998
|
*/
|
|
992
|
-
function requireLedger(root) {
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
999
|
+
function requireLedger(root, flags = {}) {
|
|
1000
|
+
const preparing = fs.existsSync(path.join(commonDir(root), 'axmap-init.lock'))
|
|
1001
|
+
if (!preparing && fs.existsSync(path.join(ledgerDir(root), '.git'))) return
|
|
1002
|
+
// Keep bootstrap messages off stdout: status --json is consumed by MCP.
|
|
1003
|
+
const args = [path.join(SELF_ROOT, 'bin', 'axmap.mjs'), 'init', '--ledger-only']
|
|
1004
|
+
if (typeof flags.remote === 'string') args.push('--remote', flags.remote)
|
|
1005
|
+
const r = spawnSync(process.execPath, args, { cwd: root, encoding: 'utf8', windowsHide: true })
|
|
1006
|
+
const note = [r.stdout, r.stderr].filter(Boolean).join('\n').trim()
|
|
1007
|
+
if (note) console.error(note)
|
|
1008
|
+
if (r.status !== 0 || !fs.existsSync(path.join(ledgerDir(root), '.git'))) {
|
|
1009
|
+
die('장부를 자동으로 준비하지 못했습니다.\n' + (r.error?.message ?? '위 오류를 해결한 뒤 다시 시도하세요.'))
|
|
1002
1010
|
}
|
|
1003
|
-
die(msg)
|
|
1004
1011
|
}
|
|
1005
1012
|
|
|
1006
1013
|
/** 지금 실행 위치가 장부 worktree 자신인가. */
|
|
@@ -1441,6 +1448,11 @@ function cmdUsersRepair() {
|
|
|
1441
1448
|
|
|
1442
1449
|
function cmdInit(flags = {}) {
|
|
1443
1450
|
const root = repoRoot()
|
|
1451
|
+
const lock = path.join(commonDir(root), 'axmap-init.lock')
|
|
1452
|
+
try { return withRepoLock(lock, () => initLedger(root, flags)) } catch (e) { die(e.message) }
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function initLedger(root, flags) {
|
|
1444
1456
|
const dir = ledgerDir(root)
|
|
1445
1457
|
|
|
1446
1458
|
// .axmap 을 손으로 지웠어도 git 쪽에는 worktree 등록이 남아
|
|
@@ -1449,6 +1461,7 @@ function cmdInit(flags = {}) {
|
|
|
1449
1461
|
|
|
1450
1462
|
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
1451
1463
|
console.log(`장부가 이미 있습니다: ${LEDGER_REL}`)
|
|
1464
|
+
if (flags['ledger-only']) return
|
|
1452
1465
|
// 🔴 여기서 끝내면 안 된다. 쪽지함은 장부보다 나중에 생겼으므로, 이미 init 을
|
|
1453
1466
|
// 돌린 사람은 장부만 있고 쪽지함이 없다. 그 사람들이 다시 init 을 불렀을 때
|
|
1454
1467
|
// 받아 가는 자리가 여기다.
|
|
@@ -1484,19 +1497,29 @@ function cmdInit(flags = {}) {
|
|
|
1484
1497
|
}
|
|
1485
1498
|
const remote = r.name
|
|
1486
1499
|
if (remote) {
|
|
1487
|
-
// 고른 근거가 추론이었다면 못 박는다. 이미 설정에서 왔으면 다시 쓰지 않는다.
|
|
1488
|
-
if (!r.source.startsWith('git config')) {
|
|
1489
|
-
git(['config', 'axmap.remote', remote], { cwd: root })
|
|
1490
|
-
}
|
|
1491
1500
|
console.log(`장부 원격: ${remote} (${r.source})`)
|
|
1492
1501
|
}
|
|
1493
1502
|
|
|
1494
1503
|
// 원격에 이미 장부가 있으면 그것을 쓰고, 없으면 빈 트리로 새로 만든다.
|
|
1495
1504
|
let base = null
|
|
1496
|
-
if (remote
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1505
|
+
if (remote) {
|
|
1506
|
+
const probe = git(['ls-remote', '--exit-code', remote, `refs/heads/${LEDGER_BRANCH}`], { cwd: root })
|
|
1507
|
+
if (probe.code === 0) {
|
|
1508
|
+
gitOrDie(['fetch', '--quiet', remote, LEDGER_BRANCH], { cwd: root })
|
|
1509
|
+
base = gitOrDie(['rev-parse', 'FETCH_HEAD'], { cwd: root }).out
|
|
1510
|
+
console.log('원격 장부를 발견했습니다. 이어서 사용합니다.')
|
|
1511
|
+
} else if (probe.code !== 2) {
|
|
1512
|
+
die(`원격 장부를 확인하지 못했습니다. 새 장부를 만들지 않습니다.\n${probe.err || probe.out}`)
|
|
1513
|
+
}
|
|
1514
|
+
// Persist only a verified choice; failed bootstrap must be retryable.
|
|
1515
|
+
if (!r.source.startsWith('git config')) gitOrDie(['config', 'axmap.remote', remote], { cwd: root })
|
|
1516
|
+
}
|
|
1517
|
+
// Reattach a surviving local branch after .axmap was removed.
|
|
1518
|
+
if (!base) {
|
|
1519
|
+
const local = git(['rev-parse', '--verify', '--quiet', LEDGER_BRANCH], { cwd: root })
|
|
1520
|
+
if (local.code === 0) base = local.out
|
|
1521
|
+
}
|
|
1522
|
+
if (!base) {
|
|
1500
1523
|
// 작업 트리를 건드리지 않고 고아 브랜치를 만드는 방법:
|
|
1501
1524
|
// 빈 트리 -> 그 트리를 가리키는 부모 없는 커밋 -> 브랜치.
|
|
1502
1525
|
const tree = gitOrDie(['mktree'], { cwd: root, input: '' }).out
|
|
@@ -1515,8 +1538,9 @@ function cmdInit(flags = {}) {
|
|
|
1515
1538
|
// 임시 파일이 장부에 실리지 않게 한다 (writeFileAtomic 참고).
|
|
1516
1539
|
ensureLedgerIgnore(root)
|
|
1517
1540
|
|
|
1518
|
-
//
|
|
1519
|
-
const gi = path.
|
|
1541
|
+
// Automatic preparation must not modify tracked project files.
|
|
1542
|
+
const gi = path.resolve(root, gitOrDie(['rev-parse', '--git-path', 'info/exclude'], { cwd: root }).out)
|
|
1543
|
+
fs.mkdirSync(path.dirname(gi), { recursive: true })
|
|
1520
1544
|
const cur = fs.existsSync(gi) ? fs.readFileSync(gi, 'utf8') : ''
|
|
1521
1545
|
if (!cur.split(/\r?\n/).includes('.axmap/')) {
|
|
1522
1546
|
fs.writeFileSync(gi, (cur && !cur.endsWith('\n') ? cur + '\n' : cur) + '.axmap/\n')
|
|
@@ -1529,10 +1553,13 @@ function cmdInit(flags = {}) {
|
|
|
1529
1553
|
warnSolo(r)
|
|
1530
1554
|
}
|
|
1531
1555
|
|
|
1532
|
-
|
|
1533
|
-
|
|
1556
|
+
if (!flags['ledger-only']) {
|
|
1557
|
+
ensureBus(root)
|
|
1558
|
+
ensureUsers(root)
|
|
1559
|
+
}
|
|
1534
1560
|
|
|
1535
|
-
console.log(
|
|
1561
|
+
console.log(flags['ledger-only'] ? `장부 자동 준비 완료: ${LEDGER_BRANCH}` :
|
|
1562
|
+
`준비 완료. 장부 ${LEDGER_BRANCH} · 쪽지함 ${BUS_BRANCH} · 사람별 상태 ${USERS_BRANCH}`)
|
|
1536
1563
|
}
|
|
1537
1564
|
|
|
1538
1565
|
// ---------------------------------------------------------------------------
|
|
@@ -1541,7 +1568,7 @@ function cmdInit(flags = {}) {
|
|
|
1541
1568
|
|
|
1542
1569
|
function cmdClaim(positional, flags) {
|
|
1543
1570
|
const root = repoRoot()
|
|
1544
|
-
requireLedger(root)
|
|
1571
|
+
requireLedger(root, flags)
|
|
1545
1572
|
const me = agentName(flags)
|
|
1546
1573
|
const session = resolveSessionId(flags)
|
|
1547
1574
|
warnRetiredTakeover(flags, 'claim')
|
|
@@ -1653,7 +1680,9 @@ function cmdClaim(positional, flags) {
|
|
|
1653
1680
|
|
|
1654
1681
|
function cmdRelease(positional, flags) {
|
|
1655
1682
|
const root = repoRoot()
|
|
1656
|
-
|
|
1683
|
+
// A context switch must not initialize a repository merely to leave it.
|
|
1684
|
+
if (flags['if-present'] && !fs.existsSync(path.join(ledgerDir(root), '.git'))) process.exit(5)
|
|
1685
|
+
requireLedger(root, flags)
|
|
1657
1686
|
const me = agentName(flags)
|
|
1658
1687
|
const session = resolveSessionId(flags)
|
|
1659
1688
|
// 다른 세션이 잡은 것까지 함께 푸는 문. 기본은 **이 세션이 잡은 것만**이다.
|
|
@@ -1730,7 +1759,7 @@ function cmdRelease(positional, flags) {
|
|
|
1730
1759
|
|
|
1731
1760
|
function cmdRenew(flags) {
|
|
1732
1761
|
const root = repoRoot()
|
|
1733
|
-
requireLedger(root)
|
|
1762
|
+
requireLedger(root, flags)
|
|
1734
1763
|
const me = agentName(flags)
|
|
1735
1764
|
const session = resolveSessionId(flags)
|
|
1736
1765
|
const ttlMs = parseTtl(flags.ttl)
|
|
@@ -1775,7 +1804,7 @@ function cmdRenew(flags) {
|
|
|
1775
1804
|
|
|
1776
1805
|
function cmdStatus(flags) {
|
|
1777
1806
|
const root = repoRoot()
|
|
1778
|
-
requireLedger(root)
|
|
1807
|
+
requireLedger(root, flags)
|
|
1779
1808
|
syncLedger(root)
|
|
1780
1809
|
const t = now()
|
|
1781
1810
|
const all = readClaims(root)
|
|
@@ -1843,7 +1872,7 @@ function cmdVerify(flags) {
|
|
|
1843
1872
|
// commitAndPush 가 --no-verify 를 쓰므로 보통은 여기 오지 않지만,
|
|
1844
1873
|
// 사람이 장부를 손으로 고칠 때를 대비한 두 번째 그물이다.
|
|
1845
1874
|
if (isLedgerWorktree(root)) return
|
|
1846
|
-
requireLedger(root)
|
|
1875
|
+
requireLedger(root, flags)
|
|
1847
1876
|
const me = agentName(flags)
|
|
1848
1877
|
syncLedger(root)
|
|
1849
1878
|
|
|
@@ -2149,7 +2178,7 @@ function cmdAudit(flags) {
|
|
|
2149
2178
|
// 받으면 덮어쓴다. 받은 자리에서 sha 로 굳힌다.
|
|
2150
2179
|
tip = gitOrDie(['rev-parse', 'FETCH_HEAD'], { cwd: root }).out
|
|
2151
2180
|
} else {
|
|
2152
|
-
requireLedger(root)
|
|
2181
|
+
requireLedger(root, flags)
|
|
2153
2182
|
tip = gitOrDie(['rev-parse', 'HEAD'], { cwd: dir }).out
|
|
2154
2183
|
}
|
|
2155
2184
|
|
|
@@ -2690,7 +2719,7 @@ async function cmdUpdate(flags) {
|
|
|
2690
2719
|
if (notice) {
|
|
2691
2720
|
for (const l of notice.split('\n')) console.log(l)
|
|
2692
2721
|
console.log('')
|
|
2693
|
-
console.log(' 🔴 저절로 바뀌지 않습니다. 위
|
|
2722
|
+
console.log(' 🔴 저절로 바뀌지 않습니다. 위 명령을 직접 실행할 때만 바뀝니다.')
|
|
2694
2723
|
} else if (skewed) {
|
|
2695
2724
|
// 여기서 "최신입니다" 라고 말하면 거짓이 된다. 이 PC 의 실력은 **가장 낡은 것**이다.
|
|
2696
2725
|
console.log('레지스트리 기준으로는 최신이지만, 위 사본이 아직 안 맞습니다.')
|