relayax-cli 0.3.64 → 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.
- package/dist/commands/install.js +146 -41
- package/dist/commands/publish.js +59 -41
- package/dist/commands/uninstall.js +20 -4
- package/dist/commands/update.js +67 -31
- package/dist/lib/command-adapter.js +8 -9
- package/dist/lib/guide.d.ts +0 -1
- package/dist/lib/guide.js +27 -77
- package/dist/lib/installer.d.ts +32 -0
- package/dist/lib/installer.js +265 -0
- package/dist/mcp/server.js +23 -0
- package/dist/prompts/create.md +62 -0
- package/dist/prompts/explore.md +28 -0
- package/dist/prompts/index.d.ts +2 -7
- package/dist/prompts/index.js +4 -11
- package/dist/types.d.ts +3 -1
- package/package.json +1 -1
package/dist/commands/update.js
CHANGED
|
@@ -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
|
|
22
|
-
const
|
|
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
|
-
//
|
|
32
|
-
const currentEntry =
|
|
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
|
-
//
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
(0,
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
(
|
|
78
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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(`
|
|
96
|
-
console.log(`
|
|
97
|
-
// Show changelog
|
|
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
|
|
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
|
-
// ───
|
|
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-
|
|
99
|
-
description: 'relay
|
|
100
|
-
body:
|
|
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-
|
|
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-
|
|
214
|
-
description: '
|
|
215
|
-
body:
|
|
212
|
+
id: 'relay-create',
|
|
213
|
+
description: '에이전트 패키지를 새로 만들어 relay에 배포합니다',
|
|
214
|
+
body: index_js_1.CREATE_PROMPT,
|
|
216
215
|
},
|
|
217
216
|
];
|
|
218
217
|
// ─── Builder Commands (로컬 설치) ───
|
package/dist/lib/guide.d.ts
CHANGED
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
20
|
-
lines.push(`- npm: ${
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
lines.push(`-
|
|
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
|
|
61
|
-
const
|
|
62
|
-
|
|
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
|
-
|
|
83
|
-
각 Step의 명령어를 순서대로 실행하세요. 요약하지 말고 바로 Step 0(환경 감지)부터 시작하세요.
|
|
84
|
-
|
|
85
|
-
${index_js_1.SETUP_ENVIRONMENT}
|
|
36
|
+
## 설치
|
|
86
37
|
|
|
87
|
-
|
|
38
|
+
\`\`\`bash
|
|
39
|
+
${installCmd}
|
|
40
|
+
\`\`\`
|
|
88
41
|
|
|
89
|
-
|
|
42
|
+
${commands.length > 0 ? `## 포함된 커맨드
|
|
90
43
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
- 환경 B (샌드박스): \`npx relayax-cli install ${scopedSlug}\`
|
|
94
|
-
|
|
95
|
-
설치 후 아래 의존성을 확인하고 환경을 구성합니다.
|
|
44
|
+
${commands.map((cmd) => `- \`/${cmd.name}\`: ${cmd.description}`).join('\n')}
|
|
45
|
+
` : ''}${requiresSummary ? `## 요구사항
|
|
96
46
|
|
|
97
|
-
${
|
|
98
|
-
|
|
99
|
-
|
|
47
|
+
${requiresSummary}
|
|
48
|
+
` : ''}---
|
|
49
|
+
*https://relayax.com — Agent Marketplace*
|
|
100
50
|
`;
|
|
101
51
|
}
|
package/dist/lib/installer.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/lib/installer.js
CHANGED
|
@@ -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))
|