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.
@@ -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 readline_1 = __importDefault(require("readline"));
10
- const DIRS = ['skills', 'commands', 'agents', 'rules'];
11
- function prompt(rl, question, defaultVal) {
12
- const hint = defaultVal ? ` (${defaultVal})` : '';
13
- return new Promise((resolve) => {
14
- rl.question(`${question}${hint}: `, (answer) => {
15
- resolve(answer.trim() || defaultVal || '');
16
- });
17
- });
18
- }
19
- function slugify(str) {
20
- return str
21
- .toLowerCase()
22
- .replace(/[^a-z0-9-]/g, '-')
23
- .replace(/-+/g, '-')
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 toYaml(data) {
25
+ function showWelcome() {
27
26
  const lines = [
28
- `name: "${data.name}"`,
29
- `slug: "${data.slug}"`,
30
- `description: "${data.description}"`,
31
- `version: "${data.version}"`,
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
- if (data.tags.length > 0) {
34
- lines.push('tags:');
35
- for (const tag of data.tags) {
36
- lines.push(` - "${tag}"`);
37
- }
38
- }
39
- else {
40
- lines.push('tags: []');
41
- }
42
- return lines.join('\n') + '\n';
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('현재 디렉토리를 RelayAX 패키지로 초기화합니다')
48
- .option('--name <name>', ' 이름')
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 pretty = program.opts().pretty ?? false;
53
- const cwd = process.cwd();
54
- const relayYamlPath = path_1.default.join(cwd, 'relay.yaml');
55
- // Check if already initialized
56
- if (fs_1.default.existsSync(relayYamlPath)) {
57
- if (pretty) {
58
- console.log('\x1b[33m이미 초기화되어 있습니다.\x1b[0m relay.yaml이 존재합니다.');
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
- else {
61
- console.log(JSON.stringify({ status: 'already_initialized', path: relayYamlPath }));
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
- const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stderr });
82
- if (pretty) {
83
- console.log('\n\x1b[1mRelayAX 팀 패키지 초기화\x1b[0m\n');
84
- if (existing.length > 0) {
85
- console.log(` 감지된 디렉토리: \x1b[36m${existing.join(', ')}\x1b[0m`);
86
- }
87
- console.log('');
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
- name = opts.name ?? await prompt(rl, '팀 이름', dirName);
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
- if (!description) {
97
- console.error(JSON.stringify({ error: 'MISSING_DESCRIPTION', message: 'description은 필수입니다' }));
98
- process.exit(1);
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
- // Create missing directories
101
- const created = [];
102
- for (const dir of missing) {
103
- fs_1.default.mkdirSync(path_1.default.join(cwd, dir), { recursive: true });
104
- // Add .gitkeep so empty dirs are tracked
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
- // Write relay.yaml
109
- const yamlData = { name, slug, description, version: '1.0.0', tags };
110
- fs_1.default.writeFileSync(relayYamlPath, toYaml(yamlData));
111
- const result = {
112
- status: 'ok',
113
- slug,
114
- name,
115
- description,
116
- existing_dirs: existing,
117
- created_dirs: created,
118
- };
119
- if (pretty) {
120
- console.log(`\n\x1b[32m✓ 초기화 완료\x1b[0m\n`);
121
- console.log(` 팀: \x1b[36m${name}\x1b[0m (${slug})`);
122
- console.log(` 설명: ${description}`);
123
- if (created.length > 0) {
124
- console.log(` 생성됨: \x1b[33m${created.join(', ')}\x1b[0m`);
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
- console.log(` 설정: \x1b[36mrelay.yaml\x1b[0m`);
127
- console.log('\n 다음 단계:');
128
- console.log(' 1. skills/, commands/ 등에 파일 추가');
129
- console.log(' 2. \x1b[36mrelay publish\x1b[0m 로 마켓에 배포');
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(JSON.stringify(result));
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
  }
@@ -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('에이전트 팀 설치 (현재 디렉토리의 .claude/에 설치)')
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);
@@ -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
+ ];
@@ -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
- * 현재 디렉토리의 .claude/ 를 기본 설치 경로로 사용한다.
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;
@@ -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
- * 현재 디렉토리의 .claude/ 를 기본 설치 경로로 사용한다.
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
- return path_1.default.join(process.cwd(), '.claude');
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",
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": ["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
  },