relayax-cli 0.1.0

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/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # relay-cli
2
+
3
+ Agent Team Marketplace CLI - 에이전트 팀을 검색하고 설치하세요.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # 글로벌 설치 없이 바로 사용
9
+ npx relay-cli install contents-team
10
+
11
+ # 또는 글로벌 설치
12
+ npm install -g relay-cli
13
+ relay install contents-team
14
+ ```
15
+
16
+ ## Commands
17
+
18
+ | Command | Description |
19
+ |---------|-------------|
20
+ | `relay init` | 초기 설정 (설치 경로, API URL) |
21
+ | `relay search <keyword>` | 팀 검색 |
22
+ | `relay install <name>` | 팀 설치 |
23
+ | `relay list` | 설치된 팀 목록 |
24
+ | `relay uninstall <name>` | 팀 제거 |
25
+
26
+ ## Options
27
+
28
+ - `--pretty` - 사람이 읽기 좋은 포맷으로 출력 (기본: JSON)
29
+ - `--json` - JSON 출력 (기본값, 에이전트 친화적)
30
+
31
+ ## For AI Agents
32
+
33
+ relay CLI는 에이전트가 1차 사용자입니다. 모든 출력은 JSON 기본입니다.
34
+
35
+ ```bash
36
+ # 에이전트가 팀 검색
37
+ relay search "콘텐츠" | jq '.results[].slug'
38
+
39
+ # 에이전트가 팀 설치
40
+ relay install contents-team
41
+ # → {"status":"ok","team":"Contents Team","commands":[...]}
42
+ ```
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerInit(program: Command): void;
@@ -0,0 +1,79 @@
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.registerInit = registerInit;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const readline_1 = __importDefault(require("readline"));
11
+ const config_js_1 = require("../lib/config.js");
12
+ const DEFAULT_API_URL = 'https://relayax.com';
13
+ const PRESET_PATHS = {
14
+ '1': path_1.default.join(os_1.default.homedir(), '.claude'),
15
+ '2': path_1.default.join(os_1.default.homedir(), '.gemini'),
16
+ };
17
+ function prompt(rl, question) {
18
+ return new Promise((resolve) => rl.question(question, resolve));
19
+ }
20
+ function registerInit(program) {
21
+ program
22
+ .command('init')
23
+ .description('relayax 초기화 및 설치 경로 설정')
24
+ .option('--path <install_path>', '설치 경로 직접 지정')
25
+ .option('--api-url <url>', `API URL (기본값: ${DEFAULT_API_URL})`)
26
+ .action(async (opts) => {
27
+ const pretty = program.opts().pretty ?? false;
28
+ const api_url = opts.apiUrl ?? DEFAULT_API_URL;
29
+ let install_path;
30
+ if (opts.path) {
31
+ install_path = opts.path;
32
+ }
33
+ else if (!pretty) {
34
+ // non-pretty / agent mode: use default
35
+ install_path = PRESET_PATHS['1'];
36
+ }
37
+ else {
38
+ const rl = readline_1.default.createInterface({
39
+ input: process.stdin,
40
+ output: process.stdout,
41
+ });
42
+ console.log('\n설치 경로를 선택하세요:');
43
+ console.log(' 1) ~/.claude/ (Claude Code) [기본값]');
44
+ console.log(' 2) ~/.gemini/ (Gemini CLI)');
45
+ console.log(' 3) 직접 입력');
46
+ const choice = (await prompt(rl, '\n선택 [1]: ')).trim() || '1';
47
+ if (choice === '3') {
48
+ install_path = (await prompt(rl, '경로 입력: ')).trim();
49
+ }
50
+ else {
51
+ install_path = PRESET_PATHS[choice] ?? PRESET_PATHS['1'];
52
+ }
53
+ rl.close();
54
+ }
55
+ // Resolve ~ in custom paths
56
+ if (install_path.startsWith('~')) {
57
+ install_path = path_1.default.join(os_1.default.homedir(), install_path.slice(1));
58
+ }
59
+ // Ensure install path exists
60
+ if (!fs_1.default.existsSync(install_path)) {
61
+ fs_1.default.mkdirSync(install_path, { recursive: true });
62
+ }
63
+ (0, config_js_1.ensureRelayDir)();
64
+ (0, config_js_1.saveConfig)({ install_path, api_url });
65
+ const result = {
66
+ status: 'ok',
67
+ install_path,
68
+ api_url,
69
+ };
70
+ if (pretty) {
71
+ console.log('\n\x1b[32m✓ relay 초기화 완료\x1b[0m');
72
+ console.log(` 설치 경로: \x1b[36m${install_path}\x1b[0m`);
73
+ console.log(` API URL: \x1b[36m${api_url}\x1b[0m`);
74
+ }
75
+ else {
76
+ console.log(JSON.stringify(result));
77
+ }
78
+ });
79
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerInstall(program: Command): void;
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerInstall = registerInstall;
4
+ const api_js_1 = require("../lib/api.js");
5
+ const storage_js_1 = require("../lib/storage.js");
6
+ const installer_js_1 = require("../lib/installer.js");
7
+ const config_js_1 = require("../lib/config.js");
8
+ function registerInstall(program) {
9
+ program
10
+ .command('install <slug>')
11
+ .description('에이전트 팀 설치')
12
+ .action(async (slug) => {
13
+ const pretty = program.opts().pretty ?? false;
14
+ const config = (0, config_js_1.ensureConfig)();
15
+ const tempDir = (0, storage_js_1.makeTempDir)();
16
+ try {
17
+ // 1. Fetch team metadata
18
+ const team = await (0, api_js_1.fetchTeamInfo)(slug);
19
+ // 2. Download package
20
+ const tarPath = await (0, storage_js_1.downloadPackage)(team.package_url, tempDir);
21
+ // 3. Extract
22
+ const extractDir = `${tempDir}/extracted`;
23
+ await (0, storage_js_1.extractPackage)(tarPath, extractDir);
24
+ // 4. Copy files to install_path
25
+ const files = (0, installer_js_1.installTeam)(extractDir, config.install_path);
26
+ // 5. Record in installed.json
27
+ const installed = (0, config_js_1.loadInstalled)();
28
+ installed[slug] = {
29
+ version: team.version,
30
+ installed_at: new Date().toISOString(),
31
+ files,
32
+ };
33
+ (0, config_js_1.saveInstalled)(installed);
34
+ // 6. Report install (non-blocking)
35
+ await (0, api_js_1.reportInstall)(slug);
36
+ const result = {
37
+ status: 'ok',
38
+ team: team.name,
39
+ slug,
40
+ version: team.version,
41
+ commands: team.commands,
42
+ files_installed: files.length,
43
+ };
44
+ if (pretty) {
45
+ console.log(`\n\x1b[32m✓ ${team.name} 설치 완료\x1b[0m v${team.version}`);
46
+ console.log(` 설치 위치: \x1b[36m${config.install_path}\x1b[0m`);
47
+ console.log(` 파일 수: ${files.length}개`);
48
+ if (team.commands.length > 0) {
49
+ console.log('\n 사용 가능한 커맨드:');
50
+ for (const cmd of team.commands) {
51
+ console.log(` \x1b[33m/${cmd.name}\x1b[0m - ${cmd.description}`);
52
+ }
53
+ }
54
+ }
55
+ else {
56
+ console.log(JSON.stringify(result));
57
+ }
58
+ }
59
+ catch (err) {
60
+ const message = err instanceof Error ? err.message : String(err);
61
+ console.error(JSON.stringify({ error: 'INSTALL_FAILED', message }));
62
+ process.exit(1);
63
+ }
64
+ finally {
65
+ (0, storage_js_1.removeTempDir)(tempDir);
66
+ }
67
+ });
68
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerList(program: Command): void;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerList = registerList;
4
+ const config_js_1 = require("../lib/config.js");
5
+ function registerList(program) {
6
+ program
7
+ .command('list')
8
+ .description('설치된 에이전트 팀 목록')
9
+ .action(() => {
10
+ const pretty = program.opts().pretty ?? false;
11
+ const installed = (0, config_js_1.loadInstalled)();
12
+ const entries = Object.entries(installed);
13
+ const installedList = entries.map(([slug, info]) => ({
14
+ slug,
15
+ version: info.version,
16
+ installed_at: info.installed_at,
17
+ files: info.files.length,
18
+ }));
19
+ if (pretty) {
20
+ if (installedList.length === 0) {
21
+ console.log('\n설치된 팀이 없습니다. `relay install <slug>`로 설치하세요.');
22
+ return;
23
+ }
24
+ console.log(`\n설치된 팀 (${installedList.length}개):\n`);
25
+ for (const item of installedList) {
26
+ const date = new Date(item.installed_at).toLocaleDateString('ko-KR');
27
+ console.log(` \x1b[36m${item.slug}\x1b[0m v${item.version} (${date}) 파일 ${item.files}개`);
28
+ }
29
+ }
30
+ else {
31
+ console.log(JSON.stringify({ installed: installedList }));
32
+ }
33
+ });
34
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerLogin(program: Command): void;
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerLogin = registerLogin;
4
+ const readline_1 = require("readline");
5
+ const child_process_1 = require("child_process");
6
+ const config_js_1 = require("../lib/config.js");
7
+ function openBrowser(url) {
8
+ const platform = process.platform;
9
+ try {
10
+ if (platform === 'darwin') {
11
+ (0, child_process_1.execSync)(`open "${url}"`, { stdio: 'ignore' });
12
+ }
13
+ else if (platform === 'win32') {
14
+ (0, child_process_1.execSync)(`start "" "${url}"`, { stdio: 'ignore' });
15
+ }
16
+ else {
17
+ (0, child_process_1.execSync)(`xdg-open "${url}"`, { stdio: 'ignore' });
18
+ }
19
+ }
20
+ catch {
21
+ // ignore browser open errors
22
+ }
23
+ }
24
+ async function readLine(prompt) {
25
+ const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stderr });
26
+ return new Promise((resolve) => {
27
+ rl.question(prompt, (answer) => {
28
+ rl.close();
29
+ resolve(answer.trim());
30
+ });
31
+ });
32
+ }
33
+ async function verifyToken(apiUrl, token) {
34
+ try {
35
+ const res = await fetch(`${apiUrl}/api/auth/me`, {
36
+ headers: { Authorization: `Bearer ${token}` },
37
+ });
38
+ if (!res.ok)
39
+ return null;
40
+ return (await res.json());
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ function registerLogin(program) {
47
+ program
48
+ .command('login')
49
+ .description('Relay 계정에 로그인합니다')
50
+ .option('--token <token>', '직접 토큰 입력 (브라우저 없이)')
51
+ .action(async (opts) => {
52
+ const pretty = program.opts().pretty ?? false;
53
+ (0, config_js_1.ensureRelayDir)();
54
+ const config = (0, config_js_1.loadConfig)();
55
+ const apiUrl = config?.api_url ?? 'https://relayax.com';
56
+ let token = opts.token;
57
+ if (!token) {
58
+ const tokenUrl = `${apiUrl}/auth/token`;
59
+ console.error('브라우저에서 로그인 후 표시되는 토큰을 복사하세요.');
60
+ console.error(`\n ${tokenUrl}\n`);
61
+ openBrowser(tokenUrl);
62
+ token = await readLine('토큰 붙여넣기: ');
63
+ }
64
+ if (!token) {
65
+ console.error(JSON.stringify({ error: 'NO_TOKEN', message: '토큰이 입력되지 않았습니다' }));
66
+ process.exit(1);
67
+ }
68
+ const user = await verifyToken(apiUrl, token);
69
+ const updatedConfig = {
70
+ install_path: config?.install_path ?? `${process.env.HOME}/.claude`,
71
+ api_url: apiUrl,
72
+ token,
73
+ };
74
+ (0, config_js_1.saveConfig)(updatedConfig);
75
+ const result = {
76
+ status: 'ok',
77
+ message: '로그인 성공',
78
+ ...(user ? { email: user.email } : {}),
79
+ };
80
+ if (pretty) {
81
+ console.log(`\x1b[32m✓ 로그인 완료\x1b[0m`);
82
+ if (user?.email)
83
+ console.log(` 계정: \x1b[36m${user.email}\x1b[0m`);
84
+ console.log(` 토큰이 ~/.relay/config.json에 저장되었습니다.`);
85
+ }
86
+ else {
87
+ console.log(JSON.stringify(result));
88
+ }
89
+ });
90
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerPublish(program: Command): void;
@@ -0,0 +1,197 @@
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.registerPublish = registerPublish;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const readline_1 = require("readline");
11
+ const tar_1 = require("tar");
12
+ const config_js_1 = require("../lib/config.js");
13
+ const VALID_DIRS = ['skills', 'agents', 'rules', 'commands'];
14
+ function slugify(str) {
15
+ return str
16
+ .toLowerCase()
17
+ .replace(/[^a-z0-9-]/g, '-')
18
+ .replace(/-+/g, '-')
19
+ .replace(/^-|-$/g, '');
20
+ }
21
+ function detectCommands(teamDir) {
22
+ const cmdDir = path_1.default.join(teamDir, 'commands');
23
+ if (!fs_1.default.existsSync(cmdDir))
24
+ return [];
25
+ const entries = [];
26
+ const files = fs_1.default.readdirSync(cmdDir).filter((f) => f.endsWith('.md'));
27
+ for (const file of files) {
28
+ const name = path_1.default.basename(file, '.md');
29
+ let description = name;
30
+ try {
31
+ const content = fs_1.default.readFileSync(path_1.default.join(cmdDir, file), 'utf-8');
32
+ const lines = content.split('\n').map((l) => l.trim()).filter(Boolean);
33
+ // Check frontmatter for description
34
+ if (lines[0] === '---') {
35
+ const endIdx = lines.indexOf('---', 1);
36
+ if (endIdx > 0) {
37
+ const frontmatter = lines.slice(1, endIdx).join('\n');
38
+ const m = frontmatter.match(/^description:\s*(.+)$/m);
39
+ if (m)
40
+ description = m[1].trim();
41
+ }
42
+ }
43
+ else if (lines[0]) {
44
+ // Use first line, strip leading # if heading
45
+ description = lines[0].replace(/^#+\s*/, '');
46
+ }
47
+ }
48
+ catch {
49
+ // ignore read errors
50
+ }
51
+ entries.push({ name, description });
52
+ }
53
+ return entries;
54
+ }
55
+ function countDir(teamDir, dirName) {
56
+ const dirPath = path_1.default.join(teamDir, dirName);
57
+ if (!fs_1.default.existsSync(dirPath))
58
+ return 0;
59
+ return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.')).length;
60
+ }
61
+ async function prompt(question, defaultVal) {
62
+ const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stderr });
63
+ const hint = defaultVal ? ` (기본값: ${defaultVal})` : '';
64
+ return new Promise((resolve) => {
65
+ rl.question(`${question}${hint}: `, (answer) => {
66
+ rl.close();
67
+ resolve(answer.trim() || defaultVal || '');
68
+ });
69
+ });
70
+ }
71
+ async function createTarball(teamDir) {
72
+ const tmpFile = path_1.default.join(os_1.default.tmpdir(), `relay-publish-${Date.now()}.tar.gz`);
73
+ const parent = path_1.default.dirname(teamDir);
74
+ const dirName = path_1.default.basename(teamDir);
75
+ await (0, tar_1.create)({
76
+ gzip: true,
77
+ file: tmpFile,
78
+ cwd: parent,
79
+ }, [dirName]);
80
+ return tmpFile;
81
+ }
82
+ async function publishToApi(apiUrl, token, tarPath, metadata) {
83
+ const fileBuffer = fs_1.default.readFileSync(tarPath);
84
+ const blob = new Blob([fileBuffer], { type: 'application/gzip' });
85
+ const form = new FormData();
86
+ form.append('package', blob, `${metadata.slug}-${metadata.version}.tar.gz`);
87
+ form.append('metadata', JSON.stringify(metadata));
88
+ const res = await fetch(`${apiUrl}/api/publish`, {
89
+ method: 'POST',
90
+ headers: { Authorization: `Bearer ${token}` },
91
+ body: form,
92
+ });
93
+ const body = await res.json();
94
+ if (!res.ok) {
95
+ const msg = typeof body.message === 'string' ? body.message : `서버 오류 (${res.status})`;
96
+ throw new Error(msg);
97
+ }
98
+ return body;
99
+ }
100
+ function registerPublish(program) {
101
+ program
102
+ .command('publish [dir]')
103
+ .description('에이전트 팀을 마켓플레이스에 배포합니다')
104
+ .option('--name <name>', '팀 표시명')
105
+ .option('--description <desc>', '한 줄 설명')
106
+ .option('--tag <tag>', '태그 (여러 번 사용 가능)', (val, prev) => [...prev, val], [])
107
+ .option('--token <token>', 'Supabase 인증 토큰')
108
+ .option('--slug <slug>', 'URL용 슬러그 (기본: 디렉토리명)')
109
+ .option('--version <ver>', '버전 (기본: 1.0.0)', '1.0.0')
110
+ .action(async (dir, opts) => {
111
+ const pretty = program.opts().pretty ?? false;
112
+ const teamDir = path_1.default.resolve(dir ?? process.cwd());
113
+ if (!fs_1.default.existsSync(teamDir) || !fs_1.default.statSync(teamDir).isDirectory()) {
114
+ console.error(JSON.stringify({ error: 'INVALID_DIR', message: `디렉토리를 찾을 수 없습니다: ${teamDir}` }));
115
+ process.exit(1);
116
+ }
117
+ // Validate structure
118
+ const hasDirs = VALID_DIRS.some((d) => fs_1.default.existsSync(path_1.default.join(teamDir, d)));
119
+ if (!hasDirs) {
120
+ console.error(JSON.stringify({
121
+ error: 'INVALID_STRUCTURE',
122
+ message: `팀 디렉토리에는 skills/, agents/, rules/, commands/ 중 하나 이상이 있어야 합니다`,
123
+ }));
124
+ process.exit(1);
125
+ }
126
+ // Get token
127
+ const config = (0, config_js_1.ensureConfig)();
128
+ const token = opts.token ?? process.env.RELAY_TOKEN ?? config.token;
129
+ if (!token) {
130
+ console.error(JSON.stringify({
131
+ error: 'NO_TOKEN',
132
+ message: '인증 토큰이 필요합니다. --token 플래그, RELAY_TOKEN 환경변수, 또는 `relay login`을 사용하세요.',
133
+ }));
134
+ process.exit(1);
135
+ }
136
+ // Auto-detect
137
+ const autoSlug = slugify(path_1.default.basename(teamDir));
138
+ const detectedCommands = detectCommands(teamDir);
139
+ const components = {
140
+ agents: countDir(teamDir, 'agents'),
141
+ rules: countDir(teamDir, 'rules'),
142
+ skills: countDir(teamDir, 'skills'),
143
+ };
144
+ // Gather metadata (prompt if not provided)
145
+ const isInteractive = process.stdin.isTTY;
146
+ const slug = opts.slug ?? (isInteractive ? await prompt('슬러그', autoSlug) : autoSlug);
147
+ const name = opts.name ?? (isInteractive ? await prompt('팀 이름', path_1.default.basename(teamDir)) : path_1.default.basename(teamDir));
148
+ const description = opts.description ?? (isInteractive ? await prompt('한 줄 설명') : '');
149
+ let tags = opts.tag;
150
+ if (tags.length === 0 && isInteractive) {
151
+ const tagsInput = await prompt('태그 (쉼표 구분)', '');
152
+ tags = tagsInput ? tagsInput.split(',').map((t) => t.trim()).filter(Boolean) : [];
153
+ }
154
+ if (!description) {
155
+ console.error(JSON.stringify({ error: 'MISSING_DESCRIPTION', message: 'description은 필수입니다' }));
156
+ process.exit(1);
157
+ }
158
+ const metadata = {
159
+ slug,
160
+ name,
161
+ description,
162
+ tags,
163
+ commands: detectedCommands,
164
+ components,
165
+ version: opts.version,
166
+ };
167
+ if (pretty) {
168
+ console.error(`패키지 생성 중...`);
169
+ }
170
+ let tarPath = null;
171
+ try {
172
+ tarPath = await createTarball(teamDir);
173
+ if (pretty) {
174
+ console.error(`업로드 중...`);
175
+ }
176
+ const result = await publishToApi(config.api_url, token, tarPath, metadata);
177
+ if (pretty) {
178
+ console.log(`\n\x1b[32m✓ ${name} 배포 완료\x1b[0m v${result.version}`);
179
+ console.log(` 슬러그: \x1b[36m${result.slug}\x1b[0m`);
180
+ console.log(` URL: \x1b[36m${result.url}\x1b[0m`);
181
+ }
182
+ else {
183
+ console.log(JSON.stringify(result));
184
+ }
185
+ }
186
+ catch (err) {
187
+ const message = err instanceof Error ? err.message : String(err);
188
+ console.error(JSON.stringify({ error: 'PUBLISH_FAILED', message }));
189
+ process.exit(1);
190
+ }
191
+ finally {
192
+ if (tarPath && fs_1.default.existsSync(tarPath)) {
193
+ fs_1.default.unlinkSync(tarPath);
194
+ }
195
+ }
196
+ });
197
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerSearch(program: Command): void;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerSearch = registerSearch;
4
+ const api_js_1 = require("../lib/api.js");
5
+ function formatTable(results) {
6
+ if (results.length === 0)
7
+ return '검색 결과가 없습니다.';
8
+ const rows = results.map((r) => ({
9
+ slug: r.slug,
10
+ name: r.name,
11
+ description: r.description.length > 50
12
+ ? r.description.slice(0, 47) + '...'
13
+ : r.description,
14
+ installs: String(r.install_count),
15
+ commands: r.commands.join(', ') || '-',
16
+ }));
17
+ const cols = ['slug', 'name', 'description', 'installs', 'commands'];
18
+ const widths = cols.map((col) => Math.max(col.length, ...rows.map((r) => r[col].length)));
19
+ const header = cols
20
+ .map((col, i) => col.padEnd(widths[i]))
21
+ .join(' ');
22
+ const separator = widths.map((w) => '-'.repeat(w)).join(' ');
23
+ const lines = rows.map((row) => cols.map((col, i) => row[col].padEnd(widths[i])).join(' '));
24
+ return ['\x1b[1m' + header + '\x1b[0m', separator, ...lines].join('\n');
25
+ }
26
+ function registerSearch(program) {
27
+ program
28
+ .command('search <keyword>')
29
+ .description('에이전트 팀 검색')
30
+ .option('--tag <tag>', '태그로 필터링')
31
+ .action(async (keyword, opts) => {
32
+ const pretty = program.opts().pretty ?? false;
33
+ try {
34
+ const results = await (0, api_js_1.searchTeams)(keyword, opts.tag);
35
+ if (pretty) {
36
+ console.log(`\n검색어: \x1b[36m${keyword}\x1b[0m${opts.tag ? ` 태그: \x1b[33m${opts.tag}\x1b[0m` : ''}\n`);
37
+ console.log(formatTable(results));
38
+ console.log(`\n총 ${results.length}건`);
39
+ }
40
+ else {
41
+ console.log(JSON.stringify({ results }));
42
+ }
43
+ }
44
+ catch (err) {
45
+ const message = err instanceof Error ? err.message : String(err);
46
+ console.error(JSON.stringify({ error: 'SEARCH_FAILED', message }));
47
+ process.exit(1);
48
+ }
49
+ });
50
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerUninstall(program: Command): void;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerUninstall = registerUninstall;
4
+ const config_js_1 = require("../lib/config.js");
5
+ const installer_js_1 = require("../lib/installer.js");
6
+ function registerUninstall(program) {
7
+ program
8
+ .command('uninstall <slug>')
9
+ .description('에이전트 팀 제거')
10
+ .action((slug) => {
11
+ const pretty = program.opts().pretty ?? false;
12
+ const installed = (0, config_js_1.loadInstalled)();
13
+ if (!installed[slug]) {
14
+ const msg = { error: 'NOT_INSTALLED', message: `'${slug}'는 설치되어 있지 않습니다.` };
15
+ if (pretty) {
16
+ console.error(`\x1b[31m오류:\x1b[0m ${msg.message}`);
17
+ }
18
+ else {
19
+ console.error(JSON.stringify(msg));
20
+ }
21
+ process.exit(1);
22
+ }
23
+ const { files } = installed[slug];
24
+ const removed = (0, installer_js_1.uninstallTeam)(files);
25
+ delete installed[slug];
26
+ (0, config_js_1.saveInstalled)(installed);
27
+ const result = {
28
+ status: 'ok',
29
+ team: slug,
30
+ files_removed: removed.length,
31
+ };
32
+ if (pretty) {
33
+ console.log(`\n\x1b[32m✓ ${slug} 제거 완료\x1b[0m`);
34
+ console.log(` 삭제된 파일: ${removed.length}개`);
35
+ }
36
+ else {
37
+ console.log(JSON.stringify(result));
38
+ }
39
+ });
40
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const init_js_1 = require("./commands/init.js");
6
+ const search_js_1 = require("./commands/search.js");
7
+ const install_js_1 = require("./commands/install.js");
8
+ const list_js_1 = require("./commands/list.js");
9
+ const uninstall_js_1 = require("./commands/uninstall.js");
10
+ const publish_js_1 = require("./commands/publish.js");
11
+ const login_js_1 = require("./commands/login.js");
12
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
13
+ const pkg = require('../package.json');
14
+ const program = new commander_1.Command();
15
+ program
16
+ .name('relay')
17
+ .description('RelayAX Agent Team Marketplace CLI')
18
+ .version(pkg.version)
19
+ .option('--pretty', '인간 친화적 출력 (기본값: JSON)');
20
+ (0, init_js_1.registerInit)(program);
21
+ (0, search_js_1.registerSearch)(program);
22
+ (0, install_js_1.registerInstall)(program);
23
+ (0, list_js_1.registerList)(program);
24
+ (0, uninstall_js_1.registerUninstall)(program);
25
+ (0, publish_js_1.registerPublish)(program);
26
+ (0, login_js_1.registerLogin)(program);
27
+ program.parse();
@@ -0,0 +1,4 @@
1
+ import type { TeamRegistryInfo, SearchResult } from '../types.js';
2
+ export declare function fetchTeamInfo(slug: string): Promise<TeamRegistryInfo>;
3
+ export declare function searchTeams(query: string, tag?: string): Promise<SearchResult[]>;
4
+ export declare function reportInstall(slug: string): Promise<void>;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchTeamInfo = fetchTeamInfo;
4
+ exports.searchTeams = searchTeams;
5
+ exports.reportInstall = reportInstall;
6
+ const config_js_1 = require("./config.js");
7
+ async function fetchTeamInfo(slug) {
8
+ const config = (0, config_js_1.ensureConfig)();
9
+ const url = `${config.api_url}/api/registry/${slug}`;
10
+ const res = await fetch(url);
11
+ if (!res.ok) {
12
+ const body = await res.text();
13
+ throw new Error(`팀 정보 조회 실패 (${res.status}): ${body}`);
14
+ }
15
+ return res.json();
16
+ }
17
+ async function searchTeams(query, tag) {
18
+ const config = (0, config_js_1.ensureConfig)();
19
+ const params = new URLSearchParams({ q: query });
20
+ if (tag)
21
+ params.set('tag', tag);
22
+ const url = `${config.api_url}/api/registry/search?${params.toString()}`;
23
+ const res = await fetch(url);
24
+ if (!res.ok) {
25
+ const body = await res.text();
26
+ throw new Error(`검색 실패 (${res.status}): ${body}`);
27
+ }
28
+ const data = (await res.json());
29
+ return data.results;
30
+ }
31
+ async function reportInstall(slug) {
32
+ const config = (0, config_js_1.ensureConfig)();
33
+ const url = `${config.api_url}/api/registry/${slug}/install`;
34
+ await fetch(url, { method: 'POST' }).catch(() => {
35
+ // non-critical: ignore errors
36
+ });
37
+ }
@@ -0,0 +1,13 @@
1
+ import type { RelayConfig, InstalledRegistry } from '../types.js';
2
+ export declare function getRelayDir(): string;
3
+ export declare function ensureRelayDir(): void;
4
+ export declare function loadConfig(): RelayConfig | null;
5
+ export declare function saveConfig(config: RelayConfig): void;
6
+ export declare function requireConfig(): RelayConfig;
7
+ /**
8
+ * npx 첫 실행처럼 config가 없을 때 기본값으로 자동 초기화한다.
9
+ * 이미 config가 있으면 그대로 반환한다.
10
+ */
11
+ export declare function ensureConfig(): RelayConfig;
12
+ export declare function loadInstalled(): InstalledRegistry;
13
+ export declare function saveInstalled(registry: InstalledRegistry): void;
@@ -0,0 +1,91 @@
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.getRelayDir = getRelayDir;
7
+ exports.ensureRelayDir = ensureRelayDir;
8
+ exports.loadConfig = loadConfig;
9
+ exports.saveConfig = saveConfig;
10
+ exports.requireConfig = requireConfig;
11
+ exports.ensureConfig = ensureConfig;
12
+ exports.loadInstalled = loadInstalled;
13
+ exports.saveInstalled = saveInstalled;
14
+ const fs_1 = __importDefault(require("fs"));
15
+ const path_1 = __importDefault(require("path"));
16
+ const os_1 = __importDefault(require("os"));
17
+ const DEFAULT_API_URL = 'https://relayax.com';
18
+ const DEFAULT_INSTALL_PATH = path_1.default.join(os_1.default.homedir(), '.claude');
19
+ const RELAY_DIR = path_1.default.join(os_1.default.homedir(), '.relay');
20
+ const CONFIG_FILE = path_1.default.join(RELAY_DIR, 'config.json');
21
+ const INSTALLED_FILE = path_1.default.join(RELAY_DIR, 'installed.json');
22
+ function getRelayDir() {
23
+ return RELAY_DIR;
24
+ }
25
+ function ensureRelayDir() {
26
+ if (!fs_1.default.existsSync(RELAY_DIR)) {
27
+ fs_1.default.mkdirSync(RELAY_DIR, { recursive: true });
28
+ }
29
+ }
30
+ function loadConfig() {
31
+ if (!fs_1.default.existsSync(CONFIG_FILE)) {
32
+ return null;
33
+ }
34
+ try {
35
+ const raw = fs_1.default.readFileSync(CONFIG_FILE, 'utf-8');
36
+ return JSON.parse(raw);
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ function saveConfig(config) {
43
+ ensureRelayDir();
44
+ fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
45
+ }
46
+ function requireConfig() {
47
+ const config = loadConfig();
48
+ if (!config) {
49
+ console.error(JSON.stringify({
50
+ error: 'NOT_INITIALIZED',
51
+ message: 'relay가 초기화되지 않았습니다. `relay init`을 먼저 실행하세요.',
52
+ }));
53
+ process.exit(1);
54
+ }
55
+ return config;
56
+ }
57
+ /**
58
+ * npx 첫 실행처럼 config가 없을 때 기본값으로 자동 초기화한다.
59
+ * 이미 config가 있으면 그대로 반환한다.
60
+ */
61
+ function ensureConfig() {
62
+ const existing = loadConfig();
63
+ if (existing)
64
+ return existing;
65
+ const config = {
66
+ install_path: DEFAULT_INSTALL_PATH,
67
+ api_url: DEFAULT_API_URL,
68
+ };
69
+ if (!fs_1.default.existsSync(config.install_path)) {
70
+ fs_1.default.mkdirSync(config.install_path, { recursive: true });
71
+ }
72
+ saveConfig(config);
73
+ console.error(`relay 초기 설정 완료 (${config.install_path}에 설치)`);
74
+ return config;
75
+ }
76
+ function loadInstalled() {
77
+ if (!fs_1.default.existsSync(INSTALLED_FILE)) {
78
+ return {};
79
+ }
80
+ try {
81
+ const raw = fs_1.default.readFileSync(INSTALLED_FILE, 'utf-8');
82
+ return JSON.parse(raw);
83
+ }
84
+ catch {
85
+ return {};
86
+ }
87
+ }
88
+ function saveInstalled(registry) {
89
+ ensureRelayDir();
90
+ fs_1.default.writeFileSync(INSTALLED_FILE, JSON.stringify(registry, null, 2));
91
+ }
@@ -0,0 +1,2 @@
1
+ export declare function installTeam(extractedDir: string, installPath: string): string[];
2
+ export declare function uninstallTeam(files: string[]): string[];
@@ -0,0 +1,52 @@
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.installTeam = installTeam;
7
+ exports.uninstallTeam = uninstallTeam;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const COPY_DIRS = ['skills', 'agents', 'rules', 'commands'];
11
+ function copyDirRecursive(src, dest) {
12
+ const copiedFiles = [];
13
+ if (!fs_1.default.existsSync(src))
14
+ return copiedFiles;
15
+ fs_1.default.mkdirSync(dest, { recursive: true });
16
+ for (const entry of fs_1.default.readdirSync(src, { withFileTypes: true })) {
17
+ const srcPath = path_1.default.join(src, entry.name);
18
+ const destPath = path_1.default.join(dest, entry.name);
19
+ if (entry.isDirectory()) {
20
+ copiedFiles.push(...copyDirRecursive(srcPath, destPath));
21
+ }
22
+ else {
23
+ fs_1.default.copyFileSync(srcPath, destPath);
24
+ copiedFiles.push(destPath);
25
+ }
26
+ }
27
+ return copiedFiles;
28
+ }
29
+ function installTeam(extractedDir, installPath) {
30
+ const installedFiles = [];
31
+ for (const dir of COPY_DIRS) {
32
+ const srcDir = path_1.default.join(extractedDir, dir);
33
+ const destDir = path_1.default.join(installPath, dir);
34
+ installedFiles.push(...copyDirRecursive(srcDir, destDir));
35
+ }
36
+ return installedFiles;
37
+ }
38
+ function uninstallTeam(files) {
39
+ const removed = [];
40
+ for (const file of files) {
41
+ try {
42
+ if (fs_1.default.existsSync(file)) {
43
+ fs_1.default.unlinkSync(file);
44
+ removed.push(file);
45
+ }
46
+ }
47
+ catch {
48
+ // best-effort removal
49
+ }
50
+ }
51
+ return removed;
52
+ }
@@ -0,0 +1,4 @@
1
+ export declare function downloadPackage(url: string, destDir: string): Promise<string>;
2
+ export declare function extractPackage(tarPath: string, destDir: string): Promise<void>;
3
+ export declare function makeTempDir(): string;
4
+ export declare function removeTempDir(dir: string): void;
@@ -0,0 +1,43 @@
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.downloadPackage = downloadPackage;
7
+ exports.extractPackage = extractPackage;
8
+ exports.makeTempDir = makeTempDir;
9
+ exports.removeTempDir = removeTempDir;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const os_1 = __importDefault(require("os"));
13
+ const fs_2 = require("fs");
14
+ const promises_1 = require("stream/promises");
15
+ const stream_1 = require("stream");
16
+ const tar_1 = require("tar");
17
+ async function downloadPackage(url, destDir) {
18
+ const res = await fetch(url);
19
+ if (!res.ok) {
20
+ throw new Error(`패키지 다운로드 실패 (${res.status}): ${url}`);
21
+ }
22
+ const fileName = path_1.default.basename(new URL(url).pathname) || 'package.tar.gz';
23
+ const destPath = path_1.default.join(destDir, fileName);
24
+ const body = res.body;
25
+ if (!body) {
26
+ throw new Error('응답 본문이 비어 있습니다');
27
+ }
28
+ const nodeReadable = stream_1.Readable.fromWeb(body);
29
+ await (0, promises_1.pipeline)(nodeReadable, (0, fs_2.createWriteStream)(destPath));
30
+ return destPath;
31
+ }
32
+ async function extractPackage(tarPath, destDir) {
33
+ if (!fs_1.default.existsSync(destDir)) {
34
+ fs_1.default.mkdirSync(destDir, { recursive: true });
35
+ }
36
+ await (0, tar_1.extract)({ file: tarPath, cwd: destDir, strip: 1 });
37
+ }
38
+ function makeTempDir() {
39
+ return fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'relay-'));
40
+ }
41
+ function removeTempDir(dir) {
42
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
43
+ }
@@ -0,0 +1,35 @@
1
+ export interface RelayConfig {
2
+ install_path: string;
3
+ api_url: string;
4
+ token?: string;
5
+ }
6
+ export interface InstalledTeam {
7
+ version: string;
8
+ installed_at: string;
9
+ files: string[];
10
+ }
11
+ export interface InstalledRegistry {
12
+ [slug: string]: InstalledTeam;
13
+ }
14
+ export interface TeamRegistryInfo {
15
+ slug: string;
16
+ name: string;
17
+ version: string;
18
+ package_url: string;
19
+ commands: {
20
+ name: string;
21
+ description: string;
22
+ }[];
23
+ components: {
24
+ agents: number;
25
+ rules: number;
26
+ skills: number;
27
+ };
28
+ }
29
+ export interface SearchResult {
30
+ slug: string;
31
+ name: string;
32
+ description: string;
33
+ commands: string[];
34
+ install_count: number;
35
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "relayax-cli",
3
+ "version": "0.1.0",
4
+ "description": "RelayAX Agent Team Marketplace CLI - Install and manage agent teams",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "relay": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "dev": "tsc --watch",
16
+ "start": "node dist/index.js",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "keywords": ["agent", "skills", "marketplace", "cli", "claude", "llm", "ai-agent", "relayax"],
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/anthropics/relayax-cli"
24
+ },
25
+ "homepage": "https://relayax.com",
26
+ "dependencies": {
27
+ "commander": "^13.1.0",
28
+ "tar": "^7.4.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^20",
32
+ "typescript": "^5"
33
+ }
34
+ }