relayax-cli 0.2.34 → 0.2.36
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/create.js +1 -0
- package/dist/commands/install.js +9 -22
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.js +6 -119
- package/dist/commands/publish.js +96 -142
- package/dist/lib/command-adapter.js +68 -99
- package/dist/types.d.ts +11 -4
- package/package.json +1 -1
package/dist/commands/create.js
CHANGED
package/dist/commands/install.js
CHANGED
|
@@ -32,7 +32,6 @@ function registerInstall(program) {
|
|
|
32
32
|
program
|
|
33
33
|
.command('install <slug>')
|
|
34
34
|
.description('에이전트 팀 패키지를 .relay/teams/에 다운로드합니다')
|
|
35
|
-
.option('--no-guide', 'GUIDE.html 브라우저 자동 오픈을 비활성화합니다')
|
|
36
35
|
.option('--join-code <code>', 'Space 초대 코드 (Space 팀 설치 시 자동 가입)')
|
|
37
36
|
.action(async (slugInput, opts) => {
|
|
38
37
|
const json = program.opts().json ?? false;
|
|
@@ -209,7 +208,6 @@ function registerInstall(program) {
|
|
|
209
208
|
contact_links: team.author.contact_links ?? [],
|
|
210
209
|
} : null,
|
|
211
210
|
welcome: team.welcome ?? null,
|
|
212
|
-
latest_post: team.latest_post ?? null,
|
|
213
211
|
};
|
|
214
212
|
if (json) {
|
|
215
213
|
console.log(JSON.stringify(result));
|
|
@@ -242,29 +240,18 @@ function registerInstall(program) {
|
|
|
242
240
|
if (authorUsername) {
|
|
243
241
|
console.log(` \x1b[90m│\x1b[0m 👤 relayax.com/@${authorUsername}`);
|
|
244
242
|
}
|
|
245
|
-
if (team.latest_post && authorUsername) {
|
|
246
|
-
console.log(` \x1b[90m│\x1b[0m 📝 relayax.com/@${authorUsername}/posts/${team.latest_post.slug}`);
|
|
247
|
-
}
|
|
248
243
|
console.log(` \x1b[90m└${'─'.repeat(44)}┘\x1b[0m`);
|
|
249
244
|
}
|
|
250
|
-
//
|
|
251
|
-
const
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
exec(`${openCmd} "${guideHtmlPath}"`);
|
|
258
|
-
console.log(`\n 📖 사용가이드를 브라우저에서 열었습니다`);
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
console.log(`\n 📖 사용가이드: ${guideHtmlPath}`);
|
|
262
|
-
}
|
|
245
|
+
// Usage hint (type-aware)
|
|
246
|
+
const teamType = team.type;
|
|
247
|
+
if (teamType === 'passive') {
|
|
248
|
+
console.log(`\n\x1b[33m💡 자동 적용됩니다. 별도 실행 없이 동작합니다.\x1b[0m`);
|
|
249
|
+
}
|
|
250
|
+
else if (teamType === 'hybrid' && team.commands && team.commands.length > 0) {
|
|
251
|
+
console.log(`\n\x1b[33m💡 자동 적용 + \x1b[1m/${team.commands[0].name}\x1b[0m\x1b[33m 으로 추가 기능을 사용할 수 있습니다.\x1b[0m`);
|
|
263
252
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
console.log(`\n\x1b[33m💡 시작하려면 채팅에 아래 명령어를 입력하세요:\x1b[0m`);
|
|
267
|
-
console.log(` \x1b[1m/${team.commands[0].name}\x1b[0m`);
|
|
253
|
+
else if (team.commands && team.commands.length > 0) {
|
|
254
|
+
console.log(`\n\x1b[33m💡 사용법: \x1b[1m/${team.commands[0].name}\x1b[0m`);
|
|
268
255
|
}
|
|
269
256
|
else {
|
|
270
257
|
console.log(`\n\x1b[33m💡 설치 완료! AI 에이전트에서 사용할 수 있습니다.\x1b[0m`);
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
/**
|
|
3
3
|
* 대화형 로그인 플로우 실행 (auto-login에서 호출).
|
|
4
|
-
* 로그인
|
|
4
|
+
* 브라우저에서 로그인 페이지를 열고 토큰을 받아 저장.
|
|
5
5
|
*/
|
|
6
6
|
export declare function runLogin(): Promise<void>;
|
|
7
7
|
export declare function registerLogin(program: Command): void;
|
package/dist/commands/login.js
CHANGED
|
@@ -6,7 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.runLogin = runLogin;
|
|
7
7
|
exports.registerLogin = registerLogin;
|
|
8
8
|
const http_1 = __importDefault(require("http"));
|
|
9
|
-
const readline_1 = __importDefault(require("readline"));
|
|
10
9
|
const child_process_1 = require("child_process");
|
|
11
10
|
const config_js_1 = require("../lib/config.js");
|
|
12
11
|
function openBrowser(url) {
|
|
@@ -104,120 +103,22 @@ function findAvailablePort() {
|
|
|
104
103
|
});
|
|
105
104
|
});
|
|
106
105
|
}
|
|
107
|
-
function
|
|
108
|
-
return new Promise((resolve) => rl.question(question, resolve));
|
|
109
|
-
}
|
|
110
|
-
function promptPassword(question) {
|
|
111
|
-
return new Promise((resolve) => {
|
|
112
|
-
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
113
|
-
process.stdout.write(question);
|
|
114
|
-
// Hide input
|
|
115
|
-
const stdin = process.stdin;
|
|
116
|
-
if (stdin.isTTY)
|
|
117
|
-
stdin.setRawMode(true);
|
|
118
|
-
let input = '';
|
|
119
|
-
process.stdin.resume();
|
|
120
|
-
process.stdin.setEncoding('utf8');
|
|
121
|
-
const onData = (ch) => {
|
|
122
|
-
if (ch === '\n' || ch === '\r' || ch === '\u0004') {
|
|
123
|
-
if (stdin.isTTY)
|
|
124
|
-
stdin.setRawMode(false);
|
|
125
|
-
process.stdout.write('\n');
|
|
126
|
-
process.stdin.removeListener('data', onData);
|
|
127
|
-
rl.close();
|
|
128
|
-
resolve(input);
|
|
129
|
-
}
|
|
130
|
-
else if (ch === '\u0003') {
|
|
131
|
-
process.exit(1);
|
|
132
|
-
}
|
|
133
|
-
else if (ch === '\u007f') {
|
|
134
|
-
input = input.slice(0, -1);
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
input += ch;
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
process.stdin.on('data', onData);
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
async function loginWithBrowser(provider, json) {
|
|
106
|
+
async function loginWithBrowser(json) {
|
|
144
107
|
const port = await findAvailablePort();
|
|
145
|
-
const loginUrl = `${config_js_1.API_URL}/auth/cli-login?port=${port}
|
|
108
|
+
const loginUrl = `${config_js_1.API_URL}/auth/cli-login?port=${port}`;
|
|
146
109
|
if (!json) {
|
|
147
|
-
|
|
148
|
-
console.error(`브라우저에서 ${providerName} 로그인을 진행합니다...`);
|
|
110
|
+
console.error(`브라우저에서 로그인 페이지를 엽니다...`);
|
|
149
111
|
}
|
|
150
112
|
openBrowser(loginUrl);
|
|
151
113
|
return waitForToken(port);
|
|
152
114
|
}
|
|
153
|
-
async function loginWithEmail(json) {
|
|
154
|
-
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
155
|
-
if (!json) {
|
|
156
|
-
console.error('이메일로 로그인합니다.');
|
|
157
|
-
}
|
|
158
|
-
let email;
|
|
159
|
-
let password;
|
|
160
|
-
try {
|
|
161
|
-
email = (await promptLine(rl, '이메일: ')).trim();
|
|
162
|
-
rl.close();
|
|
163
|
-
password = await promptPassword('비밀번호: ');
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
rl.close();
|
|
167
|
-
throw new Error('입력이 취소되었습니다');
|
|
168
|
-
}
|
|
169
|
-
const res = await fetch(`${config_js_1.API_URL}/api/auth/email-login`, {
|
|
170
|
-
method: 'POST',
|
|
171
|
-
headers: { 'Content-Type': 'application/json' },
|
|
172
|
-
body: JSON.stringify({ email, password }),
|
|
173
|
-
});
|
|
174
|
-
if (!res.ok) {
|
|
175
|
-
const body = (await res.json().catch(() => ({})));
|
|
176
|
-
const msg = body.error ?? '이메일 또는 비밀번호가 올바르지 않습니다.';
|
|
177
|
-
throw new Error(msg);
|
|
178
|
-
}
|
|
179
|
-
const data = (await res.json());
|
|
180
|
-
return {
|
|
181
|
-
token: data.access_token,
|
|
182
|
-
refresh_token: data.refresh_token,
|
|
183
|
-
expires_at: data.expires_at,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
async function selectProvider(json) {
|
|
187
|
-
if (json)
|
|
188
|
-
return 'github';
|
|
189
|
-
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
190
|
-
console.error('');
|
|
191
|
-
console.error('로그인 방법을 선택하세요:');
|
|
192
|
-
console.error(' 1) GitHub');
|
|
193
|
-
console.error(' 2) 카카오');
|
|
194
|
-
console.error(' 3) 이메일 / 비밀번호');
|
|
195
|
-
console.error('');
|
|
196
|
-
const answer = (await promptLine(rl, '선택 (기본값: 1): ')).trim();
|
|
197
|
-
rl.close();
|
|
198
|
-
if (answer === '2')
|
|
199
|
-
return 'kakao';
|
|
200
|
-
if (answer === '3')
|
|
201
|
-
return 'email';
|
|
202
|
-
return 'github';
|
|
203
|
-
}
|
|
204
115
|
/**
|
|
205
116
|
* 대화형 로그인 플로우 실행 (auto-login에서 호출).
|
|
206
|
-
* 로그인
|
|
117
|
+
* 브라우저에서 로그인 페이지를 열고 토큰을 받아 저장.
|
|
207
118
|
*/
|
|
208
119
|
async function runLogin() {
|
|
209
120
|
(0, config_js_1.ensureGlobalRelayDir)();
|
|
210
|
-
const
|
|
211
|
-
let loginResult;
|
|
212
|
-
if (provider === 'email') {
|
|
213
|
-
loginResult = await loginWithEmail(false);
|
|
214
|
-
}
|
|
215
|
-
else if (provider === 'kakao') {
|
|
216
|
-
loginResult = await loginWithBrowser('kakao', false);
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
loginResult = await loginWithBrowser('github', false);
|
|
220
|
-
}
|
|
121
|
+
const loginResult = await loginWithBrowser(false);
|
|
221
122
|
await verifyToken(loginResult.token);
|
|
222
123
|
(0, config_js_1.saveTokenData)({
|
|
223
124
|
access_token: loginResult.token,
|
|
@@ -231,7 +132,6 @@ function registerLogin(program) {
|
|
|
231
132
|
.command('login')
|
|
232
133
|
.description('RelayAX 계정에 로그인합니다')
|
|
233
134
|
.option('--token <token>', '직접 토큰 입력 (브라우저 없이)')
|
|
234
|
-
.option('--provider <provider>', '로그인 제공자 (github | kakao | email)')
|
|
235
135
|
.action(async (opts) => {
|
|
236
136
|
const json = program.opts().json ?? false;
|
|
237
137
|
(0, config_js_1.ensureGlobalRelayDir)();
|
|
@@ -240,20 +140,7 @@ function registerLogin(program) {
|
|
|
240
140
|
let expiresAt;
|
|
241
141
|
if (!accessToken) {
|
|
242
142
|
try {
|
|
243
|
-
|
|
244
|
-
if (!provider) {
|
|
245
|
-
provider = await selectProvider(json);
|
|
246
|
-
}
|
|
247
|
-
let loginResult;
|
|
248
|
-
if (provider === 'email') {
|
|
249
|
-
loginResult = await loginWithEmail(json);
|
|
250
|
-
}
|
|
251
|
-
else if (provider === 'kakao') {
|
|
252
|
-
loginResult = await loginWithBrowser('kakao', json);
|
|
253
|
-
}
|
|
254
|
-
else {
|
|
255
|
-
loginResult = await loginWithBrowser('github', json);
|
|
256
|
-
}
|
|
143
|
+
const loginResult = await loginWithBrowser(json);
|
|
257
144
|
accessToken = loginResult.token;
|
|
258
145
|
refreshToken = loginResult.refresh_token;
|
|
259
146
|
expiresAt = loginResult.expires_at;
|
package/dist/commands/publish.js
CHANGED
|
@@ -16,44 +16,21 @@ const version_check_js_1 = require("../lib/version-check.js");
|
|
|
16
16
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
17
17
|
const cliPkg = require('../../package.json');
|
|
18
18
|
const VALID_DIRS = ['skills', 'agents', 'rules', 'commands', 'bin'];
|
|
19
|
-
const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.webp'];
|
|
20
|
-
/** 개별 포트폴리오 이미지 최대 크기 (2 MB) */
|
|
21
|
-
const MAX_IMAGE_SIZE = 2 * 1024 * 1024;
|
|
22
|
-
/** 전체 업로드 최대 크기 (10 MB) */
|
|
23
|
-
const MAX_TOTAL_UPLOAD_SIZE = 10 * 1024 * 1024;
|
|
24
19
|
function parseRelayYaml(content) {
|
|
25
20
|
const raw = js_yaml_1.default.load(content) ?? {};
|
|
26
21
|
const tags = Array.isArray(raw.tags)
|
|
27
22
|
? raw.tags.map((t) => String(t))
|
|
28
23
|
: [];
|
|
29
|
-
// Parse portfolio slots structure
|
|
30
|
-
const rawPortfolio = raw.portfolio;
|
|
31
|
-
const portfolio = {};
|
|
32
|
-
if (rawPortfolio && typeof rawPortfolio === 'object' && !Array.isArray(rawPortfolio)) {
|
|
33
|
-
// Slot-based format: { demo: {...}, gallery: [...] }
|
|
34
|
-
if (rawPortfolio.demo && typeof rawPortfolio.demo === 'object') {
|
|
35
|
-
const d = rawPortfolio.demo;
|
|
36
|
-
portfolio.demo = {
|
|
37
|
-
type: String(d.type ?? 'gif'),
|
|
38
|
-
path: d.path ? String(d.path) : undefined,
|
|
39
|
-
url: d.url ? String(d.url) : undefined,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
if (Array.isArray(rawPortfolio.gallery)) {
|
|
43
|
-
portfolio.gallery = rawPortfolio.gallery
|
|
44
|
-
.map((g) => ({
|
|
45
|
-
path: String(g.path ?? ''),
|
|
46
|
-
title: String(g.title ?? ''),
|
|
47
|
-
description: g.description ? String(g.description) : undefined,
|
|
48
|
-
}))
|
|
49
|
-
.filter((g) => g.path);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
24
|
const requires = raw.requires;
|
|
53
25
|
const rawVisibility = String(raw.visibility ?? '');
|
|
54
26
|
const visibility = rawVisibility === 'private' ? 'private'
|
|
55
27
|
: rawVisibility === 'public' ? 'public'
|
|
56
28
|
: undefined;
|
|
29
|
+
const rawType = String(raw.type ?? '');
|
|
30
|
+
const type = rawType === 'command' ? 'command'
|
|
31
|
+
: rawType === 'passive' ? 'passive'
|
|
32
|
+
: rawType === 'hybrid' ? 'hybrid'
|
|
33
|
+
: undefined;
|
|
57
34
|
return {
|
|
58
35
|
name: String(raw.name ?? ''),
|
|
59
36
|
slug: String(raw.slug ?? ''),
|
|
@@ -62,9 +39,9 @@ function parseRelayYaml(content) {
|
|
|
62
39
|
changelog: raw.changelog ? String(raw.changelog) : undefined,
|
|
63
40
|
long_description: raw.long_description ? String(raw.long_description) : undefined,
|
|
64
41
|
tags,
|
|
65
|
-
portfolio,
|
|
66
42
|
requires,
|
|
67
43
|
visibility,
|
|
44
|
+
type,
|
|
68
45
|
};
|
|
69
46
|
}
|
|
70
47
|
function detectCommands(teamDir) {
|
|
@@ -111,17 +88,80 @@ function detectSkills(teamDir) {
|
|
|
111
88
|
if (!fs_1.default.existsSync(skillMd))
|
|
112
89
|
continue;
|
|
113
90
|
let description = entry.name;
|
|
91
|
+
const uses = [];
|
|
114
92
|
try {
|
|
115
93
|
const content = fs_1.default.readFileSync(skillMd, 'utf-8');
|
|
116
94
|
const m = content.match(/^---\n[\s\S]*?description:\s*[|>]?\s*\n?\s*(.+)\n[\s\S]*?---/m)
|
|
117
95
|
?? content.match(/^---\n[\s\S]*?description:\s*(.+)\n[\s\S]*?---/m);
|
|
118
96
|
if (m)
|
|
119
97
|
description = m[1].trim();
|
|
98
|
+
// Extract allowed-tools from frontmatter
|
|
99
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
100
|
+
if (frontmatterMatch) {
|
|
101
|
+
const fm = js_yaml_1.default.load(frontmatterMatch[1]);
|
|
102
|
+
if (fm && Array.isArray(fm['allowed-tools'])) {
|
|
103
|
+
for (const tool of fm['allowed-tools']) {
|
|
104
|
+
const t = String(tool).trim();
|
|
105
|
+
if (t)
|
|
106
|
+
uses.push(t);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
120
110
|
}
|
|
121
111
|
catch {
|
|
122
112
|
// ignore
|
|
123
113
|
}
|
|
124
|
-
entries.push({ name: entry.name, description });
|
|
114
|
+
entries.push({ name: entry.name, description, uses });
|
|
115
|
+
}
|
|
116
|
+
return entries;
|
|
117
|
+
}
|
|
118
|
+
const MCP_KEYWORDS = ['mcp', 'supabase', 'github', 'slack', 'notion', 'linear', 'jira', 'figma', 'stripe', 'openai', 'anthropic', 'postgres', 'mysql', 'redis', 'mongodb', 'firebase', 'aws', 'gcp', 'azure', 'vercel', 'netlify', 'docker', 'kubernetes'];
|
|
119
|
+
function detectAgentDetails(teamDir, requires) {
|
|
120
|
+
const agentsDir = path_1.default.join(teamDir, 'agents');
|
|
121
|
+
if (!fs_1.default.existsSync(agentsDir))
|
|
122
|
+
return [];
|
|
123
|
+
const mcpNames = new Set((requires?.mcp ?? []).map((m) => m.name.toLowerCase()));
|
|
124
|
+
const envNames = new Set((requires?.env ?? []).map((e) => e.name.toLowerCase()));
|
|
125
|
+
const entries = [];
|
|
126
|
+
const files = fs_1.default.readdirSync(agentsDir).filter((f) => f.endsWith('.md'));
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
const name = path_1.default.basename(file, '.md');
|
|
129
|
+
let description = name;
|
|
130
|
+
const uses = [];
|
|
131
|
+
try {
|
|
132
|
+
const content = fs_1.default.readFileSync(path_1.default.join(agentsDir, file), 'utf-8');
|
|
133
|
+
// Parse frontmatter
|
|
134
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
135
|
+
if (frontmatterMatch) {
|
|
136
|
+
const fm = js_yaml_1.default.load(frontmatterMatch[1]);
|
|
137
|
+
if (fm) {
|
|
138
|
+
if (typeof fm.description === 'string')
|
|
139
|
+
description = fm.description.trim();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Scan body for MCP-related keywords
|
|
143
|
+
const bodyLower = content.toLowerCase();
|
|
144
|
+
for (const keyword of MCP_KEYWORDS) {
|
|
145
|
+
if (bodyLower.includes(keyword) && !uses.includes(keyword)) {
|
|
146
|
+
uses.push(keyword);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Cross-reference with requires.mcp and requires.env
|
|
150
|
+
for (const mcp of mcpNames) {
|
|
151
|
+
if (bodyLower.includes(mcp) && !uses.includes(mcp)) {
|
|
152
|
+
uses.push(mcp);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const env of envNames) {
|
|
156
|
+
if (bodyLower.includes(env) && !uses.includes(env)) {
|
|
157
|
+
uses.push(env);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// ignore read errors
|
|
163
|
+
}
|
|
164
|
+
entries.push({ name, description, uses });
|
|
125
165
|
}
|
|
126
166
|
return entries;
|
|
127
167
|
}
|
|
@@ -180,53 +220,6 @@ function listDir(teamDir, dirName) {
|
|
|
180
220
|
return [];
|
|
181
221
|
return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.'));
|
|
182
222
|
}
|
|
183
|
-
/**
|
|
184
|
-
* 슬롯 기반 포트폴리오를 PortfolioEntry[] 로 평탄화한다.
|
|
185
|
-
* relay.yaml에 슬롯이 정의되어 있으면 사용, 없으면 portfolio/ 자동 스캔.
|
|
186
|
-
*/
|
|
187
|
-
function collectPortfolio(relayDir, slots) {
|
|
188
|
-
const entries = [];
|
|
189
|
-
// Demo
|
|
190
|
-
if (slots.demo) {
|
|
191
|
-
if (slots.demo.type === 'video_url' && slots.demo.url) {
|
|
192
|
-
entries.push({ path: '', title: 'Demo', slot_type: 'demo', demo_url: slots.demo.url });
|
|
193
|
-
}
|
|
194
|
-
else if (slots.demo.path) {
|
|
195
|
-
const absPath = path_1.default.resolve(relayDir, slots.demo.path);
|
|
196
|
-
if (fs_1.default.existsSync(absPath)) {
|
|
197
|
-
entries.push({ path: slots.demo.path, title: 'Demo', slot_type: 'demo' });
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
// Gallery
|
|
202
|
-
if (slots.gallery && slots.gallery.length > 0) {
|
|
203
|
-
for (const g of slots.gallery.slice(0, 5)) {
|
|
204
|
-
const absPath = path_1.default.resolve(relayDir, g.path);
|
|
205
|
-
if (fs_1.default.existsSync(absPath)) {
|
|
206
|
-
entries.push({ path: g.path, title: g.title, description: g.description, slot_type: 'gallery' });
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
// If no slots defined, auto-scan portfolio/ directory
|
|
211
|
-
if (entries.length === 0) {
|
|
212
|
-
const portfolioDir = path_1.default.join(relayDir, 'portfolio');
|
|
213
|
-
if (fs_1.default.existsSync(portfolioDir)) {
|
|
214
|
-
const files = fs_1.default.readdirSync(portfolioDir)
|
|
215
|
-
.filter((f) => IMAGE_EXTS.some((ext) => f.toLowerCase().endsWith(ext)))
|
|
216
|
-
.sort();
|
|
217
|
-
// All images as gallery
|
|
218
|
-
for (let i = 0; i < files.length && i < 6; i++) {
|
|
219
|
-
const f = files[i];
|
|
220
|
-
entries.push({
|
|
221
|
-
path: path_1.default.join('portfolio', f),
|
|
222
|
-
title: path_1.default.basename(f, path_1.default.extname(f)).replace(/[-_]/g, ' '),
|
|
223
|
-
slot_type: 'gallery',
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return entries;
|
|
229
|
-
}
|
|
230
223
|
/**
|
|
231
224
|
* long_description을 결정한다.
|
|
232
225
|
* 1. relay.yaml에 있으면 사용
|
|
@@ -251,7 +244,7 @@ async function createTarball(teamDir) {
|
|
|
251
244
|
const dirsToInclude = VALID_DIRS.filter((d) => fs_1.default.existsSync(path_1.default.join(teamDir, d)));
|
|
252
245
|
// Include root-level files if they exist
|
|
253
246
|
const entries = [...dirsToInclude];
|
|
254
|
-
const rootFiles = ['relay.yaml', '
|
|
247
|
+
const rootFiles = ['relay.yaml', 'SKILL.md'];
|
|
255
248
|
for (const file of rootFiles) {
|
|
256
249
|
if (fs_1.default.existsSync(path_1.default.join(teamDir, file))) {
|
|
257
250
|
entries.push(file);
|
|
@@ -264,63 +257,12 @@ async function createTarball(teamDir) {
|
|
|
264
257
|
}, entries);
|
|
265
258
|
return tmpFile;
|
|
266
259
|
}
|
|
267
|
-
async function publishToApi(token, tarPath, metadata
|
|
260
|
+
async function publishToApi(token, tarPath, metadata) {
|
|
268
261
|
const fileBuffer = fs_1.default.readFileSync(tarPath);
|
|
269
262
|
const blob = new Blob([fileBuffer], { type: 'application/gzip' });
|
|
270
263
|
const form = new FormData();
|
|
271
264
|
form.append('package', blob, `${metadata.slug}-${metadata.version}.tar.gz`);
|
|
272
265
|
form.append('metadata', JSON.stringify(metadata));
|
|
273
|
-
// Attach portfolio images (with size validation and slot_type)
|
|
274
|
-
if (portfolioEntries.length > 0) {
|
|
275
|
-
const portfolioMeta = [];
|
|
276
|
-
let totalImageSize = 0;
|
|
277
|
-
let fileIndex = 0;
|
|
278
|
-
for (let i = 0; i < portfolioEntries.length; i++) {
|
|
279
|
-
const entry = portfolioEntries[i];
|
|
280
|
-
// video_url demo has no file to upload
|
|
281
|
-
if (entry.slot_type === 'demo' && entry.demo_url && !entry.path) {
|
|
282
|
-
portfolioMeta.push({
|
|
283
|
-
title: entry.title,
|
|
284
|
-
description: entry.description,
|
|
285
|
-
sort_order: i,
|
|
286
|
-
slot_type: entry.slot_type,
|
|
287
|
-
demo_url: entry.demo_url,
|
|
288
|
-
});
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
const absPath = path_1.default.resolve(teamDir, entry.path);
|
|
292
|
-
const imgBuffer = fs_1.default.readFileSync(absPath);
|
|
293
|
-
if (imgBuffer.length > MAX_IMAGE_SIZE) {
|
|
294
|
-
const sizeMB = (imgBuffer.length / 1024 / 1024).toFixed(1);
|
|
295
|
-
throw new Error(`포트폴리오 이미지 '${path_1.default.basename(entry.path)}'이(가) 너무 큽니다 (${sizeMB}MB). 최대 ${MAX_IMAGE_SIZE / 1024 / 1024}MB까지 허용됩니다.`);
|
|
296
|
-
}
|
|
297
|
-
totalImageSize += imgBuffer.length;
|
|
298
|
-
if (totalImageSize > MAX_TOTAL_UPLOAD_SIZE) {
|
|
299
|
-
const totalMB = (totalImageSize / 1024 / 1024).toFixed(1);
|
|
300
|
-
throw new Error(`포트폴리오 이미지 총 크기가 너무 큽니다 (${totalMB}MB). 최대 ${MAX_TOTAL_UPLOAD_SIZE / 1024 / 1024}MB까지 허용됩니다.`);
|
|
301
|
-
}
|
|
302
|
-
const ext = path_1.default.extname(entry.path).slice(1) || 'png';
|
|
303
|
-
const mimeType = ext === 'gif' ? 'image/gif' : ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : ext === 'webp' ? 'image/webp' : 'image/png';
|
|
304
|
-
const imgBlob = new Blob([imgBuffer], { type: mimeType });
|
|
305
|
-
form.append(`portfolio[${fileIndex}]`, imgBlob, path_1.default.basename(entry.path));
|
|
306
|
-
fileIndex++;
|
|
307
|
-
portfolioMeta.push({
|
|
308
|
-
title: entry.title,
|
|
309
|
-
description: entry.description,
|
|
310
|
-
sort_order: i,
|
|
311
|
-
slot_type: entry.slot_type,
|
|
312
|
-
demo_url: entry.demo_url,
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
form.append('portfolio_meta', JSON.stringify(portfolioMeta));
|
|
316
|
-
}
|
|
317
|
-
// Attach GUIDE.html if it exists
|
|
318
|
-
const guidePath = path_1.default.join(teamDir, 'GUIDE.html');
|
|
319
|
-
if (fs_1.default.existsSync(guidePath)) {
|
|
320
|
-
const guideBuffer = fs_1.default.readFileSync(guidePath);
|
|
321
|
-
const guideBlob = new Blob([guideBuffer], { type: 'text/html' });
|
|
322
|
-
form.append('guide', guideBlob, 'GUIDE.html');
|
|
323
|
-
}
|
|
324
266
|
const res = await fetch(`${config_js_1.API_URL}/api/publish`, {
|
|
325
267
|
method: 'POST',
|
|
326
268
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -562,9 +504,9 @@ function registerPublish(program) {
|
|
|
562
504
|
rules: countDir(relayDir, 'rules'),
|
|
563
505
|
skills: countDir(relayDir, 'skills'),
|
|
564
506
|
};
|
|
565
|
-
// Collect portfolio and long_description
|
|
566
|
-
const portfolioEntries = collectPortfolio(relayDir, config.portfolio);
|
|
567
507
|
const longDescription = resolveLongDescription(relayDir, config.long_description);
|
|
508
|
+
const detectedSkills = detectSkills(relayDir);
|
|
509
|
+
const detectedAgents = detectAgentDetails(relayDir, config.requires);
|
|
568
510
|
const metadata = {
|
|
569
511
|
slug: config.slug,
|
|
570
512
|
name: config.name,
|
|
@@ -580,17 +522,21 @@ function registerPublish(program) {
|
|
|
580
522
|
cli_version: cliPkg.version,
|
|
581
523
|
agent_names: listDir(relayDir, 'agents'),
|
|
582
524
|
skill_names: listDir(relayDir, 'skills'),
|
|
525
|
+
type: config.type ?? 'hybrid',
|
|
526
|
+
agent_details: detectedAgents,
|
|
527
|
+
skill_details: detectedSkills,
|
|
583
528
|
};
|
|
584
529
|
if (!json) {
|
|
585
530
|
console.error(`패키지 생성 중... (${config.name} v${config.version})`);
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
531
|
+
}
|
|
532
|
+
// GUIDE.html deprecation warning
|
|
533
|
+
if (fs_1.default.existsSync(path_1.default.join(relayDir, 'GUIDE.html'))) {
|
|
534
|
+
console.error('\x1b[33m⚠ GUIDE.html은 더 이상 지원되지 않습니다. 상세페이지가 가이드 역할을 합니다.\x1b[0m');
|
|
535
|
+
console.error(' long_description을 활용하거나 relayax.com에서 팀 정보를 편집하세요.\n');
|
|
589
536
|
}
|
|
590
537
|
// Generate bin/relay-preamble.sh (self-contained tracking + update check)
|
|
591
538
|
(0, preamble_js_1.generatePreambleBin)(relayDir, config.slug, config_js_1.API_URL);
|
|
592
539
|
// Generate entry command (commands/{author}-{name}.md)
|
|
593
|
-
const detectedSkills = detectSkills(relayDir);
|
|
594
540
|
const entryContent = generateEntryCommand(config, detectedCommands, detectedSkills, config.slug);
|
|
595
541
|
const commandsDir = path_1.default.join(relayDir, 'commands');
|
|
596
542
|
if (!fs_1.default.existsSync(commandsDir)) {
|
|
@@ -606,7 +552,7 @@ function registerPublish(program) {
|
|
|
606
552
|
if (!json) {
|
|
607
553
|
console.error(`업로드 중...`);
|
|
608
554
|
}
|
|
609
|
-
const result = await publishToApi(token, tarPath, metadata
|
|
555
|
+
const result = await publishToApi(token, tarPath, metadata);
|
|
610
556
|
// Update entry command preamble with scoped slug from server (non-fatal)
|
|
611
557
|
try {
|
|
612
558
|
if (result.slug && result.slug !== config.slug) {
|
|
@@ -628,9 +574,6 @@ function registerPublish(program) {
|
|
|
628
574
|
console.log(`\n\x1b[32m✓ ${config.name} 배포 완료\x1b[0m v${result.version}`);
|
|
629
575
|
console.log(` 슬러그: \x1b[36m${result.slug}\x1b[0m`);
|
|
630
576
|
console.log(` URL: \x1b[36m${result.url}\x1b[0m`);
|
|
631
|
-
if (result.portfolio_count && result.portfolio_count > 0) {
|
|
632
|
-
console.log(` 포트폴리오: ${result.portfolio_count}개 이미지 업로드됨`);
|
|
633
|
-
}
|
|
634
577
|
// Show business card preview
|
|
635
578
|
const profile = result.profile;
|
|
636
579
|
if (profile) {
|
|
@@ -659,11 +602,22 @@ function registerPublish(program) {
|
|
|
659
602
|
'relay login',
|
|
660
603
|
`relay install ${result.slug}`,
|
|
661
604
|
];
|
|
662
|
-
|
|
605
|
+
// Type-based usage hint
|
|
606
|
+
const teamType = config.type ?? 'hybrid';
|
|
607
|
+
if (teamType === 'passive') {
|
|
608
|
+
guideLines.push('', '# 설치하면 자동 적용됩니다');
|
|
609
|
+
}
|
|
610
|
+
else if (detectedCommands.length > 0) {
|
|
611
|
+
const usageCmd = teamType === 'hybrid'
|
|
612
|
+
? `# 자동 적용 + /${detectedCommands[0].name} 으로 추가 기능`
|
|
613
|
+
: `# 사용법: /${detectedCommands[0].name}`;
|
|
614
|
+
guideLines.push('', usageCmd);
|
|
615
|
+
}
|
|
616
|
+
console.log(`\n \x1b[90m주변인에게 공유하세요:\x1b[0m\n`);
|
|
663
617
|
console.log('```');
|
|
664
618
|
guideLines.forEach((line) => console.log(line));
|
|
665
619
|
console.log('```');
|
|
666
|
-
console.log(`\n \x1b[90m
|
|
620
|
+
console.log(`\n \x1b[90m상세페이지: \x1b[36mrelayax.com/teams/${result.slug}\x1b[0m`);
|
|
667
621
|
}
|
|
668
622
|
}
|
|
669
623
|
}
|
|
@@ -61,9 +61,9 @@ function formatCommandFile(content) {
|
|
|
61
61
|
const LOGIN_JIT_GUIDE = `
|
|
62
62
|
### 인증 오류 처리
|
|
63
63
|
- 커맨드 실행 결과에 \`LOGIN_REQUIRED\` 에러가 포함되면:
|
|
64
|
-
1.
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
1. \`relay login\` 실행 (timeout 300초)
|
|
65
|
+
- 브라우저가 자동으로 열리고, 사용자가 로그인을 완료하면 토큰이 자동 저장됩니다.
|
|
66
|
+
2. 완료 후 \`relay status --json\`으로 로그인 성공을 확인하고, 원래 커맨드를 재실행합니다.`;
|
|
67
67
|
// ─── 명함 표시 포맷 ───
|
|
68
68
|
const BUSINESS_CARD_FORMAT = `
|
|
69
69
|
### 빌더 명함 표시
|
|
@@ -301,10 +301,27 @@ ${BUSINESS_CARD_FORMAT}
|
|
|
301
301
|
- "팔로우" → \`relay follow @{username}\` 실행. 로그인이 안 되어 있으면 \`relay login\` 먼저 실행 후 재시도.
|
|
302
302
|
- "건너뛰기" → 다음 단계로 진행
|
|
303
303
|
|
|
304
|
-
#### 3-3.
|
|
304
|
+
#### 3-3. 공유 가이드 (필수 — 설치 완료 시 반드시 표시)
|
|
305
|
+
설치 완료 후 아래 공유용 설치 가이드를 표시합니다. 복사 가능한 코드 블록으로 보여줍니다.
|
|
306
|
+
|
|
307
|
+
\`\`\`
|
|
308
|
+
주변인에게 공유하세요:
|
|
309
|
+
|
|
310
|
+
npm install -g relayax-cli
|
|
311
|
+
relay login
|
|
312
|
+
relay install {slug}
|
|
313
|
+
|
|
314
|
+
상세페이지: relayax.com/teams/{slug}
|
|
315
|
+
\`\`\`
|
|
316
|
+
|
|
317
|
+
- type 기반 사용법도 포함:
|
|
318
|
+
- command/hybrid: \`# 사용법: /{첫번째 커맨드}\`
|
|
319
|
+
- passive: \`# 설치하면 자동 적용됩니다\`
|
|
320
|
+
|
|
321
|
+
#### 3-4. 사용 제안
|
|
305
322
|
- "바로 사용해볼까요?" 제안
|
|
306
323
|
|
|
307
|
-
#### 3-
|
|
324
|
+
#### 3-5. 업데이트 확인
|
|
308
325
|
- \`relay check-update\` 명령어를 실행합니다.
|
|
309
326
|
- CLI 업데이트가 있으면 안내합니다: "relay v{new} available. Run: npm update -g relayax-cli"
|
|
310
327
|
- 다른 팀 업데이트가 있으면 안내합니다.
|
|
@@ -447,29 +464,59 @@ ${LOGIN_JIT_GUIDE}
|
|
|
447
464
|
},
|
|
448
465
|
{
|
|
449
466
|
id: 'relay-publish',
|
|
450
|
-
description: '현재 팀 패키지를
|
|
451
|
-
body: `현재 디렉토리의 에이전트 팀(.relay/)을 분석하고, 보안 점검 및 requirements를 구성한 뒤,
|
|
467
|
+
description: '현재 팀 패키지를 relay 마켓플레이스에 배포합니다',
|
|
468
|
+
body: `현재 디렉토리의 에이전트 팀(.relay/)을 분석하고, 보안 점검 및 requirements를 구성한 뒤, 사용가이드를 생성하고 relay 마켓플레이스에 배포합니다.
|
|
452
469
|
|
|
453
|
-
## 사전
|
|
470
|
+
## 사전 준비 (자동)
|
|
454
471
|
|
|
455
|
-
|
|
456
|
-
.relay/ 디렉토리가 없으면: "이 프로젝트에는 .relay/ 디렉토리가 없습니다. \`relay create <name>\`으로 팀을 먼저 만들어주세요." 안내 후 종료합니다.
|
|
472
|
+
### 0-1. 팀 프로젝트 확인 & 초기화
|
|
457
473
|
|
|
458
|
-
|
|
474
|
+
.relay/relay.yaml이 있는지 확인합니다.
|
|
475
|
+
|
|
476
|
+
**있으면** → 바로 0-2로 진행합니다.
|
|
477
|
+
|
|
478
|
+
**없으면** → 팀 프로젝트를 인라인으로 초기화합니다:
|
|
479
|
+
|
|
480
|
+
1. "이 프로젝트를 relay 팀으로 배포하겠습니다. 먼저 팀 정보를 입력해주세요." 안내
|
|
481
|
+
|
|
482
|
+
2. **AskUserQuestion 호출:**
|
|
483
|
+
- question: "팀 이름을 입력해주세요"
|
|
484
|
+
- 기본값으로 현재 디렉토리명을 제안합니다.
|
|
485
|
+
|
|
486
|
+
3. **AskUserQuestion 호출:**
|
|
487
|
+
- question: "팀을 한 줄로 설명해주세요 (마켓플레이스에 표시됩니다)"
|
|
488
|
+
- 기본값으로 적절한 값을 만들어줍니다.
|
|
489
|
+
|
|
490
|
+
4. 자동 처리:
|
|
491
|
+
- \`mkdir -p .relay/skills .relay/commands .relay/agents .relay/rules\` 실행
|
|
492
|
+
- \`.relay/relay.yaml\` 생성:
|
|
493
|
+
\`\`\`yaml
|
|
494
|
+
name: <입력된 이름>
|
|
495
|
+
slug: <이름에서 자동 생성 — 소문자, 특수문자→하이픈>
|
|
496
|
+
description: <입력된 설명>
|
|
497
|
+
version: 1.0.0
|
|
498
|
+
tags: []
|
|
499
|
+
\`\`\`
|
|
500
|
+
- \`relay init --update\` 실행하여 글로벌 커맨드 설치 보장
|
|
501
|
+
|
|
502
|
+
5. "✓ 팀 프로젝트가 초기화되었습니다. 배포를 계속 진행합니다." 안내
|
|
503
|
+
|
|
504
|
+
### 0-2. 인증 확인
|
|
459
505
|
|
|
460
|
-
### 0-1. 인증 확인
|
|
461
506
|
- \`relay status --json\` 명령어를 실행하여 로그인 상태를 확인합니다.
|
|
462
|
-
-
|
|
463
|
-
|
|
507
|
+
- 인증되어 있으면 다음 단계로 진행합니다.
|
|
508
|
+
- 미인증이면 바로 로그인을 진행합니다:
|
|
509
|
+
1. \`relay login\` 실행 (timeout 300초)
|
|
510
|
+
- 브라우저가 자동으로 열리고, 사용자가 로그인을 완료하면 토큰이 자동 저장됩니다.
|
|
511
|
+
2. 완료 후 \`relay status --json\`으로 로그인 성공을 확인합니다.
|
|
464
512
|
|
|
465
|
-
### 0-
|
|
513
|
+
### 0-3. 팀 구조 분석
|
|
466
514
|
- .relay/ 디렉토리의 skills/, agents/, rules/, commands/를 탐색합니다.
|
|
467
515
|
- 각 파일의 이름과 description을 추출합니다.
|
|
468
|
-
- .relay/relay.yaml이 있으면 읽고, 없으면 사용자에게 팀 정보(name, slug, description, tags)를 물어보고 생성합니다.
|
|
469
516
|
|
|
470
517
|
## 인터랙션 플로우
|
|
471
518
|
|
|
472
|
-
이 커맨드는
|
|
519
|
+
이 커맨드는 5단계 인터랙션으로 진행됩니다. 각 단계에서 반드시 AskUserQuestion 도구를 사용하세요.
|
|
473
520
|
|
|
474
521
|
### Step 0. 버전 범프
|
|
475
522
|
|
|
@@ -590,85 +637,14 @@ requires:
|
|
|
590
637
|
- @alice/doc-writer
|
|
591
638
|
\`\`\`
|
|
592
639
|
|
|
593
|
-
### Step 3.
|
|
594
|
-
|
|
595
|
-
output/, results/, examples/, portfolio/ 디렉토리를 스캔하여 결과물(PNG, JPG, HTML, PDF)을 찾습니다.
|
|
596
|
-
HTML 파일은 Playwright 스크린샷으로 변환합니다.
|
|
597
|
-
|
|
598
|
-
발견된 파일을 번호 리스트로 보여줍니다:
|
|
599
|
-
|
|
600
|
-
\`\`\`
|
|
601
|
-
포트폴리오 후보 (4개)
|
|
602
|
-
|
|
603
|
-
1. output/report-example.png — 리포트 예시
|
|
604
|
-
2. output/dashboard.html → 스크린샷 변환
|
|
605
|
-
3. examples/result-1.png — 결과물 1
|
|
606
|
-
4. examples/result-2.png — 결과물 2
|
|
607
|
-
\`\`\`
|
|
608
|
-
|
|
609
|
-
**AskUserQuestion 호출:**
|
|
610
|
-
- question: "어떤 결과물을 포트폴리오에 포함할까요? (최대 5개)"
|
|
611
|
-
- options: \`["전체 포함", "1", "2", "3", "4", "건너뛰기"]\`
|
|
612
|
-
|
|
613
|
-
**응답 처리:**
|
|
614
|
-
- "전체 포함" → 모든 파일을 .relay/portfolio/에 저장
|
|
615
|
-
- 번호 선택 → 해당 파일만 포함 (여러 번 선택 가능 — 한 번 선택 후 추가 선택 여부를 다시 물어봄)
|
|
616
|
-
- "건너뛰기" → 포트폴리오 없이 진행
|
|
617
|
-
|
|
618
|
-
선택된 이미지를 .relay/portfolio/에 저장하고 relay.yaml의 portfolio.gallery에 등록합니다.
|
|
619
|
-
|
|
620
|
-
\`\`\`yaml
|
|
621
|
-
portfolio:
|
|
622
|
-
demo: # 선택
|
|
623
|
-
type: gif
|
|
624
|
-
path: portfolio/demo.gif
|
|
625
|
-
gallery: # 선택, 최대 5장
|
|
626
|
-
- path: portfolio/guide-preview.png
|
|
627
|
-
title: "사용가이드 미리보기"
|
|
628
|
-
- path: portfolio/report-example.png
|
|
629
|
-
title: "리포트 예시"
|
|
630
|
-
\`\`\`
|
|
631
|
-
|
|
632
|
-
파일이 하나도 발견되지 않으면 이 단계를 건너뜁니다.
|
|
633
|
-
|
|
634
|
-
### Step 4. GUIDE.html 컨펌
|
|
635
|
-
|
|
636
|
-
설치자가 읽을 수 있는 사용가이드를 HTML로 생성합니다.
|
|
637
|
-
|
|
638
|
-
#### 4-1. 팀 소스 분석 (자동)
|
|
639
|
-
- skills/, agents/, commands/ 디렉토리의 **모든 파일 내용**을 읽습니다.
|
|
640
|
-
- 각 스킬의 SKILL.md, 에이전트 설정, 커맨드 문서를 분석하여 팀의 파이프라인 흐름을 추론합니다.
|
|
641
|
-
|
|
642
|
-
#### 4-2. GUIDE.html 생성 (자동)
|
|
643
|
-
- 팀의 핵심 기능, 시작 방법, 파이프라인 흐름, Q&A를 포함하는 단일 HTML 가이드를 생성합니다.
|
|
644
|
-
- 디자인: 깔끔한 단일 페이지, 시스템 폰트, 최대 1200px 너비, 라이트 테마.
|
|
645
|
-
- 파이프라인이 없는 단순한 팀은 시작 방법 + 기능 설명 + Q&A만 포함합니다.
|
|
646
|
-
|
|
647
|
-
#### 4-3. 미리보기 + 컨펌
|
|
648
|
-
생성된 GUIDE.html을 브라우저에서 열어 빌더에게 미리보기를 보여줍니다.
|
|
649
|
-
|
|
650
|
-
**AskUserQuestion 호출:**
|
|
651
|
-
- question: "사용가이드를 확인해주세요. 진행할까요?"
|
|
652
|
-
- options: \`["진행", "수정 요청"]\`
|
|
653
|
-
|
|
654
|
-
**응답 처리:**
|
|
655
|
-
- "진행" → GUIDE.html을 \`.relay/GUIDE.html\`에 저장
|
|
656
|
-
- "수정 요청" → 사용자와 텍스트 대화로 수정사항 파악 → GUIDE.html 재생성 → 다시 미리보기 → AskUserQuestion 반복
|
|
657
|
-
|
|
658
|
-
#### 4-4. GUIDE.html 스크린샷 → gallery 등록 (자동)
|
|
659
|
-
- GUIDE.html을 Playwright로 열어 첫 화면(뷰포트 1200x630)을 스크린샷 캡처합니다.
|
|
660
|
-
- 결과 PNG를 \`./portfolio/guide-preview.png\`에 저장합니다.
|
|
661
|
-
- relay.yaml의 portfolio gallery 첫 번째 항목으로 자동 등록합니다.
|
|
662
|
-
- 이 이미지는 마켓플레이스 카드 및 OG 이미지의 fallback으로 사용됩니다 (demo > gallery 순).
|
|
663
|
-
|
|
664
|
-
### Step 5. 최종 확인 & 배포
|
|
640
|
+
### Step 3. 최종 확인 & 배포
|
|
665
641
|
|
|
666
|
-
####
|
|
642
|
+
#### 3-1. 메타데이터 생성 (자동)
|
|
667
643
|
- description: skills 내용 기반으로 자동 생성합니다.
|
|
668
644
|
- long_description: 팀 소개 마크다운을 자동 생성합니다 (README.md가 있으면 활용).
|
|
669
645
|
- tags: 팀 특성에 맞는 태그를 추천합니다.
|
|
670
646
|
|
|
671
|
-
####
|
|
647
|
+
#### 3-2. 배포 요약 + 최종 확인
|
|
672
648
|
배포할 내용을 요약 표시합니다:
|
|
673
649
|
|
|
674
650
|
\`\`\`
|
|
@@ -677,7 +653,6 @@ portfolio:
|
|
|
677
653
|
팀: my-team v1.0.0
|
|
678
654
|
공개: 마켓플레이스 (공개)
|
|
679
655
|
Skills: 3개, Commands: 5개
|
|
680
|
-
포트폴리오: 2장
|
|
681
656
|
requires: env 2개, cli 1개
|
|
682
657
|
\`\`\`
|
|
683
658
|
|
|
@@ -689,7 +664,7 @@ requires: env 2개, cli 1개
|
|
|
689
664
|
- "배포" → \`relay publish --json\` 실행 (슬래시 커맨드에서 이미 버전/visibility를 relay.yaml에 저장했으므로 --json으로 인터랙티브 프롬프트 생략)
|
|
690
665
|
- "취소" → 중단
|
|
691
666
|
|
|
692
|
-
####
|
|
667
|
+
#### 3-3. 배포 완료 & 온보딩 가이드
|
|
693
668
|
- 배포 결과와 마켓플레이스 URL을 보여줍니다.
|
|
694
669
|
- \`relay publish\` 출력 끝에 코드블록 형태의 온보딩 가이드가 포함됩니다.
|
|
695
670
|
- 이 코드블록을 사용자에게 그대로 보여줍니다.
|
|
@@ -714,12 +689,6 @@ ${BUSINESS_CARD_FORMAT}
|
|
|
714
689
|
→ 보안 스캔 ✓ 시크릿 없음 → requires 분석 결과 표시
|
|
715
690
|
→ AskUserQuestion: "requires 설정이 맞나요?" → ["확인", "수정"]
|
|
716
691
|
→ "확인"
|
|
717
|
-
→ 포트폴리오 후보 스캔 → 리스트 표시
|
|
718
|
-
→ AskUserQuestion: "어떤 결과물을 포함할까요?" → ["전체 포함", "1", "2", "건너뛰기"]
|
|
719
|
-
→ "1" 선택
|
|
720
|
-
→ GUIDE.html 생성 → 브라우저 미리보기
|
|
721
|
-
→ AskUserQuestion: "사용가이드 진행할까요?" → ["진행", "수정 요청"]
|
|
722
|
-
→ "진행"
|
|
723
692
|
→ 배포 요약 표시
|
|
724
693
|
→ AskUserQuestion: "이대로 배포할까요?" → ["배포", "취소"]
|
|
725
694
|
→ "배포" → relay publish 실행
|
package/dist/types.d.ts
CHANGED
|
@@ -34,6 +34,17 @@ export interface TeamRegistryInfo {
|
|
|
34
34
|
name: string;
|
|
35
35
|
description: string;
|
|
36
36
|
}[];
|
|
37
|
+
type?: 'command' | 'passive' | 'hybrid';
|
|
38
|
+
agent_details?: {
|
|
39
|
+
name: string;
|
|
40
|
+
description: string;
|
|
41
|
+
uses: string[];
|
|
42
|
+
}[];
|
|
43
|
+
skill_details?: {
|
|
44
|
+
name: string;
|
|
45
|
+
description: string;
|
|
46
|
+
uses: string[];
|
|
47
|
+
}[];
|
|
37
48
|
component_agents: number;
|
|
38
49
|
component_rules: number;
|
|
39
50
|
component_skills: number;
|
|
@@ -48,10 +59,6 @@ export interface TeamRegistryInfo {
|
|
|
48
59
|
display_name: string | null;
|
|
49
60
|
contact_links: ContactItem[] | Record<string, string>;
|
|
50
61
|
} | null;
|
|
51
|
-
latest_post?: {
|
|
52
|
-
title: string;
|
|
53
|
-
slug: string;
|
|
54
|
-
} | null;
|
|
55
62
|
}
|
|
56
63
|
export interface SearchResult {
|
|
57
64
|
/** scoped slug 포맷: "@owner/name" */
|