relayax-cli 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/init.js +95 -52
- package/dist/commands/install.js +1 -1
- package/dist/commands/publish.js +80 -65
- package/dist/index.js +2 -0
- package/dist/lib/ai-tools.d.ts +14 -0
- package/dist/lib/ai-tools.js +45 -0
- package/dist/lib/command-adapter.d.ts +21 -0
- package/dist/lib/command-adapter.js +89 -0
- package/dist/lib/config.d.ts +4 -2
- package/dist/lib/config.js +11 -3
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -6,74 +6,117 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.registerInit = registerInit;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
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;
|
|
19
24
|
}
|
|
20
25
|
function registerInit(program) {
|
|
21
26
|
program
|
|
22
27
|
.command('init')
|
|
23
|
-
.description('
|
|
24
|
-
.option('--
|
|
25
|
-
.option('--api-url <url>', `API URL (기본값: ${DEFAULT_API_URL})`)
|
|
28
|
+
.description('에이전트 CLI를 감지하고 relay 슬래시 커맨드를 설치합니다')
|
|
29
|
+
.option('--tools <tools>', '설치할 에이전트 CLI 지정 (all 또는 쉼표 구분)')
|
|
26
30
|
.action(async (opts) => {
|
|
27
31
|
const pretty = program.opts().pretty ?? false;
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
else if (!pretty) {
|
|
34
|
-
// non-pretty / agent mode: use default
|
|
35
|
-
install_path = PRESET_PATHS['1'];
|
|
32
|
+
const projectPath = process.cwd();
|
|
33
|
+
// 1. 에이전트 CLI 감지 또는 --tools 지정
|
|
34
|
+
let targetToolIds;
|
|
35
|
+
if (opts.tools) {
|
|
36
|
+
targetToolIds = resolveTools(opts.tools);
|
|
36
37
|
}
|
|
37
38
|
else {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
const detected = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
|
|
40
|
+
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
|
+
}
|
|
50
|
+
process.exit(1);
|
|
49
51
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
targetToolIds = detected.map((t) => t.value);
|
|
53
|
+
}
|
|
54
|
+
// 2. 각 에이전트 CLI에 슬래시 커맨드 설치
|
|
55
|
+
const results = [];
|
|
56
|
+
for (const toolId of targetToolIds) {
|
|
57
|
+
const tool = ai_tools_js_1.AI_TOOLS.find((t) => t.value === toolId);
|
|
58
|
+
if (!tool)
|
|
59
|
+
continue;
|
|
60
|
+
const adapter = (0, command_adapter_js_1.createAdapter)(tool);
|
|
61
|
+
const installedCommands = [];
|
|
62
|
+
for (const cmd of command_adapter_js_1.RELAY_COMMANDS) {
|
|
63
|
+
const filePath = path_1.default.join(projectPath, adapter.getFilePath(cmd.id));
|
|
64
|
+
const fileContent = adapter.formatFile(cmd);
|
|
65
|
+
fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
|
|
66
|
+
fs_1.default.writeFileSync(filePath, fileContent);
|
|
67
|
+
installedCommands.push(cmd.id);
|
|
52
68
|
}
|
|
53
|
-
|
|
69
|
+
results.push({ tool: tool.name, commands: installedCommands });
|
|
54
70
|
}
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
// 3. relay.yaml 생성 (팀 패키지 구조가 있는 경우)
|
|
72
|
+
const relayYamlPath = path_1.default.join(projectPath, 'relay.yaml');
|
|
73
|
+
let relayYamlStatus = 'skipped';
|
|
74
|
+
if (fs_1.default.existsSync(relayYamlPath)) {
|
|
75
|
+
relayYamlStatus = 'exists';
|
|
58
76
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
77
|
+
else {
|
|
78
|
+
const hasTeamDirs = VALID_TEAM_DIRS.some((d) => {
|
|
79
|
+
const dirPath = path_1.default.join(projectPath, d);
|
|
80
|
+
if (!fs_1.default.existsSync(dirPath))
|
|
81
|
+
return false;
|
|
82
|
+
return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.')).length > 0;
|
|
83
|
+
});
|
|
84
|
+
if (hasTeamDirs) {
|
|
85
|
+
const dirName = path_1.default.basename(projectPath);
|
|
86
|
+
const yaml = [
|
|
87
|
+
`name: "${dirName}"`,
|
|
88
|
+
`slug: "${dirName}"`,
|
|
89
|
+
`description: ""`,
|
|
90
|
+
`version: "1.0.0"`,
|
|
91
|
+
`tags: []`,
|
|
92
|
+
].join('\n') + '\n';
|
|
93
|
+
fs_1.default.writeFileSync(relayYamlPath, yaml);
|
|
94
|
+
relayYamlStatus = 'created';
|
|
95
|
+
}
|
|
62
96
|
}
|
|
63
|
-
|
|
64
|
-
(0, config_js_1.saveConfig)({ install_path, api_url });
|
|
65
|
-
const result = {
|
|
66
|
-
status: 'ok',
|
|
67
|
-
install_path,
|
|
68
|
-
api_url,
|
|
69
|
-
};
|
|
97
|
+
// 4. 출력
|
|
70
98
|
if (pretty) {
|
|
71
|
-
console.log('\n\x1b[32m✓ relay 초기화 완료\x1b[0m');
|
|
72
|
-
|
|
73
|
-
|
|
99
|
+
console.log('\n\x1b[32m✓ relay 초기화 완료\x1b[0m\n');
|
|
100
|
+
for (const r of results) {
|
|
101
|
+
console.log(` \x1b[36m${r.tool}\x1b[0m`);
|
|
102
|
+
for (const cmd of r.commands) {
|
|
103
|
+
console.log(` /${cmd}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (relayYamlStatus === 'created') {
|
|
107
|
+
console.log(`\n relay.yaml 생성됨 (팀 패키지 구조 감지)`);
|
|
108
|
+
}
|
|
109
|
+
else if (relayYamlStatus === 'exists') {
|
|
110
|
+
console.log(`\n relay.yaml 이미 존재`);
|
|
111
|
+
}
|
|
112
|
+
console.log('\n IDE를 재시작하면 슬래시 커맨드가 활성화됩니다.');
|
|
74
113
|
}
|
|
75
114
|
else {
|
|
76
|
-
console.log(JSON.stringify(
|
|
115
|
+
console.log(JSON.stringify({
|
|
116
|
+
status: 'ok',
|
|
117
|
+
tools: results,
|
|
118
|
+
relay_yaml: relayYamlStatus,
|
|
119
|
+
}));
|
|
77
120
|
}
|
|
78
121
|
});
|
|
79
122
|
}
|
package/dist/commands/install.js
CHANGED
|
@@ -8,7 +8,7 @@ const config_js_1 = require("../lib/config.js");
|
|
|
8
8
|
function registerInstall(program) {
|
|
9
9
|
program
|
|
10
10
|
.command('install <slug>')
|
|
11
|
-
.description('에이전트 팀 설치 (
|
|
11
|
+
.description('에이전트 팀 설치 (감지된 에이전트 CLI에 설치)')
|
|
12
12
|
.option('--path <install_path>', '설치 경로 지정 (기본: ./.claude)')
|
|
13
13
|
.action(async (slug, opts) => {
|
|
14
14
|
const pretty = program.opts().pretty ?? false;
|
package/dist/commands/publish.js
CHANGED
|
@@ -7,16 +7,44 @@ exports.registerPublish = registerPublish;
|
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const os_1 = __importDefault(require("os"));
|
|
10
|
-
const readline_1 = require("readline");
|
|
11
10
|
const tar_1 = require("tar");
|
|
12
11
|
const config_js_1 = require("../lib/config.js");
|
|
13
12
|
const VALID_DIRS = ['skills', 'agents', 'rules', 'commands'];
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
.
|
|
13
|
+
function parseRelayYaml(content) {
|
|
14
|
+
const result = {};
|
|
15
|
+
const tags = [];
|
|
16
|
+
let inTags = false;
|
|
17
|
+
for (const line of content.split('\n')) {
|
|
18
|
+
const trimmed = line.trim();
|
|
19
|
+
if (inTags) {
|
|
20
|
+
if (trimmed.startsWith('- ')) {
|
|
21
|
+
tags.push(trimmed.slice(2).replace(/^["']|["']$/g, ''));
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
inTags = false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (trimmed === 'tags: []') {
|
|
29
|
+
result.tags = [];
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (trimmed === 'tags:') {
|
|
33
|
+
inTags = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const match = trimmed.match(/^(\w+):\s*["']?(.+?)["']?$/);
|
|
37
|
+
if (match) {
|
|
38
|
+
result[match[1]] = match[2];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
name: String(result.name ?? ''),
|
|
43
|
+
slug: String(result.slug ?? ''),
|
|
44
|
+
description: String(result.description ?? ''),
|
|
45
|
+
version: String(result.version ?? '1.0.0'),
|
|
46
|
+
tags,
|
|
47
|
+
};
|
|
20
48
|
}
|
|
21
49
|
function detectCommands(teamDir) {
|
|
22
50
|
const cmdDir = path_1.default.join(teamDir, 'commands');
|
|
@@ -30,7 +58,6 @@ function detectCommands(teamDir) {
|
|
|
30
58
|
try {
|
|
31
59
|
const content = fs_1.default.readFileSync(path_1.default.join(cmdDir, file), 'utf-8');
|
|
32
60
|
const lines = content.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
33
|
-
// Check frontmatter for description
|
|
34
61
|
if (lines[0] === '---') {
|
|
35
62
|
const endIdx = lines.indexOf('---', 1);
|
|
36
63
|
if (endIdx > 0) {
|
|
@@ -41,7 +68,6 @@ function detectCommands(teamDir) {
|
|
|
41
68
|
}
|
|
42
69
|
}
|
|
43
70
|
else if (lines[0]) {
|
|
44
|
-
// Use first line, strip leading # if heading
|
|
45
71
|
description = lines[0].replace(/^#+\s*/, '');
|
|
46
72
|
}
|
|
47
73
|
}
|
|
@@ -58,34 +84,24 @@ function countDir(teamDir, dirName) {
|
|
|
58
84
|
return 0;
|
|
59
85
|
return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.')).length;
|
|
60
86
|
}
|
|
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
87
|
async function createTarball(teamDir) {
|
|
72
88
|
const tmpFile = path_1.default.join(os_1.default.tmpdir(), `relay-publish-${Date.now()}.tar.gz`);
|
|
73
|
-
|
|
74
|
-
const
|
|
89
|
+
// Only include valid dirs that exist
|
|
90
|
+
const dirsToInclude = VALID_DIRS.filter((d) => fs_1.default.existsSync(path_1.default.join(teamDir, d)));
|
|
75
91
|
await (0, tar_1.create)({
|
|
76
92
|
gzip: true,
|
|
77
93
|
file: tmpFile,
|
|
78
|
-
cwd:
|
|
79
|
-
}, [
|
|
94
|
+
cwd: teamDir,
|
|
95
|
+
}, [...dirsToInclude]);
|
|
80
96
|
return tmpFile;
|
|
81
97
|
}
|
|
82
|
-
async function publishToApi(
|
|
98
|
+
async function publishToApi(token, tarPath, metadata) {
|
|
83
99
|
const fileBuffer = fs_1.default.readFileSync(tarPath);
|
|
84
100
|
const blob = new Blob([fileBuffer], { type: 'application/gzip' });
|
|
85
101
|
const form = new FormData();
|
|
86
102
|
form.append('package', blob, `${metadata.slug}-${metadata.version}.tar.gz`);
|
|
87
103
|
form.append('metadata', JSON.stringify(metadata));
|
|
88
|
-
const res = await fetch(`${
|
|
104
|
+
const res = await fetch(`${config_js_1.API_URL}/api/publish`, {
|
|
89
105
|
method: 'POST',
|
|
90
106
|
headers: { Authorization: `Bearer ${token}` },
|
|
91
107
|
body: form,
|
|
@@ -99,27 +115,42 @@ async function publishToApi(apiUrl, token, tarPath, metadata) {
|
|
|
99
115
|
}
|
|
100
116
|
function registerPublish(program) {
|
|
101
117
|
program
|
|
102
|
-
.command('publish
|
|
103
|
-
.description('
|
|
104
|
-
.option('--
|
|
105
|
-
.
|
|
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) => {
|
|
118
|
+
.command('publish')
|
|
119
|
+
.description('현재 팀 패키지를 마켓플레이스에 배포합니다 (relay.yaml 필요)')
|
|
120
|
+
.option('--token <token>', '인증 토큰')
|
|
121
|
+
.action(async (opts) => {
|
|
111
122
|
const pretty = program.opts().pretty ?? false;
|
|
112
|
-
const teamDir =
|
|
113
|
-
|
|
114
|
-
|
|
123
|
+
const teamDir = process.cwd();
|
|
124
|
+
const relayYamlPath = path_1.default.join(teamDir, 'relay.yaml');
|
|
125
|
+
// Check relay.yaml exists
|
|
126
|
+
if (!fs_1.default.existsSync(relayYamlPath)) {
|
|
127
|
+
console.error(JSON.stringify({
|
|
128
|
+
error: 'NOT_INITIALIZED',
|
|
129
|
+
message: 'relay.yaml이 없습니다. 먼저 `relay init`을 실행하세요.',
|
|
130
|
+
}));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
// Parse relay.yaml
|
|
134
|
+
const yamlContent = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
|
|
135
|
+
const config = parseRelayYaml(yamlContent);
|
|
136
|
+
if (!config.slug || !config.name || !config.description) {
|
|
137
|
+
console.error(JSON.stringify({
|
|
138
|
+
error: 'INVALID_CONFIG',
|
|
139
|
+
message: 'relay.yaml에 name, slug, description이 필요합니다.',
|
|
140
|
+
}));
|
|
115
141
|
process.exit(1);
|
|
116
142
|
}
|
|
117
143
|
// Validate structure
|
|
118
|
-
const hasDirs = VALID_DIRS.some((d) =>
|
|
144
|
+
const hasDirs = VALID_DIRS.some((d) => {
|
|
145
|
+
const dirPath = path_1.default.join(teamDir, d);
|
|
146
|
+
if (!fs_1.default.existsSync(dirPath))
|
|
147
|
+
return false;
|
|
148
|
+
return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.')).length > 0;
|
|
149
|
+
});
|
|
119
150
|
if (!hasDirs) {
|
|
120
151
|
console.error(JSON.stringify({
|
|
121
|
-
error: '
|
|
122
|
-
message:
|
|
152
|
+
error: 'EMPTY_PACKAGE',
|
|
153
|
+
message: 'skills/, agents/, rules/, commands/ 중 하나 이상에 파일이 있어야 합니다.',
|
|
123
154
|
}));
|
|
124
155
|
process.exit(1);
|
|
125
156
|
}
|
|
@@ -128,43 +159,27 @@ function registerPublish(program) {
|
|
|
128
159
|
if (!token) {
|
|
129
160
|
console.error(JSON.stringify({
|
|
130
161
|
error: 'NO_TOKEN',
|
|
131
|
-
message: '
|
|
162
|
+
message: '인증이 필요합니다. `relay login`을 먼저 실행하세요.',
|
|
132
163
|
}));
|
|
133
164
|
process.exit(1);
|
|
134
165
|
}
|
|
135
|
-
// Auto-detect
|
|
136
|
-
const autoSlug = slugify(path_1.default.basename(teamDir));
|
|
137
166
|
const detectedCommands = detectCommands(teamDir);
|
|
138
167
|
const components = {
|
|
139
168
|
agents: countDir(teamDir, 'agents'),
|
|
140
169
|
rules: countDir(teamDir, 'rules'),
|
|
141
170
|
skills: countDir(teamDir, 'skills'),
|
|
142
171
|
};
|
|
143
|
-
// Gather metadata (prompt if not provided)
|
|
144
|
-
const isInteractive = process.stdin.isTTY;
|
|
145
|
-
const slug = opts.slug ?? (isInteractive ? await prompt('슬러그', autoSlug) : autoSlug);
|
|
146
|
-
const name = opts.name ?? (isInteractive ? await prompt('팀 이름', path_1.default.basename(teamDir)) : path_1.default.basename(teamDir));
|
|
147
|
-
const description = opts.description ?? (isInteractive ? await prompt('한 줄 설명') : '');
|
|
148
|
-
let tags = opts.tag;
|
|
149
|
-
if (tags.length === 0 && isInteractive) {
|
|
150
|
-
const tagsInput = await prompt('태그 (쉼표 구분)', '');
|
|
151
|
-
tags = tagsInput ? tagsInput.split(',').map((t) => t.trim()).filter(Boolean) : [];
|
|
152
|
-
}
|
|
153
|
-
if (!description) {
|
|
154
|
-
console.error(JSON.stringify({ error: 'MISSING_DESCRIPTION', message: 'description은 필수입니다' }));
|
|
155
|
-
process.exit(1);
|
|
156
|
-
}
|
|
157
172
|
const metadata = {
|
|
158
|
-
slug,
|
|
159
|
-
name,
|
|
160
|
-
description,
|
|
161
|
-
tags,
|
|
173
|
+
slug: config.slug,
|
|
174
|
+
name: config.name,
|
|
175
|
+
description: config.description,
|
|
176
|
+
tags: config.tags,
|
|
162
177
|
commands: detectedCommands,
|
|
163
178
|
components,
|
|
164
|
-
version:
|
|
179
|
+
version: config.version,
|
|
165
180
|
};
|
|
166
181
|
if (pretty) {
|
|
167
|
-
console.error(`패키지 생성
|
|
182
|
+
console.error(`패키지 생성 중... (${config.name} v${config.version})`);
|
|
168
183
|
}
|
|
169
184
|
let tarPath = null;
|
|
170
185
|
try {
|
|
@@ -172,9 +187,9 @@ function registerPublish(program) {
|
|
|
172
187
|
if (pretty) {
|
|
173
188
|
console.error(`업로드 중...`);
|
|
174
189
|
}
|
|
175
|
-
const result = await publishToApi(
|
|
190
|
+
const result = await publishToApi(token, tarPath, metadata);
|
|
176
191
|
if (pretty) {
|
|
177
|
-
console.log(`\n\x1b[32m✓ ${name} 배포 완료\x1b[0m v${result.version}`);
|
|
192
|
+
console.log(`\n\x1b[32m✓ ${config.name} 배포 완료\x1b[0m v${result.version}`);
|
|
178
193
|
console.log(` 슬러그: \x1b[36m${result.slug}\x1b[0m`);
|
|
179
194
|
console.log(` URL: \x1b[36m${result.url}\x1b[0m`);
|
|
180
195
|
}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const commander_1 = require("commander");
|
|
5
|
+
const init_js_1 = require("./commands/init.js");
|
|
5
6
|
const search_js_1 = require("./commands/search.js");
|
|
6
7
|
const install_js_1 = require("./commands/install.js");
|
|
7
8
|
const list_js_1 = require("./commands/list.js");
|
|
@@ -16,6 +17,7 @@ program
|
|
|
16
17
|
.description('RelayAX Agent Team Marketplace CLI')
|
|
17
18
|
.version(pkg.version)
|
|
18
19
|
.option('--pretty', '인간 친화적 출력 (기본값: JSON)');
|
|
20
|
+
(0, init_js_1.registerInit)(program);
|
|
19
21
|
(0, search_js_1.registerSearch)(program);
|
|
20
22
|
(0, install_js_1.registerInstall)(program);
|
|
21
23
|
(0, list_js_1.registerList)(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,89 @@
|
|
|
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> --pretty\` 명령어를 실행합니다.
|
|
60
|
+
2. 설치 결과를 확인합니다:
|
|
61
|
+
- 설치된 파일 수
|
|
62
|
+
- 사용 가능해진 커맨드 목록
|
|
63
|
+
3. 각 커맨드의 사용법을 간단히 안내합니다.
|
|
64
|
+
4. "바로 사용해볼까요?"라고 제안합니다.
|
|
65
|
+
5. 사용자가 원하면 첫 번째 커맨드를 실행해봅니다.
|
|
66
|
+
|
|
67
|
+
## 예시
|
|
68
|
+
|
|
69
|
+
사용자: /relay-install contents-team
|
|
70
|
+
→ relay install contents-team --pretty 실행
|
|
71
|
+
→ "설치 완료! 다음 커맨드를 사용할 수 있습니다:"
|
|
72
|
+
→ /cardnews - 카드뉴스 제작
|
|
73
|
+
→ /detailpage - 상세페이지 제작
|
|
74
|
+
→ "바로 /cardnews를 사용해볼까요?"`,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'relay-publish',
|
|
78
|
+
description: '현재 팀 패키지를 relay 마켓플레이스에 배포합니다',
|
|
79
|
+
body: `현재 디렉토리의 에이전트 팀을 relay 마켓플레이스에 배포합니다.
|
|
80
|
+
|
|
81
|
+
## 실행 방법
|
|
82
|
+
|
|
83
|
+
1. relay.yaml이 있는지 확인합니다. 없으면 사용자에게 팀 정보를 물어보고 생성합니다.
|
|
84
|
+
2. skills/, agents/, rules/, commands/ 디렉토리 구조를 확인합니다.
|
|
85
|
+
3. \`relay login\`으로 인증 상태를 확인합니다. 미인증이면 로그인을 안내합니다.
|
|
86
|
+
4. \`relay publish --pretty\` 명령어를 실행합니다.
|
|
87
|
+
5. 배포 결과와 마켓플레이스 URL을 보여줍니다.`,
|
|
88
|
+
},
|
|
89
|
+
];
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { InstalledRegistry } from '../types.js';
|
|
2
2
|
export declare const API_URL = "https://relayax.com";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* --path
|
|
4
|
+
* 설치 경로를 결정한다.
|
|
5
|
+
* 1. --path 옵션이 있으면 그대로 사용
|
|
6
|
+
* 2. 에이전트 CLI 자동 감지 → 감지된 경로 사용
|
|
7
|
+
* 3. 감지 안 되면 현재 디렉토리에 직접 설치
|
|
6
8
|
*/
|
|
7
9
|
export declare function getInstallPath(override?: string): string;
|
|
8
10
|
export declare function ensureRelayDir(): void;
|
package/dist/lib/config.js
CHANGED
|
@@ -13,12 +13,15 @@ exports.saveInstalled = saveInstalled;
|
|
|
13
13
|
const fs_1 = __importDefault(require("fs"));
|
|
14
14
|
const path_1 = __importDefault(require("path"));
|
|
15
15
|
const os_1 = __importDefault(require("os"));
|
|
16
|
+
const ai_tools_js_1 = require("./ai-tools.js");
|
|
16
17
|
exports.API_URL = 'https://relayax.com';
|
|
17
18
|
const RELAY_DIR = path_1.default.join(os_1.default.homedir(), '.relay');
|
|
18
19
|
const INSTALLED_FILE = path_1.default.join(RELAY_DIR, 'installed.json');
|
|
19
20
|
/**
|
|
20
|
-
*
|
|
21
|
-
* --path
|
|
21
|
+
* 설치 경로를 결정한다.
|
|
22
|
+
* 1. --path 옵션이 있으면 그대로 사용
|
|
23
|
+
* 2. 에이전트 CLI 자동 감지 → 감지된 경로 사용
|
|
24
|
+
* 3. 감지 안 되면 현재 디렉토리에 직접 설치
|
|
22
25
|
*/
|
|
23
26
|
function getInstallPath(override) {
|
|
24
27
|
if (override) {
|
|
@@ -27,7 +30,12 @@ function getInstallPath(override) {
|
|
|
27
30
|
: path_1.default.resolve(override);
|
|
28
31
|
return resolved;
|
|
29
32
|
}
|
|
30
|
-
|
|
33
|
+
const cwd = process.cwd();
|
|
34
|
+
const detected = (0, ai_tools_js_1.detectAgentCLIs)(cwd);
|
|
35
|
+
if (detected.length >= 1) {
|
|
36
|
+
return path_1.default.join(cwd, detected[0].skillsDir);
|
|
37
|
+
}
|
|
38
|
+
return cwd;
|
|
31
39
|
}
|
|
32
40
|
function ensureRelayDir() {
|
|
33
41
|
if (!fs_1.default.existsSync(RELAY_DIR)) {
|