@team-semicolon/semo-cli 3.1.0 → 3.2.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 +362 -68
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -380,12 +380,27 @@ function createSymlinkOrJunction(targetPath, linkPath) {
380
380
  if (isWindows) {
381
381
  // Windows: Junction 사용 (절대 경로 필요)
382
382
  const absoluteTarget = path.resolve(targetPath);
383
- try {
384
- (0, child_process_1.execSync)(`cmd /c "mklink /J "${linkPath}" "${absoluteTarget}""`, { stdio: "pipe" });
383
+ // 재시도 메커니즘 (최대 3회)
384
+ let success = false;
385
+ let lastError = null;
386
+ for (let attempt = 1; attempt <= 3 && !success; attempt++) {
387
+ try {
388
+ (0, child_process_1.execSync)(`cmd /c "mklink /J "${linkPath}" "${absoluteTarget}""`, { stdio: "pipe" });
389
+ success = true;
390
+ }
391
+ catch (err) {
392
+ lastError = err;
393
+ if (attempt < 3) {
394
+ // 잠시 대기 후 재시도
395
+ (0, child_process_1.execSync)("timeout /t 1 /nobreak >nul 2>&1", { stdio: "pipe" });
396
+ }
397
+ }
385
398
  }
386
- catch {
387
- // fallback: 디렉토리 복사
388
- console.log(chalk_1.default.yellow(` ⚠ Junction 생성 실패, 복사로 대체: ${path.basename(linkPath)}`));
399
+ if (!success) {
400
+ // fallback: 디렉토리 복사 (경고 표시)
401
+ console.log(chalk_1.default.yellow(` ⚠ Junction 생성 실패 (3회 시도), 복사로 대체: ${path.basename(linkPath)}`));
402
+ console.log(chalk_1.default.gray(` 원인: ${lastError}`));
403
+ console.log(chalk_1.default.gray(` 💡 관리자 권한으로 실행하거나 개발자 모드를 활성화하세요.`));
389
404
  (0, child_process_1.execSync)(`xcopy /E /I /Q "${absoluteTarget}" "${linkPath}"`, { stdio: "pipe" });
390
405
  }
391
406
  }
@@ -395,6 +410,137 @@ function createSymlinkOrJunction(targetPath, linkPath) {
395
410
  fs.symlinkSync(relativeTarget, linkPath);
396
411
  }
397
412
  }
413
+ /**
414
+ * 심볼릭 링크가 유효한지 확인 (타겟 존재 여부)
415
+ */
416
+ function isSymlinkValid(linkPath) {
417
+ try {
418
+ const stats = fs.lstatSync(linkPath);
419
+ if (!stats.isSymbolicLink())
420
+ return true; // 일반 파일/디렉토리
421
+ // 심볼릭 링크인 경우 타겟 존재 확인
422
+ const target = fs.readlinkSync(linkPath);
423
+ const absoluteTarget = path.isAbsolute(target)
424
+ ? target
425
+ : path.resolve(path.dirname(linkPath), target);
426
+ return fs.existsSync(absoluteTarget);
427
+ }
428
+ catch {
429
+ return false;
430
+ }
431
+ }
432
+ /**
433
+ * 레거시 SEMO 환경을 감지합니다.
434
+ * 레거시: 프로젝트 루트에 semo-core/, semo-skills/ 가 직접 있는 경우
435
+ * 신규: semo-system/ 하위에 있는 경우
436
+ */
437
+ function detectLegacyEnvironment(cwd) {
438
+ const legacyPaths = [];
439
+ // 루트에 직접 있는 레거시 디렉토리 확인
440
+ const legacyDirs = ["semo-core", "semo-skills", "sax-core", "sax-skills"];
441
+ for (const dir of legacyDirs) {
442
+ const dirPath = path.join(cwd, dir);
443
+ if (fs.existsSync(dirPath) && !fs.lstatSync(dirPath).isSymbolicLink()) {
444
+ legacyPaths.push(dir);
445
+ }
446
+ }
447
+ // .claude/ 내부의 레거시 구조 확인
448
+ const claudeDir = path.join(cwd, ".claude");
449
+ if (fs.existsSync(claudeDir)) {
450
+ // 심볼릭 링크가 레거시 경로를 가리키는지 확인
451
+ const checkLegacyLink = (linkName) => {
452
+ const linkPath = path.join(claudeDir, linkName);
453
+ if (fs.existsSync(linkPath) && fs.lstatSync(linkPath).isSymbolicLink()) {
454
+ try {
455
+ const target = fs.readlinkSync(linkPath);
456
+ // 레거시 경로 패턴: ../semo-core, ../sax-core 등
457
+ if (target.match(/^\.\.\/(semo|sax)-(core|skills)/)) {
458
+ legacyPaths.push(`.claude/${linkName} → ${target}`);
459
+ }
460
+ }
461
+ catch {
462
+ // 읽기 실패 무시
463
+ }
464
+ }
465
+ };
466
+ checkLegacyLink("agents");
467
+ checkLegacyLink("skills");
468
+ checkLegacyLink("commands");
469
+ }
470
+ return {
471
+ hasLegacy: legacyPaths.length > 0,
472
+ legacyPaths,
473
+ hasSemoSystem: fs.existsSync(path.join(cwd, "semo-system")),
474
+ };
475
+ }
476
+ /**
477
+ * 레거시 환경을 새 환경으로 마이그레이션합니다.
478
+ */
479
+ async function migrateLegacyEnvironment(cwd) {
480
+ const detection = detectLegacyEnvironment(cwd);
481
+ if (!detection.hasLegacy) {
482
+ return true; // 마이그레이션 불필요
483
+ }
484
+ console.log(chalk_1.default.yellow("\n⚠️ 레거시 SEMO 환경이 감지되었습니다.\n"));
485
+ console.log(chalk_1.default.gray(" 감지된 레거시 경로:"));
486
+ detection.legacyPaths.forEach(p => {
487
+ console.log(chalk_1.default.gray(` - ${p}`));
488
+ });
489
+ console.log();
490
+ // 사용자 확인
491
+ const { shouldMigrate } = await inquirer_1.default.prompt([
492
+ {
493
+ type: "confirm",
494
+ name: "shouldMigrate",
495
+ message: "레거시 환경을 새 구조(semo-system/)로 마이그레이션하시겠습니까?",
496
+ default: true,
497
+ },
498
+ ]);
499
+ if (!shouldMigrate) {
500
+ console.log(chalk_1.default.yellow("\n마이그레이션이 취소되었습니다."));
501
+ console.log(chalk_1.default.gray("💡 수동 마이그레이션 방법:"));
502
+ console.log(chalk_1.default.gray(" 1. 기존 semo-core/, semo-skills/ 폴더 삭제"));
503
+ console.log(chalk_1.default.gray(" 2. .claude/ 폴더 삭제"));
504
+ console.log(chalk_1.default.gray(" 3. semo init 다시 실행\n"));
505
+ return false;
506
+ }
507
+ const spinner = (0, ora_1.default)("레거시 환경 마이그레이션 중...").start();
508
+ try {
509
+ // 1. 루트의 레거시 디렉토리 삭제
510
+ const legacyDirs = ["semo-core", "semo-skills", "sax-core", "sax-skills"];
511
+ for (const dir of legacyDirs) {
512
+ const dirPath = path.join(cwd, dir);
513
+ if (fs.existsSync(dirPath) && !fs.lstatSync(dirPath).isSymbolicLink()) {
514
+ removeRecursive(dirPath);
515
+ console.log(chalk_1.default.gray(` ✓ ${dir}/ 삭제됨`));
516
+ }
517
+ }
518
+ // 2. .claude/ 내부의 레거시 심볼릭 링크 삭제
519
+ const claudeDir = path.join(cwd, ".claude");
520
+ if (fs.existsSync(claudeDir)) {
521
+ const linksToCheck = ["agents", "skills", "commands"];
522
+ for (const linkName of linksToCheck) {
523
+ const linkPath = path.join(claudeDir, linkName);
524
+ if (fs.existsSync(linkPath)) {
525
+ removeRecursive(linkPath);
526
+ }
527
+ }
528
+ }
529
+ // 3. 기존 semo-system이 있으면 삭제 (새로 설치)
530
+ const semoSystemDir = path.join(cwd, "semo-system");
531
+ if (fs.existsSync(semoSystemDir)) {
532
+ removeRecursive(semoSystemDir);
533
+ }
534
+ spinner.succeed("레거시 환경 정리 완료");
535
+ console.log(chalk_1.default.green(" → 새 환경으로 설치를 진행합니다.\n"));
536
+ return true;
537
+ }
538
+ catch (error) {
539
+ spinner.fail("마이그레이션 실패");
540
+ console.error(chalk_1.default.red(` ${error}`));
541
+ return false;
542
+ }
543
+ }
398
544
  /**
399
545
  * 플랫폼에 맞는 rm -rf 실행
400
546
  */
@@ -887,19 +1033,28 @@ program
887
1033
  .option("--skip-mcp", "MCP 설정 생략")
888
1034
  .option("--no-gitignore", ".gitignore 수정 생략")
889
1035
  .option("--with <packages>", "추가 설치할 패키지 (쉼표 구분: next,backend)")
1036
+ .option("--migrate", "레거시 환경 강제 마이그레이션")
890
1037
  .action(async (options) => {
891
1038
  console.log(chalk_1.default.cyan.bold("\n🚀 SEMO 설치 시작\n"));
892
1039
  console.log(chalk_1.default.gray("Gemini 하이브리드 전략: White Box + Black Box\n"));
893
1040
  const cwd = process.cwd();
894
1041
  // 0. 버전 비교
895
1042
  await showVersionComparison(cwd);
1043
+ // 0.5. 레거시 환경 감지 및 마이그레이션
1044
+ const legacyCheck = detectLegacyEnvironment(cwd);
1045
+ if (legacyCheck.hasLegacy || options.migrate) {
1046
+ const migrationSuccess = await migrateLegacyEnvironment(cwd);
1047
+ if (!migrationSuccess) {
1048
+ process.exit(0);
1049
+ }
1050
+ }
896
1051
  // 1. 필수 도구 확인
897
1052
  const shouldContinue = await showToolsStatus();
898
1053
  if (!shouldContinue) {
899
1054
  console.log(chalk_1.default.yellow("\n설치가 취소되었습니다. 필수 도구 설치 후 다시 시도하세요.\n"));
900
1055
  process.exit(0);
901
1056
  }
902
- // 1. Git 레포지토리 확인
1057
+ // 1.5. Git 레포지토리 확인
903
1058
  const spinner = (0, ora_1.default)("Git 레포지토리 확인 중...").start();
904
1059
  try {
905
1060
  (0, child_process_1.execSync)("git rev-parse --git-dir", { cwd, stdio: "pipe" });
@@ -1003,6 +1158,8 @@ program
1003
1158
  console.log(chalk_1.default.gray(" [Standard]"));
1004
1159
  console.log(chalk_1.default.gray(" ✓ semo-core (원칙, 오케스트레이터)"));
1005
1160
  console.log(chalk_1.default.gray(" ✓ semo-skills (13개 통합 스킬)"));
1161
+ console.log(chalk_1.default.gray(" ✓ semo-agents (14개 페르소나 Agent)"));
1162
+ console.log(chalk_1.default.gray(" ✓ semo-scripts (자동화 스크립트)"));
1006
1163
  if (extensionsToInstall.length > 0) {
1007
1164
  console.log(chalk_1.default.gray(" [Extensions]"));
1008
1165
  extensionsToInstall.forEach(pkg => {
@@ -1023,7 +1180,9 @@ async function setupStandard(cwd, force) {
1023
1180
  const semoSystemDir = path.join(cwd, "semo-system");
1024
1181
  console.log(chalk_1.default.cyan("\n📚 Standard 설치 (White Box)"));
1025
1182
  console.log(chalk_1.default.gray(" semo-core: 원칙, 오케스트레이터"));
1026
- console.log(chalk_1.default.gray(" semo-skills: 13개 통합 스킬\n"));
1183
+ console.log(chalk_1.default.gray(" semo-skills: 13개 통합 스킬"));
1184
+ console.log(chalk_1.default.gray(" semo-agents: 14개 페르소나 Agent"));
1185
+ console.log(chalk_1.default.gray(" semo-scripts: 자동화 스크립트\n"));
1027
1186
  // 기존 디렉토리 확인
1028
1187
  if (fs.existsSync(semoSystemDir) && !force) {
1029
1188
  const shouldOverwrite = await confirmOverwrite("semo-system/", semoSystemDir);
@@ -1034,19 +1193,20 @@ async function setupStandard(cwd, force) {
1034
1193
  removeRecursive(semoSystemDir);
1035
1194
  console.log(chalk_1.default.green(" ✓ 기존 semo-system/ 삭제됨"));
1036
1195
  }
1037
- const spinner = (0, ora_1.default)("semo-core, semo-skills 다운로드 중...").start();
1196
+ const spinner = (0, ora_1.default)("semo-core, semo-skills, semo-agents, semo-scripts 다운로드 중...").start();
1038
1197
  try {
1039
1198
  const tempDir = path.join(cwd, ".semo-temp");
1040
1199
  removeRecursive(tempDir);
1041
1200
  (0, child_process_1.execSync)(`git clone --depth 1 ${SEMO_REPO} "${tempDir}"`, { stdio: "pipe" });
1042
1201
  fs.mkdirSync(semoSystemDir, { recursive: true });
1043
- // semo-core 복사
1044
- if (fs.existsSync(path.join(tempDir, "semo-core"))) {
1045
- copyRecursive(path.join(tempDir, "semo-core"), path.join(semoSystemDir, "semo-core"));
1046
- }
1047
- // semo-skills 복사
1048
- if (fs.existsSync(path.join(tempDir, "semo-skills"))) {
1049
- copyRecursive(path.join(tempDir, "semo-skills"), path.join(semoSystemDir, "semo-skills"));
1202
+ // Standard 패키지 목록 (semo-system/ 하위에 있는 것들)
1203
+ const standardPackages = ["semo-core", "semo-skills", "semo-agents", "semo-scripts"];
1204
+ for (const pkg of standardPackages) {
1205
+ const srcPath = path.join(tempDir, "semo-system", pkg);
1206
+ const destPath = path.join(semoSystemDir, pkg);
1207
+ if (fs.existsSync(srcPath)) {
1208
+ copyRecursive(srcPath, destPath);
1209
+ }
1050
1210
  }
1051
1211
  removeRecursive(tempDir);
1052
1212
  spinner.succeed("Standard 설치 완료");
@@ -1152,7 +1312,7 @@ function verifyInstallation(cwd, installedExtensions = []) {
1152
1312
  result.errors.push("semo-skills가 설치되지 않았습니다");
1153
1313
  result.success = false;
1154
1314
  }
1155
- // 2. agents 링크 검증
1315
+ // 2. agents 링크 검증 (isSymlinkValid 사용)
1156
1316
  const claudeAgentsDir = path.join(claudeDir, "agents");
1157
1317
  const coreAgentsDir = path.join(coreDir, "agents");
1158
1318
  if (fs.existsSync(coreAgentsDir)) {
@@ -1161,31 +1321,19 @@ function verifyInstallation(cwd, installedExtensions = []) {
1161
1321
  if (fs.existsSync(claudeAgentsDir)) {
1162
1322
  for (const agent of expectedAgents) {
1163
1323
  const linkPath = path.join(claudeAgentsDir, agent);
1164
- if (fs.existsSync(linkPath)) {
1165
- if (fs.lstatSync(linkPath).isSymbolicLink()) {
1166
- try {
1167
- fs.readlinkSync(linkPath);
1168
- const targetExists = fs.existsSync(linkPath);
1169
- if (targetExists) {
1170
- result.stats.agents.linked++;
1171
- }
1172
- else {
1173
- result.stats.agents.broken++;
1174
- result.warnings.push(`깨진 링크: .claude/agents/${agent}`);
1175
- }
1176
- }
1177
- catch {
1178
- result.stats.agents.broken++;
1179
- }
1324
+ if (fs.existsSync(linkPath) || fs.lstatSync(linkPath).isSymbolicLink()) {
1325
+ if (isSymlinkValid(linkPath)) {
1326
+ result.stats.agents.linked++;
1180
1327
  }
1181
1328
  else {
1182
- result.stats.agents.linked++; // 디렉토리로 복사된 경우
1329
+ result.stats.agents.broken++;
1330
+ result.warnings.push(`깨진 링크: .claude/agents/${agent}`);
1183
1331
  }
1184
1332
  }
1185
1333
  }
1186
1334
  }
1187
1335
  }
1188
- // 3. skills 링크 검증
1336
+ // 3. skills 링크 검증 (isSymlinkValid 사용)
1189
1337
  if (fs.existsSync(skillsDir)) {
1190
1338
  const expectedSkills = fs.readdirSync(skillsDir).filter(f => fs.statSync(path.join(skillsDir, f)).isDirectory());
1191
1339
  result.stats.skills.expected = expectedSkills.length;
@@ -1193,43 +1341,38 @@ function verifyInstallation(cwd, installedExtensions = []) {
1193
1341
  if (fs.existsSync(claudeSkillsDir)) {
1194
1342
  for (const skill of expectedSkills) {
1195
1343
  const linkPath = path.join(claudeSkillsDir, skill);
1196
- if (fs.existsSync(linkPath)) {
1197
- if (fs.lstatSync(linkPath).isSymbolicLink()) {
1198
- try {
1199
- fs.readlinkSync(linkPath);
1200
- const targetExists = fs.existsSync(linkPath);
1201
- if (targetExists) {
1202
- result.stats.skills.linked++;
1203
- }
1204
- else {
1205
- result.stats.skills.broken++;
1206
- result.warnings.push(`깨진 링크: .claude/skills/${skill}`);
1207
- }
1344
+ try {
1345
+ if (fs.existsSync(linkPath) || fs.lstatSync(linkPath).isSymbolicLink()) {
1346
+ if (isSymlinkValid(linkPath)) {
1347
+ result.stats.skills.linked++;
1208
1348
  }
1209
- catch {
1349
+ else {
1210
1350
  result.stats.skills.broken++;
1351
+ result.warnings.push(`깨진 링크: .claude/skills/${skill}`);
1211
1352
  }
1212
1353
  }
1213
- else {
1214
- result.stats.skills.linked++;
1215
- }
1354
+ }
1355
+ catch {
1356
+ // 링크가 존재하지 않음
1216
1357
  }
1217
1358
  }
1218
1359
  }
1219
1360
  }
1220
- // 4. commands 검증
1361
+ // 4. commands 검증 (isSymlinkValid 사용)
1221
1362
  const semoCommandsLink = path.join(claudeDir, "commands", "SEMO");
1222
- result.stats.commands.exists = fs.existsSync(semoCommandsLink);
1223
- if (result.stats.commands.exists) {
1224
- if (fs.lstatSync(semoCommandsLink).isSymbolicLink()) {
1225
- result.stats.commands.valid = fs.existsSync(semoCommandsLink);
1363
+ try {
1364
+ const linkExists = fs.existsSync(semoCommandsLink) || fs.lstatSync(semoCommandsLink).isSymbolicLink();
1365
+ result.stats.commands.exists = linkExists;
1366
+ if (linkExists) {
1367
+ result.stats.commands.valid = isSymlinkValid(semoCommandsLink);
1226
1368
  if (!result.stats.commands.valid) {
1227
1369
  result.warnings.push("깨진 링크: .claude/commands/SEMO");
1228
1370
  }
1229
1371
  }
1230
- else {
1231
- result.stats.commands.valid = true;
1232
- }
1372
+ }
1373
+ catch {
1374
+ result.stats.commands.exists = false;
1375
+ result.stats.commands.valid = false;
1233
1376
  }
1234
1377
  // 5. Extensions 검증
1235
1378
  for (const ext of installedExtensions) {
@@ -2721,6 +2864,8 @@ program
2721
2864
  // 업데이트 대상 결정
2722
2865
  const updateSemoCore = !isSelectiveUpdate || onlyPackages.includes("semo-core");
2723
2866
  const updateSemoSkills = !isSelectiveUpdate || onlyPackages.includes("semo-skills");
2867
+ const updateSemoAgents = !isSelectiveUpdate || onlyPackages.includes("semo-agents");
2868
+ const updateSemoScripts = !isSelectiveUpdate || onlyPackages.includes("semo-scripts");
2724
2869
  const extensionsToUpdate = isSelectiveUpdate
2725
2870
  ? installedExtensions.filter(ext => onlyPackages.includes(ext))
2726
2871
  : installedExtensions;
@@ -2730,12 +2875,16 @@ program
2730
2875
  console.log(chalk_1.default.gray(" - semo-core"));
2731
2876
  if (updateSemoSkills)
2732
2877
  console.log(chalk_1.default.gray(" - semo-skills"));
2878
+ if (updateSemoAgents)
2879
+ console.log(chalk_1.default.gray(" - semo-agents"));
2880
+ if (updateSemoScripts)
2881
+ console.log(chalk_1.default.gray(" - semo-scripts"));
2733
2882
  extensionsToUpdate.forEach(pkg => {
2734
2883
  console.log(chalk_1.default.gray(` - ${pkg}`));
2735
2884
  });
2736
- if (!updateSemoCore && !updateSemoSkills && extensionsToUpdate.length === 0) {
2885
+ if (!updateSemoCore && !updateSemoSkills && !updateSemoAgents && !updateSemoScripts && extensionsToUpdate.length === 0) {
2737
2886
  console.log(chalk_1.default.yellow("\n ⚠️ 업데이트할 패키지가 없습니다."));
2738
- console.log(chalk_1.default.gray(" 설치된 패키지: semo-core, semo-skills" +
2887
+ console.log(chalk_1.default.gray(" 설치된 패키지: semo-core, semo-skills, semo-agents, semo-scripts" +
2739
2888
  (installedExtensions.length > 0 ? ", " + installedExtensions.join(", ") : "")));
2740
2889
  return;
2741
2890
  }
@@ -2744,14 +2893,22 @@ program
2744
2893
  const tempDir = path.join(cwd, ".semo-temp");
2745
2894
  removeRecursive(tempDir);
2746
2895
  (0, child_process_1.execSync)(`git clone --depth 1 ${SEMO_REPO} "${tempDir}"`, { stdio: "pipe" });
2747
- // Standard 업데이트 (선택적)
2748
- if (updateSemoCore) {
2749
- removeRecursive(path.join(semoSystemDir, "semo-core"));
2750
- copyRecursive(path.join(tempDir, "semo-core"), path.join(semoSystemDir, "semo-core"));
2751
- }
2752
- if (updateSemoSkills) {
2753
- removeRecursive(path.join(semoSystemDir, "semo-skills"));
2754
- copyRecursive(path.join(tempDir, "semo-skills"), path.join(semoSystemDir, "semo-skills"));
2896
+ // Standard 업데이트 (선택적) - semo-system/ 하위에서 복사
2897
+ const standardUpdates = [
2898
+ { flag: updateSemoCore, name: "semo-core" },
2899
+ { flag: updateSemoSkills, name: "semo-skills" },
2900
+ { flag: updateSemoAgents, name: "semo-agents" },
2901
+ { flag: updateSemoScripts, name: "semo-scripts" },
2902
+ ];
2903
+ for (const { flag, name } of standardUpdates) {
2904
+ if (flag) {
2905
+ const srcPath = path.join(tempDir, "semo-system", name);
2906
+ const destPath = path.join(semoSystemDir, name);
2907
+ if (fs.existsSync(srcPath)) {
2908
+ removeRecursive(destPath);
2909
+ copyRecursive(srcPath, destPath);
2910
+ }
2911
+ }
2755
2912
  }
2756
2913
  // Extensions 업데이트 (선택적)
2757
2914
  for (const pkg of extensionsToUpdate) {
@@ -2873,6 +3030,143 @@ program
2873
3030
  console.log(chalk_1.default.yellow.bold("\n⚠️ SEMO 업데이트 완료 (일부 문제 발견)\n"));
2874
3031
  }
2875
3032
  });
3033
+ // === migrate 명령어 ===
3034
+ program
3035
+ .command("migrate")
3036
+ .description("레거시 SEMO 환경을 새 구조(semo-system/)로 마이그레이션")
3037
+ .option("-f, --force", "확인 없이 강제 마이그레이션")
3038
+ .action(async (options) => {
3039
+ console.log(chalk_1.default.cyan.bold("\n🔄 SEMO 마이그레이션\n"));
3040
+ const cwd = process.cwd();
3041
+ const detection = detectLegacyEnvironment(cwd);
3042
+ if (!detection.hasLegacy) {
3043
+ console.log(chalk_1.default.green("✅ 레거시 환경이 감지되지 않았습니다."));
3044
+ if (detection.hasSemoSystem) {
3045
+ console.log(chalk_1.default.gray(" 현재 환경: semo-system/ (정상)"));
3046
+ }
3047
+ else {
3048
+ console.log(chalk_1.default.gray(" SEMO가 설치되지 않았습니다. 'semo init'을 실행하세요."));
3049
+ }
3050
+ console.log();
3051
+ return;
3052
+ }
3053
+ console.log(chalk_1.default.yellow("⚠️ 레거시 SEMO 환경이 감지되었습니다.\n"));
3054
+ console.log(chalk_1.default.gray(" 감지된 레거시 경로:"));
3055
+ detection.legacyPaths.forEach(p => {
3056
+ console.log(chalk_1.default.gray(` - ${p}`));
3057
+ });
3058
+ console.log();
3059
+ if (!options.force) {
3060
+ const { confirm } = await inquirer_1.default.prompt([
3061
+ {
3062
+ type: "confirm",
3063
+ name: "confirm",
3064
+ message: "레거시 환경을 삭제하고 새 구조로 마이그레이션하시겠습니까?",
3065
+ default: true,
3066
+ },
3067
+ ]);
3068
+ if (!confirm) {
3069
+ console.log(chalk_1.default.yellow("\n마이그레이션이 취소되었습니다.\n"));
3070
+ return;
3071
+ }
3072
+ }
3073
+ const migrationSuccess = await migrateLegacyEnvironment(cwd);
3074
+ if (migrationSuccess) {
3075
+ console.log(chalk_1.default.cyan("\n새 환경 설치를 위해 'semo init'을 실행하세요.\n"));
3076
+ }
3077
+ });
3078
+ // === doctor 명령어 (설치 상태 진단) ===
3079
+ program
3080
+ .command("doctor")
3081
+ .description("SEMO 설치 상태를 진단하고 문제를 리포트")
3082
+ .action(async () => {
3083
+ console.log(chalk_1.default.cyan.bold("\n🩺 SEMO 진단\n"));
3084
+ const cwd = process.cwd();
3085
+ const semoSystemDir = path.join(cwd, "semo-system");
3086
+ const claudeDir = path.join(cwd, ".claude");
3087
+ // 1. 레거시 환경 확인
3088
+ console.log(chalk_1.default.cyan("1. 레거시 환경 확인"));
3089
+ const legacyCheck = detectLegacyEnvironment(cwd);
3090
+ if (legacyCheck.hasLegacy) {
3091
+ console.log(chalk_1.default.yellow(" ⚠️ 레거시 환경 감지됨"));
3092
+ legacyCheck.legacyPaths.forEach(p => {
3093
+ console.log(chalk_1.default.gray(` - ${p}`));
3094
+ });
3095
+ console.log(chalk_1.default.gray(" 💡 해결: semo migrate 실행"));
3096
+ }
3097
+ else {
3098
+ console.log(chalk_1.default.green(" ✅ 레거시 환경 없음"));
3099
+ }
3100
+ // 2. semo-system 확인
3101
+ console.log(chalk_1.default.cyan("\n2. semo-system 구조 확인"));
3102
+ if (!fs.existsSync(semoSystemDir)) {
3103
+ console.log(chalk_1.default.red(" ❌ semo-system/ 없음"));
3104
+ console.log(chalk_1.default.gray(" 💡 해결: semo init 실행"));
3105
+ }
3106
+ else {
3107
+ const packages = ["semo-core", "semo-skills", "semo-agents", "semo-scripts"];
3108
+ for (const pkg of packages) {
3109
+ const pkgPath = path.join(semoSystemDir, pkg);
3110
+ if (fs.existsSync(pkgPath)) {
3111
+ const versionPath = path.join(pkgPath, "VERSION");
3112
+ const version = fs.existsSync(versionPath)
3113
+ ? fs.readFileSync(versionPath, "utf-8").trim()
3114
+ : "?";
3115
+ console.log(chalk_1.default.green(` ✅ ${pkg} v${version}`));
3116
+ }
3117
+ else {
3118
+ console.log(chalk_1.default.yellow(` ⚠️ ${pkg} 없음`));
3119
+ }
3120
+ }
3121
+ }
3122
+ // 3. 심볼릭 링크 확인
3123
+ console.log(chalk_1.default.cyan("\n3. 심볼릭 링크 상태"));
3124
+ if (fs.existsSync(claudeDir)) {
3125
+ const linksToCheck = [
3126
+ { name: "agents", dir: path.join(claudeDir, "agents") },
3127
+ { name: "skills", dir: path.join(claudeDir, "skills") },
3128
+ { name: "commands/SEMO", dir: path.join(claudeDir, "commands", "SEMO") },
3129
+ ];
3130
+ for (const { name, dir } of linksToCheck) {
3131
+ if (fs.existsSync(dir)) {
3132
+ if (fs.lstatSync(dir).isSymbolicLink()) {
3133
+ if (isSymlinkValid(dir)) {
3134
+ console.log(chalk_1.default.green(` ✅ .claude/${name} (심볼릭 링크)`));
3135
+ }
3136
+ else {
3137
+ console.log(chalk_1.default.red(` ❌ .claude/${name} (깨진 링크)`));
3138
+ console.log(chalk_1.default.gray(" 💡 해결: semo update 실행"));
3139
+ }
3140
+ }
3141
+ else {
3142
+ console.log(chalk_1.default.green(` ✅ .claude/${name} (복사본)`));
3143
+ }
3144
+ }
3145
+ else {
3146
+ console.log(chalk_1.default.yellow(` ⚠️ .claude/${name} 없음`));
3147
+ }
3148
+ }
3149
+ }
3150
+ else {
3151
+ console.log(chalk_1.default.red(" ❌ .claude/ 디렉토리 없음"));
3152
+ }
3153
+ // 4. 설치 검증
3154
+ console.log(chalk_1.default.cyan("\n4. 전체 설치 검증"));
3155
+ const verificationResult = verifyInstallation(cwd, []);
3156
+ if (verificationResult.success) {
3157
+ console.log(chalk_1.default.green(" ✅ 설치 상태 정상"));
3158
+ }
3159
+ else {
3160
+ console.log(chalk_1.default.yellow(" ⚠️ 문제 발견"));
3161
+ verificationResult.errors.forEach(err => {
3162
+ console.log(chalk_1.default.red(` ❌ ${err}`));
3163
+ });
3164
+ verificationResult.warnings.forEach(warn => {
3165
+ console.log(chalk_1.default.yellow(` ⚠️ ${warn}`));
3166
+ });
3167
+ }
3168
+ console.log();
3169
+ });
2876
3170
  // === -v 옵션 처리 (program.parse 전에 직접 처리) ===
2877
3171
  async function main() {
2878
3172
  const args = process.argv.slice(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-semicolon/semo-cli",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "SEMO CLI - AI Agent Orchestration Framework Installer",
5
5
  "main": "dist/index.js",
6
6
  "bin": {