relayax-cli 0.1.7 → 0.1.9
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 +24 -6
- package/dist/commands/outdated.d.ts +2 -0
- package/dist/commands/outdated.js +70 -0
- package/dist/commands/publish.d.ts +27 -0
- package/dist/commands/publish.js +138 -103
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +93 -0
- package/dist/index.js +4 -0
- package/dist/lib/api.d.ts +7 -1
- package/dist/lib/api.js +13 -2
- package/dist/lib/command-adapter.js +42 -18
- package/dist/types.d.ts +1 -0
- package/package.json +3 -1
package/dist/commands/install.js
CHANGED
|
@@ -10,6 +10,7 @@ function registerInstall(program) {
|
|
|
10
10
|
.command('install <slug>')
|
|
11
11
|
.description('에이전트 팀 설치 (감지된 에이전트 CLI에 설치)')
|
|
12
12
|
.option('--path <install_path>', '설치 경로 지정 (기본: ./.claude)')
|
|
13
|
+
.option('--code <code>', '초대 코드 (invite-only 팀 설치 시 필요)')
|
|
13
14
|
.action(async (slug, opts) => {
|
|
14
15
|
const json = program.opts().json ?? false;
|
|
15
16
|
const installPath = (0, config_js_1.getInstallPath)(opts.path);
|
|
@@ -17,14 +18,31 @@ function registerInstall(program) {
|
|
|
17
18
|
try {
|
|
18
19
|
// 1. Fetch team metadata
|
|
19
20
|
const team = await (0, api_js_1.fetchTeamInfo)(slug);
|
|
20
|
-
// 2.
|
|
21
|
+
// 2. Visibility check
|
|
22
|
+
const visibility = team.visibility ?? 'public';
|
|
23
|
+
if (visibility === 'login-only') {
|
|
24
|
+
const token = (0, config_js_1.loadToken)();
|
|
25
|
+
if (!token) {
|
|
26
|
+
const err = { error: 'LOGIN_REQUIRED', visibility: 'login-only', slug, message: '이 팀은 로그인이 필요합니다.' };
|
|
27
|
+
console.error(JSON.stringify(err));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else if (visibility === 'invite-only') {
|
|
32
|
+
if (!opts.code) {
|
|
33
|
+
const err = { error: 'INVITE_REQUIRED', visibility: 'invite-only', slug, message: '초대 코드가 필요합니다.' };
|
|
34
|
+
console.error(JSON.stringify(err));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// 3. Download package
|
|
21
39
|
const tarPath = await (0, storage_js_1.downloadPackage)(team.package_url, tempDir);
|
|
22
|
-
//
|
|
40
|
+
// 4. Extract
|
|
23
41
|
const extractDir = `${tempDir}/extracted`;
|
|
24
42
|
await (0, storage_js_1.extractPackage)(tarPath, extractDir);
|
|
25
|
-
//
|
|
43
|
+
// 5. Copy files to install_path
|
|
26
44
|
const files = (0, installer_js_1.installTeam)(extractDir, installPath);
|
|
27
|
-
//
|
|
45
|
+
// 6. Record in installed.json
|
|
28
46
|
const installed = (0, config_js_1.loadInstalled)();
|
|
29
47
|
installed[slug] = {
|
|
30
48
|
version: team.version,
|
|
@@ -32,8 +50,8 @@ function registerInstall(program) {
|
|
|
32
50
|
files,
|
|
33
51
|
};
|
|
34
52
|
(0, config_js_1.saveInstalled)(installed);
|
|
35
|
-
//
|
|
36
|
-
await (0, api_js_1.reportInstall)(slug);
|
|
53
|
+
// 7. Report install (non-blocking)
|
|
54
|
+
await (0, api_js_1.reportInstall)(slug, opts.code);
|
|
37
55
|
const result = {
|
|
38
56
|
status: 'ok',
|
|
39
57
|
team: team.name,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerOutdated = registerOutdated;
|
|
4
|
+
const api_js_1 = require("../lib/api.js");
|
|
5
|
+
const config_js_1 = require("../lib/config.js");
|
|
6
|
+
function registerOutdated(program) {
|
|
7
|
+
program
|
|
8
|
+
.command('outdated')
|
|
9
|
+
.description('설치된 팀의 업데이트 가능 여부를 확인합니다')
|
|
10
|
+
.action(async () => {
|
|
11
|
+
const json = program.opts().json ?? false;
|
|
12
|
+
const installed = (0, config_js_1.loadInstalled)();
|
|
13
|
+
const slugs = Object.keys(installed);
|
|
14
|
+
if (slugs.length === 0) {
|
|
15
|
+
if (json) {
|
|
16
|
+
console.log(JSON.stringify([]));
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
console.log('설치된 팀이 없습니다.');
|
|
20
|
+
}
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// Fetch latest versions in parallel
|
|
24
|
+
const results = await Promise.all(slugs.map(async (slug) => {
|
|
25
|
+
const current = installed[slug].version;
|
|
26
|
+
try {
|
|
27
|
+
const team = await (0, api_js_1.fetchTeamInfo)(slug);
|
|
28
|
+
const latest = team.version;
|
|
29
|
+
return {
|
|
30
|
+
slug,
|
|
31
|
+
current,
|
|
32
|
+
latest,
|
|
33
|
+
status: current === latest ? 'up-to-date' : 'outdated',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return { slug, current, latest: '?', status: 'unknown' };
|
|
38
|
+
}
|
|
39
|
+
}));
|
|
40
|
+
if (json) {
|
|
41
|
+
console.log(JSON.stringify(results));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const allUpToDate = results.every((r) => r.status === 'up-to-date');
|
|
45
|
+
if (allUpToDate) {
|
|
46
|
+
console.log('모든 팀이 최신 버전입니다.');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Determine column widths
|
|
50
|
+
const COL_TEAM = Math.max(4, ...results.map((r) => r.slug.length));
|
|
51
|
+
const COL_CURRENT = Math.max(4, ...results.map((r) => `v${r.current}`.length));
|
|
52
|
+
const COL_LATEST = Math.max(4, ...results.map((r) => `v${r.latest}`.length));
|
|
53
|
+
const pad = (s, len) => s.padEnd(len);
|
|
54
|
+
const header = `${pad('팀', COL_TEAM)} ${pad('현재', COL_CURRENT)} ${pad('최신', COL_LATEST)} 상태`;
|
|
55
|
+
const separator = '-'.repeat(header.length);
|
|
56
|
+
console.log(header);
|
|
57
|
+
console.log(separator);
|
|
58
|
+
for (const entry of results) {
|
|
59
|
+
const statusLabel = entry.status === 'outdated'
|
|
60
|
+
? '\x1b[33m업데이트 가능\x1b[0m'
|
|
61
|
+
: entry.status === 'up-to-date'
|
|
62
|
+
? '\x1b[32m✓ 최신\x1b[0m'
|
|
63
|
+
: '\x1b[31m조회 실패\x1b[0m';
|
|
64
|
+
const slugCol = pad(entry.slug, COL_TEAM);
|
|
65
|
+
const currentCol = pad(`v${entry.current}`, COL_CURRENT);
|
|
66
|
+
const latestCol = pad(`v${entry.latest}`, COL_LATEST);
|
|
67
|
+
console.log(`${slugCol} ${currentCol} ${latestCol} ${statusLabel}`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -1,2 +1,29 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
export interface RequiresCli {
|
|
3
|
+
name: string;
|
|
4
|
+
install?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface RequiresMcp {
|
|
7
|
+
name: string;
|
|
8
|
+
package?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface RequiresEnv {
|
|
11
|
+
name: string;
|
|
12
|
+
optional?: boolean;
|
|
13
|
+
description?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface Requires {
|
|
16
|
+
cli?: RequiresCli[];
|
|
17
|
+
mcp?: RequiresMcp[];
|
|
18
|
+
npm?: string[];
|
|
19
|
+
env?: RequiresEnv[];
|
|
20
|
+
teams?: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface ContactInfo {
|
|
23
|
+
email?: string;
|
|
24
|
+
kakao?: string;
|
|
25
|
+
x?: string;
|
|
26
|
+
linkedin?: string;
|
|
27
|
+
website?: string;
|
|
28
|
+
}
|
|
2
29
|
export declare function registerPublish(program: Command): void;
|
package/dist/commands/publish.js
CHANGED
|
@@ -7,111 +7,43 @@ 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 js_yaml_1 = __importDefault(require("js-yaml"));
|
|
10
11
|
const tar_1 = require("tar");
|
|
11
12
|
const config_js_1 = require("../lib/config.js");
|
|
12
13
|
const VALID_DIRS = ['skills', 'agents', 'rules', 'commands'];
|
|
13
14
|
const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.webp'];
|
|
14
15
|
function parseRelayYaml(content) {
|
|
15
|
-
const
|
|
16
|
-
const tags =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
inLongDesc = false;
|
|
33
|
-
result.long_description = longDescLines.join('\n').trim();
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
// tags list
|
|
37
|
-
if (inTags) {
|
|
38
|
-
if (trimmed.startsWith('- ')) {
|
|
39
|
-
tags.push(trimmed.slice(2).replace(/^["']|["']$/g, ''));
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
inTags = false;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
// portfolio list
|
|
47
|
-
if (inPortfolio) {
|
|
48
|
-
if (trimmed.startsWith('- path:')) {
|
|
49
|
-
if (currentPortfolioItem?.path) {
|
|
50
|
-
portfolio.push({ path: currentPortfolioItem.path, title: currentPortfolioItem.title ?? '', description: currentPortfolioItem.description });
|
|
51
|
-
}
|
|
52
|
-
currentPortfolioItem = { path: trimmed.slice(8).replace(/^["']|["']$/g, '').trim() };
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
if (trimmed.startsWith('title:') && currentPortfolioItem) {
|
|
56
|
-
currentPortfolioItem.title = trimmed.slice(6).replace(/^["']|["']$/g, '').trim();
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
if (trimmed.startsWith('description:') && currentPortfolioItem) {
|
|
60
|
-
currentPortfolioItem.description = trimmed.slice(12).replace(/^["']|["']$/g, '').trim();
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
if (!trimmed.startsWith('-') && !trimmed.startsWith('title:') && !trimmed.startsWith('description:') && trimmed !== '') {
|
|
64
|
-
// End of portfolio section
|
|
65
|
-
if (currentPortfolioItem?.path) {
|
|
66
|
-
portfolio.push({ path: currentPortfolioItem.path, title: currentPortfolioItem.title ?? '', description: currentPortfolioItem.description });
|
|
67
|
-
}
|
|
68
|
-
currentPortfolioItem = null;
|
|
69
|
-
inPortfolio = false;
|
|
70
|
-
}
|
|
71
|
-
else if (trimmed === '') {
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (trimmed === 'tags: []') {
|
|
76
|
-
result.tags = [];
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (trimmed === 'tags:') {
|
|
80
|
-
inTags = true;
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
if (trimmed === 'portfolio:') {
|
|
84
|
-
inPortfolio = true;
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
if (trimmed === 'portfolio: []') {
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
if (trimmed === 'long_description: |') {
|
|
91
|
-
inLongDesc = true;
|
|
92
|
-
longDescLines = [];
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
const match = trimmed.match(/^(\w+):\s*["']?(.+?)["']?$/);
|
|
96
|
-
if (match) {
|
|
97
|
-
result[match[1]] = match[2];
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
// Flush remaining
|
|
101
|
-
if (inLongDesc && longDescLines.length > 0) {
|
|
102
|
-
result.long_description = longDescLines.join('\n').trim();
|
|
103
|
-
}
|
|
104
|
-
if (currentPortfolioItem?.path) {
|
|
105
|
-
portfolio.push({ path: currentPortfolioItem.path, title: currentPortfolioItem.title ?? '', description: currentPortfolioItem.description });
|
|
106
|
-
}
|
|
16
|
+
const raw = js_yaml_1.default.load(content) ?? {};
|
|
17
|
+
const tags = Array.isArray(raw.tags)
|
|
18
|
+
? raw.tags.map((t) => String(t))
|
|
19
|
+
: [];
|
|
20
|
+
const portfolio = Array.isArray(raw.portfolio)
|
|
21
|
+
? raw.portfolio.map((p) => ({
|
|
22
|
+
path: String(p.path ?? ''),
|
|
23
|
+
title: String(p.title ?? ''),
|
|
24
|
+
description: p.description ? String(p.description) : undefined,
|
|
25
|
+
})).filter((p) => p.path)
|
|
26
|
+
: [];
|
|
27
|
+
const requires = raw.requires;
|
|
28
|
+
const rawVisibility = String(raw.visibility ?? '');
|
|
29
|
+
const visibility = rawVisibility === 'login-only' ? 'login-only'
|
|
30
|
+
: rawVisibility === 'invite-only' ? 'invite-only'
|
|
31
|
+
: rawVisibility === 'public' ? 'public'
|
|
32
|
+
: undefined;
|
|
107
33
|
return {
|
|
108
|
-
name: String(
|
|
109
|
-
slug: String(
|
|
110
|
-
description: String(
|
|
111
|
-
version: String(
|
|
112
|
-
|
|
34
|
+
name: String(raw.name ?? ''),
|
|
35
|
+
slug: String(raw.slug ?? ''),
|
|
36
|
+
description: String(raw.description ?? ''),
|
|
37
|
+
version: String(raw.version ?? '1.0.0'),
|
|
38
|
+
changelog: raw.changelog ? String(raw.changelog) : undefined,
|
|
39
|
+
long_description: raw.long_description ? String(raw.long_description) : undefined,
|
|
113
40
|
tags,
|
|
114
41
|
portfolio,
|
|
42
|
+
requires,
|
|
43
|
+
welcome: raw.welcome ? String(raw.welcome) : undefined,
|
|
44
|
+
contact: raw.contact,
|
|
45
|
+
visibility,
|
|
46
|
+
invite_code: raw.invite_code ? String(raw.invite_code) : undefined,
|
|
115
47
|
};
|
|
116
48
|
}
|
|
117
49
|
function detectCommands(teamDir) {
|
|
@@ -251,13 +183,106 @@ function registerPublish(program) {
|
|
|
251
183
|
const json = program.opts().json ?? false;
|
|
252
184
|
const teamDir = process.cwd();
|
|
253
185
|
const relayYamlPath = path_1.default.join(teamDir, 'relay.yaml');
|
|
186
|
+
const isTTY = Boolean(process.stdin.isTTY) && !json;
|
|
254
187
|
// Check relay.yaml exists
|
|
255
188
|
if (!fs_1.default.existsSync(relayYamlPath)) {
|
|
256
|
-
|
|
257
|
-
error
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
189
|
+
if (!isTTY) {
|
|
190
|
+
console.error(JSON.stringify({
|
|
191
|
+
error: 'NOT_INITIALIZED',
|
|
192
|
+
message: 'relay.yaml이 없습니다. 먼저 `relay init`을 실행하세요.',
|
|
193
|
+
}));
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
// Interactive onboarding: create relay.yaml
|
|
197
|
+
const { input: promptInput, select: promptSelect, confirm: promptConfirm } = await import('@inquirer/prompts');
|
|
198
|
+
const dirName = path_1.default.basename(teamDir);
|
|
199
|
+
const defaultSlug = dirName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
200
|
+
console.error('\n\x1b[36m릴레이 팀 패키지를 초기화합니다.\x1b[0m');
|
|
201
|
+
console.error('relay.yaml을 생성하기 위해 몇 가지 정보를 입력해주세요.\n');
|
|
202
|
+
const name = await promptInput({
|
|
203
|
+
message: '팀 이름:',
|
|
204
|
+
default: dirName,
|
|
205
|
+
});
|
|
206
|
+
const slug = await promptInput({
|
|
207
|
+
message: '슬러그 (URL에 사용되는 고유 식별자):',
|
|
208
|
+
default: defaultSlug,
|
|
209
|
+
});
|
|
210
|
+
const description = await promptInput({
|
|
211
|
+
message: '팀 설명 (필수):',
|
|
212
|
+
validate: (v) => v.trim().length > 0 ? true : '설명을 입력해주세요.',
|
|
213
|
+
});
|
|
214
|
+
const tagsRaw = await promptInput({
|
|
215
|
+
message: '태그 (쉼표로 구분, 선택):',
|
|
216
|
+
default: '',
|
|
217
|
+
});
|
|
218
|
+
const visibility = await promptSelect({
|
|
219
|
+
message: '공개 범위:',
|
|
220
|
+
choices: [
|
|
221
|
+
{ name: '전체 공개', value: 'public' },
|
|
222
|
+
{ name: '로그인 사용자만', value: 'login-only' },
|
|
223
|
+
{ name: '초대 코드 필요', value: 'invite-only' },
|
|
224
|
+
],
|
|
225
|
+
});
|
|
226
|
+
let invite_code;
|
|
227
|
+
if (visibility === 'invite-only') {
|
|
228
|
+
invite_code = await promptInput({
|
|
229
|
+
message: '초대 코드:',
|
|
230
|
+
validate: (v) => v.trim().length > 0 ? true : '초대 코드를 입력해주세요.',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
// Welcome message section
|
|
234
|
+
console.error('\n\x1b[33m┌─────────────────────────────────────────────────────────────┐\x1b[0m');
|
|
235
|
+
console.error('\x1b[33m│ welcome 메시지란? │\x1b[0m');
|
|
236
|
+
console.error('\x1b[33m│ 설치할 때마다 이 메시지와 연락처가 설치자에게 전달됩니다. │\x1b[0m');
|
|
237
|
+
console.error('\x1b[33m│ (설치 = 명함 전달) │\x1b[0m');
|
|
238
|
+
console.error('\x1b[33m└─────────────────────────────────────────────────────────────┘\x1b[0m\n');
|
|
239
|
+
const wantsWelcome = await promptConfirm({
|
|
240
|
+
message: 'welcome 메시지를 작성하시겠습니까?',
|
|
241
|
+
default: false,
|
|
242
|
+
});
|
|
243
|
+
let welcome;
|
|
244
|
+
let contactEmail;
|
|
245
|
+
let contactKakao;
|
|
246
|
+
if (wantsWelcome) {
|
|
247
|
+
welcome = await promptInput({
|
|
248
|
+
message: 'welcome 메시지:',
|
|
249
|
+
validate: (v) => v.trim().length > 0 ? true : '메시지를 입력해주세요.',
|
|
250
|
+
});
|
|
251
|
+
contactEmail = await promptInput({
|
|
252
|
+
message: '이메일 (선택):',
|
|
253
|
+
default: '',
|
|
254
|
+
});
|
|
255
|
+
contactKakao = await promptInput({
|
|
256
|
+
message: '카카오 오픈채팅 링크 (선택):',
|
|
257
|
+
default: '',
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
const tags = tagsRaw
|
|
261
|
+
.split(',')
|
|
262
|
+
.map((t) => t.trim())
|
|
263
|
+
.filter(Boolean);
|
|
264
|
+
const yamlData = {
|
|
265
|
+
name,
|
|
266
|
+
slug,
|
|
267
|
+
description,
|
|
268
|
+
version: '1.0.0',
|
|
269
|
+
tags,
|
|
270
|
+
visibility,
|
|
271
|
+
};
|
|
272
|
+
if (invite_code)
|
|
273
|
+
yamlData.invite_code = invite_code;
|
|
274
|
+
if (welcome)
|
|
275
|
+
yamlData.welcome = welcome;
|
|
276
|
+
if (contactEmail || contactKakao) {
|
|
277
|
+
const contact = {};
|
|
278
|
+
if (contactEmail)
|
|
279
|
+
contact.email = contactEmail;
|
|
280
|
+
if (contactKakao)
|
|
281
|
+
contact.kakao = contactKakao;
|
|
282
|
+
yamlData.contact = contact;
|
|
283
|
+
}
|
|
284
|
+
fs_1.default.writeFileSync(relayYamlPath, js_yaml_1.default.dump(yamlData, { lineWidth: 120 }), 'utf-8');
|
|
285
|
+
console.error(`\n\x1b[32m✓ relay.yaml이 생성되었습니다.\x1b[0m\n`);
|
|
261
286
|
}
|
|
262
287
|
// Parse relay.yaml
|
|
263
288
|
const yamlContent = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
|
|
@@ -269,6 +294,10 @@ function registerPublish(program) {
|
|
|
269
294
|
}));
|
|
270
295
|
process.exit(1);
|
|
271
296
|
}
|
|
297
|
+
// Welcome hint when welcome field is missing and TTY is available
|
|
298
|
+
if (isTTY && !config.welcome) {
|
|
299
|
+
console.error('💡 welcome 메시지를 추가하면 설치할 때마다 명함이 전달됩니다. relay.yaml에 welcome 필드를 추가해보세요.');
|
|
300
|
+
}
|
|
272
301
|
// Validate structure
|
|
273
302
|
const hasDirs = VALID_DIRS.some((d) => {
|
|
274
303
|
const dirPath = path_1.default.join(teamDir, d);
|
|
@@ -283,7 +312,7 @@ function registerPublish(program) {
|
|
|
283
312
|
}));
|
|
284
313
|
process.exit(1);
|
|
285
314
|
}
|
|
286
|
-
// Get token
|
|
315
|
+
// Get token (checked before tarball creation)
|
|
287
316
|
const token = opts.token ?? process.env.RELAY_TOKEN ?? (0, config_js_1.loadToken)();
|
|
288
317
|
if (!token) {
|
|
289
318
|
console.error(JSON.stringify({
|
|
@@ -310,6 +339,12 @@ function registerPublish(program) {
|
|
|
310
339
|
commands: detectedCommands,
|
|
311
340
|
components,
|
|
312
341
|
version: config.version,
|
|
342
|
+
changelog: config.changelog,
|
|
343
|
+
requires: config.requires,
|
|
344
|
+
welcome: config.welcome,
|
|
345
|
+
contact: config.contact,
|
|
346
|
+
visibility: config.visibility,
|
|
347
|
+
invite_code: config.invite_code,
|
|
313
348
|
};
|
|
314
349
|
if (!json) {
|
|
315
350
|
console.error(`패키지 생성 중... (${config.name} v${config.version})`);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerUpdate = registerUpdate;
|
|
4
|
+
const api_js_1 = require("../lib/api.js");
|
|
5
|
+
const storage_js_1 = require("../lib/storage.js");
|
|
6
|
+
const installer_js_1 = require("../lib/installer.js");
|
|
7
|
+
const config_js_1 = require("../lib/config.js");
|
|
8
|
+
function registerUpdate(program) {
|
|
9
|
+
program
|
|
10
|
+
.command('update <slug>')
|
|
11
|
+
.description('설치된 에이전트 팀을 최신 버전으로 업데이트합니다')
|
|
12
|
+
.option('--path <install_path>', '설치 경로 지정 (기본: ./.claude)')
|
|
13
|
+
.option('--code <code>', '초대 코드 (invite-only 팀 업데이트 시 필요)')
|
|
14
|
+
.action(async (slug, opts) => {
|
|
15
|
+
const json = program.opts().json ?? false;
|
|
16
|
+
const installPath = (0, config_js_1.getInstallPath)(opts.path);
|
|
17
|
+
const tempDir = (0, storage_js_1.makeTempDir)();
|
|
18
|
+
try {
|
|
19
|
+
// Check installed.json for current version
|
|
20
|
+
const installed = (0, config_js_1.loadInstalled)();
|
|
21
|
+
const currentEntry = installed[slug];
|
|
22
|
+
const currentVersion = currentEntry?.version ?? null;
|
|
23
|
+
// Fetch latest team metadata
|
|
24
|
+
const team = await (0, api_js_1.fetchTeamInfo)(slug);
|
|
25
|
+
const latestVersion = team.version;
|
|
26
|
+
if (currentVersion && currentVersion === latestVersion) {
|
|
27
|
+
if (json) {
|
|
28
|
+
console.log(JSON.stringify({ status: 'up-to-date', slug, version: latestVersion }));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.log(`이미 최신 버전입니다 (${slug} v${latestVersion})`);
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Visibility check
|
|
36
|
+
const visibility = team.visibility ?? 'public';
|
|
37
|
+
if (visibility === 'login-only') {
|
|
38
|
+
const token = (0, config_js_1.loadToken)();
|
|
39
|
+
if (!token) {
|
|
40
|
+
console.error('이 팀은 로그인이 필요합니다. `relay login`을 먼저 실행하세요.');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (visibility === 'invite-only') {
|
|
45
|
+
if (!opts.code) {
|
|
46
|
+
console.error('초대 코드가 필요합니다. `relay update ' + slug + ' --code <code>`로 업데이트하세요.');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Download package
|
|
51
|
+
const tarPath = await (0, storage_js_1.downloadPackage)(team.package_url, tempDir);
|
|
52
|
+
// Extract
|
|
53
|
+
const extractDir = `${tempDir}/extracted`;
|
|
54
|
+
await (0, storage_js_1.extractPackage)(tarPath, extractDir);
|
|
55
|
+
// Copy files to install_path
|
|
56
|
+
const files = (0, installer_js_1.installTeam)(extractDir, installPath);
|
|
57
|
+
// Update installed.json with new version
|
|
58
|
+
installed[slug] = {
|
|
59
|
+
version: latestVersion,
|
|
60
|
+
installed_at: new Date().toISOString(),
|
|
61
|
+
files,
|
|
62
|
+
};
|
|
63
|
+
(0, config_js_1.saveInstalled)(installed);
|
|
64
|
+
// Report install (non-blocking)
|
|
65
|
+
await (0, api_js_1.reportInstall)(slug, opts.code);
|
|
66
|
+
const result = {
|
|
67
|
+
status: 'updated',
|
|
68
|
+
slug,
|
|
69
|
+
from_version: currentVersion,
|
|
70
|
+
version: latestVersion,
|
|
71
|
+
files_installed: files.length,
|
|
72
|
+
install_path: installPath,
|
|
73
|
+
};
|
|
74
|
+
if (json) {
|
|
75
|
+
console.log(JSON.stringify(result));
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const fromLabel = currentVersion ? `v${currentVersion} → ` : '';
|
|
79
|
+
console.log(`\n\x1b[32m✓ ${team.name} ${fromLabel}v${latestVersion} 업데이트 완료\x1b[0m`);
|
|
80
|
+
console.log(` 설치 위치: \x1b[36m${installPath}\x1b[0m`);
|
|
81
|
+
console.log(` 파일 수: ${files.length}개`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
86
|
+
console.error(JSON.stringify({ error: 'UPDATE_FAILED', message }));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
(0, storage_js_1.removeTempDir)(tempDir);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,8 @@ const list_js_1 = require("./commands/list.js");
|
|
|
9
9
|
const uninstall_js_1 = require("./commands/uninstall.js");
|
|
10
10
|
const publish_js_1 = require("./commands/publish.js");
|
|
11
11
|
const login_js_1 = require("./commands/login.js");
|
|
12
|
+
const update_js_1 = require("./commands/update.js");
|
|
13
|
+
const outdated_js_1 = require("./commands/outdated.js");
|
|
12
14
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
13
15
|
const pkg = require('../package.json');
|
|
14
16
|
const program = new commander_1.Command();
|
|
@@ -24,4 +26,6 @@ program
|
|
|
24
26
|
(0, uninstall_js_1.registerUninstall)(program);
|
|
25
27
|
(0, publish_js_1.registerPublish)(program);
|
|
26
28
|
(0, login_js_1.registerLogin)(program);
|
|
29
|
+
(0, update_js_1.registerUpdate)(program);
|
|
30
|
+
(0, outdated_js_1.registerOutdated)(program);
|
|
27
31
|
program.parse();
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { TeamRegistryInfo, SearchResult } from '../types.js';
|
|
2
2
|
export declare function fetchTeamInfo(slug: string): Promise<TeamRegistryInfo>;
|
|
3
3
|
export declare function searchTeams(query: string, tag?: string): Promise<SearchResult[]>;
|
|
4
|
-
export
|
|
4
|
+
export interface TeamVersionInfo {
|
|
5
|
+
version: string;
|
|
6
|
+
changelog: string | null;
|
|
7
|
+
created_at: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function fetchTeamVersions(slug: string): Promise<TeamVersionInfo[]>;
|
|
10
|
+
export declare function reportInstall(slug: string, code?: string): Promise<void>;
|
package/dist/lib/api.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.fetchTeamInfo = fetchTeamInfo;
|
|
4
4
|
exports.searchTeams = searchTeams;
|
|
5
|
+
exports.fetchTeamVersions = fetchTeamVersions;
|
|
5
6
|
exports.reportInstall = reportInstall;
|
|
6
7
|
const config_js_1 = require("./config.js");
|
|
7
8
|
async function fetchTeamInfo(slug) {
|
|
@@ -26,8 +27,18 @@ async function searchTeams(query, tag) {
|
|
|
26
27
|
const data = (await res.json());
|
|
27
28
|
return data.results;
|
|
28
29
|
}
|
|
29
|
-
async function
|
|
30
|
-
const url = `${config_js_1.API_URL}/api/registry/${slug}/
|
|
30
|
+
async function fetchTeamVersions(slug) {
|
|
31
|
+
const url = `${config_js_1.API_URL}/api/registry/${slug}/versions`;
|
|
32
|
+
const res = await fetch(url);
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
const body = await res.text();
|
|
35
|
+
throw new Error(`버전 목록 조회 실패 (${res.status}): ${body}`);
|
|
36
|
+
}
|
|
37
|
+
return res.json();
|
|
38
|
+
}
|
|
39
|
+
async function reportInstall(slug, code) {
|
|
40
|
+
const base = `${config_js_1.API_URL}/api/registry/${slug}/install`;
|
|
41
|
+
const url = code ? `${base}?code=${encodeURIComponent(code)}` : base;
|
|
31
42
|
await fetch(url, { method: 'POST' }).catch(() => {
|
|
32
43
|
// non-critical: ignore errors
|
|
33
44
|
});
|
|
@@ -52,25 +52,47 @@ exports.RELAY_COMMANDS = [
|
|
|
52
52
|
{
|
|
53
53
|
id: 'relay-install',
|
|
54
54
|
description: 'relay 마켓플레이스에서 에이전트 팀을 설치합니다',
|
|
55
|
-
body: `요청된 에이전트 팀을 relay 마켓플레이스에서 다운로드하여 현재 프로젝트에
|
|
55
|
+
body: `요청된 에이전트 팀을 relay 마켓플레이스에서 다운로드하여 현재 프로젝트에 설치하고, 의존성을 확인·설치합니다.
|
|
56
56
|
|
|
57
57
|
## 실행 방법
|
|
58
58
|
|
|
59
|
-
1.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
### 1. 팀 설치
|
|
60
|
+
- \`relay install <slug>\` 명령어를 실행합니다.
|
|
61
|
+
- 설치 결과를 확인합니다 (설치된 파일 수, 사용 가능한 커맨드 목록).
|
|
62
|
+
|
|
63
|
+
### 2. 의존성 확인 및 설치
|
|
64
|
+
설치된 팀의 relay.yaml에 \`requires\` 섹션이 있으면 각 항목을 확인하고 처리합니다:
|
|
65
|
+
|
|
66
|
+
- **cli**: \`which <name>\`으로 확인 → 없으면 install 명령 실행 또는 안내
|
|
67
|
+
- **npm**: \`npm list <package>\`로 확인 → 없으면 \`npm install\`
|
|
68
|
+
- **env**: 환경변수 확인 → optional이면 경고, 필수면 설정 안내
|
|
69
|
+
- **mcp**: MCP 서버 설정 안내
|
|
70
|
+
- **teams**: \`relay install <team>\`으로 재귀 설치
|
|
71
|
+
|
|
72
|
+
### 3. 제작자 소개 (welcome 메시지)
|
|
73
|
+
- API 응답의 \`welcome\` 필드가 있으면 제작자 메시지를 보여줍니다.
|
|
74
|
+
- \`contact\` 필드가 있으면 연락처도 함께 표시합니다.
|
|
75
|
+
|
|
76
|
+
### 4. 팔로우 제안
|
|
77
|
+
- "이 팀의 제작자 @username을 팔로우할까요?"라고 제안합니다.
|
|
78
|
+
- 인증되어 있으면 POST /api/follows로 팔로우합니다.
|
|
79
|
+
- 미인증이면 \`relay login\` 후 팔로우할 수 있다고 안내합니다.
|
|
80
|
+
|
|
81
|
+
### 5. 완료 안내
|
|
82
|
+
- 사용 가능한 커맨드 안내
|
|
83
|
+
- "바로 사용해볼까요?" 제안
|
|
66
84
|
|
|
67
85
|
## 예시
|
|
68
86
|
|
|
69
87
|
사용자: /relay-install contents-team
|
|
70
88
|
→ relay install contents-team 실행
|
|
71
|
-
→
|
|
72
|
-
→
|
|
73
|
-
→
|
|
89
|
+
→ requires 확인: ✓ playwright, ✓ sharp 설치됨
|
|
90
|
+
→ ✓ 설치 완료!
|
|
91
|
+
→ ┌─ @haemin ──────────────────────────────┐
|
|
92
|
+
→ │ contents-team을 설치해주셔서 감사합니다! │
|
|
93
|
+
→ │ 💬 카카오톡 📧 이메일 │
|
|
94
|
+
→ └─────────────────────────────────────────┘
|
|
95
|
+
→ "@haemin을 팔로우할까요?" → Yes → ✓ 팔로우 완료
|
|
74
96
|
→ "바로 /cardnews를 사용해볼까요?"`,
|
|
75
97
|
},
|
|
76
98
|
{
|
|
@@ -80,12 +102,17 @@ exports.RELAY_COMMANDS = [
|
|
|
80
102
|
|
|
81
103
|
## 실행 단계
|
|
82
104
|
|
|
83
|
-
### 1.
|
|
105
|
+
### 1. 인증 확인 (가장 먼저)
|
|
106
|
+
- \`cat ~/.relay/token\` 또는 환경변수 RELAY_TOKEN으로 토큰 존재 여부를 확인합니다.
|
|
107
|
+
- 미인증이면 즉시 안내: "먼저 \`relay login\`으로 로그인이 필요합니다." → 로그인 후 재실행 안내.
|
|
108
|
+
- 인증되어 있으면 다음 단계로 진행합니다.
|
|
109
|
+
|
|
110
|
+
### 2. 팀 구조 분석
|
|
84
111
|
- skills/, agents/, rules/, commands/ 디렉토리를 탐색합니다.
|
|
85
112
|
- 각 파일의 이름과 description을 추출합니다.
|
|
86
113
|
- relay.yaml이 있으면 읽고, 없으면 사용자에게 팀 정보(name, slug, description, tags)를 물어보고 생성합니다.
|
|
87
114
|
|
|
88
|
-
###
|
|
115
|
+
### 3. 포트폴리오 생성
|
|
89
116
|
|
|
90
117
|
#### Layer 1: 팀 구성 시각화 (자동)
|
|
91
118
|
- 분석된 팀 구조를 HTML로 생성합니다. 내용:
|
|
@@ -105,18 +132,15 @@ exports.RELAY_COMMANDS = [
|
|
|
105
132
|
- 사용자가 포트폴리오에 포함할 항목을 선택합니다.
|
|
106
133
|
- 선택된 이미지를 ./portfolio/에 저장합니다.
|
|
107
134
|
|
|
108
|
-
###
|
|
135
|
+
### 4. 메타데이터 생성
|
|
109
136
|
- description: skills 내용 기반으로 자동 생성합니다.
|
|
110
137
|
- long_description: 팀 소개 마크다운을 자동 생성합니다 (README.md가 있으면 활용).
|
|
111
138
|
- tags: 팀 특성에 맞는 태그를 추천합니다.
|
|
112
139
|
- 사용자에게 확인: "이대로 배포할까요?"
|
|
113
140
|
|
|
114
|
-
###
|
|
141
|
+
### 5. relay.yaml 업데이트
|
|
115
142
|
- 생성/수정된 메타데이터와 포트폴리오 경로를 relay.yaml에 반영합니다.
|
|
116
143
|
|
|
117
|
-
### 5. 인증 확인
|
|
118
|
-
- \`relay login\`으로 인증 상태를 확인합니다. 미인증이면 로그인을 안내합니다.
|
|
119
|
-
|
|
120
144
|
### 6. 배포
|
|
121
145
|
- \`relay publish\` 명령어를 실행합니다.
|
|
122
146
|
- 배포 결과와 마켓플레이스 URL을 보여줍니다.
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "relayax-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "RelayAX Agent Team Marketplace CLI - Install and manage agent teams",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -35,9 +35,11 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@inquirer/prompts": "^8.3.2",
|
|
37
37
|
"commander": "^13.1.0",
|
|
38
|
+
"js-yaml": "^4.1.1",
|
|
38
39
|
"tar": "^7.4.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
42
|
+
"@types/js-yaml": "^4.0.9",
|
|
41
43
|
"@types/node": "^20",
|
|
42
44
|
"typescript": "^5"
|
|
43
45
|
}
|