relayax-cli 0.1.4 → 0.1.7

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.
@@ -22,31 +22,73 @@ function resolveTools(toolsArg) {
22
22
  }
23
23
  return tokens;
24
24
  }
25
+ function showWelcome() {
26
+ const lines = [
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
+ '',
37
+ ];
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;
56
+ }
25
57
  function registerInit(program) {
26
58
  program
27
59
  .command('init')
28
60
  .description('에이전트 CLI를 감지하고 relay 슬래시 커맨드를 설치합니다')
29
61
  .option('--tools <tools>', '설치할 에이전트 CLI 지정 (all 또는 쉼표 구분)')
30
62
  .action(async (opts) => {
31
- const pretty = program.opts().pretty ?? false;
63
+ const json = program.opts().json ?? false;
32
64
  const projectPath = process.cwd();
33
- // 1. 에이전트 CLI 감지 또는 --tools 지정
65
+ const detected = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
66
+ const detectedIds = new Set(detected.map((t) => t.value));
67
+ // 1. 도구 선택
34
68
  let targetToolIds;
35
69
  if (opts.tools) {
70
+ // --tools 옵션: 비대화형
36
71
  targetToolIds = resolveTools(opts.tools);
37
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`);
78
+ }
79
+ targetToolIds = await selectToolsInteractively(detectedIds);
80
+ if (targetToolIds.length === 0) {
81
+ console.log('\n 선택된 도구가 없습니다.');
82
+ return;
83
+ }
84
+ }
38
85
  else {
39
- const detected = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
86
+ // --json 모드 또는 비TTY (에이전트가 호출): 감지된 것만 자동 사용
40
87
  if (detected.length === 0) {
41
- const msg = '에이전트 CLI 디렉토리를 찾을 수 없습니다.\n' +
42
- '프로젝트에 .claude/, .gemini/, .cursor/ 등이 있는지 확인하세요.\n' +
43
- '또는 --tools 옵션으로 직접 지정할 수 있습니다.';
44
- if (pretty) {
45
- console.error(`\n\x1b[31m✗ ${msg}\x1b[0m`);
46
- }
47
- else {
48
- console.error(JSON.stringify({ error: 'NO_AGENT_CLI', message: msg }));
49
- }
88
+ console.error(JSON.stringify({
89
+ error: 'NO_AGENT_CLI',
90
+ message: '에이전트 CLI 디렉토리를 찾을 수 없습니다. --tools 옵션으로 지정하세요.',
91
+ }));
50
92
  process.exit(1);
51
93
  }
52
94
  targetToolIds = detected.map((t) => t.value);
@@ -95,7 +137,14 @@ function registerInit(program) {
95
137
  }
96
138
  }
97
139
  // 4. 출력
98
- if (pretty) {
140
+ if (json) {
141
+ console.log(JSON.stringify({
142
+ status: 'ok',
143
+ tools: results,
144
+ relay_yaml: relayYamlStatus,
145
+ }));
146
+ }
147
+ else {
99
148
  console.log('\n\x1b[32m✓ relay 초기화 완료\x1b[0m\n');
100
149
  for (const r of results) {
101
150
  console.log(` \x1b[36m${r.tool}\x1b[0m`);
@@ -111,12 +160,5 @@ function registerInit(program) {
111
160
  }
112
161
  console.log('\n IDE를 재시작하면 슬래시 커맨드가 활성화됩니다.');
113
162
  }
114
- else {
115
- console.log(JSON.stringify({
116
- status: 'ok',
117
- tools: results,
118
- relay_yaml: relayYamlStatus,
119
- }));
120
- }
121
163
  });
122
164
  }
@@ -11,7 +11,7 @@ function registerInstall(program) {
11
11
  .description('에이전트 팀 설치 (감지된 에이전트 CLI에 설치)')
12
12
  .option('--path <install_path>', '설치 경로 지정 (기본: ./.claude)')
13
13
  .action(async (slug, opts) => {
14
- const pretty = program.opts().pretty ?? false;
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 (pretty) {
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);
@@ -7,7 +7,7 @@ function registerList(program) {
7
7
  .command('list')
8
8
  .description('설치된 에이전트 팀 목록')
9
9
  .action(() => {
10
- const pretty = program.opts().pretty ?? false;
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 (pretty) {
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
  }
@@ -95,7 +95,7 @@ function registerLogin(program) {
95
95
  .description('RelayAX 계정에 로그인합니다')
96
96
  .option('--token <token>', '직접 토큰 입력 (브라우저 없이)')
97
97
  .action(async (opts) => {
98
- const pretty = program.opts().pretty ?? false;
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 (pretty) {
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
  }
@@ -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 pretty = program.opts().pretty ?? false;
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 (pretty) {
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 (pretty) {
323
+ if (!json) {
188
324
  console.error(`업로드 중...`);
189
325
  }
190
- const result = await publishToApi(token, tarPath, metadata);
191
- if (pretty) {
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
- else {
197
- console.log(JSON.stringify(result));
334
+ if (result.portfolio_count && result.portfolio_count > 0) {
335
+ console.log(` 포트폴리오: ${result.portfolio_count}개 이미지 업로드됨`);
336
+ }
198
337
  }
199
338
  }
200
339
  catch (err) {
@@ -29,17 +29,17 @@ function registerSearch(program) {
29
29
  .description('에이전트 팀 검색')
30
30
  .option('--tag <tag>', '태그로 필터링')
31
31
  .action(async (keyword, opts) => {
32
- const pretty = program.opts().pretty ?? false;
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 (pretty) {
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 pretty = program.opts().pretty ?? false;
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 (pretty) {
16
- console.error(`\x1b[31m오류:\x1b[0m ${msg.message}`);
15
+ if (json) {
16
+ console.error(JSON.stringify(msg));
17
17
  }
18
18
  else {
19
- console.error(JSON.stringify(msg));
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 (pretty) {
33
- console.log(`\n\x1b[32m✓ ${slug} 제거 완료\x1b[0m`);
34
- console.log(` 삭제된 파일: ${removed.length}개`);
32
+ if (json) {
33
+ console.log(JSON.stringify(result));
35
34
  }
36
35
  else {
37
- console.log(JSON.stringify(result));
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('--pretty', '인간 친화적 출력 (기본값: JSON)');
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);
@@ -56,7 +56,7 @@ exports.RELAY_COMMANDS = [
56
56
 
57
57
  ## 실행 방법
58
58
 
59
- 1. \`relay install <slug> --pretty\` 명령어를 실행합니다.
59
+ 1. \`relay install <slug>\` 명령어를 실행합니다.
60
60
  2. 설치 결과를 확인합니다:
61
61
  - 설치된 파일 수
62
62
  - 사용 가능해진 커맨드 목록
@@ -67,7 +67,7 @@ exports.RELAY_COMMANDS = [
67
67
  ## 예시
68
68
 
69
69
  사용자: /relay-install contents-team
70
- → relay install contents-team --pretty 실행
70
+ → relay install contents-team 실행
71
71
  → "설치 완료! 다음 커맨드를 사용할 수 있습니다:"
72
72
  → /cardnews - 카드뉴스 제작
73
73
  → /detailpage - 상세페이지 제작
@@ -75,15 +75,60 @@ exports.RELAY_COMMANDS = [
75
75
  },
76
76
  {
77
77
  id: 'relay-publish',
78
- description: '현재 팀 패키지를 relay 마켓플레이스에 배포합니다',
79
- body: `현재 디렉토리의 에이전트 팀을 relay 마켓플레이스에 배포합니다.
78
+ description: '현재 팀 패키지를 포트폴리오와 함께 relay 마켓플레이스에 배포합니다',
79
+ body: `현재 디렉토리의 에이전트 팀을 분석하고, 포트폴리오를 생성한 뒤, relay 마켓플레이스에 배포합니다.
80
80
 
81
- ## 실행 방법
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
+ ## 예시
82
125
 
83
- 1. relay.yaml이 있는지 확인합니다. 없으면 사용자에게 팀 정보를 물어보고 생성합니다.
84
- 2. skills/, agents/, rules/, commands/ 디렉토리 구조를 확인합니다.
85
- 3. \`relay login\`으로 인증 상태를 확인합니다. 미인증이면 로그인을 안내합니다.
86
- 4. \`relay publish --pretty\` 명령어를 실행합니다.
87
- 5. 배포 결과와 마켓플레이스 URL을 보여줍니다.`,
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"`,
88
133
  },
89
134
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relayax-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.7",
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": ["agent", "skills", "marketplace", "cli", "claude", "llm", "ai-agent", "relayax"],
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
  },