relayax-cli 0.2.22 → 0.2.24

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/README.md CHANGED
@@ -9,7 +9,7 @@ Agent Team Marketplace CLI - 에이전트 팀을 검색하고 설치하세요.
9
9
  npx relay-cli install contents-team
10
10
 
11
11
  # 또는 글로벌 설치
12
- npm install -g relay-cli
12
+ npm install -g relayax-cli
13
13
  relay install contents-team
14
14
  ```
15
15
 
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerCheckUpdate = registerCheckUpdate;
4
4
  const version_check_js_1 = require("../lib/version-check.js");
5
5
  const slug_js_1 = require("../lib/slug.js");
6
- const config_js_1 = require("../lib/config.js");
7
6
  function registerCheckUpdate(program) {
8
7
  program
9
8
  .command('check-update [slug]')
@@ -32,19 +31,12 @@ function registerCheckUpdate(program) {
32
31
  scopedSlug = slug;
33
32
  }
34
33
  else {
35
- const installed = (0, config_js_1.loadInstalled)();
36
- const found = (0, slug_js_1.findInstalledByName)(installed, slug);
37
- if (found) {
38
- scopedSlug = found;
34
+ try {
35
+ const parsed = await (0, slug_js_1.resolveSlug)(slug);
36
+ scopedSlug = parsed.full;
39
37
  }
40
- else {
41
- try {
42
- const parsed = await (0, slug_js_1.resolveSlug)(slug);
43
- scopedSlug = parsed.full;
44
- }
45
- catch {
46
- scopedSlug = slug; // fallback to original
47
- }
38
+ catch {
39
+ scopedSlug = slug;
48
40
  }
49
41
  }
50
42
  const teamResult = await (0, version_check_js_1.checkTeamVersion)(scopedSlug, force);
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerDeployRecord(program: Command): void;
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerDeployRecord = registerDeployRecord;
7
+ const path_1 = __importDefault(require("path"));
8
+ const config_js_1 = require("../lib/config.js");
9
+ const slug_js_1 = require("../lib/slug.js");
10
+ function registerDeployRecord(program) {
11
+ program
12
+ .command('deploy-record <slug>')
13
+ .description('에이전트가 배치한 파일 정보를 installed.json에 기록합니다')
14
+ .requiredOption('--scope <scope>', '배치 범위 (global 또는 local)')
15
+ .option('--files <paths...>', '배치된 파일 경로 목록')
16
+ .action((slugInput, opts) => {
17
+ const json = program.opts().json ?? false;
18
+ const scope = opts.scope;
19
+ if (scope !== 'global' && scope !== 'local') {
20
+ const msg = { error: 'INVALID_SCOPE', message: '--scope는 global 또는 local이어야 합니다.' };
21
+ if (json) {
22
+ console.error(JSON.stringify(msg));
23
+ }
24
+ else {
25
+ console.error(`\x1b[31m오류:\x1b[0m ${msg.message}`);
26
+ }
27
+ process.exit(1);
28
+ }
29
+ const files = opts.files ?? [];
30
+ // Resolve absolute paths
31
+ const resolvedFiles = files.map((f) => f.startsWith('/') || f.startsWith('~')
32
+ ? f
33
+ : path_1.default.resolve(f));
34
+ // Find the team in the appropriate registry
35
+ const localRegistry = (0, config_js_1.loadInstalled)();
36
+ const globalRegistry = (0, config_js_1.loadGlobalInstalled)();
37
+ // Resolve slug — check both registries for short name match
38
+ let slug;
39
+ if ((0, slug_js_1.isScopedSlug)(slugInput)) {
40
+ slug = slugInput;
41
+ }
42
+ else {
43
+ const allKeys = [...Object.keys(localRegistry), ...Object.keys(globalRegistry)];
44
+ const match = allKeys.find((key) => {
45
+ const parsed = (0, slug_js_1.parseSlug)(key);
46
+ return parsed && parsed.name === slugInput;
47
+ });
48
+ slug = match ?? slugInput;
49
+ }
50
+ // Check if team exists in either registry
51
+ const entry = localRegistry[slug] ?? globalRegistry[slug];
52
+ if (!entry) {
53
+ const msg = { error: 'NOT_INSTALLED', message: `'${slugInput}'는 설치되어 있지 않습니다.` };
54
+ if (json) {
55
+ console.error(JSON.stringify(msg));
56
+ }
57
+ else {
58
+ console.error(`\x1b[31m오류:\x1b[0m ${msg.message}`);
59
+ }
60
+ process.exit(1);
61
+ }
62
+ // Update deploy info
63
+ entry.deploy_scope = scope;
64
+ entry.deployed_files = resolvedFiles;
65
+ // Save to the correct registry based on scope
66
+ if (scope === 'global') {
67
+ globalRegistry[slug] = entry;
68
+ (0, config_js_1.saveGlobalInstalled)(globalRegistry);
69
+ // Also update local registry if entry exists there
70
+ if (localRegistry[slug]) {
71
+ localRegistry[slug] = entry;
72
+ (0, config_js_1.saveInstalled)(localRegistry);
73
+ }
74
+ }
75
+ else {
76
+ localRegistry[slug] = entry;
77
+ (0, config_js_1.saveInstalled)(localRegistry);
78
+ }
79
+ const result = {
80
+ status: 'ok',
81
+ slug,
82
+ deploy_scope: scope,
83
+ deployed_files: resolvedFiles.length,
84
+ };
85
+ if (json) {
86
+ console.log(JSON.stringify(result));
87
+ }
88
+ else {
89
+ const scopeLabel = scope === 'global' ? '글로벌' : '로컬';
90
+ console.log(`\x1b[32m✓ ${slug} 배치 정보 기록 완료\x1b[0m (${scopeLabel}, ${resolvedFiles.length}개 파일)`);
91
+ }
92
+ });
93
+ }
@@ -1,4 +1,12 @@
1
1
  import { Command } from 'commander';
2
+ /**
3
+ * 글로벌 User 커맨드를 ~/.claude/commands/relay/에 설치한다.
4
+ * 기존 파일 중 현재 커맨드 목록에 없는 것은 제거한다.
5
+ */
6
+ export declare function installGlobalUserCommands(): {
7
+ installed: boolean;
8
+ commands: string[];
9
+ };
2
10
  /**
3
11
  * 글로벌 User 커맨드가 이미 설치되어 있는지 확인한다.
4
12
  */
@@ -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.installGlobalUserCommands = installGlobalUserCommands;
6
7
  exports.hasGlobalUserCommands = hasGlobalUserCommands;
7
8
  exports.registerInit = registerInit;
8
9
  const fs_1 = __importDefault(require("fs"));
@@ -38,14 +38,12 @@ function registerInstall(program) {
38
38
  const json = program.opts().json ?? false;
39
39
  const projectPath = process.cwd();
40
40
  const tempDir = (0, storage_js_1.makeTempDir)();
41
+ // Auto-init: 글로벌 커맨드가 없으면 자동 설치
41
42
  if (!(0, init_js_1.hasGlobalUserCommands)()) {
42
43
  if (!json) {
43
- console.error('\x1b[33m relay init이 실행되지 않았습니다. 먼저 relay init을 실행하세요.\x1b[0m');
44
+ console.error('\x1b[33m 글로벌 커맨드를 자동 설치합니다...\x1b[0m');
44
45
  }
45
- else {
46
- console.error(JSON.stringify({ error: 'NOT_INITIALIZED', message: 'relay init을 먼저 실행하세요.' }));
47
- }
48
- process.exit(1);
46
+ (0, init_js_1.installGlobalUserCommands)();
49
47
  }
50
48
  try {
51
49
  // 0. @spaces/{spaceSlug}/{teamSlug} 형식 감지 및 파싱
@@ -128,23 +126,33 @@ function registerInstall(program) {
128
126
  }
129
127
  }
130
128
  const teamDir = path_1.default.join(projectPath, '.relay', 'teams', parsed.owner, parsed.name);
131
- // 2. Visibility check
129
+ // 2. Visibility check + auto-login
132
130
  const visibility = team.visibility ?? 'public';
133
131
  if (visibility === 'private') {
134
- const token = await (0, config_js_1.getValidToken)();
132
+ let token = await (0, config_js_1.getValidToken)();
135
133
  if (!token) {
136
- if (json) {
137
- console.error(JSON.stringify({
138
- error: 'LOGIN_REQUIRED',
139
- visibility,
140
- slug,
141
- message: '이 팀은 Space 멤버만 설치할 수 있습니다. 로그인이 필요합니다.',
142
- }));
134
+ const isTTY = Boolean(process.stdin.isTTY);
135
+ if (isTTY && !json) {
136
+ // Auto-login: TTY 환경에서 자동으로 login 플로우 트리거
137
+ console.error('\x1b[33m⚙ 이 팀은 로그인이 필요합니다. 로그인을 시작합니다...\x1b[0m');
138
+ const { runLogin } = await import('./login.js');
139
+ await runLogin();
140
+ token = await (0, config_js_1.getValidToken)();
143
141
  }
144
- else {
145
- console.error('\x1b[31m이 팀은 Space 멤버만 설치할 수 있습니다. relay login 을 먼저 실행하세요.\x1b[0m');
142
+ if (!token) {
143
+ if (json) {
144
+ console.error(JSON.stringify({
145
+ error: 'LOGIN_REQUIRED',
146
+ visibility,
147
+ slug,
148
+ message: '이 팀은 로그인이 필요합니다. relay login을 먼저 실행하세요.',
149
+ }));
150
+ }
151
+ else {
152
+ console.error('\x1b[31m이 팀은 로그인이 필요합니다. relay login 을 먼저 실행하세요.\x1b[0m');
153
+ }
154
+ process.exit(1);
146
155
  }
147
- process.exit(1);
148
156
  }
149
157
  }
150
158
  // 3. Download package
@@ -252,7 +260,7 @@ function registerInstall(program) {
252
260
  else {
253
261
  console.log(`\n\x1b[33m💡 설치 완료! AI 에이전트에서 사용할 수 있습니다.\x1b[0m`);
254
262
  }
255
- console.log('\n 에이전트가 /relay-install로 환경을 구성합니다.');
263
+ console.log('\n \x1b[90m에이전트가 /relay-install로 환경을 구성합니다.\x1b[0m');
256
264
  }
257
265
  }
258
266
  catch (err) {
@@ -68,27 +68,53 @@ function registerList(program) {
68
68
  }
69
69
  return;
70
70
  }
71
- // 기본 동작: 로컬에 설치된 목록
72
- const installed = (0, config_js_1.loadInstalled)();
73
- const entries = Object.entries(installed);
74
- const installedList = entries.map(([slug, info]) => ({
75
- slug,
76
- version: info.version,
77
- installed_at: info.installed_at,
78
- files: info.files.length,
79
- }));
71
+ // 기본 동작: 글로벌 + 로컬 통합 목록
72
+ const { global: globalInstalled, local: localInstalled } = (0, config_js_1.loadMergedInstalled)();
73
+ const allEntries = [];
74
+ const seen = new Set();
75
+ // 글로벌 먼저
76
+ for (const [slug, info] of Object.entries(globalInstalled)) {
77
+ allEntries.push({
78
+ slug,
79
+ version: info.version,
80
+ installed_at: info.installed_at,
81
+ scope: 'global',
82
+ deploy_scope: info.deploy_scope,
83
+ space_slug: info.space_slug,
84
+ });
85
+ seen.add(slug);
86
+ }
87
+ // 로컬 (글로벌과 중복되지 않는 것만)
88
+ for (const [slug, info] of Object.entries(localInstalled)) {
89
+ if (seen.has(slug))
90
+ continue;
91
+ allEntries.push({
92
+ slug,
93
+ version: info.version,
94
+ installed_at: info.installed_at,
95
+ scope: 'local',
96
+ deploy_scope: info.deploy_scope,
97
+ space_slug: info.space_slug,
98
+ });
99
+ }
80
100
  if (json) {
81
- console.log(JSON.stringify({ installed: installedList }));
101
+ console.log(JSON.stringify({ installed: allEntries }));
82
102
  }
83
103
  else {
84
- if (installedList.length === 0) {
104
+ if (allEntries.length === 0) {
85
105
  console.log('\n설치된 팀이 없습니다. `relay install <slug>`로 설치하세요.');
86
106
  return;
87
107
  }
88
- console.log(`\n설치된 팀 (${installedList.length}개):\n`);
89
- for (const item of installedList) {
108
+ console.log(`\n설치된 팀 (${allEntries.length}개):\n`);
109
+ for (const item of allEntries) {
90
110
  const date = new Date(item.installed_at).toLocaleDateString('ko-KR');
91
- console.log(` \x1b[36m${item.slug}\x1b[0m v${item.version} (${date}) 파일 ${item.files}개`);
111
+ const scopeLabel = item.deploy_scope === 'global'
112
+ ? '\x1b[32m글로벌\x1b[0m'
113
+ : item.deploy_scope === 'local'
114
+ ? '\x1b[33m로컬\x1b[0m'
115
+ : '\x1b[90m미배치\x1b[0m';
116
+ const spaceLabel = item.space_slug ? ` \x1b[90m[Space: ${item.space_slug}]\x1b[0m` : '';
117
+ console.log(` \x1b[36m${item.slug}\x1b[0m v${item.version} ${scopeLabel} (${date})${spaceLabel}`);
92
118
  }
93
119
  }
94
120
  });
@@ -1,2 +1,7 @@
1
1
  import { Command } from 'commander';
2
+ /**
3
+ * 대화형 로그인 플로우 실행 (auto-login에서 호출).
4
+ * 로그인 성공 시 토큰 저장, 실패 시 throw.
5
+ */
6
+ export declare function runLogin(): Promise<void>;
2
7
  export declare function registerLogin(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.runLogin = runLogin;
6
7
  exports.registerLogin = registerLogin;
7
8
  const http_1 = __importDefault(require("http"));
8
9
  const readline_1 = __importDefault(require("readline"));
@@ -38,9 +39,6 @@ async function verifyToken(token) {
38
39
  return null;
39
40
  }
40
41
  }
41
- function parseFormBody(body) {
42
- return new URLSearchParams(body);
43
- }
44
42
  function collectBody(req) {
45
43
  return new Promise((resolve) => {
46
44
  const chunks = [];
@@ -59,24 +57,22 @@ const SUCCESS_HTML = `<!DOCTYPE html>
59
57
  </body></html>`;
60
58
  function waitForToken(port) {
61
59
  return new Promise((resolve, reject) => {
60
+ const timeout = setTimeout(() => {
61
+ server.close();
62
+ reject(new Error('로그인 시간이 초과되었습니다 (5분)'));
63
+ }, 5 * 60 * 1000);
62
64
  const server = http_1.default.createServer(async (req, res) => {
63
65
  const url = new URL(req.url ?? '/', `http://localhost:${port}`);
64
- if (url.pathname === '/callback') {
65
- // POST body (secure) 또는 GET query params (하위 호환) 모두 지원
66
- let params;
67
- if (req.method === 'POST') {
68
- const body = await collectBody(req);
69
- params = parseFormBody(body);
70
- }
71
- else {
72
- params = url.searchParams;
73
- }
66
+ if (url.pathname === '/callback' && req.method === 'POST') {
67
+ const body = await collectBody(req);
68
+ const params = new URLSearchParams(body);
74
69
  const token = params.get('token');
75
70
  const refresh_token = params.get('refresh_token') ?? undefined;
76
71
  const expires_at_raw = params.get('expires_at');
77
72
  const expires_at = expires_at_raw ? Number(expires_at_raw) : undefined;
78
73
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
79
74
  res.end(SUCCESS_HTML);
75
+ clearTimeout(timeout);
80
76
  server.close();
81
77
  if (token) {
82
78
  resolve({ token, refresh_token, expires_at });
@@ -91,11 +87,6 @@ function waitForToken(port) {
91
87
  }
92
88
  });
93
89
  server.listen(port, '127.0.0.1');
94
- // Timeout after 5 minutes
95
- setTimeout(() => {
96
- server.close();
97
- reject(new Error('로그인 시간이 초과되었습니다 (5분)'));
98
- }, 5 * 60 * 1000);
99
90
  });
100
91
  }
101
92
  function findAvailablePort() {
@@ -210,6 +201,31 @@ async function selectProvider(json) {
210
201
  return 'email';
211
202
  return 'github';
212
203
  }
204
+ /**
205
+ * 대화형 로그인 플로우 실행 (auto-login에서 호출).
206
+ * 로그인 성공 시 토큰 저장, 실패 시 throw.
207
+ */
208
+ async function runLogin() {
209
+ (0, config_js_1.ensureGlobalRelayDir)();
210
+ const provider = await selectProvider(false);
211
+ let loginResult;
212
+ if (provider === 'email') {
213
+ loginResult = await loginWithEmail(false);
214
+ }
215
+ else if (provider === 'kakao') {
216
+ loginResult = await loginWithBrowser('kakao', false);
217
+ }
218
+ else {
219
+ loginResult = await loginWithBrowser('github', false);
220
+ }
221
+ await verifyToken(loginResult.token);
222
+ (0, config_js_1.saveTokenData)({
223
+ access_token: loginResult.token,
224
+ ...(loginResult.refresh_token ? { refresh_token: loginResult.refresh_token } : {}),
225
+ ...(loginResult.expires_at ? { expires_at: loginResult.expires_at } : {}),
226
+ });
227
+ console.log(`\x1b[32m✓ 로그인 완료\x1b[0m`);
228
+ }
213
229
  function registerLogin(program) {
214
230
  program
215
231
  .command('login')
@@ -481,6 +481,58 @@ function registerPublish(program) {
481
481
  }));
482
482
  process.exit(1);
483
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
+ }
484
536
  // Profile hint
485
537
  if (isTTY) {
486
538
  console.error('💡 프로필에 연락처를 설정하면 설치 시 명함이 전달됩니다: www.relayax.com/dashboard/profile');
@@ -594,6 +646,19 @@ function registerPublish(program) {
594
646
  console.log(` \x1b[90m└${'─'.repeat(44)}┘\x1b[0m`);
595
647
  console.log(`\n \x1b[90m명함 수정: \x1b[36mwww.relayax.com/dashboard/profile\x1b[0m`);
596
648
  }
649
+ // Show shareable onboarding guide as a plain copyable block
650
+ if (isTTY) {
651
+ const guideLines = [
652
+ 'npm install -g relayax-cli',
653
+ 'relay login',
654
+ `relay install ${result.slug}`,
655
+ ];
656
+ console.log(`\n \x1b[90m공유용 온보딩 가이드:\x1b[0m\n`);
657
+ console.log('```');
658
+ guideLines.forEach((line) => console.log(line));
659
+ console.log('```');
660
+ console.log(`\n \x1b[90m위 블록을 복사하여 팀원에게 공유하세요\x1b[0m`);
661
+ }
597
662
  }
598
663
  }
599
664
  catch (err) {
@@ -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
+ }