relayax-cli 0.2.40 → 0.2.41
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/package.d.ts +2 -0
- package/dist/commands/package.js +287 -0
- package/dist/commands/publish.js +12 -1
- package/dist/commands/spaces.d.ts +0 -1
- package/dist/index.js +2 -0
- package/dist/lib/command-adapter.js +132 -36
- package/dist/lib/guide.d.ts +13 -5
- package/dist/lib/guide.js +142 -50
- package/package.json +1 -1
|
@@ -0,0 +1,287 @@
|
|
|
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.registerPackage = registerPackage;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
10
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
11
|
+
const ai_tools_js_1 = require("../lib/ai-tools.js");
|
|
12
|
+
const SYNC_DIRS = ['skills', 'commands', 'agents', 'rules'];
|
|
13
|
+
const EXCLUDE_SUBDIRS = ['relay']; // relay CLI 전용 하위 디렉토리 제외
|
|
14
|
+
// ─── Helpers ───
|
|
15
|
+
function fileHash(filePath) {
|
|
16
|
+
const content = fs_1.default.readFileSync(filePath);
|
|
17
|
+
return crypto_1.default.createHash('md5').update(content).digest('hex');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 디렉토리를 재귀 탐색하여 파일 목록을 반환한다.
|
|
21
|
+
* baseDir 기준 상대 경로 + 해시.
|
|
22
|
+
*/
|
|
23
|
+
function scanDir(baseDir, subDir) {
|
|
24
|
+
const fullDir = path_1.default.join(baseDir, subDir);
|
|
25
|
+
if (!fs_1.default.existsSync(fullDir))
|
|
26
|
+
return [];
|
|
27
|
+
const entries = [];
|
|
28
|
+
function walk(dir) {
|
|
29
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
30
|
+
if (entry.name.startsWith('.'))
|
|
31
|
+
continue;
|
|
32
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
walk(fullPath);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const relPath = path_1.default.relative(baseDir, fullPath);
|
|
38
|
+
entries.push({ relPath, hash: fileHash(fullPath) });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
walk(fullDir);
|
|
43
|
+
return entries;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 소스 디렉토리(예: .claude/)에서 배포 가능한 콘텐츠를 스캔한다.
|
|
47
|
+
* relay/ 하위 디렉토리는 제외.
|
|
48
|
+
*/
|
|
49
|
+
function scanSource(projectPath, tool) {
|
|
50
|
+
const sourceBase = path_1.default.join(projectPath, tool.skillsDir);
|
|
51
|
+
const files = [];
|
|
52
|
+
const summary = {};
|
|
53
|
+
for (const dir of SYNC_DIRS) {
|
|
54
|
+
const fullDir = path_1.default.join(sourceBase, dir);
|
|
55
|
+
if (!fs_1.default.existsSync(fullDir))
|
|
56
|
+
continue;
|
|
57
|
+
// 제외 대상 필터링 (예: commands/relay/)
|
|
58
|
+
const dirEntries = fs_1.default.readdirSync(fullDir, { withFileTypes: true });
|
|
59
|
+
let count = 0;
|
|
60
|
+
for (const entry of dirEntries) {
|
|
61
|
+
if (entry.name.startsWith('.'))
|
|
62
|
+
continue;
|
|
63
|
+
if (entry.isDirectory() && EXCLUDE_SUBDIRS.includes(entry.name))
|
|
64
|
+
continue;
|
|
65
|
+
const entryPath = path_1.default.join(fullDir, entry.name);
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
// 하위 파일 재귀 탐색
|
|
68
|
+
const subFiles = scanDir(sourceBase, path_1.default.join(dir, entry.name));
|
|
69
|
+
// relPath를 sourceBase 기준 → SYNC_DIRS 기준으로 유지
|
|
70
|
+
files.push(...subFiles);
|
|
71
|
+
count += subFiles.length > 0 ? 1 : 0; // 디렉토리 단위로 카운트
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const relPath = path_1.default.relative(sourceBase, entryPath);
|
|
75
|
+
files.push({ relPath, hash: fileHash(entryPath) });
|
|
76
|
+
count++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (count > 0)
|
|
80
|
+
summary[dir] = count;
|
|
81
|
+
}
|
|
82
|
+
return { tool, files, summary };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* .relay/ 디렉토리의 현재 콘텐츠를 스캔한다.
|
|
86
|
+
*/
|
|
87
|
+
function scanRelay(relayDir) {
|
|
88
|
+
const files = [];
|
|
89
|
+
for (const dir of SYNC_DIRS) {
|
|
90
|
+
files.push(...scanDir(relayDir, dir));
|
|
91
|
+
}
|
|
92
|
+
return files;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 소스와 .relay/를 비교하여 diff를 생성한다.
|
|
96
|
+
*/
|
|
97
|
+
function computeDiff(sourceFiles, relayFiles) {
|
|
98
|
+
const relayMap = new Map(relayFiles.map((f) => [f.relPath, f.hash]));
|
|
99
|
+
const sourceMap = new Map(sourceFiles.map((f) => [f.relPath, f.hash]));
|
|
100
|
+
const diff = [];
|
|
101
|
+
// 소스에 있는 파일
|
|
102
|
+
for (const [relPath, hash] of sourceMap) {
|
|
103
|
+
const relayHash = relayMap.get(relPath);
|
|
104
|
+
if (!relayHash) {
|
|
105
|
+
diff.push({ relPath, status: 'added' });
|
|
106
|
+
}
|
|
107
|
+
else if (relayHash !== hash) {
|
|
108
|
+
diff.push({ relPath, status: 'modified' });
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
diff.push({ relPath, status: 'unchanged' });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// .relay/에만 있는 파일 (소스에서 삭제됨)
|
|
115
|
+
for (const [relPath] of relayMap) {
|
|
116
|
+
if (!sourceMap.has(relPath)) {
|
|
117
|
+
diff.push({ relPath, status: 'deleted' });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return diff.sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 소스에서 .relay/로 파일을 동기화한다.
|
|
124
|
+
*/
|
|
125
|
+
function syncToRelay(sourceBase, relayDir, diff) {
|
|
126
|
+
for (const entry of diff) {
|
|
127
|
+
const sourcePath = path_1.default.join(sourceBase, entry.relPath);
|
|
128
|
+
const relayPath = path_1.default.join(relayDir, entry.relPath);
|
|
129
|
+
if (entry.status === 'added' || entry.status === 'modified') {
|
|
130
|
+
fs_1.default.mkdirSync(path_1.default.dirname(relayPath), { recursive: true });
|
|
131
|
+
fs_1.default.copyFileSync(sourcePath, relayPath);
|
|
132
|
+
}
|
|
133
|
+
else if (entry.status === 'deleted') {
|
|
134
|
+
if (fs_1.default.existsSync(relayPath)) {
|
|
135
|
+
fs_1.default.unlinkSync(relayPath);
|
|
136
|
+
// 빈 디렉토리 정리
|
|
137
|
+
const parentDir = path_1.default.dirname(relayPath);
|
|
138
|
+
try {
|
|
139
|
+
const remaining = fs_1.default.readdirSync(parentDir).filter((f) => !f.startsWith('.'));
|
|
140
|
+
if (remaining.length === 0)
|
|
141
|
+
fs_1.default.rmdirSync(parentDir);
|
|
142
|
+
}
|
|
143
|
+
catch { /* ignore */ }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// ─── Command ───
|
|
149
|
+
function registerPackage(program) {
|
|
150
|
+
program
|
|
151
|
+
.command('package')
|
|
152
|
+
.description('소스 디렉토리에서 .relay/로 콘텐츠를 동기화합니다')
|
|
153
|
+
.option('--source <dir>', '소스 디렉토리 지정 (예: .claude)')
|
|
154
|
+
.option('--sync', '변경사항을 .relay/에 즉시 반영', false)
|
|
155
|
+
.option('--init', '최초 패키징: 소스 감지 → .relay/ 초기화', false)
|
|
156
|
+
.action(async (opts) => {
|
|
157
|
+
const json = program.opts().json ?? false;
|
|
158
|
+
const projectPath = process.cwd();
|
|
159
|
+
const relayDir = path_1.default.join(projectPath, '.relay');
|
|
160
|
+
const relayYamlPath = path_1.default.join(relayDir, 'relay.yaml');
|
|
161
|
+
// ─── 최초 패키징 (--init) ───
|
|
162
|
+
if (opts.init || !fs_1.default.existsSync(relayYamlPath)) {
|
|
163
|
+
const detected = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
|
|
164
|
+
// 각 도구의 콘텐츠 스캔
|
|
165
|
+
const scans = detected
|
|
166
|
+
.map((tool) => scanSource(projectPath, tool))
|
|
167
|
+
.filter((s) => s.files.length > 0);
|
|
168
|
+
if (json) {
|
|
169
|
+
console.log(JSON.stringify({
|
|
170
|
+
status: 'init_required',
|
|
171
|
+
detected: scans.map((s) => ({
|
|
172
|
+
source: s.tool.skillsDir,
|
|
173
|
+
name: s.tool.name,
|
|
174
|
+
summary: s.summary,
|
|
175
|
+
fileCount: s.files.length,
|
|
176
|
+
})),
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
if (scans.length === 0) {
|
|
181
|
+
console.error('배포 가능한 에이전트 콘텐츠를 찾지 못했습니다.');
|
|
182
|
+
console.error('skills/, commands/, agents/, rules/ 중 하나를 만들어주세요.');
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
console.error('\n프로젝트에서 발견된 에이전트 콘텐츠:\n');
|
|
186
|
+
for (const scan of scans) {
|
|
187
|
+
const parts = Object.entries(scan.summary)
|
|
188
|
+
.map(([dir, count]) => `${dir} ${count}개`)
|
|
189
|
+
.join(', ');
|
|
190
|
+
console.error(` 📁 ${scan.tool.skillsDir}/ — ${parts}`);
|
|
191
|
+
}
|
|
192
|
+
console.error('');
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
// ─── 재패키징 (source 기반 동기화) ───
|
|
197
|
+
const yamlContent = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
|
|
198
|
+
const config = js_yaml_1.default.load(yamlContent);
|
|
199
|
+
const source = opts.source ?? config.source;
|
|
200
|
+
if (!source) {
|
|
201
|
+
if (json) {
|
|
202
|
+
console.log(JSON.stringify({
|
|
203
|
+
status: 'no_source',
|
|
204
|
+
message: 'relay.yaml에 source 필드가 없습니다. --source 옵션으로 지정하거나 relay.yaml에 source를 추가하세요.',
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
console.error('relay.yaml에 source 필드가 없습니다.');
|
|
209
|
+
console.error('--source <dir> 옵션으로 지정하거나 relay.yaml에 source를 추가하세요.');
|
|
210
|
+
}
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
// 소스 디렉토리 존재 확인
|
|
214
|
+
const sourceBase = path_1.default.join(projectPath, source);
|
|
215
|
+
if (!fs_1.default.existsSync(sourceBase)) {
|
|
216
|
+
const msg = `소스 디렉토리 '${source}'를 찾을 수 없습니다.`;
|
|
217
|
+
if (json) {
|
|
218
|
+
console.log(JSON.stringify({ error: 'SOURCE_NOT_FOUND', message: msg }));
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
console.error(msg);
|
|
222
|
+
}
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
// 소스에서 해당 도구 찾기
|
|
226
|
+
const allTools = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
|
|
227
|
+
const tool = allTools.find((t) => t.skillsDir === source);
|
|
228
|
+
const toolName = tool?.name ?? source;
|
|
229
|
+
// diff 계산
|
|
230
|
+
const sourceScan = tool
|
|
231
|
+
? scanSource(projectPath, tool)
|
|
232
|
+
: { tool: { name: source, value: source, skillsDir: source }, files: [], summary: {} };
|
|
233
|
+
// tool이 없으면 직접 스캔
|
|
234
|
+
if (!tool) {
|
|
235
|
+
for (const dir of SYNC_DIRS) {
|
|
236
|
+
const files = scanDir(sourceBase, dir);
|
|
237
|
+
sourceScan.files.push(...files);
|
|
238
|
+
if (files.length > 0)
|
|
239
|
+
sourceScan.summary[dir] = files.length;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const relayFiles = scanRelay(relayDir);
|
|
243
|
+
const diff = computeDiff(sourceScan.files, relayFiles);
|
|
244
|
+
const summary = {
|
|
245
|
+
added: diff.filter((d) => d.status === 'added').length,
|
|
246
|
+
modified: diff.filter((d) => d.status === 'modified').length,
|
|
247
|
+
deleted: diff.filter((d) => d.status === 'deleted').length,
|
|
248
|
+
unchanged: diff.filter((d) => d.status === 'unchanged').length,
|
|
249
|
+
};
|
|
250
|
+
const hasChanges = summary.added + summary.modified + summary.deleted > 0;
|
|
251
|
+
// --sync: 즉시 동기화
|
|
252
|
+
if (opts.sync && hasChanges) {
|
|
253
|
+
syncToRelay(sourceBase, relayDir, diff);
|
|
254
|
+
}
|
|
255
|
+
const result = {
|
|
256
|
+
source,
|
|
257
|
+
sourceName: toolName,
|
|
258
|
+
synced: opts.sync === true && hasChanges,
|
|
259
|
+
diff: diff.filter((d) => d.status !== 'unchanged'),
|
|
260
|
+
summary,
|
|
261
|
+
};
|
|
262
|
+
if (json) {
|
|
263
|
+
console.log(JSON.stringify(result));
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
if (!hasChanges) {
|
|
267
|
+
console.error(`✓ 소스(${source})와 .relay/가 동기화 상태입니다.`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
console.error(`\n📦 소스 동기화 (${source}/ → .relay/)\n`);
|
|
271
|
+
for (const entry of diff) {
|
|
272
|
+
if (entry.status === 'unchanged')
|
|
273
|
+
continue;
|
|
274
|
+
const icon = entry.status === 'added' ? ' 신규' : entry.status === 'modified' ? ' 변경' : ' 삭제';
|
|
275
|
+
console.error(`${icon}: ${entry.relPath}`);
|
|
276
|
+
}
|
|
277
|
+
console.error('');
|
|
278
|
+
console.error(` 합계: 신규 ${summary.added}, 변경 ${summary.modified}, 삭제 ${summary.deleted}, 유지 ${summary.unchanged}`);
|
|
279
|
+
if (opts.sync) {
|
|
280
|
+
console.error(`\n✓ .relay/에 반영 완료`);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
console.error(`\n반영하려면: relay package --sync`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
package/dist/commands/publish.js
CHANGED
|
@@ -43,6 +43,7 @@ function parseRelayYaml(content) {
|
|
|
43
43
|
requires,
|
|
44
44
|
visibility,
|
|
45
45
|
type,
|
|
46
|
+
source: raw.source ? String(raw.source) : undefined,
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
49
|
function detectCommands(teamDir) {
|
|
@@ -245,7 +246,7 @@ async function createTarball(teamDir) {
|
|
|
245
246
|
const dirsToInclude = VALID_DIRS.filter((d) => fs_1.default.existsSync(path_1.default.join(teamDir, d)));
|
|
246
247
|
// Include root-level files if they exist
|
|
247
248
|
const entries = [...dirsToInclude];
|
|
248
|
-
const rootFiles = ['relay.yaml', 'SKILL.md'];
|
|
249
|
+
const rootFiles = ['relay.yaml', 'SKILL.md', 'guide.md'];
|
|
249
250
|
for (const file of rootFiles) {
|
|
250
251
|
if (fs_1.default.existsSync(path_1.default.join(teamDir, file))) {
|
|
251
252
|
entries.push(file);
|
|
@@ -648,6 +649,10 @@ function registerPublish(program) {
|
|
|
648
649
|
console.error('\x1b[33m⚠ GUIDE.html은 더 이상 지원되지 않습니다. 상세페이지가 가이드 역할을 합니다.\x1b[0m');
|
|
649
650
|
console.error(' long_description을 활용하거나 relayax.com에서 팀 정보를 편집하세요.\n');
|
|
650
651
|
}
|
|
652
|
+
// Generate guide.md (consumer install guide)
|
|
653
|
+
const { generateGuide } = await import('../lib/guide.js');
|
|
654
|
+
const guideContent = generateGuide(config, detectedCommands, config.requires);
|
|
655
|
+
fs_1.default.writeFileSync(path_1.default.join(relayDir, 'guide.md'), guideContent);
|
|
651
656
|
// Generate bin/relay-preamble.sh (self-contained tracking + update check)
|
|
652
657
|
(0, preamble_js_1.generatePreambleBin)(relayDir, config.slug, config_js_1.API_URL);
|
|
653
658
|
// Generate entry command (commands/{author}-{name}.md)
|
|
@@ -720,6 +725,12 @@ function registerPublish(program) {
|
|
|
720
725
|
console.log('```');
|
|
721
726
|
console.log(`\n \x1b[90mCLI 설치된 사용자용 (짧은 버전):\x1b[0m`);
|
|
722
727
|
console.log(` /relay:relay-install ${result.slug}`);
|
|
728
|
+
if (config.visibility !== 'private') {
|
|
729
|
+
console.log(`\n \x1b[90m유료 판매하려면:\x1b[0m`);
|
|
730
|
+
console.log(` 1. 가시성을 "링크 공유"로 변경: \x1b[36mrelayax.com/dashboard\x1b[0m`);
|
|
731
|
+
console.log(` 2. API 키 발급: \x1b[36mrelayax.com/dashboard/keys\x1b[0m`);
|
|
732
|
+
console.log(` 3. 웹훅 연동 가이드: \x1b[36mrelayax.com/docs/webhook-guide.md\x1b[0m`);
|
|
733
|
+
}
|
|
723
734
|
console.log(`\n \x1b[90m상세페이지: \x1b[36mrelayax.com/@${detailSlug}\x1b[0m`);
|
|
724
735
|
}
|
|
725
736
|
}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ const search_js_1 = require("./commands/search.js");
|
|
|
9
9
|
const install_js_1 = require("./commands/install.js");
|
|
10
10
|
const list_js_1 = require("./commands/list.js");
|
|
11
11
|
const uninstall_js_1 = require("./commands/uninstall.js");
|
|
12
|
+
const package_js_1 = require("./commands/package.js");
|
|
12
13
|
const publish_js_1 = require("./commands/publish.js");
|
|
13
14
|
const login_js_1 = require("./commands/login.js");
|
|
14
15
|
const update_js_1 = require("./commands/update.js");
|
|
@@ -36,6 +37,7 @@ program
|
|
|
36
37
|
(0, install_js_1.registerInstall)(program);
|
|
37
38
|
(0, list_js_1.registerList)(program);
|
|
38
39
|
(0, uninstall_js_1.registerUninstall)(program);
|
|
40
|
+
(0, package_js_1.registerPackage)(program);
|
|
39
41
|
(0, publish_js_1.registerPublish)(program);
|
|
40
42
|
(0, login_js_1.registerLogin)(program);
|
|
41
43
|
(0, update_js_1.registerUpdate)(program);
|
|
@@ -506,58 +506,154 @@ ${ERROR_HANDLING_GUIDE}
|
|
|
506
506
|
description: '현재 팀 패키지를 relay Space에 배포합니다',
|
|
507
507
|
body: `현재 디렉토리의 에이전트 팀(.relay/)을 분석하고, 보안 점검 및 requirements를 구성한 뒤, 사용가이드를 생성하고 relay Space에 배포합니다.
|
|
508
508
|
|
|
509
|
-
## 사전 준비
|
|
509
|
+
## 사전 준비
|
|
510
510
|
|
|
511
|
-
### 0-1.
|
|
511
|
+
### 0-1. 인증 확인
|
|
512
512
|
|
|
513
|
-
|
|
513
|
+
- \`relay status --json\` 명령어를 실행하여 로그인 상태를 확인합니다.
|
|
514
|
+
- 인증되어 있으면 다음 단계로 진행합니다.
|
|
515
|
+
- 미인증이면 바로 로그인을 진행합니다:
|
|
516
|
+
1. \`relay login\` 실행 (timeout 300초)
|
|
517
|
+
- 브라우저가 자동으로 열리고, 사용자가 로그인을 완료하면 토큰이 자동 저장됩니다.
|
|
518
|
+
2. 완료 후 \`relay status --json\`으로 로그인 성공을 확인합니다.
|
|
519
|
+
|
|
520
|
+
### 0-2. 소스 패키징 (source → .relay/)
|
|
521
|
+
|
|
522
|
+
\`relay package\` CLI 명령을 사용하여 소스 디렉토리의 콘텐츠를 .relay/로 동기화합니다.
|
|
523
|
+
소스 탐색과 파일 비교는 CLI가 결정적으로 처리하고, 에이전트는 결과를 사용자에게 보여주고 흐름을 조율합니다.
|
|
524
|
+
|
|
525
|
+
#### A. 최초 배포 (.relay/relay.yaml이 없음)
|
|
514
526
|
|
|
515
|
-
|
|
527
|
+
##### 1단계: 소스 탐색
|
|
516
528
|
|
|
517
|
-
|
|
529
|
+
\`relay package --init --json\` 실행
|
|
530
|
+
- CLI가 프로젝트에서 에이전트 CLI 디렉토리(.claude/, .codex/, .gemini/ 등)를 자동 탐색합니다.
|
|
531
|
+
- JSON 결과의 \`detected\` 배열에 각 디렉토리별 콘텐츠 요약이 포함됩니다:
|
|
532
|
+
\`\`\`json
|
|
533
|
+
{
|
|
534
|
+
"status": "init_required",
|
|
535
|
+
"detected": [
|
|
536
|
+
{ "source": ".claude", "name": "Claude Code", "summary": { "skills": 2, "commands": 3 }, "fileCount": 8 },
|
|
537
|
+
{ "source": ".codex", "name": "Codex", "summary": { "agents": 1 }, "fileCount": 2 }
|
|
538
|
+
]
|
|
539
|
+
}
|
|
540
|
+
\`\`\`
|
|
518
541
|
|
|
519
|
-
|
|
542
|
+
- **detected가 0개** → "배포 가능한 에이전트 콘텐츠가 없습니다. skills/이나 commands/를 먼저 만들어주세요." 안내 후 중단
|
|
520
543
|
|
|
521
|
-
2
|
|
522
|
-
- question: "팀 이름을 입력해주세요"
|
|
523
|
-
- 기본값으로 현재 디렉토리명을 제안합니다.
|
|
544
|
+
##### 2단계: 콘텐츠 분석 & 팀 포지셔닝
|
|
524
545
|
|
|
525
|
-
|
|
526
|
-
- question: "팀을 한 줄로 설명해주세요 (Space에 표시됩니다)"
|
|
527
|
-
- 기본값으로 적절한 값을 만들어줍니다.
|
|
546
|
+
detected된 소스 디렉토리의 skills/, commands/, agents/, rules/ 파일 **내용을 직접 읽어** 팀의 정체성을 파악합니다.
|
|
528
547
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
548
|
+
**분석 관점:**
|
|
549
|
+
- 이 팀이 **무엇을 하는 팀**인지 (코드 리뷰? QA? 문서 생성? 데이터 분석?)
|
|
550
|
+
- 어떤 **기술 스택/도메인**에 특화되어 있는지 (Supabase? React? Python?)
|
|
551
|
+
- 설치자에게 **어떤 가치**를 제공하는지
|
|
552
|
+
|
|
553
|
+
이 분석을 기반으로 팀을 하나의 "제품"으로 포지셔닝합니다.
|
|
554
|
+
|
|
555
|
+
**중요: 소스 디렉토리 이름(.claude 등)은 인프라 디테일이므로 사용자에게 노출하지 않습니다.**
|
|
556
|
+
사용자에게는 팀의 기능과 정체성 중심으로 질문합니다.
|
|
557
|
+
|
|
558
|
+
##### 3단계: 배포 제안
|
|
559
|
+
|
|
560
|
+
분석 결과를 바탕으로 팀 배포를 제안합니다.
|
|
561
|
+
|
|
562
|
+
**detected가 1개일 때:**
|
|
563
|
+
|
|
564
|
+
**AskUserQuestion 호출:**
|
|
565
|
+
- question: 콘텐츠 분석 기반의 포지셔닝 질문
|
|
566
|
+
- 예시:
|
|
567
|
+
- "Supabase 웹 개발팀으로 배포할까요? (skills 2개, commands 3개)"
|
|
568
|
+
- "코드 리뷰 자동화 팀으로 배포할까요? (skills 1개, agents 2개)"
|
|
569
|
+
- "Next.js QA 테스트팀으로 배포할까요? (commands 5개)"
|
|
570
|
+
- options: \`["배포", "취소"]\`
|
|
571
|
+
|
|
572
|
+
**detected가 여러 개일 때:**
|
|
573
|
+
각 소스의 콘텐츠를 분석하여 서로 다른 팀으로 포지셔닝합니다.
|
|
574
|
+
|
|
575
|
+
**AskUserQuestion 호출:**
|
|
576
|
+
- question: "어떤 팀으로 배포할까요?"
|
|
577
|
+
- options: 콘텐츠 기반 설명 (예: \`["Supabase 웹 개발팀 (skills 2개, commands 3개)", "QA 자동화 에이전트 (agents 1개)"]\`)
|
|
578
|
+
- 디렉토리 이름은 내부적으로만 매핑하고, 사용자에게는 팀의 기능으로 보여줍니다.
|
|
579
|
+
|
|
580
|
+
##### 4단계: 팀 정보 확정
|
|
581
|
+
|
|
582
|
+
포지셔닝 분석을 기반으로 팀 이름과 설명을 제안합니다.
|
|
583
|
+
|
|
584
|
+
**AskUserQuestion 호출:**
|
|
585
|
+
- question: "팀 이름을 확인해주세요"
|
|
586
|
+
- 분석된 포지셔닝에서 자연스러운 팀 이름을 제안합니다 (예: "supabase-web-dev", "code-reviewer")
|
|
587
|
+
- 현재 디렉토리명이 아닌, **콘텐츠 기반** 이름을 기본값으로 제시합니다.
|
|
588
|
+
|
|
589
|
+
**AskUserQuestion 호출:**
|
|
590
|
+
- question: "팀 설명을 확인해주세요 (Space에 표시됩니다)"
|
|
591
|
+
- 분석한 콘텐츠를 기반으로 설치자 관점의 설명을 제안합니다.
|
|
592
|
+
- 좋은 예: "Supabase 기반 웹앱의 DB 마이그레이션, API 개발, 테스트를 자동화합니다"
|
|
593
|
+
- 나쁜 예: ".claude 디렉토리의 skills와 commands를 패키징한 팀"
|
|
594
|
+
|
|
595
|
+
##### 5단계: 초기화 & 패키징
|
|
596
|
+
|
|
597
|
+
자동 처리:
|
|
598
|
+
- \`.relay/relay.yaml\` 생성:
|
|
599
|
+
\`\`\`yaml
|
|
600
|
+
name: <확정된 이름>
|
|
601
|
+
slug: <이름에서 자동 생성 — 소문자, 특수문자→하이픈>
|
|
602
|
+
description: <확정된 설명>
|
|
603
|
+
source: <선택된 소스 디렉토리> # 예: .claude (내부용)
|
|
604
|
+
version: 1.0.0
|
|
605
|
+
tags: []
|
|
606
|
+
\`\`\`
|
|
607
|
+
- \`relay package --source <선택된 소스> --sync --json\` 실행하여 콘텐츠를 .relay/로 복사
|
|
608
|
+
- \`relay init --auto\` 실행하여 글로벌 커맨드 설치 보장
|
|
609
|
+
- 결과를 사용자에게 표시
|
|
610
|
+
|
|
611
|
+
#### B. 재배포 (.relay/relay.yaml이 있음)
|
|
612
|
+
|
|
613
|
+
1. \`relay package --json\` 실행
|
|
614
|
+
- CLI가 relay.yaml의 \`source\` 필드를 읽고, 소스 디렉토리와 .relay/를 파일 해시로 비교합니다.
|
|
615
|
+
- JSON 결과 예시:
|
|
616
|
+
\`\`\`json
|
|
617
|
+
{
|
|
618
|
+
"source": ".claude",
|
|
619
|
+
"sourceName": "Claude Code",
|
|
620
|
+
"synced": false,
|
|
621
|
+
"diff": [
|
|
622
|
+
{ "relPath": "skills/code-review/SKILL.md", "status": "modified" },
|
|
623
|
+
{ "relPath": "commands/deploy.md", "status": "added" },
|
|
624
|
+
{ "relPath": "commands/old-cmd.md", "status": "deleted" }
|
|
625
|
+
],
|
|
626
|
+
"summary": { "added": 1, "modified": 1, "deleted": 1, "unchanged": 5 }
|
|
627
|
+
}
|
|
538
628
|
\`\`\`
|
|
539
|
-
- \`relay init --update\` 실행하여 글로벌 커맨드 설치 보장
|
|
540
629
|
|
|
541
|
-
|
|
630
|
+
2. **변경이 있으면** (added + modified + deleted > 0) → diff를 사용자에게 보여줍니다:
|
|
631
|
+
\`\`\`
|
|
632
|
+
📦 소스 동기화 (.claude/ → .relay/)
|
|
633
|
+
변경: skills/code-review/SKILL.md
|
|
634
|
+
신규: commands/deploy.md
|
|
635
|
+
삭제: commands/old-cmd.md
|
|
636
|
+
유지: 5개 파일
|
|
637
|
+
\`\`\`
|
|
542
638
|
|
|
543
|
-
|
|
639
|
+
**AskUserQuestion 호출:**
|
|
640
|
+
- question: "소스 변경사항을 .relay/에 반영할까요?"
|
|
641
|
+
- options: \`["반영", "변경 확인", "건너뛰기"]\`
|
|
544
642
|
|
|
545
|
-
|
|
546
|
-
-
|
|
547
|
-
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
643
|
+
**응답 처리:**
|
|
644
|
+
- "반영" → \`relay package --sync --json\` 실행하여 동기화
|
|
645
|
+
- "변경 확인" → 변경된 파일의 내용을 직접 읽어 diff를 상세히 보여준 후 다시 AskUserQuestion
|
|
646
|
+
- "건너뛰기" → 현재 .relay/ 그대로 배포
|
|
647
|
+
|
|
648
|
+
3. **변경이 없으면** → "✓ 소스와 동기화 상태입니다." 표시 후 다음 단계로
|
|
551
649
|
|
|
552
|
-
|
|
553
|
-
- .relay/ 디렉토리의 skills/, agents/, rules/, commands/를 탐색합니다.
|
|
554
|
-
- 각 파일의 이름과 description을 추출합니다.
|
|
650
|
+
4. \`source\` 필드가 없으면 → .relay/ 내 콘텐츠를 직접 편집하는 모드로 간주하고 동기화를 건너뜁니다.
|
|
555
651
|
|
|
556
652
|
## 인터랙션 플로우
|
|
557
653
|
|
|
558
|
-
이 커맨드는
|
|
654
|
+
이 커맨드는 4단계 인터랙션으로 진행됩니다. 각 단계에서 반드시 AskUserQuestion 도구를 사용하세요.
|
|
559
655
|
|
|
560
|
-
### Step
|
|
656
|
+
### Step 1. 버전 범프
|
|
561
657
|
|
|
562
658
|
relay.yaml의 현재 \`version\`을 읽고 semver 범프를 제안합니다.
|
|
563
659
|
|
|
@@ -741,5 +837,5 @@ ${ERROR_HANDLING_GUIDE}`,
|
|
|
741
837
|
];
|
|
742
838
|
// ─── Builder Commands (로컬 설치) ───
|
|
743
839
|
// relay-publish가 글로벌로 승격되어 현재 비어있음.
|
|
744
|
-
// relay init --
|
|
840
|
+
// relay init --auto만 실행하면 모든 커맨드가 한번에 업데이트됨.
|
|
745
841
|
exports.BUILDER_COMMANDS = [];
|
package/dist/lib/guide.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import type { Requires } from '../commands/publish.js';
|
|
2
|
+
interface CommandEntry {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function generateGuide(config: {
|
|
7
|
+
slug: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
version: string;
|
|
11
|
+
visibility?: string;
|
|
12
|
+
}, commands: CommandEntry[], requires?: Requires): string;
|
|
13
|
+
export {};
|
package/dist/lib/guide.js
CHANGED
|
@@ -1,52 +1,144 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* GUIDE.html 생성 프롬프트 템플릿.
|
|
4
|
-
* publish slash command에서 Claude Code가 이 프롬프트를 읽고 GUIDE.html을 생성한다.
|
|
5
|
-
*/
|
|
6
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
3
|
+
exports.generateGuide = generateGuide;
|
|
4
|
+
function buildSetupSection(needsLogin) {
|
|
5
|
+
const installStep = `## Step 1. Relay CLI 설치
|
|
6
|
+
|
|
7
|
+
relay CLI가 설치되어 있지 않다면:
|
|
8
|
+
|
|
9
|
+
\`\`\`bash
|
|
10
|
+
npm install -g relayax-cli
|
|
11
|
+
\`\`\`
|
|
12
|
+
|
|
13
|
+
Node.js가 없는 경우:
|
|
14
|
+
- macOS: \`brew install node\`
|
|
15
|
+
- Windows: \`winget install OpenJS.NodeJS.LTS\`
|
|
16
|
+
- Linux: \`curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash && source ~/.bashrc && nvm install --lts\`
|
|
17
|
+
|
|
18
|
+
npm 권한 오류 시:
|
|
19
|
+
\`\`\`bash
|
|
20
|
+
mkdir -p ~/.npm-global && npm config set prefix '~/.npm-global'
|
|
21
|
+
echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.zshrc && source ~/.zshrc
|
|
22
|
+
npm install -g relayax-cli
|
|
23
|
+
\`\`\``;
|
|
24
|
+
if (!needsLogin)
|
|
25
|
+
return installStep;
|
|
26
|
+
return `${installStep}
|
|
27
|
+
|
|
28
|
+
## Step 2. 로그인
|
|
29
|
+
|
|
30
|
+
\`\`\`bash
|
|
31
|
+
relay login
|
|
32
|
+
\`\`\`
|
|
33
|
+
|
|
34
|
+
브라우저가 열리면 GitHub 또는 카카오 계정으로 로그인하세요.`;
|
|
35
|
+
}
|
|
36
|
+
function buildRequiresSection(requires) {
|
|
37
|
+
const sections = [];
|
|
38
|
+
if (requires.cli && requires.cli.length > 0) {
|
|
39
|
+
sections.push('### CLI 도구 설치\n');
|
|
40
|
+
for (const cli of requires.cli) {
|
|
41
|
+
const label = cli.required === false ? '(선택)' : '(필수)';
|
|
42
|
+
if (cli.install) {
|
|
43
|
+
sections.push(`- **${cli.name}** ${label}: \`${cli.install}\``);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
sections.push(`- **${cli.name}** ${label}: 설치 후 \`which ${cli.name}\`으로 확인`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
sections.push('');
|
|
50
|
+
}
|
|
51
|
+
if (requires.npm && requires.npm.length > 0) {
|
|
52
|
+
sections.push('### npm 패키지 설치\n');
|
|
53
|
+
sections.push('```bash');
|
|
54
|
+
const pkgNames = requires.npm.map((p) => typeof p === 'string' ? p : p.name);
|
|
55
|
+
sections.push(`npm install ${pkgNames.join(' ')}`);
|
|
56
|
+
sections.push('```\n');
|
|
57
|
+
}
|
|
58
|
+
if (requires.env && requires.env.length > 0) {
|
|
59
|
+
sections.push('### 환경변수 설정\n');
|
|
60
|
+
sections.push('```bash');
|
|
61
|
+
for (const env of requires.env) {
|
|
62
|
+
const label = env.required === false ? '# (선택)' : '# (필수)';
|
|
63
|
+
const desc = env.description ? ` — ${env.description}` : '';
|
|
64
|
+
sections.push(`${env.name}=your_value_here ${label}${desc}`);
|
|
65
|
+
}
|
|
66
|
+
sections.push('```\n');
|
|
67
|
+
}
|
|
68
|
+
if (requires.mcp && requires.mcp.length > 0) {
|
|
69
|
+
sections.push('### MCP 서버 설정\n');
|
|
70
|
+
for (const mcp of requires.mcp) {
|
|
71
|
+
sections.push(`**${mcp.name}:**`);
|
|
72
|
+
if (mcp.package)
|
|
73
|
+
sections.push(`- 패키지: \`${mcp.package}\``);
|
|
74
|
+
if (mcp.config) {
|
|
75
|
+
sections.push(`- 실행: \`${mcp.config.command}${mcp.config.args ? ' ' + mcp.config.args.join(' ') : ''}\``);
|
|
76
|
+
}
|
|
77
|
+
if (mcp.env && mcp.env.length > 0) {
|
|
78
|
+
sections.push(`- 필요한 환경변수: ${mcp.env.map((e) => `\`${e}\``).join(', ')}`);
|
|
79
|
+
}
|
|
80
|
+
sections.push('');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (requires.teams && requires.teams.length > 0) {
|
|
84
|
+
sections.push('### 의존 팀 설치\n');
|
|
85
|
+
sections.push('```bash');
|
|
86
|
+
for (const team of requires.teams) {
|
|
87
|
+
sections.push(`relay install ${team}`);
|
|
88
|
+
}
|
|
89
|
+
sections.push('```\n');
|
|
90
|
+
}
|
|
91
|
+
if (requires.permissions && requires.permissions.length > 0) {
|
|
92
|
+
sections.push('### 권한 설정\n');
|
|
93
|
+
sections.push('아래 도구 사용을 허용해야 합니다:\n');
|
|
94
|
+
for (const perm of requires.permissions) {
|
|
95
|
+
sections.push(`- \`${perm}\``);
|
|
96
|
+
}
|
|
97
|
+
sections.push('');
|
|
98
|
+
}
|
|
99
|
+
if (sections.length === 0)
|
|
100
|
+
return '';
|
|
101
|
+
return '## Step 4. 환경 구성\n\n' + sections.join('\n');
|
|
102
|
+
}
|
|
103
|
+
function generateGuide(config, commands, requires) {
|
|
104
|
+
const scopedSlug = config.slug.startsWith('@') ? config.slug : `@${config.slug}`;
|
|
105
|
+
const needsLogin = config.visibility === 'private' || config.visibility === 'gated';
|
|
106
|
+
const setupSection = buildSetupSection(needsLogin);
|
|
107
|
+
const installStepNum = needsLogin ? 3 : 2;
|
|
108
|
+
const requiresSection = requires ? buildRequiresSection(requires) : '';
|
|
109
|
+
const requiresSectionFinal = requiresSection
|
|
110
|
+
? requiresSection.replace('## Step 4.', `## Step ${installStepNum + 1}.`)
|
|
111
|
+
: '';
|
|
112
|
+
let usageHint = '';
|
|
113
|
+
if (commands.length > 0) {
|
|
114
|
+
usageHint = `## 사용법
|
|
115
|
+
|
|
116
|
+
설치가 완료되면 아래 커맨드를 사용할 수 있습니다:
|
|
117
|
+
|
|
118
|
+
${commands.map((cmd) => `- \`/${cmd.name}\`: ${cmd.description}`).join('\n')}
|
|
119
|
+
|
|
120
|
+
원하는 작업을 말하거나 위 커맨드를 직접 실행하세요.
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
return `# ${config.name} — 설치 가이드
|
|
124
|
+
|
|
125
|
+
> ${config.description}
|
|
126
|
+
|
|
127
|
+
이 가이드는 [RelayAX](https://relayax.com)에서 자동 생성되었습니다.
|
|
128
|
+
아래 단계를 순서대로 실행하면 Agent 팀이 설치됩니다.
|
|
129
|
+
|
|
130
|
+
${setupSection}
|
|
131
|
+
|
|
132
|
+
## Step ${installStepNum}. 팀 설치
|
|
133
|
+
|
|
134
|
+
\`\`\`bash
|
|
135
|
+
relay install ${scopedSlug}
|
|
136
|
+
\`\`\`
|
|
137
|
+
|
|
138
|
+
설치 후 Agent가 자동으로 의존성을 확인하고 환경을 구성합니다.
|
|
139
|
+
|
|
140
|
+
${requiresSectionFinal}${usageHint}---
|
|
141
|
+
*출처: https://relayax.com — Agent Team Marketplace*
|
|
142
|
+
*설치: \`relay install ${scopedSlug}\`*
|
|
143
|
+
`;
|
|
144
|
+
}
|