kedovi 0.77.0 → 0.77.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/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  `kedovi upgrade` 가 올린 버전 사이의 항목을 표로 보여 준다. 배포할 때 이번 버전 항목을 맨 위에 더한다 (`## 버전` 아래 `| 구분 | 내용 |` 표, 한 줄씩 짧게).
4
4
 
5
+ ## 0.77.2
6
+
7
+ | 구분 | 내용 |
8
+ |---|---|
9
+ | 수정 | `kedovi team whoami --refresh` 가 계정 입력 창을 띄우다 `TypeError (reading 'log')` 로 죽던 것 |
10
+ | 수정 | 화면 없는 실행(에이전트)에서 저장소 계정을 못 읽으면 이유(원격 없음 · gh · glab 로그인 · ssh 키 · git credential)를 한 줄 알림 |
11
+
12
+ ## 0.77.1
13
+
14
+ | 구분 | 내용 |
15
+ |---|---|
16
+ | 수정 | 임시 폴더(시스템 임시 폴더 · `/private/tmp` · `/var/folders` · `~/.claude/jobs`)의 워크스페이스를 이 PC 목록에 넣지 않음. 이미 있으면 `kedovi list` · `upgrade --all` 때 정리 |
17
+ | 수정 | 공개판 CI 의 GitHub action 을 Node 24 판으로 (Node 20 지원 종료 경고), setup-node 자동 캐시 끔 |
18
+
5
19
  ## 0.77.0
6
20
 
7
21
  | 구분 | 내용 |
package/bin/kedovi.js CHANGED
@@ -79,8 +79,10 @@ const { spawnSync, execFileSync } = require('child_process');
79
79
  const kit = require('./kit.js');
80
80
  const gateway = require('./gateway.js');
81
81
 
82
- let p;
83
- const clack = () => (p = p || require('@clack/prompts'));
82
+ let clackMod;
83
+ const clack = () => (clackMod = clackMod || require('@clack/prompts'));
84
+ // p.log 등을 어디서 먼저 쓰든 화면 라이브러리를 불러온다 (늦게 불러오기 때문에 생기던 "undefined (reading 'log')" 방지)
85
+ const p = new Proxy({}, { get: (_, k) => clack()[k] });
84
86
 
85
87
  const BACK = Symbol('kedovi:back');
86
88
 
@@ -396,8 +398,21 @@ function writeRegistry(list) {
396
398
  function workspaceName(root) {
397
399
  try { return JSON.parse(read(path.join(root, 'workspace.json'))).project.name || path.basename(root); } catch { return path.basename(root); }
398
400
  }
399
- /** 목록에 올린다 (이미 있으면 이름만 갱신) — 받은 경로를 그대로 돌려준다 */
401
+ /**
402
+ * 임시 폴더의 워크스페이스인가 — 시험 · 에이전트 작업이 만든 것. 이 PC 목록(kedovi list · upgrade --all)에 올리지 않는다.
403
+ * 시스템 임시 폴더(os.tmpdir · /tmp · /private/tmp · /var/folders · Windows TEMP)와 ~/.claude/jobs
404
+ */
405
+ function isTempWorkspace(dir) {
406
+ const os = require('os');
407
+ const real = (x) => { try { return fs.realpathSync(x); } catch { return path.resolve(x); } };
408
+ const p = real(dir);
409
+ const roots = [os.tmpdir(), '/tmp', '/private/tmp', '/var/folders', '/private/var/folders', process.env.TEMP, process.env.TMP, path.join(os.homedir(), '.claude', 'jobs')]
410
+ .filter(Boolean).map(real);
411
+ return roots.some((r) => p === r || p.startsWith(r + path.sep));
412
+ }
413
+ /** 목록에 올린다 (이미 있으면 이름만 갱신) — 받은 경로를 그대로 돌려준다. 임시 폴더는 올리지 않는다 */
400
414
  function remember(root) {
415
+ if (isTempWorkspace(root) && !process.env.KEDOVI_HOME) { writeShim(root); return root; }
401
416
  const all = readRegistry();
402
417
  const name = workspaceName(root);
403
418
  const old = all.find((w) => w.path === root);
@@ -425,10 +440,10 @@ function writeShim(root) {
425
440
  }
426
441
  } catch { /* 편의 기능 — 못 만들어도 명령은 계속 */ }
427
442
  }
428
- /** 아직 워크스페이스로 남아 있는 것만 — 지웠거나 옮긴 폴더는 목록에서 뺀다 */
443
+ /** 아직 워크스페이스로 남아 있는 것만 — 지웠거나 옮긴 폴더 · 임시 폴더는 목록에서 뺀다 */
429
444
  function liveRegistry() {
430
445
  const all = readRegistry();
431
- const live = all.filter((w) => fs.existsSync(path.join(w.path, 'workspace.json')));
446
+ const live = all.filter((w) => fs.existsSync(path.join(w.path, 'workspace.json')) && (process.env.KEDOVI_HOME || !isTempWorkspace(w.path)));
432
447
  if (live.length !== all.length) writeRegistry(live);
433
448
  return live;
434
449
  }
@@ -1607,32 +1622,39 @@ function requireAdmin(root, what, { soft = false } = {}) {
1607
1622
 
1608
1623
  // 이 PC 사용자 확인 — 워크스페이스 repo 원격의 저장소 계정 username. ssh 면 ssh -T 인사말, https 면 gh · glab · git credential 의 사용자 이름
1609
1624
  const NOT_USER = new Set(['oauth2', 'x-access-token', 'gitlab-ci-token', 'personalaccesstoken', 'token', 'git']);
1610
- function detectAccount(root) {
1625
+ /** why: 못 찾았을 때 이유를 쌓는다 (화면 없는 실행에서 한 줄로 알린다) */
1626
+ function detectAccount(root, why = []) {
1611
1627
  const url = (spawnSync('git', ['-C', root, 'remote', 'get-url', 'origin'], { encoding: 'utf8' }).stdout || '').trim();
1612
- if (!url) return null;
1628
+ if (!url) { why.push('워크스페이스 repo 에 origin 원격이 없음'); return null; }
1613
1629
  const ssh = url.match(/^ssh:\/\/([\w.-]+)@([^:/]+)(?::(\d+))?\//) || url.match(/^([\w.-]+)@([^:/]+):/);
1614
1630
  // 로그인 창 · 브라우저가 뜨지 않게 (Git Credential Manager · askpass)
1615
1631
  const env = { ...process.env, GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'never' };
1616
1632
  delete env.GIT_ASKPASS; delete env.SSH_ASKPASS;
1617
- const run = (cmd, args, input) => { try { const r = spawnSync(cmd, args, { encoding: 'utf8', timeout: 8000, input, env }); return `${r.stdout || ''}\n${r.stderr || ''}`; } catch { return ''; } };
1633
+ const missing = new Set();
1634
+ const run = (cmd, args, input) => { try { const r = spawnSync(cmd, args, { encoding: 'utf8', timeout: 8000, input, env }); if (r.error && r.error.code === 'ENOENT') missing.add(cmd); return `${r.stdout || ''}\n${r.stderr || ''}`; } catch { return ''; } };
1618
1635
  if (ssh && !/^https?:/.test(url)) {
1619
1636
  const [, user, host, port] = ssh;
1620
1637
  const out = run('ssh', ['-T', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=5', '-o', 'StrictHostKeyChecking=accept-new', ...(port ? ['-p', port] : []), `${user}@${host}`]);
1621
1638
  const m = out.match(/Hi ([\w.-]+)!/) || out.match(/Welcome to GitLab, @([\w.-]+)!/) || out.match(/logged in as ([\w.-]+)/i);
1622
- return m ? { username: m[1], host, source: 'ssh' } : null;
1639
+ if (m) return { username: m[1], host, source: 'ssh' };
1640
+ why.push(`ssh -T ${user}@${host}${port ? ` -p ${port}` : ''} 이 계정 이름을 돌려주지 않음 (키 등록 · 접속 확인)`);
1641
+ return null;
1623
1642
  }
1624
1643
  let host = '';
1625
- try { host = new URL(url).hostname; } catch { return null; }
1644
+ try { host = new URL(url).hostname; } catch { why.push(`원격 주소를 읽지 못함 (${url})`); return null; }
1626
1645
  if (/github/.test(host)) {
1627
1646
  const u = run('gh', ['api', 'user', '--hostname', host, '--jq', '.login']).trim().split('\n')[0];
1628
1647
  if (/^[\w.-]+$/.test(u)) return { username: u, host, source: 'gh' };
1648
+ why.push(missing.has('gh') ? 'gh 없음' : `gh 로그인 안 됨 (gh auth login --hostname ${host})`);
1629
1649
  } else {
1630
1650
  const j = run('glab', ['api', 'user', '--hostname', host]);
1631
1651
  const m = j.match(/"username"\s*:\s*"([\w.-]+)"/);
1632
1652
  if (m) return { username: m[1], host, source: 'glab' };
1653
+ why.push(missing.has('glab') ? 'glab 없음' : `glab 로그인 안 됨 (glab auth login --hostname ${host})`);
1633
1654
  }
1634
1655
  const cred = run('git', ['-C', root, 'credential', 'fill'], `protocol=https\nhost=${host}\n\n`).match(/^username=(.+)$/m);
1635
1656
  if (cred && !NOT_USER.has(cred[1].trim().toLowerCase())) return { username: cred[1].trim(), host, source: 'git credential' };
1657
+ why.push(cred ? `git credential 의 사용자가 토큰 이름(${cred[1].trim()})` : 'git credential 에 저장된 사용자 없음');
1636
1658
  return null;
1637
1659
  }
1638
1660
 
@@ -1643,10 +1665,13 @@ async function ensureAccount(root, { refresh = false, ask = process.stdin.isTTY
1643
1665
  if (cur && cur.username && !refresh) return cur;
1644
1666
  // 못 찾은 기록이 있으면 7일 동안 다시 묻지 않는다 (start 마다 느려지지 않게. 다시: kedovi team whoami --refresh)
1645
1667
  if (cur && cur.source === 'none' && !refresh && (Date.now() - Date.parse(cur.checked || 0)) < 7 * 864e5) return null;
1646
- const found = detectAccount(root);
1668
+ const why = [];
1669
+ const found = detectAccount(root, why);
1647
1670
  let me = found ? { ...found, checked: today } : null;
1671
+ // 화면 없는 실행(에이전트 등)은 물을 수 없으니 이유를 한 줄 알린다
1672
+ if (!me && !ask) clack().log.warn(`저장소 계정을 읽지 못했습니다: ${why.join(' · ') || '이유 모름'}. 터미널에서 kedovi team whoami --refresh 를 직접 실행하면 입력해 적을 수 있습니다`);
1648
1673
  if (!me && ask) {
1649
- p.log.info('워크스페이스 원격에서 저장소 계정을 읽지 못했습니다. 팀장 · 담당 repo 판정에 쓰니 직접 적어 주세요 (나중에 다시 확인: kedovi team whoami --refresh)');
1674
+ clack().log.info('워크스페이스 원격에서 저장소 계정을 읽지 못했습니다. 팀장 · 담당 repo 판정에 쓰니 직접 적어 주세요 (나중에 다시 확인: kedovi team whoami --refresh)');
1650
1675
  const v = await clack().text({ message: '저장소 계정 username', placeholder: '예: hong01' });
1651
1676
  if (!clack().isCancel(v) && String(v || '').trim()) me = { username: String(v).trim(), host: '', source: 'input', checked: today };
1652
1677
  }
@@ -1669,7 +1694,7 @@ function adoptAccount(root, username) {
1669
1694
  const all = [...(t.leads || []), ...(t.admins || []), ...(t.members || [])];
1670
1695
  if (all.some((x) => sameUser(x, username)) || !all.some((x) => sameUser(x, gitName))) return;
1671
1696
  const lead = [...(t.leads || []), ...(t.admins || [])].some((x) => sameUser(x, gitName));
1672
- p.log.info(lead
1697
+ clack().log.info(lead
1673
1698
  ? `팀 설정의 팀장 ${gitName} 이(가) git user.name 으로 적혀 있습니다. 저장소 계정으로 바꾸세요: kedovi team lead add ${username} --nick <이름> 뒤 kedovi team lead remove ${gitName}`
1674
1699
  : `팀 설정에 ${gitName} 이(가) git user.name 으로 적혀 있습니다. 팀장에게 저장소 계정 ${username} 으로 바꿔 달라고 요청하세요 (kedovi team add ${username} --nick <이름> --repos …)`);
1675
1700
  }
@@ -1756,7 +1781,7 @@ async function cmdWhoami(root, args) {
1756
1781
  console.log(`이 PC 사용자: ${w.nickname ? `${w.nickname} · ` : ''}${w.username || '(모름)'} (확인: ${w.source})`);
1757
1782
  console.log(`역할: ${w.lead ? '팀장 (모든 repo · 설정 수정)' : w.person ? `팀원 · 담당 ${(w.repos || []).join(', ') || '없음'}` : '명단에 없음'}`);
1758
1783
  if (blocked.length) console.log(`고칠 수 없는 repo: ${blocked.join(', ')} (담당자 · 팀장에게 요청)`);
1759
- if (w.source === 'git user.name') console.log(' 저장소 계정을 아직 확인하지 못했습니다: kedovi team whoami --refresh');
1784
+ if (w.source === 'git user.name' && !args.includes('--refresh')) console.log(' 저장소 계정을 아직 확인하지 못했습니다: kedovi team whoami --refresh');
1760
1785
  }
1761
1786
 
1762
1787
  /** 팀장 · 팀원 · 담당 repo · 부를 이름 (workspace.json team.leads · members) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kedovi",
3
- "version": "0.77.0",
3
+ "version": "0.77.2",
4
4
  "private": false,
5
5
  "description": "Claude Code · Codex 겸용 AI 협업 워크스페이스 킷",
6
6
  "license": "MIT",