relayax-cli 0.3.65 → 0.3.66

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.
@@ -1,12 +1,19 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.registerUpdate = registerUpdate;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const os_1 = __importDefault(require("os"));
9
+ const path_1 = __importDefault(require("path"));
4
10
  const api_js_1 = require("../lib/api.js");
5
11
  const storage_js_1 = require("../lib/storage.js");
6
12
  const installer_js_1 = require("../lib/installer.js");
7
13
  const config_js_1 = require("../lib/config.js");
8
14
  const slug_js_1 = require("../lib/slug.js");
9
15
  const preamble_js_1 = require("../lib/preamble.js");
16
+ const paths_js_1 = require("../lib/paths.js");
10
17
  function registerUpdate(program) {
11
18
  program
12
19
  .command('update <slug>')
@@ -15,11 +22,12 @@ function registerUpdate(program) {
15
22
  .option('--code <code>', '초대 코드 (비공개 에이전트 업데이트 시 필요)')
16
23
  .action(async (slugInput, opts) => {
17
24
  const json = program.opts().json ?? false;
18
- const installPath = (0, config_js_1.getInstallPath)(opts.path);
19
25
  const tempDir = (0, storage_js_1.makeTempDir)();
26
+ const projectPath = (0, paths_js_1.resolveProjectPath)(opts.path);
20
27
  try {
21
- // Resolve scoped slug (try installed.json first for offline, then server)
22
- const installed = (0, config_js_1.loadInstalled)();
28
+ // Resolve scoped slug
29
+ const localInstalled = (0, config_js_1.loadInstalled)();
30
+ const globalInstalled = (0, config_js_1.loadGlobalInstalled)();
23
31
  let slug;
24
32
  if ((0, slug_js_1.isScopedSlug)(slugInput)) {
25
33
  slug = slugInput;
@@ -28,9 +36,11 @@ function registerUpdate(program) {
28
36
  const parsed = await (0, slug_js_1.resolveSlug)(slugInput);
29
37
  slug = parsed.full;
30
38
  }
31
- // Check installed.json for current version
32
- const currentEntry = installed[slug];
39
+ // Find current entry (check both registries)
40
+ const currentEntry = localInstalled[slug] ?? globalInstalled[slug];
33
41
  const currentVersion = currentEntry?.version ?? null;
42
+ const currentScope = globalInstalled[slug] ? 'global'
43
+ : currentEntry?.deploy_scope ?? 'global';
34
44
  // Fetch latest agent metadata
35
45
  const agent = await (0, api_js_1.fetchAgentInfo)(slug);
36
46
  const latestVersion = agent.version;
@@ -52,39 +62,62 @@ function registerUpdate(program) {
52
62
  process.exit(1);
53
63
  }
54
64
  }
55
- // Download package
65
+ // Clean up old symlinks (new) and deployed_files (legacy migration)
66
+ if (currentEntry?.deployed_symlinks && currentEntry.deployed_symlinks.length > 0) {
67
+ (0, installer_js_1.removeSymlinks)(currentEntry.deployed_symlinks);
68
+ }
69
+ if (currentEntry?.deployed_files && currentEntry.deployed_files.length > 0) {
70
+ (0, installer_js_1.uninstallAgent)(currentEntry.deployed_files);
71
+ }
72
+ // Determine agent directory
73
+ const parsedSlug = (0, slug_js_1.parseSlug)(slug);
74
+ const owner = parsedSlug?.owner ?? 'unknown';
75
+ const name = parsedSlug?.name ?? slug;
76
+ const agentDir = currentScope === 'global'
77
+ ? path_1.default.join(os_1.default.homedir(), '.relay', 'agents', owner, name)
78
+ : path_1.default.join(projectPath, '.relay', 'agents', owner, name);
79
+ // Download & extract
56
80
  const tarPath = await (0, storage_js_1.downloadPackage)(agent.package_url, tempDir);
57
- // Extract
58
- const extractDir = `${tempDir}/extracted`;
59
- await (0, storage_js_1.extractPackage)(tarPath, extractDir);
60
- // Inject preamble (update check) before copying
61
- (0, preamble_js_1.injectPreambleToAgent)(extractDir, slug);
62
- // Copy files to install_path
63
- const files = (0, installer_js_1.installAgent)(extractDir, installPath);
64
- // Preserve deploy info but clear deployed_files (agent needs to re-deploy)
65
- const previousDeployScope = currentEntry?.deploy_scope;
66
- const hadDeployedFiles = (currentEntry?.deployed_files?.length ?? 0) > 0;
67
- // Update installed.json with new version
68
- installed[slug] = {
81
+ if (fs_1.default.existsSync(agentDir)) {
82
+ fs_1.default.rmSync(agentDir, { recursive: true, force: true });
83
+ }
84
+ fs_1.default.mkdirSync(agentDir, { recursive: true });
85
+ await (0, storage_js_1.extractPackage)(tarPath, agentDir);
86
+ // Inject preamble
87
+ (0, preamble_js_1.injectPreambleToAgent)(agentDir, slug);
88
+ // Deploy symlinks (always handles migration from legacy deployed_files)
89
+ const deploy = (0, installer_js_1.deploySymlinks)(agentDir, currentScope, projectPath);
90
+ // Update installed.json
91
+ const installRecord = {
69
92
  agent_id: agent.id,
70
93
  version: latestVersion,
71
94
  installed_at: new Date().toISOString(),
72
- files,
73
- // Keep deploy_scope so agent knows where to re-deploy
74
- ...(previousDeployScope ? { deploy_scope: previousDeployScope } : {}),
75
- // Clear deployed_files — agent must re-deploy and call deploy-record
95
+ files: [agentDir],
96
+ deploy_scope: currentScope,
97
+ deployed_symlinks: deploy.symlinks,
76
98
  };
77
- (0, config_js_1.saveInstalled)(installed);
78
- // Report install (non-blocking, agent_id 기반)
99
+ if (currentScope === 'global') {
100
+ globalInstalled[slug] = installRecord;
101
+ (0, config_js_1.saveGlobalInstalled)(globalInstalled);
102
+ // Clean up local entry if migrating
103
+ if (localInstalled[slug]) {
104
+ delete localInstalled[slug];
105
+ (0, config_js_1.saveInstalled)(localInstalled);
106
+ }
107
+ }
108
+ else {
109
+ localInstalled[slug] = installRecord;
110
+ (0, config_js_1.saveInstalled)(localInstalled);
111
+ }
112
+ // Report
79
113
  await (0, api_js_1.reportInstall)(agent.id, slug, latestVersion);
80
114
  const result = {
81
115
  status: 'updated',
82
116
  slug,
83
117
  from_version: currentVersion,
84
118
  version: latestVersion,
85
- files_installed: files.length,
86
- install_path: installPath,
87
- ...(hadDeployedFiles ? { needs_redeploy: true, previous_deploy_scope: previousDeployScope } : {}),
119
+ scope: currentScope,
120
+ symlinks: deploy.symlinks.length,
88
121
  };
89
122
  if (json) {
90
123
  console.log(JSON.stringify(result));
@@ -92,9 +125,9 @@ function registerUpdate(program) {
92
125
  else {
93
126
  const fromLabel = currentVersion ? `v${currentVersion} → ` : '';
94
127
  console.log(`\n\x1b[32m✓ ${agent.name} ${fromLabel}v${latestVersion} 업데이트 완료\x1b[0m`);
95
- console.log(` 설치 위치: \x1b[36m${installPath}\x1b[0m`);
96
- console.log(` 파일 수: ${files.length}개`);
97
- // Show changelog for this version
128
+ console.log(` 위치: \x1b[36m${agentDir}\x1b[0m`);
129
+ console.log(` symlink: ${deploy.symlinks.length}개`);
130
+ // Show changelog
98
131
  try {
99
132
  const versions = await (0, api_js_1.fetchAgentVersions)(slug);
100
133
  const thisVersion = versions.find((v) => v.version === latestVersion);
@@ -107,8 +140,11 @@ function registerUpdate(program) {
107
140
  }
108
141
  }
109
142
  catch {
110
- // Non-critical: skip changelog display
143
+ // Non-critical
111
144
  }
145
+ // Requires check
146
+ const requiresResults = (0, installer_js_1.checkRequires)(agentDir);
147
+ (0, installer_js_1.printRequiresCheck)(requiresResults);
112
148
  }
113
149
  }
114
150
  catch (err) {
@@ -90,14 +90,13 @@ function getGlobalCommandPathForTool(skillsDir, commandId) {
90
90
  function formatCommandFile(content) {
91
91
  return `---\ndescription: ${content.description}\n---\n\n${content.body}\n`;
92
92
  }
93
- // ─── 프롬프트 조각은 cli/src/prompts/*.md에서 관리 (SSOT) ───
94
- // REQUIREMENTS_CHECK, ERROR_HANDLING_GUIDE → import from '../prompts/index.js'
93
+ // ─── 프롬프트는 cli/src/prompts/*.md에서 관리 (SSOT) ───
95
94
  // ─── User Commands (글로벌 설치) ───
96
95
  exports.USER_COMMANDS = [
97
96
  {
98
- id: 'relay-install',
99
- description: 'relay에서 에이전트를 설치합니다',
100
- body: ENV_PREAMBLE + index_js_1.INSTALL_PROMPT,
97
+ id: 'relay-explore',
98
+ description: 'relay 마켓플레이스를 탐색하고 프로젝트에 맞는 에이전트를 찾습니다',
99
+ body: index_js_1.EXPLORE_PROMPT,
101
100
  },
102
101
  {
103
102
  id: 'relay-status',
@@ -162,7 +161,7 @@ ${index_js_1.ERROR_HANDLING_GUIDE}
162
161
  - \`--org <slug>\` 인자가 있으면: \`relay list --org <org-slug> --json\`으로 해당 Organization의 에이전트 목록도 보여줍니다.
163
162
 
164
163
  ### 4. 안내
165
- - 설치된 에이전트가 없으면 \`/relay-install\`로 에이전트를 탐색·설치해보라고 안내합니다.
164
+ - 설치된 에이전트가 없으면 \`/relay-explore\`로 에이전트를 탐색해보라고 안내합니다.
166
165
  - Org가 있으면 활용법을 안내합니다:
167
166
  - Org 에이전트 설치: \`relay install @<org-slug>/<agent>\`
168
167
  - Org 관리: www.relayax.com/orgs/<slug>
@@ -210,9 +209,9 @@ ${index_js_1.ERROR_HANDLING_GUIDE}
210
209
  → "✓ @alice/doc-writer 삭제 완료 (12개 파일 제거)"`,
211
210
  },
212
211
  {
213
- id: 'relay-publish',
214
- description: '현재 에이전트 패키지를 relay에 배포합니다',
215
- body: ENV_PREAMBLE + index_js_1.PUBLISH_PROMPT,
212
+ id: 'relay-create',
213
+ description: '에이전트 패키지를 새로 만들어 relay에 배포합니다',
214
+ body: index_js_1.CREATE_PROMPT,
216
215
  },
217
216
  ];
218
217
  // ─── Builder Commands (로컬 설치) ───
@@ -8,6 +8,5 @@ export declare function generateGuide(config: {
8
8
  name: string;
9
9
  description: string;
10
10
  version: string;
11
- visibility?: string;
12
11
  }, commands: CommandEntry[], requires?: Requires): string;
13
12
  export {};
package/dist/lib/guide.js CHANGED
@@ -1,101 +1,51 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.generateGuide = generateGuide;
4
- const index_js_1 = require("../prompts/index.js");
5
- function buildSetupSection(needsLogin) {
6
- if (!needsLogin)
7
- return index_js_1.SETUP_CLI;
8
- return `${index_js_1.SETUP_CLI}\n\n${index_js_1.SETUP_LOGIN}`;
9
- }
10
4
  function buildRequiresSummary(requires) {
11
5
  const lines = [];
12
- if (requires.cli && requires.cli.length > 0) {
13
- for (const cli of requires.cli) {
14
- const label = cli.required === false ? '선택' : '필수';
15
- lines.push(`- cli: **${cli.name}** (${label})${cli.install ? ` — \`${cli.install}\`` : ''}`);
16
- }
6
+ for (const cli of requires.cli ?? []) {
7
+ const label = cli.required === false ? '선택' : '필수';
8
+ lines.push(`- cli: **${cli.name}** (${label})${cli.install ? ` \`${cli.install}\`` : ''}`);
17
9
  }
18
10
  if (requires.npm && requires.npm.length > 0) {
19
- const pkgNames = requires.npm.map((p) => typeof p === 'string' ? p : p.name);
20
- lines.push(`- npm: ${pkgNames.map((n) => `**${n}**`).join(', ')}`);
21
- }
22
- if (requires.env && requires.env.length > 0) {
23
- for (const env of requires.env) {
24
- const label = env.required === false ? '선택' : '필수';
25
- const desc = env.description ? ` — ${env.description}` : '';
26
- lines.push(`- env: **${env.name}** (${label})${desc}`);
27
- }
11
+ const names = requires.npm.map((p) => typeof p === 'string' ? p : p.name);
12
+ lines.push(`- npm: ${names.map((n) => `**${n}**`).join(', ')}`);
28
13
  }
29
- if (requires.mcp && requires.mcp.length > 0) {
30
- for (const mcp of requires.mcp) {
31
- const pkg = mcp.package ? ` — \`${mcp.package}\`` : '';
32
- lines.push(`- mcp: **${mcp.name}**${pkg}`);
33
- }
14
+ for (const env of requires.env ?? []) {
15
+ const label = env.required === false ? '선택' : '필수';
16
+ const desc = env.description ? ` — ${env.description}` : '';
17
+ lines.push(`- env: **${env.name}** (${label})${desc}`);
34
18
  }
35
- if (requires.agents && requires.agents.length > 0) {
36
- for (const agent of requires.agents) {
37
- lines.push(`- agents: **${agent}**`);
38
- }
19
+ for (const mcp of requires.mcp ?? []) {
20
+ const pkg = mcp.package ? ` — \`${mcp.package}\`` : '';
21
+ lines.push(`- mcp: **${mcp.name}**${pkg}`);
39
22
  }
40
- if (requires.permissions && requires.permissions.length > 0) {
41
- lines.push(`- permissions: ${requires.permissions.map((p) => `\`${p}\``).join(', ')}`);
23
+ for (const agent of requires.agents ?? []) {
24
+ lines.push(`- agent: **${agent}**`);
42
25
  }
43
26
  return lines.join('\n');
44
27
  }
45
- function buildRequiresSection(requires) {
46
- const summary = buildRequiresSummary(requires);
47
- if (!summary)
48
- return '';
49
- return `## Step 4. 의존성 확인 및 설치
50
-
51
- 이 에이전트는 아래 의존성이 필요합니다:
52
-
53
- ${summary}
54
-
55
- ${index_js_1.REQUIREMENTS_CHECK}
56
- `;
57
- }
58
28
  function generateGuide(config, commands, requires) {
59
29
  const scopedSlug = config.slug.startsWith('@') ? config.slug : `@${config.slug}`;
60
- const needsLogin = config.visibility === 'internal' || config.visibility === 'private';
61
- const setupSection = buildSetupSection(needsLogin);
62
- const installStepNum = needsLogin ? 3 : 2;
63
- const requiresSection = requires ? buildRequiresSection(requires) : '';
64
- const requiresSectionFinal = requiresSection
65
- ? requiresSection.replace('## Step 4.', `## Step ${installStepNum + 1}.`)
66
- : '';
67
- let usageHint = '';
68
- if (commands.length > 0) {
69
- usageHint = `## 사용법
70
-
71
- 설치가 완료되면 아래 커맨드를 사용할 수 있습니다:
72
-
73
- ${commands.map((cmd) => `- \`/${cmd.name}\`: ${cmd.description}`).join('\n')}
74
-
75
- 원하는 작업을 말하거나 위 커맨드를 직접 실행하세요.
76
- `;
77
- }
78
- return `# ${config.name} — 설치 가이드
30
+ const installCmd = `npx relayax-cli install ${scopedSlug}`;
31
+ const requiresSummary = requires ? buildRequiresSummary(requires) : '';
32
+ return `# ${config.name}
79
33
 
80
34
  > ${config.description}
81
35
 
82
- 가이드는 Relay 에이전트를 설치하는 절차입니다.
83
- 각 Step의 명령어를 순서대로 실행하세요. 요약하지 말고 바로 Step 0(환경 감지)부터 시작하세요.
84
-
85
- ${index_js_1.SETUP_ENVIRONMENT}
36
+ ## 설치
86
37
 
87
- ${setupSection}
38
+ \`\`\`bash
39
+ ${installCmd}
40
+ \`\`\`
88
41
 
89
- ## Step ${installStepNum}. 에이전트 설치
42
+ ${commands.length > 0 ? `## 포함된 커맨드
90
43
 
91
- 감지된 환경에 맞게 실행하세요:
92
- - 환경 A (터미널): \`relay install ${scopedSlug}\`
93
- - 환경 B (샌드박스): \`npx relayax-cli install ${scopedSlug}\`
94
-
95
- 설치 후 아래 의존성을 확인하고 환경을 구성합니다.
44
+ ${commands.map((cmd) => `- \`/${cmd.name}\`: ${cmd.description}`).join('\n')}
45
+ ` : ''}${requiresSummary ? `## 요구사항
96
46
 
97
- ${requiresSectionFinal}${usageHint}---
98
- *출처: https://relayax.com — Agent Marketplace*
99
- *설치: \`relay install ${scopedSlug}\`*
47
+ ${requiresSummary}
48
+ ` : ''}---
49
+ *https://relayax.com Agent Marketplace*
100
50
  `;
101
51
  }
@@ -1,3 +1,34 @@
1
+ export interface DeployResult {
2
+ symlinks: string[];
3
+ warnings: string[];
4
+ }
5
+ /**
6
+ * agentDir 내 skills/, commands/, agents/, rules/ 하위 항목을
7
+ * 감지된 AI tool의 skillsDir에 symlink로 생성한다.
8
+ *
9
+ * @param agentDir .relay/agents/<owner>/<name>/ 경로
10
+ * @param slug @owner/name 형태
11
+ * @param scope 'global' | 'local'
12
+ * @param projectPath 프로젝트 루트 경로 (local scope 시 사용)
13
+ */
14
+ export declare function deploySymlinks(agentDir: string, scope: 'global' | 'local', projectPath: string): DeployResult;
15
+ /**
16
+ * symlink 목록을 기반으로 symlink를 제거한다.
17
+ */
18
+ export declare function removeSymlinks(symlinks: string[]): string[];
19
+ interface RequiresCheckResult {
20
+ label: string;
21
+ status: 'ok' | 'warn' | 'missing';
22
+ message: string;
23
+ }
24
+ /**
25
+ * agentDir의 relay.yaml에서 requires를 읽고 체크 결과를 반환한다.
26
+ */
27
+ export declare function checkRequires(agentDir: string): RequiresCheckResult[];
28
+ /**
29
+ * requires 체크 결과를 콘솔에 출력한다.
30
+ */
31
+ export declare function printRequiresCheck(results: RequiresCheckResult[]): void;
1
32
  export declare function installAgent(extractedDir: string, installPath: string): string[];
2
33
  export declare function uninstallAgent(files: string[]): string[];
3
34
  /**
@@ -5,3 +36,4 @@ export declare function uninstallAgent(files: string[]): string[];
5
36
  * 예: /home/.claude/skills/cardnews/ 가 비었으면 삭제, /home/.claude/skills/는 유지
6
37
  */
7
38
  export declare function cleanEmptyParents(filePath: string, boundary: string): void;
39
+ export {};
@@ -3,12 +3,277 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.deploySymlinks = deploySymlinks;
7
+ exports.removeSymlinks = removeSymlinks;
8
+ exports.checkRequires = checkRequires;
9
+ exports.printRequiresCheck = printRequiresCheck;
6
10
  exports.installAgent = installAgent;
7
11
  exports.uninstallAgent = uninstallAgent;
8
12
  exports.cleanEmptyParents = cleanEmptyParents;
9
13
  const fs_1 = __importDefault(require("fs"));
14
+ const os_1 = __importDefault(require("os"));
10
15
  const path_1 = __importDefault(require("path"));
16
+ const child_process_1 = require("child_process");
17
+ const ai_tools_js_1 = require("./ai-tools.js");
11
18
  const COPY_DIRS = ['skills', 'agents', 'rules', 'commands'];
19
+ const SYMLINK_DIRS = ['skills', 'commands', 'agents', 'rules'];
20
+ /**
21
+ * agentDir 내 skills/, commands/, agents/, rules/ 하위 항목을
22
+ * 감지된 AI tool의 skillsDir에 symlink로 생성한다.
23
+ *
24
+ * @param agentDir .relay/agents/<owner>/<name>/ 경로
25
+ * @param slug @owner/name 형태
26
+ * @param scope 'global' | 'local'
27
+ * @param projectPath 프로젝트 루트 경로 (local scope 시 사용)
28
+ */
29
+ function deploySymlinks(agentDir, scope, projectPath) {
30
+ const result = { symlinks: [], warnings: [] };
31
+ // 감지된 AI tool 목록
32
+ const tools = scope === 'global'
33
+ ? (0, ai_tools_js_1.detectGlobalCLIs)()
34
+ : (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
35
+ // Claude Code를 기본으로 포함 (글로벌에 .claude/가 없어도 생성)
36
+ if (scope === 'global') {
37
+ const hasClaudeCode = tools.some((t) => t.value === 'claude');
38
+ if (!hasClaudeCode) {
39
+ tools.push({ name: 'Claude Code', value: 'claude', skillsDir: '.claude' });
40
+ }
41
+ }
42
+ for (const tool of tools) {
43
+ const baseDir = scope === 'global'
44
+ ? path_1.default.join(os_1.default.homedir(), tool.skillsDir)
45
+ : path_1.default.join(projectPath, tool.skillsDir);
46
+ for (const dir of SYMLINK_DIRS) {
47
+ const srcDir = path_1.default.join(agentDir, dir);
48
+ if (!fs_1.default.existsSync(srcDir))
49
+ continue;
50
+ const entries = fs_1.default.readdirSync(srcDir, { withFileTypes: true });
51
+ for (const entry of entries) {
52
+ if (entry.name.startsWith('.'))
53
+ continue;
54
+ const srcPath = path_1.default.join(srcDir, entry.name);
55
+ const destDir = path_1.default.join(baseDir, dir);
56
+ const destPath = path_1.default.join(destDir, entry.name);
57
+ // 대상 디렉토리 생성
58
+ fs_1.default.mkdirSync(destDir, { recursive: true });
59
+ // 충돌 처리
60
+ if (fs_1.default.existsSync(destPath) || isSymlink(destPath)) {
61
+ if (isSymlink(destPath)) {
62
+ const existingTarget = fs_1.default.readlinkSync(destPath);
63
+ if (!existingTarget.includes('.relay/agents/') || existingTarget.startsWith(agentDir)) {
64
+ // 같은 에이전트 또는 relay가 아닌 symlink → 조용히 교체
65
+ }
66
+ else {
67
+ // 다른 에이전트의 symlink → 경고
68
+ result.warnings.push(`⚠ ${dir}/${entry.name} 가 다른 에이전트에서 교체됩니다`);
69
+ }
70
+ fs_1.default.unlinkSync(destPath);
71
+ }
72
+ else {
73
+ // 일반 파일/디렉토리 → 보호, 건너뜀
74
+ result.warnings.push(`⚠ ${destPath} 는 사용자 파일이므로 건너뜁니다`);
75
+ continue;
76
+ }
77
+ }
78
+ fs_1.default.symlinkSync(srcPath, destPath);
79
+ result.symlinks.push(destPath);
80
+ }
81
+ }
82
+ }
83
+ return result;
84
+ }
85
+ function isSymlink(p) {
86
+ try {
87
+ return fs_1.default.lstatSync(p).isSymbolicLink();
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
93
+ /**
94
+ * symlink 목록을 기반으로 symlink를 제거한다.
95
+ */
96
+ function removeSymlinks(symlinks) {
97
+ const removed = [];
98
+ for (const link of symlinks) {
99
+ try {
100
+ if (isSymlink(link)) {
101
+ fs_1.default.unlinkSync(link);
102
+ removed.push(link);
103
+ }
104
+ else if (fs_1.default.existsSync(link)) {
105
+ // symlink이 아닌 파일이면 건너뜀 (사용자 파일 보호)
106
+ }
107
+ }
108
+ catch {
109
+ // best-effort
110
+ }
111
+ }
112
+ return removed;
113
+ }
114
+ /**
115
+ * agentDir의 relay.yaml에서 requires를 읽고 체크 결과를 반환한다.
116
+ */
117
+ function checkRequires(agentDir) {
118
+ const results = [];
119
+ const yamlPath = path_1.default.join(agentDir, 'relay.yaml');
120
+ if (!fs_1.default.existsSync(yamlPath))
121
+ return results;
122
+ let requires;
123
+ try {
124
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
125
+ const yaml = require('js-yaml');
126
+ const raw = yaml.load(fs_1.default.readFileSync(yamlPath, 'utf-8'));
127
+ requires = (raw?.requires ?? undefined);
128
+ }
129
+ catch {
130
+ return results;
131
+ }
132
+ if (!requires)
133
+ return results;
134
+ // runtime
135
+ if (requires.runtime?.node) {
136
+ const ver = getCommandOutput('node --version');
137
+ if (ver) {
138
+ const clean = ver.replace(/^v/, '');
139
+ const required = requires.runtime.node.replace(/^>=?\s*/, '');
140
+ const ok = compareVersions(clean, required) >= 0;
141
+ results.push({
142
+ label: 'runtime',
143
+ status: ok ? 'ok' : 'warn',
144
+ message: ok
145
+ ? `Node.js >=${required} — ${ver} 확인됨`
146
+ : `Node.js >=${required} — ${ver} (업그레이드 필요)`,
147
+ });
148
+ }
149
+ }
150
+ if (requires.runtime?.python) {
151
+ const ver = getCommandOutput('python3 --version');
152
+ if (ver) {
153
+ const clean = ver.replace(/^Python\s*/, '');
154
+ const required = requires.runtime.python.replace(/^>=?\s*/, '');
155
+ const ok = compareVersions(clean, required) >= 0;
156
+ results.push({
157
+ label: 'runtime',
158
+ status: ok ? 'ok' : 'warn',
159
+ message: ok
160
+ ? `Python >=${required} — ${clean} 확인됨`
161
+ : `Python >=${required} — ${clean} (업그레이드 필요)`,
162
+ });
163
+ }
164
+ }
165
+ // cli
166
+ if (requires.cli) {
167
+ for (const cli of requires.cli) {
168
+ if (!isSafeName(cli.name))
169
+ continue;
170
+ const found = getCommandOutput('which', [cli.name]);
171
+ if (found) {
172
+ results.push({ label: 'cli', status: 'ok', message: `${cli.name} — 설치됨` });
173
+ }
174
+ else {
175
+ const installHint = cli.install ? ` → ${cli.install}` : '';
176
+ results.push({
177
+ label: 'cli',
178
+ status: cli.required !== false ? 'missing' : 'warn',
179
+ message: `${cli.name} — 미설치${installHint}`,
180
+ });
181
+ }
182
+ }
183
+ }
184
+ // env
185
+ if (requires.env) {
186
+ for (const env of requires.env) {
187
+ const val = process.env[env.name];
188
+ if (val) {
189
+ results.push({ label: 'env', status: 'ok', message: `${env.name} — 설정됨` });
190
+ }
191
+ else {
192
+ const desc = env.description ? ` (${env.description})` : '';
193
+ results.push({
194
+ label: 'env',
195
+ status: env.required !== false ? 'missing' : 'warn',
196
+ message: `${env.name} — 미설정${desc}`,
197
+ });
198
+ }
199
+ }
200
+ }
201
+ // npm
202
+ if (requires.npm) {
203
+ for (const pkg of requires.npm) {
204
+ const name = typeof pkg === 'string' ? pkg : pkg.name;
205
+ const isRequired = typeof pkg === 'string' ? true : pkg.required !== false;
206
+ if (!isSafeName(name))
207
+ continue;
208
+ const found = getCommandOutput('npm', ['list', name]);
209
+ const installed = found ? !found.includes('(empty)') && !found.includes('ERR') : false;
210
+ if (installed) {
211
+ results.push({ label: 'npm', status: 'ok', message: `${name} — 설치됨` });
212
+ }
213
+ else {
214
+ results.push({
215
+ label: 'npm',
216
+ status: isRequired ? 'missing' : 'warn',
217
+ message: `${name} — 미설치`,
218
+ });
219
+ }
220
+ }
221
+ }
222
+ // mcp
223
+ if (requires.mcp) {
224
+ for (const mcp of requires.mcp) {
225
+ const configStr = mcp.config
226
+ ? JSON.stringify(mcp.config, null, 2)
227
+ : mcp.package ?? mcp.name;
228
+ results.push({
229
+ label: 'mcp',
230
+ status: 'warn',
231
+ message: `${mcp.name} MCP — 설정 필요: ${configStr}`,
232
+ });
233
+ }
234
+ }
235
+ return results;
236
+ }
237
+ /**
238
+ * requires 체크 결과를 콘솔에 출력한다.
239
+ */
240
+ function printRequiresCheck(results) {
241
+ if (results.length === 0)
242
+ return;
243
+ console.log('\n\x1b[1m📋 Requirements\x1b[0m');
244
+ for (const r of results) {
245
+ const icon = r.status === 'ok' ? '✅' : r.status === 'warn' ? '⚠️ ' : '❌';
246
+ console.log(` ${icon} ${r.message}`);
247
+ }
248
+ const hasMissing = results.some((r) => r.status === 'missing');
249
+ if (hasMissing) {
250
+ console.log('\n \x1b[33m⚠️ 필수 요구사항이 충족되지 않았습니다. 에이전트 기능이 제한될 수 있습니다.\x1b[0m');
251
+ }
252
+ }
253
+ function getCommandOutput(cmd, args = []) {
254
+ try {
255
+ const full = args.length > 0 ? `${cmd} ${args.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ')}` : cmd;
256
+ return (0, child_process_1.execSync)(full, { encoding: 'utf-8', timeout: 5000 }).trim();
257
+ }
258
+ catch {
259
+ return null;
260
+ }
261
+ }
262
+ /** relay.yaml에서 온 이름이 안전한 식별자인지 확인 */
263
+ function isSafeName(name) {
264
+ return /^[a-zA-Z0-9._@/-]+$/.test(name);
265
+ }
266
+ function compareVersions(a, b) {
267
+ const pa = a.split('.').map(Number);
268
+ const pb = b.split('.').map(Number);
269
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
270
+ const na = pa[i] ?? 0;
271
+ const nb = pb[i] ?? 0;
272
+ if (na !== nb)
273
+ return na - nb;
274
+ }
275
+ return 0;
276
+ }
12
277
  function copyDirRecursive(src, dest) {
13
278
  const copiedFiles = [];
14
279
  if (!fs_1.default.existsSync(src))