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.
- package/dist/commands/access.d.ts +2 -0
- package/dist/commands/access.js +90 -0
- package/dist/commands/create.js +70 -22
- package/dist/commands/follow.js +2 -2
- package/dist/commands/init.js +18 -3
- package/dist/commands/install.js +100 -20
- package/dist/commands/join.js +5 -3
- package/dist/commands/list.js +2 -2
- package/dist/commands/login.js +1 -1
- package/dist/commands/package.d.ts +2 -0
- package/dist/commands/package.js +287 -0
- package/dist/commands/publish.js +137 -62
- package/dist/commands/search.js +1 -1
- package/dist/commands/spaces.d.ts +1 -1
- package/dist/commands/spaces.js +9 -17
- package/dist/commands/update.js +1 -1
- package/dist/index.js +4 -0
- package/dist/lib/command-adapter.js +207 -72
- package/dist/lib/guide.d.ts +13 -5
- package/dist/lib/guide.js +142 -50
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/commands/create.js
CHANGED
|
@@ -24,7 +24,10 @@ function registerCreate(program) {
|
|
|
24
24
|
program
|
|
25
25
|
.command('create <name>')
|
|
26
26
|
.description('새 에이전트 팀 프로젝트를 생성합니다')
|
|
27
|
-
.
|
|
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 (
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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 });
|
package/dist/commands/follow.js
CHANGED
|
@@ -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}`);
|
package/dist/commands/init.js
CHANGED
|
@@ -121,16 +121,31 @@ function registerInit(program) {
|
|
|
121
121
|
program
|
|
122
122
|
.command('init')
|
|
123
123
|
.description('에이전트 CLI에 relay 슬래시 커맨드를 설치합니다')
|
|
124
|
-
.option('--tools <tools>', '설치할 에이전트 CLI 지정 (
|
|
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, --
|
|
129
|
-
const autoMode = opts.auto === true ||
|
|
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 = [];
|
package/dist/commands/install.js
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
|
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: `가입 신청이
|
|
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
|
-
|
|
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 {
|
package/dist/commands/join.js
CHANGED
|
@@ -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`);
|
package/dist/commands/list.js
CHANGED
|
@@ -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`);
|
package/dist/commands/login.js
CHANGED
|
@@ -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`);
|