relayax-cli 0.3.56 → 0.3.58
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/index.js +0 -2
- package/dist/mcp/server.js +173 -6
- package/dist/prompts/_setup-environment.md +5 -0
- package/dist/prompts/install.md +4 -13
- package/dist/prompts/publish.md +43 -25
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15,7 +15,6 @@ const login_js_1 = require("./commands/login.js");
|
|
|
15
15
|
const update_js_1 = require("./commands/update.js");
|
|
16
16
|
const outdated_js_1 = require("./commands/outdated.js");
|
|
17
17
|
const check_update_js_1 = require("./commands/check-update.js");
|
|
18
|
-
const follow_js_1 = require("./commands/follow.js");
|
|
19
18
|
const changelog_js_1 = require("./commands/changelog.js");
|
|
20
19
|
const join_js_1 = require("./commands/join.js");
|
|
21
20
|
const orgs_js_1 = require("./commands/orgs.js");
|
|
@@ -48,7 +47,6 @@ program
|
|
|
48
47
|
(0, update_js_1.registerUpdate)(program);
|
|
49
48
|
(0, outdated_js_1.registerOutdated)(program);
|
|
50
49
|
(0, check_update_js_1.registerCheckUpdate)(program);
|
|
51
|
-
(0, follow_js_1.registerFollow)(program);
|
|
52
50
|
(0, changelog_js_1.registerChangelog)(program);
|
|
53
51
|
(0, join_js_1.registerJoin)(program);
|
|
54
52
|
(0, orgs_js_1.registerOrgs)(program);
|
package/dist/mcp/server.js
CHANGED
|
@@ -79,6 +79,18 @@ async function getCliUpdateWarning() {
|
|
|
79
79
|
function jsonTextWithUpdate(obj, update) {
|
|
80
80
|
return jsonText(update ? { ...obj, ...update } : obj);
|
|
81
81
|
}
|
|
82
|
+
// MCP 서버는 Claude Desktop이 spawn하므로 cwd가 / 등 예측 불가한 경로일 수 있다.
|
|
83
|
+
// project_path가 없을 때 cwd 대신 홈 디렉토리를 fallback으로 사용한다.
|
|
84
|
+
const os_1 = __importDefault(require("os"));
|
|
85
|
+
function resolveMcpProjectPath(projectPath) {
|
|
86
|
+
if (projectPath)
|
|
87
|
+
return projectPath;
|
|
88
|
+
const resolved = (0, paths_js_1.resolveProjectPath)();
|
|
89
|
+
// cwd가 / 또는 비정상적이면 홈 디렉토리 사용
|
|
90
|
+
if (resolved === '/' || resolved === '')
|
|
91
|
+
return os_1.default.homedir();
|
|
92
|
+
return resolved;
|
|
93
|
+
}
|
|
82
94
|
// ─── Server ───
|
|
83
95
|
function createMcpServer() {
|
|
84
96
|
const server = new mcp_js_1.McpServer({ name: 'relay', version: pkg.version }, { capabilities: { tools: {}, prompts: {} } });
|
|
@@ -97,10 +109,10 @@ function createMcpServer() {
|
|
|
97
109
|
});
|
|
98
110
|
server.tool('relay_install', '에이전트를 설치합니다', {
|
|
99
111
|
slug: zod_1.z.string().describe('에이전트 slug (예: @owner/name)'),
|
|
100
|
-
project_path: zod_1.z.string().optional().describe('프로젝트 경로 (기본:
|
|
112
|
+
project_path: zod_1.z.string().optional().describe('프로젝트 경로 (기본: 홈 디렉토리)'),
|
|
101
113
|
}, async ({ slug: slugInput, project_path }) => {
|
|
102
114
|
try {
|
|
103
|
-
const projectPath = project_path
|
|
115
|
+
const projectPath = resolveMcpProjectPath(project_path);
|
|
104
116
|
const token = await (0, config_js_1.getValidToken)();
|
|
105
117
|
const parsed = await (0, slug_js_1.resolveSlug)(slugInput);
|
|
106
118
|
const fullSlug = parsed.full;
|
|
@@ -124,8 +136,27 @@ function createMcpServer() {
|
|
|
124
136
|
(0, config_js_1.saveInstalled)(installed);
|
|
125
137
|
await (0, api_js_1.reportInstall)(agent.id, fullSlug, agent.version);
|
|
126
138
|
(0, api_js_1.sendUsagePing)(agent.id, fullSlug, agent.version);
|
|
139
|
+
// relay.yaml에서 tags, requires 읽기 (scope 판단용)
|
|
140
|
+
let agentTags = [];
|
|
141
|
+
let agentRequires = null;
|
|
142
|
+
let hasRules = false;
|
|
143
|
+
try {
|
|
144
|
+
const relayYamlPath = path_1.default.join(agentDir, 'relay.yaml');
|
|
145
|
+
if (fs_1.default.existsSync(relayYamlPath)) {
|
|
146
|
+
const cfg = js_yaml_1.default.load(fs_1.default.readFileSync(relayYamlPath, 'utf-8'));
|
|
147
|
+
agentTags = cfg.tags ?? [];
|
|
148
|
+
agentRequires = cfg.requires ?? null;
|
|
149
|
+
}
|
|
150
|
+
hasRules = fs_1.default.existsSync(path_1.default.join(agentDir, 'rules')) && fs_1.default.readdirSync(path_1.default.join(agentDir, 'rules')).length > 0;
|
|
151
|
+
}
|
|
152
|
+
catch { /* non-critical */ }
|
|
127
153
|
const cliUpdate = await getCliUpdateWarning();
|
|
128
|
-
return { content: [jsonTextWithUpdate({
|
|
154
|
+
return { content: [jsonTextWithUpdate({
|
|
155
|
+
status: 'ok', agent: agent.name, slug: fullSlug, version: agent.version,
|
|
156
|
+
description: agent.description ?? '', tags: agentTags, requires: agentRequires, has_rules: hasRules,
|
|
157
|
+
files: countFiles(agentDir), install_path: agentDir,
|
|
158
|
+
scope_hint: '설치 후 에이전트 성격에 따라 글로벌/로컬 배치를 사용자에게 물어보세요. tags에 특정 프레임워크가 있거나 rules/가 있으면 로컬 추천, 범용이면 글로벌 추천.',
|
|
159
|
+
}, cliUpdate)] };
|
|
129
160
|
}
|
|
130
161
|
finally {
|
|
131
162
|
(0, storage_js_1.removeTempDir)(tempDir);
|
|
@@ -174,7 +205,7 @@ function createMcpServer() {
|
|
|
174
205
|
server.tool('relay_status', '현재 relay 환경 상태를 표시합니다', {
|
|
175
206
|
project_path: zod_1.z.string().optional().describe('프로젝트 경로'),
|
|
176
207
|
}, async ({ project_path }) => {
|
|
177
|
-
const projectPath = project_path
|
|
208
|
+
const projectPath = resolveMcpProjectPath(project_path);
|
|
178
209
|
const token = await (0, config_js_1.getValidToken)();
|
|
179
210
|
let username;
|
|
180
211
|
if (token)
|
|
@@ -236,7 +267,7 @@ function createMcpServer() {
|
|
|
236
267
|
server.tool('relay_scan', '배포 가능한 스킬/에이전트/커맨드를 스캔합니다', {
|
|
237
268
|
project_path: zod_1.z.string().optional().describe('프로젝트 경로'),
|
|
238
269
|
}, async ({ project_path }) => {
|
|
239
|
-
const projectPath = project_path
|
|
270
|
+
const projectPath = resolveMcpProjectPath(project_path);
|
|
240
271
|
const homeDir = (0, paths_js_1.resolveHome)();
|
|
241
272
|
const sources = [];
|
|
242
273
|
// 로컬
|
|
@@ -310,7 +341,7 @@ function createMcpServer() {
|
|
|
310
341
|
project_path: zod_1.z.string().optional().describe('프로젝트 경로 (.relay/relay.yaml이 있는 디렉토리)'),
|
|
311
342
|
}, async ({ project_path }) => {
|
|
312
343
|
try {
|
|
313
|
-
const projectPath = project_path
|
|
344
|
+
const projectPath = resolveMcpProjectPath(project_path);
|
|
314
345
|
const relayDir = path_1.default.join(projectPath, '.relay');
|
|
315
346
|
const relayYaml = path_1.default.join(relayDir, 'relay.yaml');
|
|
316
347
|
if (!fs_1.default.existsSync(relayYaml)) {
|
|
@@ -349,6 +380,142 @@ function createMcpServer() {
|
|
|
349
380
|
return { content: [jsonText({ error: String(err) })], isError: true };
|
|
350
381
|
}
|
|
351
382
|
});
|
|
383
|
+
// ═══ grant / access / join ═══
|
|
384
|
+
server.tool('relay_grant_create', '에이전트 또는 Organization의 접근 코드를 생성합니다', {
|
|
385
|
+
agent_slug: zod_1.z.string().optional().describe('에이전트 slug (agent 접근 코드 생성 시)'),
|
|
386
|
+
org_slug: zod_1.z.string().optional().describe('Organization slug (org 접근 코드 생성 시)'),
|
|
387
|
+
max_uses: zod_1.z.number().optional().describe('최대 사용 횟수'),
|
|
388
|
+
expires_at: zod_1.z.string().optional().describe('만료일 (ISO 8601)'),
|
|
389
|
+
}, async ({ agent_slug, org_slug, max_uses, expires_at }) => {
|
|
390
|
+
try {
|
|
391
|
+
if (!agent_slug && !org_slug) {
|
|
392
|
+
return { content: [jsonText({ error: 'MISSING_OPTION', message: 'agent_slug 또는 org_slug가 필요합니다.' })], isError: true };
|
|
393
|
+
}
|
|
394
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
395
|
+
if (!token)
|
|
396
|
+
return { content: [jsonText({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다.' })], isError: true };
|
|
397
|
+
let agentId;
|
|
398
|
+
let orgId;
|
|
399
|
+
if (agent_slug) {
|
|
400
|
+
const res = await fetch(`${config_js_1.API_URL}/api/agents/${agent_slug}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
401
|
+
if (!res.ok)
|
|
402
|
+
throw new Error('에이전트를 찾을 수 없습니다.');
|
|
403
|
+
agentId = (await res.json()).id;
|
|
404
|
+
}
|
|
405
|
+
if (org_slug) {
|
|
406
|
+
const res = await fetch(`${config_js_1.API_URL}/api/orgs/${org_slug}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
407
|
+
if (!res.ok)
|
|
408
|
+
throw new Error('Organization을 찾을 수 없습니다.');
|
|
409
|
+
orgId = (await res.json()).id;
|
|
410
|
+
}
|
|
411
|
+
const { createAccessCode } = await import('../commands/grant.js');
|
|
412
|
+
const result = await createAccessCode({
|
|
413
|
+
type: agentId ? 'agent' : 'org',
|
|
414
|
+
agent_id: agentId,
|
|
415
|
+
org_id: orgId,
|
|
416
|
+
max_uses,
|
|
417
|
+
expires_at,
|
|
418
|
+
});
|
|
419
|
+
return { content: [jsonText({ status: 'created', ...result })] };
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
return { content: [jsonText({ error: String(err) })], isError: true };
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
server.tool('relay_grant_use', '접근 코드를 사용하여 org 가입 또는 에이전트 접근 권한을 획득합니다', {
|
|
426
|
+
code: zod_1.z.string().describe('접근 코드'),
|
|
427
|
+
}, async ({ code }) => {
|
|
428
|
+
try {
|
|
429
|
+
const { useAccessCode } = await import('../commands/grant.js');
|
|
430
|
+
const result = await useAccessCode(code);
|
|
431
|
+
return { content: [jsonText({ ...result, status: 'ok' })] };
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
return { content: [jsonText({ error: String(err) })], isError: true };
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
server.tool('relay_access', '접근 코드로 비공개 에이전트 접근 권한을 획득합니다 (설치는 별도로 relay_install 호출 필요)', {
|
|
438
|
+
slug: zod_1.z.string().describe('에이전트 slug'),
|
|
439
|
+
code: zod_1.z.string().describe('접근 코드'),
|
|
440
|
+
}, async ({ slug: slugInput, code }) => {
|
|
441
|
+
try {
|
|
442
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
443
|
+
if (!token)
|
|
444
|
+
return { content: [jsonText({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다.' })], isError: true };
|
|
445
|
+
const res = await fetch(`${config_js_1.API_URL}/api/agents/${slugInput}/claim-access`, {
|
|
446
|
+
method: 'POST',
|
|
447
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
448
|
+
body: JSON.stringify({ code }),
|
|
449
|
+
});
|
|
450
|
+
if (!res.ok) {
|
|
451
|
+
const body = await res.json().catch(() => ({}));
|
|
452
|
+
throw new Error(body.message ?? `접근 권한 획득 실패 (${res.status})`);
|
|
453
|
+
}
|
|
454
|
+
const result = await res.json();
|
|
455
|
+
return { content: [jsonText({ status: 'ok', ...result })] };
|
|
456
|
+
}
|
|
457
|
+
catch (err) {
|
|
458
|
+
return { content: [jsonText({ error: String(err) })], isError: true };
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
server.tool('relay_join', '초대 코드로 Organization에 가입합니다', {
|
|
462
|
+
code: zod_1.z.string().describe('초대 코드 (UUID)'),
|
|
463
|
+
}, async ({ code }) => {
|
|
464
|
+
try {
|
|
465
|
+
const { useAccessCode } = await import('../commands/grant.js');
|
|
466
|
+
const result = await useAccessCode(code);
|
|
467
|
+
return { content: [jsonText({ ...result, status: 'ok' })] };
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
return { content: [jsonText({ error: String(err) })], isError: true };
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
// ═══ relay_deploy_record — 배치 파일 기록 ═══
|
|
474
|
+
server.tool('relay_deploy_record', '에이전트 파일 배치 정보를 installed.json에 기록합니다', {
|
|
475
|
+
slug: zod_1.z.string().describe('에이전트 slug'),
|
|
476
|
+
scope: zod_1.z.enum(['global', 'local']).describe('배치 범위 (global 또는 local)'),
|
|
477
|
+
files: zod_1.z.array(zod_1.z.string()).optional().describe('배치된 파일 경로 목록'),
|
|
478
|
+
}, async ({ slug: slugInput, scope, files }) => {
|
|
479
|
+
try {
|
|
480
|
+
const { isScopedSlug, parseSlug } = await import('../lib/slug.js');
|
|
481
|
+
const localRegistry = (0, config_js_1.loadInstalled)();
|
|
482
|
+
const globalRegistry = (0, config_js_1.loadGlobalInstalled)();
|
|
483
|
+
let slug;
|
|
484
|
+
if (isScopedSlug(slugInput)) {
|
|
485
|
+
slug = slugInput;
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
const allKeys = [...Object.keys(localRegistry), ...Object.keys(globalRegistry)];
|
|
489
|
+
const match = allKeys.find((key) => {
|
|
490
|
+
const parsed = parseSlug(key);
|
|
491
|
+
return parsed && parsed.name === slugInput;
|
|
492
|
+
});
|
|
493
|
+
slug = match ?? slugInput;
|
|
494
|
+
}
|
|
495
|
+
const entry = localRegistry[slug] ?? globalRegistry[slug];
|
|
496
|
+
if (!entry) {
|
|
497
|
+
return { content: [jsonText({ error: 'NOT_INSTALLED', message: `'${slugInput}'는 설치되어 있지 않습니다.` })], isError: true };
|
|
498
|
+
}
|
|
499
|
+
entry.deploy_scope = scope;
|
|
500
|
+
entry.deployed_files = files ?? [];
|
|
501
|
+
if (scope === 'global') {
|
|
502
|
+
globalRegistry[slug] = entry;
|
|
503
|
+
(0, config_js_1.saveGlobalInstalled)(globalRegistry);
|
|
504
|
+
if (localRegistry[slug]) {
|
|
505
|
+
localRegistry[slug] = entry;
|
|
506
|
+
(0, config_js_1.saveInstalled)(localRegistry);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
localRegistry[slug] = entry;
|
|
511
|
+
(0, config_js_1.saveInstalled)(localRegistry);
|
|
512
|
+
}
|
|
513
|
+
return { content: [jsonText({ status: 'ok', slug, deploy_scope: scope, deployed_files: (files ?? []).length })] };
|
|
514
|
+
}
|
|
515
|
+
catch (err) {
|
|
516
|
+
return { content: [jsonText({ error: String(err) })], isError: true };
|
|
517
|
+
}
|
|
518
|
+
});
|
|
352
519
|
// ═══ relay_login — device code 로그인 ═══
|
|
353
520
|
server.tool('relay_login', 'Device Code 방식으로 로그인합니다. URL과 코드를 사용자에게 보여주고, 승인을 기다립니다.', {}, async () => {
|
|
354
521
|
try {
|
|
@@ -63,5 +63,10 @@ cat ~/Library/Application\ Support/Claude/claude_desktop_config.json 2>/dev/null
|
|
|
63
63
|
| "relay check-update X" | `relay check-update X` | `relay_check_update` tool 호출 (`slug: "X"`) |
|
|
64
64
|
| "relay orgs list" | `relay orgs list --json` | `relay_org_list` tool 호출 |
|
|
65
65
|
| "relay orgs create" | `relay orgs create "이름" --json` | `relay_org_create` tool 호출 (`name: "이름"`) |
|
|
66
|
+
| "relay grant create" | `relay grant create --agent <slug>` | `relay_grant_create` tool 호출 (`agent_slug: "<slug>"`) |
|
|
67
|
+
| "relay grant use" | `relay grant use --code <code>` | `relay_grant_use` tool 호출 (`code: "<code>"`) |
|
|
68
|
+
| "relay access" | `relay access <slug> --code <code>` | `relay_access` tool 호출 (`slug`, `code`) |
|
|
69
|
+
| "relay join" | `relay join <slug> --code <code>` | `relay_join` tool 호출 (`code: "<code>"`) |
|
|
70
|
+
| "relay deploy-record" | `relay deploy-record <slug> --scope <scope> --files ...` | `relay_deploy_record` tool 호출 |
|
|
66
71
|
|
|
67
72
|
처음 판별한 환경을 이후 계속 사용합니다.
|
package/dist/prompts/install.md
CHANGED
|
@@ -85,7 +85,9 @@ slug가 직접 주어지면 (`/relay-install @alice/doc-writer`) 이 단계를
|
|
|
85
85
|
3. 코드가 있으면 `relay access <slug> --code <code>`를 실행합니다.
|
|
86
86
|
4. 코드가 없으면 purchase_info의 url로 구매 안내합니다.
|
|
87
87
|
|
|
88
|
-
#### 2-2. 배치 범위 선택 (
|
|
88
|
+
#### 2-2. 배치 범위 선택 (필수 — 이 단계를 절대 건너뛰지 마세요)
|
|
89
|
+
|
|
90
|
+
**반드시 사용자에게 글로벌/로컬 중 어디에 설치할지 물어보세요.** 에이전트 메타데이터(tags, requires 등)가 없어도 반드시 물어봐야 합니다. 메타데이터가 없으면 글로벌을 추천하되, 사용자 확인은 필수입니다.
|
|
89
91
|
|
|
90
92
|
에이전트의 성격을 분석하여 글로벌/로컬 중 적합한 쪽을 추천합니다.
|
|
91
93
|
|
|
@@ -132,18 +134,7 @@ relay deploy-record <slug> --scope <global|local> --files <배치된_파일1> <
|
|
|
132
134
|
|
|
133
135
|
#### 3-1. 완료 안내
|
|
134
136
|
- 배치된 파일과 활성화된 커맨드 목록을 보여줍니다.
|
|
135
|
-
#### 3-2.
|
|
136
|
-
빌더의 username이 JSON 결과에 있으면 **반드시** 사용자 질문 도구를 호출하세요.
|
|
137
|
-
|
|
138
|
-
**사용자 질문 도구 호출:**
|
|
139
|
-
- question: `@{username}을 팔로우할까요? 새 버전 알림을 받을 수 있습니다.`
|
|
140
|
-
- options: `["팔로우", "건너뛰기"]`
|
|
141
|
-
|
|
142
|
-
**응답 처리:**
|
|
143
|
-
- "팔로우" → `relay follow @{username}` 실행. 로그인이 안 되어 있으면 `relay login` 먼저 실행 후 재시도.
|
|
144
|
-
- "건너뛰기" → 다음 단계로 진행
|
|
145
|
-
|
|
146
|
-
#### 3-3. 공유 가이드 (필수 — 설치 완료 시 반드시 표시)
|
|
137
|
+
#### 3-2. 공유 가이드 (필수 — 설치 완료 시 반드시 표시)
|
|
147
138
|
설치 완료 후 아래 공유용 설치 가이드를 표시합니다. 복사 가능한 코드 블록으로 보여줍니다.
|
|
148
139
|
|
|
149
140
|
```
|
package/dist/prompts/publish.md
CHANGED
|
@@ -263,45 +263,63 @@ relay.yaml의 현재 `version`을 읽고 semver 범프를 제안합니다.
|
|
|
263
263
|
- 유지 외 선택 → relay.yaml의 version을 선택된 값으로 업데이트
|
|
264
264
|
- 유지 → 그대로 진행
|
|
265
265
|
|
|
266
|
-
### Step 1. 공개 범위
|
|
266
|
+
### Step 1. Organization 선택 & 공개 범위 설정
|
|
267
267
|
|
|
268
|
-
|
|
268
|
+
#### 1-1. Organization 확인 (먼저 실행)
|
|
269
269
|
|
|
270
|
-
|
|
270
|
+
Organization 목록을 조회합니다:
|
|
271
|
+
- 환경 A: `relay orgs list --json` 실행
|
|
272
|
+
- 환경 B: `relay_org_list` MCP tool 호출
|
|
271
273
|
|
|
272
|
-
|
|
273
|
-
-
|
|
274
|
-
-
|
|
274
|
+
**Org가 0개이면:**
|
|
275
|
+
- **사용자 질문 도구 호출:**
|
|
276
|
+
- question: "Organization이 없습니다. 비공개 배포를 하려면 Organization이 필요합니다. Organization을 만들까요?"
|
|
277
|
+
- options: `["Organization 생성", "Organization 없이 계속 (공개/링크공유만 가능)"]`
|
|
278
|
+
- "Organization 생성" 선택 시:
|
|
279
|
+
- **사용자 질문 도구 호출:** question: "Organization 이름을 입력하세요."
|
|
280
|
+
- 환경 A: `relay orgs create "이름" --json` 실행
|
|
281
|
+
- 환경 B: `relay_org_create` MCP tool 호출
|
|
282
|
+
- 생성 후 org 목록을 갱신합니다.
|
|
283
|
+
|
|
284
|
+
**Org가 1개 이상이면:**
|
|
285
|
+
|
|
286
|
+
**사용자 질문 도구 호출 (Org가 1개여도 반드시 호출):**
|
|
287
|
+
- question: "어떤 Organization에 배포할까요?"
|
|
288
|
+
- options: `["<org1_name> (<org1_slug>)", "<org2_name> (<org2_slug>)", ..., "Organization 없이 배포 (공개/링크공유)"]`
|
|
289
|
+
- **중요: Org가 1개라도 자동 선택하지 말고 반드시 사용자에게 확인받으세요.**
|
|
275
290
|
|
|
276
291
|
**응답 처리:**
|
|
277
|
-
-
|
|
278
|
-
- "
|
|
279
|
-
- "비공개" → Organization 목록을 조회합니다:
|
|
280
|
-
- 환경 A: `relay orgs list --json` 실행
|
|
281
|
-
- 환경 B: `relay_status` tool 응답을 참고하거나, 배포 시 `relay_publish` tool이 org 선택 없이 배포하면 서버가 자동 매칭합니다.
|
|
282
|
-
- Org가 0개이면: Organization을 생성합니다.
|
|
283
|
-
- 환경 A: `relay orgs create "이름" --json` 실행
|
|
284
|
-
- 환경 B: `relay_org_create` MCP tool 호출 (tool이 없으면 사용자에게 "www.relayax.com/orgs 에서 Organization을 생성하세요"라고 안내)
|
|
285
|
-
- **사용자 질문 도구 호출:**
|
|
286
|
-
- question: "비공개 배포를 위해 Organization을 만들어야 합니다. Organization 이름을 입력하세요."
|
|
287
|
-
- 생성 후 해당 org를 선택하여 계속 진행합니다.
|
|
292
|
+
- Org 선택 → relay.yaml에 `org: <selected_slug>` 저장, 1-2단계로
|
|
293
|
+
- "Organization 없이 배포" → org 없이 1-2단계로 (비공개 옵션 제외)
|
|
288
294
|
|
|
289
|
-
|
|
290
|
-
- question: "어떤 Organization에 배포할까요?"
|
|
291
|
-
- options: `["<org1_name>", "<org2_name>", ...]`
|
|
292
|
-
- **중요: Org가 1개라도 자동 선택하지 말고 반드시 사용자에게 확인받으세요.**
|
|
295
|
+
#### 1-2. 공개 범위 선택
|
|
293
296
|
|
|
294
|
-
|
|
297
|
+
relay.yaml의 `visibility` 설정을 확인합니다.
|
|
295
298
|
|
|
296
|
-
|
|
299
|
+
**신규 배포 (visibility 미설정):**
|
|
300
|
+
|
|
301
|
+
Org가 선택된 경우:
|
|
302
|
+
- **사용자 질문 도구 호출:**
|
|
303
|
+
- question: "공개 범위를 선택하세요"
|
|
304
|
+
- options: `["공개 — 누구나 설치", "링크 공유 — 접근 링크가 있는 사람만 설치", "비공개 — Org 멤버만"]`
|
|
305
|
+
|
|
306
|
+
Org가 없는 경우:
|
|
307
|
+
- **사용자 질문 도구 호출:**
|
|
308
|
+
- question: "공개 범위를 선택하세요"
|
|
309
|
+
- options: `["공개 — 누구나 설치", "링크 공유 — 접근 링크가 있는 사람만 설치"]`
|
|
310
|
+
|
|
311
|
+
**응답 처리:**
|
|
312
|
+
- "공개" → relay.yaml에 `visibility: public` 저장
|
|
313
|
+
- "링크 공유" → relay.yaml에 `visibility: private` 저장. 배포 후 웹 대시보드(/dashboard)에서 접근 링크를 생성하고 구매 안내를 설정할 수 있다고 안내.
|
|
314
|
+
- "비공개" → relay.yaml에 `visibility: internal`, `org: <selected_slug>` 저장
|
|
297
315
|
|
|
298
|
-
|
|
316
|
+
**재배포 (visibility 이미 설정됨):**
|
|
299
317
|
|
|
300
318
|
**사용자 질문 도구 호출:**
|
|
301
319
|
- question: 공개일 때 "현재 **공개** 설정입니다. 유지할까요?", 링크공유일 때 "현재 **링크 공유** 설정입니다. 접근 링크가 있는 사람만 설치 가능합니다. 유지할까요?", 비공개일 때 "현재 **비공개** 설정입니다 (Org: {name}). 유지할까요?"
|
|
302
320
|
- options: `["유지", "변경"]`
|
|
303
321
|
|
|
304
|
-
"변경" →
|
|
322
|
+
"변경" → 1-1부터 다시 진행
|
|
305
323
|
|
|
306
324
|
### Step 2. 보안 점검 & requires 확인
|
|
307
325
|
|