relayax-cli 0.2.20 → 0.2.23

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.
@@ -64,9 +64,8 @@ function registerCreate(program) {
64
64
  visibility = await promptSelect({
65
65
  message: '공개 범위:',
66
66
  choices: [
67
- { name: '전체 공개', value: 'public' },
68
- { name: '로그인 사용자만', value: 'login-only' },
69
- { name: '초대 코드 필요', value: 'invite-only' },
67
+ { name: '공개', value: 'public' },
68
+ { name: '비공개 (Space 멤버만)', value: 'private' },
70
69
  ],
71
70
  });
72
71
  }
@@ -1,2 +1,6 @@
1
1
  import { Command } from 'commander';
2
+ /**
3
+ * 글로벌 User 커맨드가 이미 설치되어 있는지 확인한다.
4
+ */
5
+ export declare function hasGlobalUserCommands(): boolean;
2
6
  export declare function registerInit(program: Command): void;
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.hasGlobalUserCommands = hasGlobalUserCommands;
6
7
  exports.registerInit = registerInit;
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const path_1 = __importDefault(require("path"));
@@ -115,8 +116,11 @@ function registerInit(program) {
115
116
  .description('에이전트 CLI에 relay 슬래시 커맨드를 설치합니다')
116
117
  .option('--tools <tools>', '설치할 에이전트 CLI 지정 (all 또는 쉼표 구분)')
117
118
  .option('--update', '이미 설치된 슬래시 커맨드를 최신 버전으로 업데이트')
119
+ .option('--auto', '대화형 프롬프트 없이 자동으로 모든 감지된 CLI에 설치')
118
120
  .action(async (opts) => {
119
121
  const json = program.opts().json ?? false;
122
+ // auto mode: --auto flag, --json flag, or stdin is not a TTY
123
+ const autoMode = opts.auto === true || json || !process.stdin.isTTY;
120
124
  const projectPath = process.cwd();
121
125
  const detected = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
122
126
  const detectedIds = new Set(detected.map((t) => t.value));
@@ -154,7 +158,8 @@ function registerInit(program) {
154
158
  else if (opts.tools) {
155
159
  targetToolIds = resolveTools(opts.tools);
156
160
  }
157
- else if (!json && process.stdin.isTTY) {
161
+ else if (!autoMode) {
162
+ // interactive mode: only when stdin is a TTY and not --auto/--json
158
163
  showWelcome();
159
164
  if (detected.length > 0) {
160
165
  console.log(` 감지된 에이전트 CLI: \x1b[36m${detected.map((t) => t.name).join(', ')}\x1b[0m\n`);
@@ -171,18 +176,12 @@ function registerInit(program) {
171
176
  }
172
177
  }
173
178
  else {
174
- if (detected.length === 0) {
175
- if (json) {
176
- console.error(JSON.stringify({
177
- error: 'NO_AGENT_CLI',
178
- message: '에이전트 CLI 디렉토리를 찾을 수 없습니다. --tools 옵션으로 지정하세요.',
179
- }));
180
- }
181
- // 글로벌은 설치했으므로 에러로 종료하지 않음
182
- targetToolIds = [];
179
+ // auto mode: use detected CLIs, or all available tools if none detected
180
+ if (detected.length > 0) {
181
+ targetToolIds = detected.map((t) => t.value);
183
182
  }
184
183
  else {
185
- targetToolIds = detected.map((t) => t.value);
184
+ targetToolIds = ai_tools_js_1.AI_TOOLS.map((t) => t.value);
186
185
  }
187
186
  }
188
187
  // Builder 커맨드 설치 (기존 파일 중 현재 목록에 없는 것 제거)
@@ -213,7 +212,7 @@ function registerInit(program) {
213
212
  localResults.push({ tool: tool.name, commands: installedCommands });
214
213
  }
215
214
  }
216
- else if (!json && process.stdin.isTTY) {
215
+ else if (!autoMode) {
217
216
  // User 모드: 글로벌만 설치, 안내 표시
218
217
  showWelcome();
219
218
  }
@@ -12,35 +12,138 @@ 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
14
  const preamble_js_1 = require("../lib/preamble.js");
15
+ const join_js_1 = require("./join.js");
16
+ const init_js_1 = require("./init.js");
17
+ /**
18
+ * slugInput이 "@spaces/{spaceSlug}/{teamSlug}" 형식이면 파싱해 반환.
19
+ * 아니면 null.
20
+ */
21
+ function parseSpaceTarget(slugInput) {
22
+ const m = slugInput.match(/^@spaces\/([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)$/);
23
+ if (!m)
24
+ return null;
25
+ return {
26
+ spaceSlug: m[1],
27
+ rawTeamSlug: m[2],
28
+ teamSlug: `@${m[1]}/${m[2]}`,
29
+ };
30
+ }
15
31
  function registerInstall(program) {
16
32
  program
17
33
  .command('install <slug>')
18
34
  .description('에이전트 팀 패키지를 .relay/teams/에 다운로드합니다')
19
35
  .option('--no-guide', 'GUIDE.html 브라우저 자동 오픈을 비활성화합니다')
36
+ .option('--join-code <code>', 'Space 초대 코드 (Space 팀 설치 시 자동 가입)')
20
37
  .action(async (slugInput, opts) => {
21
38
  const json = program.opts().json ?? false;
22
39
  const projectPath = process.cwd();
23
40
  const tempDir = (0, storage_js_1.makeTempDir)();
41
+ if (!(0, init_js_1.hasGlobalUserCommands)()) {
42
+ if (!json) {
43
+ console.error('\x1b[33m⚠ relay init이 실행되지 않았습니다. 먼저 relay init을 실행하세요.\x1b[0m');
44
+ }
45
+ else {
46
+ console.error(JSON.stringify({ error: 'NOT_INITIALIZED', message: 'relay init을 먼저 실행하세요.' }));
47
+ }
48
+ process.exit(1);
49
+ }
24
50
  try {
25
- // 0. Resolve scoped slug
26
- const parsed = await (0, slug_js_1.resolveSlug)(slugInput);
27
- const slug = parsed.full;
28
- // 1. Fetch team metadata
29
- const team = await (0, api_js_1.fetchTeamInfo)(slug);
51
+ // 0. @spaces/{spaceSlug}/{teamSlug} 형식 감지 및 파싱
52
+ const spaceTarget = parseSpaceTarget(slugInput);
53
+ // 0a. --join-code가 있으면 먼저 Space 가입 시도
54
+ if (opts.joinCode && spaceTarget) {
55
+ try {
56
+ const { spaceName } = await (0, join_js_1.joinSpace)(spaceTarget.spaceSlug, opts.joinCode);
57
+ if (!json) {
58
+ console.log(`\x1b[32m✅ ${spaceName} Space에 가입했습니다\x1b[0m`);
59
+ }
60
+ }
61
+ catch (joinErr) {
62
+ const joinMsg = joinErr instanceof Error ? joinErr.message : String(joinErr);
63
+ // 이미 멤버인 경우 설치 계속 진행
64
+ if (joinMsg !== 'ALREADY_MEMBER') {
65
+ if (!json) {
66
+ console.error(`\x1b[33m경고: Space 가입 실패 — ${joinMsg}\x1b[0m`);
67
+ }
68
+ }
69
+ }
70
+ }
71
+ // 0b. Resolve scoped slug and fetch team metadata
72
+ let team;
73
+ let slug;
74
+ let parsed;
75
+ if (spaceTarget) {
76
+ // Space 팀: POST /api/spaces/{spaceSlug}/teams/{teamSlug}/install
77
+ // This verifies membership, increments install count, and returns metadata.
78
+ try {
79
+ team = await (0, api_js_1.installSpaceTeam)(spaceTarget.spaceSlug, spaceTarget.rawTeamSlug);
80
+ }
81
+ catch (fetchErr) {
82
+ const fetchMsg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
83
+ if (fetchMsg.includes('403')) {
84
+ if (json) {
85
+ console.error(JSON.stringify({
86
+ error: 'SPACE_ONLY',
87
+ message: '이 팀은 Space 멤버만 설치 가능합니다.',
88
+ spaceSlug: spaceTarget.spaceSlug,
89
+ }));
90
+ }
91
+ else {
92
+ console.error('\x1b[31m이 팀은 Space 멤버만 설치 가능합니다.\x1b[0m');
93
+ console.error(`\x1b[33mSpace 관리자에게 초대 코드를 요청하세요: relay join ${spaceTarget.spaceSlug} --code <코드>\x1b[0m`);
94
+ }
95
+ process.exit(1);
96
+ }
97
+ throw fetchErr;
98
+ }
99
+ // slug from server is "@spaces/{spaceSlug}/{teamSlug}" — derive local path parts
100
+ parsed = { owner: spaceTarget.spaceSlug, name: spaceTarget.rawTeamSlug, full: team.slug };
101
+ slug = team.slug;
102
+ }
103
+ else {
104
+ // Normal registry install
105
+ const resolveInput = slugInput;
106
+ parsed = await (0, slug_js_1.resolveSlug)(resolveInput);
107
+ slug = parsed.full;
108
+ try {
109
+ team = await (0, api_js_1.fetchTeamInfo)(slug);
110
+ }
111
+ catch (fetchErr) {
112
+ const fetchMsg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
113
+ if (fetchMsg.includes('403')) {
114
+ if (json) {
115
+ console.error(JSON.stringify({
116
+ error: 'SPACE_ONLY',
117
+ message: '이 팀은 Space 멤버만 설치 가능합니다.',
118
+ slug,
119
+ }));
120
+ }
121
+ else {
122
+ console.error('\x1b[31m이 팀은 Space 멤버만 설치 가능합니다.\x1b[0m');
123
+ console.error('\x1b[33mSpace 관리자에게 초대 코드를 요청하세요.\x1b[0m');
124
+ }
125
+ process.exit(1);
126
+ }
127
+ throw fetchErr;
128
+ }
129
+ }
30
130
  const teamDir = path_1.default.join(projectPath, '.relay', 'teams', parsed.owner, parsed.name);
31
131
  // 2. Visibility check
32
132
  const visibility = team.visibility ?? 'public';
33
- if (visibility === 'login-only' || visibility === 'invite-only') {
133
+ if (visibility === 'private') {
34
134
  const token = await (0, config_js_1.getValidToken)();
35
135
  if (!token) {
36
- console.error(JSON.stringify({
37
- error: 'LOGIN_REQUIRED',
38
- visibility,
39
- slug,
40
- message: visibility === 'invite-only'
41
- ? '이 팀은 초대받은 사용자만 설치할 수 있습니다. 로그인이 필요합니다.'
42
- : '이 팀은 로그인이 필요합니다.',
43
- }));
136
+ if (json) {
137
+ console.error(JSON.stringify({
138
+ error: 'LOGIN_REQUIRED',
139
+ visibility,
140
+ slug,
141
+ message: '이 팀은 Space 멤버만 설치할 수 있습니다. 로그인이 필요합니다.',
142
+ }));
143
+ }
144
+ else {
145
+ console.error('\x1b[31m이 팀은 Space 멤버만 설치할 수 있습니다. relay login 을 먼저 실행하세요.\x1b[0m');
146
+ }
44
147
  process.exit(1);
45
148
  }
46
149
  }
@@ -70,12 +173,13 @@ function registerInstall(program) {
70
173
  return count;
71
174
  }
72
175
  const fileCount = countFiles(teamDir);
73
- // 6. Record in installed.json
176
+ // 6. Record in installed.json (space_slug 포함)
74
177
  const installed = (0, config_js_1.loadInstalled)();
75
178
  installed[slug] = {
76
179
  version: team.version,
77
180
  installed_at: new Date().toISOString(),
78
181
  files: [teamDir],
182
+ ...(spaceTarget ? { space_slug: spaceTarget.spaceSlug } : {}),
79
183
  };
80
184
  (0, config_js_1.saveInstalled)(installed);
81
185
  // 7. Report install (non-blocking)
@@ -88,6 +192,7 @@ function registerInstall(program) {
88
192
  commands: team.commands,
89
193
  files: fileCount,
90
194
  install_path: teamDir,
195
+ ...(spaceTarget ? { space_slug: spaceTarget.spaceSlug } : {}),
91
196
  };
92
197
  if (json) {
93
198
  console.log(JSON.stringify(result));
@@ -139,6 +244,14 @@ function registerInstall(program) {
139
244
  console.log(`\n 📖 사용가이드: ${guideHtmlPath}`);
140
245
  }
141
246
  }
247
+ // Usage hint
248
+ if (team.commands && team.commands.length > 0) {
249
+ console.log(`\n\x1b[33m💡 시작하려면 채팅에 아래 명령어를 입력하세요:\x1b[0m`);
250
+ console.log(` \x1b[1m/${team.commands[0].name}\x1b[0m`);
251
+ }
252
+ else {
253
+ console.log(`\n\x1b[33m💡 설치 완료! AI 에이전트에서 사용할 수 있습니다.\x1b[0m`);
254
+ }
142
255
  console.log('\n 에이전트가 /relay-install로 환경을 구성합니다.');
143
256
  }
144
257
  }
@@ -0,0 +1,5 @@
1
+ import { Command } from 'commander';
2
+ export declare function joinSpace(spaceSlug: string, code: string): Promise<{
3
+ spaceName: string;
4
+ }>;
5
+ export declare function registerJoin(program: Command): void;
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.joinSpace = joinSpace;
4
+ exports.registerJoin = registerJoin;
5
+ const config_js_1 = require("../lib/config.js");
6
+ const init_js_1 = require("./init.js");
7
+ async function fetchSpaceTeams(spaceSlug, token) {
8
+ const res = await fetch(`${config_js_1.API_URL}/api/spaces/${spaceSlug}/teams`, {
9
+ headers: { Authorization: `Bearer ${token}` },
10
+ signal: AbortSignal.timeout(5000),
11
+ });
12
+ if (!res.ok)
13
+ throw new Error(`${res.status}`);
14
+ const data = (await res.json());
15
+ if (Array.isArray(data))
16
+ return data;
17
+ return data.teams ?? [];
18
+ }
19
+ async function joinSpace(spaceSlug, code) {
20
+ const token = await (0, config_js_1.getValidToken)();
21
+ if (!token) {
22
+ throw new Error('LOGIN_REQUIRED');
23
+ }
24
+ const res = await fetch(`${config_js_1.API_URL}/api/spaces/${spaceSlug}/join`, {
25
+ method: 'POST',
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ Authorization: `Bearer ${token}`,
29
+ },
30
+ body: JSON.stringify({ code }),
31
+ });
32
+ const body = (await res.json().catch(() => ({})));
33
+ if (!res.ok) {
34
+ const errCode = body.error ?? String(res.status);
35
+ switch (errCode) {
36
+ case 'INVALID_CODE':
37
+ throw new Error('초대 코드가 올바르지 않습니다.');
38
+ case 'EXPIRED_CODE':
39
+ throw new Error('초대 코드가 만료되었습니다.');
40
+ case 'ALREADY_MEMBER':
41
+ throw new Error('ALREADY_MEMBER');
42
+ default:
43
+ throw new Error(body.message ?? `Space 가입 실패 (${res.status})`);
44
+ }
45
+ }
46
+ return { spaceName: body.space_name ?? spaceSlug };
47
+ }
48
+ function registerJoin(program) {
49
+ program
50
+ .command('join <slug>')
51
+ .description('Space에 초대 코드로 가입합니다')
52
+ .requiredOption('--code <code>', '초대 코드 (UUID)')
53
+ .action(async (slug, opts) => {
54
+ const json = program.opts().json ?? false;
55
+ if (!(0, init_js_1.hasGlobalUserCommands)()) {
56
+ if (!json) {
57
+ console.error('\x1b[33m⚠ relay init이 실행되지 않았습니다. 먼저 relay init을 실행하세요.\x1b[0m');
58
+ }
59
+ else {
60
+ console.error(JSON.stringify({ error: 'NOT_INITIALIZED', message: 'relay init을 먼저 실행하세요.' }));
61
+ }
62
+ process.exit(1);
63
+ }
64
+ try {
65
+ const { spaceName } = await joinSpace(slug, opts.code);
66
+ if (json) {
67
+ // best-effort: fetch teams for JSON response
68
+ let teams = [];
69
+ try {
70
+ const token = await (0, config_js_1.getValidToken)();
71
+ if (token)
72
+ teams = await fetchSpaceTeams(slug, token);
73
+ }
74
+ catch {
75
+ // ignore
76
+ }
77
+ console.log(JSON.stringify({ status: 'ok', space: slug, space_name: spaceName, teams: teams.map((t) => ({ slug: t.slug, name: t.name })) }));
78
+ }
79
+ else {
80
+ console.log(`\x1b[32m✅ ${spaceName} Space에 가입했습니다\x1b[0m`);
81
+ // best-effort: show available teams
82
+ try {
83
+ const token = await (0, config_js_1.getValidToken)();
84
+ if (token) {
85
+ const teams = await fetchSpaceTeams(slug, token);
86
+ if (teams.length === 0) {
87
+ console.log('\n아직 추가된 팀이 없습니다.');
88
+ }
89
+ else {
90
+ console.log('\n\x1b[1m📦 사용 가능한 팀:\x1b[0m');
91
+ for (const t of teams) {
92
+ const desc = t.description ? ` \x1b[90m— ${t.description}\x1b[0m` : '';
93
+ console.log(` \x1b[36m•\x1b[0m \x1b[1m${t.slug}\x1b[0m${desc}`);
94
+ }
95
+ console.log(`\n\x1b[33m💡 설치: relay install @spaces/${slug}/<팀슬러그>\x1b[0m`);
96
+ }
97
+ }
98
+ }
99
+ catch {
100
+ // ignore — success message was already shown
101
+ }
102
+ }
103
+ }
104
+ catch (err) {
105
+ const message = err instanceof Error ? err.message : String(err);
106
+ if (message === 'ALREADY_MEMBER') {
107
+ if (json) {
108
+ console.log(JSON.stringify({ status: 'already_member', space: slug }));
109
+ }
110
+ else {
111
+ console.log(`\x1b[33m이미 ${slug} Space의 멤버입니다.\x1b[0m`);
112
+ }
113
+ return;
114
+ }
115
+ if (message === 'LOGIN_REQUIRED') {
116
+ if (json) {
117
+ console.error(JSON.stringify({
118
+ error: 'LOGIN_REQUIRED',
119
+ message: '로그인이 필요합니다. relay login 을 먼저 실행하세요.',
120
+ }));
121
+ }
122
+ else {
123
+ console.error('\x1b[31m오류: 로그인이 필요합니다.\x1b[0m');
124
+ console.error(' relay login 을 먼저 실행하세요.');
125
+ }
126
+ process.exit(1);
127
+ }
128
+ if (json) {
129
+ console.error(JSON.stringify({ error: 'JOIN_FAILED', message }));
130
+ }
131
+ else {
132
+ console.error(`\x1b[31m오류: ${message}\x1b[0m`);
133
+ }
134
+ process.exit(1);
135
+ }
136
+ });
137
+ }
@@ -2,12 +2,73 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerList = registerList;
4
4
  const config_js_1 = require("../lib/config.js");
5
+ async function fetchSpaceTeamList(spaceSlug, token) {
6
+ const res = await fetch(`${config_js_1.API_URL}/api/spaces/${spaceSlug}/teams`, {
7
+ headers: { Authorization: `Bearer ${token}` },
8
+ signal: AbortSignal.timeout(8000),
9
+ });
10
+ if (!res.ok) {
11
+ const body = await res.text();
12
+ throw new Error(`Space 팀 목록 조회 실패 (${res.status}): ${body}`);
13
+ }
14
+ const data = (await res.json());
15
+ if (Array.isArray(data))
16
+ return data;
17
+ return data.teams ?? [];
18
+ }
5
19
  function registerList(program) {
6
20
  program
7
21
  .command('list')
8
22
  .description('설치된 에이전트 팀 목록')
9
- .action(() => {
23
+ .option('--space <slug>', 'Space 마켓플레이스의 팀 목록 조회')
24
+ .action(async (opts) => {
10
25
  const json = program.opts().json ?? false;
26
+ // --space 옵션: Space 마켓플레이스 팀 목록
27
+ if (opts.space) {
28
+ const spaceSlug = opts.space;
29
+ const token = await (0, config_js_1.getValidToken)();
30
+ if (!token) {
31
+ if (json) {
32
+ console.error(JSON.stringify({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다. relay login을 먼저 실행하세요.' }));
33
+ }
34
+ else {
35
+ console.error('\x1b[31m오류: 로그인이 필요합니다.\x1b[0m');
36
+ console.error(' relay login을 먼저 실행하세요.');
37
+ }
38
+ process.exit(1);
39
+ }
40
+ try {
41
+ const teams = await fetchSpaceTeamList(spaceSlug, token);
42
+ if (json) {
43
+ console.log(JSON.stringify({ space: spaceSlug, teams }));
44
+ return;
45
+ }
46
+ if (teams.length === 0) {
47
+ console.log(`\n${spaceSlug} Space에 추가된 팀이 없습니다.`);
48
+ return;
49
+ }
50
+ console.log(`\n\x1b[1m${spaceSlug} Space 팀 목록\x1b[0m (${teams.length}개):\n`);
51
+ for (const t of teams) {
52
+ const desc = t.description
53
+ ? ` \x1b[90m${t.description.length > 50 ? t.description.slice(0, 50) + '...' : t.description}\x1b[0m`
54
+ : '';
55
+ console.log(` \x1b[36m${t.slug}\x1b[0m \x1b[1m${t.name}\x1b[0m${desc}`);
56
+ }
57
+ console.log(`\n\x1b[33m💡 설치: relay install @spaces/${spaceSlug}/<팀슬러그>\x1b[0m`);
58
+ }
59
+ catch (err) {
60
+ const message = err instanceof Error ? err.message : String(err);
61
+ if (json) {
62
+ console.error(JSON.stringify({ error: 'FETCH_FAILED', message }));
63
+ }
64
+ else {
65
+ console.error(`\x1b[31m오류: ${message}\x1b[0m`);
66
+ }
67
+ process.exit(1);
68
+ }
69
+ return;
70
+ }
71
+ // 기본 동작: 로컬에 설치된 팀 목록
11
72
  const installed = (0, config_js_1.loadInstalled)();
12
73
  const entries = Object.entries(installed);
13
74
  const installedList = entries.map(([slug, info]) => ({
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.registerLogin = registerLogin;
7
7
  const http_1 = __importDefault(require("http"));
8
+ const readline_1 = __importDefault(require("readline"));
8
9
  const child_process_1 = require("child_process");
9
10
  const config_js_1 = require("../lib/config.js");
10
11
  function openBrowser(url) {
@@ -58,6 +59,10 @@ const SUCCESS_HTML = `<!DOCTYPE html>
58
59
  </body></html>`;
59
60
  function waitForToken(port) {
60
61
  return new Promise((resolve, reject) => {
62
+ const timeout = setTimeout(() => {
63
+ server.close();
64
+ reject(new Error('로그인 시간이 초과되었습니다 (5분)'));
65
+ }, 5 * 60 * 1000);
61
66
  const server = http_1.default.createServer(async (req, res) => {
62
67
  const url = new URL(req.url ?? '/', `http://localhost:${port}`);
63
68
  if (url.pathname === '/callback') {
@@ -76,6 +81,7 @@ function waitForToken(port) {
76
81
  const expires_at = expires_at_raw ? Number(expires_at_raw) : undefined;
77
82
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
78
83
  res.end(SUCCESS_HTML);
84
+ clearTimeout(timeout);
79
85
  server.close();
80
86
  if (token) {
81
87
  resolve({ token, refresh_token, expires_at });
@@ -90,11 +96,6 @@ function waitForToken(port) {
90
96
  }
91
97
  });
92
98
  server.listen(port, '127.0.0.1');
93
- // Timeout after 5 minutes
94
- setTimeout(() => {
95
- server.close();
96
- reject(new Error('로그인 시간이 초과되었습니다 (5분)'));
97
- }, 5 * 60 * 1000);
98
99
  });
99
100
  }
100
101
  function findAvailablePort() {
@@ -112,11 +113,109 @@ function findAvailablePort() {
112
113
  });
113
114
  });
114
115
  }
116
+ function promptLine(rl, question) {
117
+ return new Promise((resolve) => rl.question(question, resolve));
118
+ }
119
+ function promptPassword(question) {
120
+ return new Promise((resolve) => {
121
+ const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
122
+ process.stdout.write(question);
123
+ // Hide input
124
+ const stdin = process.stdin;
125
+ if (stdin.isTTY)
126
+ stdin.setRawMode(true);
127
+ let input = '';
128
+ process.stdin.resume();
129
+ process.stdin.setEncoding('utf8');
130
+ const onData = (ch) => {
131
+ if (ch === '\n' || ch === '\r' || ch === '\u0004') {
132
+ if (stdin.isTTY)
133
+ stdin.setRawMode(false);
134
+ process.stdout.write('\n');
135
+ process.stdin.removeListener('data', onData);
136
+ rl.close();
137
+ resolve(input);
138
+ }
139
+ else if (ch === '\u0003') {
140
+ process.exit(1);
141
+ }
142
+ else if (ch === '\u007f') {
143
+ input = input.slice(0, -1);
144
+ }
145
+ else {
146
+ input += ch;
147
+ }
148
+ };
149
+ process.stdin.on('data', onData);
150
+ });
151
+ }
152
+ async function loginWithBrowser(provider, json) {
153
+ const port = await findAvailablePort();
154
+ const loginUrl = `${config_js_1.API_URL}/auth/cli-login?port=${port}&provider=${provider}`;
155
+ if (!json) {
156
+ const providerName = provider === 'github' ? 'GitHub' : '카카오';
157
+ console.error(`브라우저에서 ${providerName} 로그인을 진행합니다...`);
158
+ }
159
+ openBrowser(loginUrl);
160
+ return waitForToken(port);
161
+ }
162
+ async function loginWithEmail(json) {
163
+ const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
164
+ if (!json) {
165
+ console.error('이메일로 로그인합니다.');
166
+ }
167
+ let email;
168
+ let password;
169
+ try {
170
+ email = (await promptLine(rl, '이메일: ')).trim();
171
+ rl.close();
172
+ password = await promptPassword('비밀번호: ');
173
+ }
174
+ catch {
175
+ rl.close();
176
+ throw new Error('입력이 취소되었습니다');
177
+ }
178
+ const res = await fetch(`${config_js_1.API_URL}/api/auth/email-login`, {
179
+ method: 'POST',
180
+ headers: { 'Content-Type': 'application/json' },
181
+ body: JSON.stringify({ email, password }),
182
+ });
183
+ if (!res.ok) {
184
+ const body = (await res.json().catch(() => ({})));
185
+ const msg = body.error ?? '이메일 또는 비밀번호가 올바르지 않습니다.';
186
+ throw new Error(msg);
187
+ }
188
+ const data = (await res.json());
189
+ return {
190
+ token: data.access_token,
191
+ refresh_token: data.refresh_token,
192
+ expires_at: data.expires_at,
193
+ };
194
+ }
195
+ async function selectProvider(json) {
196
+ if (json)
197
+ return 'github';
198
+ const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
199
+ console.error('');
200
+ console.error('로그인 방법을 선택하세요:');
201
+ console.error(' 1) GitHub');
202
+ console.error(' 2) 카카오');
203
+ console.error(' 3) 이메일 / 비밀번호');
204
+ console.error('');
205
+ const answer = (await promptLine(rl, '선택 (기본값: 1): ')).trim();
206
+ rl.close();
207
+ if (answer === '2')
208
+ return 'kakao';
209
+ if (answer === '3')
210
+ return 'email';
211
+ return 'github';
212
+ }
115
213
  function registerLogin(program) {
116
214
  program
117
215
  .command('login')
118
216
  .description('RelayAX 계정에 로그인합니다')
119
217
  .option('--token <token>', '직접 토큰 입력 (브라우저 없이)')
218
+ .option('--provider <provider>', '로그인 제공자 (github | kakao | email)')
120
219
  .action(async (opts) => {
121
220
  const json = program.opts().json ?? false;
122
221
  (0, config_js_1.ensureGlobalRelayDir)();
@@ -125,18 +224,32 @@ function registerLogin(program) {
125
224
  let expiresAt;
126
225
  if (!accessToken) {
127
226
  try {
128
- const port = await findAvailablePort();
129
- const loginUrl = `${config_js_1.API_URL}/auth/cli-login?port=${port}`;
130
- console.error('브라우저에서 로그인을 진행합니다...');
131
- openBrowser(loginUrl);
132
- const loginResult = await waitForToken(port);
227
+ let provider = opts.provider;
228
+ if (!provider) {
229
+ provider = await selectProvider(json);
230
+ }
231
+ let loginResult;
232
+ if (provider === 'email') {
233
+ loginResult = await loginWithEmail(json);
234
+ }
235
+ else if (provider === 'kakao') {
236
+ loginResult = await loginWithBrowser('kakao', json);
237
+ }
238
+ else {
239
+ loginResult = await loginWithBrowser('github', json);
240
+ }
133
241
  accessToken = loginResult.token;
134
242
  refreshToken = loginResult.refresh_token;
135
243
  expiresAt = loginResult.expires_at;
136
244
  }
137
245
  catch (err) {
138
246
  const msg = err instanceof Error ? err.message : '로그인 실패';
139
- console.error(JSON.stringify({ error: 'LOGIN_FAILED', message: msg }));
247
+ if (json) {
248
+ console.error(JSON.stringify({ error: 'LOGIN_FAILED', message: msg }));
249
+ }
250
+ else {
251
+ console.error(`\x1b[31m오류: ${msg}\x1b[0m`);
252
+ }
140
253
  process.exit(1);
141
254
  }
142
255
  }
@@ -51,10 +51,9 @@ function parseRelayYaml(content) {
51
51
  }
52
52
  const requires = raw.requires;
53
53
  const rawVisibility = String(raw.visibility ?? '');
54
- const visibility = rawVisibility === 'login-only' ? 'login-only'
55
- : rawVisibility === 'invite-only' ? 'invite-only'
56
- : rawVisibility === 'public' ? 'public'
57
- : undefined;
54
+ const visibility = rawVisibility === 'private' ? 'private'
55
+ : rawVisibility === 'public' ? 'public'
56
+ : undefined;
58
57
  return {
59
58
  name: String(raw.name ?? ''),
60
59
  slug: String(raw.slug ?? ''),
@@ -447,14 +446,13 @@ function registerPublish(program) {
447
446
  const visibility = await promptSelect({
448
447
  message: '공개 범위:',
449
448
  choices: [
450
- { name: '전체 공개', value: 'public' },
451
- { name: '로그인 사용자만', value: 'login-only' },
452
- { name: '초대 코드 필요', value: 'invite-only' },
449
+ { name: '공개', value: 'public' },
450
+ { name: '비공개 (Space 멤버만)', value: 'private' },
453
451
  ],
454
452
  });
455
453
  console.error('\n\x1b[2m💡 프로필에 연락처를 설정하면 설치 시 명함이 전달됩니다: www.relayax.com/dashboard/profile\x1b[0m');
456
- if (visibility === 'invite-only') {
457
- console.error('\x1b[2m💡 invite-only 팀은 대시보드에서 사용자를 초대하세요: www.relayax.com/dashboard/teams\x1b[0m');
454
+ if (visibility === 'private') {
455
+ console.error('\x1b[2m💡 비공개 팀은 Space를 통해 멤버를 관리하세요: www.relayax.com/dashboard/teams\x1b[0m');
458
456
  }
459
457
  console.error('');
460
458
  const tags = tagsRaw
@@ -483,6 +481,58 @@ function registerPublish(program) {
483
481
  }));
484
482
  process.exit(1);
485
483
  }
484
+ // Visibility validation: must be explicitly set
485
+ if (!config.visibility) {
486
+ if (isTTY) {
487
+ const { select: promptSelect } = await import('@inquirer/prompts');
488
+ console.error('\n\x1b[33m⚠ relay.yaml에 visibility가 설정되지 않았습니다.\x1b[0m');
489
+ // Show user's spaces to help decide
490
+ try {
491
+ const { fetchMySpaces } = await import('./spaces.js');
492
+ const spaceToken = opts.token ?? process.env.RELAY_TOKEN ?? await (0, config_js_1.getValidToken)();
493
+ if (spaceToken) {
494
+ const spaces = await fetchMySpaces(spaceToken);
495
+ const nonPersonal = spaces.filter((s) => !s.is_personal);
496
+ if (nonPersonal.length > 0) {
497
+ console.error(`\n 내 Space:`);
498
+ for (const s of nonPersonal) {
499
+ console.error(` \x1b[36m${s.slug}\x1b[0m — ${s.name}`);
500
+ }
501
+ console.error(`\n 비공개로 설정하면 위 Space 멤버만 접근할 수 있습니다.\n`);
502
+ }
503
+ }
504
+ }
505
+ catch {
506
+ // Space 조회 실패는 무시 — visibility 선택은 계속 진행
507
+ }
508
+ config.visibility = await promptSelect({
509
+ message: '공개 범위를 선택하세요:',
510
+ choices: [
511
+ { name: '공개 — 마켓플레이스에 누구나 검색·설치', value: 'public' },
512
+ { name: '비공개 — Space 멤버만 접근', value: 'private' },
513
+ ],
514
+ });
515
+ // Save back to relay.yaml
516
+ const yamlData = js_yaml_1.default.load(yamlContent);
517
+ yamlData.visibility = config.visibility;
518
+ fs_1.default.writeFileSync(relayYamlPath, js_yaml_1.default.dump(yamlData, { lineWidth: 120 }), 'utf-8');
519
+ console.error(` → relay.yaml에 visibility: ${config.visibility} 저장됨\n`);
520
+ }
521
+ else {
522
+ console.error(JSON.stringify({
523
+ error: 'MISSING_VISIBILITY',
524
+ message: 'relay.yaml에 visibility (public 또는 private)를 설정해주세요.',
525
+ }));
526
+ process.exit(1);
527
+ }
528
+ }
529
+ // Confirm visibility before publish
530
+ if (isTTY) {
531
+ const visLabel = config.visibility === 'public'
532
+ ? '\x1b[32m공개\x1b[0m (마켓플레이스에 누구나 검색·설치)'
533
+ : '\x1b[33m비공개\x1b[0m (Space 멤버만 접근)';
534
+ console.error(`공개 범위: ${visLabel}`);
535
+ }
486
536
  // Profile hint
487
537
  if (isTTY) {
488
538
  console.error('💡 프로필에 연락처를 설정하면 설치 시 명함이 전달됩니다: www.relayax.com/dashboard/profile');
@@ -0,0 +1,11 @@
1
+ import { Command } from 'commander';
2
+ export interface SpaceInfo {
3
+ id: string;
4
+ slug: string;
5
+ name: string;
6
+ description: string | null;
7
+ is_personal: boolean;
8
+ role: string;
9
+ }
10
+ export declare function fetchMySpaces(token: string): Promise<SpaceInfo[]>;
11
+ export declare function registerSpaces(program: Command): void;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchMySpaces = fetchMySpaces;
4
+ exports.registerSpaces = registerSpaces;
5
+ const config_js_1 = require("../lib/config.js");
6
+ async function fetchMySpaces(token) {
7
+ const res = await fetch(`${config_js_1.API_URL}/api/spaces`, {
8
+ headers: { Authorization: `Bearer ${token}` },
9
+ signal: AbortSignal.timeout(8000),
10
+ });
11
+ if (!res.ok) {
12
+ throw new Error(`Space 목록 조회 실패 (${res.status})`);
13
+ }
14
+ return (await res.json());
15
+ }
16
+ function registerSpaces(program) {
17
+ program
18
+ .command('spaces')
19
+ .description('내 Space 목록을 확인합니다')
20
+ .action(async () => {
21
+ const json = program.opts().json ?? false;
22
+ const token = await (0, config_js_1.getValidToken)();
23
+ if (!token) {
24
+ if (json) {
25
+ console.error(JSON.stringify({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다. relay login을 먼저 실행하세요.' }));
26
+ }
27
+ else {
28
+ console.error('\x1b[31m오류: 로그인이 필요합니다.\x1b[0m');
29
+ console.error(' relay login을 먼저 실행하세요.');
30
+ }
31
+ process.exit(1);
32
+ }
33
+ try {
34
+ const spaces = await fetchMySpaces(token);
35
+ if (json) {
36
+ console.log(JSON.stringify({ spaces }));
37
+ return;
38
+ }
39
+ const personal = spaces.find((s) => s.is_personal);
40
+ const others = spaces.filter((s) => !s.is_personal);
41
+ if (personal) {
42
+ console.log(`\n\x1b[90m개인 스페이스:\x1b[0m`);
43
+ console.log(` \x1b[36m${personal.slug}\x1b[0m \x1b[1m${personal.name}\x1b[0m`);
44
+ }
45
+ if (others.length > 0) {
46
+ console.log(`\n\x1b[1m내 Space\x1b[0m (${others.length}개):\n`);
47
+ for (const s of others) {
48
+ const role = s.role === 'owner' ? '\x1b[33m소유자\x1b[0m'
49
+ : s.role === 'admin' ? '\x1b[36m관리자\x1b[0m'
50
+ : '\x1b[90m멤버\x1b[0m';
51
+ const desc = s.description
52
+ ? ` \x1b[90m${s.description.length > 40 ? s.description.slice(0, 40) + '...' : s.description}\x1b[0m`
53
+ : '';
54
+ console.log(` \x1b[36m${s.slug}\x1b[0m \x1b[1m${s.name}\x1b[0m ${role}${desc}`);
55
+ }
56
+ }
57
+ if (!personal && others.length === 0) {
58
+ console.log('\nSpace가 없습니다.');
59
+ console.log('\x1b[33m💡 Space를 만들려면: www.relayax.com/spaces/new\x1b[0m');
60
+ }
61
+ if (others.length > 0) {
62
+ console.log(`\n\x1b[33m💡 Space 팀 목록: relay list --space <slug>\x1b[0m`);
63
+ console.log(`\x1b[33m💡 비공개 배포: relay.yaml에 visibility: private 설정 후 relay publish\x1b[0m`);
64
+ }
65
+ }
66
+ catch (err) {
67
+ const message = err instanceof Error ? err.message : String(err);
68
+ if (json) {
69
+ console.error(JSON.stringify({ error: 'FETCH_FAILED', message }));
70
+ }
71
+ else {
72
+ console.error(`\x1b[31m오류: ${message}\x1b[0m`);
73
+ }
74
+ process.exit(1);
75
+ }
76
+ });
77
+ }
@@ -13,7 +13,7 @@ function registerUpdate(program) {
13
13
  .command('update <slug>')
14
14
  .description('설치된 에이전트 팀을 최신 버전으로 업데이트합니다')
15
15
  .option('--path <install_path>', '설치 경로 지정 (기본: ./.claude)')
16
- .option('--code <code>', '초대 코드 (invite-only 팀 업데이트 시 필요)')
16
+ .option('--code <code>', '초대 코드 (비공개 팀 업데이트 시 필요)')
17
17
  .action(async (slugInput, opts) => {
18
18
  const json = program.opts().json ?? false;
19
19
  const installPath = (0, config_js_1.getInstallPath)(opts.path);
@@ -52,16 +52,10 @@ function registerUpdate(program) {
52
52
  }
53
53
  // Visibility check
54
54
  const visibility = team.visibility ?? 'public';
55
- if (visibility === 'login-only') {
55
+ if (visibility === 'private') {
56
56
  const token = await (0, config_js_1.getValidToken)();
57
57
  if (!token) {
58
- console.error('이 팀은 로그인이 필요합니다. `relay login`을 먼저 실행하세요.');
59
- process.exit(1);
60
- }
61
- }
62
- else if (visibility === 'invite-only') {
63
- if (!opts.code) {
64
- console.error('초대 코드가 필요합니다. `relay update ' + slug + ' --code <code>`로 업데이트하세요.');
58
+ console.error('이 팀은 Space 멤버만 업데이트할 수 있습니다. `relay login`을 먼저 실행하세요.');
65
59
  process.exit(1);
66
60
  }
67
61
  }
package/dist/index.js CHANGED
@@ -16,6 +16,8 @@ const outdated_js_1 = require("./commands/outdated.js");
16
16
  const check_update_js_1 = require("./commands/check-update.js");
17
17
  const follow_js_1 = require("./commands/follow.js");
18
18
  const changelog_js_1 = require("./commands/changelog.js");
19
+ const join_js_1 = require("./commands/join.js");
20
+ const spaces_js_1 = require("./commands/spaces.js");
19
21
  // eslint-disable-next-line @typescript-eslint/no-var-requires
20
22
  const pkg = require('../package.json');
21
23
  const program = new commander_1.Command();
@@ -38,4 +40,6 @@ program
38
40
  (0, check_update_js_1.registerCheckUpdate)(program);
39
41
  (0, follow_js_1.registerFollow)(program);
40
42
  (0, changelog_js_1.registerChangelog)(program);
43
+ (0, join_js_1.registerJoin)(program);
44
+ (0, spaces_js_1.registerSpaces)(program);
41
45
  program.parse();
package/dist/lib/api.d.ts CHANGED
@@ -8,6 +8,11 @@ export interface TeamVersionInfo {
8
8
  }
9
9
  export declare function fetchTeamVersions(slug: string): Promise<TeamVersionInfo[]>;
10
10
  export declare function reportInstall(slug: string, version?: string): Promise<void>;
11
+ /**
12
+ * Space 팀 설치: 멤버십 검증 + install count 증가 + 팀 메타데이터 반환을 한 번에 처리.
13
+ * POST /api/spaces/{spaceSlug}/teams/{teamSlug}/install
14
+ */
15
+ export declare function installSpaceTeam(spaceSlug: string, teamSlug: string, version?: string): Promise<TeamRegistryInfo>;
11
16
  export interface ResolvedSlug {
12
17
  owner: string;
13
18
  name: string;
package/dist/lib/api.js CHANGED
@@ -4,6 +4,7 @@ exports.fetchTeamInfo = fetchTeamInfo;
4
4
  exports.searchTeams = searchTeams;
5
5
  exports.fetchTeamVersions = fetchTeamVersions;
6
6
  exports.reportInstall = reportInstall;
7
+ exports.installSpaceTeam = installSpaceTeam;
7
8
  exports.resolveSlugFromServer = resolveSlugFromServer;
8
9
  exports.sendUsagePing = sendUsagePing;
9
10
  exports.followBuilder = followBuilder;
@@ -62,6 +63,31 @@ async function reportInstall(slug, version) {
62
63
  // non-critical: ignore errors
63
64
  });
64
65
  }
66
+ /**
67
+ * Space 팀 설치: 멤버십 검증 + install count 증가 + 팀 메타데이터 반환을 한 번에 처리.
68
+ * POST /api/spaces/{spaceSlug}/teams/{teamSlug}/install
69
+ */
70
+ async function installSpaceTeam(spaceSlug, teamSlug, version) {
71
+ const url = `${config_js_1.API_URL}/api/spaces/${spaceSlug}/teams/${teamSlug}/install`;
72
+ const headers = { 'Content-Type': 'application/json' };
73
+ const token = await (0, config_js_1.getValidToken)();
74
+ if (token) {
75
+ headers['Authorization'] = `Bearer ${token}`;
76
+ }
77
+ const body = {};
78
+ if (version)
79
+ body.version = version;
80
+ const res = await fetch(url, {
81
+ method: 'POST',
82
+ headers,
83
+ body: JSON.stringify(body),
84
+ });
85
+ if (!res.ok) {
86
+ const text = await res.text();
87
+ throw new Error(`Space 팀 설치 실패 (${res.status}): ${text}`);
88
+ }
89
+ return res.json();
90
+ }
65
91
  async function resolveSlugFromServer(name) {
66
92
  const url = `${config_js_1.API_URL}/api/registry/resolve?name=${encodeURIComponent(name)}`;
67
93
  const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
@@ -108,7 +108,10 @@ exports.USER_COMMANDS = [
108
108
  - 업데이트 여부와 관계없이 설치를 계속 진행합니다.
109
109
 
110
110
  ### 1. 팀 패키지 다운로드
111
- - \`relay install <@author/slug>\` 명령어를 실행합니다.
111
+ - Public 마켓 팀: \`relay install <@author/slug>\` 명령어를 실행합니다.
112
+ - Space 팀: \`relay install @spaces/<space-slug>/<team-slug>\` 명령어를 실행합니다.
113
+ - Space 가입이 필요하면: \`relay join <space-slug> --code <invite-code>\` 를 먼저 실행합니다.
114
+ - 또는 \`relay install @spaces/<space-slug>/<team-slug> --join-code <code>\` 로 가입+설치를 한번에 할 수 있습니다.
112
115
  - 패키지가 \`.relay/teams/<slug>/\`에 다운로드됩니다.
113
116
 
114
117
  ### 2. 패키지 내용 확인
@@ -160,7 +163,13 @@ ${BUSINESS_CARD_FORMAT}
160
163
  → commands/cardnews.md → .claude/commands/cardnews.md 복사
161
164
  → skills/pdf-gen.md → .claude/skills/pdf-gen.md 복사
162
165
  → requires 확인: ✓ playwright 설치됨, ✓ sharp 설치됨
163
- → "✓ 설치 완료! /cardnews를 사용해볼까요?"`,
166
+ → "✓ 설치 완료! /cardnews를 사용해볼까요?"
167
+
168
+ 사용자: /relay-install @spaces/bobusan/pm-bot
169
+ → relay install @spaces/bobusan/pm-bot 실행
170
+ → Space 멤버 확인 → 정상
171
+ → 패키지 다운로드 및 배치
172
+ → "✓ 설치 완료!"`,
164
173
  },
165
174
  {
166
175
  id: 'relay-list',
@@ -176,13 +185,23 @@ ${BUSINESS_CARD_FORMAT}
176
185
  - 사용 가능한 커맨드
177
186
  3. 설치된 팀이 없으면 \`/relay-explore\`로 팀을 탐색해보라고 안내합니다.
178
187
 
188
+ ### Space 팀 목록 확인
189
+ - 특정 Space에서 사용 가능한 팀 목록을 보려면: \`relay list --space <space-slug> --json\`
190
+ - Space에 가입되어 있어야 합니다.
191
+
179
192
  ## 예시
180
193
 
181
194
  사용자: /relay-list
182
195
  → relay list --json 실행
183
196
  → "2개 팀이 설치되어 있어요:"
184
197
  → " @example/contents-team v1.2.0 (2일 전 설치) - /cardnews, /pdf-report"
185
- → " qa-team v0.5.1 (1주 전 설치) - /qa, /qa-only"`,
198
+ → " qa-team v0.5.1 (1주 전 설치) - /qa, /qa-only"
199
+
200
+ 사용자: /relay-list --space bobusan
201
+ → relay list --space bobusan --json 실행
202
+ → "bobusan Space에서 설치 가능한 팀:"
203
+ → " pm-bot — 프로젝트 관리 봇"
204
+ → " cs-bot — 고객 응대 봇"`,
186
205
  },
187
206
  {
188
207
  id: 'relay-update',
@@ -372,7 +391,17 @@ requires:
372
391
  - tags: 팀 특성에 맞는 태그를 추천합니다.
373
392
  - 사용자에게 확인: "이대로 배포할까요?"
374
393
 
375
- ### 7. .relay/relay.yaml 업데이트
394
+ ### 7. 공개 범위 확인 (필수)
395
+ - .relay/relay.yaml에 \`visibility\`가 반드시 설정되어 있어야 합니다.
396
+ - 설정되어 있지 않으면 빌더에게 반드시 물어봅니다:
397
+ - "공개 (마켓플레이스에 누구나 검색·설치 가능)" vs "비공개 (Space 멤버만 접근)"
398
+ - 선택 결과를 relay.yaml에 저장합니다.
399
+ - 이미 설정되어 있으면 현재 값을 확인합니다:
400
+ - 공개인 경우: "⚠ 이 팀은 **공개**로 설정되어 있어 마켓플레이스에 노출됩니다. 맞나요?"
401
+ - 비공개인 경우: "이 팀은 **비공개**로 설정되어 Space 멤버만 접근 가능합니다."
402
+ - 빌더가 변경을 원하면 relay.yaml을 업데이트합니다.
403
+
404
+ ### 8. .relay/relay.yaml 업데이트
376
405
  - 메타데이터, requires, 포트폴리오 슬롯을 .relay/relay.yaml에 반영합니다.
377
406
 
378
407
  \`\`\`yaml
@@ -387,7 +416,7 @@ portfolio:
387
416
  title: "카드뉴스 예시"
388
417
  \`\`\`
389
418
 
390
- ### 8. 배포
419
+ ### 9. 배포
391
420
  - \`relay publish\` 명령어를 실행합니다.
392
421
  - 배포 결과와 마켓플레이스 URL을 보여줍니다.
393
422
  ${BUSINESS_CARD_FORMAT}
package/dist/types.d.ts CHANGED
@@ -8,6 +8,8 @@ export interface InstalledTeam {
8
8
  installed_at: string;
9
9
  files: string[];
10
10
  type?: 'team' | 'system';
11
+ /** Space 소속 팀인 경우 Space slug */
12
+ space_slug?: string;
11
13
  }
12
14
  /** 키는 scoped slug 포맷: "@owner/name" */
13
15
  export interface InstalledRegistry {
@@ -30,7 +32,7 @@ export interface TeamRegistryInfo {
30
32
  tags?: string[];
31
33
  install_count?: number;
32
34
  requires?: Record<string, unknown>;
33
- visibility?: "public" | "login-only" | "invite-only";
35
+ visibility?: "public" | "private";
34
36
  welcome?: string | null;
35
37
  contact?: Record<string, string> | null;
36
38
  author?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relayax-cli",
3
- "version": "0.2.20",
3
+ "version": "0.2.23",
4
4
  "description": "RelayAX Agent Team Marketplace CLI - Install and manage agent teams",
5
5
  "main": "dist/index.js",
6
6
  "bin": {