axmap-cli 1.2.5 → 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/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
- * 바로잡는 것뿐이다. 바로잡은 자리에 장부가 없으면 `requireLedger` 는 지금처럼
670
- * 거부한다. 뭉개면 `init` 을 안 돌린 저장소에서 장부 없이 커밋이 통과한다.
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
- if (fs.existsSync(path.join(ledgerDir(root), '.git'))) return
994
- const home = axmapHome(root)
995
- let msg = `장부가 없습니다. 먼저 실행하세요:\n axmap init`
996
- // 링크된 worktree 에서 실행 중이면 어디를 보고 없다고 하는지 밝힌다.
997
- // 안 밝히면 "여기 .axmap 이 없는데?" 로 읽혀 장부를 또 만들게 된다 (그러면 진실이 둘이다).
998
- if (path.resolve(home) !== path.resolve(root)) {
999
- msg += `\n\n찾아본 곳: ${path.join(home, LEDGER_REL)}`
1000
- msg += `\n(여기는 링크된 worktree 라 장부는 메인 체크아웃에 하나만 둡니다.`
1001
- msg += `\n 이 폴더 안에 장부를 또 만들지 마세요 — 같은 저장소에 장부가 둘이 되면 락이 아닙니다.)`
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 && git(['fetch', '--quiet', remote, LEDGER_BRANCH], { cwd: root }).code === 0) {
1497
- base = git(['rev-parse', 'FETCH_HEAD'], { cwd: root }).out
1498
- console.log('원격 장부를 발견했습니다. 이어서 사용합니다.')
1499
- } else {
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
- // 장부 worktree 가 본 저장소에서 untracked 로 보이지 않게 한다.
1519
- const gi = path.join(root, '.gitignore')
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
- ensureBus(root)
1533
- ensureUsers(root)
1556
+ if (!flags['ledger-only']) {
1557
+ ensureBus(root)
1558
+ ensureUsers(root)
1559
+ }
1534
1560
 
1535
- console.log(`준비 완료. 장부 ${LEDGER_BRANCH} · 쪽지함 ${BUS_BRANCH} · 사람별 상태 ${USERS_BRANCH}`)
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
- requireLedger(root)
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