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,287 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerPackage = registerPackage;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
10
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
11
|
+
const ai_tools_js_1 = require("../lib/ai-tools.js");
|
|
12
|
+
const SYNC_DIRS = ['skills', 'commands', 'agents', 'rules'];
|
|
13
|
+
const EXCLUDE_SUBDIRS = ['relay']; // relay CLI 전용 하위 디렉토리 제외
|
|
14
|
+
// ─── Helpers ───
|
|
15
|
+
function fileHash(filePath) {
|
|
16
|
+
const content = fs_1.default.readFileSync(filePath);
|
|
17
|
+
return crypto_1.default.createHash('md5').update(content).digest('hex');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 디렉토리를 재귀 탐색하여 파일 목록을 반환한다.
|
|
21
|
+
* baseDir 기준 상대 경로 + 해시.
|
|
22
|
+
*/
|
|
23
|
+
function scanDir(baseDir, subDir) {
|
|
24
|
+
const fullDir = path_1.default.join(baseDir, subDir);
|
|
25
|
+
if (!fs_1.default.existsSync(fullDir))
|
|
26
|
+
return [];
|
|
27
|
+
const entries = [];
|
|
28
|
+
function walk(dir) {
|
|
29
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
30
|
+
if (entry.name.startsWith('.'))
|
|
31
|
+
continue;
|
|
32
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
walk(fullPath);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const relPath = path_1.default.relative(baseDir, fullPath);
|
|
38
|
+
entries.push({ relPath, hash: fileHash(fullPath) });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
walk(fullDir);
|
|
43
|
+
return entries;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 소스 디렉토리(예: .claude/)에서 배포 가능한 콘텐츠를 스캔한다.
|
|
47
|
+
* relay/ 하위 디렉토리는 제외.
|
|
48
|
+
*/
|
|
49
|
+
function scanSource(projectPath, tool) {
|
|
50
|
+
const sourceBase = path_1.default.join(projectPath, tool.skillsDir);
|
|
51
|
+
const files = [];
|
|
52
|
+
const summary = {};
|
|
53
|
+
for (const dir of SYNC_DIRS) {
|
|
54
|
+
const fullDir = path_1.default.join(sourceBase, dir);
|
|
55
|
+
if (!fs_1.default.existsSync(fullDir))
|
|
56
|
+
continue;
|
|
57
|
+
// 제외 대상 필터링 (예: commands/relay/)
|
|
58
|
+
const dirEntries = fs_1.default.readdirSync(fullDir, { withFileTypes: true });
|
|
59
|
+
let count = 0;
|
|
60
|
+
for (const entry of dirEntries) {
|
|
61
|
+
if (entry.name.startsWith('.'))
|
|
62
|
+
continue;
|
|
63
|
+
if (entry.isDirectory() && EXCLUDE_SUBDIRS.includes(entry.name))
|
|
64
|
+
continue;
|
|
65
|
+
const entryPath = path_1.default.join(fullDir, entry.name);
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
// 하위 파일 재귀 탐색
|
|
68
|
+
const subFiles = scanDir(sourceBase, path_1.default.join(dir, entry.name));
|
|
69
|
+
// relPath를 sourceBase 기준 → SYNC_DIRS 기준으로 유지
|
|
70
|
+
files.push(...subFiles);
|
|
71
|
+
count += subFiles.length > 0 ? 1 : 0; // 디렉토리 단위로 카운트
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const relPath = path_1.default.relative(sourceBase, entryPath);
|
|
75
|
+
files.push({ relPath, hash: fileHash(entryPath) });
|
|
76
|
+
count++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (count > 0)
|
|
80
|
+
summary[dir] = count;
|
|
81
|
+
}
|
|
82
|
+
return { tool, files, summary };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* .relay/ 디렉토리의 현재 콘텐츠를 스캔한다.
|
|
86
|
+
*/
|
|
87
|
+
function scanRelay(relayDir) {
|
|
88
|
+
const files = [];
|
|
89
|
+
for (const dir of SYNC_DIRS) {
|
|
90
|
+
files.push(...scanDir(relayDir, dir));
|
|
91
|
+
}
|
|
92
|
+
return files;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 소스와 .relay/를 비교하여 diff를 생성한다.
|
|
96
|
+
*/
|
|
97
|
+
function computeDiff(sourceFiles, relayFiles) {
|
|
98
|
+
const relayMap = new Map(relayFiles.map((f) => [f.relPath, f.hash]));
|
|
99
|
+
const sourceMap = new Map(sourceFiles.map((f) => [f.relPath, f.hash]));
|
|
100
|
+
const diff = [];
|
|
101
|
+
// 소스에 있는 파일
|
|
102
|
+
for (const [relPath, hash] of sourceMap) {
|
|
103
|
+
const relayHash = relayMap.get(relPath);
|
|
104
|
+
if (!relayHash) {
|
|
105
|
+
diff.push({ relPath, status: 'added' });
|
|
106
|
+
}
|
|
107
|
+
else if (relayHash !== hash) {
|
|
108
|
+
diff.push({ relPath, status: 'modified' });
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
diff.push({ relPath, status: 'unchanged' });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// .relay/에만 있는 파일 (소스에서 삭제됨)
|
|
115
|
+
for (const [relPath] of relayMap) {
|
|
116
|
+
if (!sourceMap.has(relPath)) {
|
|
117
|
+
diff.push({ relPath, status: 'deleted' });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return diff.sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 소스에서 .relay/로 파일을 동기화한다.
|
|
124
|
+
*/
|
|
125
|
+
function syncToRelay(sourceBase, relayDir, diff) {
|
|
126
|
+
for (const entry of diff) {
|
|
127
|
+
const sourcePath = path_1.default.join(sourceBase, entry.relPath);
|
|
128
|
+
const relayPath = path_1.default.join(relayDir, entry.relPath);
|
|
129
|
+
if (entry.status === 'added' || entry.status === 'modified') {
|
|
130
|
+
fs_1.default.mkdirSync(path_1.default.dirname(relayPath), { recursive: true });
|
|
131
|
+
fs_1.default.copyFileSync(sourcePath, relayPath);
|
|
132
|
+
}
|
|
133
|
+
else if (entry.status === 'deleted') {
|
|
134
|
+
if (fs_1.default.existsSync(relayPath)) {
|
|
135
|
+
fs_1.default.unlinkSync(relayPath);
|
|
136
|
+
// 빈 디렉토리 정리
|
|
137
|
+
const parentDir = path_1.default.dirname(relayPath);
|
|
138
|
+
try {
|
|
139
|
+
const remaining = fs_1.default.readdirSync(parentDir).filter((f) => !f.startsWith('.'));
|
|
140
|
+
if (remaining.length === 0)
|
|
141
|
+
fs_1.default.rmdirSync(parentDir);
|
|
142
|
+
}
|
|
143
|
+
catch { /* ignore */ }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// ─── Command ───
|
|
149
|
+
function registerPackage(program) {
|
|
150
|
+
program
|
|
151
|
+
.command('package')
|
|
152
|
+
.description('소스 디렉토리에서 .relay/로 콘텐츠를 동기화합니다')
|
|
153
|
+
.option('--source <dir>', '소스 디렉토리 지정 (예: .claude)')
|
|
154
|
+
.option('--sync', '변경사항을 .relay/에 즉시 반영', false)
|
|
155
|
+
.option('--init', '최초 패키징: 소스 감지 → .relay/ 초기화', false)
|
|
156
|
+
.action(async (opts) => {
|
|
157
|
+
const json = program.opts().json ?? false;
|
|
158
|
+
const projectPath = process.cwd();
|
|
159
|
+
const relayDir = path_1.default.join(projectPath, '.relay');
|
|
160
|
+
const relayYamlPath = path_1.default.join(relayDir, 'relay.yaml');
|
|
161
|
+
// ─── 최초 패키징 (--init) ───
|
|
162
|
+
if (opts.init || !fs_1.default.existsSync(relayYamlPath)) {
|
|
163
|
+
const detected = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
|
|
164
|
+
// 각 도구의 콘텐츠 스캔
|
|
165
|
+
const scans = detected
|
|
166
|
+
.map((tool) => scanSource(projectPath, tool))
|
|
167
|
+
.filter((s) => s.files.length > 0);
|
|
168
|
+
if (json) {
|
|
169
|
+
console.log(JSON.stringify({
|
|
170
|
+
status: 'init_required',
|
|
171
|
+
detected: scans.map((s) => ({
|
|
172
|
+
source: s.tool.skillsDir,
|
|
173
|
+
name: s.tool.name,
|
|
174
|
+
summary: s.summary,
|
|
175
|
+
fileCount: s.files.length,
|
|
176
|
+
})),
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
if (scans.length === 0) {
|
|
181
|
+
console.error('배포 가능한 에이전트 콘텐츠를 찾지 못했습니다.');
|
|
182
|
+
console.error('skills/, commands/, agents/, rules/ 중 하나를 만들어주세요.');
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
console.error('\n프로젝트에서 발견된 에이전트 콘텐츠:\n');
|
|
186
|
+
for (const scan of scans) {
|
|
187
|
+
const parts = Object.entries(scan.summary)
|
|
188
|
+
.map(([dir, count]) => `${dir} ${count}개`)
|
|
189
|
+
.join(', ');
|
|
190
|
+
console.error(` 📁 ${scan.tool.skillsDir}/ — ${parts}`);
|
|
191
|
+
}
|
|
192
|
+
console.error('');
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
// ─── 재패키징 (source 기반 동기화) ───
|
|
197
|
+
const yamlContent = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
|
|
198
|
+
const config = js_yaml_1.default.load(yamlContent);
|
|
199
|
+
const source = opts.source ?? config.source;
|
|
200
|
+
if (!source) {
|
|
201
|
+
if (json) {
|
|
202
|
+
console.log(JSON.stringify({
|
|
203
|
+
status: 'no_source',
|
|
204
|
+
message: 'relay.yaml에 source 필드가 없습니다. --source 옵션으로 지정하거나 relay.yaml에 source를 추가하세요.',
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
console.error('relay.yaml에 source 필드가 없습니다.');
|
|
209
|
+
console.error('--source <dir> 옵션으로 지정하거나 relay.yaml에 source를 추가하세요.');
|
|
210
|
+
}
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
// 소스 디렉토리 존재 확인
|
|
214
|
+
const sourceBase = path_1.default.join(projectPath, source);
|
|
215
|
+
if (!fs_1.default.existsSync(sourceBase)) {
|
|
216
|
+
const msg = `소스 디렉토리 '${source}'를 찾을 수 없습니다.`;
|
|
217
|
+
if (json) {
|
|
218
|
+
console.log(JSON.stringify({ error: 'SOURCE_NOT_FOUND', message: msg }));
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
console.error(msg);
|
|
222
|
+
}
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
// 소스에서 해당 도구 찾기
|
|
226
|
+
const allTools = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
|
|
227
|
+
const tool = allTools.find((t) => t.skillsDir === source);
|
|
228
|
+
const toolName = tool?.name ?? source;
|
|
229
|
+
// diff 계산
|
|
230
|
+
const sourceScan = tool
|
|
231
|
+
? scanSource(projectPath, tool)
|
|
232
|
+
: { tool: { name: source, value: source, skillsDir: source }, files: [], summary: {} };
|
|
233
|
+
// tool이 없으면 직접 스캔
|
|
234
|
+
if (!tool) {
|
|
235
|
+
for (const dir of SYNC_DIRS) {
|
|
236
|
+
const files = scanDir(sourceBase, dir);
|
|
237
|
+
sourceScan.files.push(...files);
|
|
238
|
+
if (files.length > 0)
|
|
239
|
+
sourceScan.summary[dir] = files.length;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const relayFiles = scanRelay(relayDir);
|
|
243
|
+
const diff = computeDiff(sourceScan.files, relayFiles);
|
|
244
|
+
const summary = {
|
|
245
|
+
added: diff.filter((d) => d.status === 'added').length,
|
|
246
|
+
modified: diff.filter((d) => d.status === 'modified').length,
|
|
247
|
+
deleted: diff.filter((d) => d.status === 'deleted').length,
|
|
248
|
+
unchanged: diff.filter((d) => d.status === 'unchanged').length,
|
|
249
|
+
};
|
|
250
|
+
const hasChanges = summary.added + summary.modified + summary.deleted > 0;
|
|
251
|
+
// --sync: 즉시 동기화
|
|
252
|
+
if (opts.sync && hasChanges) {
|
|
253
|
+
syncToRelay(sourceBase, relayDir, diff);
|
|
254
|
+
}
|
|
255
|
+
const result = {
|
|
256
|
+
source,
|
|
257
|
+
sourceName: toolName,
|
|
258
|
+
synced: opts.sync === true && hasChanges,
|
|
259
|
+
diff: diff.filter((d) => d.status !== 'unchanged'),
|
|
260
|
+
summary,
|
|
261
|
+
};
|
|
262
|
+
if (json) {
|
|
263
|
+
console.log(JSON.stringify(result));
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
if (!hasChanges) {
|
|
267
|
+
console.error(`✓ 소스(${source})와 .relay/가 동기화 상태입니다.`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
console.error(`\n📦 소스 동기화 (${source}/ → .relay/)\n`);
|
|
271
|
+
for (const entry of diff) {
|
|
272
|
+
if (entry.status === 'unchanged')
|
|
273
|
+
continue;
|
|
274
|
+
const icon = entry.status === 'added' ? ' 신규' : entry.status === 'modified' ? ' 변경' : ' 삭제';
|
|
275
|
+
console.error(`${icon}: ${entry.relPath}`);
|
|
276
|
+
}
|
|
277
|
+
console.error('');
|
|
278
|
+
console.error(` 합계: 신규 ${summary.added}, 변경 ${summary.modified}, 삭제 ${summary.deleted}, 유지 ${summary.unchanged}`);
|
|
279
|
+
if (opts.sync) {
|
|
280
|
+
console.error(`\n✓ .relay/에 반영 완료`);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
console.error(`\n반영하려면: relay package --sync`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
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'
|
|
@@ -42,6 +43,7 @@ function parseRelayYaml(content) {
|
|
|
42
43
|
requires,
|
|
43
44
|
visibility,
|
|
44
45
|
type,
|
|
46
|
+
source: raw.source ? String(raw.source) : undefined,
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
function detectCommands(teamDir) {
|
|
@@ -244,7 +246,7 @@ async function createTarball(teamDir) {
|
|
|
244
246
|
const dirsToInclude = VALID_DIRS.filter((d) => fs_1.default.existsSync(path_1.default.join(teamDir, d)));
|
|
245
247
|
// Include root-level files if they exist
|
|
246
248
|
const entries = [...dirsToInclude];
|
|
247
|
-
const rootFiles = ['relay.yaml', 'SKILL.md'];
|
|
249
|
+
const rootFiles = ['relay.yaml', 'SKILL.md', 'guide.md'];
|
|
248
250
|
for (const file of rootFiles) {
|
|
249
251
|
if (fs_1.default.existsSync(path_1.default.join(teamDir, file))) {
|
|
250
252
|
entries.push(file);
|
|
@@ -281,6 +283,8 @@ function registerPublish(program) {
|
|
|
281
283
|
.command('publish')
|
|
282
284
|
.description('현재 팀 패키지를 Space에 배포합니다 (relay.yaml 필요)')
|
|
283
285
|
.option('--token <token>', '인증 토큰')
|
|
286
|
+
.option('--space <slug>', '배포할 Space 지정')
|
|
287
|
+
.option('--version <version>', '배포 버전 지정 (relay.yaml 업데이트)')
|
|
284
288
|
.action(async (opts) => {
|
|
285
289
|
const json = program.opts().json ?? false;
|
|
286
290
|
const teamDir = process.cwd();
|
|
@@ -317,6 +321,7 @@ function registerPublish(program) {
|
|
|
317
321
|
console.error(JSON.stringify({
|
|
318
322
|
error: 'NOT_INITIALIZED',
|
|
319
323
|
message: '.relay/relay.yaml이 없습니다. 먼저 `relay create`를 실행하세요.',
|
|
324
|
+
fix: 'relay create 또는 .relay/relay.yaml을 생성하세요.',
|
|
320
325
|
}));
|
|
321
326
|
process.exit(1);
|
|
322
327
|
}
|
|
@@ -345,12 +350,16 @@ function registerPublish(program) {
|
|
|
345
350
|
const visibility = await promptSelect({
|
|
346
351
|
message: '공개 범위:',
|
|
347
352
|
choices: [
|
|
348
|
-
{ name: '공개', value: 'public' },
|
|
349
|
-
{ name: '
|
|
353
|
+
{ name: '공개 — 누구나 설치', value: 'public' },
|
|
354
|
+
{ name: '링크 공유 — 접근 링크가 있는 사람만 설치', value: 'gated' },
|
|
355
|
+
{ name: '비공개 — Space 멤버만', value: 'private' },
|
|
350
356
|
],
|
|
351
357
|
});
|
|
352
358
|
console.error('\n\x1b[2m💡 프로필에 연락처를 설정하면 설치 시 명함이 전달됩니다: www.relayax.com/dashboard/profile\x1b[0m');
|
|
353
|
-
if (visibility === '
|
|
359
|
+
if (visibility === 'gated') {
|
|
360
|
+
console.error('\x1b[2m💡 링크 공유 팀은 웹 대시보드에서 접근 링크와 구매 안내를 설정하세요: www.relayax.com/dashboard\x1b[0m');
|
|
361
|
+
}
|
|
362
|
+
else if (visibility === 'private') {
|
|
354
363
|
console.error('\x1b[2m💡 비공개 팀은 Space를 통해 멤버를 관리하세요: www.relayax.com/dashboard/teams\x1b[0m');
|
|
355
364
|
}
|
|
356
365
|
console.error('');
|
|
@@ -377,11 +386,18 @@ function registerPublish(program) {
|
|
|
377
386
|
console.error(JSON.stringify({
|
|
378
387
|
error: 'INVALID_CONFIG',
|
|
379
388
|
message: 'relay.yaml에 name, slug, description이 필요합니다.',
|
|
389
|
+
fix: 'relay.yaml에 name, slug, description을 확인하세요.',
|
|
380
390
|
}));
|
|
381
391
|
process.exit(1);
|
|
382
392
|
}
|
|
383
|
-
// Version bump
|
|
384
|
-
if (
|
|
393
|
+
// Version bump: --version flag takes priority
|
|
394
|
+
if (opts.version) {
|
|
395
|
+
config.version = opts.version;
|
|
396
|
+
const yamlData = js_yaml_1.default.load(fs_1.default.readFileSync(relayYamlPath, 'utf-8'));
|
|
397
|
+
yamlData.version = opts.version;
|
|
398
|
+
fs_1.default.writeFileSync(relayYamlPath, js_yaml_1.default.dump(yamlData, { lineWidth: 120 }), 'utf-8');
|
|
399
|
+
}
|
|
400
|
+
else if (isTTY) {
|
|
385
401
|
const { select: promptVersion } = await import('@inquirer/prompts');
|
|
386
402
|
const [major, minor, patch] = config.version.split('.').map(Number);
|
|
387
403
|
const bumpPatch = `${major}.${minor}.${patch + 1}`;
|
|
@@ -415,6 +431,7 @@ function registerPublish(program) {
|
|
|
415
431
|
console.error(JSON.stringify({
|
|
416
432
|
error: 'EMPTY_PACKAGE',
|
|
417
433
|
message: '.relay/ 안에 skills/, agents/, rules/, commands/ 중 하나 이상에 파일이 있어야 합니다.',
|
|
434
|
+
fix: '.relay/ 안에 skills/, agents/, rules/, commands/ 중 하나에 파일을 추가하세요.',
|
|
418
435
|
}));
|
|
419
436
|
process.exit(1);
|
|
420
437
|
}
|
|
@@ -424,71 +441,110 @@ function registerPublish(program) {
|
|
|
424
441
|
console.error(JSON.stringify({
|
|
425
442
|
error: 'NO_TOKEN',
|
|
426
443
|
message: '인증이 필요합니다. `relay login`을 먼저 실행하세요.',
|
|
444
|
+
fix: 'relay login 실행 후 재시도하세요.',
|
|
427
445
|
}));
|
|
428
446
|
process.exit(1);
|
|
429
447
|
}
|
|
430
448
|
// Fetch user's Spaces and select publish target
|
|
431
449
|
let selectedSpaceId;
|
|
432
|
-
let
|
|
450
|
+
let selectedSpaceSlug;
|
|
451
|
+
let selectedJoinPolicy;
|
|
433
452
|
try {
|
|
434
453
|
const { fetchMySpaces } = await import('./spaces.js');
|
|
435
454
|
const spaces = await fetchMySpaces(token);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
455
|
+
// --space flag: resolve Space by slug
|
|
456
|
+
if (opts.space) {
|
|
457
|
+
const matched = spaces.find((s) => s.slug === opts.space);
|
|
458
|
+
if (matched) {
|
|
459
|
+
selectedSpaceId = matched.id;
|
|
460
|
+
selectedSpaceSlug = matched.slug;
|
|
461
|
+
selectedJoinPolicy = matched.join_policy;
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
if (json) {
|
|
465
|
+
console.error(JSON.stringify({
|
|
466
|
+
error: 'INVALID_SPACE',
|
|
467
|
+
message: `Space '${opts.space}'를 찾을 수 없습니다.`,
|
|
468
|
+
fix: `사용 가능한 Space: ${spaces.map((s) => s.slug).join(', ')}`,
|
|
469
|
+
options: spaces.map((s) => ({ value: s.slug, label: `${s.name} (${s.slug})` })),
|
|
470
|
+
}));
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
console.error(`Space '${opts.space}'를 찾을 수 없습니다.`);
|
|
474
|
+
}
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
else if (isTTY) {
|
|
439
479
|
if (spaces.length === 0) {
|
|
440
480
|
// No spaces at all — publish without space_id
|
|
441
481
|
console.error('\x1b[33m⚠ 소속 Space가 없습니다. 개인 계정으로 배포합니다.\x1b[0m\n');
|
|
442
482
|
}
|
|
443
|
-
else if (spaces.length === 1
|
|
444
|
-
// Only
|
|
445
|
-
selectedSpaceId =
|
|
446
|
-
|
|
447
|
-
|
|
483
|
+
else if (spaces.length === 1) {
|
|
484
|
+
// Only one Space — auto-select regardless of type
|
|
485
|
+
selectedSpaceId = spaces[0].id;
|
|
486
|
+
selectedSpaceSlug = spaces[0].slug;
|
|
487
|
+
selectedJoinPolicy = spaces[0].join_policy;
|
|
488
|
+
console.error(`\x1b[2m Space: ${spaces[0].name} (${spaces[0].slug})\x1b[0m\n`);
|
|
448
489
|
}
|
|
449
490
|
else {
|
|
450
491
|
// Multiple spaces — prompt user
|
|
451
492
|
const { select: selectSpace } = await import('@inquirer/prompts');
|
|
452
|
-
const spaceChoices =
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
493
|
+
const spaceChoices = spaces.map((s) => ({
|
|
494
|
+
name: `${s.name} (${s.slug})`,
|
|
495
|
+
value: s.id,
|
|
496
|
+
slug: s.slug,
|
|
497
|
+
join_policy: s.join_policy,
|
|
498
|
+
}));
|
|
456
499
|
const chosenId = await selectSpace({
|
|
457
500
|
message: '어떤 Space에 배포할까요?',
|
|
458
501
|
choices: spaceChoices.map((c) => ({ name: c.name, value: c.value })),
|
|
459
502
|
});
|
|
460
503
|
const chosen = spaceChoices.find((c) => c.value === chosenId);
|
|
461
504
|
selectedSpaceId = chosenId;
|
|
462
|
-
|
|
505
|
+
selectedSpaceSlug = chosen?.slug;
|
|
506
|
+
selectedJoinPolicy = chosen?.join_policy;
|
|
463
507
|
const chosenLabel = chosen?.name ?? chosenId;
|
|
464
508
|
console.error(` → Space: ${chosenLabel}\n`);
|
|
465
509
|
}
|
|
466
510
|
}
|
|
467
|
-
else if (
|
|
468
|
-
|
|
469
|
-
|
|
511
|
+
else if (spaces.length > 1 && json) {
|
|
512
|
+
// --json 모드 + 여러 Space: 에이전트가 선택할 수 있도록 에러 반환
|
|
513
|
+
console.error(JSON.stringify({
|
|
514
|
+
error: 'MISSING_SPACE',
|
|
515
|
+
message: '배포할 Space를 선택하세요.',
|
|
516
|
+
fix: `relay publish --space <slug> --json`,
|
|
517
|
+
options: spaces.map((s) => ({ value: s.slug, label: `${s.name} (${s.slug})` })),
|
|
518
|
+
}));
|
|
519
|
+
process.exit(1);
|
|
520
|
+
}
|
|
521
|
+
else if (spaces.length > 0) {
|
|
522
|
+
selectedSpaceId = spaces[0].id;
|
|
523
|
+
selectedSpaceSlug = spaces[0].slug;
|
|
524
|
+
selectedJoinPolicy = spaces[0].join_policy;
|
|
470
525
|
}
|
|
471
526
|
}
|
|
472
527
|
catch {
|
|
473
528
|
// Space 조회 실패 시 무시하고 계속 진행
|
|
474
529
|
}
|
|
475
|
-
// Visibility default based on
|
|
476
|
-
const defaultVisibility =
|
|
477
|
-
const defaultVisLabel = defaultVisibility === 'public'
|
|
478
|
-
? '공개 (개인 Space 기본값)'
|
|
479
|
-
: '비공개 (팀 Space 기본값)';
|
|
530
|
+
// Visibility default based on join_policy: approval → private, otherwise → public
|
|
531
|
+
const defaultVisibility = selectedJoinPolicy === 'approval' ? 'private' : 'public';
|
|
480
532
|
// Visibility validation: must be explicitly set
|
|
481
533
|
if (!config.visibility) {
|
|
482
534
|
if (isTTY) {
|
|
483
535
|
const { select: promptSelect } = await import('@inquirer/prompts');
|
|
484
|
-
console.error(`\n\x1b[33m⚠ relay.yaml에 visibility가 설정되지 않았습니다.\x1b[0m (기본값: ${
|
|
536
|
+
console.error(`\n\x1b[33m⚠ relay.yaml에 visibility가 설정되지 않았습니다.\x1b[0m (기본값: ${defaultVisibility === 'public' ? '공개' : '비공개'})`);
|
|
485
537
|
config.visibility = await promptSelect({
|
|
486
538
|
message: '공개 범위를 선택하세요:',
|
|
487
539
|
choices: [
|
|
488
540
|
{
|
|
489
|
-
name: `공개 —
|
|
541
|
+
name: `공개 — 누구나 설치${defaultVisibility === 'public' ? ' ✓ 추천' : ''}`,
|
|
490
542
|
value: 'public',
|
|
491
543
|
},
|
|
544
|
+
{
|
|
545
|
+
name: '링크 공유 — 접근 링크가 있는 사람만 설치',
|
|
546
|
+
value: 'gated',
|
|
547
|
+
},
|
|
492
548
|
{
|
|
493
549
|
name: `비공개 — Space 멤버만 접근${defaultVisibility === 'private' ? ' ✓ 추천' : ''}`,
|
|
494
550
|
value: 'private',
|
|
@@ -505,7 +561,13 @@ function registerPublish(program) {
|
|
|
505
561
|
else {
|
|
506
562
|
console.error(JSON.stringify({
|
|
507
563
|
error: 'MISSING_VISIBILITY',
|
|
508
|
-
message: 'relay.yaml에 visibility
|
|
564
|
+
message: 'relay.yaml에 visibility를 설정해주세요.',
|
|
565
|
+
options: [
|
|
566
|
+
{ value: 'public', label: '공개 — 누구나 설치' },
|
|
567
|
+
{ value: 'gated', label: '링크 공유 — 접근 링크가 있는 사람만 설치' },
|
|
568
|
+
{ value: 'private', label: '비공개 — Space 멤버만 접근' },
|
|
569
|
+
],
|
|
570
|
+
fix: 'relay.yaml의 visibility 필드를 위 옵션 중 하나로 설정하세요.',
|
|
509
571
|
}));
|
|
510
572
|
process.exit(1);
|
|
511
573
|
}
|
|
@@ -513,23 +575,36 @@ function registerPublish(program) {
|
|
|
513
575
|
// Confirm visibility before publish (재배포 시 변경 기회 제공)
|
|
514
576
|
if (isTTY) {
|
|
515
577
|
const { select: promptConfirmVis } = await import('@inquirer/prompts');
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
:
|
|
519
|
-
|
|
520
|
-
|
|
578
|
+
const visLabelMap = {
|
|
579
|
+
public: '공개',
|
|
580
|
+
gated: '링크공유',
|
|
581
|
+
private: '비공개',
|
|
582
|
+
};
|
|
583
|
+
const currentVisLabel = visLabelMap[config.visibility ?? 'public'] ?? config.visibility;
|
|
584
|
+
const newVisibility = await promptConfirmVis({
|
|
585
|
+
message: `공개 범위: ${currentVisLabel} — 유지하거나 변경하세요`,
|
|
521
586
|
choices: [
|
|
522
|
-
{
|
|
523
|
-
|
|
587
|
+
{
|
|
588
|
+
name: `공개 — 누구나 설치${defaultVisibility === 'public' ? ' ✓ 추천' : ''}`,
|
|
589
|
+
value: 'public',
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
name: '링크공유 — 접근 링크가 있는 사람만 설치',
|
|
593
|
+
value: 'gated',
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
name: `비공개 — Space 멤버만 접근${defaultVisibility === 'private' ? ' ✓ 추천' : ''}`,
|
|
597
|
+
value: 'private',
|
|
598
|
+
},
|
|
524
599
|
],
|
|
600
|
+
default: config.visibility ?? defaultVisibility,
|
|
525
601
|
});
|
|
526
|
-
if (
|
|
527
|
-
config.visibility =
|
|
602
|
+
if (newVisibility !== config.visibility) {
|
|
603
|
+
config.visibility = newVisibility;
|
|
528
604
|
const yamlData = js_yaml_1.default.load(fs_1.default.readFileSync(relayYamlPath, 'utf-8'));
|
|
529
605
|
yamlData.visibility = config.visibility;
|
|
530
606
|
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`);
|
|
607
|
+
console.error(` → relay.yaml에 visibility: ${config.visibility} 저장됨 (${visLabelMap[config.visibility]})\n`);
|
|
533
608
|
}
|
|
534
609
|
}
|
|
535
610
|
// Profile hint
|
|
@@ -564,6 +639,7 @@ function registerPublish(program) {
|
|
|
564
639
|
agent_details: detectedAgents,
|
|
565
640
|
skill_details: detectedSkills,
|
|
566
641
|
...(selectedSpaceId ? { space_id: selectedSpaceId } : {}),
|
|
642
|
+
...(selectedSpaceSlug ? { space_slug: selectedSpaceSlug } : {}),
|
|
567
643
|
};
|
|
568
644
|
if (!json) {
|
|
569
645
|
console.error(`패키지 생성 중... (${config.name} v${config.version})`);
|
|
@@ -573,6 +649,10 @@ function registerPublish(program) {
|
|
|
573
649
|
console.error('\x1b[33m⚠ GUIDE.html은 더 이상 지원되지 않습니다. 상세페이지가 가이드 역할을 합니다.\x1b[0m');
|
|
574
650
|
console.error(' long_description을 활용하거나 relayax.com에서 팀 정보를 편집하세요.\n');
|
|
575
651
|
}
|
|
652
|
+
// Generate guide.md (consumer install guide)
|
|
653
|
+
const { generateGuide } = await import('../lib/guide.js');
|
|
654
|
+
const guideContent = generateGuide(config, detectedCommands, config.requires);
|
|
655
|
+
fs_1.default.writeFileSync(path_1.default.join(relayDir, 'guide.md'), guideContent);
|
|
576
656
|
// Generate bin/relay-preamble.sh (self-contained tracking + update check)
|
|
577
657
|
(0, preamble_js_1.generatePreambleBin)(relayDir, config.slug, config_js_1.API_URL);
|
|
578
658
|
// Generate entry command (commands/{author}-{name}.md)
|
|
@@ -636,33 +716,28 @@ function registerPublish(program) {
|
|
|
636
716
|
}
|
|
637
717
|
// Show shareable onboarding guide as a plain copyable block
|
|
638
718
|
if (isTTY) {
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
`/relay:relay-install ${result.slug}`,
|
|
642
|
-
];
|
|
643
|
-
// Type-based usage hint
|
|
644
|
-
const teamType = config.type ?? 'hybrid';
|
|
645
|
-
if (teamType === 'passive') {
|
|
646
|
-
guideLines.push('', '# 설치하면 자동 적용됩니다');
|
|
647
|
-
}
|
|
648
|
-
else if (detectedCommands.length > 0) {
|
|
649
|
-
const usageCmd = teamType === 'hybrid'
|
|
650
|
-
? `# 자동 적용 + /${detectedCommands[0].name} 으로 추가 기능`
|
|
651
|
-
: `# 사용법: /${detectedCommands[0].name}`;
|
|
652
|
-
guideLines.push('', usageCmd);
|
|
653
|
-
}
|
|
719
|
+
const detailSlug = result.slug.startsWith('@') ? result.slug.slice(1) : result.slug;
|
|
720
|
+
const guideUrl = `https://relayax.com/api/registry/${detailSlug}/guide.md`;
|
|
654
721
|
console.log(`\n \x1b[90m주변인에게 공유하세요:\x1b[0m\n`);
|
|
655
722
|
console.log('```');
|
|
656
|
-
|
|
723
|
+
console.log('아래 가이드를 읽고 그대로 따라해줘:');
|
|
724
|
+
console.log(guideUrl);
|
|
657
725
|
console.log('```');
|
|
658
|
-
|
|
726
|
+
console.log(`\n \x1b[90mCLI 설치된 사용자용 (짧은 버전):\x1b[0m`);
|
|
727
|
+
console.log(` /relay:relay-install ${result.slug}`);
|
|
728
|
+
if (config.visibility !== 'private') {
|
|
729
|
+
console.log(`\n \x1b[90m유료 판매하려면:\x1b[0m`);
|
|
730
|
+
console.log(` 1. 가시성을 "링크 공유"로 변경: \x1b[36mrelayax.com/dashboard\x1b[0m`);
|
|
731
|
+
console.log(` 2. API 키 발급: \x1b[36mrelayax.com/dashboard/keys\x1b[0m`);
|
|
732
|
+
console.log(` 3. 웹훅 연동 가이드: \x1b[36mrelayax.com/docs/webhook-guide.md\x1b[0m`);
|
|
733
|
+
}
|
|
659
734
|
console.log(`\n \x1b[90m상세페이지: \x1b[36mrelayax.com/@${detailSlug}\x1b[0m`);
|
|
660
735
|
}
|
|
661
736
|
}
|
|
662
737
|
}
|
|
663
738
|
catch (err) {
|
|
664
739
|
const message = err instanceof Error ? err.message : String(err);
|
|
665
|
-
console.error(JSON.stringify({ error: 'PUBLISH_FAILED', message }));
|
|
740
|
+
console.error(JSON.stringify({ error: 'PUBLISH_FAILED', message, fix: message }));
|
|
666
741
|
process.exit(1);
|
|
667
742
|
}
|
|
668
743
|
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
|
});
|