relayax-cli 0.2.37 → 0.2.38
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/create.js +1 -1
- package/dist/commands/init.js +5 -20
- package/dist/commands/install.js +77 -12
- package/dist/commands/list.js +2 -2
- package/dist/commands/publish.js +89 -49
- package/dist/commands/search.js +8 -3
- package/dist/lib/api.d.ts +1 -1
- package/dist/lib/api.js +3 -1
- package/dist/lib/command-adapter.js +29 -26
- package/dist/lib/config.js +15 -2
- package/dist/lib/preamble.js +6 -2
- package/dist/lib/slug.d.ts +1 -0
- package/dist/lib/slug.js +7 -3
- package/package.json +1 -1
package/dist/commands/create.js
CHANGED
|
@@ -129,7 +129,7 @@ function registerCreate(program) {
|
|
|
129
129
|
if (globalInstalled) {
|
|
130
130
|
console.log(`\n \x1b[36mUser 커맨드 (글로벌)\x1b[0m — 설치됨`);
|
|
131
131
|
}
|
|
132
|
-
console.log(`\n 다음 단계: \x1b[33m/relay-publish\x1b[0m로
|
|
132
|
+
console.log(`\n 다음 단계: \x1b[33m/relay-publish\x1b[0m로 Space에 배포`);
|
|
133
133
|
console.log(' IDE를 재시작하면 슬래시 커맨드가 활성화됩니다.\n');
|
|
134
134
|
}
|
|
135
135
|
});
|
package/dist/commands/init.js
CHANGED
|
@@ -122,7 +122,6 @@ function registerInit(program) {
|
|
|
122
122
|
.command('init')
|
|
123
123
|
.description('에이전트 CLI에 relay 슬래시 커맨드를 설치합니다')
|
|
124
124
|
.option('--tools <tools>', '설치할 에이전트 CLI 지정 (all 또는 쉼표 구분)')
|
|
125
|
-
.option('--update', '이미 설치된 슬래시 커맨드를 최신 버전으로 업데이트')
|
|
126
125
|
.option('--auto', '대화형 프롬프트 없이 자동으로 모든 감지된 CLI에 설치')
|
|
127
126
|
.action(async (opts) => {
|
|
128
127
|
const json = program.opts().json ?? false;
|
|
@@ -135,9 +134,9 @@ function registerInit(program) {
|
|
|
135
134
|
// ── 1. 글로벌 User 커맨드 설치 ──
|
|
136
135
|
let globalStatus = 'already';
|
|
137
136
|
let globalTools = [];
|
|
138
|
-
|
|
137
|
+
{
|
|
139
138
|
const result = installGlobalUserCommands();
|
|
140
|
-
globalStatus =
|
|
139
|
+
globalStatus = hasGlobalUserCommands() ? 'updated' : 'installed';
|
|
141
140
|
globalTools = result.tools;
|
|
142
141
|
// Register relay-core in installed.json
|
|
143
142
|
const installed = (0, config_js_1.loadInstalled)();
|
|
@@ -155,17 +154,7 @@ function registerInit(program) {
|
|
|
155
154
|
if (isBuilder && command_adapter_js_1.BUILDER_COMMANDS.length > 0) {
|
|
156
155
|
// 도구 선택
|
|
157
156
|
let targetToolIds;
|
|
158
|
-
if (opts.
|
|
159
|
-
const installed = detected.filter((tool) => {
|
|
160
|
-
const cmdDir = path_1.default.join(projectPath, tool.skillsDir, 'commands', 'relay');
|
|
161
|
-
return fs_1.default.existsSync(cmdDir);
|
|
162
|
-
});
|
|
163
|
-
if (installed.length === 0 && !json) {
|
|
164
|
-
console.error('로컬에 설치된 relay Builder 커맨드가 없습니다.');
|
|
165
|
-
}
|
|
166
|
-
targetToolIds = installed.map((t) => t.value);
|
|
167
|
-
}
|
|
168
|
-
else if (opts.tools) {
|
|
157
|
+
if (opts.tools) {
|
|
169
158
|
targetToolIds = resolveTools(opts.tools);
|
|
170
159
|
}
|
|
171
160
|
else if (!autoMode) {
|
|
@@ -240,9 +229,9 @@ function registerInit(program) {
|
|
|
240
229
|
}));
|
|
241
230
|
}
|
|
242
231
|
else {
|
|
243
|
-
console.log(`\n\x1b[32m✓ relay
|
|
232
|
+
console.log(`\n\x1b[32m✓ relay 초기화 완료\x1b[0m\n`);
|
|
244
233
|
// 글로벌
|
|
245
|
-
|
|
234
|
+
{
|
|
246
235
|
const toolNames = globalTools.length > 0 ? globalTools.join(', ') : 'Claude Code';
|
|
247
236
|
console.log(` \x1b[36mUser 커맨드 (글로벌)\x1b[0m — ${globalStatus === 'updated' ? '업데이트됨' : '설치됨'}`);
|
|
248
237
|
console.log(` 감지된 CLI: \x1b[36m${toolNames}\x1b[0m`);
|
|
@@ -251,10 +240,6 @@ function registerInit(program) {
|
|
|
251
240
|
}
|
|
252
241
|
console.log();
|
|
253
242
|
}
|
|
254
|
-
else {
|
|
255
|
-
console.log(` \x1b[36mUser 커맨드 (글로벌)\x1b[0m — 이미 설치됨`);
|
|
256
|
-
console.log();
|
|
257
|
-
}
|
|
258
243
|
// 로컬 Builder
|
|
259
244
|
if (localResults.length > 0) {
|
|
260
245
|
console.log(` \x1b[36mBuilder 커맨드 (로컬)\x1b[0m`);
|
package/dist/commands/install.js
CHANGED
|
@@ -99,8 +99,7 @@ function registerInstall(program) {
|
|
|
99
99
|
}
|
|
100
100
|
else {
|
|
101
101
|
// Normal registry install
|
|
102
|
-
|
|
103
|
-
parsed = await (0, slug_js_1.resolveSlug)(resolveInput);
|
|
102
|
+
parsed = await (0, slug_js_1.resolveSlug)(slugInput);
|
|
104
103
|
slug = parsed.full;
|
|
105
104
|
try {
|
|
106
105
|
team = await (0, api_js_1.fetchTeamInfo)(slug);
|
|
@@ -108,20 +107,86 @@ function registerInstall(program) {
|
|
|
108
107
|
catch (fetchErr) {
|
|
109
108
|
const fetchMsg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
|
|
110
109
|
if (fetchMsg.includes('403')) {
|
|
111
|
-
if
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
110
|
+
// Parse join_policy and membership_status from error body if available
|
|
111
|
+
let joinPolicy;
|
|
112
|
+
let membershipStatus;
|
|
113
|
+
try {
|
|
114
|
+
const errBody = JSON.parse(fetchMsg.replace(/^.*?(\{)/, '{'));
|
|
115
|
+
joinPolicy = typeof errBody.join_policy === 'string' ? errBody.join_policy : undefined;
|
|
116
|
+
membershipStatus = typeof errBody.membership_status === 'string' ? errBody.membership_status : undefined;
|
|
117
|
+
}
|
|
118
|
+
catch { /* ignore parse errors */ }
|
|
119
|
+
if (joinPolicy === 'auto') {
|
|
120
|
+
// Auto-join the Space then retry install
|
|
121
|
+
if (!json) {
|
|
122
|
+
console.error(`\x1b[33m⚙ Space에 자동 가입합니다...\x1b[0m`);
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const spaceSlug = parsed.owner;
|
|
126
|
+
await (0, join_js_1.joinSpace)(spaceSlug, '');
|
|
127
|
+
if (!json) {
|
|
128
|
+
console.error(`\x1b[32m✓ Space에 가입했습니다\x1b[0m`);
|
|
129
|
+
}
|
|
130
|
+
team = await (0, api_js_1.fetchTeamInfo)(slug);
|
|
131
|
+
}
|
|
132
|
+
catch (joinErr) {
|
|
133
|
+
const joinMsg = joinErr instanceof Error ? joinErr.message : String(joinErr);
|
|
134
|
+
if (json) {
|
|
135
|
+
console.error(JSON.stringify({ error: 'JOIN_FAILED', message: joinMsg, slug }));
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
console.error(`\x1b[31mSpace 가입 실패: ${joinMsg}\x1b[0m`);
|
|
139
|
+
}
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else if (joinPolicy === 'approval') {
|
|
144
|
+
const spaceSlug = parsed.owner;
|
|
145
|
+
if (json) {
|
|
146
|
+
console.error(JSON.stringify({
|
|
147
|
+
error: 'APPROVAL_REQUIRED',
|
|
148
|
+
message: `가입 신청이 필요합니다. \`relay join @${spaceSlug}\`로 가입하세요.`,
|
|
149
|
+
slug,
|
|
150
|
+
spaceSlug,
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
console.error(`\x1b[33m가입 신청이 필요합니다. \`relay join @${spaceSlug}\`로 가입하세요.\x1b[0m`);
|
|
155
|
+
}
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
else if (membershipStatus === 'member') {
|
|
159
|
+
// Member but no access to this specific team
|
|
160
|
+
if (json) {
|
|
161
|
+
console.error(JSON.stringify({
|
|
162
|
+
error: 'NO_ACCESS',
|
|
163
|
+
message: '이 팀에 대한 접근 권한이 없습니다.',
|
|
164
|
+
slug,
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
console.error('\x1b[31m이 팀에 대한 접근 권한이 없습니다.\x1b[0m');
|
|
169
|
+
}
|
|
170
|
+
process.exit(1);
|
|
117
171
|
}
|
|
118
172
|
else {
|
|
119
|
-
|
|
120
|
-
|
|
173
|
+
if (json) {
|
|
174
|
+
console.error(JSON.stringify({
|
|
175
|
+
error: 'SPACE_ONLY',
|
|
176
|
+
message: '이 팀은 Space 멤버만 설치 가능합니다.',
|
|
177
|
+
slug,
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
console.error('\x1b[31m이 팀은 Space 멤버만 설치 가능합니다.\x1b[0m');
|
|
182
|
+
console.error('\x1b[33mSpace 관리자에게 초대 코드를 요청하세요.\x1b[0m');
|
|
183
|
+
}
|
|
184
|
+
process.exit(1);
|
|
121
185
|
}
|
|
122
|
-
process.exit(1);
|
|
123
186
|
}
|
|
124
|
-
|
|
187
|
+
else {
|
|
188
|
+
throw fetchErr;
|
|
189
|
+
}
|
|
125
190
|
}
|
|
126
191
|
}
|
|
127
192
|
const teamDir = path_1.default.join(projectPath, '.relay', 'teams', parsed.owner, parsed.name);
|
package/dist/commands/list.js
CHANGED
|
@@ -20,10 +20,10 @@ function registerList(program) {
|
|
|
20
20
|
program
|
|
21
21
|
.command('list')
|
|
22
22
|
.description('설치된 에이전트 팀 목록')
|
|
23
|
-
.option('--space <slug>', 'Space
|
|
23
|
+
.option('--space <slug>', 'Space 팀 목록 조회')
|
|
24
24
|
.action(async (opts) => {
|
|
25
25
|
const json = program.opts().json ?? false;
|
|
26
|
-
// --space 옵션: Space
|
|
26
|
+
// --space 옵션: Space 팀 목록
|
|
27
27
|
if (opts.space) {
|
|
28
28
|
const spaceSlug = opts.space;
|
|
29
29
|
const token = await (0, config_js_1.getValidToken)();
|
package/dist/commands/publish.js
CHANGED
|
@@ -279,7 +279,7 @@ async function publishToApi(token, tarPath, metadata) {
|
|
|
279
279
|
function registerPublish(program) {
|
|
280
280
|
program
|
|
281
281
|
.command('publish')
|
|
282
|
-
.description('현재 팀 패키지를
|
|
282
|
+
.description('현재 팀 패키지를 Space에 배포합니다 (relay.yaml 필요)')
|
|
283
283
|
.option('--token <token>', '인증 토큰')
|
|
284
284
|
.action(async (opts) => {
|
|
285
285
|
const json = program.opts().json ?? false;
|
|
@@ -404,36 +404,97 @@ function registerPublish(program) {
|
|
|
404
404
|
console.error(` → relay.yaml에 version: ${newVersion} 저장됨\n`);
|
|
405
405
|
}
|
|
406
406
|
}
|
|
407
|
+
// Validate structure (콘텐츠는 .relay/ 안에 있음)
|
|
408
|
+
const hasDirs = VALID_DIRS.some((d) => {
|
|
409
|
+
const dirPath = path_1.default.join(relayDir, d);
|
|
410
|
+
if (!fs_1.default.existsSync(dirPath))
|
|
411
|
+
return false;
|
|
412
|
+
return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.')).length > 0;
|
|
413
|
+
});
|
|
414
|
+
if (!hasDirs) {
|
|
415
|
+
console.error(JSON.stringify({
|
|
416
|
+
error: 'EMPTY_PACKAGE',
|
|
417
|
+
message: '.relay/ 안에 skills/, agents/, rules/, commands/ 중 하나 이상에 파일이 있어야 합니다.',
|
|
418
|
+
}));
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
// Get token (checked before tarball creation)
|
|
422
|
+
const token = opts.token ?? process.env.RELAY_TOKEN ?? await (0, config_js_1.getValidToken)();
|
|
423
|
+
if (!token) {
|
|
424
|
+
console.error(JSON.stringify({
|
|
425
|
+
error: 'NO_TOKEN',
|
|
426
|
+
message: '인증이 필요합니다. `relay login`을 먼저 실행하세요.',
|
|
427
|
+
}));
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
// Fetch user's Spaces and select publish target
|
|
431
|
+
let selectedSpaceId;
|
|
432
|
+
let selectedSpaceIsPersonal = true;
|
|
433
|
+
try {
|
|
434
|
+
const { fetchMySpaces } = await import('./spaces.js');
|
|
435
|
+
const spaces = await fetchMySpaces(token);
|
|
436
|
+
const personalSpace = spaces.find((s) => s.is_personal);
|
|
437
|
+
const teamSpaces = spaces.filter((s) => !s.is_personal);
|
|
438
|
+
if (isTTY) {
|
|
439
|
+
if (spaces.length === 0) {
|
|
440
|
+
// No spaces at all — publish without space_id
|
|
441
|
+
console.error('\x1b[33m⚠ 소속 Space가 없습니다. 개인 계정으로 배포합니다.\x1b[0m\n');
|
|
442
|
+
}
|
|
443
|
+
else if (spaces.length === 1 && personalSpace) {
|
|
444
|
+
// Only personal Space — auto-select
|
|
445
|
+
selectedSpaceId = personalSpace.id;
|
|
446
|
+
selectedSpaceIsPersonal = true;
|
|
447
|
+
console.error(`\x1b[2m Space: 개인 스페이스 (${personalSpace.slug})\x1b[0m\n`);
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
// Multiple spaces — prompt user
|
|
451
|
+
const { select: selectSpace } = await import('@inquirer/prompts');
|
|
452
|
+
const spaceChoices = [
|
|
453
|
+
...(personalSpace ? [{ name: `개인 스페이스 (${personalSpace.slug})`, value: personalSpace.id, isPersonal: true }] : []),
|
|
454
|
+
...teamSpaces.map((s) => ({ name: `${s.name} (${s.slug})`, value: s.id, isPersonal: false })),
|
|
455
|
+
];
|
|
456
|
+
const chosenId = await selectSpace({
|
|
457
|
+
message: '어떤 Space에 배포할까요?',
|
|
458
|
+
choices: spaceChoices.map((c) => ({ name: c.name, value: c.value })),
|
|
459
|
+
});
|
|
460
|
+
const chosen = spaceChoices.find((c) => c.value === chosenId);
|
|
461
|
+
selectedSpaceId = chosenId;
|
|
462
|
+
selectedSpaceIsPersonal = chosen?.isPersonal ?? false;
|
|
463
|
+
const chosenLabel = chosen?.name ?? chosenId;
|
|
464
|
+
console.error(` → Space: ${chosenLabel}\n`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
else if (personalSpace) {
|
|
468
|
+
selectedSpaceId = personalSpace.id;
|
|
469
|
+
selectedSpaceIsPersonal = true;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
// Space 조회 실패 시 무시하고 계속 진행
|
|
474
|
+
}
|
|
475
|
+
// Visibility default based on Space type: personal → public, team Space → private
|
|
476
|
+
const defaultVisibility = selectedSpaceIsPersonal ? 'public' : 'private';
|
|
477
|
+
const defaultVisLabel = defaultVisibility === 'public'
|
|
478
|
+
? '공개 (개인 Space 기본값)'
|
|
479
|
+
: '비공개 (팀 Space 기본값)';
|
|
407
480
|
// Visibility validation: must be explicitly set
|
|
408
481
|
if (!config.visibility) {
|
|
409
482
|
if (isTTY) {
|
|
410
483
|
const { select: promptSelect } = await import('@inquirer/prompts');
|
|
411
|
-
console.error(
|
|
412
|
-
// Show user's spaces to help decide
|
|
413
|
-
try {
|
|
414
|
-
const { fetchMySpaces } = await import('./spaces.js');
|
|
415
|
-
const spaceToken = opts.token ?? process.env.RELAY_TOKEN ?? await (0, config_js_1.getValidToken)();
|
|
416
|
-
if (spaceToken) {
|
|
417
|
-
const spaces = await fetchMySpaces(spaceToken);
|
|
418
|
-
const nonPersonal = spaces.filter((s) => !s.is_personal);
|
|
419
|
-
if (nonPersonal.length > 0) {
|
|
420
|
-
console.error(`\n 내 Space:`);
|
|
421
|
-
for (const s of nonPersonal) {
|
|
422
|
-
console.error(` \x1b[36m${s.slug}\x1b[0m — ${s.name}`);
|
|
423
|
-
}
|
|
424
|
-
console.error(`\n 비공개로 설정하면 위 Space 멤버만 접근할 수 있습니다.\n`);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
catch {
|
|
429
|
-
// Space 조회 실패는 무시 — visibility 선택은 계속 진행
|
|
430
|
-
}
|
|
484
|
+
console.error(`\n\x1b[33m⚠ relay.yaml에 visibility가 설정되지 않았습니다.\x1b[0m (기본값: ${defaultVisLabel})`);
|
|
431
485
|
config.visibility = await promptSelect({
|
|
432
486
|
message: '공개 범위를 선택하세요:',
|
|
433
487
|
choices: [
|
|
434
|
-
{
|
|
435
|
-
|
|
488
|
+
{
|
|
489
|
+
name: `공개 — Space에 누구나 검색·설치${defaultVisibility === 'public' ? ' ✓ 추천' : ''}`,
|
|
490
|
+
value: 'public',
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
name: `비공개 — Space 멤버만 접근${defaultVisibility === 'private' ? ' ✓ 추천' : ''}`,
|
|
494
|
+
value: 'private',
|
|
495
|
+
},
|
|
436
496
|
],
|
|
497
|
+
default: defaultVisibility,
|
|
437
498
|
});
|
|
438
499
|
// Save back to relay.yaml
|
|
439
500
|
const yamlData = js_yaml_1.default.load(yamlContent);
|
|
@@ -453,8 +514,8 @@ function registerPublish(program) {
|
|
|
453
514
|
if (isTTY) {
|
|
454
515
|
const { select: promptConfirmVis } = await import('@inquirer/prompts');
|
|
455
516
|
const visLabel = config.visibility === 'public'
|
|
456
|
-
?
|
|
457
|
-
:
|
|
517
|
+
? `\x1b[32m공개\x1b[0m (Space에 누구나 검색·설치)${defaultVisibility === 'public' ? ' ✓ 추천' : ''}`
|
|
518
|
+
: `\x1b[33m비공개\x1b[0m (Space 멤버만 접근)${defaultVisibility === 'private' ? ' ✓ 추천' : ''}`;
|
|
458
519
|
const visAction = await promptConfirmVis({
|
|
459
520
|
message: `공개 범위: ${config.visibility === 'public' ? '공개' : '비공개'} — 유지할까요?`,
|
|
460
521
|
choices: [
|
|
@@ -475,29 +536,6 @@ function registerPublish(program) {
|
|
|
475
536
|
if (isTTY) {
|
|
476
537
|
console.error('💡 프로필에 연락처를 설정하면 설치 시 명함이 전달됩니다: www.relayax.com/dashboard/profile');
|
|
477
538
|
}
|
|
478
|
-
// Validate structure (콘텐츠는 .relay/ 안에 있음)
|
|
479
|
-
const hasDirs = VALID_DIRS.some((d) => {
|
|
480
|
-
const dirPath = path_1.default.join(relayDir, d);
|
|
481
|
-
if (!fs_1.default.existsSync(dirPath))
|
|
482
|
-
return false;
|
|
483
|
-
return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.')).length > 0;
|
|
484
|
-
});
|
|
485
|
-
if (!hasDirs) {
|
|
486
|
-
console.error(JSON.stringify({
|
|
487
|
-
error: 'EMPTY_PACKAGE',
|
|
488
|
-
message: '.relay/ 안에 skills/, agents/, rules/, commands/ 중 하나 이상에 파일이 있어야 합니다.',
|
|
489
|
-
}));
|
|
490
|
-
process.exit(1);
|
|
491
|
-
}
|
|
492
|
-
// Get token (checked before tarball creation)
|
|
493
|
-
const token = opts.token ?? process.env.RELAY_TOKEN ?? await (0, config_js_1.getValidToken)();
|
|
494
|
-
if (!token) {
|
|
495
|
-
console.error(JSON.stringify({
|
|
496
|
-
error: 'NO_TOKEN',
|
|
497
|
-
message: '인증이 필요합니다. `relay login`을 먼저 실행하세요.',
|
|
498
|
-
}));
|
|
499
|
-
process.exit(1);
|
|
500
|
-
}
|
|
501
539
|
const detectedCommands = detectCommands(relayDir);
|
|
502
540
|
const components = {
|
|
503
541
|
agents: countDir(relayDir, 'agents'),
|
|
@@ -525,6 +563,7 @@ function registerPublish(program) {
|
|
|
525
563
|
type: config.type ?? 'hybrid',
|
|
526
564
|
agent_details: detectedAgents,
|
|
527
565
|
skill_details: detectedSkills,
|
|
566
|
+
...(selectedSpaceId ? { space_id: selectedSpaceId } : {}),
|
|
528
567
|
};
|
|
529
568
|
if (!json) {
|
|
530
569
|
console.error(`패키지 생성 중... (${config.name} v${config.version})`);
|
|
@@ -617,7 +656,8 @@ function registerPublish(program) {
|
|
|
617
656
|
console.log('```');
|
|
618
657
|
guideLines.forEach((line) => console.log(line));
|
|
619
658
|
console.log('```');
|
|
620
|
-
|
|
659
|
+
const detailSlug = result.slug.startsWith('@') ? result.slug.slice(1) : result.slug;
|
|
660
|
+
console.log(`\n \x1b[90m상세페이지: \x1b[36mrelayax.com/@${detailSlug}\x1b[0m`);
|
|
621
661
|
}
|
|
622
662
|
}
|
|
623
663
|
}
|
package/dist/commands/search.js
CHANGED
|
@@ -26,19 +26,24 @@ function formatTable(results) {
|
|
|
26
26
|
function registerSearch(program) {
|
|
27
27
|
program
|
|
28
28
|
.command('search <keyword>')
|
|
29
|
-
.description('에이전트 팀 검색')
|
|
29
|
+
.description('Space에서 에이전트 팀 검색 (공개 팀 + 내 Space 팀)')
|
|
30
30
|
.option('--tag <tag>', '태그로 필터링')
|
|
31
|
+
.option('--space <space>', '특정 Space 내에서 검색')
|
|
31
32
|
.action(async (keyword, opts) => {
|
|
32
33
|
const json = program.opts().json ?? false;
|
|
33
34
|
try {
|
|
34
|
-
const results = await (0, api_js_1.searchTeams)(keyword, opts.tag);
|
|
35
|
+
const results = await (0, api_js_1.searchTeams)(keyword, opts.tag, opts.space);
|
|
35
36
|
if (json) {
|
|
36
37
|
console.log(JSON.stringify({ results }));
|
|
37
38
|
}
|
|
38
39
|
else {
|
|
39
|
-
|
|
40
|
+
const spaceSuffix = opts.space ? ` Space: \x1b[35m@${opts.space}\x1b[0m` : '';
|
|
41
|
+
console.log(`\n검색어: \x1b[36m${keyword}\x1b[0m${opts.tag ? ` 태그: \x1b[33m${opts.tag}\x1b[0m` : ''}${spaceSuffix}\n`);
|
|
40
42
|
console.log(formatTable(results));
|
|
41
43
|
console.log(`\n총 ${results.length}건`);
|
|
44
|
+
if (!opts.space && results.length === 0) {
|
|
45
|
+
console.log('\x1b[33m💡 내 Space에서 검색하려면: relay search <keyword> --space <space-slug>\x1b[0m');
|
|
46
|
+
}
|
|
42
47
|
}
|
|
43
48
|
}
|
|
44
49
|
catch (err) {
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TeamRegistryInfo, SearchResult } from '../types.js';
|
|
2
2
|
export declare function fetchTeamInfo(slug: string): Promise<TeamRegistryInfo>;
|
|
3
|
-
export declare function searchTeams(query: string, tag?: string): Promise<SearchResult[]>;
|
|
3
|
+
export declare function searchTeams(query: string, tag?: string, space?: string): Promise<SearchResult[]>;
|
|
4
4
|
export interface TeamVersionInfo {
|
|
5
5
|
version: string;
|
|
6
6
|
changelog: string | null;
|
package/dist/lib/api.js
CHANGED
|
@@ -19,10 +19,12 @@ async function fetchTeamInfo(slug) {
|
|
|
19
19
|
}
|
|
20
20
|
return res.json();
|
|
21
21
|
}
|
|
22
|
-
async function searchTeams(query, tag) {
|
|
22
|
+
async function searchTeams(query, tag, space) {
|
|
23
23
|
const params = new URLSearchParams({ q: query });
|
|
24
24
|
if (tag)
|
|
25
25
|
params.set('tag', tag);
|
|
26
|
+
if (space)
|
|
27
|
+
params.set('space', space);
|
|
26
28
|
const url = `${config_js_1.API_URL}/api/registry/search?${params.toString()}`;
|
|
27
29
|
const res = await fetch(url);
|
|
28
30
|
if (!res.ok) {
|
|
@@ -111,32 +111,32 @@ JSON 결과 예시:
|
|
|
111
111
|
exports.USER_COMMANDS = [
|
|
112
112
|
{
|
|
113
113
|
id: 'relay-install',
|
|
114
|
-
description: 'relay
|
|
115
|
-
body: `요청된 에이전트 팀을 relay
|
|
114
|
+
description: 'relay Space에서 에이전트 팀을 설치합니다',
|
|
115
|
+
body: `요청된 에이전트 팀을 relay Space에서 다운로드하고, 현재 에이전트 환경에 맞게 구성합니다.
|
|
116
116
|
인자 없이 호출하면 인터랙티브 탐색 모드로 진입합니다.
|
|
117
117
|
|
|
118
118
|
## 인터랙션 플로우
|
|
119
119
|
|
|
120
120
|
이 커맨드는 3단계 인터랙션으로 진행됩니다. 각 단계에서 반드시 AskUserQuestion 도구를 사용하세요.
|
|
121
121
|
|
|
122
|
-
### Step 1.
|
|
122
|
+
### Step 1. Space 선택 & 팀 탐색 (slug가 없을 때만)
|
|
123
123
|
|
|
124
124
|
slug가 직접 주어지면 (\`/relay-install @alice/doc-writer\`) 이 단계를 건너뛰고 Step 2로 갑니다.
|
|
125
125
|
|
|
126
|
-
#### 1-1.
|
|
126
|
+
#### 1-1. Space 선택
|
|
127
127
|
\`relay spaces --json\` 을 실행하여 사용자의 Space 목록을 가져옵니다.
|
|
128
128
|
|
|
129
129
|
**AskUserQuestion 호출:**
|
|
130
130
|
- question: "어디서 팀을 찾을까요?"
|
|
131
|
-
- options: Space가 있으면 \`["
|
|
131
|
+
- options: Space가 있으면 \`["<space1_name> (개인)", "<space2_name>", ...]\`, 없으면 이 단계를 건너뛰고 바로 내 Space 탐색으로 진행
|
|
132
132
|
|
|
133
133
|
**응답 처리:**
|
|
134
|
-
-
|
|
135
|
-
- Space 이름 선택 → 1-3. Space 팀 목록으로 진행
|
|
134
|
+
- Space 이름 선택 → 1-2. Space 팀 탐색으로 진행
|
|
136
135
|
|
|
137
|
-
#### 1-2.
|
|
138
|
-
|
|
136
|
+
#### 1-2. Space 팀 탐색
|
|
137
|
+
선택된 Space에서 팀을 검색합니다.
|
|
139
138
|
\`relay search <keyword>\` 명령어를 실행합니다 (필요하면 여러 키워드로 반복).
|
|
139
|
+
또는 \`relay list --space <space-slug> --json\`으로 전체 목록을 가져옵니다.
|
|
140
140
|
|
|
141
141
|
검색 결과를 번호 리스트로 보여줍니다:
|
|
142
142
|
|
|
@@ -155,12 +155,13 @@ slug가 직접 주어지면 (\`/relay-install @alice/doc-writer\`) 이 단계를
|
|
|
155
155
|
|
|
156
156
|
**AskUserQuestion 호출:**
|
|
157
157
|
- question: "어떤 팀을 설치할까요?"
|
|
158
|
-
- options: \`["1", "2", "3", "다시 검색"]\`
|
|
158
|
+
- options: \`["1", "2", "3", "다시 검색", "돌아가기"]\`
|
|
159
159
|
|
|
160
160
|
"다시 검색" → 새 키워드로 1-2 반복
|
|
161
|
+
"돌아가기" → 1-1로 돌아감
|
|
161
162
|
번호 선택 → 해당 팀의 slug로 설치 진행
|
|
162
163
|
|
|
163
|
-
#### 1-3. Space 팀 목록
|
|
164
|
+
#### 1-3. Space 팀 목록 (전체 보기)
|
|
164
165
|
\`relay list --space <space-slug> --json\` 을 실행합니다.
|
|
165
166
|
|
|
166
167
|
팀 목록을 번호 리스트로 보여줍니다 (1-2와 동일 형식).
|
|
@@ -175,9 +176,9 @@ slug가 직접 주어지면 (\`/relay-install @alice/doc-writer\`) 이 단계를
|
|
|
175
176
|
### Step 2. 설치 & 배치 범위 선택
|
|
176
177
|
|
|
177
178
|
#### 2-1. 패키지 다운로드
|
|
178
|
-
\`relay install <@
|
|
179
|
-
-
|
|
180
|
-
- Space
|
|
179
|
+
\`relay install <@space/team> --json\` 명령어를 실행합니다.
|
|
180
|
+
- 공개 팀: \`relay install <@space/team> --json\`
|
|
181
|
+
- Space 팀 (비공개): \`relay install @<space-slug>/<team-slug> --json\`
|
|
181
182
|
- Space 가입이 필요하면: \`relay join <space-slug> --code <invite-code>\` 를 먼저 실행합니다.
|
|
182
183
|
- 또는 \`--join-code <code>\`로 가입+설치를 한번에 할 수 있습니다.
|
|
183
184
|
- CLI가 init과 login을 자동으로 처리합니다 (사용자가 별도 실행할 필요 없음).
|
|
@@ -311,9 +312,11 @@ npm install -g relayax-cli
|
|
|
311
312
|
relay login
|
|
312
313
|
relay install {slug}
|
|
313
314
|
|
|
314
|
-
상세페이지: relayax.com/
|
|
315
|
+
상세페이지: relayax.com/@{owner}/{team}
|
|
315
316
|
\`\`\`
|
|
316
317
|
|
|
318
|
+
- \`{slug}\`가 \`@owner/team\` 형식이면 상세페이지는 \`relayax.com/@{owner}/{team}\`으로 치환합니다.
|
|
319
|
+
|
|
317
320
|
- type 기반 사용법도 포함:
|
|
318
321
|
- command/hybrid: \`# 사용법: /{첫번째 커맨드}\`
|
|
319
322
|
- passive: \`# 설치하면 자동 적용됩니다\`
|
|
@@ -330,8 +333,8 @@ relay install {slug}
|
|
|
330
333
|
|
|
331
334
|
### 인터랙티브 모드 (/relay-install)
|
|
332
335
|
→ relay spaces --json 실행
|
|
333
|
-
→ AskUserQuestion: "어디서 팀을 찾을까요?" → ["
|
|
334
|
-
→ "
|
|
336
|
+
→ AskUserQuestion: "어디서 팀을 찾을까요?" → ["개인 Space (alice)", "Acme Corp"]
|
|
337
|
+
→ "개인 Space" 선택 → "어떤 팀을 찾고 계세요?"
|
|
335
338
|
→ relay search "문서" 실행 → 결과 리스트 표시
|
|
336
339
|
→ AskUserQuestion: "어떤 팀을 설치할까요?" → ["1", "2", "3", "다시 검색"]
|
|
337
340
|
→ "1" 선택 (@alice/doc-writer)
|
|
@@ -464,8 +467,8 @@ ${LOGIN_JIT_GUIDE}
|
|
|
464
467
|
},
|
|
465
468
|
{
|
|
466
469
|
id: 'relay-publish',
|
|
467
|
-
description: '현재 팀 패키지를 relay
|
|
468
|
-
body: `현재 디렉토리의 에이전트 팀(.relay/)을 분석하고, 보안 점검 및 requirements를 구성한 뒤, 사용가이드를 생성하고 relay
|
|
470
|
+
description: '현재 팀 패키지를 relay Space에 배포합니다',
|
|
471
|
+
body: `현재 디렉토리의 에이전트 팀(.relay/)을 분석하고, 보안 점검 및 requirements를 구성한 뒤, 사용가이드를 생성하고 relay Space에 배포합니다.
|
|
469
472
|
|
|
470
473
|
## 사전 준비 (자동)
|
|
471
474
|
|
|
@@ -484,7 +487,7 @@ ${LOGIN_JIT_GUIDE}
|
|
|
484
487
|
- 기본값으로 현재 디렉토리명을 제안합니다.
|
|
485
488
|
|
|
486
489
|
3. **AskUserQuestion 호출:**
|
|
487
|
-
- question: "팀을 한 줄로 설명해주세요 (
|
|
490
|
+
- question: "팀을 한 줄로 설명해주세요 (Space에 표시됩니다)"
|
|
488
491
|
- 기본값으로 적절한 값을 만들어줍니다.
|
|
489
492
|
|
|
490
493
|
4. 자동 처리:
|
|
@@ -540,7 +543,7 @@ relay.yaml의 \`visibility\` 설정을 확인합니다.
|
|
|
540
543
|
|
|
541
544
|
**AskUserQuestion 호출:**
|
|
542
545
|
- question: "어디에 배포할까요?"
|
|
543
|
-
- options: \`["공개 (
|
|
546
|
+
- options: \`["공개 (Space 공개)", "비공개 (Space 전용)"]\`
|
|
544
547
|
|
|
545
548
|
**응답 처리:**
|
|
546
549
|
- "공개" → relay.yaml에 \`visibility: public\` 저장
|
|
@@ -560,7 +563,7 @@ relay.yaml의 \`visibility\` 설정을 확인합니다.
|
|
|
560
563
|
현재 설정을 확인합니다:
|
|
561
564
|
|
|
562
565
|
**AskUserQuestion 호출:**
|
|
563
|
-
- question: 공개일 때 "현재 **공개** 설정입니다.
|
|
566
|
+
- question: 공개일 때 "현재 **공개** 설정입니다. Space에 공개됩니다. 유지할까요?", 비공개일 때 "현재 **비공개** 설정입니다 (Space: {name}). 유지할까요?"
|
|
564
567
|
- options: \`["유지", "변경"]\`
|
|
565
568
|
|
|
566
569
|
"변경" → 신규 배포와 동일한 플로우
|
|
@@ -654,7 +657,7 @@ requires:
|
|
|
654
657
|
배포 요약
|
|
655
658
|
|
|
656
659
|
팀: my-team v1.0.0
|
|
657
|
-
공개:
|
|
660
|
+
공개: Space 공개
|
|
658
661
|
Skills: 3개, Commands: 5개
|
|
659
662
|
requires: env 2개, cli 1개
|
|
660
663
|
\`\`\`
|
|
@@ -668,7 +671,7 @@ requires: env 2개, cli 1개
|
|
|
668
671
|
- "취소" → 중단
|
|
669
672
|
|
|
670
673
|
#### 3-3. 배포 완료 & 온보딩 가이드
|
|
671
|
-
- 배포 결과와
|
|
674
|
+
- 배포 결과와 Space URL을 보여줍니다.
|
|
672
675
|
- \`relay publish\` 출력 끝에 코드블록 형태의 온보딩 가이드가 포함됩니다.
|
|
673
676
|
- 이 코드블록을 사용자에게 그대로 보여줍니다.
|
|
674
677
|
- 출력에 코드블록이 없으면 아래 형태로 직접 생성합니다:
|
|
@@ -687,7 +690,7 @@ ${BUSINESS_CARD_FORMAT}
|
|
|
687
690
|
|
|
688
691
|
사용자: /relay-publish
|
|
689
692
|
→ 인증 확인 ✓, 팀 구조 분석 (skills 3개, commands 5개)
|
|
690
|
-
→ AskUserQuestion: "어디에 배포할까요?" → ["공개 (
|
|
693
|
+
→ AskUserQuestion: "어디에 배포할까요?" → ["공개 (Space 공개)", "비공개 (Space 전용)"]
|
|
691
694
|
→ "공개" 선택
|
|
692
695
|
→ 보안 스캔 ✓ 시크릿 없음 → requires 분석 결과 표시
|
|
693
696
|
→ AskUserQuestion: "requires 설정이 맞나요?" → ["확인", "수정"]
|
|
@@ -695,7 +698,7 @@ ${BUSINESS_CARD_FORMAT}
|
|
|
695
698
|
→ 배포 요약 표시
|
|
696
699
|
→ AskUserQuestion: "이대로 배포할까요?" → ["배포", "취소"]
|
|
697
700
|
→ "배포" → relay publish 실행
|
|
698
|
-
→ "배포 완료! URL: https://relayax.com/
|
|
701
|
+
→ "배포 완료! URL: https://relayax.com/@my-space/my-team"
|
|
699
702
|
→ 온보딩 가이드 코드블록 표시`,
|
|
700
703
|
},
|
|
701
704
|
];
|
package/dist/lib/config.js
CHANGED
|
@@ -121,6 +121,19 @@ async function getValidToken() {
|
|
|
121
121
|
return undefined;
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* `@spaces/{spaceSlug}/{teamSlug}` 레거시 키를 `@{spaceSlug}/{teamSlug}`로 정규화한다.
|
|
126
|
+
* installed.json 로드 시 자동 적용되어 모든 소비자가 일관된 키를 사용한다.
|
|
127
|
+
*/
|
|
128
|
+
function normalizeInstalledRegistry(raw) {
|
|
129
|
+
const normalized = {};
|
|
130
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
131
|
+
const m = key.match(/^@spaces\/([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)$/);
|
|
132
|
+
const normalizedKey = m ? `@${m[1]}/${m[2]}` : key;
|
|
133
|
+
normalized[normalizedKey] = value;
|
|
134
|
+
}
|
|
135
|
+
return normalized;
|
|
136
|
+
}
|
|
124
137
|
/** 프로젝트 로컬 installed.json 읽기 */
|
|
125
138
|
function loadInstalled() {
|
|
126
139
|
const file = path_1.default.join(process.cwd(), '.relay', 'installed.json');
|
|
@@ -128,7 +141,7 @@ function loadInstalled() {
|
|
|
128
141
|
return {};
|
|
129
142
|
}
|
|
130
143
|
try {
|
|
131
|
-
return JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
|
|
144
|
+
return normalizeInstalledRegistry(JSON.parse(fs_1.default.readFileSync(file, 'utf-8')));
|
|
132
145
|
}
|
|
133
146
|
catch {
|
|
134
147
|
return {};
|
|
@@ -147,7 +160,7 @@ function loadGlobalInstalled() {
|
|
|
147
160
|
if (!fs_1.default.existsSync(file))
|
|
148
161
|
return {};
|
|
149
162
|
try {
|
|
150
|
-
return JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
|
|
163
|
+
return normalizeInstalledRegistry(JSON.parse(fs_1.default.readFileSync(file, 'utf-8')));
|
|
151
164
|
}
|
|
152
165
|
catch {
|
|
153
166
|
return {};
|
package/dist/lib/preamble.js
CHANGED
|
@@ -17,7 +17,11 @@ const PREAMBLE_END = '<!-- RELAY_PREAMBLE_END -->';
|
|
|
17
17
|
* relay CLI가 있으면 사용, 없으면 curl fallback.
|
|
18
18
|
*/
|
|
19
19
|
function generatePreambleScript(slug, apiUrl) {
|
|
20
|
-
|
|
20
|
+
// slug format: @space/team → space=space, team=team
|
|
21
|
+
const stripped = slug.startsWith('@') ? slug.slice(1) : slug;
|
|
22
|
+
const slashIdx = stripped.indexOf('/');
|
|
23
|
+
const spacePart = slashIdx !== -1 ? stripped.slice(0, slashIdx) : stripped;
|
|
24
|
+
const teamPart = slashIdx !== -1 ? stripped.slice(slashIdx + 1) : stripped;
|
|
21
25
|
return `#!/usr/bin/env bash
|
|
22
26
|
# relay-preamble.sh — auto-generated by relay publish
|
|
23
27
|
set +e
|
|
@@ -28,7 +32,7 @@ DEVICE_HASH=$(echo "$HOSTNAME:$USER" | shasum -a 256 | cut -d' ' -f1)
|
|
|
28
32
|
if command -v relay &>/dev/null; then
|
|
29
33
|
relay ping "${slug}" --quiet 2>/dev/null &
|
|
30
34
|
else
|
|
31
|
-
curl -sf -X POST "${apiUrl}/api/
|
|
35
|
+
curl -sf -X POST "${apiUrl}/api/spaces/${spacePart}/teams/${teamPart}/ping" \\
|
|
32
36
|
-H "Content-Type: application/json" \\
|
|
33
37
|
-d "{\\"device_hash\\":\\"$DEVICE_HASH\\"}" \\
|
|
34
38
|
2>/dev/null &
|
package/dist/lib/slug.d.ts
CHANGED
|
@@ -15,5 +15,6 @@ export declare function isSimpleSlug(input: string): boolean;
|
|
|
15
15
|
/**
|
|
16
16
|
* Scoped 또는 단순 slug를 받아 ParsedSlug를 반환한다.
|
|
17
17
|
* 단순 slug는 서버에 resolve를 요청한다.
|
|
18
|
+
* `@spaces/{spaceSlug}/{teamSlug}` 형식은 `@{spaceSlug}/{teamSlug}`로 정규화된다.
|
|
18
19
|
*/
|
|
19
20
|
export declare function resolveSlug(input: string): Promise<ParsedSlug>;
|
package/dist/lib/slug.js
CHANGED
|
@@ -28,18 +28,22 @@ function isSimpleSlug(input) {
|
|
|
28
28
|
/**
|
|
29
29
|
* Scoped 또는 단순 slug를 받아 ParsedSlug를 반환한다.
|
|
30
30
|
* 단순 slug는 서버에 resolve를 요청한다.
|
|
31
|
+
* `@spaces/{spaceSlug}/{teamSlug}` 형식은 `@{spaceSlug}/{teamSlug}`로 정규화된다.
|
|
31
32
|
*/
|
|
32
33
|
async function resolveSlug(input) {
|
|
34
|
+
// @spaces/{spaceSlug}/{teamSlug} → @{spaceSlug}/{teamSlug} 정규화
|
|
35
|
+
const spacesPrefix = input.match(/^@spaces\/([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)$/);
|
|
36
|
+
const normalized = spacesPrefix ? `@${spacesPrefix[1]}/${spacesPrefix[2]}` : input;
|
|
33
37
|
// scoped slug면 바로 파싱
|
|
34
|
-
const parsed = parseSlug(
|
|
38
|
+
const parsed = parseSlug(normalized);
|
|
35
39
|
if (parsed)
|
|
36
40
|
return parsed;
|
|
37
41
|
// 단순 slug인지 검증
|
|
38
|
-
if (!isSimpleSlug(
|
|
42
|
+
if (!isSimpleSlug(normalized)) {
|
|
39
43
|
throw new Error(`잘못된 slug 형식입니다: '${input}'. @owner/name 또는 name 형태로 입력하세요.`);
|
|
40
44
|
}
|
|
41
45
|
// 서버에 resolve 요청
|
|
42
|
-
const results = await (0, api_js_1.resolveSlugFromServer)(
|
|
46
|
+
const results = await (0, api_js_1.resolveSlugFromServer)(normalized);
|
|
43
47
|
if (results.length === 0) {
|
|
44
48
|
throw new Error(`'${input}' 팀을 찾을 수 없습니다.`);
|
|
45
49
|
}
|