relayax-cli 0.2.18 → 0.2.20
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/install.js +20 -2
- package/dist/commands/publish.js +35 -16
- package/dist/commands/update.js +3 -0
- package/dist/lib/api.d.ts +2 -2
- package/dist/lib/api.js +26 -5
- package/dist/lib/command-adapter.js +46 -55
- package/dist/lib/guide.d.ts +5 -0
- package/dist/lib/guide.js +52 -0
- package/dist/lib/version-check.js +1 -1
- package/package.json +1 -1
package/dist/commands/install.js
CHANGED
|
@@ -11,11 +11,13 @@ const storage_js_1 = require("../lib/storage.js");
|
|
|
11
11
|
const config_js_1 = require("../lib/config.js");
|
|
12
12
|
const slug_js_1 = require("../lib/slug.js");
|
|
13
13
|
const contact_format_js_1 = require("../lib/contact-format.js");
|
|
14
|
+
const preamble_js_1 = require("../lib/preamble.js");
|
|
14
15
|
function registerInstall(program) {
|
|
15
16
|
program
|
|
16
17
|
.command('install <slug>')
|
|
17
18
|
.description('에이전트 팀 패키지를 .relay/teams/에 다운로드합니다')
|
|
18
|
-
.
|
|
19
|
+
.option('--no-guide', 'GUIDE.html 브라우저 자동 오픈을 비활성화합니다')
|
|
20
|
+
.action(async (slugInput, opts) => {
|
|
19
21
|
const json = program.opts().json ?? false;
|
|
20
22
|
const projectPath = process.cwd();
|
|
21
23
|
const tempDir = (0, storage_js_1.makeTempDir)();
|
|
@@ -50,6 +52,8 @@ function registerInstall(program) {
|
|
|
50
52
|
}
|
|
51
53
|
fs_1.default.mkdirSync(teamDir, { recursive: true });
|
|
52
54
|
await (0, storage_js_1.extractPackage)(tarPath, teamDir);
|
|
55
|
+
// 4.5. Inject preamble (update check) into SKILL.md and commands
|
|
56
|
+
(0, preamble_js_1.injectPreambleToTeam)(teamDir, slug);
|
|
53
57
|
// 5. Count extracted files
|
|
54
58
|
function countFiles(dir) {
|
|
55
59
|
let count = 0;
|
|
@@ -75,7 +79,7 @@ function registerInstall(program) {
|
|
|
75
79
|
};
|
|
76
80
|
(0, config_js_1.saveInstalled)(installed);
|
|
77
81
|
// 7. Report install (non-blocking)
|
|
78
|
-
await (0, api_js_1.reportInstall)(slug);
|
|
82
|
+
await (0, api_js_1.reportInstall)(slug, team.version);
|
|
79
83
|
const result = {
|
|
80
84
|
status: 'ok',
|
|
81
85
|
team: team.name,
|
|
@@ -121,6 +125,20 @@ function registerInstall(program) {
|
|
|
121
125
|
}
|
|
122
126
|
console.log(` \x1b[90m└${'─'.repeat(44)}┘\x1b[0m`);
|
|
123
127
|
}
|
|
128
|
+
// Open GUIDE.html in browser if present
|
|
129
|
+
const guideHtmlPath = path_1.default.join(teamDir, 'GUIDE.html');
|
|
130
|
+
if (opts.guide !== false && fs_1.default.existsSync(guideHtmlPath)) {
|
|
131
|
+
const isTTY = Boolean(process.stdin.isTTY);
|
|
132
|
+
if (isTTY) {
|
|
133
|
+
const { exec } = await import('child_process');
|
|
134
|
+
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start ""' : 'xdg-open';
|
|
135
|
+
exec(`${openCmd} "${guideHtmlPath}"`);
|
|
136
|
+
console.log(`\n 📖 사용가이드를 브라우저에서 열었습니다`);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
console.log(`\n 📖 사용가이드: ${guideHtmlPath}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
124
142
|
console.log('\n 에이전트가 /relay-install로 환경을 구성합니다.');
|
|
125
143
|
}
|
|
126
144
|
}
|
package/dist/commands/publish.js
CHANGED
|
@@ -30,12 +30,7 @@ function parseRelayYaml(content) {
|
|
|
30
30
|
const rawPortfolio = raw.portfolio;
|
|
31
31
|
const portfolio = {};
|
|
32
32
|
if (rawPortfolio && typeof rawPortfolio === 'object' && !Array.isArray(rawPortfolio)) {
|
|
33
|
-
//
|
|
34
|
-
if (rawPortfolio.cover && typeof rawPortfolio.cover === 'object') {
|
|
35
|
-
const c = rawPortfolio.cover;
|
|
36
|
-
if (c.path)
|
|
37
|
-
portfolio.cover = { path: String(c.path) };
|
|
38
|
-
}
|
|
33
|
+
// Slot-based format: { demo: {...}, gallery: [...] }
|
|
39
34
|
if (rawPortfolio.demo && typeof rawPortfolio.demo === 'object') {
|
|
40
35
|
const d = rawPortfolio.demo;
|
|
41
36
|
portfolio.demo = {
|
|
@@ -223,19 +218,18 @@ function countDir(teamDir, dirName) {
|
|
|
223
218
|
return 0;
|
|
224
219
|
return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.')).length;
|
|
225
220
|
}
|
|
221
|
+
function listDir(teamDir, dirName) {
|
|
222
|
+
const dirPath = path_1.default.join(teamDir, dirName);
|
|
223
|
+
if (!fs_1.default.existsSync(dirPath))
|
|
224
|
+
return [];
|
|
225
|
+
return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.'));
|
|
226
|
+
}
|
|
226
227
|
/**
|
|
227
228
|
* 슬롯 기반 포트폴리오를 PortfolioEntry[] 로 평탄화한다.
|
|
228
229
|
* relay.yaml에 슬롯이 정의되어 있으면 사용, 없으면 portfolio/ 자동 스캔.
|
|
229
230
|
*/
|
|
230
231
|
function collectPortfolio(relayDir, slots) {
|
|
231
232
|
const entries = [];
|
|
232
|
-
// Cover
|
|
233
|
-
if (slots.cover?.path) {
|
|
234
|
-
const absPath = path_1.default.resolve(relayDir, slots.cover.path);
|
|
235
|
-
if (fs_1.default.existsSync(absPath)) {
|
|
236
|
-
entries.push({ path: slots.cover.path, title: 'Cover', slot_type: 'cover' });
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
233
|
// Demo
|
|
240
234
|
if (slots.demo) {
|
|
241
235
|
if (slots.demo.type === 'video_url' && slots.demo.url) {
|
|
@@ -264,13 +258,13 @@ function collectPortfolio(relayDir, slots) {
|
|
|
264
258
|
const files = fs_1.default.readdirSync(portfolioDir)
|
|
265
259
|
.filter((f) => IMAGE_EXTS.some((ext) => f.toLowerCase().endsWith(ext)))
|
|
266
260
|
.sort();
|
|
267
|
-
//
|
|
261
|
+
// All images as gallery
|
|
268
262
|
for (let i = 0; i < files.length && i < 6; i++) {
|
|
269
263
|
const f = files[i];
|
|
270
264
|
entries.push({
|
|
271
265
|
path: path_1.default.join('portfolio', f),
|
|
272
266
|
title: path_1.default.basename(f, path_1.default.extname(f)).replace(/[-_]/g, ' '),
|
|
273
|
-
slot_type:
|
|
267
|
+
slot_type: 'gallery',
|
|
274
268
|
});
|
|
275
269
|
}
|
|
276
270
|
}
|
|
@@ -299,11 +293,14 @@ function resolveLongDescription(teamDir, yamlValue) {
|
|
|
299
293
|
async function createTarball(teamDir) {
|
|
300
294
|
const tmpFile = path_1.default.join(os_1.default.tmpdir(), `relay-publish-${Date.now()}.tar.gz`);
|
|
301
295
|
const dirsToInclude = VALID_DIRS.filter((d) => fs_1.default.existsSync(path_1.default.join(teamDir, d)));
|
|
302
|
-
// Include root SKILL.md if
|
|
296
|
+
// Include root SKILL.md and GUIDE.html if they exist
|
|
303
297
|
const entries = [...dirsToInclude];
|
|
304
298
|
if (fs_1.default.existsSync(path_1.default.join(teamDir, 'SKILL.md'))) {
|
|
305
299
|
entries.push('SKILL.md');
|
|
306
300
|
}
|
|
301
|
+
if (fs_1.default.existsSync(path_1.default.join(teamDir, 'GUIDE.html'))) {
|
|
302
|
+
entries.push('GUIDE.html');
|
|
303
|
+
}
|
|
307
304
|
await (0, tar_1.create)({
|
|
308
305
|
gzip: true,
|
|
309
306
|
file: tmpFile,
|
|
@@ -361,6 +358,13 @@ async function publishToApi(token, tarPath, metadata, teamDir, portfolioEntries)
|
|
|
361
358
|
}
|
|
362
359
|
form.append('portfolio_meta', JSON.stringify(portfolioMeta));
|
|
363
360
|
}
|
|
361
|
+
// Attach GUIDE.html if it exists
|
|
362
|
+
const guidePath = path_1.default.join(teamDir, 'GUIDE.html');
|
|
363
|
+
if (fs_1.default.existsSync(guidePath)) {
|
|
364
|
+
const guideBuffer = fs_1.default.readFileSync(guidePath);
|
|
365
|
+
const guideBlob = new Blob([guideBuffer], { type: 'text/html' });
|
|
366
|
+
form.append('guide', guideBlob, 'GUIDE.html');
|
|
367
|
+
}
|
|
364
368
|
const res = await fetch(`${config_js_1.API_URL}/api/publish`, {
|
|
365
369
|
method: 'POST',
|
|
366
370
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -528,6 +532,8 @@ function registerPublish(program) {
|
|
|
528
532
|
requires: config.requires,
|
|
529
533
|
visibility: config.visibility,
|
|
530
534
|
cli_version: cliPkg.version,
|
|
535
|
+
agent_names: listDir(relayDir, 'agents'),
|
|
536
|
+
skill_names: listDir(relayDir, 'skills'),
|
|
531
537
|
};
|
|
532
538
|
if (!json) {
|
|
533
539
|
console.error(`패키지 생성 중... (${config.name} v${config.version})`);
|
|
@@ -546,6 +552,19 @@ function registerPublish(program) {
|
|
|
546
552
|
console.error(`업로드 중...`);
|
|
547
553
|
}
|
|
548
554
|
const result = await publishToApi(token, tarPath, metadata, relayDir, portfolioEntries);
|
|
555
|
+
// Update local SKILL.md preamble with scoped slug from server (non-fatal)
|
|
556
|
+
try {
|
|
557
|
+
if (result.slug && result.slug !== config.slug) {
|
|
558
|
+
const localSkillMd = path_1.default.join(relayDir, 'SKILL.md');
|
|
559
|
+
if (fs_1.default.existsSync(localSkillMd)) {
|
|
560
|
+
const { injectPreamble } = await import('../lib/preamble.js');
|
|
561
|
+
injectPreamble(localSkillMd, result.slug);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
// preamble update is best-effort — publish already succeeded
|
|
567
|
+
}
|
|
549
568
|
if (json) {
|
|
550
569
|
console.log(JSON.stringify(result));
|
|
551
570
|
}
|
package/dist/commands/update.js
CHANGED
|
@@ -7,6 +7,7 @@ const installer_js_1 = require("../lib/installer.js");
|
|
|
7
7
|
const config_js_1 = require("../lib/config.js");
|
|
8
8
|
const slug_js_1 = require("../lib/slug.js");
|
|
9
9
|
const contact_format_js_1 = require("../lib/contact-format.js");
|
|
10
|
+
const preamble_js_1 = require("../lib/preamble.js");
|
|
10
11
|
function registerUpdate(program) {
|
|
11
12
|
program
|
|
12
13
|
.command('update <slug>')
|
|
@@ -69,6 +70,8 @@ function registerUpdate(program) {
|
|
|
69
70
|
// Extract
|
|
70
71
|
const extractDir = `${tempDir}/extracted`;
|
|
71
72
|
await (0, storage_js_1.extractPackage)(tarPath, extractDir);
|
|
73
|
+
// Inject preamble (update check) before copying
|
|
74
|
+
(0, preamble_js_1.injectPreambleToTeam)(extractDir, slug);
|
|
72
75
|
// Copy files to install_path
|
|
73
76
|
const files = (0, installer_js_1.installTeam)(extractDir, installPath);
|
|
74
77
|
// Update installed.json with new version
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -7,12 +7,12 @@ export interface TeamVersionInfo {
|
|
|
7
7
|
created_at: string;
|
|
8
8
|
}
|
|
9
9
|
export declare function fetchTeamVersions(slug: string): Promise<TeamVersionInfo[]>;
|
|
10
|
-
export declare function reportInstall(slug: string): Promise<void>;
|
|
10
|
+
export declare function reportInstall(slug: string, version?: string): Promise<void>;
|
|
11
11
|
export interface ResolvedSlug {
|
|
12
12
|
owner: string;
|
|
13
13
|
name: string;
|
|
14
14
|
full: string;
|
|
15
15
|
}
|
|
16
16
|
export declare function resolveSlugFromServer(name: string): Promise<ResolvedSlug[]>;
|
|
17
|
-
export declare function sendUsagePing(slug: string): Promise<void>;
|
|
17
|
+
export declare function sendUsagePing(slug: string, version?: string): Promise<void>;
|
|
18
18
|
export declare function followBuilder(username: string): Promise<void>;
|
package/dist/lib/api.js
CHANGED
|
@@ -41,10 +41,24 @@ async function fetchTeamVersions(slug) {
|
|
|
41
41
|
}
|
|
42
42
|
return res.json();
|
|
43
43
|
}
|
|
44
|
-
async function reportInstall(slug) {
|
|
44
|
+
async function reportInstall(slug, version) {
|
|
45
45
|
const registrySlug = slug.startsWith('@') ? slug.slice(1) : slug;
|
|
46
46
|
const url = `${config_js_1.API_URL}/api/registry/${registrySlug}/install`;
|
|
47
|
-
|
|
47
|
+
const headers = {};
|
|
48
|
+
const body = {};
|
|
49
|
+
if (version) {
|
|
50
|
+
headers['Content-Type'] = 'application/json';
|
|
51
|
+
body.version = version;
|
|
52
|
+
}
|
|
53
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
54
|
+
if (token) {
|
|
55
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
56
|
+
}
|
|
57
|
+
await fetch(url, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
60
|
+
body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined,
|
|
61
|
+
}).catch(() => {
|
|
48
62
|
// non-critical: ignore errors
|
|
49
63
|
});
|
|
50
64
|
}
|
|
@@ -57,7 +71,7 @@ async function resolveSlugFromServer(name) {
|
|
|
57
71
|
const data = (await res.json());
|
|
58
72
|
return data.results;
|
|
59
73
|
}
|
|
60
|
-
async function sendUsagePing(slug) {
|
|
74
|
+
async function sendUsagePing(slug, version) {
|
|
61
75
|
const registrySlug = slug.startsWith('@') ? slug.slice(1) : slug;
|
|
62
76
|
const { createHash } = await import('crypto');
|
|
63
77
|
const { hostname, userInfo } = await import('os');
|
|
@@ -65,10 +79,17 @@ async function sendUsagePing(slug) {
|
|
|
65
79
|
.update(`${hostname()}:${userInfo().username}`)
|
|
66
80
|
.digest('hex');
|
|
67
81
|
const url = `${config_js_1.API_URL}/api/registry/${registrySlug}/ping`;
|
|
82
|
+
const payload = { device_hash: deviceHash };
|
|
83
|
+
if (version)
|
|
84
|
+
payload.installed_version = version;
|
|
85
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
86
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
87
|
+
if (token)
|
|
88
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
68
89
|
await fetch(url, {
|
|
69
90
|
method: 'POST',
|
|
70
|
-
headers
|
|
71
|
-
body: JSON.stringify(
|
|
91
|
+
headers,
|
|
92
|
+
body: JSON.stringify(payload),
|
|
72
93
|
}).catch(() => {
|
|
73
94
|
// fire-and-forget: ignore errors
|
|
74
95
|
});
|
|
@@ -241,7 +241,7 @@ exports.BUILDER_COMMANDS = [
|
|
|
241
241
|
{
|
|
242
242
|
id: 'relay-publish',
|
|
243
243
|
description: '현재 팀 패키지를 포트폴리오와 함께 relay 마켓플레이스에 배포합니다',
|
|
244
|
-
body: `현재 디렉토리의 에이전트 팀(.relay/)을 분석하고, 보안 점검 및 requirements를 구성한 뒤, 포트폴리오를 생성하고 relay 마켓플레이스에 배포합니다.
|
|
244
|
+
body: `현재 디렉토리의 에이전트 팀(.relay/)을 분석하고, 보안 점검 및 requirements를 구성한 뒤, 사용가이드와 포트폴리오를 생성하고 relay 마켓플레이스에 배포합니다.
|
|
245
245
|
|
|
246
246
|
## 실행 단계
|
|
247
247
|
|
|
@@ -327,76 +327,67 @@ requires:
|
|
|
327
327
|
- @example/contents-team
|
|
328
328
|
\`\`\`
|
|
329
329
|
|
|
330
|
-
### 4. 포트폴리오
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
- 규격: 1200x630px, WebP, 최대 500KB
|
|
336
|
-
- 팀 구조를 요약하는 카드 HTML을 생성합니다:
|
|
337
|
-
- 팀 이름, 버전
|
|
338
|
-
- Skills 목록 (이름 + 설명)
|
|
339
|
-
- Commands 목록
|
|
340
|
-
- 주요 기능 요약
|
|
341
|
-
- 생성된 HTML을 Playwright로 1200x630 뷰포트에서 스크린샷 캡처합니다.
|
|
342
|
-
- .relay/portfolio/cover.png에 저장합니다.
|
|
343
|
-
- 사용자에게 "이 cover를 사용할까요?" 확인. 직접 제공도 가능.
|
|
344
|
-
|
|
345
|
-
#### 슬롯 2: demo (자동 생성 — 동작 시연)
|
|
346
|
-
에이전트가 **직접 팀의 커맨드를 실행하여** demo를 자동 생성합니다. 사용자에게 묻기 전에 먼저 만듭니다.
|
|
347
|
-
|
|
348
|
-
**생성 절차:**
|
|
349
|
-
1. .relay/commands/ 의 커맨드 목록을 읽고, 가장 대표적인 커맨드를 선택합니다.
|
|
350
|
-
2. 해당 커맨드를 예시 주제/데이터로 실행합니다.
|
|
351
|
-
- 콘텐츠 팀: "라즈베리파이5 신제품 소개" 같은 예시 주제로 커맨드 실행
|
|
352
|
-
- 크롤링 팀: 예시 URL로 실행하며 브라우저 동작을 녹화
|
|
353
|
-
- 분석 팀: 샘플 데이터로 실행하여 결과 캡처
|
|
354
|
-
3. 실행 결과물로 demo를 생성합니다:
|
|
355
|
-
- 여러 장 이미지 (카드뉴스 등): 슬라이드쇼 GIF (각 장 2초 간격)
|
|
356
|
-
- 브라우저 동작: 동작 녹화 GIF
|
|
357
|
-
- 단일 결과물: 결과 이미지를 demo로 사용
|
|
358
|
-
4. 생성된 demo를 사용자에게 보여줍니다: "이 demo를 사용할까요?"
|
|
359
|
-
- 수정 요청 가능: "다른 주제로 다시 만들어줘", "좀 더 짧게"
|
|
360
|
-
- 확정 시 .relay/portfolio/demo.gif에 저장
|
|
361
|
-
|
|
362
|
-
**예시 주제 선택 기준:**
|
|
363
|
-
- relay.yaml의 description, tags에서 도메인 힌트 추출
|
|
364
|
-
- 기존 output/에 결과물이 있으면 같은 주제 활용
|
|
365
|
-
- 없으면 팀 설명에서 가장 그럴듯한 예시 주제를 자체 생성
|
|
366
|
-
|
|
367
|
-
**GIF 규격:** 최대 5MB, .relay/portfolio/demo.gif
|
|
368
|
-
**대안:** 외부 영상 URL (YouTube, Loom)도 가능 — relay.yaml에 기록
|
|
369
|
-
|
|
370
|
-
#### 슬롯 3: gallery (선택 — 결과물 쇼케이스, 최대 5장)
|
|
371
|
-
- 규격: 800x600px 이하, WebP, 각 500KB 이하
|
|
372
|
-
- output/, results/, examples/ 디렉토리를 스캔합니다.
|
|
373
|
-
- 큰 이미지 → 사용자에게 "어느 영역을 보여줄까요?" 확인 후 핵심 영역 crop
|
|
374
|
-
- HTML 파일 → Playwright 스크린샷으로 변환
|
|
330
|
+
### 4. 포트폴리오 수집
|
|
331
|
+
|
|
332
|
+
- output/, results/, examples/, portfolio/ 디렉토리를 스캔합니다.
|
|
333
|
+
- 발견된 결과물(PNG, JPG, HTML, PDF)을 사용자에게 보여줍니다.
|
|
334
|
+
- HTML 파일은 Playwright 스크린샷으로 변환합니다.
|
|
375
335
|
- 사용자가 포트폴리오에 포함할 항목을 선택합니다.
|
|
376
|
-
- .relay/portfolio/에 저장합니다.
|
|
336
|
+
- 선택된 이미지를 .relay/portfolio/에 저장합니다.
|
|
337
|
+
|
|
338
|
+
### 5. 사용가이드 생성 (GUIDE.html)
|
|
339
|
+
|
|
340
|
+
설치자가 읽을 수 있는 사용가이드를 HTML로 생성합니다.
|
|
341
|
+
|
|
342
|
+
#### 5-1. 팀 소스 분석
|
|
343
|
+
- skills/, agents/, commands/ 디렉토리의 **모든 파일 내용**을 읽습니다.
|
|
344
|
+
- 각 스킬의 SKILL.md, 에이전트 설정, 커맨드 문서를 분석하여 팀의 파이프라인 흐름을 추론합니다.
|
|
345
|
+
|
|
346
|
+
#### 5-2. GUIDE.html 생성
|
|
347
|
+
- \`cli/src/lib/guide.ts\`의 \`GUIDE_HTML_PROMPT\`를 읽고, 해당 프롬프트의 콘텐츠 구조와 디자인 규칙을 따라 GUIDE.html을 생성합니다.
|
|
348
|
+
- 5-1에서 분석한 팀 소스 정보를 프롬프트에 반영합니다.
|
|
349
|
+
- 파이프라인이 없는 단순한 팀은 시작 방법 + 기능 설명 + Q&A만 포함합니다.
|
|
350
|
+
|
|
351
|
+
#### 5-3. 미리보기 + 컨펌
|
|
352
|
+
- 생성된 GUIDE.html을 브라우저에서 열어 빌더에게 미리보기를 보여줍니다.
|
|
353
|
+
- 빌더에게 확인: "사용가이드를 확인해주세요. 이대로 진행할까요?"
|
|
354
|
+
|
|
355
|
+
#### 5-4. 재생성 루프
|
|
356
|
+
- 빌더가 수정을 요청하면 (예: "Q&A 추가해줘", "파이프라인 설명 더 자세히") 요청사항을 반영하여 GUIDE.html을 재생성합니다.
|
|
357
|
+
- 재생성 후 다시 브라우저에서 미리보기를 보여줍니다.
|
|
358
|
+
- 빌더가 컨펌할 때까지 반복합니다.
|
|
359
|
+
|
|
360
|
+
#### 5-5. 저장
|
|
361
|
+
- 컨펌된 GUIDE.html을 \`.relay/GUIDE.html\`에 저장합니다.
|
|
362
|
+
|
|
363
|
+
#### 5-6. GUIDE.html 스크린샷 → gallery 등록
|
|
364
|
+
- GUIDE.html을 Playwright로 열어 첫 화면(뷰포트 1200x630)을 스크린샷 캡처합니다. (gstack 또는 webapp-testing 스킬 활용)
|
|
365
|
+
- 결과 PNG를 \`./portfolio/guide-preview.png\`에 저장합니다.
|
|
366
|
+
- relay.yaml의 portfolio gallery 첫 번째 항목으로 자동 등록합니다.
|
|
367
|
+
- 이 이미지는 마켓플레이스 카드 및 OG 이미지의 fallback으로 사용됩니다 (demo > gallery 순).
|
|
377
368
|
|
|
378
|
-
###
|
|
369
|
+
### 6. 메타데이터 생성
|
|
379
370
|
- description: skills 내용 기반으로 자동 생성합니다.
|
|
380
371
|
- long_description: 팀 소개 마크다운을 자동 생성합니다 (README.md가 있으면 활용).
|
|
381
372
|
- tags: 팀 특성에 맞는 태그를 추천합니다.
|
|
382
373
|
- 사용자에게 확인: "이대로 배포할까요?"
|
|
383
374
|
|
|
384
|
-
###
|
|
375
|
+
### 7. .relay/relay.yaml 업데이트
|
|
385
376
|
- 메타데이터, requires, 포트폴리오 슬롯을 .relay/relay.yaml에 반영합니다.
|
|
386
377
|
|
|
387
378
|
\`\`\`yaml
|
|
388
379
|
portfolio:
|
|
389
|
-
cover:
|
|
390
|
-
path: portfolio/cover.png
|
|
391
380
|
demo: # 선택
|
|
392
381
|
type: gif
|
|
393
382
|
path: portfolio/demo.gif
|
|
394
383
|
gallery: # 선택, 최대 5장
|
|
384
|
+
- path: portfolio/guide-preview.png
|
|
385
|
+
title: "사용가이드 미리보기"
|
|
395
386
|
- path: portfolio/example-1.png
|
|
396
387
|
title: "카드뉴스 예시"
|
|
397
388
|
\`\`\`
|
|
398
389
|
|
|
399
|
-
###
|
|
390
|
+
### 8. 배포
|
|
400
391
|
- \`relay publish\` 명령어를 실행합니다.
|
|
401
392
|
- 배포 결과와 마켓플레이스 URL을 보여줍니다.
|
|
402
393
|
${BUSINESS_CARD_FORMAT}
|
|
@@ -408,9 +399,9 @@ ${BUSINESS_CARD_FORMAT}
|
|
|
408
399
|
→ 보안 스캔: ✓ 시크릿 없음
|
|
409
400
|
→ 환경변수 감지: OPENAI_API_KEY (필수), DATABASE_URL (선택)
|
|
410
401
|
→ requires 업데이트 완료
|
|
411
|
-
→
|
|
412
|
-
→
|
|
413
|
-
→
|
|
402
|
+
→ 포트폴리오: output/ 스캔 → "카드뉴스 예시.png 포함?" → Yes
|
|
403
|
+
→ GUIDE.html 생성 → 브라우저에서 미리보기 → 빌더 컨펌
|
|
404
|
+
→ GUIDE.html 스크린샷 → gallery 첫 번째 이미지로 등록
|
|
414
405
|
→ relay publish 실행
|
|
415
406
|
→ "배포 완료! URL: https://relayax.com/teams/my-team"`,
|
|
416
407
|
},
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GUIDE.html 생성 프롬프트 템플릿.
|
|
4
|
+
* publish slash command에서 Claude Code가 이 프롬프트를 읽고 GUIDE.html을 생성한다.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.GUIDE_HTML_PROMPT = void 0;
|
|
8
|
+
exports.GUIDE_HTML_PROMPT = `
|
|
9
|
+
당신은 에이전트 팀의 사용가이드(GUIDE.html)를 생성하는 에이전트입니다.
|
|
10
|
+
팀의 skills/, agents/, commands/ 파일을 분석한 결과를 바탕으로, 설치자가 읽을 수 있는 standalone HTML 파일을 생성하세요.
|
|
11
|
+
|
|
12
|
+
## 콘텐츠 구조 (순서대로)
|
|
13
|
+
|
|
14
|
+
1. **헤더**: 팀 이름 + 한 줄 소개
|
|
15
|
+
2. **설치 방법 (준비하기)**:
|
|
16
|
+
- Step 1: CLI 설치 — \`npm install -g relayax-cli\`
|
|
17
|
+
- Step 2: 팀 설치 — \`relay install @owner/slug\` (실제 팀의 scoped slug 사용)
|
|
18
|
+
- Step 3: 에이전트에 복사붙여넣기 — "사용하시는 AI 에이전트(Claude Code, Codex 등)의 채팅창에 아래 명령어를 붙여넣기 하세요!"
|
|
19
|
+
3. **안내 Callout**: 노란 배경 박스로 시작 방법 강조 — "채팅창에 '○○○'라고 입력하세요."
|
|
20
|
+
4. **워크플로우 요약**: 전체 흐름을 한 줄로 — \`[0 온보딩 → 1 분석 → 1.5 선택 → 2 추천 → 3 생성]\`
|
|
21
|
+
5. **파이프라인 다이어그램**: 2-column 세로 플로우차트
|
|
22
|
+
- 왼쪽 컬럼: 파이프라인 단계 (노드 + 연결선, 세로 흐름)
|
|
23
|
+
- 오른쪽 컬럼: 해당 단계에서 사용하는 skill 카드 (skill명 + 한 줄 설명)
|
|
24
|
+
- 색상 구분: 유저 개입 단계 = 베이지/주황 톤, AI 자동 단계 = 연두/초록 톤
|
|
25
|
+
- 상단에 범례: ■ 유저 개입 / ■ 자동 실행
|
|
26
|
+
- 선택적 단계(optional)는 점선 테두리 그룹으로 묶기
|
|
27
|
+
6. **단계별 상세 테이블**: 4-column 테이블
|
|
28
|
+
- 컬럼: 단계 | skill명 | 액션 | 상세
|
|
29
|
+
- 각 행에 역할 뱃지: "AI가 알아서 해줘요" (초록 배경) / "직접 아래 액션을 수행해주세요!" (주황 배경, 볼드)
|
|
30
|
+
- 상세 컬럼에 bullet list로 구체적 동작 설명
|
|
31
|
+
- 주의사항은 빨간 이탤릭 (*주의: ...)
|
|
32
|
+
7. **사전 준비사항**: 필요한 환경변수, 접근 권한, 데이터 등
|
|
33
|
+
8. **Q&A**: 접이식 토글(details/summary)로 자주 묻는 질문
|
|
34
|
+
- 커스터마이징 방법 (개수 변경, 타입 고정 등)
|
|
35
|
+
- 중단/재시작 방법
|
|
36
|
+
- 데이터 형식 요구사항
|
|
37
|
+
|
|
38
|
+
파이프라인이 없는 단순한 팀은 시작 방법 + 기능 설명 + Q&A만 포함합니다.
|
|
39
|
+
|
|
40
|
+
## 디자인 규칙 (Notion 스타일)
|
|
41
|
+
|
|
42
|
+
- **라이트 모드**: 배경 #ffffff / 부배경 #fafaf8, 텍스트 다크 #1a1a18
|
|
43
|
+
- **레이아웃**: max-width 900px, padding 40px, Notion 문서형 넓은 여백
|
|
44
|
+
- **타이포그래피**: system-ui, -apple-system, sans-serif. 본문 15px, line-height 1.7
|
|
45
|
+
- **Callout 박스**: 노란 배경 #fffbeb + 좌측 보더 4px solid #fcd34d (중요 안내), 파란 배경 #eff6ff + 보더 #93c5fd (참고)
|
|
46
|
+
- **역할 뱃지**: 둥근 모서리 pill, 초록 배경 #e0f5ea + 텍스트 #0d6b2e (AI) / 주황 배경 #fff0e0 + 텍스트 #b85a00 (유저)
|
|
47
|
+
- **테이블**: 깔끔한 border #e8e8e4, hover 시 행 하이라이트 #f5f5f3, 헤더 회색 배경 #f5f5f3
|
|
48
|
+
- **색상**: 유저 단계 = 베이지/주황 (#fff8f0, #f0c090), AI 단계 = 연두 (#f0faf4, #90d4a8)
|
|
49
|
+
- **톤**: 따뜻하고 친근한 한국어, 이모지 적절히 활용
|
|
50
|
+
- **반응형**: 600px 이하에서 테이블 가로스크롤, 다이어그램 단일 컬럼
|
|
51
|
+
- **기술 제약**: standalone HTML, CSS는 style 태그 내, 외부 CDN/폰트 없음, JS 최소화 (details/summary만)
|
|
52
|
+
`.trim();
|
|
@@ -43,7 +43,7 @@ async function checkTeamVersion(slug, force) {
|
|
|
43
43
|
const team = await (0, api_js_1.fetchTeamInfo)(slug);
|
|
44
44
|
(0, update_cache_js_1.updateCacheTimestamp)(slug);
|
|
45
45
|
// Fire-and-forget usage ping (only when cache expired = actual API call happened)
|
|
46
|
-
(0, api_js_1.sendUsagePing)(slug);
|
|
46
|
+
(0, api_js_1.sendUsagePing)(slug, entry.version);
|
|
47
47
|
if (team.version !== entry.version) {
|
|
48
48
|
return {
|
|
49
49
|
type: 'team',
|