relayax-cli 0.1.8 → 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
+ }
@@ -19,4 +19,11 @@ export interface Requires {
19
19
  env?: RequiresEnv[];
20
20
  teams?: string[];
21
21
  }
22
+ export interface ContactInfo {
23
+ email?: string;
24
+ kakao?: string;
25
+ x?: string;
26
+ linkedin?: string;
27
+ website?: string;
28
+ }
22
29
  export declare function registerPublish(program: Command): void;
@@ -25,15 +25,25 @@ function parseRelayYaml(content) {
25
25
  })).filter((p) => p.path)
26
26
  : [];
27
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;
28
33
  return {
29
34
  name: String(raw.name ?? ''),
30
35
  slug: String(raw.slug ?? ''),
31
36
  description: String(raw.description ?? ''),
32
37
  version: String(raw.version ?? '1.0.0'),
38
+ changelog: raw.changelog ? String(raw.changelog) : undefined,
33
39
  long_description: raw.long_description ? String(raw.long_description) : undefined,
34
40
  tags,
35
41
  portfolio,
36
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,
37
47
  };
38
48
  }
39
49
  function detectCommands(teamDir) {
@@ -173,13 +183,106 @@ function registerPublish(program) {
173
183
  const json = program.opts().json ?? false;
174
184
  const teamDir = process.cwd();
175
185
  const relayYamlPath = path_1.default.join(teamDir, 'relay.yaml');
186
+ const isTTY = Boolean(process.stdin.isTTY) && !json;
176
187
  // Check relay.yaml exists
177
188
  if (!fs_1.default.existsSync(relayYamlPath)) {
178
- console.error(JSON.stringify({
179
- error: 'NOT_INITIALIZED',
180
- message: 'relay.yaml이 없습니다. 먼저 `relay init`을 실행하세요.',
181
- }));
182
- 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`);
183
286
  }
184
287
  // Parse relay.yaml
185
288
  const yamlContent = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
@@ -191,6 +294,10 @@ function registerPublish(program) {
191
294
  }));
192
295
  process.exit(1);
193
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
+ }
194
301
  // Validate structure
195
302
  const hasDirs = VALID_DIRS.some((d) => {
196
303
  const dirPath = path_1.default.join(teamDir, d);
@@ -205,7 +312,7 @@ function registerPublish(program) {
205
312
  }));
206
313
  process.exit(1);
207
314
  }
208
- // Get token
315
+ // Get token (checked before tarball creation)
209
316
  const token = opts.token ?? process.env.RELAY_TOKEN ?? (0, config_js_1.loadToken)();
210
317
  if (!token) {
211
318
  console.error(JSON.stringify({
@@ -232,7 +339,12 @@ function registerPublish(program) {
232
339
  commands: detectedCommands,
233
340
  components,
234
341
  version: config.version,
342
+ changelog: config.changelog,
235
343
  requires: config.requires,
344
+ welcome: config.welcome,
345
+ contact: config.contact,
346
+ visibility: config.visibility,
347
+ invite_code: config.invite_code,
236
348
  };
237
349
  if (!json) {
238
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
  });
@@ -69,8 +69,16 @@ exports.RELAY_COMMANDS = [
69
69
  - **mcp**: MCP 서버 설정 안내
70
70
  - **teams**: \`relay install <team>\`으로 재귀 설치
71
71
 
72
- ### 3. 완료 안내
73
- - 의존성 결과 요약 ("✓ 확인됨", "⚠ 미설정")
72
+ ### 3. 제작자 소개 (welcome 메시지)
73
+ - API 응답의 \`welcome\` 필드가 있으면 제작자 메시지를 보여줍니다.
74
+ - \`contact\` 필드가 있으면 연락처도 함께 표시합니다.
75
+
76
+ ### 4. 팔로우 제안
77
+ - "이 팀의 제작자 @username을 팔로우할까요?"라고 제안합니다.
78
+ - 인증되어 있으면 POST /api/follows로 팔로우합니다.
79
+ - 미인증이면 \`relay login\` 후 팔로우할 수 있다고 안내합니다.
80
+
81
+ ### 5. 완료 안내
74
82
  - 사용 가능한 커맨드 안내
75
83
  - "바로 사용해볼까요?" 제안
76
84
 
@@ -79,9 +87,12 @@ exports.RELAY_COMMANDS = [
79
87
  사용자: /relay-install contents-team
80
88
  → relay install contents-team 실행
81
89
  → requires 확인: ✓ playwright, ✓ sharp 설치됨
82
- "설치 완료! 다음 커맨드를 사용할 수 있습니다:"
83
- /cardnews - 카드뉴스 제작
84
- /detailpage - 상세페이지 제작
90
+ 설치 완료!
91
+ ┌─ @haemin ──────────────────────────────┐
92
+ contents-team을 설치해주셔서 감사합니다! │
93
+ → │ 💬 카카오톡 📧 이메일 │
94
+ → └─────────────────────────────────────────┘
95
+ → "@haemin을 팔로우할까요?" → Yes → ✓ 팔로우 완료
85
96
  → "바로 /cardnews를 사용해볼까요?"`,
86
97
  },
87
98
  {
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.8",
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": {