kedovi 0.76.0 → 0.77.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/CHANGELOG.md +23 -0
- package/bin/kedovi.js +216 -41
- package/bin/kit.js +142 -22
- package/bin/models.js +19 -6
- package/docs/commands.md +11 -9
- package/docs/model-catalog.md +11 -6
- package/docs/notify.md +2 -2
- package/docs/pack-making.md +2 -2
- package/docs/packs.md +3 -3
- package/docs/quickstart.md +2 -0
- package/docs/rules.md +2 -0
- package/docs/team-models.md +8 -5
- package/docs/workflow.md +19 -18
- package/docs/workspace.md +40 -9
- package/package.json +1 -1
- package/packs/core/bin/board.js +3 -2
- package/packs/core/bin/doctor.js +36 -7
- package/packs/core/bin/repo-check.js +36 -0
- package/packs/core/bin/team-status.js +11 -3
- package/packs/core/hooks/repo-guard.js +155 -0
- package/packs/core/hooks/settings-hint.js +49 -0
- package/packs/core/hooks.json +2 -0
- package/packs/core/lib/team.js +100 -0
- package/packs/core/models.json +95 -48
- package/packs/core/pack.json +1 -1
- package/packs/core/rules/10-communication.md +2 -1
- package/packs/core/settings-map.json +202 -0
- package/packs/core/skills/kedovi/SKILL.md +14 -12
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
`kedovi upgrade` 가 올린 버전 사이의 항목을 표로 보여 준다. 배포할 때 이번 버전 항목을 맨 위에 더한다 (`## 버전` 아래 `| 구분 | 내용 |` 표, 한 줄씩 짧게).
|
|
4
4
|
|
|
5
|
+
## 0.77.0
|
|
6
|
+
|
|
7
|
+
| 구분 | 내용 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| 기능 | 팀장 · 팀원 · 담당 repo: `workspace.json` `team.leads` `[{ username, nickname }]` (모든 repo · 설정) · `team.members[].repos` (담당 repo 만, 한 repo 에 여러 명). 담당 없는 repo 는 누구나. 옛 `team.admins` 는 팀장으로 읽음 |
|
|
10
|
+
| 기능 | 담당 아닌 repo 수정 막기: Claude 편집 · Codex `apply_patch` · 셸 쓰기 명령(훅 `repo-guard`), repo 커밋(pre-commit). 읽기 · `git pull` 은 된다. 막을 때 담당자 · 팀장 이름으로 안내 |
|
|
11
|
+
| 기능 | 이 PC 사용자를 저장소 계정으로 확인 (ssh · gh · glab · git credential, 못 읽으면 한 번 묻기). `kedovi team whoami [--refresh]` |
|
|
12
|
+
| 기능 | 명령 `kedovi team lead add\|remove` · `leads` · `add <username> --nick 이름 --repos a,b` · `repos` · `nick` |
|
|
13
|
+
| 규칙 | 사람을 nickname 으로 부름 (개인 지침 「팀과 담당」). 관리자 → 팀장으로 용어 통일 |
|
|
14
|
+
| 기능 | 설정 안내: "왜 이래 · 이런 거 안 되나 · 매번 …" 같은 불만이 킷 설정으로 풀리면 설정 · 범위(이 PC · 팀) · 명령을 알리고 바꿀지 물음 (훅 `settings-hint`, 설정 지도 `packs/core/settings-map.json`) |
|
|
15
|
+
| 문서 | README 「플러그인 · MCP · 도구」 (필수 · 권장 · 선택), `kedovi doctor` 가 권장 플러그인 · MCP 점검 |
|
|
16
|
+
| 적용 | Codex 는 훅이 바뀌어 `/hooks` 에서 다시 신뢰해야 한다. 팀장은 `kedovi team lead add <계정> --nick 이름` 뒤 팀원 담당을 `kedovi team add <계정> --nick 이름 --repos …` 로 |
|
|
17
|
+
|
|
18
|
+
## 0.76.1
|
|
19
|
+
|
|
20
|
+
| 구분 | 내용 |
|
|
21
|
+
|---|---|
|
|
22
|
+
| 모델 | Claude `haiku` = Haiku 5.5 (2026-10-07 출시, API $0.10·$0.50, effort 지원). 설명 · 가격 · 벤치마크 갱신 |
|
|
23
|
+
| 모델 | 표준 · 절약의 Claude 테스트 담당: GPT-6 Luna(Codex 위임) → Haiku 5.5 xhigh (LiveBench 같은 점수, 더 쌈, 위임 없음). 게이트웨이에서 haiku 가 4.5 면 GPT-6 Luna max |
|
|
24
|
+
| 모델 | Codex 선택지에 GPT-6.1 Terra (글쓰기 · 화면 이해 · 컴퓨터 사용, 추천에는 안 씀). GPT-6 Sol 은 Codex 목록에서 빠졌다고 표시 |
|
|
25
|
+
| 수정 | `npm run models:livebench`: LiveBench 데이터가 `/v1/` 로 옮겨 404 나던 것 |
|
|
26
|
+
| 적용 | 별칭을 쓰므로 `kedovi upgrade` 뒤 sync 만 하면 새 추천이 들어간다 (직접 고른 모델은 그대로) |
|
|
27
|
+
|
|
5
28
|
## 0.76.0
|
|
6
29
|
|
|
7
30
|
| 구분 | 내용 |
|
package/bin/kedovi.js
CHANGED
|
@@ -19,11 +19,14 @@
|
|
|
19
19
|
* kedovi repo guides [<이름> follow|kit|ignore] [--rescan [<이름>]] repo 안에 원래 있던 AI 지침(CLAUDE.md · AGENTS.md · 룰 · 스킬 등) 보기 · 따르는 방식 정하기 (follow = repo 지침 우선, 기본 · kit = 킷 규칙 우선 · ignore = 무시) · 다시 훑기
|
|
20
20
|
* kedovi team [id] [--take 1,2|all] 팀 현황 (모두의 핸드오프) · 한 사람 자세히 · 그 사람 TODO 항목을 내 TODO 로 인수
|
|
21
21
|
* kedovi team add|remove <id…> · members · restrict on|off 팀 명단 (workspace.json team) · 명단 밖 사람의 kedovi start 막기
|
|
22
|
-
* kedovi team edit allow|deny · admins [id…] 팀원 설정 수정 허용·불가 (불가면
|
|
22
|
+
* kedovi team edit allow|deny · admins [id…] 팀원 설정 수정 허용·불가 (불가면 팀장만 workspace.json·팩을 바꾼다)
|
|
23
|
+
* kedovi team lead add|remove <username> [--nick 이름] · leads 팀장 (모든 repo · 설정을 고친다)
|
|
24
|
+
* kedovi team add <username> [--nick 이름] [--repos a,b] · repos <username> <a,b|-> · nick <username> <이름> 팀원 · 담당 repo (담당 밖 repo 는 훅 · 커밋 검사가 막는다) · 부를 이름
|
|
25
|
+
* kedovi team whoami [--refresh] 이 PC 사용자 (저장소 계정 username · nickname · 팀장/팀원 · 담당 repo)
|
|
23
26
|
* kedovi detect --hint <스택> --contains <정규식> 사내 라이브러리를 거쳐 쓰는 스택의 감지 힌트를 더한다
|
|
24
27
|
* kedovi models [--migrate | --json] 모델 프로필·역할별·교차 검토 모델을 바꾼다 (화면 없이 실행하면 표, --json 은 JSON) / 퇴역 모델을 대체 모델로 일괄 교체
|
|
25
28
|
* kedovi models profile <이름> [--team] · set <역할> <도구> <모델> [--team] [--effort e] 화면 없이 (AI 가 대신 실행). 내 프로필은 팀보다 낮은 것만
|
|
26
|
-
* kedovi models allow-higher on|off 팀원이 팀보다 높은 프로필을 고를 수 있게 (팀,
|
|
29
|
+
* kedovi models allow-higher on|off 팀원이 팀보다 높은 프로필을 고를 수 있게 (팀, 팀장)
|
|
27
30
|
* kedovi agent --claude|--codex [--args "<인자>"] 화면 없이 메인 에이전트
|
|
28
31
|
* kedovi list 이 PC 에서 만들거나 쓴 워크스페이스 목록
|
|
29
32
|
* kedovi reset [항목,… | --all] [--yes] 이 PC 의 개인 설정을 처음 상태로 (이 PC 설정 · 텔레그램 채널 · 임시 기록 · 개인 지침 · 토큰 중 고른다). 팀 설정 · repos 는 그대로
|
|
@@ -45,13 +48,13 @@
|
|
|
45
48
|
* kedovi board [--no-open] [--no-fetch] 워크스페이스 현황판: 로드맵 · 팀원별 마지막 핸드오프 · 미완 작업 · 공지 + 기록 쪽 (.agents/board/, 이 PC 만 · 보기 전용)
|
|
46
49
|
* kedovi screens check [repo|폴더] 화면설계서 검사: 근거 ID 가 색인에 있는지 · 화면 ID 중복 · 기능 ID 누락 (screens 팩)
|
|
47
50
|
* kedovi datamodel check [repo|폴더] 데이터 설계 검사: 참조 · 기본키 · 중복 · 표 형식 · 컨벤션 · 기능 ID 대조 (datamodel 팩)
|
|
48
|
-
* kedovi secret set <이름> [--global] · list [--all] · test <이름> · rm <이름> [--global] · import [--global|--project] · exec [--only A,B] -- <명령> 비밀값(토큰): OS 저장소(키체인 · 자격 증명
|
|
51
|
+
* kedovi secret set <이름> [--global] · list [--all] · test <이름> · rm <이름> [--global] · import [--global|--project] · exec [--only A,B] -- <명령> 비밀값(토큰): OS 저장소(키체인 · 자격 증명 팀장 · libsecret)에 프로젝트별 · 전역으로. 값은 숨김 입력, 어디에도 출력하지 않는다
|
|
49
52
|
* kedovi gitlab pipeline [--repo R] [--ref 브랜치 | --mr 번호 | --id ID] [--limit N] [--log [줄 수]] [--json] GitLab 파이프라인 조회 (gitlab 팩, 읽기만): 최근 목록 · 단계별 작업 상태 · 실패한 작업의 로그 끝부분. MR 은 머지 커밋의 파이프라인도 찾는다
|
|
50
53
|
* kedovi prefs [language ko|en|team · style learn|normal|expert|team] [--team] · mermaid [install] · setup 응답 언어 · 대화 스타일(학습 · 일반 · 전문가) · 그림 미리보기(claude-mermaid). 이 PC 값이 팀 값보다 먼저. 인자 없이 = 지금 값
|
|
51
54
|
* kedovi timestamp [on|off|team] [--team] 답변 첫 줄에 요청 받은 시각(yyyy-MM-dd HH:mm:ss) 표시 (Claude Code, 기본 켜짐). 이 PC 값이 팀 값보다 먼저, team = 이 PC 값 지우기. 인자 없이 = 지금 값
|
|
52
|
-
* kedovi type [유형] [--approach a] [--code 이름,…] 유형 바꾸기: 분석·설계 → 구축 · 고도화 등. 유형 팩 · 현행 repo 중 고칠 것(개발 repo 로) · 진행 방식 · 로드맵 새 뼈대 (
|
|
53
|
-
* kedovi approach [sprint|full|issue] 진행 방식: MVP · 스프린트(최소 기능부터, 스프린트마다 데모하며 하나씩) · 워터폴 · 전체(전체 설계 뒤 구축) · 이슈형(운영 · 유지보수, 이슈 보드). 바꾸면 로드맵도 맞춘다 (
|
|
54
|
-
* kedovi team-mode [auto|fixed] · prefer <역할> <claude|codex>:<모델>|- · [--team] 팀 구성: 자동(리더가 작업마다 역할·모델, 요금제 여유도 반영) · 고정(프로필대로), 역할별 선호 모델. --team 은 팀 기본값(
|
|
55
|
+
* kedovi type [유형] [--approach a] [--code 이름,…] 유형 바꾸기: 분석·설계 → 구축 · 고도화 등. 유형 팩 · 현행 repo 중 고칠 것(개발 repo 로) · 진행 방식 · 로드맵 새 뼈대 (팀장)
|
|
56
|
+
* kedovi approach [sprint|full|issue] 진행 방식: MVP · 스프린트(최소 기능부터, 스프린트마다 데모하며 하나씩) · 워터폴 · 전체(전체 설계 뒤 구축) · 이슈형(운영 · 유지보수, 이슈 보드). 바꾸면 로드맵도 맞춘다 (팀장)
|
|
57
|
+
* kedovi team-mode [auto|fixed] · prefer <역할> <claude|codex>:<모델>|- · [--team] 팀 구성: 자동(리더가 작업마다 역할·모델, 요금제 여유도 반영) · 고정(프로필대로), 역할별 선호 모델. --team 은 팀 기본값(팀장)
|
|
55
58
|
* kedovi review status · mark [--repo R] [--by "<누가>"] · skip --reason "<사유>" [--repo R] 리뷰 확인(commit · push 전에 리뷰를 거쳤나): 대상 repo 마다 리뷰 안 된 줄 수 · 지금 상태를 리뷰했다고 기록 · 한 번 생략(사용자 승인 뒤). 켜기는 team-mode enforce review
|
|
56
59
|
* kedovi team-mode enforce review off|warn|block [--repos a,b|*] [--min-lines N] [--paths p1,p2] [--team] commit · push 전 리뷰 확인 (기본 꺼짐. Claude Code). 대상 · 기준 줄 수 · 경로는 팀 값(--team)
|
|
57
60
|
* kedovi import <폴더> [--json] 기존 AI 작업 폴더 훑기 (읽기만): 지침 · 룰 · 스킬 · 메모 · 핸드오프 · 스크립트 · 큰 폴더 분류, 문서 속 절대경로, 비밀값 의심 줄(값은 가림). 옮기기는 채팅에서 import 스킬로 (계획 → 승인 → 반영)
|
|
@@ -65,7 +68,7 @@
|
|
|
65
68
|
* kedovi team-mode enforce name off|warn|block · roster off|warn [--team] 훅 강제 수준. name: 팀 모드(Claude Code agent teams)에서 역할 에이전트를 이름 없이 띄우면 막기(기본) · 경고 · 끄기. roster: 한 요청의 첫 역할 에이전트 때 실행 방식 · 구성표를 알렸는지 한 줄(기본) · 끄기. 이 PC 값은 팀 값보다 세게만
|
|
66
69
|
* kedovi sync | check | doctor | packs 생성물 다시 만들기 · 생성물 검사 · 환경 진단 · 팩 목록
|
|
67
70
|
* kedovi upgrade [--add 팩] 이 킷 버전으로 워크스페이스의 bin·팩을 갱신
|
|
68
|
-
* kedovi upgrade --all [--scan <폴더>] [--yes] 이 PC 의 워크스페이스(kedovi list)를 설치된 킷 버전으로 한꺼번에 올린다.
|
|
71
|
+
* kedovi upgrade --all [--scan <폴더>] [--yes] 이 PC 의 워크스페이스(kedovi list)를 설치된 킷 버전으로 한꺼번에 올린다. 팀장 아님 · 원격에 받지 않은 커밋 · 커밋 안 한 변경이 있으면 건너뛴다. --scan 은 폴더 아래 워크스페이스를 찾아 목록에 더한다
|
|
69
72
|
* kedovi help [번호|명령|--all] 명령 도움말: 인자 없이 = 자주 쓰는 명령 · 분류 목록 (터미널이면 분류를 골라 보기) · 번호 = 그 분류의 명령 · --all = 전체
|
|
70
73
|
*/
|
|
71
74
|
'use strict';
|
|
@@ -435,9 +438,9 @@ function wsVersion(root) {
|
|
|
435
438
|
try { return (JSON.parse(read(path.join(root, 'workspace.json'))).kit || {}).version || null; } catch { return null; }
|
|
436
439
|
}
|
|
437
440
|
|
|
438
|
-
/** 지금 올리면 안 되는 이유 (없으면 null):
|
|
441
|
+
/** 지금 올리면 안 되는 이유 (없으면 null): 팀장 아님 · 원격에 받지 않은 커밋(팀원이 먼저 올렸을 수 있다) · 커밋 안 한 변경 */
|
|
439
442
|
function upgradeBlocker(root) {
|
|
440
|
-
try { if (!kit.lockState(root, kit.loadWorkspace(root)).admin) return '
|
|
443
|
+
try { if (!kit.lockState(root, kit.loadWorkspace(root)).admin) return '팀장만 올릴 수 있음 (팀원 설정 수정이 막혀 있음)'; } catch { /* 옛 킷 */ }
|
|
441
444
|
const git = (...a) => spawnSync('git', ['-C', root, ...a], { encoding: 'utf8', timeout: 6000, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
|
|
442
445
|
if (git('rev-parse', '--is-inside-work-tree').status !== 0) return null;
|
|
443
446
|
if (git('rev-parse', '--abbrev-ref', '@{u}').status === 0) {
|
|
@@ -1413,10 +1416,10 @@ async function cmdInit(argv) {
|
|
|
1413
1416
|
if (leadId) {
|
|
1414
1417
|
const f = path.join(dir, 'handoffs', leadId, 'TODO.md');
|
|
1415
1418
|
if (!fs.existsSync(f)) { fs.mkdirSync(path.dirname(f), { recursive: true }); fs.writeFileSync(f, `# TODO (${leadId})\n`); }
|
|
1416
|
-
// 만든 사람이
|
|
1419
|
+
// 만든 사람이 팀장 — 팀원 설정 수정을 막을 때(kedovi team edit deny) 이 사람만 설정을 바꾼다
|
|
1417
1420
|
const wsFile = path.join(dir, 'workspace.json');
|
|
1418
1421
|
const raw = JSON.parse(read(wsFile));
|
|
1419
|
-
if (!(raw.team && (raw.team.admins || []).length)) { raw.team = { ...(raw.team || {}),
|
|
1422
|
+
if (!(raw.team && ((raw.team.admins || []).length || (raw.team.leads || []).length))) { const acct = detectAccount(dir); raw.team = { ...(raw.team || {}), leads: [acct ? { username: acct.username } : { username: leadId, source: 'git' }] }; if (acct) { writeLocal(dir, { me: { ...acct, checked: new Date().toISOString().slice(0, 10) } }); fs.writeFileSync(path.join(dir, 'handoffs', leadId, '.account'), `${acct.username}\n`); } fs.writeFileSync(wsFile, JSON.stringify(raw, null, 2) + '\n'); }
|
|
1420
1423
|
}
|
|
1421
1424
|
// repo 별 상태: clone 실패 · 빠진 repo 는 질문 화면들 뒤로 밀려 안 보이므로 끝에 다시 보인다 (다시 받기 = kedovi install, sync 는 받지 않는다)
|
|
1422
1425
|
const repoNote = repoStatus(dir, res.repos || []);
|
|
@@ -1590,49 +1593,128 @@ function teamOf(root) {
|
|
|
1590
1593
|
try { return JSON.parse(read(path.join(root, 'workspace.json'))).team || {}; } catch { return {}; }
|
|
1591
1594
|
}
|
|
1592
1595
|
|
|
1593
|
-
/** 설정 잠금(팀원 설정 수정 불가)일 때
|
|
1596
|
+
/** 설정 잠금(팀원 설정 수정 불가)일 때 팀장만. 아니면 멈춘다. soft 면 경고하고 false */
|
|
1594
1597
|
function requireAdmin(root, what, { soft = false } = {}) {
|
|
1595
1598
|
let st = { admin: true };
|
|
1596
1599
|
try { st = kit.lockState(root, kit.loadWorkspace(root)); } catch { /* 옛 킷 */ }
|
|
1597
1600
|
if (st.admin) return true;
|
|
1598
|
-
const
|
|
1601
|
+
const t = teamOf(root);
|
|
1602
|
+
const leads = [...(t.leads || []), ...(t.admins || [])].map((x) => (typeof x === 'string' ? x : x.nickname || x.username));
|
|
1603
|
+
const msg = `팀원 설정 수정이 막혀 있습니다 (팀장: ${leads.join(', ')}). ${what} 은(는) 팀장에게 요청하세요`;
|
|
1599
1604
|
if (soft) { p.log.warn(msg); return false; }
|
|
1600
1605
|
fail(msg);
|
|
1601
1606
|
}
|
|
1602
1607
|
|
|
1608
|
+
// 이 PC 사용자 확인 — 워크스페이스 repo 원격의 저장소 계정 username. ssh 면 ssh -T 인사말, https 면 gh · glab · git credential 의 사용자 이름
|
|
1609
|
+
const NOT_USER = new Set(['oauth2', 'x-access-token', 'gitlab-ci-token', 'personalaccesstoken', 'token', 'git']);
|
|
1610
|
+
function detectAccount(root) {
|
|
1611
|
+
const url = (spawnSync('git', ['-C', root, 'remote', 'get-url', 'origin'], { encoding: 'utf8' }).stdout || '').trim();
|
|
1612
|
+
if (!url) return null;
|
|
1613
|
+
const ssh = url.match(/^ssh:\/\/([\w.-]+)@([^:/]+)(?::(\d+))?\//) || url.match(/^([\w.-]+)@([^:/]+):/);
|
|
1614
|
+
// 로그인 창 · 브라우저가 뜨지 않게 (Git Credential Manager · askpass)
|
|
1615
|
+
const env = { ...process.env, GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'never' };
|
|
1616
|
+
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 ''; } };
|
|
1618
|
+
if (ssh && !/^https?:/.test(url)) {
|
|
1619
|
+
const [, user, host, port] = ssh;
|
|
1620
|
+
const out = run('ssh', ['-T', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=5', '-o', 'StrictHostKeyChecking=accept-new', ...(port ? ['-p', port] : []), `${user}@${host}`]);
|
|
1621
|
+
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;
|
|
1623
|
+
}
|
|
1624
|
+
let host = '';
|
|
1625
|
+
try { host = new URL(url).hostname; } catch { return null; }
|
|
1626
|
+
if (/github/.test(host)) {
|
|
1627
|
+
const u = run('gh', ['api', 'user', '--hostname', host, '--jq', '.login']).trim().split('\n')[0];
|
|
1628
|
+
if (/^[\w.-]+$/.test(u)) return { username: u, host, source: 'gh' };
|
|
1629
|
+
} else {
|
|
1630
|
+
const j = run('glab', ['api', 'user', '--hostname', host]);
|
|
1631
|
+
const m = j.match(/"username"\s*:\s*"([\w.-]+)"/);
|
|
1632
|
+
if (m) return { username: m[1], host, source: 'glab' };
|
|
1633
|
+
}
|
|
1634
|
+
const cred = run('git', ['-C', root, 'credential', 'fill'], `protocol=https\nhost=${host}\n\n`).match(/^username=(.+)$/m);
|
|
1635
|
+
if (cred && !NOT_USER.has(cred[1].trim().toLowerCase())) return { username: cred[1].trim(), host, source: 'git credential' };
|
|
1636
|
+
return null;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/** 이 PC 사용자 저장 (.agents/local.json me). 이미 있고 refresh 가 아니면 그대로. 못 찾으면 화면에서 한 번 묻는다 */
|
|
1640
|
+
async function ensureAccount(root, { refresh = false, ask = process.stdin.isTTY } = {}) {
|
|
1641
|
+
const cur = readLocal(root).me;
|
|
1642
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
1643
|
+
if (cur && cur.username && !refresh) return cur;
|
|
1644
|
+
// 못 찾은 기록이 있으면 7일 동안 다시 묻지 않는다 (start 마다 느려지지 않게. 다시: kedovi team whoami --refresh)
|
|
1645
|
+
if (cur && cur.source === 'none' && !refresh && (Date.now() - Date.parse(cur.checked || 0)) < 7 * 864e5) return null;
|
|
1646
|
+
const found = detectAccount(root);
|
|
1647
|
+
let me = found ? { ...found, checked: today } : null;
|
|
1648
|
+
if (!me && ask) {
|
|
1649
|
+
p.log.info('워크스페이스 원격에서 저장소 계정을 읽지 못했습니다. 팀장 · 담당 repo 판정에 쓰니 직접 적어 주세요 (나중에 다시 확인: kedovi team whoami --refresh)');
|
|
1650
|
+
const v = await clack().text({ message: '저장소 계정 username', placeholder: '예: hong01' });
|
|
1651
|
+
if (!clack().isCancel(v) && String(v || '').trim()) me = { username: String(v).trim(), host: '', source: 'input', checked: today };
|
|
1652
|
+
}
|
|
1653
|
+
if (!me) { if (ask) writeLocal(root, { me: { source: 'none', checked: today } }); return null; } // 물을 수 없던 실행(화면 없음)은 기록하지 않는다
|
|
1654
|
+
writeLocal(root, { me });
|
|
1655
|
+
return me;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
/**
|
|
1659
|
+
* 계정을 찾으면: (1) 핸드오프 폴더에 계정을 적는다 (팀 현황이 git user.name 폴더 ↔ username 을 잇는다. 합류 기록 · 다음 핸드오프와 함께 커밋된다)
|
|
1660
|
+
* (2) 팀 설정에 이 PC 가 git user.name 으로 적혀 있으면 계정으로 바꾸는 명령을 알린다 (자동으로 고치지 않는다: 다른 사람이 이름만 바꿔 그 자리를 가져가지 못하게)
|
|
1661
|
+
*/
|
|
1662
|
+
function adoptAccount(root, username) {
|
|
1663
|
+
const gitName = memberId(root);
|
|
1664
|
+
if (!gitName || !username) return;
|
|
1665
|
+
const dir = path.join(root, 'handoffs', gitName);
|
|
1666
|
+
try { if (fs.existsSync(dir) && (fs.existsSync(path.join(dir, '.account')) ? read(path.join(dir, '.account')).trim() : '') !== username) fs.writeFileSync(path.join(dir, '.account'), `${username}\n`); } catch { /* 편의 */ }
|
|
1667
|
+
if (gitName === username) return;
|
|
1668
|
+
const t = teamOf(root);
|
|
1669
|
+
const all = [...(t.leads || []), ...(t.admins || []), ...(t.members || [])];
|
|
1670
|
+
if (all.some((x) => sameUser(x, username)) || !all.some((x) => sameUser(x, gitName))) return;
|
|
1671
|
+
const lead = [...(t.leads || []), ...(t.admins || [])].some((x) => sameUser(x, gitName));
|
|
1672
|
+
p.log.info(lead
|
|
1673
|
+
? `팀 설정의 팀장 ${gitName} 이(가) git user.name 으로 적혀 있습니다. 저장소 계정으로 바꾸세요: kedovi team lead add ${username} --nick <이름> 뒤 kedovi team lead remove ${gitName}`
|
|
1674
|
+
: `팀 설정에 ${gitName} 이(가) git user.name 으로 적혀 있습니다. 팀장에게 저장소 계정 ${username} 으로 바꿔 달라고 요청하세요 (kedovi team add ${username} --nick <이름> --repos …)`);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/** 사람 항목 다루기 — 문자열(옛 형식)도 객체로 맞춘다 */
|
|
1678
|
+
const personOf = (x) => (typeof x === 'string' ? { username: x } : { ...x });
|
|
1679
|
+
const sameUser = (x, u) => personOf(x).username === u;
|
|
1680
|
+
function optVal(args, name) { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; }
|
|
1681
|
+
|
|
1603
1682
|
/** 리더용 — 명단 추가·삭제·보기, 명단 제한 켜기·끄기. workspace.json 을 고치고 commit · push 를 안내한다 */
|
|
1604
1683
|
function cmdTeamAdmin(root, [sub, ...ids]) {
|
|
1684
|
+
if (sub === 'whoami') return cmdWhoami(root, ids);
|
|
1685
|
+
if (['lead', 'leads', 'repos', 'nick'].includes(sub) || (sub === 'add' && (ids.includes('--nick') || ids.includes('--repos')))) return cmdTeamPeople(root, [sub, ...ids]);
|
|
1605
1686
|
const file = path.join(root, 'workspace.json');
|
|
1606
1687
|
const raw = JSON.parse(read(file));
|
|
1607
1688
|
const team = raw.team || {};
|
|
1608
1689
|
team.members = team.members || [];
|
|
1609
1690
|
if (sub !== 'members' && !(sub === 'admins' && !ids.length)) requireAdmin(root, `kedovi team ${sub}`);
|
|
1610
1691
|
if (sub === 'edit') {
|
|
1611
|
-
// 팀원 설정 수정 허용·불가. deny 면
|
|
1692
|
+
// 팀원 설정 수정 허용·불가. deny 면 팀장만 workspace.json·팩·생성물을 바꾸고, 팀원은 자기 handoffs/ 와 로드맵(handoffs/PLAN.md)만 커밋한다
|
|
1612
1693
|
const v = ids[0];
|
|
1613
1694
|
if (!['allow', 'deny'].includes(v)) fail('사용법: kedovi team edit allow|deny');
|
|
1614
|
-
const me = memberId(root);
|
|
1615
|
-
team.
|
|
1616
|
-
|
|
1695
|
+
const me = (readLocal(root).me || {}).username || memberId(root);
|
|
1696
|
+
if (!(team.leads || []).length && !(team.admins || []).length && me) team.leads = [{ username: me, ...((readLocal(root).me || {}).username ? {} : { source: 'git' }) }];
|
|
1697
|
+
const leadNames = [...(team.leads || []), ...(team.admins || [])].map((x) => personOf(x).nickname || personOf(x).username);
|
|
1698
|
+
if (v === 'deny' && !leadNames.length) fail('팀장이 없습니다. 먼저 kedovi team lead add <username>');
|
|
1617
1699
|
team.lock = v === 'deny';
|
|
1618
1700
|
raw.team = team;
|
|
1619
1701
|
fs.writeFileSync(file, JSON.stringify(raw, null, 2) + '\n');
|
|
1620
1702
|
runWorkspaceKit(root, ['sync', '--quiet']);
|
|
1621
|
-
console.log(v === 'deny' ? `✔ 팀원 설정 수정: 불가.
|
|
1703
|
+
console.log(v === 'deny' ? `✔ 팀원 설정 수정: 불가. 팀장(${leadNames.join(', ')})만 workspace.json · 팩 · 생성물을 바꾸고, 팀원은 자기 handoffs/ 와 로드맵(handoffs/PLAN.md)만 커밋한다` : '✔ 팀원 설정 수정: 허용 (누구나)');
|
|
1622
1704
|
console.log(' workspace.json 이 바뀌었습니다. commit · push 하면 팀에 적용됩니다. 서버에서 확실히 막으려면 README 의 GitLab 설정 참고');
|
|
1623
1705
|
return;
|
|
1624
1706
|
}
|
|
1625
1707
|
if (sub === 'admins') {
|
|
1626
|
-
if (!ids.length) { console.log(
|
|
1708
|
+
if (!ids.length) { console.log(`팀장: ${[...(team.leads || []), ...(team.admins || [])].map((x) => personOf(x).nickname || personOf(x).username).join(', ') || '(없음)'} · 팀원 설정 수정: ${team.lock ? '불가' : '허용'}`); return; }
|
|
1627
1709
|
team.admins = [...new Set(ids)];
|
|
1628
1710
|
raw.team = team;
|
|
1629
1711
|
fs.writeFileSync(file, JSON.stringify(raw, null, 2) + '\n');
|
|
1630
1712
|
runWorkspaceKit(root, ['sync', '--quiet']);
|
|
1631
|
-
console.log(`✔
|
|
1713
|
+
console.log(`✔ 팀장: ${team.admins.join(', ')}. workspace.json 을 commit · push 하면 팀에 적용됩니다`);
|
|
1632
1714
|
return;
|
|
1633
1715
|
}
|
|
1634
1716
|
if (sub === 'members') {
|
|
1635
|
-
console.log(team.members.length ? `팀 명단 (${team.members.length}명): ${team.members.join(', ')}${team.restrict ? '\n명단 제한: 켜짐. 명단 밖 사람은 kedovi start 가 멈춘다' : ''}` : '팀 명단이 비어 있습니다. kedovi team add <id> (id = 그 사람 워크스페이스의 git user.name)');
|
|
1717
|
+
console.log(team.members.length ? `팀 명단 (${team.members.length}명): ${team.members.map((x) => { const q = personOf(x); return `${q.nickname ? `${q.nickname}(${q.username})` : q.username}${q.repos ? ` · 담당 ${q.repos.join(', ')}` : ''}`; }).join(', ')}${team.restrict ? '\n명단 제한: 켜짐. 명단 밖 사람은 kedovi start 가 멈춘다' : ''}` : '팀 명단이 비어 있습니다. kedovi team add <id> (id = 그 사람 워크스페이스의 git user.name)');
|
|
1636
1718
|
return;
|
|
1637
1719
|
}
|
|
1638
1720
|
if (sub === 'restrict') {
|
|
@@ -1643,24 +1725,100 @@ function cmdTeamAdmin(root, [sub, ...ids]) {
|
|
|
1643
1725
|
} else {
|
|
1644
1726
|
if (!ids.length) fail(`사용법: kedovi team ${sub} <id…>`);
|
|
1645
1727
|
for (const id of ids) {
|
|
1646
|
-
if (sub === 'add' && !team.members.
|
|
1647
|
-
if (sub === 'remove') team.members = team.members.filter((x) => x
|
|
1728
|
+
if (sub === 'add' && !team.members.some((x) => sameUser(x, id))) team.members.push({ username: id }); // 새로 넣는 사람은 계정 username 항목 (문자열은 옛 git user.name 형식)
|
|
1729
|
+
if (sub === 'remove') team.members = team.members.filter((x) => !sameUser(x, id));
|
|
1648
1730
|
}
|
|
1649
1731
|
if (!team.members.length) delete team.restrict;
|
|
1650
1732
|
}
|
|
1651
1733
|
raw.team = team;
|
|
1652
1734
|
fs.writeFileSync(file, JSON.stringify(raw, null, 2) + '\n');
|
|
1653
|
-
console.log(`✔ 팀 명단: ${team.members.join(', ') || '(없음)'}${team.restrict ? ' · 명단 제한 켜짐' : ''}`);
|
|
1735
|
+
console.log(`✔ 팀 명단: ${team.members.map((x) => personOf(x).nickname || personOf(x).username).join(', ') || '(없음)'}${team.restrict ? ' · 명단 제한 켜짐' : ''}`);
|
|
1654
1736
|
console.log(' workspace.json 이 바뀌었습니다. commit · push 하면 팀에 적용됩니다');
|
|
1655
1737
|
}
|
|
1656
1738
|
|
|
1739
|
+
/** 팀 판정 모듈: 워크스페이스 것, 옛 워크스페이스면 킷 것 */
|
|
1740
|
+
function teamMod(root) {
|
|
1741
|
+
const own = path.join(root, 'packs', 'core', 'lib', 'team.js');
|
|
1742
|
+
return require(fs.existsSync(own) ? own : path.join(KIT_ROOT, 'packs', 'core', 'lib', 'team.js'));
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
async function cmdWhoami(root, args) {
|
|
1746
|
+
if (args.includes('--refresh')) {
|
|
1747
|
+
const before = (readLocal(root).me || {}).username;
|
|
1748
|
+
const me = await ensureAccount(root, { refresh: true });
|
|
1749
|
+
if (me && me.username) adoptAccount(root, me.username);
|
|
1750
|
+
if ((me || {}).username !== before) runWorkspaceKit(root, ['sync', '--quiet']);
|
|
1751
|
+
}
|
|
1752
|
+
const ws = kit.loadWorkspace(root);
|
|
1753
|
+
const T = teamMod(root);
|
|
1754
|
+
const w = T.whoAmI(root, ws);
|
|
1755
|
+
const blocked = T.blockedRepos(root, ws);
|
|
1756
|
+
console.log(`이 PC 사용자: ${w.nickname ? `${w.nickname} · ` : ''}${w.username || '(모름)'} (확인: ${w.source})`);
|
|
1757
|
+
console.log(`역할: ${w.lead ? '팀장 (모든 repo · 설정 수정)' : w.person ? `팀원 · 담당 ${(w.repos || []).join(', ') || '없음'}` : '명단에 없음'}`);
|
|
1758
|
+
if (blocked.length) console.log(`고칠 수 없는 repo: ${blocked.join(', ')} (담당자 · 팀장에게 요청)`);
|
|
1759
|
+
if (w.source === 'git user.name') console.log(' 저장소 계정을 아직 확인하지 못했습니다: kedovi team whoami --refresh');
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
/** 팀장 · 팀원 · 담당 repo · 부를 이름 (workspace.json team.leads · members) */
|
|
1763
|
+
function cmdTeamPeople(root, [sub, ...args]) {
|
|
1764
|
+
const file = path.join(root, 'workspace.json');
|
|
1765
|
+
const raw = JSON.parse(read(file));
|
|
1766
|
+
const team = raw.team || {};
|
|
1767
|
+
const ws = kit.loadWorkspace(root);
|
|
1768
|
+
const repoNames = new Set(ws.repos.map((r) => r.name));
|
|
1769
|
+
const nick = optVal(args, '--nick');
|
|
1770
|
+
const reposArg = optVal(args, '--repos');
|
|
1771
|
+
const pos = args.filter((a, i) => !a.startsWith('--') && !['--nick', '--repos'].includes(args[i - 1]));
|
|
1772
|
+
const show = () => {
|
|
1773
|
+
const T = teamMod(root);
|
|
1774
|
+
const { leads, members } = T.roster({ team });
|
|
1775
|
+
console.log(`팀장: ${leads.map((x) => `${T.callName(x)}(${x.username})`).join(', ') || '(없음)'}`);
|
|
1776
|
+
for (const m of members) console.log(` 팀원 ${T.callName(m)}(${m.username}) · 담당 ${(m.repos || []).join(', ') || '-'}`);
|
|
1777
|
+
};
|
|
1778
|
+
if (sub === 'leads') return show();
|
|
1779
|
+
requireAdmin(root, `kedovi team ${sub}`);
|
|
1780
|
+
const save = (msg) => { raw.team = team; fs.writeFileSync(file, JSON.stringify(raw, null, 2) + '\n'); runWorkspaceKit(root, ['sync', '--quiet']); console.log(`✔ ${msg}`); show(); console.log(' workspace.json 이 바뀌었습니다. commit · push 하면 팀에 적용됩니다'); };
|
|
1781
|
+
const checkRepos = (list) => { const bad = list.filter((x) => !repoNames.has(x)); if (bad.length) fail(`repos 에 없는 이름: ${bad.join(', ')} (있는 repo: ${[...repoNames].join(', ')})`); return list; };
|
|
1782
|
+
if (sub === 'lead') {
|
|
1783
|
+
const [act, user] = pos;
|
|
1784
|
+
if (!['add', 'remove'].includes(act) || !user) fail('사용법: kedovi team lead add|remove <username> [--nick 이름]');
|
|
1785
|
+
team.leads = (team.leads || []).map(personOf).filter((x) => x.username !== user);
|
|
1786
|
+
team.admins = (team.admins || []).filter((x) => x !== user);
|
|
1787
|
+
if (!team.admins.length) delete team.admins;
|
|
1788
|
+
if (act === 'add') team.leads.push({ username: user, ...(nick ? { nickname: nick } : {}) });
|
|
1789
|
+
if (act === 'remove' && !team.leads.length && !(team.admins || []).length && team.lock) fail('마지막 팀장은 뺄 수 없습니다 (설정 잠금이 켜져 있음). 다른 팀장을 먼저 넣으세요');
|
|
1790
|
+
return save(act === 'add' ? `팀장 추가: ${nick || user}` : `팀장에서 뺌: ${user}`);
|
|
1791
|
+
}
|
|
1792
|
+
const [user, val] = pos;
|
|
1793
|
+
if (!user) fail(`사용법: kedovi team ${sub === 'nick' ? 'nick <username> <이름>' : sub === 'repos' ? 'repos <username> <a,b|->' : 'add <username> [--nick 이름] [--repos a,b]'}`);
|
|
1794
|
+
team.members = (team.members || []).map(personOf);
|
|
1795
|
+
let m = team.members.find((x) => x.username === user);
|
|
1796
|
+
if (!m && sub !== 'nick') { m = { username: user }; team.members.push(m); }
|
|
1797
|
+
if (sub === 'nick') {
|
|
1798
|
+
if (!val) fail('사용법: kedovi team nick <username> <이름>');
|
|
1799
|
+
if ((team.admins || []).includes(user)) { team.admins = team.admins.filter((x) => x !== user); if (!team.admins.length) delete team.admins; team.leads = [...(team.leads || []), { username: user }]; } // 옛 팀장 → 팀장 항목으로
|
|
1800
|
+
const l = (team.leads || []).map(personOf).find((x) => x.username === user);
|
|
1801
|
+
if (l) { team.leads = team.leads.map((x) => (sameUser(x, user) ? { ...personOf(x), nickname: val } : x)); return save(`부를 이름: ${user} → ${val}`); }
|
|
1802
|
+
if (!m) fail(`${user} 은(는) 팀장 · 팀원 명단에 없습니다. 먼저 kedovi team add ${user} --nick ${val}`);
|
|
1803
|
+
m.nickname = val;
|
|
1804
|
+
return save(`부를 이름: ${user} → ${val}`);
|
|
1805
|
+
}
|
|
1806
|
+
if (nick) m.nickname = nick;
|
|
1807
|
+
const rv = sub === 'repos' ? val : reposArg;
|
|
1808
|
+
if (rv !== undefined) {
|
|
1809
|
+
if (rv === '-' || rv === '') delete m.repos; else m.repos = checkRepos(String(rv).split(',').map((x) => x.trim()).filter(Boolean));
|
|
1810
|
+
}
|
|
1811
|
+
return save(sub === 'repos' ? `${m.nickname || user} 담당: ${(m.repos || []).join(', ') || '없음'}` : `팀원: ${m.nickname || user}${m.repos ? ` · 담당 ${m.repos.join(', ')}` : ''}`);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1657
1814
|
/** 합류 기록 — install 때 묻고, 예면 handoffs/<id>/TODO.md 를 만들어 워크스페이스 repo 에 push. 팀 현황에 바로 보인다 */
|
|
1658
1815
|
async function joinTeam(root) {
|
|
1659
1816
|
const me = memberId(root);
|
|
1660
1817
|
if (!me) { p.log.warn('워크스페이스의 git user.name 이 없어 팀 합류 기록을 남기지 못했습니다. git config user.name <id> 후 kedovi install'); return; }
|
|
1661
1818
|
const git = (...a) => spawnSync('git', ['-C', root, ...a], { encoding: 'utf8' });
|
|
1662
1819
|
const team = teamOf(root);
|
|
1663
|
-
|
|
1820
|
+
const meAcct = (readLocal(root).me || {}).username;
|
|
1821
|
+
if ((team.members || []).length && !team.members.some((x) => sameUser(x, me) || (meAcct && sameUser(x, meAcct))) && ![...(team.leads || []), ...(team.admins || [])].some((x) => sameUser(x, me) || (meAcct && sameUser(x, meAcct)))) {
|
|
1664
1822
|
p.log.warn(`${me} 은(는) 팀 명단에 없습니다. 리더에게 명단 추가(kedovi team add ${me})를 요청하세요${team.restrict ? ' (명단 제한이 켜져 있어 그 전에는 kedovi start 가 멈춥니다)' : ''}`);
|
|
1665
1823
|
}
|
|
1666
1824
|
const rel = `handoffs/${me}/TODO.md`;
|
|
@@ -1674,7 +1832,12 @@ async function joinTeam(root) {
|
|
|
1674
1832
|
const file = path.join(root, rel);
|
|
1675
1833
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1676
1834
|
if (!fs.existsSync(file)) fs.writeFileSync(file, `# TODO (${me})\n`);
|
|
1677
|
-
|
|
1835
|
+
// 저장소 계정도 함께 (팀 현황이 git user.name 폴더 ↔ 명단 username 을 잇는다)
|
|
1836
|
+
const acct = (readLocal(root).me || {}).username;
|
|
1837
|
+
const accRel = `handoffs/${me}/.account`;
|
|
1838
|
+
if (acct) fs.writeFileSync(path.join(root, accRel), `${acct}\n`);
|
|
1839
|
+
const paths = acct ? [rel, accRel] : [rel];
|
|
1840
|
+
const steps = [['add', '--', ...paths], ['commit', '-q', '-m', `docs: ${me} 팀 합류`, '--', ...paths], ['pull', '-q', '--rebase', '--autostash'], ['push', '-q']];
|
|
1678
1841
|
for (const st of steps) {
|
|
1679
1842
|
const r = git(...st);
|
|
1680
1843
|
if (r.status !== 0) { p.log.warn(`합류 기록을 올리지 못했습니다 (git ${st[0]}): ${(r.stderr || '').trim().split('\n').pop()}. 나중에 첫 핸드오프 때 함께 올라갑니다`); return; }
|
|
@@ -2112,7 +2275,7 @@ function migrateModels(root) {
|
|
|
2112
2275
|
* 화면 없이 모델 바꾸기 (AI 가 대신 실행할 때) — 반환: 처리했으면 true
|
|
2113
2276
|
* kedovi models profile [rich|standard|lean|team] [--team] 프로필 (team = 이 PC 값 지우고 팀 기본 따름, 호환: - · 팀). 이 PC 값은 팀보다 낮은 것만. 인자 없이 실행하면 화면에서 고른다
|
|
2114
2277
|
* kedovi models set <main|crossReview|역할> <claude|codex> <모델|-> [--team] [--effort <e>] 모델 (- = 직접 지정 지우기, 위임은 codex:<모델>)
|
|
2115
|
-
* kedovi models allow-higher on|off 팀원이 팀보다 높은 프로필을 둘 수 있게 (workspace.json agents.allowHigherProfile,
|
|
2278
|
+
* kedovi models allow-higher on|off 팀원이 팀보다 높은 프로필을 둘 수 있게 (workspace.json agents.allowHigherProfile, 팀장)
|
|
2116
2279
|
*/
|
|
2117
2280
|
async function modelsCommand(root, argv) {
|
|
2118
2281
|
const [sub, ...rest] = argv;
|
|
@@ -2162,7 +2325,7 @@ async function modelsCommand(root, argv) {
|
|
|
2162
2325
|
const clear = CLEAR.includes(v);
|
|
2163
2326
|
if (!team && !clear && !profileAllowed(root, cat, v)) {
|
|
2164
2327
|
const { profile } = kit.resolveModels(root, kit.loadWorkspace(root));
|
|
2165
|
-
fail(`이 PC 프로필은 팀 프로필(${kit.profileLabel(cat, profile.team)})과 같거나 낮은 것만 됩니다. 팀 값을 올리려면 kedovi models profile ${v} --team, 팀원에게 높은 프로필을 허용하려면 kedovi models allow-higher on (
|
|
2328
|
+
fail(`이 PC 프로필은 팀 프로필(${kit.profileLabel(cat, profile.team)})과 같거나 낮은 것만 됩니다. 팀 값을 올리려면 kedovi models profile ${v} --team, 팀원에게 높은 프로필을 허용하려면 kedovi models allow-higher on (팀장)`);
|
|
2166
2329
|
}
|
|
2167
2330
|
saveProfile(root, team ? 'team' : 'local', clear ? '' : v);
|
|
2168
2331
|
} else {
|
|
@@ -2329,7 +2492,7 @@ function cmdApproach(argv) {
|
|
|
2329
2492
|
}
|
|
2330
2493
|
|
|
2331
2494
|
/**
|
|
2332
|
-
* 유형 바꾸기 (
|
|
2495
|
+
* 유형 바꾸기 (팀장): 분석·설계를 끝내고 같은 워크스페이스에서 구축 · 고도화로 넘어갈 때 등.
|
|
2333
2496
|
* 유형 팩 교체 · 필수 repo(현행 repo 중 고칠 것을 개발 repo 로) · 진행 방식 · 로드맵(새 뼈대, 지난 로드맵은 아래에 남김) → sync
|
|
2334
2497
|
*/
|
|
2335
2498
|
async function cmdType(argv) {
|
|
@@ -2422,7 +2585,7 @@ function cmdTimestamp(argv) {
|
|
|
2422
2585
|
const m = val(readLocal(root));
|
|
2423
2586
|
const now = m !== undefined ? m : t !== undefined ? t : true;
|
|
2424
2587
|
console.log(`답변 첫 줄 시각 표시 (Claude Code): ${now ? '켜짐' : '꺼짐'} (이 PC ${word(m)} · 팀 ${word(t)} · 기본 켜짐)`);
|
|
2425
|
-
console.log(' 바꾸기: kedovi timestamp on|off · 팀 기본값: --team (
|
|
2588
|
+
console.log(' 바꾸기: kedovi timestamp on|off · 팀 기본값: --team (팀장) · 이 PC 값 지우기: kedovi timestamp team');
|
|
2426
2589
|
};
|
|
2427
2590
|
if (!want) return show();
|
|
2428
2591
|
if (!['on', 'off', 'team'].includes(want) || (want === 'team' && team)) fail('사용법: kedovi timestamp [on|off] [--team] · team (이 PC 값을 지워 팀 값을 따름)');
|
|
@@ -2475,7 +2638,7 @@ function showPrefs(root) {
|
|
|
2475
2638
|
console.log(`대화 스타일: ${S(r.style)} (${where[r.from.style]}) · 이 PC ${S(r.local.style)} · 팀 ${S(r.team.style)}`);
|
|
2476
2639
|
const m = mermaidInstalled();
|
|
2477
2640
|
console.log(`그림 미리보기(claude-mermaid): ${m === null ? '확인 못 함 (Claude Code 없음)' : m ? '설치됨: 세션에 mermaid_preview 가 있으면 브라우저로 띄움' : '없음: 텍스트 그림으로 (설치: kedovi prefs mermaid install)'}`);
|
|
2478
|
-
console.log(' 바꾸기: kedovi prefs language ko|en · kedovi prefs style learn|normal|expert · 이 PC 값 지우기: team · 팀 기본값: --team (
|
|
2641
|
+
console.log(' 바꾸기: kedovi prefs language ko|en · kedovi prefs style learn|normal|expert · 이 PC 값 지우기: team · 팀 기본값: --team (팀장)');
|
|
2479
2642
|
}
|
|
2480
2643
|
|
|
2481
2644
|
/** 저장 → sync. key = language | style, value = 값 | 'team'(이 PC 값 지우기) */
|
|
@@ -2594,7 +2757,7 @@ function cmdTeamMode(argv) {
|
|
|
2594
2757
|
const lv = (v) => kit.ENFORCE_LEVELS.indexOf(v);
|
|
2595
2758
|
if (!key || lv(a2) < 0 || lv(a2) > lv(kit.ENFORCE_DEFAULTS[key].max)) fail('사용법: kedovi team-mode enforce name off|warn|block · roster off|warn · review off|warn|block [--repos a,b|*] [--min-lines N] [--paths p1,p2] · idle off|warn|block [--after 분] [--remind 분] · status off|block · ask off|block · run off|warn|block [--after 분] · check off|warn|block · plan off|warn|block [--team] (name: 팀 모드에서 이름 없는 역할 에이전트 · roster: 첫 역할 에이전트 때 구성표 알림 · review: commit · push 전 리뷰 확인 · idle: 대기로 오래 남은 팀원 알림 · status: 남은 것이 있는 채 멈출 때 상태 줄 · ask: 결정 질문을 글로 끝낼 때 · run: 오래 일하는 팀원 · 위임 · check: 리뷰 전 검사 · plan: 코드 수정 전 착수 전 확인)');
|
|
2596
2759
|
const teamNow = kit.resolveModels(root, JSON.parse(read(wsFile))).teamEnforce[key];
|
|
2597
|
-
if (!team && lv(a2) < lv(teamNow)) fail(`이 PC 값은 팀 값(${teamNow})보다 세게만 둘 수 있습니다. 팀 값을 바꾸려면
|
|
2760
|
+
if (!team && lv(a2) < lv(teamNow)) fail(`이 PC 값은 팀 값(${teamNow})보다 세게만 둘 수 있습니다. 팀 값을 바꾸려면 팀장이 kedovi team-mode enforce ${a1} ${a2} --team`);
|
|
2598
2761
|
// 리뷰 확인의 대상 · 기준은 팀 값만
|
|
2599
2762
|
const extra = {};
|
|
2600
2763
|
if (a1 === 'review') {
|
|
@@ -2602,7 +2765,7 @@ function cmdTeamMode(argv) {
|
|
|
2602
2765
|
if (optAt('--repos') !== undefined) extra.reviewRepos = list(optAt('--repos'));
|
|
2603
2766
|
if (optAt('--min-lines') !== undefined) { const n = Number(optAt('--min-lines')); if (!Number.isInteger(n) || n < 0) fail('--min-lines 는 0 이상의 정수'); extra.reviewMinLines = n; }
|
|
2604
2767
|
if (optAt('--paths') !== undefined) extra.reviewPaths = optAt('--paths') === '-' ? [] : list(optAt('--paths'));
|
|
2605
|
-
if (Object.keys(extra).length && !team) fail('리뷰 확인의 대상(--repos) · 기준 줄 수(--min-lines) · 경로(--paths)는 팀 값입니다. --team 을 붙이세요 (
|
|
2768
|
+
if (Object.keys(extra).length && !team) fail('리뷰 확인의 대상(--repos) · 기준 줄 수(--min-lines) · 경로(--paths)는 팀 값입니다. --team 을 붙이세요 (팀장)');
|
|
2606
2769
|
const known = (JSON.parse(read(wsFile)).repos || []).map((r) => r.name);
|
|
2607
2770
|
const bad = (extra.reviewRepos || []).filter((n) => n !== '*' && !known.includes(n));
|
|
2608
2771
|
if (bad.length) fail(`workspace.json repos 에 없는 이름: ${bad.join(', ')} (있는 것: ${known.join(', ') || '없음'})`);
|
|
@@ -2614,12 +2777,12 @@ function cmdTeamMode(argv) {
|
|
|
2614
2777
|
if (!Number.isInteger(n) || n < 1) fail(`${opt} 는 1 이상의 정수(분)`);
|
|
2615
2778
|
extra[field] = n;
|
|
2616
2779
|
}
|
|
2617
|
-
if (Object.keys(extra).length && !team) fail('대기 상한(--after) · 다시 알리는 간격(--remind)은 팀 값입니다. --team 을 붙이세요 (
|
|
2780
|
+
if (Object.keys(extra).length && !team) fail('대기 상한(--after) · 다시 알리는 간격(--remind)은 팀 값입니다. --team 을 붙이세요 (팀장)');
|
|
2618
2781
|
}
|
|
2619
2782
|
if (a1 === 'run' && optAt('--after') !== undefined) {
|
|
2620
2783
|
const n = Number(optAt('--after'));
|
|
2621
2784
|
if (!Number.isInteger(n) || n < 1) fail('--after 는 1 이상의 정수(분)');
|
|
2622
|
-
if (!team) fail('실행 상한(--after)은 팀 값입니다. --team 을 붙이세요 (
|
|
2785
|
+
if (!team) fail('실행 상한(--after)은 팀 값입니다. --team 을 붙이세요 (팀장)');
|
|
2623
2786
|
extra.runMinutes = n;
|
|
2624
2787
|
}
|
|
2625
2788
|
edit((x) => ({ ...x, enforce: { ...(x.enforce || {}), [key]: a2, ...extra } }));
|
|
@@ -2832,6 +2995,7 @@ async function cmdInstall() {
|
|
|
2832
2995
|
clack();
|
|
2833
2996
|
ask(await chooseAgent(root, { force: !readLocal(root).agent ? true : false, progress: true }));
|
|
2834
2997
|
await choosePrefs(root, { onlyMissing: true });
|
|
2998
|
+
await ensureAccount(root);
|
|
2835
2999
|
await joinTeam(root);
|
|
2836
3000
|
const cur = readLocal(root);
|
|
2837
3001
|
p.outro(`메인 에이전트: ${AGENTS[cur.agent].label}${cur.args && cur.args.length ? ` (${cur.args.join(' ')})` : ''}: 바꾸려면 kedovi agent`);
|
|
@@ -2843,10 +3007,21 @@ async function cmdStart(argv) {
|
|
|
2843
3007
|
const root = locateWorkspace();
|
|
2844
3008
|
// 리더가 명단 제한을 켰으면 명단에 없는 사람은 시작하지 않는다 (안전장치 — 권한 통제는 GitLab·GitHub 멤버로)
|
|
2845
3009
|
const team = teamOf(root);
|
|
3010
|
+
// 팀이 있으면 이 PC 사용자(저장소 계정)를 한 번 확인해 둔다 — 팀장 · 담당 repo 판정과 nickname 부르기에 쓴다
|
|
3011
|
+
if ((team.leads || []).length || (team.members || []).length || (team.admins || []).length) {
|
|
3012
|
+
const before = (readLocal(root).me || {}).username;
|
|
3013
|
+
const acct = await ensureAccount(root);
|
|
3014
|
+
if (acct && acct.username) adoptAccount(root, acct.username); // 핸드오프 폴더의 .account 가 없으면 만든다
|
|
3015
|
+
// 계정이 새로 잡혔거나 팀 설정(담당 repo 등)이 git pull 로 바뀌었으면 이 PC 의 편집 금지 · 지침을 다시 맞춘다
|
|
3016
|
+
const hash = require('crypto').createHash('sha1').update(JSON.stringify(team)).digest('hex').slice(0, 12);
|
|
3017
|
+
if ((acct && acct.username !== before) || readLocal(root).teamHash !== hash) { runWorkspaceKit(root, ['sync', '--quiet']); writeLocal(root, { teamHash: hash }); }
|
|
3018
|
+
}
|
|
2846
3019
|
if (team.restrict && (team.members || []).length) {
|
|
2847
3020
|
const me = memberId(root);
|
|
2848
|
-
|
|
2849
|
-
if (!
|
|
3021
|
+
const acct = (readLocal(root).me || {}).username;
|
|
3022
|
+
if (!me && !acct) fail('이 워크스페이스는 팀 명단에 있는 사람만 시작합니다. 먼저 kedovi team whoami --refresh 로 저장소 계정을 확인하세요');
|
|
3023
|
+
const listed = [...team.members, ...(team.leads || []), ...(team.admins || [])].some((x) => sameUser(x, me) || (acct && sameUser(x, acct)));
|
|
3024
|
+
if (!listed) fail(`${acct || me} 은(는) 이 워크스페이스 팀 명단에 없습니다. 팀장에게 명단 추가(kedovi team add ${acct || me} --nick 이름)를 요청하세요 (명단: ${team.members.map((x) => personOf(x).nickname || personOf(x).username).join(', ')})`);
|
|
2850
3025
|
}
|
|
2851
3026
|
const dash = argv.indexOf('--');
|
|
2852
3027
|
const extra = dash >= 0 ? argv.slice(dash + 1) : [];
|
|
@@ -3179,7 +3354,7 @@ const NOTIFY_USAGE = `사용법
|
|
|
3179
3354
|
kedovi notify my channel on [--new]|off|status 양방향 대화 (기본 꺼짐. 이 프로젝트 전용 봇 → Claude Code 세션. 봇 토큰은 숨김 입력)
|
|
3180
3355
|
kedovi notify my channel ask dialog|text 질문 방식: 선택지 창(기본) · 텔레그램 버튼 질문(= 자리 비움)
|
|
3181
3356
|
kedovi notify my channel progress <분|off> 입력 없이 오래 작업하면 진행 현황 (기본 5분)
|
|
3182
|
-
kedovi notify team setup [--new] 팀 봇 연결 (
|
|
3357
|
+
kedovi notify team setup [--new] 팀 봇 연결 (팀장에게 받은 토큰을 숨김 입력에)
|
|
3183
3358
|
kedovi notify team chat <그룹방 ID|off> 팀 방 지정 (팀 설정: commit · push)
|
|
3184
3359
|
kedovi notify team announce "<내용>" · test`;
|
|
3185
3360
|
|
|
@@ -3277,7 +3452,7 @@ async function cmdNotifySetup() {
|
|
|
3277
3452
|
}
|
|
3278
3453
|
} else if (pick === 'team') {
|
|
3279
3454
|
const tm = await choose({ message: '팀 방 알림', hint: '핸드오프 push · 설정 변경 push · 공지를 팀 그룹방으로.', initialValue: team.token ? 'chat' : 'bot', options: [
|
|
3280
|
-
{ value: 'bot', label: '팀 봇 연결', hint: '
|
|
3455
|
+
{ value: 'bot', label: '팀 봇 연결', hint: '팀장에게 받은 팀 봇 토큰' },
|
|
3281
3456
|
{ value: 'chat', label: '팀 방 지정', hint: '팀 설정 (workspace.json, commit · push)' },
|
|
3282
3457
|
{ value: 'off', label: '팀 방 알림 끄기', hint: '팀 설정' },
|
|
3283
3458
|
...(team.token ? [{ value: 'remove', label: '팀 봇 제거', hint: '이 PC 의 팀 봇 토큰을 지운다' }] : [])] });
|
|
@@ -3286,7 +3461,7 @@ async function cmdNotifySetup() {
|
|
|
3286
3461
|
else if (tm === 'remove') runTool(['remove', '--team']);
|
|
3287
3462
|
else {
|
|
3288
3463
|
const v = await input(tm === 'bot'
|
|
3289
|
-
? { message: '팀 봇 토큰', hint: '
|
|
3464
|
+
? { message: '팀 봇 토큰', hint: '팀장에게 받은 토큰. 봇을 팀 방에 초대하고 방에서 아무 말이나 해 두면 방 ID 를 보여 준다. 입력은 가려집니다.', placeholder: '123456789:AA…', secret: true }
|
|
3290
3465
|
: { message: '팀 방 ID', hint: '팀 봇 연결 때 보여 준 그룹방 ID (-100…).', placeholder: '-100…' });
|
|
3291
3466
|
if (v !== BACK && String(v || '').trim()) { if (tm === 'bot') runTool(['setup', '--team', '--new'], String(v).trim()); else runTool(['team-chat', String(v).trim()]); }
|
|
3292
3467
|
}
|
|
@@ -3528,7 +3703,7 @@ async function main() {
|
|
|
3528
3703
|
const root = locateWorkspace();
|
|
3529
3704
|
const tool = path.join(root, 'packs', 'core', 'bin', 'team-status.js');
|
|
3530
3705
|
if (!fs.existsSync(tool)) fail('이 워크스페이스에는 팀 현황 도구가 없습니다. kedovi upgrade 후 다시 하세요');
|
|
3531
|
-
if (['add', 'remove', 'restrict', 'members', 'edit', 'admins'].includes(rest[0])) return cmdTeamAdmin(root, rest);
|
|
3706
|
+
if (['add', 'remove', 'restrict', 'members', 'edit', 'admins', 'lead', 'leads', 'repos', 'nick', 'whoami'].includes(rest[0])) return cmdTeamAdmin(root, rest);
|
|
3532
3707
|
const [id, ...more] = rest;
|
|
3533
3708
|
const toolArgs = !id || id.startsWith('--') ? rest : more.includes('--take') ? ['--take', id, '--items', more[more.indexOf('--take') + 1] || ''] : ['--id', id, ...more];
|
|
3534
3709
|
const r = spawnSync(process.execPath, [tool, ...toolArgs], { cwd: root, stdio: 'inherit' });
|