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.
@@ -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. Download package
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
- // 3. Extract
40
+ // 4. Extract
23
41
  const extractDir = `${tempDir}/extracted`;
24
42
  await (0, storage_js_1.extractPackage)(tarPath, extractDir);
25
- // 4. Copy files to install_path
43
+ // 5. Copy files to install_path
26
44
  const files = (0, installer_js_1.installTeam)(extractDir, installPath);
27
- // 5. Record in installed.json
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
- // 6. Report install (non-blocking)
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,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerOutdated(program: Command): void;
@@ -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;
@@ -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 result = {};
16
- const tags = [];
17
- const portfolio = [];
18
- let inTags = false;
19
- let inPortfolio = false;
20
- let inLongDesc = false;
21
- let longDescLines = [];
22
- let currentPortfolioItem = null;
23
- for (const line of content.split('\n')) {
24
- const trimmed = line.trim();
25
- // long_description multiline (YAML | block)
26
- if (inLongDesc) {
27
- if (line.startsWith(' ') || line.startsWith('\t') || trimmed === '') {
28
- longDescLines.push(line.replace(/^ {2}/, '').replace(/^\t/, ''));
29
- continue;
30
- }
31
- else {
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(result.name ?? ''),
109
- slug: String(result.slug ?? ''),
110
- description: String(result.description ?? ''),
111
- version: String(result.version ?? '1.0.0'),
112
- long_description: result.long_description,
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
- console.error(JSON.stringify({
257
- error: 'NOT_INITIALIZED',
258
- message: 'relay.yaml이 없습니다. 먼저 `relay init`을 실행하세요.',
259
- }));
260
- process.exit(1);
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,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerUpdate(program: Command): void;
@@ -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 declare function reportInstall(slug: string): Promise<void>;
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 reportInstall(slug) {
30
- const url = `${config_js_1.API_URL}/api/registry/${slug}/install`;
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. \`relay install <slug>\` 명령어를 실행합니다.
60
- 2. 설치 결과를 확인합니다:
61
- - 설치된 파일
62
- - 사용 가능해진 커맨드 목록
63
- 3. 커맨드의 사용법을 간단히 안내합니다.
64
- 4. "바로 사용해볼까요?"라고 제안합니다.
65
- 5. 사용자가 원하면 첫 번째 커맨드를 실행해봅니다.
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
- /cardnews - 카드뉴스 제작
73
- /detailpage - 상세페이지 제작
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
- ### 2. 포트폴리오 생성
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
- ### 3. 메타데이터 생성
135
+ ### 4. 메타데이터 생성
109
136
  - description: skills 내용 기반으로 자동 생성합니다.
110
137
  - long_description: 팀 소개 마크다운을 자동 생성합니다 (README.md가 있으면 활용).
111
138
  - tags: 팀 특성에 맞는 태그를 추천합니다.
112
139
  - 사용자에게 확인: "이대로 배포할까요?"
113
140
 
114
- ### 4. relay.yaml 업데이트
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
@@ -20,6 +20,7 @@ export interface TeamRegistryInfo {
20
20
  rules: number;
21
21
  skills: number;
22
22
  };
23
+ visibility?: "public" | "login-only" | "invite-only";
23
24
  }
24
25
  export interface SearchResult {
25
26
  slug: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relayax-cli",
3
- "version": "0.1.7",
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
  }