relayax-cli 0.2.37 → 0.2.39
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 +90 -51
- 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 +31 -30
- 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})`);
|
|
@@ -599,8 +638,7 @@ function registerPublish(program) {
|
|
|
599
638
|
if (isTTY) {
|
|
600
639
|
const guideLines = [
|
|
601
640
|
'npm install -g relayax-cli',
|
|
602
|
-
|
|
603
|
-
`relay install ${result.slug}`,
|
|
641
|
+
`/relay:relay-install ${result.slug}`,
|
|
604
642
|
];
|
|
605
643
|
// Type-based usage hint
|
|
606
644
|
const teamType = config.type ?? 'hybrid';
|
|
@@ -617,7 +655,8 @@ function registerPublish(program) {
|
|
|
617
655
|
console.log('```');
|
|
618
656
|
guideLines.forEach((line) => console.log(line));
|
|
619
657
|
console.log('```');
|
|
620
|
-
|
|
658
|
+
const detailSlug = result.slug.startsWith('@') ? result.slug.slice(1) : result.slug;
|
|
659
|
+
console.log(`\n \x1b[90m상세페이지: \x1b[36mrelayax.com/@${detailSlug}\x1b[0m`);
|
|
621
660
|
}
|
|
622
661
|
}
|
|
623
662
|
}
|
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을 자동으로 처리합니다 (사용자가 별도 실행할 필요 없음).
|
|
@@ -308,12 +309,13 @@ ${BUSINESS_CARD_FORMAT}
|
|
|
308
309
|
주변인에게 공유하세요:
|
|
309
310
|
|
|
310
311
|
npm install -g relayax-cli
|
|
311
|
-
relay
|
|
312
|
-
relay install {slug}
|
|
312
|
+
/relay:relay-install {slug}
|
|
313
313
|
|
|
314
|
-
상세페이지: relayax.com/
|
|
314
|
+
상세페이지: relayax.com/@{owner}/{team}
|
|
315
315
|
\`\`\`
|
|
316
316
|
|
|
317
|
+
- \`{slug}\`가 \`@owner/team\` 형식이면 상세페이지는 \`relayax.com/@{owner}/{team}\`으로 치환합니다.
|
|
318
|
+
|
|
317
319
|
- type 기반 사용법도 포함:
|
|
318
320
|
- command/hybrid: \`# 사용법: /{첫번째 커맨드}\`
|
|
319
321
|
- passive: \`# 설치하면 자동 적용됩니다\`
|
|
@@ -330,8 +332,8 @@ relay install {slug}
|
|
|
330
332
|
|
|
331
333
|
### 인터랙티브 모드 (/relay-install)
|
|
332
334
|
→ relay spaces --json 실행
|
|
333
|
-
→ AskUserQuestion: "어디서 팀을 찾을까요?" → ["
|
|
334
|
-
→ "
|
|
335
|
+
→ AskUserQuestion: "어디서 팀을 찾을까요?" → ["개인 Space (alice)", "Acme Corp"]
|
|
336
|
+
→ "개인 Space" 선택 → "어떤 팀을 찾고 계세요?"
|
|
335
337
|
→ relay search "문서" 실행 → 결과 리스트 표시
|
|
336
338
|
→ AskUserQuestion: "어떤 팀을 설치할까요?" → ["1", "2", "3", "다시 검색"]
|
|
337
339
|
→ "1" 선택 (@alice/doc-writer)
|
|
@@ -464,8 +466,8 @@ ${LOGIN_JIT_GUIDE}
|
|
|
464
466
|
},
|
|
465
467
|
{
|
|
466
468
|
id: 'relay-publish',
|
|
467
|
-
description: '현재 팀 패키지를 relay
|
|
468
|
-
body: `현재 디렉토리의 에이전트 팀(.relay/)을 분석하고, 보안 점검 및 requirements를 구성한 뒤, 사용가이드를 생성하고 relay
|
|
469
|
+
description: '현재 팀 패키지를 relay Space에 배포합니다',
|
|
470
|
+
body: `현재 디렉토리의 에이전트 팀(.relay/)을 분석하고, 보안 점검 및 requirements를 구성한 뒤, 사용가이드를 생성하고 relay Space에 배포합니다.
|
|
469
471
|
|
|
470
472
|
## 사전 준비 (자동)
|
|
471
473
|
|
|
@@ -484,7 +486,7 @@ ${LOGIN_JIT_GUIDE}
|
|
|
484
486
|
- 기본값으로 현재 디렉토리명을 제안합니다.
|
|
485
487
|
|
|
486
488
|
3. **AskUserQuestion 호출:**
|
|
487
|
-
- question: "팀을 한 줄로 설명해주세요 (
|
|
489
|
+
- question: "팀을 한 줄로 설명해주세요 (Space에 표시됩니다)"
|
|
488
490
|
- 기본값으로 적절한 값을 만들어줍니다.
|
|
489
491
|
|
|
490
492
|
4. 자동 처리:
|
|
@@ -540,7 +542,7 @@ relay.yaml의 \`visibility\` 설정을 확인합니다.
|
|
|
540
542
|
|
|
541
543
|
**AskUserQuestion 호출:**
|
|
542
544
|
- question: "어디에 배포할까요?"
|
|
543
|
-
- options: \`["공개 (
|
|
545
|
+
- options: \`["공개 (Space 공개)", "비공개 (Space 전용)"]\`
|
|
544
546
|
|
|
545
547
|
**응답 처리:**
|
|
546
548
|
- "공개" → relay.yaml에 \`visibility: public\` 저장
|
|
@@ -560,7 +562,7 @@ relay.yaml의 \`visibility\` 설정을 확인합니다.
|
|
|
560
562
|
현재 설정을 확인합니다:
|
|
561
563
|
|
|
562
564
|
**AskUserQuestion 호출:**
|
|
563
|
-
- question: 공개일 때 "현재 **공개** 설정입니다.
|
|
565
|
+
- question: 공개일 때 "현재 **공개** 설정입니다. Space에 공개됩니다. 유지할까요?", 비공개일 때 "현재 **비공개** 설정입니다 (Space: {name}). 유지할까요?"
|
|
564
566
|
- options: \`["유지", "변경"]\`
|
|
565
567
|
|
|
566
568
|
"변경" → 신규 배포와 동일한 플로우
|
|
@@ -654,7 +656,7 @@ requires:
|
|
|
654
656
|
배포 요약
|
|
655
657
|
|
|
656
658
|
팀: my-team v1.0.0
|
|
657
|
-
공개:
|
|
659
|
+
공개: Space 공개
|
|
658
660
|
Skills: 3개, Commands: 5개
|
|
659
661
|
requires: env 2개, cli 1개
|
|
660
662
|
\`\`\`
|
|
@@ -668,15 +670,14 @@ requires: env 2개, cli 1개
|
|
|
668
670
|
- "취소" → 중단
|
|
669
671
|
|
|
670
672
|
#### 3-3. 배포 완료 & 온보딩 가이드
|
|
671
|
-
- 배포 결과와
|
|
673
|
+
- 배포 결과와 Space URL을 보여줍니다.
|
|
672
674
|
- \`relay publish\` 출력 끝에 코드블록 형태의 온보딩 가이드가 포함됩니다.
|
|
673
675
|
- 이 코드블록을 사용자에게 그대로 보여줍니다.
|
|
674
676
|
- 출력에 코드블록이 없으면 아래 형태로 직접 생성합니다:
|
|
675
677
|
|
|
676
678
|
\\\`\\\`\\\`
|
|
677
679
|
npm install -g relayax-cli
|
|
678
|
-
relay
|
|
679
|
-
relay install <slug>
|
|
680
|
+
/relay:relay-install <slug>
|
|
680
681
|
\\\`\\\`\\\`
|
|
681
682
|
|
|
682
683
|
- \`<slug>\`는 배포된 팀의 실제 슬러그로 치환합니다.
|
|
@@ -687,7 +688,7 @@ ${BUSINESS_CARD_FORMAT}
|
|
|
687
688
|
|
|
688
689
|
사용자: /relay-publish
|
|
689
690
|
→ 인증 확인 ✓, 팀 구조 분석 (skills 3개, commands 5개)
|
|
690
|
-
→ AskUserQuestion: "어디에 배포할까요?" → ["공개 (
|
|
691
|
+
→ AskUserQuestion: "어디에 배포할까요?" → ["공개 (Space 공개)", "비공개 (Space 전용)"]
|
|
691
692
|
→ "공개" 선택
|
|
692
693
|
→ 보안 스캔 ✓ 시크릿 없음 → requires 분석 결과 표시
|
|
693
694
|
→ AskUserQuestion: "requires 설정이 맞나요?" → ["확인", "수정"]
|
|
@@ -695,7 +696,7 @@ ${BUSINESS_CARD_FORMAT}
|
|
|
695
696
|
→ 배포 요약 표시
|
|
696
697
|
→ AskUserQuestion: "이대로 배포할까요?" → ["배포", "취소"]
|
|
697
698
|
→ "배포" → relay publish 실행
|
|
698
|
-
→ "배포 완료! URL: https://relayax.com/
|
|
699
|
+
→ "배포 완료! URL: https://relayax.com/@my-space/my-team"
|
|
699
700
|
→ 온보딩 가이드 코드블록 표시`,
|
|
700
701
|
},
|
|
701
702
|
];
|
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
|
}
|