relayax-cli 0.3.65 → 0.3.67
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/init.d.ts +1 -5
- package/dist/commands/init.js +28 -6
- package/dist/commands/install.js +146 -41
- package/dist/commands/publish.js +103 -74
- 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/prompts/create.md +108 -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/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))
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
에이전트를 만들거나 업데이트하여 relay에 배포합니다.
|
|
2
|
+
relay.yaml이 없으면 새로 만들고, 있으면 변경사항을 반영합니다.
|
|
3
|
+
|
|
4
|
+
> 빌더는 터미널 환경에서 작업합니다. CLI 명령어를 직접 실행하세요.
|
|
5
|
+
|
|
6
|
+
## 분기: 최초 생성 vs 업데이트
|
|
7
|
+
|
|
8
|
+
`.relay/relay.yaml`이 있는지 확인합니다.
|
|
9
|
+
|
|
10
|
+
- **없음** → 아래 "최초 생성" 플로우
|
|
11
|
+
- **있음** → 아래 "업데이트" 플로우
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 최초 생성 (relay.yaml 없음)
|
|
16
|
+
|
|
17
|
+
### 1. 콘텐츠 파악
|
|
18
|
+
|
|
19
|
+
`relay package --init --json`으로 소스를 스캔합니다.
|
|
20
|
+
결과의 `sources[]`에서 사용자에게 어떤 콘텐츠를 포함할지 물어봅니다.
|
|
21
|
+
|
|
22
|
+
선택된 콘텐츠의 파일을 직접 읽어 기능을 파악합니다:
|
|
23
|
+
- SKILL.md, 에이전트 파일, 커맨드 파일의 내용
|
|
24
|
+
- 참조하는 스킬/에이전트 의존성
|
|
25
|
+
|
|
26
|
+
### 2. 포지셔닝
|
|
27
|
+
|
|
28
|
+
콘텐츠 분석을 기반으로 에이전트를 하나의 "제품"으로 포지셔닝합니다.
|
|
29
|
+
|
|
30
|
+
분석 관점:
|
|
31
|
+
- 이 에이전트가 **무엇을 하는** 에이전트인지
|
|
32
|
+
- 어떤 **기술 스택/도메인**에 특화되어 있는지
|
|
33
|
+
- 설치자에게 **어떤 가치**를 제공하는지
|
|
34
|
+
|
|
35
|
+
이름(name)은 한국어 가능. slug는 영문 소문자+하이픈.
|
|
36
|
+
설명은 설치자 관점으로 ("~를 자동화합니다").
|
|
37
|
+
|
|
38
|
+
### 3. requires 판단 + 보안 점검
|
|
39
|
+
|
|
40
|
+
콘텐츠 파일을 읽고 requires를 판단합니다:
|
|
41
|
+
|
|
42
|
+
- **env**: 환경변수 참조를 찾고 맥락에서 필수/선택 판단
|
|
43
|
+
- 핵심 로직에서 사용 → `required: true`
|
|
44
|
+
- 테스트/선택 기능에서 사용 → `required: false`
|
|
45
|
+
- **cli**: 외부 CLI 도구 참조 (playwright, ffmpeg 등)
|
|
46
|
+
- **npm**: import/require 패키지
|
|
47
|
+
- **mcp**: MCP 서버 참조 (supabase, github 등)
|
|
48
|
+
- **runtime**: Node.js/Python 최소 버전
|
|
49
|
+
- **agents**: 의존하는 다른 relay 에이전트
|
|
50
|
+
|
|
51
|
+
보안 점검:
|
|
52
|
+
- 하드코딩된 API 키, 토큰 (sk-*, ghp_*, AKIA* 등)
|
|
53
|
+
- 파일 컨텍스트를 읽어 실제 시크릿 vs 예시 코드를 구분
|
|
54
|
+
- 발견 시 **반드시 경고**하고 환경변수 대체 안내
|
|
55
|
+
|
|
56
|
+
### 4. relay.yaml 작성 & 배포
|
|
57
|
+
|
|
58
|
+
판단 결과를 relay.yaml에 반영합니다:
|
|
59
|
+
- name, slug, description, version, tags
|
|
60
|
+
- requires (판단 결과)
|
|
61
|
+
- org: `relay orgs list --json`으로 Org 목록을 조회합니다.
|
|
62
|
+
- Org가 있으면: 개인 배포 vs Org 배포를 사용자에게 물어봅니다.
|
|
63
|
+
- Org가 없으면: 개인 배포로 진행합니다.
|
|
64
|
+
- visibility: Org 선택 결과에 따라 옵션이 달라집니다:
|
|
65
|
+
- **Org 없이 배포**: `public`, `private` (2개)
|
|
66
|
+
- **Org에 배포**: `public`, `private`, `internal` (3개)
|
|
67
|
+
- `public` — 누구나 설치
|
|
68
|
+
- `private` — 접근 링크가 있는 사람만 설치
|
|
69
|
+
- `internal` — Org 멤버만 설치 (Org 배포 시에만 선택 가능)
|
|
70
|
+
|
|
71
|
+
`relay publish --json`으로 배포합니다.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 업데이트 (relay.yaml 있음)
|
|
76
|
+
|
|
77
|
+
### 1. 변경 사항 확인
|
|
78
|
+
|
|
79
|
+
`relay package --json`으로 현재 상태를 확인합니다.
|
|
80
|
+
- 변경된 콘텐츠 (modified)
|
|
81
|
+
- 새로 추가된 콘텐츠 (new_items)
|
|
82
|
+
|
|
83
|
+
사용자에게 어떤 부분을 변경하려는지 물어봅니다:
|
|
84
|
+
- 콘텐츠 변경 반영 (sync)
|
|
85
|
+
- 새 스킬/커맨드 추가
|
|
86
|
+
- 설명/태그 개선
|
|
87
|
+
- requires 재분석
|
|
88
|
+
|
|
89
|
+
### 2. 필요한 부분만 업데이트
|
|
90
|
+
|
|
91
|
+
사용자 요청에 따라:
|
|
92
|
+
- **콘텐츠 추가**: 새 콘텐츠의 파일을 읽고 기능 파악 → relay.yaml의 contents에 추가
|
|
93
|
+
- **requires 변경**: 콘텐츠를 다시 읽고 requires 재판단
|
|
94
|
+
- **설명 개선**: 현재 포지셔닝을 분석하고 개선안 제안
|
|
95
|
+
- **보안 재점검**: 시크릿/개인정보 확인
|
|
96
|
+
|
|
97
|
+
### 3. 배포
|
|
98
|
+
|
|
99
|
+
`relay publish --json`으로 배포합니다.
|
|
100
|
+
버전 범프가 필요하면 사용자에게 patch/minor/major 중 확인합니다.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 공유 문구
|
|
105
|
+
|
|
106
|
+
배포 완료 후 `relay publish` 출력에 포함된 공유 문구를 보여줍니다.
|
|
107
|
+
|
|
108
|
+
{{ERROR_HANDLING_GUIDE}}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
프로젝트에 맞는 에이전트를 찾아 추천하고 설치합니다.
|
|
2
|
+
|
|
3
|
+
## 프로젝트 분석
|
|
4
|
+
|
|
5
|
+
현재 프로젝트의 기술 스택, 구조, 패턴을 파악합니다:
|
|
6
|
+
- 사용 중인 프레임워크/라이브러리 (package.json, import 패턴)
|
|
7
|
+
- 프로젝트 구조 (디렉토리 레이아웃, 주요 파일)
|
|
8
|
+
- 기존 설치된 에이전트 (`relay list --json`)
|
|
9
|
+
|
|
10
|
+
## 에이전트 검색
|
|
11
|
+
|
|
12
|
+
`relay search <keyword>` 명령어로 검색합니다.
|
|
13
|
+
프로젝트 컨텍스트에 맞는 키워드를 선택합니다 (기술 스택, 작업 유형 등).
|
|
14
|
+
여러 키워드로 반복 검색하여 폭넓게 탐색합니다.
|
|
15
|
+
|
|
16
|
+
## 추천
|
|
17
|
+
|
|
18
|
+
검색 결과 중 프로젝트에 가장 적합한 에이전트를 추천합니다:
|
|
19
|
+
- 추천 이유를 프로젝트 컨텍스트와 연결하여 설명
|
|
20
|
+
- 에이전트의 주요 커맨드/기능을 요약
|
|
21
|
+
|
|
22
|
+
## 설치
|
|
23
|
+
|
|
24
|
+
사용자가 선택하면 `relay install <slug>` 을 실행합니다.
|
|
25
|
+
|
|
26
|
+
scope 결정 기준:
|
|
27
|
+
- 범용 도구 (코드 리뷰, 문서 생성, 테스트) → `relay install <slug>` (글로벌 기본)
|
|
28
|
+
- 프로젝트 특화 (특정 프레임워크, 팀 컨벤션) → `relay install <slug> --local`
|
package/dist/prompts/index.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
export declare const REQUIREMENTS_CHECK: string;
|
|
2
1
|
export declare const ERROR_HANDLING_GUIDE: string;
|
|
3
|
-
export declare const
|
|
4
|
-
export declare const
|
|
5
|
-
export declare const SETUP_LOGIN: string;
|
|
6
|
-
export declare const GUIDE_INSTRUCTION: string;
|
|
7
|
-
export declare const INSTALL_PROMPT: string;
|
|
8
|
-
export declare const PUBLISH_PROMPT: string;
|
|
2
|
+
export declare const EXPLORE_PROMPT: string;
|
|
3
|
+
export declare const CREATE_PROMPT: string;
|
package/dist/prompts/index.js
CHANGED
|
@@ -3,7 +3,7 @@ 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.
|
|
6
|
+
exports.CREATE_PROMPT = exports.EXPLORE_PROMPT = exports.ERROR_HANDLING_GUIDE = void 0;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
function readPrompt(filename) {
|
|
@@ -13,17 +13,10 @@ function interpolate(template, vars) {
|
|
|
13
13
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
|
|
14
14
|
}
|
|
15
15
|
// ─── 공유 조각 ───
|
|
16
|
-
exports.REQUIREMENTS_CHECK = readPrompt('_requirements-check.md');
|
|
17
16
|
exports.ERROR_HANDLING_GUIDE = readPrompt('_error-handling.md');
|
|
18
|
-
exports.SETUP_ENVIRONMENT = readPrompt('_setup-environment.md');
|
|
19
|
-
exports.SETUP_CLI = readPrompt('_setup-cli.md');
|
|
20
|
-
exports.SETUP_LOGIN = readPrompt('_setup-login.md');
|
|
21
|
-
exports.GUIDE_INSTRUCTION = readPrompt('_guide-instruction.md');
|
|
22
17
|
const fragments = {
|
|
23
|
-
REQUIREMENTS_CHECK: exports.REQUIREMENTS_CHECK,
|
|
24
18
|
ERROR_HANDLING_GUIDE: exports.ERROR_HANDLING_GUIDE,
|
|
25
|
-
GUIDE_INSTRUCTION: exports.GUIDE_INSTRUCTION,
|
|
26
19
|
};
|
|
27
|
-
// ───
|
|
28
|
-
exports.
|
|
29
|
-
exports.
|
|
20
|
+
// ─── 프롬프트 ───
|
|
21
|
+
exports.EXPLORE_PROMPT = interpolate(readPrompt('explore.md'), fragments);
|
|
22
|
+
exports.CREATE_PROMPT = interpolate(readPrompt('create.md'), fragments);
|
package/dist/types.d.ts
CHANGED
|
@@ -14,8 +14,10 @@ export interface InstalledAgent {
|
|
|
14
14
|
org_slug?: string;
|
|
15
15
|
/** 배치 범위 — 에이전트가 relay deploy-record로 기록 */
|
|
16
16
|
deploy_scope?: 'global' | 'local';
|
|
17
|
-
/** 배치된 파일 절대경로 목록 — 에이전트가 relay deploy-record로 기록 */
|
|
17
|
+
/** 배치된 파일 절대경로 목록 — 에이전트가 relay deploy-record로 기록 (legacy) */
|
|
18
18
|
deployed_files?: string[];
|
|
19
|
+
/** symlink 절대경로 목록 — relay install이 직접 기록 */
|
|
20
|
+
deployed_symlinks?: string[];
|
|
19
21
|
}
|
|
20
22
|
/** 키는 scoped slug 포맷: "@owner/name" */
|
|
21
23
|
export interface InstalledRegistry {
|