relayax-cli 0.2.39 → 0.2.41

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.
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerAccess(program: Command): void;
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerAccess = registerAccess;
4
+ const commander_1 = require("commander");
5
+ const config_js_1 = require("../lib/config.js");
6
+ async function claimAccess(slug, code) {
7
+ const token = await (0, config_js_1.getValidToken)();
8
+ if (!token) {
9
+ throw new Error('LOGIN_REQUIRED');
10
+ }
11
+ const res = await fetch(`${config_js_1.API_URL}/api/teams/${slug}/claim-access`, {
12
+ method: 'POST',
13
+ headers: {
14
+ 'Content-Type': 'application/json',
15
+ Authorization: `Bearer ${token}`,
16
+ },
17
+ body: JSON.stringify({ code }),
18
+ signal: AbortSignal.timeout(10000),
19
+ });
20
+ const body = (await res.json().catch(() => ({})));
21
+ if (!res.ok) {
22
+ const errCode = body.error ?? String(res.status);
23
+ switch (errCode) {
24
+ case 'INVALID_LINK':
25
+ throw new Error('초대 링크가 유효하지 않거나 만료되었습니다.');
26
+ case 'NOT_FOUND':
27
+ throw new Error('팀을 찾을 수 없습니다.');
28
+ case 'UNAUTHORIZED':
29
+ throw new Error('LOGIN_REQUIRED');
30
+ default:
31
+ throw new Error(body.message ?? `접근 권한 요청 실패 (${res.status})`);
32
+ }
33
+ }
34
+ return body;
35
+ }
36
+ function registerAccess(program) {
37
+ program
38
+ .command('access <slug>')
39
+ .description('초대 코드로 팀에 접근 권한을 얻고 바로 설치합니다')
40
+ .requiredOption('--code <code>', '팀 초대 코드')
41
+ .action(async (slug, opts) => {
42
+ const json = program.opts().json ?? false;
43
+ try {
44
+ const result = await claimAccess(slug, opts.code);
45
+ if (!result.success || !result.team) {
46
+ throw new Error('서버 응답이 올바르지 않습니다.');
47
+ }
48
+ const teamSlug = result.team.slug;
49
+ if (json) {
50
+ console.log(JSON.stringify({ status: 'ok', team: result.team }));
51
+ }
52
+ else {
53
+ console.log(`\x1b[32m접근 권한이 부여되었습니다: ${result.team.name}\x1b[0m`);
54
+ console.log(`\x1b[33m팀을 설치합니다: relay install ${teamSlug}\x1b[0m\n`);
55
+ }
56
+ // Automatically install the team
57
+ const { registerInstall } = await import('./install.js');
58
+ const subProgram = new commander_1.Command();
59
+ subProgram.option('--json', '구조화된 JSON 출력');
60
+ if (json)
61
+ subProgram.setOptionValue('json', true);
62
+ registerInstall(subProgram);
63
+ await subProgram.parseAsync(['node', 'relay', 'install', teamSlug]);
64
+ }
65
+ catch (err) {
66
+ const message = err instanceof Error ? err.message : String(err);
67
+ if (message === 'LOGIN_REQUIRED') {
68
+ if (json) {
69
+ console.error(JSON.stringify({
70
+ error: 'LOGIN_REQUIRED',
71
+ message: '로그인이 필요합니다. relay login을 먼저 실행하세요.',
72
+ fix: 'relay login 실행 후 재시도하세요.',
73
+ }));
74
+ }
75
+ else {
76
+ console.error('\x1b[31m오류: 로그인이 필요합니다.\x1b[0m');
77
+ console.error(' relay login을 먼저 실행하세요.');
78
+ }
79
+ process.exit(1);
80
+ }
81
+ if (json) {
82
+ console.error(JSON.stringify({ error: 'ACCESS_FAILED', message, fix: '접근 링크 코드를 확인하거나 팀 제작자에게 문의하세요.' }));
83
+ }
84
+ else {
85
+ console.error(`\x1b[31m오류: ${message}\x1b[0m`);
86
+ }
87
+ process.exit(1);
88
+ }
89
+ });
90
+ }
@@ -24,7 +24,10 @@ function registerCreate(program) {
24
24
  program
25
25
  .command('create <name>')
26
26
  .description('새 에이전트 팀 프로젝트를 생성합니다')
27
- .action(async (name) => {
27
+ .option('--description <desc>', '팀 설명')
28
+ .option('--tags <tags>', '태그 (쉼표 구분)')
29
+ .option('--visibility <visibility>', '공개 범위 (public, gated, private)')
30
+ .action(async (name, opts) => {
28
31
  const json = program.opts().json ?? false;
29
32
  const projectPath = process.cwd();
30
33
  const relayDir = path_1.default.join(projectPath, '.relay');
@@ -33,7 +36,7 @@ function registerCreate(program) {
33
36
  // 1. .relay/relay.yaml 이미 존재하면 에러
34
37
  if (fs_1.default.existsSync(relayYamlPath)) {
35
38
  if (json) {
36
- console.error(JSON.stringify({ error: 'ALREADY_EXISTS', message: '.relay/relay.yaml이 이미 존재합니다.' }));
39
+ console.error(JSON.stringify({ error: 'ALREADY_EXISTS', message: '.relay/relay.yaml이 이미 존재합니다.', fix: '기존 .relay/relay.yaml을 확인하세요. 새로 시작하려면 삭제 후 재시도.' }));
37
40
  }
38
41
  else {
39
42
  console.error('.relay/relay.yaml이 이미 존재합니다. 기존 팀 프로젝트에서는 `relay init`을 사용하세요.');
@@ -42,28 +45,73 @@ function registerCreate(program) {
42
45
  }
43
46
  // 2. 메타데이터 수집
44
47
  const defaultSlug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
45
- let description = '';
46
- let tags = [];
47
- let visibility = 'public';
48
- if (isTTY) {
48
+ let description = opts.description ?? '';
49
+ let tags = opts.tags ? opts.tags.split(',').map((t) => t.trim()).filter(Boolean) : [];
50
+ let visibility = opts.visibility ?? 'public';
51
+ if (json) {
52
+ // --json 모드: 필수 값 부족 시 에러 반환 (프롬프트 없음)
53
+ if (!opts.description) {
54
+ console.error(JSON.stringify({
55
+ error: 'MISSING_FIELD',
56
+ message: '팀 설명이 필요합니다.',
57
+ fix: `relay create ${name} --description <설명> --json`,
58
+ field: 'description',
59
+ }));
60
+ process.exit(1);
61
+ }
62
+ if (!opts.visibility) {
63
+ console.error(JSON.stringify({
64
+ error: 'MISSING_VISIBILITY',
65
+ message: '공개 범위를 선택하세요.',
66
+ fix: `relay create ${name} --description "${description}" --visibility <visibility> --json`,
67
+ options: [
68
+ { value: 'public', label: '공개 — 누구나 설치' },
69
+ { value: 'gated', label: '링크 공유 — 접근 링크가 있는 사람만' },
70
+ { value: 'private', label: '비공개 — Space 멤버만' },
71
+ ],
72
+ }));
73
+ process.exit(1);
74
+ }
75
+ if (!['public', 'gated', 'private'].includes(opts.visibility)) {
76
+ console.error(JSON.stringify({
77
+ error: 'INVALID_FIELD',
78
+ message: `유효하지 않은 visibility 값: ${opts.visibility}`,
79
+ fix: `visibility는 public, gated, private 중 하나여야 합니다.`,
80
+ options: [
81
+ { value: 'public', label: '공개' },
82
+ { value: 'gated', label: '링크 공유' },
83
+ { value: 'private', label: '비공개' },
84
+ ],
85
+ }));
86
+ process.exit(1);
87
+ }
88
+ }
89
+ else if (isTTY) {
49
90
  const { input: promptInput, select: promptSelect } = await import('@inquirer/prompts');
50
91
  console.log(`\n \x1b[33m⚡\x1b[0m \x1b[1mrelay create\x1b[0m — 새 팀 프로젝트\n`);
51
- description = await promptInput({
52
- message: '팀 설명:',
53
- validate: (v) => v.trim().length > 0 ? true : '설명을 입력해주세요.',
54
- });
55
- const tagsRaw = await promptInput({
56
- message: '태그 (쉼표로 구분, 선택):',
57
- default: '',
58
- });
59
- tags = tagsRaw.split(',').map((t) => t.trim()).filter(Boolean);
60
- visibility = await promptSelect({
61
- message: '공개 범위:',
62
- choices: [
63
- { name: '공개', value: 'public' },
64
- { name: '비공개 (Space 멤버만)', value: 'private' },
65
- ],
66
- });
92
+ if (!description) {
93
+ description = await promptInput({
94
+ message: ' 설명:',
95
+ validate: (v) => v.trim().length > 0 ? true : '설명을 입력해주세요.',
96
+ });
97
+ }
98
+ if (!opts.tags) {
99
+ const tagsRaw = await promptInput({
100
+ message: '태그 (쉼표로 구분, 선택):',
101
+ default: '',
102
+ });
103
+ tags = tagsRaw.split(',').map((t) => t.trim()).filter(Boolean);
104
+ }
105
+ if (!opts.visibility) {
106
+ visibility = await promptSelect({
107
+ message: '공개 범위:',
108
+ choices: [
109
+ { name: '공개', value: 'public' },
110
+ { name: '링크 공유 (접근 링크 필요)', value: 'gated' },
111
+ { name: '비공개 (Space 멤버만)', value: 'private' },
112
+ ],
113
+ });
114
+ }
67
115
  }
68
116
  // 3. .relay/relay.yaml 생성
69
117
  fs_1.default.mkdirSync(relayDir, { recursive: true });
@@ -15,7 +15,7 @@ function registerFollow(program) {
15
15
  if (!token) {
16
16
  const msg = '로그인이 필요합니다. `relay login`을 먼저 실행하세요.';
17
17
  if (json) {
18
- console.log(JSON.stringify({ error: 'NO_TOKEN', message: msg }));
18
+ console.log(JSON.stringify({ error: 'NO_TOKEN', message: msg, fix: 'relay login 실행 후 재시도하세요.' }));
19
19
  }
20
20
  else {
21
21
  console.error(msg);
@@ -34,7 +34,7 @@ function registerFollow(program) {
34
34
  catch (err) {
35
35
  const message = err instanceof Error ? err.message : String(err);
36
36
  if (json) {
37
- console.log(JSON.stringify({ error: 'FOLLOW_FAILED', message }));
37
+ console.log(JSON.stringify({ error: 'FOLLOW_FAILED', message, fix: 'username을 확인하거나 잠시 후 재시도하세요.' }));
38
38
  }
39
39
  else {
40
40
  console.error(`팔로우 실패: ${message}`);
@@ -121,16 +121,31 @@ function registerInit(program) {
121
121
  program
122
122
  .command('init')
123
123
  .description('에이전트 CLI에 relay 슬래시 커맨드를 설치합니다')
124
- .option('--tools <tools>', '설치할 에이전트 CLI 지정 (all 또는 쉼표 구분)')
124
+ .option('--tools <tools>', '설치할 에이전트 CLI 지정 (쉼표 구분)')
125
+ .option('--all', '감지된 모든 에이전트 CLI에 설치')
125
126
  .option('--auto', '대화형 프롬프트 없이 자동으로 모든 감지된 CLI에 설치')
126
127
  .action(async (opts) => {
127
128
  const json = program.opts().json ?? false;
128
- // auto mode: --auto flag, --json flag, or stdin is not a TTY
129
- const autoMode = opts.auto === true || json || !process.stdin.isTTY;
129
+ // auto mode: --auto flag, --all flag, or stdin is not a TTY (but NOT --json alone)
130
+ const autoMode = opts.auto === true || opts.all === true || !process.stdin.isTTY;
130
131
  const projectPath = process.cwd();
131
132
  const detected = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
132
133
  const detectedIds = new Set(detected.map((t) => t.value));
133
134
  const isBuilder = isTeamProject(projectPath);
135
+ // ── 0. --json 모드에서 --tools/--all 없으면 MISSING_TOOLS 에러 ──
136
+ if (json && !opts.tools && !opts.all && !opts.auto) {
137
+ const detectedOptions = detected.map((t) => ({ value: t.value, label: t.name }));
138
+ if (detectedOptions.length === 0) {
139
+ detectedOptions.push(...ai_tools_js_1.AI_TOOLS.slice(0, 5).map((t) => ({ value: t.value, label: t.name })));
140
+ }
141
+ console.error(JSON.stringify({
142
+ error: 'MISSING_TOOLS',
143
+ message: '설치할 에이전트 CLI를 선택하세요.',
144
+ fix: `relay init --tools <도구1,도구2> --json 또는 relay init --all --json`,
145
+ options: detectedOptions,
146
+ }));
147
+ process.exit(1);
148
+ }
134
149
  // ── 1. 글로벌 User 커맨드 설치 ──
135
150
  let globalStatus = 'already';
136
151
  let globalTools = [];
@@ -15,18 +15,21 @@ const preamble_js_1 = require("../lib/preamble.js");
15
15
  const join_js_1 = require("./join.js");
16
16
  const init_js_1 = require("./init.js");
17
17
  /**
18
- * slugInput이 "@spaces/{spaceSlug}/{teamSlug}" 형식이면 파싱해 반환.
18
+ * slugInput이 "@spaces/{spaceSlug}/{teamSlug}" 또는 "@{spaceSlug}/{teamSlug}" 형식이면 파싱해 반환.
19
19
  * 아니면 null.
20
20
  */
21
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
- };
22
+ // @spaces/{spaceSlug}/{teamSlug} 형식 (기존)
23
+ const m1 = slugInput.match(/^@spaces\/([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)$/);
24
+ if (m1) {
25
+ return { spaceSlug: m1[1], rawTeamSlug: m1[2], teamSlug: `@${m1[1]}/${m1[2]}` };
26
+ }
27
+ // @{spaceSlug}/{teamSlug} 형식 (신규)
28
+ const m2 = slugInput.match(/^@([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)$/);
29
+ if (m2) {
30
+ return { spaceSlug: m2[1], rawTeamSlug: m2[2], teamSlug: `@${m2[1]}/${m2[2]}` };
31
+ }
32
+ return null;
30
33
  }
31
34
  function registerInstall(program) {
32
35
  program
@@ -69,20 +72,29 @@ function registerInstall(program) {
69
72
  let team;
70
73
  let slug;
71
74
  let parsed;
75
+ // Whether the spaceTarget was matched via the ambiguous @{slug}/{team} pattern
76
+ // (i.e. NOT the explicit @spaces/... prefix). Used for 404 fallback below.
77
+ const isAmbiguousSpaceTarget = spaceTarget !== null && !slugInput.startsWith('@spaces/');
72
78
  if (spaceTarget) {
73
79
  // Space 팀: POST /api/spaces/{spaceSlug}/teams/{teamSlug}/install
74
80
  // This verifies membership, increments install count, and returns metadata.
81
+ let usedSpacePath = true;
75
82
  try {
76
83
  team = await (0, api_js_1.installSpaceTeam)(spaceTarget.spaceSlug, spaceTarget.rawTeamSlug);
77
84
  }
78
85
  catch (fetchErr) {
79
86
  const fetchMsg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
80
- if (fetchMsg.includes('403')) {
87
+ if (fetchMsg.includes('404') && isAmbiguousSpaceTarget) {
88
+ // Space not found — @{owner}/{team} is actually a normal registry slug, fall back
89
+ usedSpacePath = false;
90
+ }
91
+ else if (fetchMsg.includes('403')) {
81
92
  if (json) {
82
93
  console.error(JSON.stringify({
83
94
  error: 'SPACE_ONLY',
84
95
  message: '이 팀은 Space 멤버만 설치 가능합니다.',
85
96
  spaceSlug: spaceTarget.spaceSlug,
97
+ fix: `relay join ${spaceTarget.spaceSlug} --code <초대코드>로 Space에 가입한 후 재시도하세요.`,
86
98
  }));
87
99
  }
88
100
  else {
@@ -91,11 +103,22 @@ function registerInstall(program) {
91
103
  }
92
104
  process.exit(1);
93
105
  }
94
- throw fetchErr;
106
+ else {
107
+ throw fetchErr;
108
+ }
109
+ }
110
+ if (!usedSpacePath) {
111
+ // Fallback: treat as normal registry install
112
+ parsed = await (0, slug_js_1.resolveSlug)(slugInput);
113
+ slug = parsed.full;
114
+ team = await (0, api_js_1.fetchTeamInfo)(slug);
115
+ }
116
+ else {
117
+ // slug from server is "@spaces/{spaceSlug}/{teamSlug}" — derive local path parts
118
+ // team is guaranteed assigned by installSpaceTeam above (usedSpacePath === true)
119
+ parsed = { owner: spaceTarget.spaceSlug, name: spaceTarget.rawTeamSlug, full: team.slug };
120
+ slug = team.slug;
95
121
  }
96
- // slug from server is "@spaces/{spaceSlug}/{teamSlug}" — derive local path parts
97
- parsed = { owner: spaceTarget.spaceSlug, name: spaceTarget.rawTeamSlug, full: team.slug };
98
- slug = team.slug;
99
122
  }
100
123
  else {
101
124
  // Normal registry install
@@ -107,15 +130,44 @@ function registerInstall(program) {
107
130
  catch (fetchErr) {
108
131
  const fetchMsg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
109
132
  if (fetchMsg.includes('403')) {
110
- // Parse join_policy and membership_status from error body if available
133
+ // Parse error body for join_policy, membership_status, visibility, purchase_info
111
134
  let joinPolicy;
112
135
  let membershipStatus;
136
+ let errorVisibility;
137
+ let purchaseInfo;
113
138
  try {
114
139
  const errBody = JSON.parse(fetchMsg.replace(/^.*?(\{)/, '{'));
115
140
  joinPolicy = typeof errBody.join_policy === 'string' ? errBody.join_policy : undefined;
116
141
  membershipStatus = typeof errBody.membership_status === 'string' ? errBody.membership_status : undefined;
142
+ errorVisibility = typeof errBody.visibility === 'string' ? errBody.visibility : undefined;
143
+ if (errBody.purchase_info && typeof errBody.purchase_info === 'object') {
144
+ purchaseInfo = errBody.purchase_info;
145
+ }
117
146
  }
118
147
  catch { /* ignore parse errors */ }
148
+ // Gated team: show purchase info + relay access hint
149
+ if (errorVisibility === 'gated' || purchaseInfo) {
150
+ if (json) {
151
+ console.error(JSON.stringify({
152
+ error: 'GATED_ACCESS_REQUIRED',
153
+ message: '이 팀은 접근 권한이 필요합니다.',
154
+ slug,
155
+ purchase_info: purchaseInfo ?? null,
156
+ fix: '접근 링크 코드가 있으면: relay access <slug> --code <코드>',
157
+ }));
158
+ }
159
+ else {
160
+ console.error('\x1b[31m🔒 이 팀은 접근 권한이 필요합니다.\x1b[0m');
161
+ if (purchaseInfo?.message) {
162
+ console.error(`\n \x1b[36m${purchaseInfo.message}\x1b[0m`);
163
+ }
164
+ if (purchaseInfo?.url) {
165
+ console.error(` \x1b[36m${purchaseInfo.url}\x1b[0m`);
166
+ }
167
+ console.error(`\n\x1b[33m접근 링크 코드가 있으면: relay access ${slugInput} --code <코드>\x1b[0m`);
168
+ }
169
+ process.exit(1);
170
+ }
119
171
  if (joinPolicy === 'auto') {
120
172
  // Auto-join the Space then retry install
121
173
  if (!json) {
@@ -132,7 +184,7 @@ function registerInstall(program) {
132
184
  catch (joinErr) {
133
185
  const joinMsg = joinErr instanceof Error ? joinErr.message : String(joinErr);
134
186
  if (json) {
135
- console.error(JSON.stringify({ error: 'JOIN_FAILED', message: joinMsg, slug }));
187
+ console.error(JSON.stringify({ error: 'JOIN_FAILED', message: joinMsg, slug, fix: 'Space slug와 초대 코드를 확인 후 재시도하세요.' }));
136
188
  }
137
189
  else {
138
190
  console.error(`\x1b[31mSpace 가입 실패: ${joinMsg}\x1b[0m`);
@@ -145,9 +197,10 @@ function registerInstall(program) {
145
197
  if (json) {
146
198
  console.error(JSON.stringify({
147
199
  error: 'APPROVAL_REQUIRED',
148
- message: `가입 신청이 필요합니다. \`relay join @${spaceSlug}\`로 가입하세요.`,
200
+ message: `가입 신청이 필요합니다.`,
149
201
  slug,
150
202
  spaceSlug,
203
+ fix: `relay join @${spaceSlug} --code <초대코드>로 Space에 가입한 후 재시도하세요. 초대 코드는 Space 관리자에게 요청하세요.`,
151
204
  }));
152
205
  }
153
206
  else {
@@ -162,6 +215,7 @@ function registerInstall(program) {
162
215
  error: 'NO_ACCESS',
163
216
  message: '이 팀에 대한 접근 권한이 없습니다.',
164
217
  slug,
218
+ fix: '이 팀의 접근 링크 코드가 있으면 `relay access ' + slugInput + ' --code <코드>`로 접근 권한을 얻으세요. 없으면 팀 제작자에게 문의하세요.',
165
219
  }));
166
220
  }
167
221
  else {
@@ -175,6 +229,7 @@ function registerInstall(program) {
175
229
  error: 'SPACE_ONLY',
176
230
  message: '이 팀은 Space 멤버만 설치 가능합니다.',
177
231
  slug,
232
+ fix: 'Space 관리자에게 초대 코드를 요청한 후 `relay join <space-slug> --code <코드>`로 가입하세요.',
178
233
  }));
179
234
  }
180
235
  else {
@@ -189,6 +244,8 @@ function registerInstall(program) {
189
244
  }
190
245
  }
191
246
  }
247
+ if (!team)
248
+ throw new Error('팀 정보를 가져오지 못했습니다.');
192
249
  const teamDir = path_1.default.join(projectPath, '.relay', 'teams', parsed.owner, parsed.name);
193
250
  // 2. Visibility check + auto-login
194
251
  const visibility = team.visibility ?? 'public';
@@ -210,6 +267,7 @@ function registerInstall(program) {
210
267
  visibility,
211
268
  slug,
212
269
  message: '이 팀은 로그인이 필요합니다. relay login을 먼저 실행하세요.',
270
+ fix: 'relay login 실행 후 재시도하세요.',
213
271
  }));
214
272
  }
215
273
  else {
@@ -219,8 +277,30 @@ function registerInstall(program) {
219
277
  }
220
278
  }
221
279
  }
222
- // 3. Download package
223
- const tarPath = await (0, storage_js_1.downloadPackage)(team.package_url, tempDir);
280
+ // 3. Download package (retry once if signed URL expired)
281
+ let tarPath;
282
+ try {
283
+ tarPath = await (0, storage_js_1.downloadPackage)(team.package_url, tempDir);
284
+ }
285
+ catch (dlErr) {
286
+ const dlMsg = dlErr instanceof Error ? dlErr.message : String(dlErr);
287
+ if (dlMsg.includes('403') || dlMsg.includes('expired')) {
288
+ // Signed URL expired — re-fetch team info for new URL and retry
289
+ if (!json) {
290
+ console.error('\x1b[33m⚙ 다운로드 URL 만료, 재시도 중...\x1b[0m');
291
+ }
292
+ if (spaceTarget) {
293
+ team = await (0, api_js_1.installSpaceTeam)(spaceTarget.spaceSlug, spaceTarget.rawTeamSlug);
294
+ }
295
+ else {
296
+ team = await (0, api_js_1.fetchTeamInfo)(slug);
297
+ }
298
+ tarPath = await (0, storage_js_1.downloadPackage)(team.package_url, tempDir);
299
+ }
300
+ else {
301
+ throw dlErr;
302
+ }
303
+ }
224
304
  // 4. Extract to .relay/teams/<slug>/
225
305
  if (fs_1.default.existsSync(teamDir)) {
226
306
  fs_1.default.rmSync(teamDir, { recursive: true, force: true });
@@ -326,7 +406,7 @@ function registerInstall(program) {
326
406
  }
327
407
  catch (err) {
328
408
  const message = err instanceof Error ? err.message : String(err);
329
- console.error(JSON.stringify({ error: 'INSTALL_FAILED', message }));
409
+ console.error(JSON.stringify({ error: 'INSTALL_FAILED', message, fix: message }));
330
410
  process.exit(1);
331
411
  }
332
412
  finally {
@@ -57,7 +57,7 @@ function registerJoin(program) {
57
57
  console.error('\x1b[33m⚠ relay init이 실행되지 않았습니다. 먼저 relay init을 실행하세요.\x1b[0m');
58
58
  }
59
59
  else {
60
- console.error(JSON.stringify({ error: 'NOT_INITIALIZED', message: 'relay init을 먼저 실행하세요.' }));
60
+ console.error(JSON.stringify({ error: 'NOT_INITIALIZED', message: 'relay init을 먼저 실행하세요.', fix: 'relay init 실행하세요.' }));
61
61
  }
62
62
  process.exit(1);
63
63
  }
@@ -92,7 +92,8 @@ function registerJoin(program) {
92
92
  const desc = t.description ? ` \x1b[90m— ${t.description}\x1b[0m` : '';
93
93
  console.log(` \x1b[36m•\x1b[0m \x1b[1m${t.slug}\x1b[0m${desc}`);
94
94
  }
95
- console.log(`\n\x1b[33m💡 설치: relay install @spaces/${slug}/<팀슬러그>\x1b[0m`);
95
+ console.log(`\n\x1b[33m💡 전체 설치: relay install @spaces/${slug}/<팀슬러그>\x1b[0m`);
96
+ console.log(`\x1b[33m💡 가이드 URL 공유: https://relayax.com/api/spaces/${slug}/guide.md\x1b[0m`);
96
97
  }
97
98
  }
98
99
  }
@@ -117,6 +118,7 @@ function registerJoin(program) {
117
118
  console.error(JSON.stringify({
118
119
  error: 'LOGIN_REQUIRED',
119
120
  message: '로그인이 필요합니다. relay login 을 먼저 실행하세요.',
121
+ fix: 'relay login 실행 후 재시도하세요.',
120
122
  }));
121
123
  }
122
124
  else {
@@ -126,7 +128,7 @@ function registerJoin(program) {
126
128
  process.exit(1);
127
129
  }
128
130
  if (json) {
129
- console.error(JSON.stringify({ error: 'JOIN_FAILED', message }));
131
+ console.error(JSON.stringify({ error: 'JOIN_FAILED', message, fix: 'Space slug와 초대 코드를 확인 후 재시도하세요.' }));
130
132
  }
131
133
  else {
132
134
  console.error(`\x1b[31m오류: ${message}\x1b[0m`);
@@ -29,7 +29,7 @@ function registerList(program) {
29
29
  const token = await (0, config_js_1.getValidToken)();
30
30
  if (!token) {
31
31
  if (json) {
32
- console.error(JSON.stringify({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다. relay login을 먼저 실행하세요.' }));
32
+ console.error(JSON.stringify({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다. relay login을 먼저 실행하세요.', fix: 'relay login 실행 후 재시도하세요.' }));
33
33
  }
34
34
  else {
35
35
  console.error('\x1b[31m오류: 로그인이 필요합니다.\x1b[0m');
@@ -59,7 +59,7 @@ function registerList(program) {
59
59
  catch (err) {
60
60
  const message = err instanceof Error ? err.message : String(err);
61
61
  if (json) {
62
- console.error(JSON.stringify({ error: 'FETCH_FAILED', message }));
62
+ console.error(JSON.stringify({ error: 'FETCH_FAILED', message, fix: '네트워크 연결을 확인하거나 잠시 후 재시도하세요.' }));
63
63
  }
64
64
  else {
65
65
  console.error(`\x1b[31m오류: ${message}\x1b[0m`);
@@ -148,7 +148,7 @@ function registerLogin(program) {
148
148
  catch (err) {
149
149
  const msg = err instanceof Error ? err.message : '로그인 실패';
150
150
  if (json) {
151
- console.error(JSON.stringify({ error: 'LOGIN_FAILED', message: msg }));
151
+ console.error(JSON.stringify({ error: 'LOGIN_FAILED', message: msg, fix: '브라우저에서 로그인을 완료하고 relay login을 재시도하세요.' }));
152
152
  }
153
153
  else {
154
154
  console.error(`\x1b[31m오류: ${msg}\x1b[0m`);
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerPackage(program: Command): void;