relayax-cli 0.1.3 → 0.1.6
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.js +134 -105
- package/dist/commands/install.js +6 -6
- package/dist/commands/list.js +5 -5
- package/dist/commands/login.js +5 -5
- package/dist/commands/publish.js +149 -10
- package/dist/commands/search.js +5 -5
- package/dist/commands/uninstall.js +8 -8
- package/dist/index.js +1 -1
- package/dist/lib/ai-tools.d.ts +14 -0
- package/dist/lib/ai-tools.js +45 -0
- package/dist/lib/command-adapter.d.ts +21 -0
- package/dist/lib/command-adapter.js +134 -0
- package/dist/lib/config.d.ts +4 -2
- package/dist/lib/config.js +11 -3
- package/package.json +12 -2
package/dist/commands/init.js
CHANGED
|
@@ -6,130 +6,159 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.registerInit = registerInit;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
.replace(/^-|-$/g, '');
|
|
9
|
+
const ai_tools_js_1 = require("../lib/ai-tools.js");
|
|
10
|
+
const command_adapter_js_1 = require("../lib/command-adapter.js");
|
|
11
|
+
const VALID_TEAM_DIRS = ['skills', 'agents', 'rules', 'commands'];
|
|
12
|
+
function resolveTools(toolsArg) {
|
|
13
|
+
const raw = toolsArg.trim().toLowerCase();
|
|
14
|
+
if (raw === 'all') {
|
|
15
|
+
return ai_tools_js_1.AI_TOOLS.map((t) => t.value);
|
|
16
|
+
}
|
|
17
|
+
const tokens = raw.split(',').map((t) => t.trim()).filter(Boolean);
|
|
18
|
+
const valid = new Set(ai_tools_js_1.AI_TOOLS.map((t) => t.value));
|
|
19
|
+
const invalid = tokens.filter((t) => !valid.has(t));
|
|
20
|
+
if (invalid.length > 0) {
|
|
21
|
+
throw new Error(`알 수 없는 도구: ${invalid.join(', ')}\n사용 가능: ${[...valid].join(', ')}`);
|
|
22
|
+
}
|
|
23
|
+
return tokens;
|
|
25
24
|
}
|
|
26
|
-
function
|
|
25
|
+
function showWelcome() {
|
|
27
26
|
const lines = [
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
'',
|
|
28
|
+
' \x1b[33m⚡\x1b[0m \x1b[1mrelay\x1b[0m — Agent Team Marketplace',
|
|
29
|
+
'',
|
|
30
|
+
' 에이전트 CLI에 relay 커맨드를 연결합니다.',
|
|
31
|
+
' 설치 후 에이전트가 팀을 탐색하고 설치할 수 있습니다.',
|
|
32
|
+
'',
|
|
33
|
+
' \x1b[2m/relay-explore\x1b[0m 마켓플레이스 탐색',
|
|
34
|
+
' \x1b[2m/relay-install\x1b[0m 팀 설치',
|
|
35
|
+
' \x1b[2m/relay-publish\x1b[0m 팀 배포',
|
|
36
|
+
'',
|
|
32
37
|
];
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
console.log(lines.join('\n'));
|
|
39
|
+
}
|
|
40
|
+
async function selectToolsInteractively(detectedIds) {
|
|
41
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
42
|
+
const choices = ai_tools_js_1.AI_TOOLS.map((tool) => {
|
|
43
|
+
const detected = detectedIds.has(tool.value);
|
|
44
|
+
return {
|
|
45
|
+
name: detected ? `${tool.name} \x1b[32m(detected)\x1b[0m` : tool.name,
|
|
46
|
+
value: tool.value,
|
|
47
|
+
checked: detected,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
const selected = await checkbox({
|
|
51
|
+
message: `연결할 에이전트 CLI를 선택하세요`,
|
|
52
|
+
choices,
|
|
53
|
+
pageSize: 8,
|
|
54
|
+
});
|
|
55
|
+
return selected;
|
|
43
56
|
}
|
|
44
57
|
function registerInit(program) {
|
|
45
58
|
program
|
|
46
59
|
.command('init')
|
|
47
|
-
.description('
|
|
48
|
-
.option('--
|
|
49
|
-
.option('--slug <slug>', 'URL 슬러그')
|
|
50
|
-
.option('--description <desc>', '한 줄 설명')
|
|
60
|
+
.description('에이전트 CLI를 감지하고 relay 슬래시 커맨드를 설치합니다')
|
|
61
|
+
.option('--tools <tools>', '설치할 에이전트 CLI 지정 (all 또는 쉼표 구분)')
|
|
51
62
|
.action(async (opts) => {
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
const json = program.opts().json ?? false;
|
|
64
|
+
const projectPath = process.cwd();
|
|
65
|
+
const detected = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
|
|
66
|
+
const detectedIds = new Set(detected.map((t) => t.value));
|
|
67
|
+
// 1. 도구 선택
|
|
68
|
+
let targetToolIds;
|
|
69
|
+
if (opts.tools) {
|
|
70
|
+
// --tools 옵션: 비대화형
|
|
71
|
+
targetToolIds = resolveTools(opts.tools);
|
|
72
|
+
}
|
|
73
|
+
else if (!json && process.stdin.isTTY) {
|
|
74
|
+
// 기본(human) + TTY: 대화형 UI
|
|
75
|
+
showWelcome();
|
|
76
|
+
if (detected.length > 0) {
|
|
77
|
+
console.log(` 감지된 에이전트 CLI: \x1b[36m${detected.map((t) => t.name).join(', ')}\x1b[0m\n`);
|
|
59
78
|
}
|
|
60
|
-
|
|
61
|
-
|
|
79
|
+
targetToolIds = await selectToolsInteractively(detectedIds);
|
|
80
|
+
if (targetToolIds.length === 0) {
|
|
81
|
+
console.log('\n 선택된 도구가 없습니다.');
|
|
82
|
+
return;
|
|
62
83
|
}
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
// Detect existing directories
|
|
66
|
-
const existing = DIRS.filter((d) => fs_1.default.existsSync(path_1.default.join(cwd, d)));
|
|
67
|
-
const missing = DIRS.filter((d) => !fs_1.default.existsSync(path_1.default.join(cwd, d)));
|
|
68
|
-
const dirName = path_1.default.basename(cwd);
|
|
69
|
-
const autoSlug = slugify(dirName);
|
|
70
|
-
let name;
|
|
71
|
-
let slug;
|
|
72
|
-
let description;
|
|
73
|
-
let tags = [];
|
|
74
|
-
if (opts.name && opts.slug && opts.description) {
|
|
75
|
-
// Non-interactive
|
|
76
|
-
name = opts.name;
|
|
77
|
-
slug = opts.slug;
|
|
78
|
-
description = opts.description;
|
|
79
84
|
}
|
|
80
85
|
else {
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
console.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
86
|
+
// --json 모드 또는 비TTY (에이전트가 호출): 감지된 것만 자동 사용
|
|
87
|
+
if (detected.length === 0) {
|
|
88
|
+
console.error(JSON.stringify({
|
|
89
|
+
error: 'NO_AGENT_CLI',
|
|
90
|
+
message: '에이전트 CLI 디렉토리를 찾을 수 없습니다. --tools 옵션으로 지정하세요.',
|
|
91
|
+
}));
|
|
92
|
+
process.exit(1);
|
|
88
93
|
}
|
|
89
|
-
|
|
90
|
-
slug = opts.slug ?? await prompt(rl, '슬러그', autoSlug);
|
|
91
|
-
description = opts.description ?? await prompt(rl, '한 줄 설명');
|
|
92
|
-
const tagsInput = await prompt(rl, '태그 (쉼표 구분)', '');
|
|
93
|
-
tags = tagsInput ? tagsInput.split(',').map((t) => t.trim()).filter(Boolean) : [];
|
|
94
|
-
rl.close();
|
|
94
|
+
targetToolIds = detected.map((t) => t.value);
|
|
95
95
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
// 2. 각 에이전트 CLI에 슬래시 커맨드 설치
|
|
97
|
+
const results = [];
|
|
98
|
+
for (const toolId of targetToolIds) {
|
|
99
|
+
const tool = ai_tools_js_1.AI_TOOLS.find((t) => t.value === toolId);
|
|
100
|
+
if (!tool)
|
|
101
|
+
continue;
|
|
102
|
+
const adapter = (0, command_adapter_js_1.createAdapter)(tool);
|
|
103
|
+
const installedCommands = [];
|
|
104
|
+
for (const cmd of command_adapter_js_1.RELAY_COMMANDS) {
|
|
105
|
+
const filePath = path_1.default.join(projectPath, adapter.getFilePath(cmd.id));
|
|
106
|
+
const fileContent = adapter.formatFile(cmd);
|
|
107
|
+
fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
|
|
108
|
+
fs_1.default.writeFileSync(filePath, fileContent);
|
|
109
|
+
installedCommands.push(cmd.id);
|
|
110
|
+
}
|
|
111
|
+
results.push({ tool: tool.name, commands: installedCommands });
|
|
99
112
|
}
|
|
100
|
-
//
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
fs_1.default.writeFileSync(path_1.default.join(cwd, dir, '.gitkeep'), '');
|
|
106
|
-
created.push(dir);
|
|
113
|
+
// 3. relay.yaml 생성 (팀 패키지 구조가 있는 경우)
|
|
114
|
+
const relayYamlPath = path_1.default.join(projectPath, 'relay.yaml');
|
|
115
|
+
let relayYamlStatus = 'skipped';
|
|
116
|
+
if (fs_1.default.existsSync(relayYamlPath)) {
|
|
117
|
+
relayYamlStatus = 'exists';
|
|
107
118
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
else {
|
|
120
|
+
const hasTeamDirs = VALID_TEAM_DIRS.some((d) => {
|
|
121
|
+
const dirPath = path_1.default.join(projectPath, d);
|
|
122
|
+
if (!fs_1.default.existsSync(dirPath))
|
|
123
|
+
return false;
|
|
124
|
+
return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.')).length > 0;
|
|
125
|
+
});
|
|
126
|
+
if (hasTeamDirs) {
|
|
127
|
+
const dirName = path_1.default.basename(projectPath);
|
|
128
|
+
const yaml = [
|
|
129
|
+
`name: "${dirName}"`,
|
|
130
|
+
`slug: "${dirName}"`,
|
|
131
|
+
`description: ""`,
|
|
132
|
+
`version: "1.0.0"`,
|
|
133
|
+
`tags: []`,
|
|
134
|
+
].join('\n') + '\n';
|
|
135
|
+
fs_1.default.writeFileSync(relayYamlPath, yaml);
|
|
136
|
+
relayYamlStatus = 'created';
|
|
125
137
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
console.log(
|
|
138
|
+
}
|
|
139
|
+
// 4. 출력
|
|
140
|
+
if (json) {
|
|
141
|
+
console.log(JSON.stringify({
|
|
142
|
+
status: 'ok',
|
|
143
|
+
tools: results,
|
|
144
|
+
relay_yaml: relayYamlStatus,
|
|
145
|
+
}));
|
|
130
146
|
}
|
|
131
147
|
else {
|
|
132
|
-
console.log(
|
|
148
|
+
console.log('\n\x1b[32m✓ relay 초기화 완료\x1b[0m\n');
|
|
149
|
+
for (const r of results) {
|
|
150
|
+
console.log(` \x1b[36m${r.tool}\x1b[0m`);
|
|
151
|
+
for (const cmd of r.commands) {
|
|
152
|
+
console.log(` /${cmd}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (relayYamlStatus === 'created') {
|
|
156
|
+
console.log(`\n relay.yaml 생성됨 (팀 패키지 구조 감지)`);
|
|
157
|
+
}
|
|
158
|
+
else if (relayYamlStatus === 'exists') {
|
|
159
|
+
console.log(`\n relay.yaml 이미 존재`);
|
|
160
|
+
}
|
|
161
|
+
console.log('\n IDE를 재시작하면 슬래시 커맨드가 활성화됩니다.');
|
|
133
162
|
}
|
|
134
163
|
});
|
|
135
164
|
}
|
package/dist/commands/install.js
CHANGED
|
@@ -8,10 +8,10 @@ const config_js_1 = require("../lib/config.js");
|
|
|
8
8
|
function registerInstall(program) {
|
|
9
9
|
program
|
|
10
10
|
.command('install <slug>')
|
|
11
|
-
.description('에이전트 팀 설치 (
|
|
11
|
+
.description('에이전트 팀 설치 (감지된 에이전트 CLI에 설치)')
|
|
12
12
|
.option('--path <install_path>', '설치 경로 지정 (기본: ./.claude)')
|
|
13
13
|
.action(async (slug, opts) => {
|
|
14
|
-
const
|
|
14
|
+
const json = program.opts().json ?? false;
|
|
15
15
|
const installPath = (0, config_js_1.getInstallPath)(opts.path);
|
|
16
16
|
const tempDir = (0, storage_js_1.makeTempDir)();
|
|
17
17
|
try {
|
|
@@ -43,7 +43,10 @@ function registerInstall(program) {
|
|
|
43
43
|
files_installed: files.length,
|
|
44
44
|
install_path: installPath,
|
|
45
45
|
};
|
|
46
|
-
if (
|
|
46
|
+
if (json) {
|
|
47
|
+
console.log(JSON.stringify(result));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
47
50
|
console.log(`\n\x1b[32m✓ ${team.name} 설치 완료\x1b[0m v${team.version}`);
|
|
48
51
|
console.log(` 설치 위치: \x1b[36m${installPath}\x1b[0m`);
|
|
49
52
|
console.log(` 파일 수: ${files.length}개`);
|
|
@@ -54,9 +57,6 @@ function registerInstall(program) {
|
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
|
-
else {
|
|
58
|
-
console.log(JSON.stringify(result));
|
|
59
|
-
}
|
|
60
60
|
}
|
|
61
61
|
catch (err) {
|
|
62
62
|
const message = err instanceof Error ? err.message : String(err);
|
package/dist/commands/list.js
CHANGED
|
@@ -7,7 +7,7 @@ function registerList(program) {
|
|
|
7
7
|
.command('list')
|
|
8
8
|
.description('설치된 에이전트 팀 목록')
|
|
9
9
|
.action(() => {
|
|
10
|
-
const
|
|
10
|
+
const json = program.opts().json ?? false;
|
|
11
11
|
const installed = (0, config_js_1.loadInstalled)();
|
|
12
12
|
const entries = Object.entries(installed);
|
|
13
13
|
const installedList = entries.map(([slug, info]) => ({
|
|
@@ -16,7 +16,10 @@ function registerList(program) {
|
|
|
16
16
|
installed_at: info.installed_at,
|
|
17
17
|
files: info.files.length,
|
|
18
18
|
}));
|
|
19
|
-
if (
|
|
19
|
+
if (json) {
|
|
20
|
+
console.log(JSON.stringify({ installed: installedList }));
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
20
23
|
if (installedList.length === 0) {
|
|
21
24
|
console.log('\n설치된 팀이 없습니다. `relay install <slug>`로 설치하세요.');
|
|
22
25
|
return;
|
|
@@ -27,8 +30,5 @@ function registerList(program) {
|
|
|
27
30
|
console.log(` \x1b[36m${item.slug}\x1b[0m v${item.version} (${date}) 파일 ${item.files}개`);
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
|
-
else {
|
|
31
|
-
console.log(JSON.stringify({ installed: installedList }));
|
|
32
|
-
}
|
|
33
33
|
});
|
|
34
34
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -95,7 +95,7 @@ function registerLogin(program) {
|
|
|
95
95
|
.description('RelayAX 계정에 로그인합니다')
|
|
96
96
|
.option('--token <token>', '직접 토큰 입력 (브라우저 없이)')
|
|
97
97
|
.action(async (opts) => {
|
|
98
|
-
const
|
|
98
|
+
const json = program.opts().json ?? false;
|
|
99
99
|
(0, config_js_1.ensureRelayDir)();
|
|
100
100
|
let token = opts.token;
|
|
101
101
|
if (!token) {
|
|
@@ -119,13 +119,13 @@ function registerLogin(program) {
|
|
|
119
119
|
message: '로그인 성공',
|
|
120
120
|
...(user ? { email: user.email } : {}),
|
|
121
121
|
};
|
|
122
|
-
if (
|
|
122
|
+
if (json) {
|
|
123
|
+
console.log(JSON.stringify(result));
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
123
126
|
console.log(`\x1b[32m✓ 로그인 완료\x1b[0m`);
|
|
124
127
|
if (user?.email)
|
|
125
128
|
console.log(` 계정: \x1b[36m${user.email}\x1b[0m`);
|
|
126
129
|
}
|
|
127
|
-
else {
|
|
128
|
-
console.log(JSON.stringify(result));
|
|
129
|
-
}
|
|
130
130
|
});
|
|
131
131
|
}
|
package/dist/commands/publish.js
CHANGED
|
@@ -10,12 +10,30 @@ const os_1 = __importDefault(require("os"));
|
|
|
10
10
|
const tar_1 = require("tar");
|
|
11
11
|
const config_js_1 = require("../lib/config.js");
|
|
12
12
|
const VALID_DIRS = ['skills', 'agents', 'rules', 'commands'];
|
|
13
|
+
const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.webp'];
|
|
13
14
|
function parseRelayYaml(content) {
|
|
14
15
|
const result = {};
|
|
15
16
|
const tags = [];
|
|
17
|
+
const portfolio = [];
|
|
16
18
|
let inTags = false;
|
|
19
|
+
let inPortfolio = false;
|
|
20
|
+
let inLongDesc = false;
|
|
21
|
+
let longDescLines = [];
|
|
22
|
+
let currentPortfolioItem = null;
|
|
17
23
|
for (const line of content.split('\n')) {
|
|
18
24
|
const trimmed = line.trim();
|
|
25
|
+
// long_description multiline (YAML | block)
|
|
26
|
+
if (inLongDesc) {
|
|
27
|
+
if (line.startsWith(' ') || line.startsWith('\t') || trimmed === '') {
|
|
28
|
+
longDescLines.push(line.replace(/^ {2}/, '').replace(/^\t/, ''));
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
inLongDesc = false;
|
|
33
|
+
result.long_description = longDescLines.join('\n').trim();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// tags list
|
|
19
37
|
if (inTags) {
|
|
20
38
|
if (trimmed.startsWith('- ')) {
|
|
21
39
|
tags.push(trimmed.slice(2).replace(/^["']|["']$/g, ''));
|
|
@@ -25,6 +43,35 @@ function parseRelayYaml(content) {
|
|
|
25
43
|
inTags = false;
|
|
26
44
|
}
|
|
27
45
|
}
|
|
46
|
+
// portfolio list
|
|
47
|
+
if (inPortfolio) {
|
|
48
|
+
if (trimmed.startsWith('- path:')) {
|
|
49
|
+
if (currentPortfolioItem?.path) {
|
|
50
|
+
portfolio.push({ path: currentPortfolioItem.path, title: currentPortfolioItem.title ?? '', description: currentPortfolioItem.description });
|
|
51
|
+
}
|
|
52
|
+
currentPortfolioItem = { path: trimmed.slice(8).replace(/^["']|["']$/g, '').trim() };
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (trimmed.startsWith('title:') && currentPortfolioItem) {
|
|
56
|
+
currentPortfolioItem.title = trimmed.slice(6).replace(/^["']|["']$/g, '').trim();
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (trimmed.startsWith('description:') && currentPortfolioItem) {
|
|
60
|
+
currentPortfolioItem.description = trimmed.slice(12).replace(/^["']|["']$/g, '').trim();
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (!trimmed.startsWith('-') && !trimmed.startsWith('title:') && !trimmed.startsWith('description:') && trimmed !== '') {
|
|
64
|
+
// End of portfolio section
|
|
65
|
+
if (currentPortfolioItem?.path) {
|
|
66
|
+
portfolio.push({ path: currentPortfolioItem.path, title: currentPortfolioItem.title ?? '', description: currentPortfolioItem.description });
|
|
67
|
+
}
|
|
68
|
+
currentPortfolioItem = null;
|
|
69
|
+
inPortfolio = false;
|
|
70
|
+
}
|
|
71
|
+
else if (trimmed === '') {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
28
75
|
if (trimmed === 'tags: []') {
|
|
29
76
|
result.tags = [];
|
|
30
77
|
continue;
|
|
@@ -33,17 +80,38 @@ function parseRelayYaml(content) {
|
|
|
33
80
|
inTags = true;
|
|
34
81
|
continue;
|
|
35
82
|
}
|
|
83
|
+
if (trimmed === 'portfolio:') {
|
|
84
|
+
inPortfolio = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (trimmed === 'portfolio: []') {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (trimmed === 'long_description: |') {
|
|
91
|
+
inLongDesc = true;
|
|
92
|
+
longDescLines = [];
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
36
95
|
const match = trimmed.match(/^(\w+):\s*["']?(.+?)["']?$/);
|
|
37
96
|
if (match) {
|
|
38
97
|
result[match[1]] = match[2];
|
|
39
98
|
}
|
|
40
99
|
}
|
|
100
|
+
// Flush remaining
|
|
101
|
+
if (inLongDesc && longDescLines.length > 0) {
|
|
102
|
+
result.long_description = longDescLines.join('\n').trim();
|
|
103
|
+
}
|
|
104
|
+
if (currentPortfolioItem?.path) {
|
|
105
|
+
portfolio.push({ path: currentPortfolioItem.path, title: currentPortfolioItem.title ?? '', description: currentPortfolioItem.description });
|
|
106
|
+
}
|
|
41
107
|
return {
|
|
42
108
|
name: String(result.name ?? ''),
|
|
43
109
|
slug: String(result.slug ?? ''),
|
|
44
110
|
description: String(result.description ?? ''),
|
|
45
111
|
version: String(result.version ?? '1.0.0'),
|
|
112
|
+
long_description: result.long_description,
|
|
46
113
|
tags,
|
|
114
|
+
portfolio,
|
|
47
115
|
};
|
|
48
116
|
}
|
|
49
117
|
function detectCommands(teamDir) {
|
|
@@ -84,9 +152,51 @@ function countDir(teamDir, dirName) {
|
|
|
84
152
|
return 0;
|
|
85
153
|
return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.')).length;
|
|
86
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* 포트폴리오 이미지를 수집한다.
|
|
157
|
+
* 1. relay.yaml에 portfolio 섹션이 있으면 사용
|
|
158
|
+
* 2. 없으면 ./portfolio/ 디렉토리 자동 스캔
|
|
159
|
+
*/
|
|
160
|
+
function collectPortfolio(teamDir, yamlPortfolio) {
|
|
161
|
+
if (yamlPortfolio.length > 0) {
|
|
162
|
+
return yamlPortfolio.filter((p) => {
|
|
163
|
+
const absPath = path_1.default.resolve(teamDir, p.path);
|
|
164
|
+
return fs_1.default.existsSync(absPath);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// Auto-scan ./portfolio/
|
|
168
|
+
const portfolioDir = path_1.default.join(teamDir, 'portfolio');
|
|
169
|
+
if (!fs_1.default.existsSync(portfolioDir))
|
|
170
|
+
return [];
|
|
171
|
+
const files = fs_1.default.readdirSync(portfolioDir)
|
|
172
|
+
.filter((f) => IMAGE_EXTS.some((ext) => f.toLowerCase().endsWith(ext)))
|
|
173
|
+
.sort();
|
|
174
|
+
return files.map((f) => ({
|
|
175
|
+
path: path_1.default.join('portfolio', f),
|
|
176
|
+
title: path_1.default.basename(f, path_1.default.extname(f)).replace(/[-_]/g, ' '),
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* long_description을 결정한다.
|
|
181
|
+
* 1. relay.yaml에 있으면 사용
|
|
182
|
+
* 2. README.md가 있으면 fallback
|
|
183
|
+
*/
|
|
184
|
+
function resolveLongDescription(teamDir, yamlValue) {
|
|
185
|
+
if (yamlValue)
|
|
186
|
+
return yamlValue;
|
|
187
|
+
const readmePath = path_1.default.join(teamDir, 'README.md');
|
|
188
|
+
if (fs_1.default.existsSync(readmePath)) {
|
|
189
|
+
try {
|
|
190
|
+
return fs_1.default.readFileSync(readmePath, 'utf-8').trim() || undefined;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
87
198
|
async function createTarball(teamDir) {
|
|
88
199
|
const tmpFile = path_1.default.join(os_1.default.tmpdir(), `relay-publish-${Date.now()}.tar.gz`);
|
|
89
|
-
// Only include valid dirs that exist
|
|
90
200
|
const dirsToInclude = VALID_DIRS.filter((d) => fs_1.default.existsSync(path_1.default.join(teamDir, d)));
|
|
91
201
|
await (0, tar_1.create)({
|
|
92
202
|
gzip: true,
|
|
@@ -95,12 +205,31 @@ async function createTarball(teamDir) {
|
|
|
95
205
|
}, [...dirsToInclude]);
|
|
96
206
|
return tmpFile;
|
|
97
207
|
}
|
|
98
|
-
async function publishToApi(token, tarPath, metadata) {
|
|
208
|
+
async function publishToApi(token, tarPath, metadata, teamDir, portfolioEntries) {
|
|
99
209
|
const fileBuffer = fs_1.default.readFileSync(tarPath);
|
|
100
210
|
const blob = new Blob([fileBuffer], { type: 'application/gzip' });
|
|
101
211
|
const form = new FormData();
|
|
102
212
|
form.append('package', blob, `${metadata.slug}-${metadata.version}.tar.gz`);
|
|
103
213
|
form.append('metadata', JSON.stringify(metadata));
|
|
214
|
+
// Attach portfolio images
|
|
215
|
+
if (portfolioEntries.length > 0) {
|
|
216
|
+
const portfolioMeta = [];
|
|
217
|
+
for (let i = 0; i < portfolioEntries.length; i++) {
|
|
218
|
+
const entry = portfolioEntries[i];
|
|
219
|
+
const absPath = path_1.default.resolve(teamDir, entry.path);
|
|
220
|
+
const imgBuffer = fs_1.default.readFileSync(absPath);
|
|
221
|
+
const ext = path_1.default.extname(entry.path).slice(1) || 'png';
|
|
222
|
+
const mimeType = ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : ext === 'webp' ? 'image/webp' : 'image/png';
|
|
223
|
+
const imgBlob = new Blob([imgBuffer], { type: mimeType });
|
|
224
|
+
form.append(`portfolio[${i}]`, imgBlob, path_1.default.basename(entry.path));
|
|
225
|
+
portfolioMeta.push({
|
|
226
|
+
title: entry.title,
|
|
227
|
+
description: entry.description,
|
|
228
|
+
sort_order: i,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
form.append('portfolio_meta', JSON.stringify(portfolioMeta));
|
|
232
|
+
}
|
|
104
233
|
const res = await fetch(`${config_js_1.API_URL}/api/publish`, {
|
|
105
234
|
method: 'POST',
|
|
106
235
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -119,7 +248,7 @@ function registerPublish(program) {
|
|
|
119
248
|
.description('현재 팀 패키지를 마켓플레이스에 배포합니다 (relay.yaml 필요)')
|
|
120
249
|
.option('--token <token>', '인증 토큰')
|
|
121
250
|
.action(async (opts) => {
|
|
122
|
-
const
|
|
251
|
+
const json = program.opts().json ?? false;
|
|
123
252
|
const teamDir = process.cwd();
|
|
124
253
|
const relayYamlPath = path_1.default.join(teamDir, 'relay.yaml');
|
|
125
254
|
// Check relay.yaml exists
|
|
@@ -169,32 +298,42 @@ function registerPublish(program) {
|
|
|
169
298
|
rules: countDir(teamDir, 'rules'),
|
|
170
299
|
skills: countDir(teamDir, 'skills'),
|
|
171
300
|
};
|
|
301
|
+
// Collect portfolio and long_description
|
|
302
|
+
const portfolioEntries = collectPortfolio(teamDir, config.portfolio);
|
|
303
|
+
const longDescription = resolveLongDescription(teamDir, config.long_description);
|
|
172
304
|
const metadata = {
|
|
173
305
|
slug: config.slug,
|
|
174
306
|
name: config.name,
|
|
175
307
|
description: config.description,
|
|
308
|
+
long_description: longDescription,
|
|
176
309
|
tags: config.tags,
|
|
177
310
|
commands: detectedCommands,
|
|
178
311
|
components,
|
|
179
312
|
version: config.version,
|
|
180
313
|
};
|
|
181
|
-
if (
|
|
314
|
+
if (!json) {
|
|
182
315
|
console.error(`패키지 생성 중... (${config.name} v${config.version})`);
|
|
316
|
+
if (portfolioEntries.length > 0) {
|
|
317
|
+
console.error(`포트폴리오 이미지: ${portfolioEntries.length}개`);
|
|
318
|
+
}
|
|
183
319
|
}
|
|
184
320
|
let tarPath = null;
|
|
185
321
|
try {
|
|
186
322
|
tarPath = await createTarball(teamDir);
|
|
187
|
-
if (
|
|
323
|
+
if (!json) {
|
|
188
324
|
console.error(`업로드 중...`);
|
|
189
325
|
}
|
|
190
|
-
const result = await publishToApi(token, tarPath, metadata);
|
|
191
|
-
if (
|
|
326
|
+
const result = await publishToApi(token, tarPath, metadata, teamDir, portfolioEntries);
|
|
327
|
+
if (json) {
|
|
328
|
+
console.log(JSON.stringify(result));
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
192
331
|
console.log(`\n\x1b[32m✓ ${config.name} 배포 완료\x1b[0m v${result.version}`);
|
|
193
332
|
console.log(` 슬러그: \x1b[36m${result.slug}\x1b[0m`);
|
|
194
333
|
console.log(` URL: \x1b[36m${result.url}\x1b[0m`);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
334
|
+
if (result.portfolio_count && result.portfolio_count > 0) {
|
|
335
|
+
console.log(` 포트폴리오: ${result.portfolio_count}개 이미지 업로드됨`);
|
|
336
|
+
}
|
|
198
337
|
}
|
|
199
338
|
}
|
|
200
339
|
catch (err) {
|
package/dist/commands/search.js
CHANGED
|
@@ -29,17 +29,17 @@ function registerSearch(program) {
|
|
|
29
29
|
.description('에이전트 팀 검색')
|
|
30
30
|
.option('--tag <tag>', '태그로 필터링')
|
|
31
31
|
.action(async (keyword, opts) => {
|
|
32
|
-
const
|
|
32
|
+
const json = program.opts().json ?? false;
|
|
33
33
|
try {
|
|
34
34
|
const results = await (0, api_js_1.searchTeams)(keyword, opts.tag);
|
|
35
|
-
if (
|
|
35
|
+
if (json) {
|
|
36
|
+
console.log(JSON.stringify({ results }));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
36
39
|
console.log(`\n검색어: \x1b[36m${keyword}\x1b[0m${opts.tag ? ` 태그: \x1b[33m${opts.tag}\x1b[0m` : ''}\n`);
|
|
37
40
|
console.log(formatTable(results));
|
|
38
41
|
console.log(`\n총 ${results.length}건`);
|
|
39
42
|
}
|
|
40
|
-
else {
|
|
41
|
-
console.log(JSON.stringify({ results }));
|
|
42
|
-
}
|
|
43
43
|
}
|
|
44
44
|
catch (err) {
|
|
45
45
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -8,15 +8,15 @@ function registerUninstall(program) {
|
|
|
8
8
|
.command('uninstall <slug>')
|
|
9
9
|
.description('에이전트 팀 제거')
|
|
10
10
|
.action((slug) => {
|
|
11
|
-
const
|
|
11
|
+
const json = program.opts().json ?? false;
|
|
12
12
|
const installed = (0, config_js_1.loadInstalled)();
|
|
13
13
|
if (!installed[slug]) {
|
|
14
14
|
const msg = { error: 'NOT_INSTALLED', message: `'${slug}'는 설치되어 있지 않습니다.` };
|
|
15
|
-
if (
|
|
16
|
-
console.error(
|
|
15
|
+
if (json) {
|
|
16
|
+
console.error(JSON.stringify(msg));
|
|
17
17
|
}
|
|
18
18
|
else {
|
|
19
|
-
console.error(
|
|
19
|
+
console.error(`\x1b[31m오류:\x1b[0m ${msg.message}`);
|
|
20
20
|
}
|
|
21
21
|
process.exit(1);
|
|
22
22
|
}
|
|
@@ -29,12 +29,12 @@ function registerUninstall(program) {
|
|
|
29
29
|
team: slug,
|
|
30
30
|
files_removed: removed.length,
|
|
31
31
|
};
|
|
32
|
-
if (
|
|
33
|
-
console.log(
|
|
34
|
-
console.log(` 삭제된 파일: ${removed.length}개`);
|
|
32
|
+
if (json) {
|
|
33
|
+
console.log(JSON.stringify(result));
|
|
35
34
|
}
|
|
36
35
|
else {
|
|
37
|
-
console.log(
|
|
36
|
+
console.log(`\n\x1b[32m✓ ${slug} 제거 완료\x1b[0m`);
|
|
37
|
+
console.log(` 삭제된 파일: ${removed.length}개`);
|
|
38
38
|
}
|
|
39
39
|
});
|
|
40
40
|
}
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ program
|
|
|
16
16
|
.name('relay')
|
|
17
17
|
.description('RelayAX Agent Team Marketplace CLI')
|
|
18
18
|
.version(pkg.version)
|
|
19
|
-
.option('--
|
|
19
|
+
.option('--json', '구조화된 JSON 출력');
|
|
20
20
|
(0, init_js_1.registerInit)(program);
|
|
21
21
|
(0, search_js_1.registerSearch)(program);
|
|
22
22
|
(0, install_js_1.registerInstall)(program);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface AITool {
|
|
2
|
+
name: string;
|
|
3
|
+
value: string;
|
|
4
|
+
skillsDir: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Agent Skills 표준을 지원하는 에이전트 CLI 목록.
|
|
8
|
+
* @fission-ai/openspec의 AI_TOOLS에서 차용.
|
|
9
|
+
*/
|
|
10
|
+
export declare const AI_TOOLS: AITool[];
|
|
11
|
+
/**
|
|
12
|
+
* 프로젝트 디렉토리에서 에이전트 CLI 디렉토리를 감지한다.
|
|
13
|
+
*/
|
|
14
|
+
export declare function detectAgentCLIs(projectPath: string): AITool[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AI_TOOLS = void 0;
|
|
7
|
+
exports.detectAgentCLIs = detectAgentCLIs;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
/**
|
|
11
|
+
* Agent Skills 표준을 지원하는 에이전트 CLI 목록.
|
|
12
|
+
* @fission-ai/openspec의 AI_TOOLS에서 차용.
|
|
13
|
+
*/
|
|
14
|
+
exports.AI_TOOLS = [
|
|
15
|
+
{ name: 'Amazon Q Developer', value: 'amazon-q', skillsDir: '.amazonq' },
|
|
16
|
+
{ name: 'Antigravity', value: 'antigravity', skillsDir: '.agent' },
|
|
17
|
+
{ name: 'Auggie', value: 'auggie', skillsDir: '.augment' },
|
|
18
|
+
{ name: 'Claude Code', value: 'claude', skillsDir: '.claude' },
|
|
19
|
+
{ name: 'Cline', value: 'cline', skillsDir: '.cline' },
|
|
20
|
+
{ name: 'Codex', value: 'codex', skillsDir: '.codex' },
|
|
21
|
+
{ name: 'CodeBuddy', value: 'codebuddy', skillsDir: '.codebuddy' },
|
|
22
|
+
{ name: 'Continue', value: 'continue', skillsDir: '.continue' },
|
|
23
|
+
{ name: 'CoStrict', value: 'costrict', skillsDir: '.cospec' },
|
|
24
|
+
{ name: 'Crush', value: 'crush', skillsDir: '.crush' },
|
|
25
|
+
{ name: 'Cursor', value: 'cursor', skillsDir: '.cursor' },
|
|
26
|
+
{ name: 'Factory Droid', value: 'factory', skillsDir: '.factory' },
|
|
27
|
+
{ name: 'Gemini CLI', value: 'gemini', skillsDir: '.gemini' },
|
|
28
|
+
{ name: 'GitHub Copilot', value: 'github-copilot', skillsDir: '.github' },
|
|
29
|
+
{ name: 'iFlow', value: 'iflow', skillsDir: '.iflow' },
|
|
30
|
+
{ name: 'Kilo Code', value: 'kilocode', skillsDir: '.kilocode' },
|
|
31
|
+
{ name: 'Kiro', value: 'kiro', skillsDir: '.kiro' },
|
|
32
|
+
{ name: 'OpenCode', value: 'opencode', skillsDir: '.opencode' },
|
|
33
|
+
{ name: 'Pi', value: 'pi', skillsDir: '.pi' },
|
|
34
|
+
{ name: 'Qoder', value: 'qoder', skillsDir: '.qoder' },
|
|
35
|
+
{ name: 'Qwen Code', value: 'qwen', skillsDir: '.qwen' },
|
|
36
|
+
{ name: 'RooCode', value: 'roocode', skillsDir: '.roo' },
|
|
37
|
+
{ name: 'Trae', value: 'trae', skillsDir: '.trae' },
|
|
38
|
+
{ name: 'Windsurf', value: 'windsurf', skillsDir: '.windsurf' },
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* 프로젝트 디렉토리에서 에이전트 CLI 디렉토리를 감지한다.
|
|
42
|
+
*/
|
|
43
|
+
function detectAgentCLIs(projectPath) {
|
|
44
|
+
return exports.AI_TOOLS.filter((tool) => fs_1.default.existsSync(path_1.default.join(projectPath, tool.skillsDir)));
|
|
45
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AITool } from './ai-tools.js';
|
|
2
|
+
export interface CommandContent {
|
|
3
|
+
id: string;
|
|
4
|
+
description: string;
|
|
5
|
+
body: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ToolCommandAdapter {
|
|
8
|
+
toolId: string;
|
|
9
|
+
getFilePath(commandId: string): string;
|
|
10
|
+
formatFile(content: CommandContent): string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 기본 어댑터 — 대부분의 에이전트 CLI가 동일한 패턴 사용.
|
|
14
|
+
* {skillsDir}/commands/relay/{id}.md
|
|
15
|
+
*/
|
|
16
|
+
export declare function createAdapter(tool: AITool): ToolCommandAdapter;
|
|
17
|
+
/**
|
|
18
|
+
* relay 슬래시 커맨드 템플릿.
|
|
19
|
+
* CLI 명령어(원자적 API)를 조합하는 에이전트 워크플로우.
|
|
20
|
+
*/
|
|
21
|
+
export declare const RELAY_COMMANDS: CommandContent[];
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RELAY_COMMANDS = void 0;
|
|
7
|
+
exports.createAdapter = createAdapter;
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
/**
|
|
10
|
+
* 기본 어댑터 — 대부분의 에이전트 CLI가 동일한 패턴 사용.
|
|
11
|
+
* {skillsDir}/commands/relay/{id}.md
|
|
12
|
+
*/
|
|
13
|
+
function createAdapter(tool) {
|
|
14
|
+
return {
|
|
15
|
+
toolId: tool.value,
|
|
16
|
+
getFilePath(commandId) {
|
|
17
|
+
return path_1.default.join(tool.skillsDir, 'commands', 'relay', `${commandId}.md`);
|
|
18
|
+
},
|
|
19
|
+
formatFile(content) {
|
|
20
|
+
return `---\ndescription: ${content.description}\n---\n\n${content.body}\n`;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* relay 슬래시 커맨드 템플릿.
|
|
26
|
+
* CLI 명령어(원자적 API)를 조합하는 에이전트 워크플로우.
|
|
27
|
+
*/
|
|
28
|
+
exports.RELAY_COMMANDS = [
|
|
29
|
+
{
|
|
30
|
+
id: 'relay-explore',
|
|
31
|
+
description: 'relay 마켓플레이스를 탐색하고 프로젝트에 맞는 팀을 찾습니다',
|
|
32
|
+
body: `사용자의 요청이나 현재 프로젝트 맥락에 맞는 에이전트 팀을 relay 마켓플레이스에서 탐색합니다.
|
|
33
|
+
|
|
34
|
+
## 실행 방법
|
|
35
|
+
|
|
36
|
+
1. 사용자의 요청에서 키워드를 추출합니다. 명시적 키워드가 없으면 현재 프로젝트를 분석하여 적절한 검색어를 판단합니다.
|
|
37
|
+
2. \`relay search <keyword>\` 명령어를 실행합니다 (필요하면 여러 키워드로 반복).
|
|
38
|
+
3. 결과를 현재 프로젝트 맥락과 대조하여 가장 도움될 팀을 추천합니다:
|
|
39
|
+
- 팀 이름과 설명
|
|
40
|
+
- 제공하는 커맨드 목록
|
|
41
|
+
- 왜 이 팀이 지금 프로젝트에 맞는지 설명
|
|
42
|
+
4. 관심 있는 팀이 있다면 \`/relay-install <slug>\`로 바로 설치할 수 있다고 안내합니다.
|
|
43
|
+
|
|
44
|
+
## 예시
|
|
45
|
+
|
|
46
|
+
사용자: /relay-explore 콘텐츠 만들 수 있는 팀 있어?
|
|
47
|
+
→ relay search 콘텐츠 실행
|
|
48
|
+
→ 결과 해석: "contents-team이 카드뉴스, PDF, PPT를 만들 수 있어요"
|
|
49
|
+
→ 프로젝트 맥락 기반 추천
|
|
50
|
+
→ "/relay-install contents-team으로 설치할 수 있어요"`,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'relay-install',
|
|
54
|
+
description: 'relay 마켓플레이스에서 에이전트 팀을 설치합니다',
|
|
55
|
+
body: `요청된 에이전트 팀을 relay 마켓플레이스에서 다운로드하여 현재 프로젝트에 설치합니다.
|
|
56
|
+
|
|
57
|
+
## 실행 방법
|
|
58
|
+
|
|
59
|
+
1. \`relay install <slug>\` 명령어를 실행합니다.
|
|
60
|
+
2. 설치 결과를 확인합니다:
|
|
61
|
+
- 설치된 파일 수
|
|
62
|
+
- 사용 가능해진 커맨드 목록
|
|
63
|
+
3. 각 커맨드의 사용법을 간단히 안내합니다.
|
|
64
|
+
4. "바로 사용해볼까요?"라고 제안합니다.
|
|
65
|
+
5. 사용자가 원하면 첫 번째 커맨드를 실행해봅니다.
|
|
66
|
+
|
|
67
|
+
## 예시
|
|
68
|
+
|
|
69
|
+
사용자: /relay-install contents-team
|
|
70
|
+
→ relay install contents-team 실행
|
|
71
|
+
→ "설치 완료! 다음 커맨드를 사용할 수 있습니다:"
|
|
72
|
+
→ /cardnews - 카드뉴스 제작
|
|
73
|
+
→ /detailpage - 상세페이지 제작
|
|
74
|
+
→ "바로 /cardnews를 사용해볼까요?"`,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'relay-publish',
|
|
78
|
+
description: '현재 팀 패키지를 포트폴리오와 함께 relay 마켓플레이스에 배포합니다',
|
|
79
|
+
body: `현재 디렉토리의 에이전트 팀을 분석하고, 포트폴리오를 생성한 뒤, relay 마켓플레이스에 배포합니다.
|
|
80
|
+
|
|
81
|
+
## 실행 단계
|
|
82
|
+
|
|
83
|
+
### 1. 팀 구조 분석
|
|
84
|
+
- skills/, agents/, rules/, commands/ 디렉토리를 탐색합니다.
|
|
85
|
+
- 각 파일의 이름과 description을 추출합니다.
|
|
86
|
+
- relay.yaml이 있으면 읽고, 없으면 사용자에게 팀 정보(name, slug, description, tags)를 물어보고 생성합니다.
|
|
87
|
+
|
|
88
|
+
### 2. 포트폴리오 생성
|
|
89
|
+
|
|
90
|
+
#### Layer 1: 팀 구성 시각화 (자동)
|
|
91
|
+
- 분석된 팀 구조를 HTML로 생성합니다. 내용:
|
|
92
|
+
- 팀 이름, 버전
|
|
93
|
+
- Skills 목록 (이름 + 설명)
|
|
94
|
+
- Commands 목록 (이름 + 설명)
|
|
95
|
+
- Agents 목록
|
|
96
|
+
- Rules 목록
|
|
97
|
+
- 비시각적 팀의 경우 기술 스택이나 데이터 종류 등 추가 정보
|
|
98
|
+
- 생성된 HTML을 Playwright로 스크린샷 캡처합니다. (gstack 또는 webapp-testing 스킬 활용)
|
|
99
|
+
- 결과 PNG를 ./portfolio/team-overview.png에 저장합니다.
|
|
100
|
+
|
|
101
|
+
#### Layer 2: 결과물 쇼케이스 (선택)
|
|
102
|
+
- output/, results/, examples/, portfolio/ 디렉토리를 스캔합니다.
|
|
103
|
+
- 발견된 결과물(PNG, JPG, HTML, PDF)을 사용자에게 보여줍니다.
|
|
104
|
+
- HTML 파일은 Playwright 스크린샷으로 변환합니다.
|
|
105
|
+
- 사용자가 포트폴리오에 포함할 항목을 선택합니다.
|
|
106
|
+
- 선택된 이미지를 ./portfolio/에 저장합니다.
|
|
107
|
+
|
|
108
|
+
### 3. 메타데이터 생성
|
|
109
|
+
- description: skills 내용 기반으로 자동 생성합니다.
|
|
110
|
+
- long_description: 팀 소개 마크다운을 자동 생성합니다 (README.md가 있으면 활용).
|
|
111
|
+
- tags: 팀 특성에 맞는 태그를 추천합니다.
|
|
112
|
+
- 사용자에게 확인: "이대로 배포할까요?"
|
|
113
|
+
|
|
114
|
+
### 4. relay.yaml 업데이트
|
|
115
|
+
- 생성/수정된 메타데이터와 포트폴리오 경로를 relay.yaml에 반영합니다.
|
|
116
|
+
|
|
117
|
+
### 5. 인증 확인
|
|
118
|
+
- \`relay login\`으로 인증 상태를 확인합니다. 미인증이면 로그인을 안내합니다.
|
|
119
|
+
|
|
120
|
+
### 6. 배포
|
|
121
|
+
- \`relay publish\` 명령어를 실행합니다.
|
|
122
|
+
- 배포 결과와 마켓플레이스 URL을 보여줍니다.
|
|
123
|
+
|
|
124
|
+
## 예시
|
|
125
|
+
|
|
126
|
+
사용자: /relay-publish
|
|
127
|
+
→ 팀 구조 분석: skills 3개, commands 5개, agents 2개
|
|
128
|
+
→ Layer 1: 팀 구성 시각화 HTML 생성 → 스크린샷 캡처
|
|
129
|
+
→ Layer 2: output/ 스캔 → "카드뉴스 예시.png, PDF 보고서.png 발견. 포트폴리오에 포함할까요?"
|
|
130
|
+
→ 사용자 확인 후 relay.yaml 업데이트
|
|
131
|
+
→ relay publish 실행
|
|
132
|
+
→ "배포 완료! URL: https://relayax.com/teams/contents-team"`,
|
|
133
|
+
},
|
|
134
|
+
];
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { InstalledRegistry } from '../types.js';
|
|
2
2
|
export declare const API_URL = "https://relayax.com";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* --path
|
|
4
|
+
* 설치 경로를 결정한다.
|
|
5
|
+
* 1. --path 옵션이 있으면 그대로 사용
|
|
6
|
+
* 2. 에이전트 CLI 자동 감지 → 감지된 경로 사용
|
|
7
|
+
* 3. 감지 안 되면 현재 디렉토리에 직접 설치
|
|
6
8
|
*/
|
|
7
9
|
export declare function getInstallPath(override?: string): string;
|
|
8
10
|
export declare function ensureRelayDir(): void;
|
package/dist/lib/config.js
CHANGED
|
@@ -13,12 +13,15 @@ exports.saveInstalled = saveInstalled;
|
|
|
13
13
|
const fs_1 = __importDefault(require("fs"));
|
|
14
14
|
const path_1 = __importDefault(require("path"));
|
|
15
15
|
const os_1 = __importDefault(require("os"));
|
|
16
|
+
const ai_tools_js_1 = require("./ai-tools.js");
|
|
16
17
|
exports.API_URL = 'https://relayax.com';
|
|
17
18
|
const RELAY_DIR = path_1.default.join(os_1.default.homedir(), '.relay');
|
|
18
19
|
const INSTALLED_FILE = path_1.default.join(RELAY_DIR, 'installed.json');
|
|
19
20
|
/**
|
|
20
|
-
*
|
|
21
|
-
* --path
|
|
21
|
+
* 설치 경로를 결정한다.
|
|
22
|
+
* 1. --path 옵션이 있으면 그대로 사용
|
|
23
|
+
* 2. 에이전트 CLI 자동 감지 → 감지된 경로 사용
|
|
24
|
+
* 3. 감지 안 되면 현재 디렉토리에 직접 설치
|
|
22
25
|
*/
|
|
23
26
|
function getInstallPath(override) {
|
|
24
27
|
if (override) {
|
|
@@ -27,7 +30,12 @@ function getInstallPath(override) {
|
|
|
27
30
|
: path_1.default.resolve(override);
|
|
28
31
|
return resolved;
|
|
29
32
|
}
|
|
30
|
-
|
|
33
|
+
const cwd = process.cwd();
|
|
34
|
+
const detected = (0, ai_tools_js_1.detectAgentCLIs)(cwd);
|
|
35
|
+
if (detected.length >= 1) {
|
|
36
|
+
return path_1.default.join(cwd, detected[0].skillsDir);
|
|
37
|
+
}
|
|
38
|
+
return cwd;
|
|
31
39
|
}
|
|
32
40
|
function ensureRelayDir() {
|
|
33
41
|
if (!fs_1.default.existsSync(RELAY_DIR)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "relayax-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "RelayAX Agent Team Marketplace CLI - Install and manage agent teams",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,16 @@
|
|
|
16
16
|
"start": "node dist/index.js",
|
|
17
17
|
"prepublishOnly": "npm run build"
|
|
18
18
|
},
|
|
19
|
-
"keywords": [
|
|
19
|
+
"keywords": [
|
|
20
|
+
"agent",
|
|
21
|
+
"skills",
|
|
22
|
+
"marketplace",
|
|
23
|
+
"cli",
|
|
24
|
+
"claude",
|
|
25
|
+
"llm",
|
|
26
|
+
"ai-agent",
|
|
27
|
+
"relayax"
|
|
28
|
+
],
|
|
20
29
|
"license": "MIT",
|
|
21
30
|
"repository": {
|
|
22
31
|
"type": "git",
|
|
@@ -24,6 +33,7 @@
|
|
|
24
33
|
},
|
|
25
34
|
"homepage": "https://relayax.com",
|
|
26
35
|
"dependencies": {
|
|
36
|
+
"@inquirer/prompts": "^8.3.2",
|
|
27
37
|
"commander": "^13.1.0",
|
|
28
38
|
"tar": "^7.4.0"
|
|
29
39
|
},
|