@team-semicolon/semo-cli 3.8.1 → 3.10.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 +384 -447
  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,23 @@ 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. Extension 패키지 처리 (--with 옵션만 지원, 인터랙션 없음)
1163
+ let extensionsToInstall = [];
1001
1164
  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])];
1165
+ // --with 옵션으로 명시적 패키지 지정 시에만 Extension 설치
1166
+ extensionsToInstall = options.with.split(",").map((p) => p.trim()).filter((p) => p in EXTENSION_PACKAGES || p in SHORTNAME_MAPPING);
1167
+ // 별칭 처리
1168
+ extensionsToInstall = extensionsToInstall.map((p) => SHORTNAME_MAPPING[p] || p);
1169
+ }
1170
+ // 프로젝트 유형 감지는 정보 제공용으로만 사용 (자동 설치 안 함)
1171
+ const detected = detectProjectType(cwd);
1172
+ if (detected.length > 0 && !options.with) {
1173
+ console.log(chalk_1.default.cyan("\n💡 감지된 프로젝트 유형:"));
1174
+ detected.forEach(pkg => {
1175
+ console.log(chalk_1.default.gray(` - ${EXTENSION_PACKAGES[pkg].name}: ${EXTENSION_PACKAGES[pkg].desc}`));
1176
+ });
1177
+ console.log(chalk_1.default.gray(`\n 추가 패키지가 필요하면: semo add ${detected[0].split("/")[1] || detected[0]}`));
1004
1178
  }
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
1179
  // 3. .claude 디렉토리 생성
1010
1180
  const claudeDir = path.join(cwd, ".claude");
1011
1181
  if (!fs.existsSync(claudeDir)) {
@@ -1058,23 +1228,12 @@ program
1058
1228
  console.log(chalk_1.default.gray(` ✓ ${EXTENSION_PACKAGES[pkg].name}`));
1059
1229
  });
1060
1230
  }
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
1231
  console.log(chalk_1.default.cyan("\n다음 단계:"));
1072
1232
  console.log(chalk_1.default.gray(" 1. Claude Code에서 프로젝트 열기"));
1073
1233
  console.log(chalk_1.default.gray(" 2. 자연어로 요청하기 (예: \"댓글 기능 구현해줘\")"));
1074
1234
  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)"));
1235
+ if (extensionsToInstall.length === 0) {
1236
+ console.log(chalk_1.default.gray("\n💡 추가 패키지가 필요하면: semo add <package> (예: semo add next)"));
1078
1237
  }
1079
1238
  console.log();
1080
1239
  });
@@ -1163,42 +1322,24 @@ async function createStandardSymlinks(cwd) {
1163
1322
  }
1164
1323
  console.log(chalk_1.default.green(` ✓ .claude/skills/ (${skills.length}개 skill 링크됨)`));
1165
1324
  }
1166
- // commands 링크 (개별 파일 심볼릭 링크 방식 - 여러 패키지 지원)
1325
+ // commands 링크
1167
1326
  const commandsDir = path.join(claudeDir, "commands");
1168
1327
  fs.mkdirSync(commandsDir, { recursive: true });
1169
- const semoCommandsDir = path.join(commandsDir, "SEMO");
1170
- // 기존 링크/디렉토리가 있으면 삭제 재생성
1171
- if (fs.existsSync(semoCommandsDir)) {
1172
- if (fs.lstatSync(semoCommandsDir).isSymbolicLink()) {
1173
- fs.unlinkSync(semoCommandsDir);
1328
+ const semoCommandsLink = path.join(commandsDir, "SEMO");
1329
+ const commandsTarget = path.join(semoSystemDir, "semo-core", "commands", "SEMO");
1330
+ // 기존 링크가 있으면 삭제 후 재생성 (업데이트 시에도 최신 반영)
1331
+ if (fs.existsSync(semoCommandsLink)) {
1332
+ if (fs.lstatSync(semoCommandsLink).isSymbolicLink()) {
1333
+ fs.unlinkSync(semoCommandsLink);
1174
1334
  }
1175
1335
  else {
1176
- removeRecursive(semoCommandsDir);
1336
+ removeRecursive(semoCommandsLink);
1177
1337
  }
1178
1338
  }
1179
- // SEMO 커맨드 디렉토리 생성 (실제 디렉토리)
1180
- fs.mkdirSync(semoCommandsDir, { recursive: true });
1181
- // 커맨드 소스 디렉토리 목록 (semo-core + semo-remote)
1182
- const commandSources = [
1183
- { name: "semo-core", dir: path.join(semoSystemDir, "semo-core", "commands", "SEMO") },
1184
- { name: "semo-remote", dir: path.join(semoSystemDir, "semo-remote", "commands", "SEMO") },
1185
- ];
1186
- let totalCommands = 0;
1187
- for (const source of commandSources) {
1188
- if (fs.existsSync(source.dir)) {
1189
- const commandFiles = fs.readdirSync(source.dir).filter(f => f.endsWith(".md"));
1190
- for (const cmdFile of commandFiles) {
1191
- const cmdTarget = path.join(source.dir, cmdFile);
1192
- const cmdLink = path.join(semoCommandsDir, cmdFile);
1193
- // 이미 링크가 있으면 건너뛰기 (semo-core 우선)
1194
- if (!fs.existsSync(cmdLink)) {
1195
- createSymlinkOrJunction(cmdTarget, cmdLink);
1196
- totalCommands++;
1197
- }
1198
- }
1199
- }
1339
+ if (fs.existsSync(commandsTarget)) {
1340
+ createSymlinkOrJunction(commandsTarget, semoCommandsLink);
1341
+ console.log(chalk_1.default.green(" ✓ .claude/commands/SEMO semo-system/semo-core/commands/SEMO"));
1200
1342
  }
1201
- console.log(chalk_1.default.green(` ✓ .claude/commands/SEMO (${totalCommands}개 command 링크됨)`));
1202
1343
  }
1203
1344
  /**
1204
1345
  * 설치 상태를 검증하고 문제점을 리포트
@@ -1279,26 +1420,15 @@ function verifyInstallation(cwd, installedExtensions = []) {
1279
1420
  }
1280
1421
  }
1281
1422
  }
1282
- // 4. commands 검증 (디렉토리 또는 개별 파일 심볼릭 링크 방식)
1283
- const semoCommandsDir = path.join(claudeDir, "commands", "SEMO");
1423
+ // 4. commands 검증 (isSymlinkValid 사용)
1424
+ const semoCommandsLink = path.join(claudeDir, "commands", "SEMO");
1284
1425
  try {
1285
- const dirExists = fs.existsSync(semoCommandsDir);
1286
- result.stats.commands.exists = dirExists;
1287
- if (dirExists) {
1288
- // 디렉토리인 경우: 내부 파일 검증
1289
- if (fs.lstatSync(semoCommandsDir).isDirectory() && !fs.lstatSync(semoCommandsDir).isSymbolicLink()) {
1290
- const cmdFiles = fs.readdirSync(semoCommandsDir).filter(f => f.endsWith(".md"));
1291
- result.stats.commands.valid = cmdFiles.length > 0;
1292
- if (!result.stats.commands.valid) {
1293
- result.warnings.push("commands/SEMO 디렉토리가 비어 있습니다");
1294
- }
1295
- }
1296
- else if (fs.lstatSync(semoCommandsDir).isSymbolicLink()) {
1297
- // 심볼릭 링크인 경우 (구버전 호환)
1298
- result.stats.commands.valid = isSymlinkValid(semoCommandsDir);
1299
- if (!result.stats.commands.valid) {
1300
- result.warnings.push("깨진 링크: .claude/commands/SEMO");
1301
- }
1426
+ const linkExists = fs.existsSync(semoCommandsLink) || fs.lstatSync(semoCommandsLink).isSymbolicLink();
1427
+ result.stats.commands.exists = linkExists;
1428
+ if (linkExists) {
1429
+ result.stats.commands.valid = isSymlinkValid(semoCommandsLink);
1430
+ if (!result.stats.commands.valid) {
1431
+ result.warnings.push("깨진 링크: .claude/commands/SEMO");
1302
1432
  }
1303
1433
  }
1304
1434
  }
@@ -1451,8 +1581,7 @@ async function downloadExtensions(cwd, packages, force) {
1451
1581
  }
1452
1582
  // 개별 패키지 복사
1453
1583
  for (const pkg of packages) {
1454
- // Extension 패키지는 semo-system/ 폴더에 있음
1455
- const srcPath = path.join(tempDir, "semo-system", pkg);
1584
+ const srcPath = path.join(tempDir, "packages", pkg);
1456
1585
  const destPath = path.join(semoSystemDir, pkg);
1457
1586
  if (fs.existsSync(srcPath)) {
1458
1587
  if (fs.existsSync(destPath) && !force) {
@@ -1528,26 +1657,6 @@ function createMergedOrchestrator(claudeAgentsDir, orchestratorSources) {
1528
1657
  }
1529
1658
  }
1530
1659
  }
1531
- // meta 패키지 포함 여부 확인
1532
- const hasMetaInSources = orchestratorSources.some(s => s.pkg === "meta");
1533
- // Meta 자동 체이닝 섹션
1534
- const metaAutoChainSection = hasMetaInSources ? `
1535
- ## 🔴 Meta 환경 자동 체이닝 (NON-NEGOTIABLE)
1536
-
1537
- > **조건**: semo-system/ 내 파일 수정이 감지되면
1538
- > **동작**: 작업 종료 전 자동으로 \`skill:meta-workflow\` 호출
1539
-
1540
- \`\`\`text
1541
- semo-system/ 파일 수정 감지
1542
-
1543
- [자동] skill:meta-workflow 호출
1544
-
1545
- 버저닝 → 배포 → 로컬 동기화
1546
- \`\`\`
1547
-
1548
- **이 규칙은 우회할 수 없습니다.**
1549
-
1550
- ` : "";
1551
1660
  // 병합된 orchestrator.md 생성
1552
1661
  const mergedContent = `---
1553
1662
  name: orchestrator
@@ -1616,7 +1725,7 @@ ${routingTables.join("\n\n---\n\n")}
1616
1725
  3. **Package Priority**: 라우팅 충돌 시 설치 순서대로 우선순위 적용
1617
1726
  4. **Cross-Package**: 다른 패키지 전문 영역 요청 시 인계 권유
1618
1727
 
1619
- ${metaAutoChainSection}${crossPackageRouting.length > 0 ? `## 🔄 Cross-Package Routing
1728
+ ${crossPackageRouting.length > 0 ? `## 🔄 Cross-Package Routing
1620
1729
 
1621
1730
  ${crossPackageRouting[0]}` : ""}
1622
1731
 
@@ -1735,29 +1844,8 @@ const BASE_MCP_SERVERS = [
1735
1844
  name: "github",
1736
1845
  command: "npx",
1737
1846
  args: ["-y", "@modelcontextprotocol/server-github"],
1738
- env: {
1739
- GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_PERSONAL_ACCESS_TOKEN}",
1740
- },
1741
1847
  },
1742
1848
  ];
1743
- // === GitHub 토큰 자동 감지 ===
1744
- function getGitHubTokenFromCLI() {
1745
- try {
1746
- const token = (0, child_process_1.execSync)("gh auth token", { stdio: "pipe", encoding: "utf-8" }).trim();
1747
- return token || null;
1748
- }
1749
- catch {
1750
- return null;
1751
- }
1752
- }
1753
- function isGitHubTokenConfigured() {
1754
- // 환경변수 확인
1755
- if (process.env.GITHUB_PERSONAL_ACCESS_TOKEN) {
1756
- return true;
1757
- }
1758
- // gh CLI 토큰 확인
1759
- return getGitHubTokenFromCLI() !== null;
1760
- }
1761
1849
  // === Claude MCP 서버 존재 여부 확인 ===
1762
1850
  function isMCPServerRegistered(serverName) {
1763
1851
  try {
@@ -2395,12 +2483,34 @@ async function setupClaudeMd(cwd, extensions, force) {
2395
2483
  const extensionsList = extensions.length > 0
2396
2484
  ? extensions.map(pkg => `├── ${pkg}/ # ${EXTENSION_PACKAGES[pkg].name}`).join("\n")
2397
2485
  : "";
2398
- // 패키지별 CLAUDE.md 병합 섹션 생성
2486
+ // 그룹 및 패키지별 CLAUDE.md 병합 섹션 생성
2399
2487
  let packageClaudeMdSections = "";
2400
- // meta 패키지 설치 여부 확인
2401
- const hasMetaPackage = extensions.includes("meta");
2402
- // 개별 패키지 CLAUDE.md 병합 - 중복 제거 적용
2488
+ // 0. meta 패키지가 설치된 경우 먼저 확인 (meta는 특별 처리)
2489
+ const isMetaInstalled = extensions.includes("meta") ||
2490
+ fs.existsSync(path.join(semoSystemDir, "meta", "VERSION"));
2491
+ // 1. 설치된 패키지에서 그룹 추출 (중복 제거)
2492
+ const installedGroups = [...new Set(extensions.map(pkg => pkg.split("/")[0]).filter(g => PACKAGE_GROUPS.includes(g)))];
2493
+ // 2. 그룹 레벨 CLAUDE.md 먼저 병합 (biz, eng, ops) - 중복 제거 적용
2494
+ for (const group of installedGroups) {
2495
+ const groupClaudeMdPath = path.join(semoSystemDir, group, "CLAUDE.md");
2496
+ if (fs.existsSync(groupClaudeMdPath)) {
2497
+ const groupContent = fs.readFileSync(groupClaudeMdPath, "utf-8");
2498
+ // 중복 제거 후 고유 콘텐츠만 추출
2499
+ const uniqueContent = extractUniqueContent(groupContent, group);
2500
+ // 헤더 레벨 조정 (# → ##, ## → ###)
2501
+ const adjustedContent = uniqueContent
2502
+ .replace(/^# /gm, "## ")
2503
+ .replace(/^## /gm, "### ")
2504
+ .replace(/^### /gm, "#### ");
2505
+ packageClaudeMdSections += `\n\n---\n\n${adjustedContent}`;
2506
+ console.log(chalk_1.default.green(` + ${group}/ 그룹 CLAUDE.md 병합됨 (고유 섹션만)`));
2507
+ }
2508
+ }
2509
+ // 3. 개별 패키지 CLAUDE.md 병합 - 중복 제거 적용
2403
2510
  for (const pkg of extensions) {
2511
+ // meta 패키지는 별도 처리 (아래에서 전체 내용 병합)
2512
+ if (pkg === "meta")
2513
+ continue;
2404
2514
  const pkgClaudeMdPath = path.join(semoSystemDir, pkg, "CLAUDE.md");
2405
2515
  if (fs.existsSync(pkgClaudeMdPath)) {
2406
2516
  const pkgContent = fs.readFileSync(pkgClaudeMdPath, "utf-8");
@@ -2415,22 +2525,48 @@ async function setupClaudeMd(cwd, extensions, force) {
2415
2525
  console.log(chalk_1.default.gray(` + ${pkg}/CLAUDE.md 병합됨 (고유 섹션만)`));
2416
2526
  }
2417
2527
  }
2418
- // 4. Orchestrator 참조 경로 결정 (Extension 패키지 우선)
2528
+ // 3.5. meta 패키지 CLAUDE.md 병합 (전체 내용 - Meta 환경 규칙 포함)
2529
+ if (isMetaInstalled) {
2530
+ const metaClaudeMdPath = path.join(semoSystemDir, "meta", "CLAUDE.md");
2531
+ if (fs.existsSync(metaClaudeMdPath)) {
2532
+ const metaContent = fs.readFileSync(metaClaudeMdPath, "utf-8");
2533
+ const pkgName = EXTENSION_PACKAGES["meta"]?.name || "Meta";
2534
+ // meta는 중복 제거 없이 전체 내용 유지 (Core Rules가 중요)
2535
+ // 헤더 레벨만 조정 (# → ###, ## → ####)
2536
+ const adjustedContent = metaContent
2537
+ .replace(/^# /gm, "### ")
2538
+ .replace(/^## /gm, "#### ");
2539
+ packageClaudeMdSections += `\n\n---\n\n## ${pkgName} 패키지 컨텍스트\n\n${adjustedContent}`;
2540
+ console.log(chalk_1.default.green(` + meta/CLAUDE.md 병합됨 (전체 내용 - Meta 환경 규칙 포함)`));
2541
+ }
2542
+ }
2543
+ // 4. Orchestrator 참조 경로 결정 (Extension 패키지 우선, meta 포함)
2419
2544
  // Extension 패키지 중 orchestrator가 있는 첫 번째 패키지를 Primary로 설정
2420
2545
  let primaryOrchestratorPath = "semo-core/agents/orchestrator/orchestrator.md";
2421
2546
  const orchestratorPaths = [];
2547
+ // meta 패키지 orchestrator 먼저 확인 (meta가 설치되어 있으면 최우선)
2548
+ if (isMetaInstalled) {
2549
+ const metaOrchestratorPath = path.join(semoSystemDir, "meta", "agents/orchestrator/orchestrator.md");
2550
+ if (fs.existsSync(metaOrchestratorPath)) {
2551
+ orchestratorPaths.push("semo-system/meta/agents/orchestrator/orchestrator.md");
2552
+ primaryOrchestratorPath = "meta/agents/orchestrator/orchestrator.md";
2553
+ }
2554
+ }
2555
+ // 나머지 Extension 패키지 orchestrator 확인
2422
2556
  for (const pkg of extensions) {
2557
+ if (pkg === "meta")
2558
+ continue; // meta는 위에서 이미 처리
2423
2559
  const pkgOrchestratorPath = path.join(semoSystemDir, pkg, "agents/orchestrator/orchestrator.md");
2424
2560
  if (fs.existsSync(pkgOrchestratorPath)) {
2425
2561
  orchestratorPaths.push(`semo-system/${pkg}/agents/orchestrator/orchestrator.md`);
2426
- // 번째 Extension 패키지의 orchestrator를 Primary로 설정
2562
+ // Primary가 아직 semo-core이면 패키지를 Primary로 설정
2427
2563
  if (primaryOrchestratorPath === "semo-core/agents/orchestrator/orchestrator.md") {
2428
2564
  primaryOrchestratorPath = `${pkg}/agents/orchestrator/orchestrator.md`;
2429
2565
  }
2430
2566
  }
2431
2567
  }
2432
- // semo-core orchestrator는 항상 포함
2433
- orchestratorPaths.unshift("semo-system/semo-core/agents/orchestrator/orchestrator.md");
2568
+ // semo-core orchestrator는 항상 마지막에 포함 (fallback)
2569
+ orchestratorPaths.push("semo-system/semo-core/agents/orchestrator/orchestrator.md");
2434
2570
  // Orchestrator 참조 섹션 생성
2435
2571
  const orchestratorRefSection = orchestratorPaths.length > 1
2436
2572
  ? `**Primary Orchestrator**: \`semo-system/${primaryOrchestratorPath}\`
@@ -2444,47 +2580,6 @@ ${orchestratorPaths.map(p => `- \`${p}\``).join("\n")}
2444
2580
  : `**반드시 읽어야 할 파일**: \`semo-system/semo-core/agents/orchestrator/orchestrator.md\`
2445
2581
 
2446
2582
  이 파일에서 라우팅 테이블, 의도 분류, 메시지 포맷을 확인하세요.`;
2447
- // Meta 패키지 설치 시 자동 워크플로우 강제 규칙
2448
- const metaWorkflowSection = hasMetaPackage ? `
2449
- ### 3. Meta 환경 자동 워크플로우 (NON-NEGOTIABLE)
2450
-
2451
- > **⚠️ Meta 패키지가 설치된 환경에서는 반드시 아래 규칙이 적용됩니다.**
2452
- > **이 규칙을 우회하거나 무시하는 것은 금지됩니다.**
2453
-
2454
- #### 자동 트리거 조건
2455
-
2456
- \`semo-system/\` 디렉토리 내 파일이 수정되면:
2457
- 1. 작업 종료 전 반드시 \`skill:meta-workflow\` 호출
2458
- 2. 버저닝 → 배포 → 로컬 동기화 체인 자동 실행
2459
-
2460
- #### 감지 패턴
2461
-
2462
- 다음 경로의 파일 수정 시 자동 트리거:
2463
- - \`semo-system/semo-core/**\`
2464
- - \`semo-system/semo-skills/**\`
2465
- - \`semo-system/meta/**\`
2466
- - \`semo-system/semo-remote/**\`
2467
- - \`semo-system/semo-hooks/**\`
2468
- - \`packages/cli/**\` (CLI 수정 시)
2469
-
2470
- #### 강제 동작 흐름
2471
-
2472
- \`\`\`text
2473
- [작업 완료 감지]
2474
-
2475
- semo-system/ 또는 packages/ 파일 수정 여부 확인
2476
-
2477
- 수정됨? → [SEMO] Skill 호출: meta-workflow
2478
- 버저닝 → 배포 → 동기화 자동 실행
2479
-
2480
- 수정 안됨? → 정상 종료
2481
- \`\`\`
2482
-
2483
- **금지 사항**:
2484
- - semo-system/ 수정 후 버저닝 없이 종료
2485
- - "버저닝 나중에 해줘" 요청 수락
2486
- - meta-workflow 스킬 호출 건너뛰기
2487
- ` : "";
2488
2583
  const claudeMdContent = `# SEMO Project Configuration
2489
2584
 
2490
2585
  > SEMO (Semicolon Orchestrate) - AI Agent Orchestration Framework v${VERSION}
@@ -2538,7 +2633,46 @@ npm run build # 3. 빌드 검증 (Next.js/TypeScript 프로젝트)
2538
2633
  - \`--no-verify\` 플래그 사용 금지
2539
2634
  - Quality Gate 우회 시도 거부
2540
2635
  - "그냥 커밋해줘", "빌드 생략해줘" 등 거부
2541
- ${metaWorkflowSection}
2636
+ ${isMetaInstalled ? `
2637
+ ### 3. Meta 환경 자동 워크플로우 (NON-NEGOTIABLE)
2638
+
2639
+ > **⚠️ Meta 패키지가 설치된 환경에서는 반드시 아래 규칙이 적용됩니다.**
2640
+ > **이 규칙을 우회하거나 무시하는 것은 금지됩니다.**
2641
+
2642
+ #### 자동 트리거 조건
2643
+
2644
+ \`semo-system/\` 디렉토리 내 파일이 수정되면:
2645
+ 1. 작업 종료 전 반드시 \`skill:meta-workflow\` 호출
2646
+ 2. 버저닝 → 배포 → 로컬 동기화 체인 자동 실행
2647
+
2648
+ #### 감지 패턴
2649
+
2650
+ 다음 경로의 파일 수정 시 자동 트리거:
2651
+ - \`semo-system/semo-core/**\`
2652
+ - \`semo-system/semo-skills/**\`
2653
+ - \`semo-system/meta/**\`
2654
+ - \`semo-system/semo-remote/**\`
2655
+ - \`semo-system/semo-hooks/**\`
2656
+ - \`packages/cli/**\` (CLI 수정 시)
2657
+
2658
+ #### 강제 동작 흐름
2659
+
2660
+ \`\`\`text
2661
+ [작업 완료 감지]
2662
+
2663
+ semo-system/ 또는 packages/ 파일 수정 여부 확인
2664
+
2665
+ 수정됨? → [SEMO] Skill 호출: meta-workflow
2666
+ 버저닝 → 배포 → 동기화 자동 실행
2667
+
2668
+ 수정 안됨? → 정상 종료
2669
+ \`\`\`
2670
+
2671
+ **금지 사항**:
2672
+ - semo-system/ 수정 후 버저닝 없이 종료
2673
+ - "버저닝 나중에 해줘" 요청 수락
2674
+ - meta-workflow 스킬 호출 건너뛰기
2675
+ ` : ``}
2542
2676
  ---
2543
2677
 
2544
2678
  ## 설치된 구성
@@ -2607,7 +2741,7 @@ ${packageClaudeMdSections}
2607
2741
  // === add 명령어 ===
2608
2742
  program
2609
2743
  .command("add <packages>")
2610
- .description("Extension 패키지를 추가로 설치합니다 (meta, semo-hooks, semo-remote)")
2744
+ .description("Extension 패키지를 추가로 설치합니다 (그룹: biz, eng, ops, system / 개별: biz/discovery, eng/nextjs, semo-hooks)")
2611
2745
  .option("-f, --force", "기존 설정 덮어쓰기")
2612
2746
  .action(async (packagesInput, options) => {
2613
2747
  const cwd = process.cwd();
@@ -2616,16 +2750,25 @@ program
2616
2750
  console.log(chalk_1.default.red("\nSEMO가 설치되어 있지 않습니다. 'semo init'을 먼저 실행하세요.\n"));
2617
2751
  process.exit(1);
2618
2752
  }
2619
- // 패키지 입력 해석
2620
- const { packages } = resolvePackageInput(packagesInput);
2753
+ // 패키지 입력 해석 (그룹, 레거시, 쉼표 구분 모두 처리)
2754
+ const { packages, isGroup, groupName } = resolvePackageInput(packagesInput);
2621
2755
  if (packages.length === 0) {
2622
2756
  console.log(chalk_1.default.red(`\n알 수 없는 패키지: ${packagesInput}`));
2757
+ console.log(chalk_1.default.gray(`사용 가능한 그룹: ${PACKAGE_GROUPS.join(", ")}`));
2623
2758
  console.log(chalk_1.default.gray(`사용 가능한 패키지: ${Object.keys(EXTENSION_PACKAGES).join(", ")}`));
2624
2759
  console.log(chalk_1.default.gray(`단축명: ${Object.keys(SHORTNAME_MAPPING).join(", ")}\n`));
2625
2760
  process.exit(1);
2626
2761
  }
2627
- // 패키지 설치 안내
2628
- if (packages.length === 1) {
2762
+ // 그룹 설치인 경우 안내
2763
+ if (isGroup) {
2764
+ console.log(chalk_1.default.cyan.bold(`\n📦 ${groupName?.toUpperCase()} 그룹 패키지 일괄 설치\n`));
2765
+ console.log(chalk_1.default.gray(" 포함된 패키지:"));
2766
+ for (const pkg of packages) {
2767
+ console.log(chalk_1.default.gray(` - ${pkg} (${EXTENSION_PACKAGES[pkg].name})`));
2768
+ }
2769
+ console.log();
2770
+ }
2771
+ else if (packages.length === 1) {
2629
2772
  // 단일 패키지
2630
2773
  const pkg = packages[0];
2631
2774
  console.log(chalk_1.default.cyan(`\n📦 ${EXTENSION_PACKAGES[pkg].name} 패키지 설치\n`));
@@ -2691,28 +2834,50 @@ program
2691
2834
  .action(() => {
2692
2835
  const cwd = process.cwd();
2693
2836
  const semoSystemDir = path.join(cwd, "semo-system");
2694
- console.log(chalk_1.default.cyan.bold("\n📦 SEMO 패키지 목록\n"));
2695
- // Standard (필수)
2837
+ console.log(chalk_1.default.cyan.bold("\n📦 SEMO 패키지 목록 (v3.0)\n"));
2838
+ // Standard
2696
2839
  console.log(chalk_1.default.white.bold("Standard (필수)"));
2697
2840
  const coreInstalled = fs.existsSync(path.join(semoSystemDir, "semo-core"));
2698
2841
  const skillsInstalled = fs.existsSync(path.join(semoSystemDir, "semo-skills"));
2699
2842
  console.log(` ${coreInstalled ? chalk_1.default.green("✓") : chalk_1.default.gray("○")} semo-core - 원칙, 오케스트레이터`);
2700
2843
  console.log(` ${skillsInstalled ? chalk_1.default.green("✓") : chalk_1.default.gray("○")} semo-skills - 통합 스킬`);
2701
2844
  console.log();
2702
- // Extensions
2703
- console.log(chalk_1.default.white.bold("Extensions (선택)"));
2704
- const extensionList = [
2705
- { key: "meta", name: "Meta", desc: "SEMO 프레임워크 자체 개발/관리" },
2706
- { key: "semo-hooks", name: "Hooks", desc: "Claude Code Hooks 기반 로깅 시스템" },
2707
- { key: "semo-remote", name: "Remote", desc: "Claude Code 원격 제어 (모바일 PWA)" },
2708
- ];
2709
- for (const ext of extensionList) {
2710
- const isInstalled = fs.existsSync(path.join(semoSystemDir, ext.key));
2711
- const status = isInstalled ? chalk_1.default.green("✓") : chalk_1.default.gray("○");
2712
- console.log(` ${status} ${chalk_1.default.cyan(ext.key)} - ${ext.desc}`);
2713
- console.log(chalk_1.default.gray(` semo add ${ext.key}`));
2845
+ // Extensions - 레이어별 그룹화
2846
+ const layers = {
2847
+ biz: { title: "Business Layer", emoji: "💼" },
2848
+ eng: { title: "Engineering Layer", emoji: "⚙️" },
2849
+ ops: { title: "Operations Layer", emoji: "📊" },
2850
+ meta: { title: "Meta", emoji: "🔧" },
2851
+ system: { title: "System", emoji: "🔩" },
2852
+ };
2853
+ for (const [layerKey, layerInfo] of Object.entries(layers)) {
2854
+ const layerPackages = Object.entries(EXTENSION_PACKAGES).filter(([, pkg]) => pkg.layer === layerKey);
2855
+ if (layerPackages.length === 0)
2856
+ continue;
2857
+ console.log(chalk_1.default.white.bold(`${layerInfo.emoji} ${layerInfo.title}`));
2858
+ for (const [key, pkg] of layerPackages) {
2859
+ const isInstalled = fs.existsSync(path.join(semoSystemDir, key));
2860
+ const status = isInstalled ? chalk_1.default.green("✓") : chalk_1.default.gray("○");
2861
+ const displayKey = key.includes("/") ? key.split("/")[1] : key;
2862
+ console.log(` ${status} ${chalk_1.default.cyan(displayKey)} - ${pkg.desc}`);
2863
+ console.log(chalk_1.default.gray(` semo add ${key}`));
2864
+ }
2865
+ console.log();
2714
2866
  }
2867
+ // 그룹 설치 안내
2868
+ console.log(chalk_1.default.gray("─".repeat(50)));
2869
+ console.log(chalk_1.default.white.bold("📦 그룹 일괄 설치"));
2870
+ console.log(chalk_1.default.gray(" semo add biz → Business 전체 (discovery, design, management, poc)"));
2871
+ console.log(chalk_1.default.gray(" semo add eng → Engineering 전체 (nextjs, spring, ms, infra)"));
2872
+ console.log(chalk_1.default.gray(" semo add ops → Operations 전체 (qa, monitor, improve)"));
2873
+ console.log(chalk_1.default.gray(" semo add system → System 전체 (hooks, remote)"));
2715
2874
  console.log();
2875
+ // 단축명 안내
2876
+ console.log(chalk_1.default.gray("─".repeat(50)));
2877
+ console.log(chalk_1.default.white.bold("⚡ 단축명 지원"));
2878
+ console.log(chalk_1.default.gray(" semo add discovery → biz/discovery"));
2879
+ console.log(chalk_1.default.gray(" semo add qa → ops/qa"));
2880
+ console.log(chalk_1.default.gray(" semo add nextjs → eng/nextjs\n"));
2716
2881
  });
2717
2882
  // === status 명령어 ===
2718
2883
  program
@@ -2997,71 +3162,9 @@ program
2997
3162
  }
2998
3163
  }
2999
3164
  }
3000
- // === 6. semo-hooks 체크 및 업데이트 ===
3001
- console.log(chalk_1.default.cyan("\n🪝 semo-hooks 상태 확인"));
3002
- const hooksDir = path.join(semoSystemDir, "semo-hooks");
3003
- if (!fs.existsSync(hooksDir)) {
3004
- console.log(chalk_1.default.gray(" ⏭️ semo-hooks 미설치 (선택 패키지)"));
3005
- console.log(chalk_1.default.gray(" 💡 설치: semo add hooks"));
3006
- }
3007
- else {
3008
- const hooksVersionPath = path.join(hooksDir, "VERSION");
3009
- const hooksVersion = fs.existsSync(hooksVersionPath)
3010
- ? fs.readFileSync(hooksVersionPath, "utf-8").trim()
3011
- : "?";
3012
- console.log(chalk_1.default.green(` ✓ semo-hooks v${hooksVersion} 설치됨`));
3013
- // hooks 빌드 및 설정 업데이트
3014
- await setupHooks(cwd, true);
3015
- }
3016
- // === 7. semo-mcp 체크 ===
3017
- console.log(chalk_1.default.cyan("\n📡 semo-mcp 상태 확인"));
3018
- const userHomeDir2 = process.env.HOME || process.env.USERPROFILE || "";
3019
- const claudeSettingsPath = path.join(userHomeDir2, ".claude", "settings.local.json");
3020
- if (!fs.existsSync(claudeSettingsPath)) {
3021
- console.log(chalk_1.default.gray(" ⏭️ MCP 설정 없음 (선택사항)"));
3022
- console.log(chalk_1.default.gray(" 💡 장기 기억이 필요하면 settings.local.json 설정 추가"));
3023
- }
3024
- else {
3025
- try {
3026
- const settings = JSON.parse(fs.readFileSync(claudeSettingsPath, "utf-8"));
3027
- const mcpServers = settings.mcpServers || {};
3028
- const semoMcp = mcpServers["semo-integrations"];
3029
- if (!semoMcp) {
3030
- console.log(chalk_1.default.gray(" ⏭️ semo-integrations 미등록 (선택사항)"));
3031
- console.log(chalk_1.default.gray(" 💡 장기 기억/원격 제어가 필요하면 MCP 설정 추가"));
3032
- }
3033
- else {
3034
- console.log(chalk_1.default.green(" ✓ semo-integrations MCP 서버 등록됨"));
3035
- // v3.0: SEMO_DB_PASSWORD만 체크
3036
- const env = semoMcp.env || {};
3037
- if (env["SEMO_DB_PASSWORD"]) {
3038
- console.log(chalk_1.default.green(" ✓ 장기 기억: 활성화"));
3039
- }
3040
- else {
3041
- console.log(chalk_1.default.gray(" ⏭️ 장기 기억: 비활성화 (SEMO_DB_PASSWORD 미설정)"));
3042
- }
3043
- }
3044
- }
3045
- catch {
3046
- console.log(chalk_1.default.yellow(" ⚠ settings.local.json 파싱 오류"));
3047
- }
3048
- }
3049
- // CLI 도구 체크 (v3.0: 스킬에서 CLI 직접 호출)
3050
- console.log(chalk_1.default.cyan("\n🔧 CLI 도구 확인"));
3051
- const cliToolsUpdate = [
3052
- { name: "gh", desc: "GitHub CLI" },
3053
- { name: "supabase", desc: "Supabase CLI" },
3054
- ];
3055
- for (const tool of cliToolsUpdate) {
3056
- try {
3057
- (0, child_process_1.execSync)(`${tool.name} --version`, { stdio: ["pipe", "pipe", "pipe"] });
3058
- console.log(chalk_1.default.green(` ✓ ${tool.name} 설치됨`));
3059
- }
3060
- catch {
3061
- console.log(chalk_1.default.yellow(` ⚠ ${tool.name} 미설치 (${tool.desc})`));
3062
- }
3063
- }
3064
- // === 8. 설치 검증 ===
3165
+ // === 6. Hooks 업데이트 ===
3166
+ await setupHooks(cwd, true);
3167
+ // === 7. 설치 검증 ===
3065
3168
  const verificationResult = verifyInstallation(cwd, installedExtensions);
3066
3169
  printVerificationResult(verificationResult);
3067
3170
  if (verificationResult.success) {
@@ -3191,174 +3294,8 @@ program
3191
3294
  else {
3192
3295
  console.log(chalk_1.default.red(" ❌ .claude/ 디렉토리 없음"));
3193
3296
  }
3194
- // 4. semo-hooks 상태 확인
3195
- console.log(chalk_1.default.cyan("\n4. semo-hooks (Claude Code Hooks)"));
3196
- const hooksDir = path.join(semoSystemDir, "semo-hooks");
3197
- const hooksDistDir = path.join(hooksDir, "dist");
3198
- const hooksIndexJs = path.join(hooksDistDir, "index.js");
3199
- if (!fs.existsSync(hooksDir)) {
3200
- console.log(chalk_1.default.gray(" ⏭️ semo-hooks 미설치 (선택 패키지)"));
3201
- console.log(chalk_1.default.gray(" 💡 설치: semo add hooks"));
3202
- }
3203
- else {
3204
- // hooks 버전 확인
3205
- const hooksVersionPath = path.join(hooksDir, "VERSION");
3206
- const hooksVersion = fs.existsSync(hooksVersionPath)
3207
- ? fs.readFileSync(hooksVersionPath, "utf-8").trim()
3208
- : "?";
3209
- console.log(chalk_1.default.green(` ✅ semo-hooks v${hooksVersion} 설치됨`));
3210
- // 빌드 상태 확인
3211
- if (!fs.existsSync(hooksDistDir) || !fs.existsSync(hooksIndexJs)) {
3212
- console.log(chalk_1.default.red(" ❌ 빌드되지 않음 (dist/index.js 없음)"));
3213
- console.log(chalk_1.default.gray(" 💡 해결: semo hooks enable"));
3214
- }
3215
- else {
3216
- console.log(chalk_1.default.green(" ✅ 빌드 완료 (dist/index.js 존재)"));
3217
- }
3218
- // settings.local.json hooks 설정 확인
3219
- const homeDir = process.env.HOME || process.env.USERPROFILE || "";
3220
- const settingsPath = path.join(homeDir, ".claude", "settings.local.json");
3221
- if (!fs.existsSync(settingsPath)) {
3222
- console.log(chalk_1.default.yellow(" ⚠️ settings.local.json 없음"));
3223
- console.log(chalk_1.default.gray(" 💡 해결: semo hooks enable"));
3224
- }
3225
- else {
3226
- try {
3227
- const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
3228
- const hooksConfig = settings.hooks;
3229
- if (!hooksConfig) {
3230
- console.log(chalk_1.default.yellow(" ⚠️ hooks 설정 없음"));
3231
- console.log(chalk_1.default.gray(" 💡 해결: semo hooks enable"));
3232
- }
3233
- else {
3234
- const requiredHooks = ["SessionStart", "UserPromptSubmit", "Stop", "SessionEnd"];
3235
- const missingHooks = [];
3236
- const invalidPathHooks = [];
3237
- for (const hookName of requiredHooks) {
3238
- const hookArray = hooksConfig[hookName];
3239
- if (!hookArray || !Array.isArray(hookArray) || hookArray.length === 0) {
3240
- missingHooks.push(hookName);
3241
- }
3242
- else {
3243
- // 경로 검증
3244
- const hookEntry = hookArray[0];
3245
- const innerHooks = hookEntry?.hooks;
3246
- if (innerHooks && Array.isArray(innerHooks) && innerHooks.length > 0) {
3247
- const command = innerHooks[0]?.command || "";
3248
- // 현재 프로젝트의 semo-hooks 경로와 비교
3249
- if (!command.includes(hooksDir) && !command.includes("semo-hooks")) {
3250
- invalidPathHooks.push(hookName);
3251
- }
3252
- }
3253
- }
3254
- }
3255
- if (missingHooks.length > 0) {
3256
- console.log(chalk_1.default.yellow(` ⚠️ 누락된 hooks: ${missingHooks.join(", ")}`));
3257
- console.log(chalk_1.default.gray(" 💡 해결: semo hooks enable"));
3258
- }
3259
- else if (invalidPathHooks.length > 0) {
3260
- console.log(chalk_1.default.yellow(` ⚠️ 경로 불일치: ${invalidPathHooks.join(", ")}`));
3261
- console.log(chalk_1.default.gray(" 💡 해결: semo hooks enable (다른 프로젝트 설정 감지)"));
3262
- }
3263
- else {
3264
- console.log(chalk_1.default.green(" ✅ hooks 설정 완료 (4개 hook 등록됨)"));
3265
- }
3266
- }
3267
- }
3268
- catch {
3269
- console.log(chalk_1.default.red(" ❌ settings.local.json 파싱 오류"));
3270
- }
3271
- }
3272
- }
3273
- // 5. semo-mcp (MCP 서버) 상태 확인
3274
- // v3.0: semo-mcp는 Memory + Remote만 제공 (선택사항)
3275
- // Slack/GitHub/Supabase는 스킬에서 CLI 직접 호출
3276
- console.log(chalk_1.default.cyan("\n5. semo-mcp (MCP 서버) - 선택사항"));
3277
- const userHomeDir = process.env.HOME || process.env.USERPROFILE || "";
3278
- const claudeSettingsPath = path.join(userHomeDir, ".claude", "settings.local.json");
3279
- // MCP 서버 설정 확인
3280
- if (!fs.existsSync(claudeSettingsPath)) {
3281
- console.log(chalk_1.default.gray(" ⏭️ settings.local.json 없음 (MCP 미사용)"));
3282
- console.log(chalk_1.default.gray(" 💡 장기 기억이 필요하면 semo-mcp 설정 추가"));
3283
- }
3284
- else {
3285
- try {
3286
- const settings = JSON.parse(fs.readFileSync(claudeSettingsPath, "utf-8"));
3287
- const mcpServers = settings.mcpServers || {};
3288
- // semo-integrations MCP 서버 확인
3289
- const semoMcp = mcpServers["semo-integrations"];
3290
- if (!semoMcp) {
3291
- console.log(chalk_1.default.gray(" ⏭️ semo-integrations 미등록 (선택사항)"));
3292
- console.log(chalk_1.default.gray(" 💡 장기 기억/원격 제어가 필요하면 MCP 설정 추가"));
3293
- }
3294
- else {
3295
- console.log(chalk_1.default.green(" ✅ semo-integrations MCP 서버 등록됨"));
3296
- // 명령어 경로 확인
3297
- const mcpCommand = semoMcp.command || "";
3298
- const mcpArgs = semoMcp.args || [];
3299
- if (mcpCommand === "npx") {
3300
- console.log(chalk_1.default.green(" ✅ npx 방식 실행 (자동 업데이트)"));
3301
- }
3302
- else if (mcpCommand === "node") {
3303
- const scriptPath = mcpArgs[0] || "";
3304
- if (scriptPath && fs.existsSync(scriptPath)) {
3305
- console.log(chalk_1.default.green(` ✅ 로컬 스크립트: ${scriptPath}`));
3306
- }
3307
- else if (scriptPath) {
3308
- console.log(chalk_1.default.red(` ❌ 스크립트 경로 없음: ${scriptPath}`));
3309
- }
3310
- }
3311
- // v3.0: 환경변수 체크 - SEMO_DB_PASSWORD만 확인 (장기 기억용)
3312
- const env = semoMcp.env || {};
3313
- if (env["SEMO_DB_PASSWORD"]) {
3314
- console.log(chalk_1.default.green(" ✅ 장기 기억: 활성화 (SEMO_DB_PASSWORD 설정됨)"));
3315
- }
3316
- else {
3317
- console.log(chalk_1.default.gray(" ⏭️ 장기 기억: 비활성화"));
3318
- console.log(chalk_1.default.gray(" 💡 SEMO_DB_PASSWORD 설정 시 활성화"));
3319
- }
3320
- // v3.0: 도구 목록 업데이트 (Memory + Remote만)
3321
- const v3Tools = [
3322
- // Memory
3323
- "semo_remember", "semo_recall", "semo_save_fact",
3324
- "semo_get_facts", "semo_get_history", "semo_memory_status",
3325
- "semo_process_embeddings", "semo_recall_smart",
3326
- // Remote
3327
- "semo_remote_request", "semo_remote_respond", "semo_remote_pending",
3328
- ];
3329
- console.log(chalk_1.default.gray(` 📦 제공 도구: ${v3Tools.length}개 (Memory ${8}, Remote ${3})`));
3330
- }
3331
- // 다른 MCP 서버 확인
3332
- const otherServers = Object.keys(mcpServers).filter(k => k !== "semo-integrations");
3333
- if (otherServers.length > 0) {
3334
- console.log(chalk_1.default.gray(` 📡 기타 MCP 서버: ${otherServers.join(", ")}`));
3335
- }
3336
- }
3337
- catch {
3338
- console.log(chalk_1.default.red(" ❌ settings.local.json 파싱 오류"));
3339
- }
3340
- }
3341
- // 5-1. CLI 도구 확인 (v3.0: 스킬에서 CLI 직접 호출)
3342
- console.log(chalk_1.default.cyan("\n5-1. CLI 도구 (Skill용)"));
3343
- const cliTools = [
3344
- { name: "gh", desc: "GitHub CLI", check: "gh --version" },
3345
- { name: "supabase", desc: "Supabase CLI", check: "supabase --version" },
3346
- ];
3347
- for (const tool of cliTools) {
3348
- try {
3349
- const { execSync } = require("child_process");
3350
- const version = execSync(tool.check, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim().split("\n")[0];
3351
- console.log(chalk_1.default.green(` ✅ ${tool.name}: ${version}`));
3352
- }
3353
- catch {
3354
- console.log(chalk_1.default.yellow(` ⚠️ ${tool.name} 미설치 (${tool.desc})`));
3355
- console.log(chalk_1.default.gray(` 💡 일부 스킬 기능이 제한될 수 있습니다`));
3356
- }
3357
- }
3358
- // Slack은 curl로 호출하므로 별도 체크 불필요
3359
- console.log(chalk_1.default.gray(" ℹ️ Slack: curl 사용 (별도 CLI 불필요)"));
3360
- // 6. 전체 설치 검증
3361
- console.log(chalk_1.default.cyan("\n6. 전체 설치 검증"));
3297
+ // 4. 설치 검증
3298
+ console.log(chalk_1.default.cyan("\n4. 전체 설치 검증"));
3362
3299
  const verificationResult = verifyInstallation(cwd, []);
3363
3300
  if (verificationResult.success) {
3364
3301
  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.8.1",
3
+ "version": "3.10.0",
4
4
  "description": "SEMO CLI - AI Agent Orchestration Framework Installer",
5
5
  "main": "dist/index.js",
6
6
  "bin": {