relayax-cli 0.2.38 → 0.2.40
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/publish.js +125 -62
- package/dist/commands/search.js +1 -1
- package/dist/commands/spaces.d.ts +2 -1
- package/dist/commands/spaces.js +9 -17
- package/dist/commands/update.js +1 -1
- package/dist/index.js +2 -0
- package/dist/lib/command-adapter.js +75 -38
- 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`);
|
package/dist/commands/publish.js
CHANGED
|
@@ -24,8 +24,9 @@ function parseRelayYaml(content) {
|
|
|
24
24
|
const requires = raw.requires;
|
|
25
25
|
const rawVisibility = String(raw.visibility ?? '');
|
|
26
26
|
const visibility = rawVisibility === 'private' ? 'private'
|
|
27
|
-
: rawVisibility === '
|
|
28
|
-
:
|
|
27
|
+
: rawVisibility === 'gated' ? 'gated'
|
|
28
|
+
: rawVisibility === 'public' ? 'public'
|
|
29
|
+
: undefined;
|
|
29
30
|
const rawType = String(raw.type ?? '');
|
|
30
31
|
const type = rawType === 'command' ? 'command'
|
|
31
32
|
: rawType === 'passive' ? 'passive'
|
|
@@ -281,6 +282,8 @@ function registerPublish(program) {
|
|
|
281
282
|
.command('publish')
|
|
282
283
|
.description('현재 팀 패키지를 Space에 배포합니다 (relay.yaml 필요)')
|
|
283
284
|
.option('--token <token>', '인증 토큰')
|
|
285
|
+
.option('--space <slug>', '배포할 Space 지정')
|
|
286
|
+
.option('--version <version>', '배포 버전 지정 (relay.yaml 업데이트)')
|
|
284
287
|
.action(async (opts) => {
|
|
285
288
|
const json = program.opts().json ?? false;
|
|
286
289
|
const teamDir = process.cwd();
|
|
@@ -317,6 +320,7 @@ function registerPublish(program) {
|
|
|
317
320
|
console.error(JSON.stringify({
|
|
318
321
|
error: 'NOT_INITIALIZED',
|
|
319
322
|
message: '.relay/relay.yaml이 없습니다. 먼저 `relay create`를 실행하세요.',
|
|
323
|
+
fix: 'relay create 또는 .relay/relay.yaml을 생성하세요.',
|
|
320
324
|
}));
|
|
321
325
|
process.exit(1);
|
|
322
326
|
}
|
|
@@ -345,12 +349,16 @@ function registerPublish(program) {
|
|
|
345
349
|
const visibility = await promptSelect({
|
|
346
350
|
message: '공개 범위:',
|
|
347
351
|
choices: [
|
|
348
|
-
{ name: '공개', value: 'public' },
|
|
349
|
-
{ name: '
|
|
352
|
+
{ name: '공개 — 누구나 설치', value: 'public' },
|
|
353
|
+
{ name: '링크 공유 — 접근 링크가 있는 사람만 설치', value: 'gated' },
|
|
354
|
+
{ name: '비공개 — Space 멤버만', value: 'private' },
|
|
350
355
|
],
|
|
351
356
|
});
|
|
352
357
|
console.error('\n\x1b[2m💡 프로필에 연락처를 설정하면 설치 시 명함이 전달됩니다: www.relayax.com/dashboard/profile\x1b[0m');
|
|
353
|
-
if (visibility === '
|
|
358
|
+
if (visibility === 'gated') {
|
|
359
|
+
console.error('\x1b[2m💡 링크 공유 팀은 웹 대시보드에서 접근 링크와 구매 안내를 설정하세요: www.relayax.com/dashboard\x1b[0m');
|
|
360
|
+
}
|
|
361
|
+
else if (visibility === 'private') {
|
|
354
362
|
console.error('\x1b[2m💡 비공개 팀은 Space를 통해 멤버를 관리하세요: www.relayax.com/dashboard/teams\x1b[0m');
|
|
355
363
|
}
|
|
356
364
|
console.error('');
|
|
@@ -377,11 +385,18 @@ function registerPublish(program) {
|
|
|
377
385
|
console.error(JSON.stringify({
|
|
378
386
|
error: 'INVALID_CONFIG',
|
|
379
387
|
message: 'relay.yaml에 name, slug, description이 필요합니다.',
|
|
388
|
+
fix: 'relay.yaml에 name, slug, description을 확인하세요.',
|
|
380
389
|
}));
|
|
381
390
|
process.exit(1);
|
|
382
391
|
}
|
|
383
|
-
// Version bump
|
|
384
|
-
if (
|
|
392
|
+
// Version bump: --version flag takes priority
|
|
393
|
+
if (opts.version) {
|
|
394
|
+
config.version = opts.version;
|
|
395
|
+
const yamlData = js_yaml_1.default.load(fs_1.default.readFileSync(relayYamlPath, 'utf-8'));
|
|
396
|
+
yamlData.version = opts.version;
|
|
397
|
+
fs_1.default.writeFileSync(relayYamlPath, js_yaml_1.default.dump(yamlData, { lineWidth: 120 }), 'utf-8');
|
|
398
|
+
}
|
|
399
|
+
else if (isTTY) {
|
|
385
400
|
const { select: promptVersion } = await import('@inquirer/prompts');
|
|
386
401
|
const [major, minor, patch] = config.version.split('.').map(Number);
|
|
387
402
|
const bumpPatch = `${major}.${minor}.${patch + 1}`;
|
|
@@ -415,6 +430,7 @@ function registerPublish(program) {
|
|
|
415
430
|
console.error(JSON.stringify({
|
|
416
431
|
error: 'EMPTY_PACKAGE',
|
|
417
432
|
message: '.relay/ 안에 skills/, agents/, rules/, commands/ 중 하나 이상에 파일이 있어야 합니다.',
|
|
433
|
+
fix: '.relay/ 안에 skills/, agents/, rules/, commands/ 중 하나에 파일을 추가하세요.',
|
|
418
434
|
}));
|
|
419
435
|
process.exit(1);
|
|
420
436
|
}
|
|
@@ -424,71 +440,110 @@ function registerPublish(program) {
|
|
|
424
440
|
console.error(JSON.stringify({
|
|
425
441
|
error: 'NO_TOKEN',
|
|
426
442
|
message: '인증이 필요합니다. `relay login`을 먼저 실행하세요.',
|
|
443
|
+
fix: 'relay login 실행 후 재시도하세요.',
|
|
427
444
|
}));
|
|
428
445
|
process.exit(1);
|
|
429
446
|
}
|
|
430
447
|
// Fetch user's Spaces and select publish target
|
|
431
448
|
let selectedSpaceId;
|
|
432
|
-
let
|
|
449
|
+
let selectedSpaceSlug;
|
|
450
|
+
let selectedJoinPolicy;
|
|
433
451
|
try {
|
|
434
452
|
const { fetchMySpaces } = await import('./spaces.js');
|
|
435
453
|
const spaces = await fetchMySpaces(token);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
454
|
+
// --space flag: resolve Space by slug
|
|
455
|
+
if (opts.space) {
|
|
456
|
+
const matched = spaces.find((s) => s.slug === opts.space);
|
|
457
|
+
if (matched) {
|
|
458
|
+
selectedSpaceId = matched.id;
|
|
459
|
+
selectedSpaceSlug = matched.slug;
|
|
460
|
+
selectedJoinPolicy = matched.join_policy;
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
if (json) {
|
|
464
|
+
console.error(JSON.stringify({
|
|
465
|
+
error: 'INVALID_SPACE',
|
|
466
|
+
message: `Space '${opts.space}'를 찾을 수 없습니다.`,
|
|
467
|
+
fix: `사용 가능한 Space: ${spaces.map((s) => s.slug).join(', ')}`,
|
|
468
|
+
options: spaces.map((s) => ({ value: s.slug, label: `${s.name} (${s.slug})` })),
|
|
469
|
+
}));
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
console.error(`Space '${opts.space}'를 찾을 수 없습니다.`);
|
|
473
|
+
}
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
else if (isTTY) {
|
|
439
478
|
if (spaces.length === 0) {
|
|
440
479
|
// No spaces at all — publish without space_id
|
|
441
480
|
console.error('\x1b[33m⚠ 소속 Space가 없습니다. 개인 계정으로 배포합니다.\x1b[0m\n');
|
|
442
481
|
}
|
|
443
|
-
else if (spaces.length === 1
|
|
444
|
-
// Only
|
|
445
|
-
selectedSpaceId =
|
|
446
|
-
|
|
447
|
-
|
|
482
|
+
else if (spaces.length === 1) {
|
|
483
|
+
// Only one Space — auto-select regardless of type
|
|
484
|
+
selectedSpaceId = spaces[0].id;
|
|
485
|
+
selectedSpaceSlug = spaces[0].slug;
|
|
486
|
+
selectedJoinPolicy = spaces[0].join_policy;
|
|
487
|
+
console.error(`\x1b[2m Space: ${spaces[0].name} (${spaces[0].slug})\x1b[0m\n`);
|
|
448
488
|
}
|
|
449
489
|
else {
|
|
450
490
|
// Multiple spaces — prompt user
|
|
451
491
|
const { select: selectSpace } = await import('@inquirer/prompts');
|
|
452
|
-
const spaceChoices =
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
492
|
+
const spaceChoices = spaces.map((s) => ({
|
|
493
|
+
name: `${s.name} (${s.slug})`,
|
|
494
|
+
value: s.id,
|
|
495
|
+
slug: s.slug,
|
|
496
|
+
join_policy: s.join_policy,
|
|
497
|
+
}));
|
|
456
498
|
const chosenId = await selectSpace({
|
|
457
499
|
message: '어떤 Space에 배포할까요?',
|
|
458
500
|
choices: spaceChoices.map((c) => ({ name: c.name, value: c.value })),
|
|
459
501
|
});
|
|
460
502
|
const chosen = spaceChoices.find((c) => c.value === chosenId);
|
|
461
503
|
selectedSpaceId = chosenId;
|
|
462
|
-
|
|
504
|
+
selectedSpaceSlug = chosen?.slug;
|
|
505
|
+
selectedJoinPolicy = chosen?.join_policy;
|
|
463
506
|
const chosenLabel = chosen?.name ?? chosenId;
|
|
464
507
|
console.error(` → Space: ${chosenLabel}\n`);
|
|
465
508
|
}
|
|
466
509
|
}
|
|
467
|
-
else if (
|
|
468
|
-
|
|
469
|
-
|
|
510
|
+
else if (spaces.length > 1 && json) {
|
|
511
|
+
// --json 모드 + 여러 Space: 에이전트가 선택할 수 있도록 에러 반환
|
|
512
|
+
console.error(JSON.stringify({
|
|
513
|
+
error: 'MISSING_SPACE',
|
|
514
|
+
message: '배포할 Space를 선택하세요.',
|
|
515
|
+
fix: `relay publish --space <slug> --json`,
|
|
516
|
+
options: spaces.map((s) => ({ value: s.slug, label: `${s.name} (${s.slug})` })),
|
|
517
|
+
}));
|
|
518
|
+
process.exit(1);
|
|
519
|
+
}
|
|
520
|
+
else if (spaces.length > 0) {
|
|
521
|
+
selectedSpaceId = spaces[0].id;
|
|
522
|
+
selectedSpaceSlug = spaces[0].slug;
|
|
523
|
+
selectedJoinPolicy = spaces[0].join_policy;
|
|
470
524
|
}
|
|
471
525
|
}
|
|
472
526
|
catch {
|
|
473
527
|
// Space 조회 실패 시 무시하고 계속 진행
|
|
474
528
|
}
|
|
475
|
-
// Visibility default based on
|
|
476
|
-
const defaultVisibility =
|
|
477
|
-
const defaultVisLabel = defaultVisibility === 'public'
|
|
478
|
-
? '공개 (개인 Space 기본값)'
|
|
479
|
-
: '비공개 (팀 Space 기본값)';
|
|
529
|
+
// Visibility default based on join_policy: approval → private, otherwise → public
|
|
530
|
+
const defaultVisibility = selectedJoinPolicy === 'approval' ? 'private' : 'public';
|
|
480
531
|
// Visibility validation: must be explicitly set
|
|
481
532
|
if (!config.visibility) {
|
|
482
533
|
if (isTTY) {
|
|
483
534
|
const { select: promptSelect } = await import('@inquirer/prompts');
|
|
484
|
-
console.error(`\n\x1b[33m⚠ relay.yaml에 visibility가 설정되지 않았습니다.\x1b[0m (기본값: ${
|
|
535
|
+
console.error(`\n\x1b[33m⚠ relay.yaml에 visibility가 설정되지 않았습니다.\x1b[0m (기본값: ${defaultVisibility === 'public' ? '공개' : '비공개'})`);
|
|
485
536
|
config.visibility = await promptSelect({
|
|
486
537
|
message: '공개 범위를 선택하세요:',
|
|
487
538
|
choices: [
|
|
488
539
|
{
|
|
489
|
-
name: `공개 —
|
|
540
|
+
name: `공개 — 누구나 설치${defaultVisibility === 'public' ? ' ✓ 추천' : ''}`,
|
|
490
541
|
value: 'public',
|
|
491
542
|
},
|
|
543
|
+
{
|
|
544
|
+
name: '링크 공유 — 접근 링크가 있는 사람만 설치',
|
|
545
|
+
value: 'gated',
|
|
546
|
+
},
|
|
492
547
|
{
|
|
493
548
|
name: `비공개 — Space 멤버만 접근${defaultVisibility === 'private' ? ' ✓ 추천' : ''}`,
|
|
494
549
|
value: 'private',
|
|
@@ -505,7 +560,13 @@ function registerPublish(program) {
|
|
|
505
560
|
else {
|
|
506
561
|
console.error(JSON.stringify({
|
|
507
562
|
error: 'MISSING_VISIBILITY',
|
|
508
|
-
message: 'relay.yaml에 visibility
|
|
563
|
+
message: 'relay.yaml에 visibility를 설정해주세요.',
|
|
564
|
+
options: [
|
|
565
|
+
{ value: 'public', label: '공개 — 누구나 설치' },
|
|
566
|
+
{ value: 'gated', label: '링크 공유 — 접근 링크가 있는 사람만 설치' },
|
|
567
|
+
{ value: 'private', label: '비공개 — Space 멤버만 접근' },
|
|
568
|
+
],
|
|
569
|
+
fix: 'relay.yaml의 visibility 필드를 위 옵션 중 하나로 설정하세요.',
|
|
509
570
|
}));
|
|
510
571
|
process.exit(1);
|
|
511
572
|
}
|
|
@@ -513,23 +574,36 @@ function registerPublish(program) {
|
|
|
513
574
|
// Confirm visibility before publish (재배포 시 변경 기회 제공)
|
|
514
575
|
if (isTTY) {
|
|
515
576
|
const { select: promptConfirmVis } = await import('@inquirer/prompts');
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
:
|
|
519
|
-
|
|
520
|
-
|
|
577
|
+
const visLabelMap = {
|
|
578
|
+
public: '공개',
|
|
579
|
+
gated: '링크공유',
|
|
580
|
+
private: '비공개',
|
|
581
|
+
};
|
|
582
|
+
const currentVisLabel = visLabelMap[config.visibility ?? 'public'] ?? config.visibility;
|
|
583
|
+
const newVisibility = await promptConfirmVis({
|
|
584
|
+
message: `공개 범위: ${currentVisLabel} — 유지하거나 변경하세요`,
|
|
521
585
|
choices: [
|
|
522
|
-
{
|
|
523
|
-
|
|
586
|
+
{
|
|
587
|
+
name: `공개 — 누구나 설치${defaultVisibility === 'public' ? ' ✓ 추천' : ''}`,
|
|
588
|
+
value: 'public',
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
name: '링크공유 — 접근 링크가 있는 사람만 설치',
|
|
592
|
+
value: 'gated',
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
name: `비공개 — Space 멤버만 접근${defaultVisibility === 'private' ? ' ✓ 추천' : ''}`,
|
|
596
|
+
value: 'private',
|
|
597
|
+
},
|
|
524
598
|
],
|
|
599
|
+
default: config.visibility ?? defaultVisibility,
|
|
525
600
|
});
|
|
526
|
-
if (
|
|
527
|
-
config.visibility =
|
|
601
|
+
if (newVisibility !== config.visibility) {
|
|
602
|
+
config.visibility = newVisibility;
|
|
528
603
|
const yamlData = js_yaml_1.default.load(fs_1.default.readFileSync(relayYamlPath, 'utf-8'));
|
|
529
604
|
yamlData.visibility = config.visibility;
|
|
530
605
|
fs_1.default.writeFileSync(relayYamlPath, js_yaml_1.default.dump(yamlData, { lineWidth: 120 }), 'utf-8');
|
|
531
|
-
|
|
532
|
-
console.error(` → relay.yaml에 visibility: ${config.visibility} 저장됨 (${newLabel})\n`);
|
|
606
|
+
console.error(` → relay.yaml에 visibility: ${config.visibility} 저장됨 (${visLabelMap[config.visibility]})\n`);
|
|
533
607
|
}
|
|
534
608
|
}
|
|
535
609
|
// Profile hint
|
|
@@ -564,6 +638,7 @@ function registerPublish(program) {
|
|
|
564
638
|
agent_details: detectedAgents,
|
|
565
639
|
skill_details: detectedSkills,
|
|
566
640
|
...(selectedSpaceId ? { space_id: selectedSpaceId } : {}),
|
|
641
|
+
...(selectedSpaceSlug ? { space_slug: selectedSpaceSlug } : {}),
|
|
567
642
|
};
|
|
568
643
|
if (!json) {
|
|
569
644
|
console.error(`패키지 생성 중... (${config.name} v${config.version})`);
|
|
@@ -636,34 +711,22 @@ function registerPublish(program) {
|
|
|
636
711
|
}
|
|
637
712
|
// Show shareable onboarding guide as a plain copyable block
|
|
638
713
|
if (isTTY) {
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
'relay login',
|
|
642
|
-
`relay install ${result.slug}`,
|
|
643
|
-
];
|
|
644
|
-
// Type-based usage hint
|
|
645
|
-
const teamType = config.type ?? 'hybrid';
|
|
646
|
-
if (teamType === 'passive') {
|
|
647
|
-
guideLines.push('', '# 설치하면 자동 적용됩니다');
|
|
648
|
-
}
|
|
649
|
-
else if (detectedCommands.length > 0) {
|
|
650
|
-
const usageCmd = teamType === 'hybrid'
|
|
651
|
-
? `# 자동 적용 + /${detectedCommands[0].name} 으로 추가 기능`
|
|
652
|
-
: `# 사용법: /${detectedCommands[0].name}`;
|
|
653
|
-
guideLines.push('', usageCmd);
|
|
654
|
-
}
|
|
714
|
+
const detailSlug = result.slug.startsWith('@') ? result.slug.slice(1) : result.slug;
|
|
715
|
+
const guideUrl = `https://relayax.com/api/registry/${detailSlug}/guide.md`;
|
|
655
716
|
console.log(`\n \x1b[90m주변인에게 공유하세요:\x1b[0m\n`);
|
|
656
717
|
console.log('```');
|
|
657
|
-
|
|
718
|
+
console.log('아래 가이드를 읽고 그대로 따라해줘:');
|
|
719
|
+
console.log(guideUrl);
|
|
658
720
|
console.log('```');
|
|
659
|
-
|
|
721
|
+
console.log(`\n \x1b[90mCLI 설치된 사용자용 (짧은 버전):\x1b[0m`);
|
|
722
|
+
console.log(` /relay:relay-install ${result.slug}`);
|
|
660
723
|
console.log(`\n \x1b[90m상세페이지: \x1b[36mrelayax.com/@${detailSlug}\x1b[0m`);
|
|
661
724
|
}
|
|
662
725
|
}
|
|
663
726
|
}
|
|
664
727
|
catch (err) {
|
|
665
728
|
const message = err instanceof Error ? err.message : String(err);
|
|
666
|
-
console.error(JSON.stringify({ error: 'PUBLISH_FAILED', message }));
|
|
729
|
+
console.error(JSON.stringify({ error: 'PUBLISH_FAILED', message, fix: message }));
|
|
667
730
|
process.exit(1);
|
|
668
731
|
}
|
|
669
732
|
finally {
|
package/dist/commands/search.js
CHANGED
|
@@ -48,7 +48,7 @@ function registerSearch(program) {
|
|
|
48
48
|
}
|
|
49
49
|
catch (err) {
|
|
50
50
|
const message = err instanceof Error ? err.message : String(err);
|
|
51
|
-
console.error(JSON.stringify({ error: 'SEARCH_FAILED', message }));
|
|
51
|
+
console.error(JSON.stringify({ error: 'SEARCH_FAILED', message, fix: '검색어를 변경하거나 잠시 후 재시도하세요.' }));
|
|
52
52
|
process.exit(1);
|
|
53
53
|
}
|
|
54
54
|
});
|
package/dist/commands/spaces.js
CHANGED
|
@@ -22,7 +22,7 @@ function registerSpaces(program) {
|
|
|
22
22
|
const token = await (0, config_js_1.getValidToken)();
|
|
23
23
|
if (!token) {
|
|
24
24
|
if (json) {
|
|
25
|
-
console.error(JSON.stringify({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다. relay login을 먼저 실행하세요.' }));
|
|
25
|
+
console.error(JSON.stringify({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다. relay login을 먼저 실행하세요.', fix: 'relay login 실행 후 재시도하세요.' }));
|
|
26
26
|
}
|
|
27
27
|
else {
|
|
28
28
|
console.error('\x1b[31m오류: 로그인이 필요합니다.\x1b[0m');
|
|
@@ -36,29 +36,21 @@ function registerSpaces(program) {
|
|
|
36
36
|
console.log(JSON.stringify({ spaces }));
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
console.log(`\n\x1b[90m개인 스페이스:\x1b[0m`);
|
|
43
|
-
console.log(` \x1b[36m${personal.slug}\x1b[0m \x1b[1m${personal.name}\x1b[0m`);
|
|
39
|
+
if (spaces.length === 0) {
|
|
40
|
+
console.log('\nSpace가 없습니다.');
|
|
41
|
+
console.log('\x1b[33m💡 Space를 만들려면: www.relayax.com/spaces/new\x1b[0m');
|
|
44
42
|
}
|
|
45
|
-
|
|
46
|
-
console.log(`\n\x1b[1m내 Space\x1b[0m (${
|
|
47
|
-
for (const s of
|
|
43
|
+
else {
|
|
44
|
+
console.log(`\n\x1b[1m내 Space\x1b[0m (${spaces.length}개):\n`);
|
|
45
|
+
for (const s of spaces) {
|
|
48
46
|
const role = s.role === 'owner' ? '\x1b[33m소유자\x1b[0m'
|
|
49
|
-
: s.role === '
|
|
47
|
+
: s.role === 'builder' ? '\x1b[36m빌더\x1b[0m'
|
|
50
48
|
: '\x1b[90m멤버\x1b[0m';
|
|
51
49
|
const desc = s.description
|
|
52
50
|
? ` \x1b[90m${s.description.length > 40 ? s.description.slice(0, 40) + '...' : s.description}\x1b[0m`
|
|
53
51
|
: '';
|
|
54
52
|
console.log(` \x1b[36m${s.slug}\x1b[0m \x1b[1m${s.name}\x1b[0m ${role}${desc}`);
|
|
55
53
|
}
|
|
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
54
|
console.log(`\n\x1b[33m💡 Space 팀 목록: relay list --space <slug>\x1b[0m`);
|
|
63
55
|
console.log(`\x1b[33m💡 비공개 배포: relay.yaml에 visibility: private 설정 후 relay publish\x1b[0m`);
|
|
64
56
|
}
|
|
@@ -66,7 +58,7 @@ function registerSpaces(program) {
|
|
|
66
58
|
catch (err) {
|
|
67
59
|
const message = err instanceof Error ? err.message : String(err);
|
|
68
60
|
if (json) {
|
|
69
|
-
console.error(JSON.stringify({ error: 'FETCH_FAILED', message }));
|
|
61
|
+
console.error(JSON.stringify({ error: 'FETCH_FAILED', message, fix: '네트워크 연결을 확인하거나 잠시 후 재시도하세요.' }));
|
|
70
62
|
}
|
|
71
63
|
else {
|
|
72
64
|
console.error(`\x1b[31m오류: ${message}\x1b[0m`);
|
package/dist/commands/update.js
CHANGED
|
@@ -133,7 +133,7 @@ function registerUpdate(program) {
|
|
|
133
133
|
}
|
|
134
134
|
catch (err) {
|
|
135
135
|
const message = err instanceof Error ? err.message : String(err);
|
|
136
|
-
console.error(JSON.stringify({ error: 'UPDATE_FAILED', message }));
|
|
136
|
+
console.error(JSON.stringify({ error: 'UPDATE_FAILED', message, fix: 'npm update -g relayax-cli로 수동 업데이트하세요.' }));
|
|
137
137
|
process.exit(1);
|
|
138
138
|
}
|
|
139
139
|
finally {
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ const join_js_1 = require("./commands/join.js");
|
|
|
20
20
|
const spaces_js_1 = require("./commands/spaces.js");
|
|
21
21
|
const deploy_record_js_1 = require("./commands/deploy-record.js");
|
|
22
22
|
const ping_js_1 = require("./commands/ping.js");
|
|
23
|
+
const access_js_1 = require("./commands/access.js");
|
|
23
24
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
24
25
|
const pkg = require('../package.json');
|
|
25
26
|
const program = new commander_1.Command();
|
|
@@ -46,4 +47,5 @@ program
|
|
|
46
47
|
(0, spaces_js_1.registerSpaces)(program);
|
|
47
48
|
(0, deploy_record_js_1.registerDeployRecord)(program);
|
|
48
49
|
(0, ping_js_1.registerPing)(program);
|
|
50
|
+
(0, access_js_1.registerAccess)(program);
|
|
49
51
|
program.parse();
|
|
@@ -57,13 +57,46 @@ function getGlobalCommandPathForTool(skillsDir, commandId) {
|
|
|
57
57
|
function formatCommandFile(content) {
|
|
58
58
|
return `---\ndescription: ${content.description}\n---\n\n${content.body}\n`;
|
|
59
59
|
}
|
|
60
|
-
// ───
|
|
61
|
-
const
|
|
62
|
-
###
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
// ─── 에러 처리 가이드 (결정적 실행 + 비결정적 조율) ───
|
|
61
|
+
const ERROR_HANDLING_GUIDE = `
|
|
62
|
+
### 에러 처리 가이드
|
|
63
|
+
|
|
64
|
+
CLI 명령 실행 후 JSON 에러가 반환되면 아래 기준에 따라 처리합니다.
|
|
65
|
+
**원칙: "되돌릴 수 없는 영향이 있는가?"로 판단합니다.**
|
|
66
|
+
|
|
67
|
+
#### 1. 자동 해결 (사용자에게 물어보지 않음)
|
|
68
|
+
되돌릴 수 있고, 부작용 없는 에러:
|
|
69
|
+
|
|
70
|
+
| 에러 코드 | 행동 |
|
|
71
|
+
|-----------|------|
|
|
72
|
+
| \`LOGIN_REQUIRED\` / \`NO_TOKEN\` | \`relay login\` 실행 (timeout 300초, 브라우저 자동 열림) → 성공 후 원래 명령 재시도 |
|
|
73
|
+
| \`NOT_INITIALIZED\` | \`relay init --all --json\` 실행 → 원래 명령 재시도 |
|
|
74
|
+
| \`FETCH_FAILED\` | 3초 대기 후 원래 명령 재시도 (최대 2회). 2회 실패 시 사용자에게 안내 |
|
|
75
|
+
|
|
76
|
+
#### 2. 사용자에게 선택지 제시 (AskUserQuestion)
|
|
77
|
+
\`options\` 필드가 있는 에러:
|
|
78
|
+
|
|
79
|
+
| 에러 코드 | 행동 |
|
|
80
|
+
|-----------|------|
|
|
81
|
+
| \`MISSING_VISIBILITY\` | options의 label을 선택지로 AskUserQuestion 호출 |
|
|
82
|
+
| \`MISSING_FIELD\` | fix 안내 + 사용자에게 값 입력 요청 |
|
|
83
|
+
| \`MISSING_TOOLS\` | options의 감지된 도구 목록을 선택지로 AskUserQuestion 호출 |
|
|
84
|
+
| \`MISSING_SPACE\` | options의 Space 목록을 선택지로 AskUserQuestion 호출 |
|
|
85
|
+
|
|
86
|
+
사용자가 선택하면, 선택된 값을 CLI 플래그에 반영하여 명령을 재호출합니다.
|
|
87
|
+
|
|
88
|
+
#### 3. 사용자에게 안내 (되돌릴 수 없는 에러)
|
|
89
|
+
구매, 접근 권한, 보안 관련:
|
|
90
|
+
|
|
91
|
+
| 에러 코드 | 행동 |
|
|
92
|
+
|-----------|------|
|
|
93
|
+
| \`GATED_ACCESS_REQUIRED\` | purchase_info의 message/url 표시 → "접근 코드가 있으신가요?" AskUserQuestion |
|
|
94
|
+
| \`SPACE_ONLY\` | Space 가입 필요 안내 → "초대 코드가 있으신가요?" AskUserQuestion |
|
|
95
|
+
| \`APPROVAL_REQUIRED\` | 승인 대기 안내 |
|
|
96
|
+
| \`NO_ACCESS\` | 접근 방법 안내 |
|
|
97
|
+
|
|
98
|
+
#### 4. 그 외 에러
|
|
99
|
+
\`fix\` 필드의 메시지를 사용자에게 전달하고, 필요하면 다음 행동을 제안합니다.`;
|
|
67
100
|
// ─── 명함 표시 포맷 ───
|
|
68
101
|
const BUSINESS_CARD_FORMAT = `
|
|
69
102
|
### 빌더 명함 표시
|
|
@@ -128,7 +161,7 @@ slug가 직접 주어지면 (\`/relay-install @alice/doc-writer\`) 이 단계를
|
|
|
128
161
|
|
|
129
162
|
**AskUserQuestion 호출:**
|
|
130
163
|
- question: "어디서 팀을 찾을까요?"
|
|
131
|
-
- options: Space가 있으면 \`["<space1_name>
|
|
164
|
+
- options: Space가 있으면 \`["<space1_name>", "<space2_name>", ...]\`, 없으면 이 단계를 건너뛰고 바로 내 Space 탐색으로 진행
|
|
132
165
|
|
|
133
166
|
**응답 처리:**
|
|
134
167
|
- Space 이름 선택 → 1-2. Space 팀 탐색으로 진행
|
|
@@ -177,13 +210,24 @@ slug가 직접 주어지면 (\`/relay-install @alice/doc-writer\`) 이 단계를
|
|
|
177
210
|
|
|
178
211
|
#### 2-1. 패키지 다운로드
|
|
179
212
|
\`relay install <@space/team> --json\` 명령어를 실행합니다.
|
|
180
|
-
- 공개
|
|
181
|
-
-
|
|
213
|
+
- 공개 팀 (public): \`relay install <@space/team> --json\`
|
|
214
|
+
- 링크 공유 팀 (gated): \`relay install <slug> --json\`
|
|
215
|
+
- 접근 권한이 없으면 CLI가 **purchase_info** (구매 안내 메시지 + URL)를 표시합니다.
|
|
216
|
+
- 접근 링크 코드가 있으면: \`relay access <slug> --code <code>\` 로 접근 부여 + 자동 설치를 한번에 수행합니다.
|
|
217
|
+
- Space 멤버이면 접근 확인 없이 바로 설치됩니다.
|
|
218
|
+
- Space 팀 (비공개/private): \`relay install @<space-slug>/<team-slug> --json\`
|
|
182
219
|
- Space 가입이 필요하면: \`relay join <space-slug> --code <invite-code>\` 를 먼저 실행합니다.
|
|
183
220
|
- 또는 \`--join-code <code>\`로 가입+설치를 한번에 할 수 있습니다.
|
|
184
221
|
- CLI가 init과 login을 자동으로 처리합니다 (사용자가 별도 실행할 필요 없음).
|
|
185
222
|
- JSON 출력에서 \`install_path\` (패키지 경로)를 확인합니다.
|
|
186
223
|
|
|
224
|
+
**gated 팀 접근 거부 처리:**
|
|
225
|
+
- CLI가 403 + \`GATED_ACCESS_REQUIRED\` 에러를 반환하면:
|
|
226
|
+
1. purchase_info의 message와 url을 사용자에게 표시합니다.
|
|
227
|
+
2. "접근 링크 코드가 있으신가요?"라고 물어봅니다.
|
|
228
|
+
3. 코드가 있으면 \`relay access <slug> --code <code>\`를 실행합니다.
|
|
229
|
+
4. 코드가 없으면 purchase_info의 url로 구매 안내합니다.
|
|
230
|
+
|
|
187
231
|
#### 2-2. 배치 범위 선택 (추천 포함)
|
|
188
232
|
|
|
189
233
|
팀의 성격을 분석하여 글로벌/로컬 중 적합한 쪽을 추천합니다.
|
|
@@ -283,7 +327,7 @@ requires 섹션이 없거나 비어있으면 이 단계를 건너뜁니다.
|
|
|
283
327
|
\`\`\`
|
|
284
328
|
⚠️ 필수 요구사항이 충족되지 않았습니다. 팀 기능이 제한될 수 있습니다.
|
|
285
329
|
\`\`\`
|
|
286
|
-
${
|
|
330
|
+
${ERROR_HANDLING_GUIDE}
|
|
287
331
|
|
|
288
332
|
### Step 3. 완료 & 팔로우 제안
|
|
289
333
|
|
|
@@ -308,18 +352,12 @@ ${BUSINESS_CARD_FORMAT}
|
|
|
308
352
|
\`\`\`
|
|
309
353
|
주변인에게 공유하세요:
|
|
310
354
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
relay install {slug}
|
|
314
|
-
|
|
315
|
-
상세페이지: relayax.com/@{owner}/{team}
|
|
355
|
+
아래 가이드를 읽고 그대로 따라해줘:
|
|
356
|
+
https://relayax.com/api/registry/{owner}/{slug}/guide.md
|
|
316
357
|
\`\`\`
|
|
317
358
|
|
|
318
|
-
- \`{slug}\`가 \`@owner/team\` 형식이면
|
|
319
|
-
|
|
320
|
-
- type 기반 사용법도 포함:
|
|
321
|
-
- command/hybrid: \`# 사용법: /{첫번째 커맨드}\`
|
|
322
|
-
- passive: \`# 설치하면 자동 적용됩니다\`
|
|
359
|
+
- \`{slug}\`가 \`@owner/team\` 형식이면 \`{owner}\`과 \`{slug}\`를 분리하여 URL을 구성합니다.
|
|
360
|
+
- CLI가 이미 설치된 사용자를 위한 짧은 버전도 함께 표시합니다: \`/relay:relay-install {slug}\`
|
|
323
361
|
|
|
324
362
|
#### 3-4. 사용 제안
|
|
325
363
|
- "바로 사용해볼까요?" 제안
|
|
@@ -333,8 +371,8 @@ relay install {slug}
|
|
|
333
371
|
|
|
334
372
|
### 인터랙티브 모드 (/relay-install)
|
|
335
373
|
→ relay spaces --json 실행
|
|
336
|
-
→ AskUserQuestion: "어디서 팀을 찾을까요?" → ["
|
|
337
|
-
→ "
|
|
374
|
+
→ AskUserQuestion: "어디서 팀을 찾을까요?" → ["Alice's Space (alice)", "Acme Corp"]
|
|
375
|
+
→ "Alice's Space" 선택 → "어떤 팀을 찾고 계세요?"
|
|
338
376
|
→ relay search "문서" 실행 → 결과 리스트 표시
|
|
339
377
|
→ AskUserQuestion: "어떤 팀을 설치할까요?" → ["1", "2", "3", "다시 검색"]
|
|
340
378
|
→ "1" 선택 (@alice/doc-writer)
|
|
@@ -401,7 +439,6 @@ relay install {slug}
|
|
|
401
439
|
"slug": "my-space",
|
|
402
440
|
"name": "내 스페이스",
|
|
403
441
|
"description": "설명",
|
|
404
|
-
"is_personal": false,
|
|
405
442
|
"role": "owner"
|
|
406
443
|
}
|
|
407
444
|
]
|
|
@@ -409,9 +446,8 @@ relay install {slug}
|
|
|
409
446
|
\`\`\`
|
|
410
447
|
|
|
411
448
|
**표시:**
|
|
412
|
-
- \`
|
|
413
|
-
|
|
414
|
-
${LOGIN_JIT_GUIDE}
|
|
449
|
+
- \`role\`: owner → 소유자, builder → 빌더, member → 멤버
|
|
450
|
+
${ERROR_HANDLING_GUIDE}
|
|
415
451
|
- Spaces 조회 실패해도 설치된 팀 목록은 정상 표시합니다 (로컬 데이터).
|
|
416
452
|
|
|
417
453
|
### 3. Space 팀 목록 (옵션)
|
|
@@ -542,14 +578,14 @@ relay.yaml의 \`visibility\` 설정을 확인합니다.
|
|
|
542
578
|
#### 신규 배포 (visibility 미설정)
|
|
543
579
|
|
|
544
580
|
**AskUserQuestion 호출:**
|
|
545
|
-
- question: "
|
|
546
|
-
- options: \`["공개
|
|
581
|
+
- question: "공개 범위를 선택하세요"
|
|
582
|
+
- options: \`["공개 — 누구나 설치", "링크 공유 — 접근 링크가 있는 사람만 설치", "비공개 — Space 멤버만"]\`
|
|
547
583
|
|
|
548
584
|
**응답 처리:**
|
|
549
585
|
- "공개" → relay.yaml에 \`visibility: public\` 저장
|
|
586
|
+
- "링크 공유" → relay.yaml에 \`visibility: gated\` 저장. 배포 후 웹 대시보드(/dashboard)에서 접근 링크를 생성하고 구매 안내를 설정할 수 있다고 안내.
|
|
550
587
|
- "비공개" → \`relay spaces --json\` 실행 후 Space 목록 표시
|
|
551
|
-
-
|
|
552
|
-
- 팀 Space가 0개이면: "비공개 배포하려면 팀 Space가 필요합니다. www.relayax.com/spaces 에서 Space를 생성하세요."라고 안내하고 중단합니다.
|
|
588
|
+
- Space가 0개이면: "비공개 배포하려면 Space가 필요합니다. www.relayax.com/spaces 에서 Space를 생성하세요."라고 안내하고 중단합니다.
|
|
553
589
|
|
|
554
590
|
**AskUserQuestion 호출 (Space가 1개여도 반드시 호출):**
|
|
555
591
|
- question: "어떤 Space에 배포할까요?"
|
|
@@ -563,7 +599,7 @@ relay.yaml의 \`visibility\` 설정을 확인합니다.
|
|
|
563
599
|
현재 설정을 확인합니다:
|
|
564
600
|
|
|
565
601
|
**AskUserQuestion 호출:**
|
|
566
|
-
- question: 공개일 때 "현재 **공개** 설정입니다.
|
|
602
|
+
- question: 공개일 때 "현재 **공개** 설정입니다. 유지할까요?", 링크공유일 때 "현재 **링크 공유** 설정입니다. 접근 링크가 있는 사람만 설치 가능합니다. 유지할까요?", 비공개일 때 "현재 **비공개** 설정입니다 (Space: {name}). 유지할까요?"
|
|
567
603
|
- options: \`["유지", "변경"]\`
|
|
568
604
|
|
|
569
605
|
"변경" → 신규 배포와 동일한 플로우
|
|
@@ -677,13 +713,13 @@ requires: env 2개, cli 1개
|
|
|
677
713
|
- 출력에 코드블록이 없으면 아래 형태로 직접 생성합니다:
|
|
678
714
|
|
|
679
715
|
\\\`\\\`\\\`
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
relay install <slug>
|
|
716
|
+
아래 가이드를 읽고 그대로 따라해줘:
|
|
717
|
+
https://relayax.com/api/registry/{owner}/{slug}/guide.md
|
|
683
718
|
\\\`\\\`\\\`
|
|
684
719
|
|
|
685
|
-
-
|
|
686
|
-
- "이 블록을 팀원에게 공유하면
|
|
720
|
+
- \`{owner}\`과 \`{slug}\`는 배포된 팀의 실제 슬러그에서 추출합니다 (\`@owner/slug\` → \`owner\`, \`slug\`).
|
|
721
|
+
- "이 블록을 팀원에게 공유하면 Claude가 환경 체크부터 설치까지 자동으로 해줍니다"라고 안내합니다.
|
|
722
|
+
- CLI가 이미 설치된 사용자를 위한 짧은 버전도 함께 표시: \`/relay:relay-install <slug>\`
|
|
687
723
|
${BUSINESS_CARD_FORMAT}
|
|
688
724
|
|
|
689
725
|
## 예시
|
|
@@ -699,7 +735,8 @@ ${BUSINESS_CARD_FORMAT}
|
|
|
699
735
|
→ AskUserQuestion: "이대로 배포할까요?" → ["배포", "취소"]
|
|
700
736
|
→ "배포" → relay publish 실행
|
|
701
737
|
→ "배포 완료! URL: https://relayax.com/@my-space/my-team"
|
|
702
|
-
→ 온보딩 가이드 코드블록
|
|
738
|
+
→ 온보딩 가이드 코드블록 표시
|
|
739
|
+
${ERROR_HANDLING_GUIDE}`,
|
|
703
740
|
},
|
|
704
741
|
];
|
|
705
742
|
// ─── Builder Commands (로컬 설치) ───
|
package/dist/types.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export interface TeamRegistryInfo {
|
|
|
51
51
|
tags?: string[];
|
|
52
52
|
install_count?: number;
|
|
53
53
|
requires?: Record<string, unknown>;
|
|
54
|
-
visibility?: "public" | "private";
|
|
54
|
+
visibility?: "public" | "gated" | "private";
|
|
55
55
|
welcome?: string | null;
|
|
56
56
|
contact?: Record<string, string> | null;
|
|
57
57
|
author?: {
|