@team-semicolon/semo-cli 3.1.0 → 3.3.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 +411 -80
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -131,11 +131,12 @@ async function getRemotePackageVersion(packagePath) {
131
131
  }
132
132
  }
133
133
  /**
134
- * semo-core/semo-skills 원격 버전 가져오기
134
+ * semo-core/semo-skills 원격 버전 가져오기 (semo-system/ 하위 경로)
135
135
  */
136
136
  async function getRemoteCoreVersion(type) {
137
137
  try {
138
- const url = `https://raw.githubusercontent.com/semicolon-devteam/semo/main/${type}/VERSION`;
138
+ // v5.0: semo-system/ 하위에 Standard 패키지가 위치
139
+ const url = `https://raw.githubusercontent.com/semicolon-devteam/semo/main/semo-system/${type}/VERSION`;
139
140
  const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
140
141
  if (!response.ok)
141
142
  return null;
@@ -168,12 +169,24 @@ async function showVersionComparison(cwd) {
168
169
  needsUpdate: latestCliVersion ? isVersionLower(currentCliVersion, latestCliVersion) : false,
169
170
  level: 0,
170
171
  });
171
- // semo-core (루트 또는 semo-system 내부)
172
- const corePathRoot = path.join(cwd, "semo-core", "VERSION");
172
+ // 레거시 환경 경고 (루트에 semo-core/semo-skills가 있는 경우)
173
+ const hasLegacyCore = fs.existsSync(path.join(cwd, "semo-core"));
174
+ const hasLegacySkills = fs.existsSync(path.join(cwd, "semo-skills"));
175
+ if (hasLegacyCore || hasLegacySkills) {
176
+ spinner.warn("레거시 환경 감지됨");
177
+ console.log(chalk_1.default.yellow("\n ⚠️ 구버전 SEMO 구조가 감지되었습니다."));
178
+ console.log(chalk_1.default.gray(" 루트에 semo-core/ 또는 semo-skills/가 있습니다."));
179
+ console.log(chalk_1.default.cyan("\n 💡 마이그레이션 방법:"));
180
+ console.log(chalk_1.default.gray(" 1. 기존 semo-core/, semo-skills/ 폴더 삭제"));
181
+ console.log(chalk_1.default.gray(" 2. .claude/ 폴더 삭제"));
182
+ console.log(chalk_1.default.gray(" 3. semo init 다시 실행\n"));
183
+ console.log(chalk_1.default.gray(" 또는: semo migrate --force\n"));
184
+ return;
185
+ }
186
+ // semo-core (semo-system/ 내부만 확인)
173
187
  const corePathSystem = path.join(semoSystemDir, "semo-core", "VERSION");
174
- const corePath = fs.existsSync(corePathRoot) ? corePathRoot : corePathSystem;
175
- if (fs.existsSync(corePath)) {
176
- const localCore = fs.readFileSync(corePath, "utf-8").trim();
188
+ if (fs.existsSync(corePathSystem)) {
189
+ const localCore = fs.readFileSync(corePathSystem, "utf-8").trim();
177
190
  const remoteCore = await getRemoteCoreVersion("semo-core");
178
191
  versionInfos.push({
179
192
  name: "semo-core",
@@ -183,12 +196,10 @@ async function showVersionComparison(cwd) {
183
196
  level: 0,
184
197
  });
185
198
  }
186
- // semo-skills (루트 또는 semo-system 내부)
187
- const skillsPathRoot = path.join(cwd, "semo-skills", "VERSION");
199
+ // semo-skills (semo-system/ 내부만 확인)
188
200
  const skillsPathSystem = path.join(semoSystemDir, "semo-skills", "VERSION");
189
- const skillsPath = fs.existsSync(skillsPathRoot) ? skillsPathRoot : skillsPathSystem;
190
- if (fs.existsSync(skillsPath)) {
191
- const localSkills = fs.readFileSync(skillsPath, "utf-8").trim();
201
+ if (fs.existsSync(skillsPathSystem)) {
202
+ const localSkills = fs.readFileSync(skillsPathSystem, "utf-8").trim();
192
203
  const remoteSkills = await getRemoteCoreVersion("semo-skills");
193
204
  versionInfos.push({
194
205
  name: "semo-skills",
@@ -198,6 +209,32 @@ async function showVersionComparison(cwd) {
198
209
  level: 0,
199
210
  });
200
211
  }
212
+ // semo-agents (semo-system/ 내부)
213
+ const agentsPathSystem = path.join(semoSystemDir, "semo-agents", "VERSION");
214
+ if (fs.existsSync(agentsPathSystem)) {
215
+ const localAgents = fs.readFileSync(agentsPathSystem, "utf-8").trim();
216
+ const remoteAgents = await getRemoteCoreVersion("semo-agents");
217
+ versionInfos.push({
218
+ name: "semo-agents",
219
+ local: localAgents,
220
+ remote: remoteAgents,
221
+ needsUpdate: remoteAgents ? isVersionLower(localAgents, remoteAgents) : false,
222
+ level: 0,
223
+ });
224
+ }
225
+ // semo-scripts (semo-system/ 내부)
226
+ const scriptsPathSystem = path.join(semoSystemDir, "semo-scripts", "VERSION");
227
+ if (fs.existsSync(scriptsPathSystem)) {
228
+ const localScripts = fs.readFileSync(scriptsPathSystem, "utf-8").trim();
229
+ const remoteScripts = await getRemoteCoreVersion("semo-scripts");
230
+ versionInfos.push({
231
+ name: "semo-scripts",
232
+ local: localScripts,
233
+ remote: remoteScripts,
234
+ needsUpdate: remoteScripts ? isVersionLower(localScripts, remoteScripts) : false,
235
+ level: 0,
236
+ });
237
+ }
201
238
  // 그룹 패키지 (eng, biz, ops) 및 하위 Extension - semo-system 내부
202
239
  // 그룹별로 묶어서 계층 구조로 출력
203
240
  if (hasSemoSystem) {
@@ -380,12 +417,27 @@ function createSymlinkOrJunction(targetPath, linkPath) {
380
417
  if (isWindows) {
381
418
  // Windows: Junction 사용 (절대 경로 필요)
382
419
  const absoluteTarget = path.resolve(targetPath);
383
- try {
384
- (0, child_process_1.execSync)(`cmd /c "mklink /J "${linkPath}" "${absoluteTarget}""`, { stdio: "pipe" });
420
+ // 재시도 메커니즘 (최대 3회)
421
+ let success = false;
422
+ let lastError = null;
423
+ for (let attempt = 1; attempt <= 3 && !success; attempt++) {
424
+ try {
425
+ (0, child_process_1.execSync)(`cmd /c "mklink /J "${linkPath}" "${absoluteTarget}""`, { stdio: "pipe" });
426
+ success = true;
427
+ }
428
+ catch (err) {
429
+ lastError = err;
430
+ if (attempt < 3) {
431
+ // 잠시 대기 후 재시도
432
+ (0, child_process_1.execSync)("timeout /t 1 /nobreak >nul 2>&1", { stdio: "pipe" });
433
+ }
434
+ }
385
435
  }
386
- catch {
387
- // fallback: 디렉토리 복사
388
- console.log(chalk_1.default.yellow(` ⚠ Junction 생성 실패, 복사로 대체: ${path.basename(linkPath)}`));
436
+ if (!success) {
437
+ // fallback: 디렉토리 복사 (경고 표시)
438
+ console.log(chalk_1.default.yellow(` ⚠ Junction 생성 실패 (3회 시도), 복사로 대체: ${path.basename(linkPath)}`));
439
+ console.log(chalk_1.default.gray(` 원인: ${lastError}`));
440
+ console.log(chalk_1.default.gray(` 💡 관리자 권한으로 실행하거나 개발자 모드를 활성화하세요.`));
389
441
  (0, child_process_1.execSync)(`xcopy /E /I /Q "${absoluteTarget}" "${linkPath}"`, { stdio: "pipe" });
390
442
  }
391
443
  }
@@ -395,6 +447,137 @@ function createSymlinkOrJunction(targetPath, linkPath) {
395
447
  fs.symlinkSync(relativeTarget, linkPath);
396
448
  }
397
449
  }
450
+ /**
451
+ * 심볼릭 링크가 유효한지 확인 (타겟 존재 여부)
452
+ */
453
+ function isSymlinkValid(linkPath) {
454
+ try {
455
+ const stats = fs.lstatSync(linkPath);
456
+ if (!stats.isSymbolicLink())
457
+ return true; // 일반 파일/디렉토리
458
+ // 심볼릭 링크인 경우 타겟 존재 확인
459
+ const target = fs.readlinkSync(linkPath);
460
+ const absoluteTarget = path.isAbsolute(target)
461
+ ? target
462
+ : path.resolve(path.dirname(linkPath), target);
463
+ return fs.existsSync(absoluteTarget);
464
+ }
465
+ catch {
466
+ return false;
467
+ }
468
+ }
469
+ /**
470
+ * 레거시 SEMO 환경을 감지합니다.
471
+ * 레거시: 프로젝트 루트에 semo-core/, semo-skills/ 가 직접 있는 경우
472
+ * 신규: semo-system/ 하위에 있는 경우
473
+ */
474
+ function detectLegacyEnvironment(cwd) {
475
+ const legacyPaths = [];
476
+ // 루트에 직접 있는 레거시 디렉토리 확인
477
+ const legacyDirs = ["semo-core", "semo-skills", "sax-core", "sax-skills"];
478
+ for (const dir of legacyDirs) {
479
+ const dirPath = path.join(cwd, dir);
480
+ if (fs.existsSync(dirPath) && !fs.lstatSync(dirPath).isSymbolicLink()) {
481
+ legacyPaths.push(dir);
482
+ }
483
+ }
484
+ // .claude/ 내부의 레거시 구조 확인
485
+ const claudeDir = path.join(cwd, ".claude");
486
+ if (fs.existsSync(claudeDir)) {
487
+ // 심볼릭 링크가 레거시 경로를 가리키는지 확인
488
+ const checkLegacyLink = (linkName) => {
489
+ const linkPath = path.join(claudeDir, linkName);
490
+ if (fs.existsSync(linkPath) && fs.lstatSync(linkPath).isSymbolicLink()) {
491
+ try {
492
+ const target = fs.readlinkSync(linkPath);
493
+ // 레거시 경로 패턴: ../semo-core, ../sax-core 등
494
+ if (target.match(/^\.\.\/(semo|sax)-(core|skills)/)) {
495
+ legacyPaths.push(`.claude/${linkName} → ${target}`);
496
+ }
497
+ }
498
+ catch {
499
+ // 읽기 실패 무시
500
+ }
501
+ }
502
+ };
503
+ checkLegacyLink("agents");
504
+ checkLegacyLink("skills");
505
+ checkLegacyLink("commands");
506
+ }
507
+ return {
508
+ hasLegacy: legacyPaths.length > 0,
509
+ legacyPaths,
510
+ hasSemoSystem: fs.existsSync(path.join(cwd, "semo-system")),
511
+ };
512
+ }
513
+ /**
514
+ * 레거시 환경을 새 환경으로 마이그레이션합니다.
515
+ */
516
+ async function migrateLegacyEnvironment(cwd) {
517
+ const detection = detectLegacyEnvironment(cwd);
518
+ if (!detection.hasLegacy) {
519
+ return true; // 마이그레이션 불필요
520
+ }
521
+ console.log(chalk_1.default.yellow("\n⚠️ 레거시 SEMO 환경이 감지되었습니다.\n"));
522
+ console.log(chalk_1.default.gray(" 감지된 레거시 경로:"));
523
+ detection.legacyPaths.forEach(p => {
524
+ console.log(chalk_1.default.gray(` - ${p}`));
525
+ });
526
+ console.log();
527
+ // 사용자 확인
528
+ const { shouldMigrate } = await inquirer_1.default.prompt([
529
+ {
530
+ type: "confirm",
531
+ name: "shouldMigrate",
532
+ message: "레거시 환경을 새 구조(semo-system/)로 마이그레이션하시겠습니까?",
533
+ default: true,
534
+ },
535
+ ]);
536
+ if (!shouldMigrate) {
537
+ console.log(chalk_1.default.yellow("\n마이그레이션이 취소되었습니다."));
538
+ console.log(chalk_1.default.gray("💡 수동 마이그레이션 방법:"));
539
+ console.log(chalk_1.default.gray(" 1. 기존 semo-core/, semo-skills/ 폴더 삭제"));
540
+ console.log(chalk_1.default.gray(" 2. .claude/ 폴더 삭제"));
541
+ console.log(chalk_1.default.gray(" 3. semo init 다시 실행\n"));
542
+ return false;
543
+ }
544
+ const spinner = (0, ora_1.default)("레거시 환경 마이그레이션 중...").start();
545
+ try {
546
+ // 1. 루트의 레거시 디렉토리 삭제
547
+ const legacyDirs = ["semo-core", "semo-skills", "sax-core", "sax-skills"];
548
+ for (const dir of legacyDirs) {
549
+ const dirPath = path.join(cwd, dir);
550
+ if (fs.existsSync(dirPath) && !fs.lstatSync(dirPath).isSymbolicLink()) {
551
+ removeRecursive(dirPath);
552
+ console.log(chalk_1.default.gray(` ✓ ${dir}/ 삭제됨`));
553
+ }
554
+ }
555
+ // 2. .claude/ 내부의 레거시 심볼릭 링크 삭제
556
+ const claudeDir = path.join(cwd, ".claude");
557
+ if (fs.existsSync(claudeDir)) {
558
+ const linksToCheck = ["agents", "skills", "commands"];
559
+ for (const linkName of linksToCheck) {
560
+ const linkPath = path.join(claudeDir, linkName);
561
+ if (fs.existsSync(linkPath)) {
562
+ removeRecursive(linkPath);
563
+ }
564
+ }
565
+ }
566
+ // 3. 기존 semo-system이 있으면 삭제 (새로 설치)
567
+ const semoSystemDir = path.join(cwd, "semo-system");
568
+ if (fs.existsSync(semoSystemDir)) {
569
+ removeRecursive(semoSystemDir);
570
+ }
571
+ spinner.succeed("레거시 환경 정리 완료");
572
+ console.log(chalk_1.default.green(" → 새 환경으로 설치를 진행합니다.\n"));
573
+ return true;
574
+ }
575
+ catch (error) {
576
+ spinner.fail("마이그레이션 실패");
577
+ console.error(chalk_1.default.red(` ${error}`));
578
+ return false;
579
+ }
580
+ }
398
581
  /**
399
582
  * 플랫폼에 맞는 rm -rf 실행
400
583
  */
@@ -887,19 +1070,28 @@ program
887
1070
  .option("--skip-mcp", "MCP 설정 생략")
888
1071
  .option("--no-gitignore", ".gitignore 수정 생략")
889
1072
  .option("--with <packages>", "추가 설치할 패키지 (쉼표 구분: next,backend)")
1073
+ .option("--migrate", "레거시 환경 강제 마이그레이션")
890
1074
  .action(async (options) => {
891
1075
  console.log(chalk_1.default.cyan.bold("\n🚀 SEMO 설치 시작\n"));
892
1076
  console.log(chalk_1.default.gray("Gemini 하이브리드 전략: White Box + Black Box\n"));
893
1077
  const cwd = process.cwd();
894
1078
  // 0. 버전 비교
895
1079
  await showVersionComparison(cwd);
1080
+ // 0.5. 레거시 환경 감지 및 마이그레이션
1081
+ const legacyCheck = detectLegacyEnvironment(cwd);
1082
+ if (legacyCheck.hasLegacy || options.migrate) {
1083
+ const migrationSuccess = await migrateLegacyEnvironment(cwd);
1084
+ if (!migrationSuccess) {
1085
+ process.exit(0);
1086
+ }
1087
+ }
896
1088
  // 1. 필수 도구 확인
897
1089
  const shouldContinue = await showToolsStatus();
898
1090
  if (!shouldContinue) {
899
1091
  console.log(chalk_1.default.yellow("\n설치가 취소되었습니다. 필수 도구 설치 후 다시 시도하세요.\n"));
900
1092
  process.exit(0);
901
1093
  }
902
- // 1. Git 레포지토리 확인
1094
+ // 1.5. Git 레포지토리 확인
903
1095
  const spinner = (0, ora_1.default)("Git 레포지토리 확인 중...").start();
904
1096
  try {
905
1097
  (0, child_process_1.execSync)("git rev-parse --git-dir", { cwd, stdio: "pipe" });
@@ -1003,6 +1195,8 @@ program
1003
1195
  console.log(chalk_1.default.gray(" [Standard]"));
1004
1196
  console.log(chalk_1.default.gray(" ✓ semo-core (원칙, 오케스트레이터)"));
1005
1197
  console.log(chalk_1.default.gray(" ✓ semo-skills (13개 통합 스킬)"));
1198
+ console.log(chalk_1.default.gray(" ✓ semo-agents (14개 페르소나 Agent)"));
1199
+ console.log(chalk_1.default.gray(" ✓ semo-scripts (자동화 스크립트)"));
1006
1200
  if (extensionsToInstall.length > 0) {
1007
1201
  console.log(chalk_1.default.gray(" [Extensions]"));
1008
1202
  extensionsToInstall.forEach(pkg => {
@@ -1023,7 +1217,9 @@ async function setupStandard(cwd, force) {
1023
1217
  const semoSystemDir = path.join(cwd, "semo-system");
1024
1218
  console.log(chalk_1.default.cyan("\n📚 Standard 설치 (White Box)"));
1025
1219
  console.log(chalk_1.default.gray(" semo-core: 원칙, 오케스트레이터"));
1026
- console.log(chalk_1.default.gray(" semo-skills: 13개 통합 스킬\n"));
1220
+ console.log(chalk_1.default.gray(" semo-skills: 13개 통합 스킬"));
1221
+ console.log(chalk_1.default.gray(" semo-agents: 14개 페르소나 Agent"));
1222
+ console.log(chalk_1.default.gray(" semo-scripts: 자동화 스크립트\n"));
1027
1223
  // 기존 디렉토리 확인
1028
1224
  if (fs.existsSync(semoSystemDir) && !force) {
1029
1225
  const shouldOverwrite = await confirmOverwrite("semo-system/", semoSystemDir);
@@ -1034,19 +1230,20 @@ async function setupStandard(cwd, force) {
1034
1230
  removeRecursive(semoSystemDir);
1035
1231
  console.log(chalk_1.default.green(" ✓ 기존 semo-system/ 삭제됨"));
1036
1232
  }
1037
- const spinner = (0, ora_1.default)("semo-core, semo-skills 다운로드 중...").start();
1233
+ const spinner = (0, ora_1.default)("semo-core, semo-skills, semo-agents, semo-scripts 다운로드 중...").start();
1038
1234
  try {
1039
1235
  const tempDir = path.join(cwd, ".semo-temp");
1040
1236
  removeRecursive(tempDir);
1041
1237
  (0, child_process_1.execSync)(`git clone --depth 1 ${SEMO_REPO} "${tempDir}"`, { stdio: "pipe" });
1042
1238
  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"));
1239
+ // Standard 패키지 목록 (semo-system/ 하위에 있는 것들)
1240
+ const standardPackages = ["semo-core", "semo-skills", "semo-agents", "semo-scripts"];
1241
+ for (const pkg of standardPackages) {
1242
+ const srcPath = path.join(tempDir, "semo-system", pkg);
1243
+ const destPath = path.join(semoSystemDir, pkg);
1244
+ if (fs.existsSync(srcPath)) {
1245
+ copyRecursive(srcPath, destPath);
1246
+ }
1050
1247
  }
1051
1248
  removeRecursive(tempDir);
1052
1249
  spinner.succeed("Standard 설치 완료");
@@ -1152,7 +1349,7 @@ function verifyInstallation(cwd, installedExtensions = []) {
1152
1349
  result.errors.push("semo-skills가 설치되지 않았습니다");
1153
1350
  result.success = false;
1154
1351
  }
1155
- // 2. agents 링크 검증
1352
+ // 2. agents 링크 검증 (isSymlinkValid 사용)
1156
1353
  const claudeAgentsDir = path.join(claudeDir, "agents");
1157
1354
  const coreAgentsDir = path.join(coreDir, "agents");
1158
1355
  if (fs.existsSync(coreAgentsDir)) {
@@ -1161,31 +1358,19 @@ function verifyInstallation(cwd, installedExtensions = []) {
1161
1358
  if (fs.existsSync(claudeAgentsDir)) {
1162
1359
  for (const agent of expectedAgents) {
1163
1360
  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
- }
1361
+ if (fs.existsSync(linkPath) || fs.lstatSync(linkPath).isSymbolicLink()) {
1362
+ if (isSymlinkValid(linkPath)) {
1363
+ result.stats.agents.linked++;
1180
1364
  }
1181
1365
  else {
1182
- result.stats.agents.linked++; // 디렉토리로 복사된 경우
1366
+ result.stats.agents.broken++;
1367
+ result.warnings.push(`깨진 링크: .claude/agents/${agent}`);
1183
1368
  }
1184
1369
  }
1185
1370
  }
1186
1371
  }
1187
1372
  }
1188
- // 3. skills 링크 검증
1373
+ // 3. skills 링크 검증 (isSymlinkValid 사용)
1189
1374
  if (fs.existsSync(skillsDir)) {
1190
1375
  const expectedSkills = fs.readdirSync(skillsDir).filter(f => fs.statSync(path.join(skillsDir, f)).isDirectory());
1191
1376
  result.stats.skills.expected = expectedSkills.length;
@@ -1193,43 +1378,38 @@ function verifyInstallation(cwd, installedExtensions = []) {
1193
1378
  if (fs.existsSync(claudeSkillsDir)) {
1194
1379
  for (const skill of expectedSkills) {
1195
1380
  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
- }
1381
+ try {
1382
+ if (fs.existsSync(linkPath) || fs.lstatSync(linkPath).isSymbolicLink()) {
1383
+ if (isSymlinkValid(linkPath)) {
1384
+ result.stats.skills.linked++;
1208
1385
  }
1209
- catch {
1386
+ else {
1210
1387
  result.stats.skills.broken++;
1388
+ result.warnings.push(`깨진 링크: .claude/skills/${skill}`);
1211
1389
  }
1212
1390
  }
1213
- else {
1214
- result.stats.skills.linked++;
1215
- }
1391
+ }
1392
+ catch {
1393
+ // 링크가 존재하지 않음
1216
1394
  }
1217
1395
  }
1218
1396
  }
1219
1397
  }
1220
- // 4. commands 검증
1398
+ // 4. commands 검증 (isSymlinkValid 사용)
1221
1399
  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);
1400
+ try {
1401
+ const linkExists = fs.existsSync(semoCommandsLink) || fs.lstatSync(semoCommandsLink).isSymbolicLink();
1402
+ result.stats.commands.exists = linkExists;
1403
+ if (linkExists) {
1404
+ result.stats.commands.valid = isSymlinkValid(semoCommandsLink);
1226
1405
  if (!result.stats.commands.valid) {
1227
1406
  result.warnings.push("깨진 링크: .claude/commands/SEMO");
1228
1407
  }
1229
1408
  }
1230
- else {
1231
- result.stats.commands.valid = true;
1232
- }
1409
+ }
1410
+ catch {
1411
+ result.stats.commands.exists = false;
1412
+ result.stats.commands.valid = false;
1233
1413
  }
1234
1414
  // 5. Extensions 검증
1235
1415
  for (const ext of installedExtensions) {
@@ -2721,6 +2901,8 @@ program
2721
2901
  // 업데이트 대상 결정
2722
2902
  const updateSemoCore = !isSelectiveUpdate || onlyPackages.includes("semo-core");
2723
2903
  const updateSemoSkills = !isSelectiveUpdate || onlyPackages.includes("semo-skills");
2904
+ const updateSemoAgents = !isSelectiveUpdate || onlyPackages.includes("semo-agents");
2905
+ const updateSemoScripts = !isSelectiveUpdate || onlyPackages.includes("semo-scripts");
2724
2906
  const extensionsToUpdate = isSelectiveUpdate
2725
2907
  ? installedExtensions.filter(ext => onlyPackages.includes(ext))
2726
2908
  : installedExtensions;
@@ -2730,12 +2912,16 @@ program
2730
2912
  console.log(chalk_1.default.gray(" - semo-core"));
2731
2913
  if (updateSemoSkills)
2732
2914
  console.log(chalk_1.default.gray(" - semo-skills"));
2915
+ if (updateSemoAgents)
2916
+ console.log(chalk_1.default.gray(" - semo-agents"));
2917
+ if (updateSemoScripts)
2918
+ console.log(chalk_1.default.gray(" - semo-scripts"));
2733
2919
  extensionsToUpdate.forEach(pkg => {
2734
2920
  console.log(chalk_1.default.gray(` - ${pkg}`));
2735
2921
  });
2736
- if (!updateSemoCore && !updateSemoSkills && extensionsToUpdate.length === 0) {
2922
+ if (!updateSemoCore && !updateSemoSkills && !updateSemoAgents && !updateSemoScripts && extensionsToUpdate.length === 0) {
2737
2923
  console.log(chalk_1.default.yellow("\n ⚠️ 업데이트할 패키지가 없습니다."));
2738
- console.log(chalk_1.default.gray(" 설치된 패키지: semo-core, semo-skills" +
2924
+ console.log(chalk_1.default.gray(" 설치된 패키지: semo-core, semo-skills, semo-agents, semo-scripts" +
2739
2925
  (installedExtensions.length > 0 ? ", " + installedExtensions.join(", ") : "")));
2740
2926
  return;
2741
2927
  }
@@ -2744,14 +2930,22 @@ program
2744
2930
  const tempDir = path.join(cwd, ".semo-temp");
2745
2931
  removeRecursive(tempDir);
2746
2932
  (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"));
2933
+ // Standard 업데이트 (선택적) - semo-system/ 하위에서 복사
2934
+ const standardUpdates = [
2935
+ { flag: updateSemoCore, name: "semo-core" },
2936
+ { flag: updateSemoSkills, name: "semo-skills" },
2937
+ { flag: updateSemoAgents, name: "semo-agents" },
2938
+ { flag: updateSemoScripts, name: "semo-scripts" },
2939
+ ];
2940
+ for (const { flag, name } of standardUpdates) {
2941
+ if (flag) {
2942
+ const srcPath = path.join(tempDir, "semo-system", name);
2943
+ const destPath = path.join(semoSystemDir, name);
2944
+ if (fs.existsSync(srcPath)) {
2945
+ removeRecursive(destPath);
2946
+ copyRecursive(srcPath, destPath);
2947
+ }
2948
+ }
2755
2949
  }
2756
2950
  // Extensions 업데이트 (선택적)
2757
2951
  for (const pkg of extensionsToUpdate) {
@@ -2873,6 +3067,143 @@ program
2873
3067
  console.log(chalk_1.default.yellow.bold("\n⚠️ SEMO 업데이트 완료 (일부 문제 발견)\n"));
2874
3068
  }
2875
3069
  });
3070
+ // === migrate 명령어 ===
3071
+ program
3072
+ .command("migrate")
3073
+ .description("레거시 SEMO 환경을 새 구조(semo-system/)로 마이그레이션")
3074
+ .option("-f, --force", "확인 없이 강제 마이그레이션")
3075
+ .action(async (options) => {
3076
+ console.log(chalk_1.default.cyan.bold("\n🔄 SEMO 마이그레이션\n"));
3077
+ const cwd = process.cwd();
3078
+ const detection = detectLegacyEnvironment(cwd);
3079
+ if (!detection.hasLegacy) {
3080
+ console.log(chalk_1.default.green("✅ 레거시 환경이 감지되지 않았습니다."));
3081
+ if (detection.hasSemoSystem) {
3082
+ console.log(chalk_1.default.gray(" 현재 환경: semo-system/ (정상)"));
3083
+ }
3084
+ else {
3085
+ console.log(chalk_1.default.gray(" SEMO가 설치되지 않았습니다. 'semo init'을 실행하세요."));
3086
+ }
3087
+ console.log();
3088
+ return;
3089
+ }
3090
+ console.log(chalk_1.default.yellow("⚠️ 레거시 SEMO 환경이 감지되었습니다.\n"));
3091
+ console.log(chalk_1.default.gray(" 감지된 레거시 경로:"));
3092
+ detection.legacyPaths.forEach(p => {
3093
+ console.log(chalk_1.default.gray(` - ${p}`));
3094
+ });
3095
+ console.log();
3096
+ if (!options.force) {
3097
+ const { confirm } = await inquirer_1.default.prompt([
3098
+ {
3099
+ type: "confirm",
3100
+ name: "confirm",
3101
+ message: "레거시 환경을 삭제하고 새 구조로 마이그레이션하시겠습니까?",
3102
+ default: true,
3103
+ },
3104
+ ]);
3105
+ if (!confirm) {
3106
+ console.log(chalk_1.default.yellow("\n마이그레이션이 취소되었습니다.\n"));
3107
+ return;
3108
+ }
3109
+ }
3110
+ const migrationSuccess = await migrateLegacyEnvironment(cwd);
3111
+ if (migrationSuccess) {
3112
+ console.log(chalk_1.default.cyan("\n새 환경 설치를 위해 'semo init'을 실행하세요.\n"));
3113
+ }
3114
+ });
3115
+ // === doctor 명령어 (설치 상태 진단) ===
3116
+ program
3117
+ .command("doctor")
3118
+ .description("SEMO 설치 상태를 진단하고 문제를 리포트")
3119
+ .action(async () => {
3120
+ console.log(chalk_1.default.cyan.bold("\n🩺 SEMO 진단\n"));
3121
+ const cwd = process.cwd();
3122
+ const semoSystemDir = path.join(cwd, "semo-system");
3123
+ const claudeDir = path.join(cwd, ".claude");
3124
+ // 1. 레거시 환경 확인
3125
+ console.log(chalk_1.default.cyan("1. 레거시 환경 확인"));
3126
+ const legacyCheck = detectLegacyEnvironment(cwd);
3127
+ if (legacyCheck.hasLegacy) {
3128
+ console.log(chalk_1.default.yellow(" ⚠️ 레거시 환경 감지됨"));
3129
+ legacyCheck.legacyPaths.forEach(p => {
3130
+ console.log(chalk_1.default.gray(` - ${p}`));
3131
+ });
3132
+ console.log(chalk_1.default.gray(" 💡 해결: semo migrate 실행"));
3133
+ }
3134
+ else {
3135
+ console.log(chalk_1.default.green(" ✅ 레거시 환경 없음"));
3136
+ }
3137
+ // 2. semo-system 확인
3138
+ console.log(chalk_1.default.cyan("\n2. semo-system 구조 확인"));
3139
+ if (!fs.existsSync(semoSystemDir)) {
3140
+ console.log(chalk_1.default.red(" ❌ semo-system/ 없음"));
3141
+ console.log(chalk_1.default.gray(" 💡 해결: semo init 실행"));
3142
+ }
3143
+ else {
3144
+ const packages = ["semo-core", "semo-skills", "semo-agents", "semo-scripts"];
3145
+ for (const pkg of packages) {
3146
+ const pkgPath = path.join(semoSystemDir, pkg);
3147
+ if (fs.existsSync(pkgPath)) {
3148
+ const versionPath = path.join(pkgPath, "VERSION");
3149
+ const version = fs.existsSync(versionPath)
3150
+ ? fs.readFileSync(versionPath, "utf-8").trim()
3151
+ : "?";
3152
+ console.log(chalk_1.default.green(` ✅ ${pkg} v${version}`));
3153
+ }
3154
+ else {
3155
+ console.log(chalk_1.default.yellow(` ⚠️ ${pkg} 없음`));
3156
+ }
3157
+ }
3158
+ }
3159
+ // 3. 심볼릭 링크 확인
3160
+ console.log(chalk_1.default.cyan("\n3. 심볼릭 링크 상태"));
3161
+ if (fs.existsSync(claudeDir)) {
3162
+ const linksToCheck = [
3163
+ { name: "agents", dir: path.join(claudeDir, "agents") },
3164
+ { name: "skills", dir: path.join(claudeDir, "skills") },
3165
+ { name: "commands/SEMO", dir: path.join(claudeDir, "commands", "SEMO") },
3166
+ ];
3167
+ for (const { name, dir } of linksToCheck) {
3168
+ if (fs.existsSync(dir)) {
3169
+ if (fs.lstatSync(dir).isSymbolicLink()) {
3170
+ if (isSymlinkValid(dir)) {
3171
+ console.log(chalk_1.default.green(` ✅ .claude/${name} (심볼릭 링크)`));
3172
+ }
3173
+ else {
3174
+ console.log(chalk_1.default.red(` ❌ .claude/${name} (깨진 링크)`));
3175
+ console.log(chalk_1.default.gray(" 💡 해결: semo update 실행"));
3176
+ }
3177
+ }
3178
+ else {
3179
+ console.log(chalk_1.default.green(` ✅ .claude/${name} (복사본)`));
3180
+ }
3181
+ }
3182
+ else {
3183
+ console.log(chalk_1.default.yellow(` ⚠️ .claude/${name} 없음`));
3184
+ }
3185
+ }
3186
+ }
3187
+ else {
3188
+ console.log(chalk_1.default.red(" ❌ .claude/ 디렉토리 없음"));
3189
+ }
3190
+ // 4. 설치 검증
3191
+ console.log(chalk_1.default.cyan("\n4. 전체 설치 검증"));
3192
+ const verificationResult = verifyInstallation(cwd, []);
3193
+ if (verificationResult.success) {
3194
+ console.log(chalk_1.default.green(" ✅ 설치 상태 정상"));
3195
+ }
3196
+ else {
3197
+ console.log(chalk_1.default.yellow(" ⚠️ 문제 발견"));
3198
+ verificationResult.errors.forEach(err => {
3199
+ console.log(chalk_1.default.red(` ❌ ${err}`));
3200
+ });
3201
+ verificationResult.warnings.forEach(warn => {
3202
+ console.log(chalk_1.default.yellow(` ⚠️ ${warn}`));
3203
+ });
3204
+ }
3205
+ console.log();
3206
+ });
2876
3207
  // === -v 옵션 처리 (program.parse 전에 직접 처리) ===
2877
3208
  async function main() {
2878
3209
  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.3.0",
4
4
  "description": "SEMO CLI - AI Agent Orchestration Framework Installer",
5
5
  "main": "dist/index.js",
6
6
  "bin": {