@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.
- package/dist/index.js +411 -80
- 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
|
-
|
|
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
|
-
//
|
|
172
|
-
const
|
|
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
|
-
|
|
175
|
-
|
|
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 (
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
384
|
-
|
|
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
|
-
|
|
387
|
-
// fallback: 디렉토리 복사
|
|
388
|
-
console.log(chalk_1.default.yellow(` ⚠ Junction 생성
|
|
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개 통합
|
|
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-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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 (
|
|
1166
|
-
|
|
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.
|
|
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
|
-
|
|
1197
|
-
if (fs.lstatSync(linkPath).isSymbolicLink()) {
|
|
1198
|
-
|
|
1199
|
-
|
|
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
|
-
|
|
1386
|
+
else {
|
|
1210
1387
|
result.stats.skills.broken++;
|
|
1388
|
+
result.warnings.push(`깨진 링크: .claude/skills/${skill}`);
|
|
1211
1389
|
}
|
|
1212
1390
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
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
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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
|
-
|
|
1231
|
-
|
|
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
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
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);
|