@team-semicolon/semo-cli 3.7.4 → 3.9.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.
Files changed (2) hide show
  1. package/dist/index.js +395 -325
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -235,9 +235,48 @@ async function showVersionComparison(cwd) {
235
235
  level: 0,
236
236
  });
237
237
  }
238
- // Extension 패키지들 (meta, semo-hooks, semo-remote 등) - semo-system 내부
238
+ // 그룹 패키지 (eng, biz, ops) 및 하위 Extension - semo-system 내부
239
+ // 그룹별로 묶어서 계층 구조로 출력
239
240
  if (hasSemoSystem) {
240
- for (const key of Object.keys(EXTENSION_PACKAGES)) {
241
+ for (const group of PACKAGE_GROUPS) {
242
+ const groupVersionPath = path.join(semoSystemDir, group, "VERSION");
243
+ const hasGroupVersion = fs.existsSync(groupVersionPath);
244
+ // 해당 그룹의 하위 패키지 찾기
245
+ const groupExtensions = Object.keys(EXTENSION_PACKAGES).filter(key => key.startsWith(`${group}/`));
246
+ const installedGroupExtensions = groupExtensions.filter(key => fs.existsSync(path.join(semoSystemDir, key, "VERSION")));
247
+ // 그룹 버전이 있거나 하위 패키지가 설치된 경우에만 표시
248
+ if (hasGroupVersion || installedGroupExtensions.length > 0) {
249
+ // 그룹 패키지 버전 추가
250
+ if (hasGroupVersion) {
251
+ const localGroup = fs.readFileSync(groupVersionPath, "utf-8").trim();
252
+ const remoteGroup = await getRemotePackageVersion(group);
253
+ versionInfos.push({
254
+ name: group,
255
+ local: localGroup,
256
+ remote: remoteGroup,
257
+ needsUpdate: remoteGroup ? isVersionLower(localGroup, remoteGroup) : false,
258
+ level: 1,
259
+ });
260
+ }
261
+ // 하위 Extension 패키지들 추가
262
+ for (const key of installedGroupExtensions) {
263
+ const extVersionPath = path.join(semoSystemDir, key, "VERSION");
264
+ const localExt = fs.readFileSync(extVersionPath, "utf-8").trim();
265
+ const remoteExt = await getRemotePackageVersion(key);
266
+ versionInfos.push({
267
+ name: key,
268
+ local: localExt,
269
+ remote: remoteExt,
270
+ needsUpdate: remoteExt ? isVersionLower(localExt, remoteExt) : false,
271
+ level: 2,
272
+ group: group,
273
+ });
274
+ }
275
+ }
276
+ }
277
+ // 그룹에 속하지 않는 Extension (meta 등)
278
+ const nonGroupExtensions = Object.keys(EXTENSION_PACKAGES).filter(key => !PACKAGE_GROUPS.some(g => key.startsWith(`${g}/`)));
279
+ for (const key of nonGroupExtensions) {
241
280
  const extVersionPath = path.join(semoSystemDir, key, "VERSION");
242
281
  if (fs.existsSync(extVersionPath)) {
243
282
  const localExt = fs.readFileSync(extVersionPath, "utf-8").trim();
@@ -354,10 +393,21 @@ async function showVersionComparison(cwd) {
354
393
  }
355
394
  }
356
395
  // === Windows 지원 유틸리티 ===
357
- const isWindows = os.platform() === "win32";
396
+ // Git Bash, WSL 등에서도 Windows로 인식하도록 확장
397
+ const isWindows = os.platform() === "win32" ||
398
+ process.env.OSTYPE?.includes("msys") ||
399
+ process.env.OSTYPE?.includes("cygwin") ||
400
+ process.env.TERM_PROGRAM === "mintty";
358
401
  /**
359
402
  * Windows에서 Junction 링크를 생성하거나, Unix에서 심볼릭 링크를 생성
360
403
  * Junction은 관리자 권한 없이 디렉토리 링크를 생성할 수 있음
404
+ *
405
+ * Windows 환경 (Git Bash, PowerShell, CMD 포함):
406
+ * 1. Junction 시도 (폴더만 가능)
407
+ * 2. 실패 시 xcopy로 복사
408
+ *
409
+ * Unix/Mac 환경:
410
+ * - 상대 경로 심볼릭 링크
361
411
  */
362
412
  function createSymlinkOrJunction(targetPath, linkPath) {
363
413
  // 이미 존재하는 링크/파일 제거 (깨진 심볼릭 링크도 처리)
@@ -376,30 +426,42 @@ function createSymlinkOrJunction(targetPath, linkPath) {
376
426
  // 파일이 존재하지 않음 - 정상
377
427
  }
378
428
  if (isWindows) {
379
- // Windows: Junction 사용 (절대 경로 필요)
429
+ // Windows: Junction 사용 (절대 경로, Windows 형식 필요)
430
+ // path.resolve는 Git Bash에서도 Windows 경로를 반환
380
431
  const absoluteTarget = path.resolve(targetPath);
381
- // 재시도 메커니즘 (최대 3회)
382
- let success = false;
383
- let lastError = null;
384
- for (let attempt = 1; attempt <= 3 && !success; attempt++) {
432
+ const absoluteLink = path.resolve(linkPath);
433
+ // 타겟이 파일인지 폴더인지 확인
434
+ const targetStats = fs.statSync(targetPath);
435
+ const isDirectory = targetStats.isDirectory();
436
+ if (isDirectory) {
437
+ // 폴더: Junction 시도
438
+ let success = false;
385
439
  try {
386
- (0, child_process_1.execSync)(`cmd /c "mklink /J "${linkPath}" "${absoluteTarget}""`, { stdio: "pipe" });
440
+ // mklink /J 사용 (관리자 권한 불필요)
441
+ (0, child_process_1.execSync)(`cmd /c mklink /J "${absoluteLink}" "${absoluteTarget}"`, {
442
+ stdio: "pipe",
443
+ windowsHide: true,
444
+ });
387
445
  success = true;
388
446
  }
389
- catch (err) {
390
- lastError = err;
391
- if (attempt < 3) {
392
- // 잠시 대기 후 재시도
393
- (0, child_process_1.execSync)("timeout /t 1 /nobreak >nul 2>&1", { stdio: "pipe" });
394
- }
447
+ catch {
448
+ // Junction 실패 - xcopy로 복사
449
+ success = false;
450
+ }
451
+ if (!success) {
452
+ // fallback: 디렉토리 복사
453
+ console.log(chalk_1.default.yellow(` ⚠ Junction 생성 실패, 복사로 대체: ${path.basename(linkPath)}`));
454
+ console.log(chalk_1.default.gray(` 💡 업데이트 시 semo update 명령으로 동기화하세요.`));
455
+ fs.mkdirSync(absoluteLink, { recursive: true });
456
+ (0, child_process_1.execSync)(`xcopy /E /I /Q /Y "${absoluteTarget}" "${absoluteLink}"`, {
457
+ stdio: "pipe",
458
+ windowsHide: true,
459
+ });
395
460
  }
396
461
  }
397
- if (!success) {
398
- // fallback: 디렉토리 복사 (경고 표시)
399
- console.log(chalk_1.default.yellow(` ⚠ Junction 생성 실패 (3회 시도), 복사로 대체: ${path.basename(linkPath)}`));
400
- console.log(chalk_1.default.gray(` 원인: ${lastError}`));
401
- console.log(chalk_1.default.gray(` 💡 관리자 권한으로 실행하거나 개발자 모드를 활성화하세요.`));
402
- (0, child_process_1.execSync)(`xcopy /E /I /Q "${absoluteTarget}" "${linkPath}"`, { stdio: "pipe" });
462
+ else {
463
+ // 파일: 직접 복사 (Junction은 폴더만 지원)
464
+ fs.copyFileSync(absoluteTarget, absoluteLink);
403
465
  }
404
466
  }
405
467
  else {
@@ -611,39 +673,92 @@ function copyRecursive(src, dest) {
611
673
  }
612
674
  }
613
675
  const SEMO_REPO = "https://github.com/semicolon-devteam/semo.git";
614
- // Extension 패키지 정의 (통합 구조)
676
+ // 확장 패키지 정의 (v3.0 구조)
615
677
  const EXTENSION_PACKAGES = {
616
- meta: { name: "Meta", desc: "SEMO 프레임워크 자체 개발/관리" },
617
- "semo-hooks": { name: "Hooks", desc: "Claude Code Hooks 기반 로깅 시스템" },
618
- "semo-remote": { name: "Remote", desc: "Claude Code 원격 제어 (모바일 PWA)" },
678
+ // Business Layer
679
+ "biz/discovery": { name: "Discovery", desc: "아이템 발굴, 시장 조사, Epic/Task", layer: "biz", detect: [] },
680
+ "biz/design": { name: "Design", desc: "컨셉 설계, 목업, UX", layer: "biz", detect: [] },
681
+ "biz/management": { name: "Management", desc: "일정/인력/스프린트 관리", layer: "biz", detect: [] },
682
+ "biz/poc": { name: "PoC", desc: "빠른 PoC, 패스트트랙", layer: "biz", detect: [] },
683
+ // Engineering Layer
684
+ "eng/nextjs": { name: "Next.js", desc: "Next.js 프론트엔드 개발", layer: "eng", detect: ["next.config.js", "next.config.mjs", "next.config.ts"] },
685
+ "eng/spring": { name: "Spring", desc: "Spring Boot 백엔드 개발", layer: "eng", detect: ["pom.xml", "build.gradle"] },
686
+ "eng/ms": { name: "Microservice", desc: "마이크로서비스 아키텍처", layer: "eng", detect: [] },
687
+ "eng/infra": { name: "Infra", desc: "인프라/배포 관리", layer: "eng", detect: ["docker-compose.yml", "Dockerfile"] },
688
+ // Operations Layer
689
+ "ops/qa": { name: "QA", desc: "테스트/품질 관리", layer: "ops", detect: [] },
690
+ "ops/monitor": { name: "Monitor", desc: "서비스 현황 모니터링", layer: "ops", detect: [] },
691
+ "ops/improve": { name: "Improve", desc: "개선 제안", layer: "ops", detect: [] },
692
+ // Meta
693
+ meta: { name: "Meta", desc: "SEMO 프레임워크 자체 개발/관리", layer: "meta", detect: ["semo-core", "semo-skills"] },
694
+ // System (semo-system 하위 패키지)
695
+ "semo-hooks": { name: "Hooks", desc: "Claude Code Hooks 기반 로깅 시스템", layer: "system", detect: [] },
696
+ "semo-remote": { name: "Remote", desc: "Claude Code 원격 제어 (모바일 PWA)", layer: "system", detect: [] },
619
697
  };
620
698
  // 단축명 → 전체 패키지 경로 매핑
621
699
  const SHORTNAME_MAPPING = {
700
+ // 하위 패키지명 단축 (discovery → biz/discovery)
701
+ discovery: "biz/discovery",
702
+ design: "biz/design",
703
+ management: "biz/management",
704
+ poc: "biz/poc",
705
+ nextjs: "eng/nextjs",
706
+ spring: "eng/spring",
707
+ ms: "eng/ms",
708
+ infra: "eng/infra",
709
+ qa: "ops/qa",
710
+ monitor: "ops/monitor",
711
+ improve: "ops/improve",
712
+ // 추가 별칭
713
+ next: "eng/nextjs",
714
+ backend: "eng/spring",
715
+ mvp: "biz/poc",
716
+ // System 패키지 단축명
622
717
  hooks: "semo-hooks",
623
718
  remote: "semo-remote",
624
719
  };
625
- // 패키지 입력을 해석
720
+ // 그룹 이름 목록 (biz, eng, ops, meta, system)
721
+ const PACKAGE_GROUPS = ["biz", "eng", "ops", "meta", "system"];
722
+ // 그룹명 → 해당 그룹의 모든 패키지 반환
723
+ function getPackagesByGroup(group) {
724
+ return Object.entries(EXTENSION_PACKAGES)
725
+ .filter(([, pkg]) => pkg.layer === group)
726
+ .map(([key]) => key);
727
+ }
728
+ // 패키지 입력을 해석 (그룹, 레거시, 쉼표 구분 모두 처리)
626
729
  function resolvePackageInput(input) {
627
730
  // 쉼표로 구분된 여러 패키지 처리
628
731
  const parts = input.split(",").map(p => p.trim()).filter(p => p);
629
732
  const resolvedPackages = [];
733
+ let isGroup = false;
734
+ let groupName;
630
735
  for (const part of parts) {
631
- // 1. 단축명 매핑 확인 (hooks semo-hooks )
736
+ // 1. 그룹명인지 확인 (biz, eng, ops, meta)
737
+ if (PACKAGE_GROUPS.includes(part)) {
738
+ const groupPackages = getPackagesByGroup(part);
739
+ resolvedPackages.push(...groupPackages);
740
+ isGroup = true;
741
+ groupName = part;
742
+ continue;
743
+ }
744
+ // 2. 단축명 매핑 확인 (discovery → biz/discovery 등)
632
745
  if (part in SHORTNAME_MAPPING) {
633
746
  resolvedPackages.push(SHORTNAME_MAPPING[part]);
634
747
  continue;
635
748
  }
636
- // 2. 직접 패키지명 확인
749
+ // 3. 직접 패키지명 확인
637
750
  if (part in EXTENSION_PACKAGES) {
638
751
  resolvedPackages.push(part);
639
752
  continue;
640
753
  }
641
- // 3. 유효하지 않은 패키지명
754
+ // 4. 유효하지 않은 패키지명
642
755
  // (빈 배열 대신 null을 추가하여 나중에 에러 처리)
643
756
  }
644
757
  // 중복 제거
645
758
  return {
646
759
  packages: [...new Set(resolvedPackages)],
760
+ isGroup,
761
+ groupName
647
762
  };
648
763
  }
649
764
  const program = new commander_1.Command();
@@ -704,10 +819,47 @@ async function showVersionInfo() {
704
819
  level: 0,
705
820
  });
706
821
  }
707
- // 4. Extension 패키지들 (meta, semo-hooks, semo-remote 등) - semo-system 내부
822
+ // 4. 그룹 패키지 (eng, biz, ops) 및 하위 Extension - semo-system 내부
708
823
  const semoSystemDir = path.join(cwd, "semo-system");
709
824
  if (fs.existsSync(semoSystemDir)) {
710
- for (const key of Object.keys(EXTENSION_PACKAGES)) {
825
+ for (const group of PACKAGE_GROUPS) {
826
+ const groupVersionPath = path.join(semoSystemDir, group, "VERSION");
827
+ const hasGroupVersion = fs.existsSync(groupVersionPath);
828
+ // 해당 그룹의 하위 패키지 찾기
829
+ const groupExtensions = Object.keys(EXTENSION_PACKAGES).filter(key => key.startsWith(`${group}/`));
830
+ const installedGroupExtensions = groupExtensions.filter(key => fs.existsSync(path.join(semoSystemDir, key, "VERSION")));
831
+ if (hasGroupVersion || installedGroupExtensions.length > 0) {
832
+ // 그룹 패키지 버전 추가
833
+ if (hasGroupVersion) {
834
+ const localGroup = fs.readFileSync(groupVersionPath, "utf-8").trim();
835
+ const remoteGroup = await getRemotePackageVersion(group);
836
+ versionInfos.push({
837
+ name: group,
838
+ local: localGroup,
839
+ remote: remoteGroup,
840
+ needsUpdate: remoteGroup ? isVersionLower(localGroup, remoteGroup) : false,
841
+ level: 1,
842
+ });
843
+ }
844
+ // 하위 Extension 패키지들 추가
845
+ for (const key of installedGroupExtensions) {
846
+ const extVersionPath = path.join(semoSystemDir, key, "VERSION");
847
+ const localExt = fs.readFileSync(extVersionPath, "utf-8").trim();
848
+ const remoteExt = await getRemotePackageVersion(key);
849
+ versionInfos.push({
850
+ name: key,
851
+ local: localExt,
852
+ remote: remoteExt,
853
+ needsUpdate: remoteExt ? isVersionLower(localExt, remoteExt) : false,
854
+ level: 2,
855
+ group: group,
856
+ });
857
+ }
858
+ }
859
+ }
860
+ // 그룹에 속하지 않는 Extension (meta 등)
861
+ const nonGroupExtensions = Object.keys(EXTENSION_PACKAGES).filter(key => !PACKAGE_GROUPS.some(g => key.startsWith(`${g}/`)));
862
+ for (const key of nonGroupExtensions) {
711
863
  const extVersionPath = path.join(semoSystemDir, key, "VERSION");
712
864
  if (fs.existsSync(extVersionPath)) {
713
865
  const localExt = fs.readFileSync(extVersionPath, "utf-8").trim();
@@ -866,6 +1018,18 @@ async function confirmOverwrite(itemName, itemPath) {
866
1018
  ]);
867
1019
  return shouldOverwrite;
868
1020
  }
1021
+ function detectProjectType(cwd) {
1022
+ const detected = [];
1023
+ for (const [key, pkg] of Object.entries(EXTENSION_PACKAGES)) {
1024
+ for (const file of pkg.detect) {
1025
+ if (fs.existsSync(path.join(cwd, file))) {
1026
+ detected.push(key);
1027
+ break;
1028
+ }
1029
+ }
1030
+ }
1031
+ return detected;
1032
+ }
869
1033
  // === 설치된 Extension 패키지 스캔 ===
870
1034
  function getInstalledExtensions(cwd) {
871
1035
  const semoSystemDir = path.join(cwd, "semo-system");
@@ -995,17 +1159,56 @@ program
995
1159
  spinner.fail("Git 레포지토리가 아닙니다. 'git init'을 먼저 실행하세요.");
996
1160
  process.exit(1);
997
1161
  }
998
- // 2. Extension 패키지 처리
999
- // semo-hooks는 기본 포함 (Claude Code Hooks 로깅 시스템)
1000
- let extensionsToInstall = ["semo-hooks"];
1162
+ // 2. 프로젝트 유형 감지
1163
+ const detected = detectProjectType(cwd);
1164
+ let extensionsToInstall = [];
1001
1165
  if (options.with) {
1002
- const additionalPkgs = options.with.split(",").map((p) => p.trim()).filter((p) => p in EXTENSION_PACKAGES);
1003
- extensionsToInstall = [...new Set([...extensionsToInstall, ...additionalPkgs])];
1166
+ extensionsToInstall = options.with.split(",").map((p) => p.trim()).filter((p) => p in EXTENSION_PACKAGES);
1167
+ }
1168
+ else if (detected.length > 0) {
1169
+ console.log(chalk_1.default.cyan("\n📦 감지된 프로젝트 유형:"));
1170
+ detected.forEach(pkg => {
1171
+ console.log(chalk_1.default.gray(` - ${EXTENSION_PACKAGES[pkg].name}: ${EXTENSION_PACKAGES[pkg].desc}`));
1172
+ });
1173
+ const { installDetected } = await inquirer_1.default.prompt([
1174
+ {
1175
+ type: "confirm",
1176
+ name: "installDetected",
1177
+ message: "감지된 패키지를 함께 설치할까요?",
1178
+ default: true,
1179
+ },
1180
+ ]);
1181
+ if (installDetected) {
1182
+ extensionsToInstall = detected;
1183
+ }
1184
+ }
1185
+ else {
1186
+ // 프로젝트 유형이 감지되지 않은 경우 패키지 선택 프롬프트
1187
+ console.log(chalk_1.default.cyan("\n📦 추가 패키지 선택"));
1188
+ console.log(chalk_1.default.gray(" 기본 설치 (semo-core + semo-skills) 외에 추가할 패키지를 선택하세요.\n"));
1189
+ // 그룹별로 패키지 구성
1190
+ const packageChoices = [
1191
+ new inquirer_1.default.Separator(chalk_1.default.yellow("── Engineering ──")),
1192
+ { name: `eng/nextjs - ${EXTENSION_PACKAGES["eng/nextjs"].desc}`, value: "eng/nextjs" },
1193
+ { name: `eng/spring - ${EXTENSION_PACKAGES["eng/spring"].desc}`, value: "eng/spring" },
1194
+ { name: `eng/infra - ${EXTENSION_PACKAGES["eng/infra"].desc}`, value: "eng/infra" },
1195
+ new inquirer_1.default.Separator(chalk_1.default.yellow("── Business ──")),
1196
+ { name: `biz/discovery - ${EXTENSION_PACKAGES["biz/discovery"].desc}`, value: "biz/discovery" },
1197
+ { name: `biz/management - ${EXTENSION_PACKAGES["biz/management"].desc}`, value: "biz/management" },
1198
+ { name: `biz/design - ${EXTENSION_PACKAGES["biz/design"].desc}`, value: "biz/design" },
1199
+ new inquirer_1.default.Separator(chalk_1.default.yellow("── Operations ──")),
1200
+ { name: `ops/qa - ${EXTENSION_PACKAGES["ops/qa"].desc}`, value: "ops/qa" },
1201
+ ];
1202
+ const { selectedPackages } = await inquirer_1.default.prompt([
1203
+ {
1204
+ type: "checkbox",
1205
+ name: "selectedPackages",
1206
+ message: "설치할 패키지 선택 (Space로 선택, Enter로 완료):",
1207
+ choices: packageChoices,
1208
+ },
1209
+ ]);
1210
+ extensionsToInstall = selectedPackages;
1004
1211
  }
1005
- console.log(chalk_1.default.cyan("\n📦 Extension 설치:"));
1006
- extensionsToInstall.forEach(pkg => {
1007
- console.log(chalk_1.default.gray(` - ${EXTENSION_PACKAGES[pkg].name}: ${EXTENSION_PACKAGES[pkg].desc}`));
1008
- });
1009
1212
  // 3. .claude 디렉토리 생성
1010
1213
  const claudeDir = path.join(cwd, ".claude");
1011
1214
  if (!fs.existsSync(claudeDir)) {
@@ -1058,23 +1261,12 @@ program
1058
1261
  console.log(chalk_1.default.gray(` ✓ ${EXTENSION_PACKAGES[pkg].name}`));
1059
1262
  });
1060
1263
  }
1061
- // GitHub MCP 토큰 설정 안내
1062
- if (!isGitHubTokenConfigured()) {
1063
- console.log(chalk_1.default.yellow("\n⚠️ GitHub MCP 설정 필요"));
1064
- console.log(chalk_1.default.gray(" GitHub 이슈/PR 생성 기능을 사용하려면 토큰을 설정하세요:"));
1065
- console.log(chalk_1.default.white("\n export GITHUB_PERSONAL_ACCESS_TOKEN=\"$(gh auth token)\""));
1066
- console.log(chalk_1.default.gray("\n 영구 설정: ~/.zshrc 또는 ~/.bashrc에 위 명령어 추가"));
1067
- }
1068
- else {
1069
- console.log(chalk_1.default.green("\n✓ GitHub 토큰 감지됨 (MCP 사용 가능)"));
1070
- }
1071
1264
  console.log(chalk_1.default.cyan("\n다음 단계:"));
1072
1265
  console.log(chalk_1.default.gray(" 1. Claude Code에서 프로젝트 열기"));
1073
1266
  console.log(chalk_1.default.gray(" 2. 자연어로 요청하기 (예: \"댓글 기능 구현해줘\")"));
1074
1267
  console.log(chalk_1.default.gray(" 3. /SEMO:help로 도움말 확인"));
1075
- // semo-hooks만 설치된 경우 추가 패키지 안내
1076
- if (extensionsToInstall.length === 1 && extensionsToInstall[0] === "semo-hooks") {
1077
- console.log(chalk_1.default.gray("\n💡 추가 패키지: semo add <package> (예: semo add meta)"));
1268
+ if (extensionsToInstall.length === 0 && detected.length === 0) {
1269
+ console.log(chalk_1.default.gray("\n💡 추가 패키지: semo add <package> (예: semo add next)"));
1078
1270
  }
1079
1271
  console.log();
1080
1272
  });
@@ -1422,8 +1614,7 @@ async function downloadExtensions(cwd, packages, force) {
1422
1614
  }
1423
1615
  // 개별 패키지 복사
1424
1616
  for (const pkg of packages) {
1425
- // Extension 패키지는 semo-system/ 폴더에 있음
1426
- const srcPath = path.join(tempDir, "semo-system", pkg);
1617
+ const srcPath = path.join(tempDir, "packages", pkg);
1427
1618
  const destPath = path.join(semoSystemDir, pkg);
1428
1619
  if (fs.existsSync(srcPath)) {
1429
1620
  if (fs.existsSync(destPath) && !force) {
@@ -1499,26 +1690,6 @@ function createMergedOrchestrator(claudeAgentsDir, orchestratorSources) {
1499
1690
  }
1500
1691
  }
1501
1692
  }
1502
- // meta 패키지 포함 여부 확인
1503
- const hasMetaInSources = orchestratorSources.some(s => s.pkg === "meta");
1504
- // Meta 자동 체이닝 섹션
1505
- const metaAutoChainSection = hasMetaInSources ? `
1506
- ## 🔴 Meta 환경 자동 체이닝 (NON-NEGOTIABLE)
1507
-
1508
- > **조건**: semo-system/ 내 파일 수정이 감지되면
1509
- > **동작**: 작업 종료 전 자동으로 \`skill:meta-workflow\` 호출
1510
-
1511
- \`\`\`text
1512
- semo-system/ 파일 수정 감지
1513
-
1514
- [자동] skill:meta-workflow 호출
1515
-
1516
- 버저닝 → 배포 → 로컬 동기화
1517
- \`\`\`
1518
-
1519
- **이 규칙은 우회할 수 없습니다.**
1520
-
1521
- ` : "";
1522
1693
  // 병합된 orchestrator.md 생성
1523
1694
  const mergedContent = `---
1524
1695
  name: orchestrator
@@ -1587,7 +1758,7 @@ ${routingTables.join("\n\n---\n\n")}
1587
1758
  3. **Package Priority**: 라우팅 충돌 시 설치 순서대로 우선순위 적용
1588
1759
  4. **Cross-Package**: 다른 패키지 전문 영역 요청 시 인계 권유
1589
1760
 
1590
- ${metaAutoChainSection}${crossPackageRouting.length > 0 ? `## 🔄 Cross-Package Routing
1761
+ ${crossPackageRouting.length > 0 ? `## 🔄 Cross-Package Routing
1591
1762
 
1592
1763
  ${crossPackageRouting[0]}` : ""}
1593
1764
 
@@ -1706,29 +1877,8 @@ const BASE_MCP_SERVERS = [
1706
1877
  name: "github",
1707
1878
  command: "npx",
1708
1879
  args: ["-y", "@modelcontextprotocol/server-github"],
1709
- env: {
1710
- GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_PERSONAL_ACCESS_TOKEN}",
1711
- },
1712
1880
  },
1713
1881
  ];
1714
- // === GitHub 토큰 자동 감지 ===
1715
- function getGitHubTokenFromCLI() {
1716
- try {
1717
- const token = (0, child_process_1.execSync)("gh auth token", { stdio: "pipe", encoding: "utf-8" }).trim();
1718
- return token || null;
1719
- }
1720
- catch {
1721
- return null;
1722
- }
1723
- }
1724
- function isGitHubTokenConfigured() {
1725
- // 환경변수 확인
1726
- if (process.env.GITHUB_PERSONAL_ACCESS_TOKEN) {
1727
- return true;
1728
- }
1729
- // gh CLI 토큰 확인
1730
- return getGitHubTokenFromCLI() !== null;
1731
- }
1732
1882
  // === Claude MCP 서버 존재 여부 확인 ===
1733
1883
  function isMCPServerRegistered(serverName) {
1734
1884
  try {
@@ -2366,12 +2516,34 @@ async function setupClaudeMd(cwd, extensions, force) {
2366
2516
  const extensionsList = extensions.length > 0
2367
2517
  ? extensions.map(pkg => `├── ${pkg}/ # ${EXTENSION_PACKAGES[pkg].name}`).join("\n")
2368
2518
  : "";
2369
- // 패키지별 CLAUDE.md 병합 섹션 생성
2519
+ // 그룹 및 패키지별 CLAUDE.md 병합 섹션 생성
2370
2520
  let packageClaudeMdSections = "";
2371
- // meta 패키지 설치 여부 확인
2372
- const hasMetaPackage = extensions.includes("meta");
2373
- // 개별 패키지 CLAUDE.md 병합 - 중복 제거 적용
2521
+ // 0. meta 패키지가 설치된 경우 먼저 확인 (meta는 특별 처리)
2522
+ const isMetaInstalled = extensions.includes("meta") ||
2523
+ fs.existsSync(path.join(semoSystemDir, "meta", "VERSION"));
2524
+ // 1. 설치된 패키지에서 그룹 추출 (중복 제거)
2525
+ const installedGroups = [...new Set(extensions.map(pkg => pkg.split("/")[0]).filter(g => PACKAGE_GROUPS.includes(g)))];
2526
+ // 2. 그룹 레벨 CLAUDE.md 먼저 병합 (biz, eng, ops) - 중복 제거 적용
2527
+ for (const group of installedGroups) {
2528
+ const groupClaudeMdPath = path.join(semoSystemDir, group, "CLAUDE.md");
2529
+ if (fs.existsSync(groupClaudeMdPath)) {
2530
+ const groupContent = fs.readFileSync(groupClaudeMdPath, "utf-8");
2531
+ // 중복 제거 후 고유 콘텐츠만 추출
2532
+ const uniqueContent = extractUniqueContent(groupContent, group);
2533
+ // 헤더 레벨 조정 (# → ##, ## → ###)
2534
+ const adjustedContent = uniqueContent
2535
+ .replace(/^# /gm, "## ")
2536
+ .replace(/^## /gm, "### ")
2537
+ .replace(/^### /gm, "#### ");
2538
+ packageClaudeMdSections += `\n\n---\n\n${adjustedContent}`;
2539
+ console.log(chalk_1.default.green(` + ${group}/ 그룹 CLAUDE.md 병합됨 (고유 섹션만)`));
2540
+ }
2541
+ }
2542
+ // 3. 개별 패키지 CLAUDE.md 병합 - 중복 제거 적용
2374
2543
  for (const pkg of extensions) {
2544
+ // meta 패키지는 별도 처리 (아래에서 전체 내용 병합)
2545
+ if (pkg === "meta")
2546
+ continue;
2375
2547
  const pkgClaudeMdPath = path.join(semoSystemDir, pkg, "CLAUDE.md");
2376
2548
  if (fs.existsSync(pkgClaudeMdPath)) {
2377
2549
  const pkgContent = fs.readFileSync(pkgClaudeMdPath, "utf-8");
@@ -2386,22 +2558,48 @@ async function setupClaudeMd(cwd, extensions, force) {
2386
2558
  console.log(chalk_1.default.gray(` + ${pkg}/CLAUDE.md 병합됨 (고유 섹션만)`));
2387
2559
  }
2388
2560
  }
2389
- // 4. Orchestrator 참조 경로 결정 (Extension 패키지 우선)
2561
+ // 3.5. meta 패키지 CLAUDE.md 병합 (전체 내용 - Meta 환경 규칙 포함)
2562
+ if (isMetaInstalled) {
2563
+ const metaClaudeMdPath = path.join(semoSystemDir, "meta", "CLAUDE.md");
2564
+ if (fs.existsSync(metaClaudeMdPath)) {
2565
+ const metaContent = fs.readFileSync(metaClaudeMdPath, "utf-8");
2566
+ const pkgName = EXTENSION_PACKAGES["meta"]?.name || "Meta";
2567
+ // meta는 중복 제거 없이 전체 내용 유지 (Core Rules가 중요)
2568
+ // 헤더 레벨만 조정 (# → ###, ## → ####)
2569
+ const adjustedContent = metaContent
2570
+ .replace(/^# /gm, "### ")
2571
+ .replace(/^## /gm, "#### ");
2572
+ packageClaudeMdSections += `\n\n---\n\n## ${pkgName} 패키지 컨텍스트\n\n${adjustedContent}`;
2573
+ console.log(chalk_1.default.green(` + meta/CLAUDE.md 병합됨 (전체 내용 - Meta 환경 규칙 포함)`));
2574
+ }
2575
+ }
2576
+ // 4. Orchestrator 참조 경로 결정 (Extension 패키지 우선, meta 포함)
2390
2577
  // Extension 패키지 중 orchestrator가 있는 첫 번째 패키지를 Primary로 설정
2391
2578
  let primaryOrchestratorPath = "semo-core/agents/orchestrator/orchestrator.md";
2392
2579
  const orchestratorPaths = [];
2580
+ // meta 패키지 orchestrator 먼저 확인 (meta가 설치되어 있으면 최우선)
2581
+ if (isMetaInstalled) {
2582
+ const metaOrchestratorPath = path.join(semoSystemDir, "meta", "agents/orchestrator/orchestrator.md");
2583
+ if (fs.existsSync(metaOrchestratorPath)) {
2584
+ orchestratorPaths.push("semo-system/meta/agents/orchestrator/orchestrator.md");
2585
+ primaryOrchestratorPath = "meta/agents/orchestrator/orchestrator.md";
2586
+ }
2587
+ }
2588
+ // 나머지 Extension 패키지 orchestrator 확인
2393
2589
  for (const pkg of extensions) {
2590
+ if (pkg === "meta")
2591
+ continue; // meta는 위에서 이미 처리
2394
2592
  const pkgOrchestratorPath = path.join(semoSystemDir, pkg, "agents/orchestrator/orchestrator.md");
2395
2593
  if (fs.existsSync(pkgOrchestratorPath)) {
2396
2594
  orchestratorPaths.push(`semo-system/${pkg}/agents/orchestrator/orchestrator.md`);
2397
- // 번째 Extension 패키지의 orchestrator를 Primary로 설정
2595
+ // Primary가 아직 semo-core이면 패키지를 Primary로 설정
2398
2596
  if (primaryOrchestratorPath === "semo-core/agents/orchestrator/orchestrator.md") {
2399
2597
  primaryOrchestratorPath = `${pkg}/agents/orchestrator/orchestrator.md`;
2400
2598
  }
2401
2599
  }
2402
2600
  }
2403
- // semo-core orchestrator는 항상 포함
2404
- orchestratorPaths.unshift("semo-system/semo-core/agents/orchestrator/orchestrator.md");
2601
+ // semo-core orchestrator는 항상 마지막에 포함 (fallback)
2602
+ orchestratorPaths.push("semo-system/semo-core/agents/orchestrator/orchestrator.md");
2405
2603
  // Orchestrator 참조 섹션 생성
2406
2604
  const orchestratorRefSection = orchestratorPaths.length > 1
2407
2605
  ? `**Primary Orchestrator**: \`semo-system/${primaryOrchestratorPath}\`
@@ -2415,47 +2613,6 @@ ${orchestratorPaths.map(p => `- \`${p}\``).join("\n")}
2415
2613
  : `**반드시 읽어야 할 파일**: \`semo-system/semo-core/agents/orchestrator/orchestrator.md\`
2416
2614
 
2417
2615
  이 파일에서 라우팅 테이블, 의도 분류, 메시지 포맷을 확인하세요.`;
2418
- // Meta 패키지 설치 시 자동 워크플로우 강제 규칙
2419
- const metaWorkflowSection = hasMetaPackage ? `
2420
- ### 3. Meta 환경 자동 워크플로우 (NON-NEGOTIABLE)
2421
-
2422
- > **⚠️ Meta 패키지가 설치된 환경에서는 반드시 아래 규칙이 적용됩니다.**
2423
- > **이 규칙을 우회하거나 무시하는 것은 금지됩니다.**
2424
-
2425
- #### 자동 트리거 조건
2426
-
2427
- \`semo-system/\` 디렉토리 내 파일이 수정되면:
2428
- 1. 작업 종료 전 반드시 \`skill:meta-workflow\` 호출
2429
- 2. 버저닝 → 배포 → 로컬 동기화 체인 자동 실행
2430
-
2431
- #### 감지 패턴
2432
-
2433
- 다음 경로의 파일 수정 시 자동 트리거:
2434
- - \`semo-system/semo-core/**\`
2435
- - \`semo-system/semo-skills/**\`
2436
- - \`semo-system/meta/**\`
2437
- - \`semo-system/semo-remote/**\`
2438
- - \`semo-system/semo-hooks/**\`
2439
- - \`packages/cli/**\` (CLI 수정 시)
2440
-
2441
- #### 강제 동작 흐름
2442
-
2443
- \`\`\`text
2444
- [작업 완료 감지]
2445
-
2446
- semo-system/ 또는 packages/ 파일 수정 여부 확인
2447
-
2448
- 수정됨? → [SEMO] Skill 호출: meta-workflow
2449
- 버저닝 → 배포 → 동기화 자동 실행
2450
-
2451
- 수정 안됨? → 정상 종료
2452
- \`\`\`
2453
-
2454
- **금지 사항**:
2455
- - semo-system/ 수정 후 버저닝 없이 종료
2456
- - "버저닝 나중에 해줘" 요청 수락
2457
- - meta-workflow 스킬 호출 건너뛰기
2458
- ` : "";
2459
2616
  const claudeMdContent = `# SEMO Project Configuration
2460
2617
 
2461
2618
  > SEMO (Semicolon Orchestrate) - AI Agent Orchestration Framework v${VERSION}
@@ -2509,7 +2666,46 @@ npm run build # 3. 빌드 검증 (Next.js/TypeScript 프로젝트)
2509
2666
  - \`--no-verify\` 플래그 사용 금지
2510
2667
  - Quality Gate 우회 시도 거부
2511
2668
  - "그냥 커밋해줘", "빌드 생략해줘" 등 거부
2512
- ${metaWorkflowSection}
2669
+ ${isMetaInstalled ? `
2670
+ ### 3. Meta 환경 자동 워크플로우 (NON-NEGOTIABLE)
2671
+
2672
+ > **⚠️ Meta 패키지가 설치된 환경에서는 반드시 아래 규칙이 적용됩니다.**
2673
+ > **이 규칙을 우회하거나 무시하는 것은 금지됩니다.**
2674
+
2675
+ #### 자동 트리거 조건
2676
+
2677
+ \`semo-system/\` 디렉토리 내 파일이 수정되면:
2678
+ 1. 작업 종료 전 반드시 \`skill:meta-workflow\` 호출
2679
+ 2. 버저닝 → 배포 → 로컬 동기화 체인 자동 실행
2680
+
2681
+ #### 감지 패턴
2682
+
2683
+ 다음 경로의 파일 수정 시 자동 트리거:
2684
+ - \`semo-system/semo-core/**\`
2685
+ - \`semo-system/semo-skills/**\`
2686
+ - \`semo-system/meta/**\`
2687
+ - \`semo-system/semo-remote/**\`
2688
+ - \`semo-system/semo-hooks/**\`
2689
+ - \`packages/cli/**\` (CLI 수정 시)
2690
+
2691
+ #### 강제 동작 흐름
2692
+
2693
+ \`\`\`text
2694
+ [작업 완료 감지]
2695
+
2696
+ semo-system/ 또는 packages/ 파일 수정 여부 확인
2697
+
2698
+ 수정됨? → [SEMO] Skill 호출: meta-workflow
2699
+ 버저닝 → 배포 → 동기화 자동 실행
2700
+
2701
+ 수정 안됨? → 정상 종료
2702
+ \`\`\`
2703
+
2704
+ **금지 사항**:
2705
+ - semo-system/ 수정 후 버저닝 없이 종료
2706
+ - "버저닝 나중에 해줘" 요청 수락
2707
+ - meta-workflow 스킬 호출 건너뛰기
2708
+ ` : ``}
2513
2709
  ---
2514
2710
 
2515
2711
  ## 설치된 구성
@@ -2578,7 +2774,7 @@ ${packageClaudeMdSections}
2578
2774
  // === add 명령어 ===
2579
2775
  program
2580
2776
  .command("add <packages>")
2581
- .description("Extension 패키지를 추가로 설치합니다 (meta, semo-hooks, semo-remote)")
2777
+ .description("Extension 패키지를 추가로 설치합니다 (그룹: biz, eng, ops, system / 개별: biz/discovery, eng/nextjs, semo-hooks)")
2582
2778
  .option("-f, --force", "기존 설정 덮어쓰기")
2583
2779
  .action(async (packagesInput, options) => {
2584
2780
  const cwd = process.cwd();
@@ -2587,16 +2783,25 @@ program
2587
2783
  console.log(chalk_1.default.red("\nSEMO가 설치되어 있지 않습니다. 'semo init'을 먼저 실행하세요.\n"));
2588
2784
  process.exit(1);
2589
2785
  }
2590
- // 패키지 입력 해석
2591
- const { packages } = resolvePackageInput(packagesInput);
2786
+ // 패키지 입력 해석 (그룹, 레거시, 쉼표 구분 모두 처리)
2787
+ const { packages, isGroup, groupName } = resolvePackageInput(packagesInput);
2592
2788
  if (packages.length === 0) {
2593
2789
  console.log(chalk_1.default.red(`\n알 수 없는 패키지: ${packagesInput}`));
2790
+ console.log(chalk_1.default.gray(`사용 가능한 그룹: ${PACKAGE_GROUPS.join(", ")}`));
2594
2791
  console.log(chalk_1.default.gray(`사용 가능한 패키지: ${Object.keys(EXTENSION_PACKAGES).join(", ")}`));
2595
2792
  console.log(chalk_1.default.gray(`단축명: ${Object.keys(SHORTNAME_MAPPING).join(", ")}\n`));
2596
2793
  process.exit(1);
2597
2794
  }
2598
- // 패키지 설치 안내
2599
- if (packages.length === 1) {
2795
+ // 그룹 설치인 경우 안내
2796
+ if (isGroup) {
2797
+ console.log(chalk_1.default.cyan.bold(`\n📦 ${groupName?.toUpperCase()} 그룹 패키지 일괄 설치\n`));
2798
+ console.log(chalk_1.default.gray(" 포함된 패키지:"));
2799
+ for (const pkg of packages) {
2800
+ console.log(chalk_1.default.gray(` - ${pkg} (${EXTENSION_PACKAGES[pkg].name})`));
2801
+ }
2802
+ console.log();
2803
+ }
2804
+ else if (packages.length === 1) {
2600
2805
  // 단일 패키지
2601
2806
  const pkg = packages[0];
2602
2807
  console.log(chalk_1.default.cyan(`\n📦 ${EXTENSION_PACKAGES[pkg].name} 패키지 설치\n`));
@@ -2662,28 +2867,50 @@ program
2662
2867
  .action(() => {
2663
2868
  const cwd = process.cwd();
2664
2869
  const semoSystemDir = path.join(cwd, "semo-system");
2665
- console.log(chalk_1.default.cyan.bold("\n📦 SEMO 패키지 목록\n"));
2666
- // Standard (필수)
2870
+ console.log(chalk_1.default.cyan.bold("\n📦 SEMO 패키지 목록 (v3.0)\n"));
2871
+ // Standard
2667
2872
  console.log(chalk_1.default.white.bold("Standard (필수)"));
2668
2873
  const coreInstalled = fs.existsSync(path.join(semoSystemDir, "semo-core"));
2669
2874
  const skillsInstalled = fs.existsSync(path.join(semoSystemDir, "semo-skills"));
2670
2875
  console.log(` ${coreInstalled ? chalk_1.default.green("✓") : chalk_1.default.gray("○")} semo-core - 원칙, 오케스트레이터`);
2671
2876
  console.log(` ${skillsInstalled ? chalk_1.default.green("✓") : chalk_1.default.gray("○")} semo-skills - 통합 스킬`);
2672
2877
  console.log();
2673
- // Extensions
2674
- console.log(chalk_1.default.white.bold("Extensions (선택)"));
2675
- const extensionList = [
2676
- { key: "meta", name: "Meta", desc: "SEMO 프레임워크 자체 개발/관리" },
2677
- { key: "semo-hooks", name: "Hooks", desc: "Claude Code Hooks 기반 로깅 시스템" },
2678
- { key: "semo-remote", name: "Remote", desc: "Claude Code 원격 제어 (모바일 PWA)" },
2679
- ];
2680
- for (const ext of extensionList) {
2681
- const isInstalled = fs.existsSync(path.join(semoSystemDir, ext.key));
2682
- const status = isInstalled ? chalk_1.default.green("✓") : chalk_1.default.gray("○");
2683
- console.log(` ${status} ${chalk_1.default.cyan(ext.key)} - ${ext.desc}`);
2684
- console.log(chalk_1.default.gray(` semo add ${ext.key}`));
2878
+ // Extensions - 레이어별 그룹화
2879
+ const layers = {
2880
+ biz: { title: "Business Layer", emoji: "💼" },
2881
+ eng: { title: "Engineering Layer", emoji: "⚙️" },
2882
+ ops: { title: "Operations Layer", emoji: "📊" },
2883
+ meta: { title: "Meta", emoji: "🔧" },
2884
+ system: { title: "System", emoji: "🔩" },
2885
+ };
2886
+ for (const [layerKey, layerInfo] of Object.entries(layers)) {
2887
+ const layerPackages = Object.entries(EXTENSION_PACKAGES).filter(([, pkg]) => pkg.layer === layerKey);
2888
+ if (layerPackages.length === 0)
2889
+ continue;
2890
+ console.log(chalk_1.default.white.bold(`${layerInfo.emoji} ${layerInfo.title}`));
2891
+ for (const [key, pkg] of layerPackages) {
2892
+ const isInstalled = fs.existsSync(path.join(semoSystemDir, key));
2893
+ const status = isInstalled ? chalk_1.default.green("✓") : chalk_1.default.gray("○");
2894
+ const displayKey = key.includes("/") ? key.split("/")[1] : key;
2895
+ console.log(` ${status} ${chalk_1.default.cyan(displayKey)} - ${pkg.desc}`);
2896
+ console.log(chalk_1.default.gray(` semo add ${key}`));
2897
+ }
2898
+ console.log();
2685
2899
  }
2900
+ // 그룹 설치 안내
2901
+ console.log(chalk_1.default.gray("─".repeat(50)));
2902
+ console.log(chalk_1.default.white.bold("📦 그룹 일괄 설치"));
2903
+ console.log(chalk_1.default.gray(" semo add biz → Business 전체 (discovery, design, management, poc)"));
2904
+ console.log(chalk_1.default.gray(" semo add eng → Engineering 전체 (nextjs, spring, ms, infra)"));
2905
+ console.log(chalk_1.default.gray(" semo add ops → Operations 전체 (qa, monitor, improve)"));
2906
+ console.log(chalk_1.default.gray(" semo add system → System 전체 (hooks, remote)"));
2686
2907
  console.log();
2908
+ // 단축명 안내
2909
+ console.log(chalk_1.default.gray("─".repeat(50)));
2910
+ console.log(chalk_1.default.white.bold("⚡ 단축명 지원"));
2911
+ console.log(chalk_1.default.gray(" semo add discovery → biz/discovery"));
2912
+ console.log(chalk_1.default.gray(" semo add qa → ops/qa"));
2913
+ console.log(chalk_1.default.gray(" semo add nextjs → eng/nextjs\n"));
2687
2914
  });
2688
2915
  // === status 명령어 ===
2689
2916
  program
@@ -3100,165 +3327,8 @@ program
3100
3327
  else {
3101
3328
  console.log(chalk_1.default.red(" ❌ .claude/ 디렉토리 없음"));
3102
3329
  }
3103
- // 4. semo-hooks 상태 확인
3104
- console.log(chalk_1.default.cyan("\n4. semo-hooks (Claude Code Hooks)"));
3105
- const hooksDir = path.join(semoSystemDir, "semo-hooks");
3106
- const hooksDistDir = path.join(hooksDir, "dist");
3107
- const hooksIndexJs = path.join(hooksDistDir, "index.js");
3108
- if (!fs.existsSync(hooksDir)) {
3109
- console.log(chalk_1.default.gray(" ⏭️ semo-hooks 미설치 (선택 패키지)"));
3110
- console.log(chalk_1.default.gray(" 💡 설치: semo add hooks"));
3111
- }
3112
- else {
3113
- // hooks 버전 확인
3114
- const hooksVersionPath = path.join(hooksDir, "VERSION");
3115
- const hooksVersion = fs.existsSync(hooksVersionPath)
3116
- ? fs.readFileSync(hooksVersionPath, "utf-8").trim()
3117
- : "?";
3118
- console.log(chalk_1.default.green(` ✅ semo-hooks v${hooksVersion} 설치됨`));
3119
- // 빌드 상태 확인
3120
- if (!fs.existsSync(hooksDistDir) || !fs.existsSync(hooksIndexJs)) {
3121
- console.log(chalk_1.default.red(" ❌ 빌드되지 않음 (dist/index.js 없음)"));
3122
- console.log(chalk_1.default.gray(" 💡 해결: semo hooks enable"));
3123
- }
3124
- else {
3125
- console.log(chalk_1.default.green(" ✅ 빌드 완료 (dist/index.js 존재)"));
3126
- }
3127
- // settings.local.json hooks 설정 확인
3128
- const homeDir = process.env.HOME || process.env.USERPROFILE || "";
3129
- const settingsPath = path.join(homeDir, ".claude", "settings.local.json");
3130
- if (!fs.existsSync(settingsPath)) {
3131
- console.log(chalk_1.default.yellow(" ⚠️ settings.local.json 없음"));
3132
- console.log(chalk_1.default.gray(" 💡 해결: semo hooks enable"));
3133
- }
3134
- else {
3135
- try {
3136
- const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
3137
- const hooksConfig = settings.hooks;
3138
- if (!hooksConfig) {
3139
- console.log(chalk_1.default.yellow(" ⚠️ hooks 설정 없음"));
3140
- console.log(chalk_1.default.gray(" 💡 해결: semo hooks enable"));
3141
- }
3142
- else {
3143
- const requiredHooks = ["SessionStart", "UserPromptSubmit", "Stop", "SessionEnd"];
3144
- const missingHooks = [];
3145
- const invalidPathHooks = [];
3146
- for (const hookName of requiredHooks) {
3147
- const hookArray = hooksConfig[hookName];
3148
- if (!hookArray || !Array.isArray(hookArray) || hookArray.length === 0) {
3149
- missingHooks.push(hookName);
3150
- }
3151
- else {
3152
- // 경로 검증
3153
- const hookEntry = hookArray[0];
3154
- const innerHooks = hookEntry?.hooks;
3155
- if (innerHooks && Array.isArray(innerHooks) && innerHooks.length > 0) {
3156
- const command = innerHooks[0]?.command || "";
3157
- // 현재 프로젝트의 semo-hooks 경로와 비교
3158
- if (!command.includes(hooksDir) && !command.includes("semo-hooks")) {
3159
- invalidPathHooks.push(hookName);
3160
- }
3161
- }
3162
- }
3163
- }
3164
- if (missingHooks.length > 0) {
3165
- console.log(chalk_1.default.yellow(` ⚠️ 누락된 hooks: ${missingHooks.join(", ")}`));
3166
- console.log(chalk_1.default.gray(" 💡 해결: semo hooks enable"));
3167
- }
3168
- else if (invalidPathHooks.length > 0) {
3169
- console.log(chalk_1.default.yellow(` ⚠️ 경로 불일치: ${invalidPathHooks.join(", ")}`));
3170
- console.log(chalk_1.default.gray(" 💡 해결: semo hooks enable (다른 프로젝트 설정 감지)"));
3171
- }
3172
- else {
3173
- console.log(chalk_1.default.green(" ✅ hooks 설정 완료 (4개 hook 등록됨)"));
3174
- }
3175
- }
3176
- }
3177
- catch {
3178
- console.log(chalk_1.default.red(" ❌ settings.local.json 파싱 오류"));
3179
- }
3180
- }
3181
- }
3182
- // 5. semo-mcp (MCP 서버) 상태 확인
3183
- console.log(chalk_1.default.cyan("\n5. semo-mcp (MCP 서버)"));
3184
- const userHomeDir = process.env.HOME || process.env.USERPROFILE || "";
3185
- const claudeSettingsPath = path.join(userHomeDir, ".claude", "settings.local.json");
3186
- // MCP 서버 설정 확인
3187
- if (!fs.existsSync(claudeSettingsPath)) {
3188
- console.log(chalk_1.default.yellow(" ⚠️ settings.local.json 없음"));
3189
- console.log(chalk_1.default.gray(" 💡 MCP 서버 설정이 필요합니다"));
3190
- }
3191
- else {
3192
- try {
3193
- const settings = JSON.parse(fs.readFileSync(claudeSettingsPath, "utf-8"));
3194
- const mcpServers = settings.mcpServers || {};
3195
- // semo-integrations MCP 서버 확인
3196
- const semoMcp = mcpServers["semo-integrations"];
3197
- if (!semoMcp) {
3198
- console.log(chalk_1.default.yellow(" ⚠️ semo-integrations MCP 서버 미등록"));
3199
- console.log(chalk_1.default.gray(" 💡 .claude/settings.json에 MCP 서버 추가 필요"));
3200
- }
3201
- else {
3202
- console.log(chalk_1.default.green(" ✅ semo-integrations MCP 서버 등록됨"));
3203
- // 명령어 경로 확인
3204
- const mcpCommand = semoMcp.command || "";
3205
- const mcpArgs = semoMcp.args || [];
3206
- if (mcpCommand === "npx") {
3207
- console.log(chalk_1.default.green(" ✅ npx 방식 실행 (자동 업데이트)"));
3208
- }
3209
- else if (mcpCommand === "node") {
3210
- const scriptPath = mcpArgs[0] || "";
3211
- if (scriptPath && fs.existsSync(scriptPath)) {
3212
- console.log(chalk_1.default.green(` ✅ 로컬 스크립트: ${scriptPath}`));
3213
- }
3214
- else if (scriptPath) {
3215
- console.log(chalk_1.default.red(` ❌ 스크립트 경로 없음: ${scriptPath}`));
3216
- }
3217
- }
3218
- // 환경변수 확인
3219
- const env = semoMcp.env || {};
3220
- const requiredEnvVars = ["SLACK_BOT_TOKEN", "GITHUB_TOKEN"];
3221
- const missingEnvVars = [];
3222
- const configuredEnvVars = [];
3223
- for (const envVar of requiredEnvVars) {
3224
- if (env[envVar]) {
3225
- configuredEnvVars.push(envVar);
3226
- }
3227
- else {
3228
- missingEnvVars.push(envVar);
3229
- }
3230
- }
3231
- if (configuredEnvVars.length > 0) {
3232
- console.log(chalk_1.default.green(` ✅ 환경변수: ${configuredEnvVars.join(", ")}`));
3233
- }
3234
- if (missingEnvVars.length > 0) {
3235
- console.log(chalk_1.default.yellow(` ⚠️ 미설정 환경변수: ${missingEnvVars.join(", ")}`));
3236
- console.log(chalk_1.default.gray(" 💡 일부 기능이 제한될 수 있습니다"));
3237
- }
3238
- // 도구 목록 (알려진 도구)
3239
- const knownTools = [
3240
- "slack_send_message",
3241
- "slack_lookup_user",
3242
- "slack_list_channels",
3243
- "slack_find_channel",
3244
- "github_create_issue",
3245
- "github_create_pr",
3246
- "supabase_query",
3247
- ];
3248
- console.log(chalk_1.default.gray(` 📦 제공 도구: ${knownTools.length}개`));
3249
- }
3250
- // 다른 MCP 서버 확인
3251
- const otherServers = Object.keys(mcpServers).filter(k => k !== "semo-integrations");
3252
- if (otherServers.length > 0) {
3253
- console.log(chalk_1.default.gray(` 📡 기타 MCP 서버: ${otherServers.join(", ")}`));
3254
- }
3255
- }
3256
- catch {
3257
- console.log(chalk_1.default.red(" ❌ settings.local.json 파싱 오류"));
3258
- }
3259
- }
3260
- // 6. 전체 설치 검증
3261
- console.log(chalk_1.default.cyan("\n6. 전체 설치 검증"));
3330
+ // 4. 설치 검증
3331
+ console.log(chalk_1.default.cyan("\n4. 전체 설치 검증"));
3262
3332
  const verificationResult = verifyInstallation(cwd, []);
3263
3333
  if (verificationResult.success) {
3264
3334
  console.log(chalk_1.default.green(" ✅ 설치 상태 정상"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-semicolon/semo-cli",
3
- "version": "3.7.4",
3
+ "version": "3.9.0",
4
4
  "description": "SEMO CLI - AI Agent Orchestration Framework Installer",
5
5
  "main": "dist/index.js",
6
6
  "bin": {