relayax-cli 0.2.19 → 0.2.22
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 +2 -3
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.js +11 -12
- package/dist/commands/install.js +147 -16
- package/dist/commands/join.d.ts +5 -0
- package/dist/commands/join.js +137 -0
- package/dist/commands/list.js +62 -1
- package/dist/commands/login.js +119 -6
- package/dist/commands/publish.js +42 -25
- package/dist/commands/update.js +6 -9
- package/dist/index.js +2 -0
- package/dist/lib/api.d.ts +5 -0
- package/dist/lib/api.js +26 -0
- package/dist/lib/command-adapter.js +68 -58
- package/dist/lib/guide.d.ts +5 -0
- package/dist/lib/guide.js +52 -0
- package/dist/types.d.ts +3 -1
- package/package.json +1 -1
package/dist/commands/create.js
CHANGED
|
@@ -64,9 +64,8 @@ function registerCreate(program) {
|
|
|
64
64
|
visibility = await promptSelect({
|
|
65
65
|
message: '공개 범위:',
|
|
66
66
|
choices: [
|
|
67
|
-
{ name: '
|
|
68
|
-
{ name: '
|
|
69
|
-
{ name: '초대 코드 필요', value: 'invite-only' },
|
|
67
|
+
{ name: '공개', value: 'public' },
|
|
68
|
+
{ name: '비공개 (Space 멤버만)', value: 'private' },
|
|
70
69
|
],
|
|
71
70
|
});
|
|
72
71
|
}
|
package/dist/commands/init.d.ts
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.hasGlobalUserCommands = hasGlobalUserCommands;
|
|
6
7
|
exports.registerInit = registerInit;
|
|
7
8
|
const fs_1 = __importDefault(require("fs"));
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
@@ -115,8 +116,11 @@ function registerInit(program) {
|
|
|
115
116
|
.description('에이전트 CLI에 relay 슬래시 커맨드를 설치합니다')
|
|
116
117
|
.option('--tools <tools>', '설치할 에이전트 CLI 지정 (all 또는 쉼표 구분)')
|
|
117
118
|
.option('--update', '이미 설치된 슬래시 커맨드를 최신 버전으로 업데이트')
|
|
119
|
+
.option('--auto', '대화형 프롬프트 없이 자동으로 모든 감지된 CLI에 설치')
|
|
118
120
|
.action(async (opts) => {
|
|
119
121
|
const json = program.opts().json ?? false;
|
|
122
|
+
// auto mode: --auto flag, --json flag, or stdin is not a TTY
|
|
123
|
+
const autoMode = opts.auto === true || json || !process.stdin.isTTY;
|
|
120
124
|
const projectPath = process.cwd();
|
|
121
125
|
const detected = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
|
|
122
126
|
const detectedIds = new Set(detected.map((t) => t.value));
|
|
@@ -154,7 +158,8 @@ function registerInit(program) {
|
|
|
154
158
|
else if (opts.tools) {
|
|
155
159
|
targetToolIds = resolveTools(opts.tools);
|
|
156
160
|
}
|
|
157
|
-
else if (!
|
|
161
|
+
else if (!autoMode) {
|
|
162
|
+
// interactive mode: only when stdin is a TTY and not --auto/--json
|
|
158
163
|
showWelcome();
|
|
159
164
|
if (detected.length > 0) {
|
|
160
165
|
console.log(` 감지된 에이전트 CLI: \x1b[36m${detected.map((t) => t.name).join(', ')}\x1b[0m\n`);
|
|
@@ -171,18 +176,12 @@ function registerInit(program) {
|
|
|
171
176
|
}
|
|
172
177
|
}
|
|
173
178
|
else {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
error: 'NO_AGENT_CLI',
|
|
178
|
-
message: '에이전트 CLI 디렉토리를 찾을 수 없습니다. --tools 옵션으로 지정하세요.',
|
|
179
|
-
}));
|
|
180
|
-
}
|
|
181
|
-
// 글로벌은 설치했으므로 에러로 종료하지 않음
|
|
182
|
-
targetToolIds = [];
|
|
179
|
+
// auto mode: use detected CLIs, or all available tools if none detected
|
|
180
|
+
if (detected.length > 0) {
|
|
181
|
+
targetToolIds = detected.map((t) => t.value);
|
|
183
182
|
}
|
|
184
183
|
else {
|
|
185
|
-
targetToolIds =
|
|
184
|
+
targetToolIds = ai_tools_js_1.AI_TOOLS.map((t) => t.value);
|
|
186
185
|
}
|
|
187
186
|
}
|
|
188
187
|
// Builder 커맨드 설치 (기존 파일 중 현재 목록에 없는 것 제거)
|
|
@@ -213,7 +212,7 @@ function registerInit(program) {
|
|
|
213
212
|
localResults.push({ tool: tool.name, commands: installedCommands });
|
|
214
213
|
}
|
|
215
214
|
}
|
|
216
|
-
else if (!
|
|
215
|
+
else if (!autoMode) {
|
|
217
216
|
// User 모드: 글로벌만 설치, 안내 표시
|
|
218
217
|
showWelcome();
|
|
219
218
|
}
|
package/dist/commands/install.js
CHANGED
|
@@ -11,34 +11,139 @@ const storage_js_1 = require("../lib/storage.js");
|
|
|
11
11
|
const config_js_1 = require("../lib/config.js");
|
|
12
12
|
const slug_js_1 = require("../lib/slug.js");
|
|
13
13
|
const contact_format_js_1 = require("../lib/contact-format.js");
|
|
14
|
+
const preamble_js_1 = require("../lib/preamble.js");
|
|
15
|
+
const join_js_1 = require("./join.js");
|
|
16
|
+
const init_js_1 = require("./init.js");
|
|
17
|
+
/**
|
|
18
|
+
* slugInput이 "@spaces/{spaceSlug}/{teamSlug}" 형식이면 파싱해 반환.
|
|
19
|
+
* 아니면 null.
|
|
20
|
+
*/
|
|
21
|
+
function parseSpaceTarget(slugInput) {
|
|
22
|
+
const m = slugInput.match(/^@spaces\/([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)$/);
|
|
23
|
+
if (!m)
|
|
24
|
+
return null;
|
|
25
|
+
return {
|
|
26
|
+
spaceSlug: m[1],
|
|
27
|
+
rawTeamSlug: m[2],
|
|
28
|
+
teamSlug: `@${m[1]}/${m[2]}`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
14
31
|
function registerInstall(program) {
|
|
15
32
|
program
|
|
16
33
|
.command('install <slug>')
|
|
17
34
|
.description('에이전트 팀 패키지를 .relay/teams/에 다운로드합니다')
|
|
18
|
-
.
|
|
35
|
+
.option('--no-guide', 'GUIDE.html 브라우저 자동 오픈을 비활성화합니다')
|
|
36
|
+
.option('--join-code <code>', 'Space 초대 코드 (Space 팀 설치 시 자동 가입)')
|
|
37
|
+
.action(async (slugInput, opts) => {
|
|
19
38
|
const json = program.opts().json ?? false;
|
|
20
39
|
const projectPath = process.cwd();
|
|
21
40
|
const tempDir = (0, storage_js_1.makeTempDir)();
|
|
41
|
+
if (!(0, init_js_1.hasGlobalUserCommands)()) {
|
|
42
|
+
if (!json) {
|
|
43
|
+
console.error('\x1b[33m⚠ relay init이 실행되지 않았습니다. 먼저 relay init을 실행하세요.\x1b[0m');
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.error(JSON.stringify({ error: 'NOT_INITIALIZED', message: 'relay init을 먼저 실행하세요.' }));
|
|
47
|
+
}
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
22
50
|
try {
|
|
23
|
-
// 0.
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
51
|
+
// 0. @spaces/{spaceSlug}/{teamSlug} 형식 감지 및 파싱
|
|
52
|
+
const spaceTarget = parseSpaceTarget(slugInput);
|
|
53
|
+
// 0a. --join-code가 있으면 먼저 Space 가입 시도
|
|
54
|
+
if (opts.joinCode && spaceTarget) {
|
|
55
|
+
try {
|
|
56
|
+
const { spaceName } = await (0, join_js_1.joinSpace)(spaceTarget.spaceSlug, opts.joinCode);
|
|
57
|
+
if (!json) {
|
|
58
|
+
console.log(`\x1b[32m✅ ${spaceName} Space에 가입했습니다\x1b[0m`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (joinErr) {
|
|
62
|
+
const joinMsg = joinErr instanceof Error ? joinErr.message : String(joinErr);
|
|
63
|
+
// 이미 멤버인 경우 설치 계속 진행
|
|
64
|
+
if (joinMsg !== 'ALREADY_MEMBER') {
|
|
65
|
+
if (!json) {
|
|
66
|
+
console.error(`\x1b[33m경고: Space 가입 실패 — ${joinMsg}\x1b[0m`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// 0b. Resolve scoped slug and fetch team metadata
|
|
72
|
+
let team;
|
|
73
|
+
let slug;
|
|
74
|
+
let parsed;
|
|
75
|
+
if (spaceTarget) {
|
|
76
|
+
// Space 팀: POST /api/spaces/{spaceSlug}/teams/{teamSlug}/install
|
|
77
|
+
// This verifies membership, increments install count, and returns metadata.
|
|
78
|
+
try {
|
|
79
|
+
team = await (0, api_js_1.installSpaceTeam)(spaceTarget.spaceSlug, spaceTarget.rawTeamSlug);
|
|
80
|
+
}
|
|
81
|
+
catch (fetchErr) {
|
|
82
|
+
const fetchMsg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
|
|
83
|
+
if (fetchMsg.includes('403')) {
|
|
84
|
+
if (json) {
|
|
85
|
+
console.error(JSON.stringify({
|
|
86
|
+
error: 'SPACE_ONLY',
|
|
87
|
+
message: '이 팀은 Space 멤버만 설치 가능합니다.',
|
|
88
|
+
spaceSlug: spaceTarget.spaceSlug,
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.error('\x1b[31m이 팀은 Space 멤버만 설치 가능합니다.\x1b[0m');
|
|
93
|
+
console.error(`\x1b[33mSpace 관리자에게 초대 코드를 요청하세요: relay join ${spaceTarget.spaceSlug} --code <코드>\x1b[0m`);
|
|
94
|
+
}
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
throw fetchErr;
|
|
98
|
+
}
|
|
99
|
+
// slug from server is "@spaces/{spaceSlug}/{teamSlug}" — derive local path parts
|
|
100
|
+
parsed = { owner: spaceTarget.spaceSlug, name: spaceTarget.rawTeamSlug, full: team.slug };
|
|
101
|
+
slug = team.slug;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Normal registry install
|
|
105
|
+
const resolveInput = slugInput;
|
|
106
|
+
parsed = await (0, slug_js_1.resolveSlug)(resolveInput);
|
|
107
|
+
slug = parsed.full;
|
|
108
|
+
try {
|
|
109
|
+
team = await (0, api_js_1.fetchTeamInfo)(slug);
|
|
110
|
+
}
|
|
111
|
+
catch (fetchErr) {
|
|
112
|
+
const fetchMsg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
|
|
113
|
+
if (fetchMsg.includes('403')) {
|
|
114
|
+
if (json) {
|
|
115
|
+
console.error(JSON.stringify({
|
|
116
|
+
error: 'SPACE_ONLY',
|
|
117
|
+
message: '이 팀은 Space 멤버만 설치 가능합니다.',
|
|
118
|
+
slug,
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
console.error('\x1b[31m이 팀은 Space 멤버만 설치 가능합니다.\x1b[0m');
|
|
123
|
+
console.error('\x1b[33mSpace 관리자에게 초대 코드를 요청하세요.\x1b[0m');
|
|
124
|
+
}
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
throw fetchErr;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
28
130
|
const teamDir = path_1.default.join(projectPath, '.relay', 'teams', parsed.owner, parsed.name);
|
|
29
131
|
// 2. Visibility check
|
|
30
132
|
const visibility = team.visibility ?? 'public';
|
|
31
|
-
if (visibility === '
|
|
133
|
+
if (visibility === 'private') {
|
|
32
134
|
const token = await (0, config_js_1.getValidToken)();
|
|
33
135
|
if (!token) {
|
|
34
|
-
|
|
35
|
-
error
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
136
|
+
if (json) {
|
|
137
|
+
console.error(JSON.stringify({
|
|
138
|
+
error: 'LOGIN_REQUIRED',
|
|
139
|
+
visibility,
|
|
140
|
+
slug,
|
|
141
|
+
message: '이 팀은 Space 멤버만 설치할 수 있습니다. 로그인이 필요합니다.',
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
console.error('\x1b[31m이 팀은 Space 멤버만 설치할 수 있습니다. relay login 을 먼저 실행하세요.\x1b[0m');
|
|
146
|
+
}
|
|
42
147
|
process.exit(1);
|
|
43
148
|
}
|
|
44
149
|
}
|
|
@@ -50,6 +155,8 @@ function registerInstall(program) {
|
|
|
50
155
|
}
|
|
51
156
|
fs_1.default.mkdirSync(teamDir, { recursive: true });
|
|
52
157
|
await (0, storage_js_1.extractPackage)(tarPath, teamDir);
|
|
158
|
+
// 4.5. Inject preamble (update check) into SKILL.md and commands
|
|
159
|
+
(0, preamble_js_1.injectPreambleToTeam)(teamDir, slug);
|
|
53
160
|
// 5. Count extracted files
|
|
54
161
|
function countFiles(dir) {
|
|
55
162
|
let count = 0;
|
|
@@ -66,12 +173,13 @@ function registerInstall(program) {
|
|
|
66
173
|
return count;
|
|
67
174
|
}
|
|
68
175
|
const fileCount = countFiles(teamDir);
|
|
69
|
-
// 6. Record in installed.json
|
|
176
|
+
// 6. Record in installed.json (space_slug 포함)
|
|
70
177
|
const installed = (0, config_js_1.loadInstalled)();
|
|
71
178
|
installed[slug] = {
|
|
72
179
|
version: team.version,
|
|
73
180
|
installed_at: new Date().toISOString(),
|
|
74
181
|
files: [teamDir],
|
|
182
|
+
...(spaceTarget ? { space_slug: spaceTarget.spaceSlug } : {}),
|
|
75
183
|
};
|
|
76
184
|
(0, config_js_1.saveInstalled)(installed);
|
|
77
185
|
// 7. Report install (non-blocking)
|
|
@@ -84,6 +192,7 @@ function registerInstall(program) {
|
|
|
84
192
|
commands: team.commands,
|
|
85
193
|
files: fileCount,
|
|
86
194
|
install_path: teamDir,
|
|
195
|
+
...(spaceTarget ? { space_slug: spaceTarget.spaceSlug } : {}),
|
|
87
196
|
};
|
|
88
197
|
if (json) {
|
|
89
198
|
console.log(JSON.stringify(result));
|
|
@@ -121,6 +230,28 @@ function registerInstall(program) {
|
|
|
121
230
|
}
|
|
122
231
|
console.log(` \x1b[90m└${'─'.repeat(44)}┘\x1b[0m`);
|
|
123
232
|
}
|
|
233
|
+
// Open GUIDE.html in browser if present
|
|
234
|
+
const guideHtmlPath = path_1.default.join(teamDir, 'GUIDE.html');
|
|
235
|
+
if (opts.guide !== false && fs_1.default.existsSync(guideHtmlPath)) {
|
|
236
|
+
const isTTY = Boolean(process.stdin.isTTY);
|
|
237
|
+
if (isTTY) {
|
|
238
|
+
const { exec } = await import('child_process');
|
|
239
|
+
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start ""' : 'xdg-open';
|
|
240
|
+
exec(`${openCmd} "${guideHtmlPath}"`);
|
|
241
|
+
console.log(`\n 📖 사용가이드를 브라우저에서 열었습니다`);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
console.log(`\n 📖 사용가이드: ${guideHtmlPath}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Usage hint
|
|
248
|
+
if (team.commands && team.commands.length > 0) {
|
|
249
|
+
console.log(`\n\x1b[33m💡 시작하려면 채팅에 아래 명령어를 입력하세요:\x1b[0m`);
|
|
250
|
+
console.log(` \x1b[1m/${team.commands[0].name}\x1b[0m`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
console.log(`\n\x1b[33m💡 설치 완료! AI 에이전트에서 사용할 수 있습니다.\x1b[0m`);
|
|
254
|
+
}
|
|
124
255
|
console.log('\n 에이전트가 /relay-install로 환경을 구성합니다.');
|
|
125
256
|
}
|
|
126
257
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.joinSpace = joinSpace;
|
|
4
|
+
exports.registerJoin = registerJoin;
|
|
5
|
+
const config_js_1 = require("../lib/config.js");
|
|
6
|
+
const init_js_1 = require("./init.js");
|
|
7
|
+
async function fetchSpaceTeams(spaceSlug, token) {
|
|
8
|
+
const res = await fetch(`${config_js_1.API_URL}/api/spaces/${spaceSlug}/teams`, {
|
|
9
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
10
|
+
signal: AbortSignal.timeout(5000),
|
|
11
|
+
});
|
|
12
|
+
if (!res.ok)
|
|
13
|
+
throw new Error(`${res.status}`);
|
|
14
|
+
const data = (await res.json());
|
|
15
|
+
if (Array.isArray(data))
|
|
16
|
+
return data;
|
|
17
|
+
return data.teams ?? [];
|
|
18
|
+
}
|
|
19
|
+
async function joinSpace(spaceSlug, code) {
|
|
20
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
21
|
+
if (!token) {
|
|
22
|
+
throw new Error('LOGIN_REQUIRED');
|
|
23
|
+
}
|
|
24
|
+
const res = await fetch(`${config_js_1.API_URL}/api/spaces/${spaceSlug}/join`, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
Authorization: `Bearer ${token}`,
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({ code }),
|
|
31
|
+
});
|
|
32
|
+
const body = (await res.json().catch(() => ({})));
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
const errCode = body.error ?? String(res.status);
|
|
35
|
+
switch (errCode) {
|
|
36
|
+
case 'INVALID_CODE':
|
|
37
|
+
throw new Error('초대 코드가 올바르지 않습니다.');
|
|
38
|
+
case 'EXPIRED_CODE':
|
|
39
|
+
throw new Error('초대 코드가 만료되었습니다.');
|
|
40
|
+
case 'ALREADY_MEMBER':
|
|
41
|
+
throw new Error('ALREADY_MEMBER');
|
|
42
|
+
default:
|
|
43
|
+
throw new Error(body.message ?? `Space 가입 실패 (${res.status})`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { spaceName: body.space_name ?? spaceSlug };
|
|
47
|
+
}
|
|
48
|
+
function registerJoin(program) {
|
|
49
|
+
program
|
|
50
|
+
.command('join <slug>')
|
|
51
|
+
.description('Space에 초대 코드로 가입합니다')
|
|
52
|
+
.requiredOption('--code <code>', '초대 코드 (UUID)')
|
|
53
|
+
.action(async (slug, opts) => {
|
|
54
|
+
const json = program.opts().json ?? false;
|
|
55
|
+
if (!(0, init_js_1.hasGlobalUserCommands)()) {
|
|
56
|
+
if (!json) {
|
|
57
|
+
console.error('\x1b[33m⚠ relay init이 실행되지 않았습니다. 먼저 relay init을 실행하세요.\x1b[0m');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.error(JSON.stringify({ error: 'NOT_INITIALIZED', message: 'relay init을 먼저 실행하세요.' }));
|
|
61
|
+
}
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const { spaceName } = await joinSpace(slug, opts.code);
|
|
66
|
+
if (json) {
|
|
67
|
+
// best-effort: fetch teams for JSON response
|
|
68
|
+
let teams = [];
|
|
69
|
+
try {
|
|
70
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
71
|
+
if (token)
|
|
72
|
+
teams = await fetchSpaceTeams(slug, token);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// ignore
|
|
76
|
+
}
|
|
77
|
+
console.log(JSON.stringify({ status: 'ok', space: slug, space_name: spaceName, teams: teams.map((t) => ({ slug: t.slug, name: t.name })) }));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
console.log(`\x1b[32m✅ ${spaceName} Space에 가입했습니다\x1b[0m`);
|
|
81
|
+
// best-effort: show available teams
|
|
82
|
+
try {
|
|
83
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
84
|
+
if (token) {
|
|
85
|
+
const teams = await fetchSpaceTeams(slug, token);
|
|
86
|
+
if (teams.length === 0) {
|
|
87
|
+
console.log('\n아직 추가된 팀이 없습니다.');
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.log('\n\x1b[1m📦 사용 가능한 팀:\x1b[0m');
|
|
91
|
+
for (const t of teams) {
|
|
92
|
+
const desc = t.description ? ` \x1b[90m— ${t.description}\x1b[0m` : '';
|
|
93
|
+
console.log(` \x1b[36m•\x1b[0m \x1b[1m${t.slug}\x1b[0m${desc}`);
|
|
94
|
+
}
|
|
95
|
+
console.log(`\n\x1b[33m💡 설치: relay install @spaces/${slug}/<팀슬러그>\x1b[0m`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// ignore — success message was already shown
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
106
|
+
if (message === 'ALREADY_MEMBER') {
|
|
107
|
+
if (json) {
|
|
108
|
+
console.log(JSON.stringify({ status: 'already_member', space: slug }));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
console.log(`\x1b[33m이미 ${slug} Space의 멤버입니다.\x1b[0m`);
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (message === 'LOGIN_REQUIRED') {
|
|
116
|
+
if (json) {
|
|
117
|
+
console.error(JSON.stringify({
|
|
118
|
+
error: 'LOGIN_REQUIRED',
|
|
119
|
+
message: '로그인이 필요합니다. relay login 을 먼저 실행하세요.',
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
console.error('\x1b[31m오류: 로그인이 필요합니다.\x1b[0m');
|
|
124
|
+
console.error(' relay login 을 먼저 실행하세요.');
|
|
125
|
+
}
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
if (json) {
|
|
129
|
+
console.error(JSON.stringify({ error: 'JOIN_FAILED', message }));
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
console.error(`\x1b[31m오류: ${message}\x1b[0m`);
|
|
133
|
+
}
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
package/dist/commands/list.js
CHANGED
|
@@ -2,12 +2,73 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerList = registerList;
|
|
4
4
|
const config_js_1 = require("../lib/config.js");
|
|
5
|
+
async function fetchSpaceTeamList(spaceSlug, token) {
|
|
6
|
+
const res = await fetch(`${config_js_1.API_URL}/api/spaces/${spaceSlug}/teams`, {
|
|
7
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
8
|
+
signal: AbortSignal.timeout(8000),
|
|
9
|
+
});
|
|
10
|
+
if (!res.ok) {
|
|
11
|
+
const body = await res.text();
|
|
12
|
+
throw new Error(`Space 팀 목록 조회 실패 (${res.status}): ${body}`);
|
|
13
|
+
}
|
|
14
|
+
const data = (await res.json());
|
|
15
|
+
if (Array.isArray(data))
|
|
16
|
+
return data;
|
|
17
|
+
return data.teams ?? [];
|
|
18
|
+
}
|
|
5
19
|
function registerList(program) {
|
|
6
20
|
program
|
|
7
21
|
.command('list')
|
|
8
22
|
.description('설치된 에이전트 팀 목록')
|
|
9
|
-
.
|
|
23
|
+
.option('--space <slug>', 'Space 마켓플레이스의 팀 목록 조회')
|
|
24
|
+
.action(async (opts) => {
|
|
10
25
|
const json = program.opts().json ?? false;
|
|
26
|
+
// --space 옵션: Space 마켓플레이스 팀 목록
|
|
27
|
+
if (opts.space) {
|
|
28
|
+
const spaceSlug = opts.space;
|
|
29
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
30
|
+
if (!token) {
|
|
31
|
+
if (json) {
|
|
32
|
+
console.error(JSON.stringify({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다. relay login을 먼저 실행하세요.' }));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
console.error('\x1b[31m오류: 로그인이 필요합니다.\x1b[0m');
|
|
36
|
+
console.error(' relay login을 먼저 실행하세요.');
|
|
37
|
+
}
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const teams = await fetchSpaceTeamList(spaceSlug, token);
|
|
42
|
+
if (json) {
|
|
43
|
+
console.log(JSON.stringify({ space: spaceSlug, teams }));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (teams.length === 0) {
|
|
47
|
+
console.log(`\n${spaceSlug} Space에 추가된 팀이 없습니다.`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.log(`\n\x1b[1m${spaceSlug} Space 팀 목록\x1b[0m (${teams.length}개):\n`);
|
|
51
|
+
for (const t of teams) {
|
|
52
|
+
const desc = t.description
|
|
53
|
+
? ` \x1b[90m${t.description.length > 50 ? t.description.slice(0, 50) + '...' : t.description}\x1b[0m`
|
|
54
|
+
: '';
|
|
55
|
+
console.log(` \x1b[36m${t.slug}\x1b[0m \x1b[1m${t.name}\x1b[0m${desc}`);
|
|
56
|
+
}
|
|
57
|
+
console.log(`\n\x1b[33m💡 설치: relay install @spaces/${spaceSlug}/<팀슬러그>\x1b[0m`);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
61
|
+
if (json) {
|
|
62
|
+
console.error(JSON.stringify({ error: 'FETCH_FAILED', message }));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.error(`\x1b[31m오류: ${message}\x1b[0m`);
|
|
66
|
+
}
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// 기본 동작: 로컬에 설치된 팀 목록
|
|
11
72
|
const installed = (0, config_js_1.loadInstalled)();
|
|
12
73
|
const entries = Object.entries(installed);
|
|
13
74
|
const installedList = entries.map(([slug, info]) => ({
|
package/dist/commands/login.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.registerLogin = registerLogin;
|
|
7
7
|
const http_1 = __importDefault(require("http"));
|
|
8
|
+
const readline_1 = __importDefault(require("readline"));
|
|
8
9
|
const child_process_1 = require("child_process");
|
|
9
10
|
const config_js_1 = require("../lib/config.js");
|
|
10
11
|
function openBrowser(url) {
|
|
@@ -112,11 +113,109 @@ function findAvailablePort() {
|
|
|
112
113
|
});
|
|
113
114
|
});
|
|
114
115
|
}
|
|
116
|
+
function promptLine(rl, question) {
|
|
117
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
118
|
+
}
|
|
119
|
+
function promptPassword(question) {
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
122
|
+
process.stdout.write(question);
|
|
123
|
+
// Hide input
|
|
124
|
+
const stdin = process.stdin;
|
|
125
|
+
if (stdin.isTTY)
|
|
126
|
+
stdin.setRawMode(true);
|
|
127
|
+
let input = '';
|
|
128
|
+
process.stdin.resume();
|
|
129
|
+
process.stdin.setEncoding('utf8');
|
|
130
|
+
const onData = (ch) => {
|
|
131
|
+
if (ch === '\n' || ch === '\r' || ch === '\u0004') {
|
|
132
|
+
if (stdin.isTTY)
|
|
133
|
+
stdin.setRawMode(false);
|
|
134
|
+
process.stdout.write('\n');
|
|
135
|
+
process.stdin.removeListener('data', onData);
|
|
136
|
+
rl.close();
|
|
137
|
+
resolve(input);
|
|
138
|
+
}
|
|
139
|
+
else if (ch === '\u0003') {
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
else if (ch === '\u007f') {
|
|
143
|
+
input = input.slice(0, -1);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
input += ch;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
process.stdin.on('data', onData);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async function loginWithBrowser(provider, json) {
|
|
153
|
+
const port = await findAvailablePort();
|
|
154
|
+
const loginUrl = `${config_js_1.API_URL}/auth/cli-login?port=${port}&provider=${provider}`;
|
|
155
|
+
if (!json) {
|
|
156
|
+
const providerName = provider === 'github' ? 'GitHub' : '카카오';
|
|
157
|
+
console.error(`브라우저에서 ${providerName} 로그인을 진행합니다...`);
|
|
158
|
+
}
|
|
159
|
+
openBrowser(loginUrl);
|
|
160
|
+
return waitForToken(port);
|
|
161
|
+
}
|
|
162
|
+
async function loginWithEmail(json) {
|
|
163
|
+
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
164
|
+
if (!json) {
|
|
165
|
+
console.error('이메일로 로그인합니다.');
|
|
166
|
+
}
|
|
167
|
+
let email;
|
|
168
|
+
let password;
|
|
169
|
+
try {
|
|
170
|
+
email = (await promptLine(rl, '이메일: ')).trim();
|
|
171
|
+
rl.close();
|
|
172
|
+
password = await promptPassword('비밀번호: ');
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
rl.close();
|
|
176
|
+
throw new Error('입력이 취소되었습니다');
|
|
177
|
+
}
|
|
178
|
+
const res = await fetch(`${config_js_1.API_URL}/api/auth/email-login`, {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: { 'Content-Type': 'application/json' },
|
|
181
|
+
body: JSON.stringify({ email, password }),
|
|
182
|
+
});
|
|
183
|
+
if (!res.ok) {
|
|
184
|
+
const body = (await res.json().catch(() => ({})));
|
|
185
|
+
const msg = body.error ?? '이메일 또는 비밀번호가 올바르지 않습니다.';
|
|
186
|
+
throw new Error(msg);
|
|
187
|
+
}
|
|
188
|
+
const data = (await res.json());
|
|
189
|
+
return {
|
|
190
|
+
token: data.access_token,
|
|
191
|
+
refresh_token: data.refresh_token,
|
|
192
|
+
expires_at: data.expires_at,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async function selectProvider(json) {
|
|
196
|
+
if (json)
|
|
197
|
+
return 'github';
|
|
198
|
+
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
199
|
+
console.error('');
|
|
200
|
+
console.error('로그인 방법을 선택하세요:');
|
|
201
|
+
console.error(' 1) GitHub');
|
|
202
|
+
console.error(' 2) 카카오');
|
|
203
|
+
console.error(' 3) 이메일 / 비밀번호');
|
|
204
|
+
console.error('');
|
|
205
|
+
const answer = (await promptLine(rl, '선택 (기본값: 1): ')).trim();
|
|
206
|
+
rl.close();
|
|
207
|
+
if (answer === '2')
|
|
208
|
+
return 'kakao';
|
|
209
|
+
if (answer === '3')
|
|
210
|
+
return 'email';
|
|
211
|
+
return 'github';
|
|
212
|
+
}
|
|
115
213
|
function registerLogin(program) {
|
|
116
214
|
program
|
|
117
215
|
.command('login')
|
|
118
216
|
.description('RelayAX 계정에 로그인합니다')
|
|
119
217
|
.option('--token <token>', '직접 토큰 입력 (브라우저 없이)')
|
|
218
|
+
.option('--provider <provider>', '로그인 제공자 (github | kakao | email)')
|
|
120
219
|
.action(async (opts) => {
|
|
121
220
|
const json = program.opts().json ?? false;
|
|
122
221
|
(0, config_js_1.ensureGlobalRelayDir)();
|
|
@@ -125,18 +224,32 @@ function registerLogin(program) {
|
|
|
125
224
|
let expiresAt;
|
|
126
225
|
if (!accessToken) {
|
|
127
226
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
227
|
+
let provider = opts.provider;
|
|
228
|
+
if (!provider) {
|
|
229
|
+
provider = await selectProvider(json);
|
|
230
|
+
}
|
|
231
|
+
let loginResult;
|
|
232
|
+
if (provider === 'email') {
|
|
233
|
+
loginResult = await loginWithEmail(json);
|
|
234
|
+
}
|
|
235
|
+
else if (provider === 'kakao') {
|
|
236
|
+
loginResult = await loginWithBrowser('kakao', json);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
loginResult = await loginWithBrowser('github', json);
|
|
240
|
+
}
|
|
133
241
|
accessToken = loginResult.token;
|
|
134
242
|
refreshToken = loginResult.refresh_token;
|
|
135
243
|
expiresAt = loginResult.expires_at;
|
|
136
244
|
}
|
|
137
245
|
catch (err) {
|
|
138
246
|
const msg = err instanceof Error ? err.message : '로그인 실패';
|
|
139
|
-
|
|
247
|
+
if (json) {
|
|
248
|
+
console.error(JSON.stringify({ error: 'LOGIN_FAILED', message: msg }));
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
console.error(`\x1b[31m오류: ${msg}\x1b[0m`);
|
|
252
|
+
}
|
|
140
253
|
process.exit(1);
|
|
141
254
|
}
|
|
142
255
|
}
|
package/dist/commands/publish.js
CHANGED
|
@@ -30,12 +30,7 @@ function parseRelayYaml(content) {
|
|
|
30
30
|
const rawPortfolio = raw.portfolio;
|
|
31
31
|
const portfolio = {};
|
|
32
32
|
if (rawPortfolio && typeof rawPortfolio === 'object' && !Array.isArray(rawPortfolio)) {
|
|
33
|
-
//
|
|
34
|
-
if (rawPortfolio.cover && typeof rawPortfolio.cover === 'object') {
|
|
35
|
-
const c = rawPortfolio.cover;
|
|
36
|
-
if (c.path)
|
|
37
|
-
portfolio.cover = { path: String(c.path) };
|
|
38
|
-
}
|
|
33
|
+
// Slot-based format: { demo: {...}, gallery: [...] }
|
|
39
34
|
if (rawPortfolio.demo && typeof rawPortfolio.demo === 'object') {
|
|
40
35
|
const d = rawPortfolio.demo;
|
|
41
36
|
portfolio.demo = {
|
|
@@ -56,10 +51,9 @@ function parseRelayYaml(content) {
|
|
|
56
51
|
}
|
|
57
52
|
const requires = raw.requires;
|
|
58
53
|
const rawVisibility = String(raw.visibility ?? '');
|
|
59
|
-
const visibility = rawVisibility === '
|
|
60
|
-
: rawVisibility === '
|
|
61
|
-
:
|
|
62
|
-
: undefined;
|
|
54
|
+
const visibility = rawVisibility === 'private' ? 'private'
|
|
55
|
+
: rawVisibility === 'public' ? 'public'
|
|
56
|
+
: undefined;
|
|
63
57
|
return {
|
|
64
58
|
name: String(raw.name ?? ''),
|
|
65
59
|
slug: String(raw.slug ?? ''),
|
|
@@ -223,19 +217,18 @@ function countDir(teamDir, dirName) {
|
|
|
223
217
|
return 0;
|
|
224
218
|
return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.')).length;
|
|
225
219
|
}
|
|
220
|
+
function listDir(teamDir, dirName) {
|
|
221
|
+
const dirPath = path_1.default.join(teamDir, dirName);
|
|
222
|
+
if (!fs_1.default.existsSync(dirPath))
|
|
223
|
+
return [];
|
|
224
|
+
return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.'));
|
|
225
|
+
}
|
|
226
226
|
/**
|
|
227
227
|
* 슬롯 기반 포트폴리오를 PortfolioEntry[] 로 평탄화한다.
|
|
228
228
|
* relay.yaml에 슬롯이 정의되어 있으면 사용, 없으면 portfolio/ 자동 스캔.
|
|
229
229
|
*/
|
|
230
230
|
function collectPortfolio(relayDir, slots) {
|
|
231
231
|
const entries = [];
|
|
232
|
-
// Cover
|
|
233
|
-
if (slots.cover?.path) {
|
|
234
|
-
const absPath = path_1.default.resolve(relayDir, slots.cover.path);
|
|
235
|
-
if (fs_1.default.existsSync(absPath)) {
|
|
236
|
-
entries.push({ path: slots.cover.path, title: 'Cover', slot_type: 'cover' });
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
232
|
// Demo
|
|
240
233
|
if (slots.demo) {
|
|
241
234
|
if (slots.demo.type === 'video_url' && slots.demo.url) {
|
|
@@ -264,13 +257,13 @@ function collectPortfolio(relayDir, slots) {
|
|
|
264
257
|
const files = fs_1.default.readdirSync(portfolioDir)
|
|
265
258
|
.filter((f) => IMAGE_EXTS.some((ext) => f.toLowerCase().endsWith(ext)))
|
|
266
259
|
.sort();
|
|
267
|
-
//
|
|
260
|
+
// All images as gallery
|
|
268
261
|
for (let i = 0; i < files.length && i < 6; i++) {
|
|
269
262
|
const f = files[i];
|
|
270
263
|
entries.push({
|
|
271
264
|
path: path_1.default.join('portfolio', f),
|
|
272
265
|
title: path_1.default.basename(f, path_1.default.extname(f)).replace(/[-_]/g, ' '),
|
|
273
|
-
slot_type:
|
|
266
|
+
slot_type: 'gallery',
|
|
274
267
|
});
|
|
275
268
|
}
|
|
276
269
|
}
|
|
@@ -299,11 +292,14 @@ function resolveLongDescription(teamDir, yamlValue) {
|
|
|
299
292
|
async function createTarball(teamDir) {
|
|
300
293
|
const tmpFile = path_1.default.join(os_1.default.tmpdir(), `relay-publish-${Date.now()}.tar.gz`);
|
|
301
294
|
const dirsToInclude = VALID_DIRS.filter((d) => fs_1.default.existsSync(path_1.default.join(teamDir, d)));
|
|
302
|
-
// Include root SKILL.md if
|
|
295
|
+
// Include root SKILL.md and GUIDE.html if they exist
|
|
303
296
|
const entries = [...dirsToInclude];
|
|
304
297
|
if (fs_1.default.existsSync(path_1.default.join(teamDir, 'SKILL.md'))) {
|
|
305
298
|
entries.push('SKILL.md');
|
|
306
299
|
}
|
|
300
|
+
if (fs_1.default.existsSync(path_1.default.join(teamDir, 'GUIDE.html'))) {
|
|
301
|
+
entries.push('GUIDE.html');
|
|
302
|
+
}
|
|
307
303
|
await (0, tar_1.create)({
|
|
308
304
|
gzip: true,
|
|
309
305
|
file: tmpFile,
|
|
@@ -361,6 +357,13 @@ async function publishToApi(token, tarPath, metadata, teamDir, portfolioEntries)
|
|
|
361
357
|
}
|
|
362
358
|
form.append('portfolio_meta', JSON.stringify(portfolioMeta));
|
|
363
359
|
}
|
|
360
|
+
// Attach GUIDE.html if it exists
|
|
361
|
+
const guidePath = path_1.default.join(teamDir, 'GUIDE.html');
|
|
362
|
+
if (fs_1.default.existsSync(guidePath)) {
|
|
363
|
+
const guideBuffer = fs_1.default.readFileSync(guidePath);
|
|
364
|
+
const guideBlob = new Blob([guideBuffer], { type: 'text/html' });
|
|
365
|
+
form.append('guide', guideBlob, 'GUIDE.html');
|
|
366
|
+
}
|
|
364
367
|
const res = await fetch(`${config_js_1.API_URL}/api/publish`, {
|
|
365
368
|
method: 'POST',
|
|
366
369
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -443,14 +446,13 @@ function registerPublish(program) {
|
|
|
443
446
|
const visibility = await promptSelect({
|
|
444
447
|
message: '공개 범위:',
|
|
445
448
|
choices: [
|
|
446
|
-
{ name: '
|
|
447
|
-
{ name: '
|
|
448
|
-
{ name: '초대 코드 필요', value: 'invite-only' },
|
|
449
|
+
{ name: '공개', value: 'public' },
|
|
450
|
+
{ name: '비공개 (Space 멤버만)', value: 'private' },
|
|
449
451
|
],
|
|
450
452
|
});
|
|
451
453
|
console.error('\n\x1b[2m💡 프로필에 연락처를 설정하면 설치 시 명함이 전달됩니다: www.relayax.com/dashboard/profile\x1b[0m');
|
|
452
|
-
if (visibility === '
|
|
453
|
-
console.error('\x1b[2m💡
|
|
454
|
+
if (visibility === 'private') {
|
|
455
|
+
console.error('\x1b[2m💡 비공개 팀은 Space를 통해 멤버를 관리하세요: www.relayax.com/dashboard/teams\x1b[0m');
|
|
454
456
|
}
|
|
455
457
|
console.error('');
|
|
456
458
|
const tags = tagsRaw
|
|
@@ -528,6 +530,8 @@ function registerPublish(program) {
|
|
|
528
530
|
requires: config.requires,
|
|
529
531
|
visibility: config.visibility,
|
|
530
532
|
cli_version: cliPkg.version,
|
|
533
|
+
agent_names: listDir(relayDir, 'agents'),
|
|
534
|
+
skill_names: listDir(relayDir, 'skills'),
|
|
531
535
|
};
|
|
532
536
|
if (!json) {
|
|
533
537
|
console.error(`패키지 생성 중... (${config.name} v${config.version})`);
|
|
@@ -546,6 +550,19 @@ function registerPublish(program) {
|
|
|
546
550
|
console.error(`업로드 중...`);
|
|
547
551
|
}
|
|
548
552
|
const result = await publishToApi(token, tarPath, metadata, relayDir, portfolioEntries);
|
|
553
|
+
// Update local SKILL.md preamble with scoped slug from server (non-fatal)
|
|
554
|
+
try {
|
|
555
|
+
if (result.slug && result.slug !== config.slug) {
|
|
556
|
+
const localSkillMd = path_1.default.join(relayDir, 'SKILL.md');
|
|
557
|
+
if (fs_1.default.existsSync(localSkillMd)) {
|
|
558
|
+
const { injectPreamble } = await import('../lib/preamble.js');
|
|
559
|
+
injectPreamble(localSkillMd, result.slug);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
// preamble update is best-effort — publish already succeeded
|
|
565
|
+
}
|
|
549
566
|
if (json) {
|
|
550
567
|
console.log(JSON.stringify(result));
|
|
551
568
|
}
|
package/dist/commands/update.js
CHANGED
|
@@ -7,12 +7,13 @@ const installer_js_1 = require("../lib/installer.js");
|
|
|
7
7
|
const config_js_1 = require("../lib/config.js");
|
|
8
8
|
const slug_js_1 = require("../lib/slug.js");
|
|
9
9
|
const contact_format_js_1 = require("../lib/contact-format.js");
|
|
10
|
+
const preamble_js_1 = require("../lib/preamble.js");
|
|
10
11
|
function registerUpdate(program) {
|
|
11
12
|
program
|
|
12
13
|
.command('update <slug>')
|
|
13
14
|
.description('설치된 에이전트 팀을 최신 버전으로 업데이트합니다')
|
|
14
15
|
.option('--path <install_path>', '설치 경로 지정 (기본: ./.claude)')
|
|
15
|
-
.option('--code <code>', '초대 코드 (
|
|
16
|
+
.option('--code <code>', '초대 코드 (비공개 팀 업데이트 시 필요)')
|
|
16
17
|
.action(async (slugInput, opts) => {
|
|
17
18
|
const json = program.opts().json ?? false;
|
|
18
19
|
const installPath = (0, config_js_1.getInstallPath)(opts.path);
|
|
@@ -51,16 +52,10 @@ function registerUpdate(program) {
|
|
|
51
52
|
}
|
|
52
53
|
// Visibility check
|
|
53
54
|
const visibility = team.visibility ?? 'public';
|
|
54
|
-
if (visibility === '
|
|
55
|
+
if (visibility === 'private') {
|
|
55
56
|
const token = await (0, config_js_1.getValidToken)();
|
|
56
57
|
if (!token) {
|
|
57
|
-
console.error('이 팀은
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
else if (visibility === 'invite-only') {
|
|
62
|
-
if (!opts.code) {
|
|
63
|
-
console.error('초대 코드가 필요합니다. `relay update ' + slug + ' --code <code>`로 업데이트하세요.');
|
|
58
|
+
console.error('이 팀은 Space 멤버만 업데이트할 수 있습니다. `relay login`을 먼저 실행하세요.');
|
|
64
59
|
process.exit(1);
|
|
65
60
|
}
|
|
66
61
|
}
|
|
@@ -69,6 +64,8 @@ function registerUpdate(program) {
|
|
|
69
64
|
// Extract
|
|
70
65
|
const extractDir = `${tempDir}/extracted`;
|
|
71
66
|
await (0, storage_js_1.extractPackage)(tarPath, extractDir);
|
|
67
|
+
// Inject preamble (update check) before copying
|
|
68
|
+
(0, preamble_js_1.injectPreambleToTeam)(extractDir, slug);
|
|
72
69
|
// Copy files to install_path
|
|
73
70
|
const files = (0, installer_js_1.installTeam)(extractDir, installPath);
|
|
74
71
|
// Update installed.json with new version
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ const outdated_js_1 = require("./commands/outdated.js");
|
|
|
16
16
|
const check_update_js_1 = require("./commands/check-update.js");
|
|
17
17
|
const follow_js_1 = require("./commands/follow.js");
|
|
18
18
|
const changelog_js_1 = require("./commands/changelog.js");
|
|
19
|
+
const join_js_1 = require("./commands/join.js");
|
|
19
20
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
20
21
|
const pkg = require('../package.json');
|
|
21
22
|
const program = new commander_1.Command();
|
|
@@ -38,4 +39,5 @@ program
|
|
|
38
39
|
(0, check_update_js_1.registerCheckUpdate)(program);
|
|
39
40
|
(0, follow_js_1.registerFollow)(program);
|
|
40
41
|
(0, changelog_js_1.registerChangelog)(program);
|
|
42
|
+
(0, join_js_1.registerJoin)(program);
|
|
41
43
|
program.parse();
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ export interface TeamVersionInfo {
|
|
|
8
8
|
}
|
|
9
9
|
export declare function fetchTeamVersions(slug: string): Promise<TeamVersionInfo[]>;
|
|
10
10
|
export declare function reportInstall(slug: string, version?: string): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Space 팀 설치: 멤버십 검증 + install count 증가 + 팀 메타데이터 반환을 한 번에 처리.
|
|
13
|
+
* POST /api/spaces/{spaceSlug}/teams/{teamSlug}/install
|
|
14
|
+
*/
|
|
15
|
+
export declare function installSpaceTeam(spaceSlug: string, teamSlug: string, version?: string): Promise<TeamRegistryInfo>;
|
|
11
16
|
export interface ResolvedSlug {
|
|
12
17
|
owner: string;
|
|
13
18
|
name: string;
|
package/dist/lib/api.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.fetchTeamInfo = fetchTeamInfo;
|
|
|
4
4
|
exports.searchTeams = searchTeams;
|
|
5
5
|
exports.fetchTeamVersions = fetchTeamVersions;
|
|
6
6
|
exports.reportInstall = reportInstall;
|
|
7
|
+
exports.installSpaceTeam = installSpaceTeam;
|
|
7
8
|
exports.resolveSlugFromServer = resolveSlugFromServer;
|
|
8
9
|
exports.sendUsagePing = sendUsagePing;
|
|
9
10
|
exports.followBuilder = followBuilder;
|
|
@@ -62,6 +63,31 @@ async function reportInstall(slug, version) {
|
|
|
62
63
|
// non-critical: ignore errors
|
|
63
64
|
});
|
|
64
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Space 팀 설치: 멤버십 검증 + install count 증가 + 팀 메타데이터 반환을 한 번에 처리.
|
|
68
|
+
* POST /api/spaces/{spaceSlug}/teams/{teamSlug}/install
|
|
69
|
+
*/
|
|
70
|
+
async function installSpaceTeam(spaceSlug, teamSlug, version) {
|
|
71
|
+
const url = `${config_js_1.API_URL}/api/spaces/${spaceSlug}/teams/${teamSlug}/install`;
|
|
72
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
73
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
74
|
+
if (token) {
|
|
75
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
76
|
+
}
|
|
77
|
+
const body = {};
|
|
78
|
+
if (version)
|
|
79
|
+
body.version = version;
|
|
80
|
+
const res = await fetch(url, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers,
|
|
83
|
+
body: JSON.stringify(body),
|
|
84
|
+
});
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
const text = await res.text();
|
|
87
|
+
throw new Error(`Space 팀 설치 실패 (${res.status}): ${text}`);
|
|
88
|
+
}
|
|
89
|
+
return res.json();
|
|
90
|
+
}
|
|
65
91
|
async function resolveSlugFromServer(name) {
|
|
66
92
|
const url = `${config_js_1.API_URL}/api/registry/resolve?name=${encodeURIComponent(name)}`;
|
|
67
93
|
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
@@ -108,7 +108,10 @@ exports.USER_COMMANDS = [
|
|
|
108
108
|
- 업데이트 여부와 관계없이 설치를 계속 진행합니다.
|
|
109
109
|
|
|
110
110
|
### 1. 팀 패키지 다운로드
|
|
111
|
-
- \`relay install <@author/slug>\` 명령어를 실행합니다.
|
|
111
|
+
- Public 마켓 팀: \`relay install <@author/slug>\` 명령어를 실행합니다.
|
|
112
|
+
- Space 팀: \`relay install @spaces/<space-slug>/<team-slug>\` 명령어를 실행합니다.
|
|
113
|
+
- Space 가입이 필요하면: \`relay join <space-slug> --code <invite-code>\` 를 먼저 실행합니다.
|
|
114
|
+
- 또는 \`relay install @spaces/<space-slug>/<team-slug> --join-code <code>\` 로 가입+설치를 한번에 할 수 있습니다.
|
|
112
115
|
- 패키지가 \`.relay/teams/<slug>/\`에 다운로드됩니다.
|
|
113
116
|
|
|
114
117
|
### 2. 패키지 내용 확인
|
|
@@ -160,7 +163,13 @@ ${BUSINESS_CARD_FORMAT}
|
|
|
160
163
|
→ commands/cardnews.md → .claude/commands/cardnews.md 복사
|
|
161
164
|
→ skills/pdf-gen.md → .claude/skills/pdf-gen.md 복사
|
|
162
165
|
→ requires 확인: ✓ playwright 설치됨, ✓ sharp 설치됨
|
|
163
|
-
→ "✓ 설치 완료! /cardnews를 사용해볼까요?"
|
|
166
|
+
→ "✓ 설치 완료! /cardnews를 사용해볼까요?"
|
|
167
|
+
|
|
168
|
+
사용자: /relay-install @spaces/bobusan/pm-bot
|
|
169
|
+
→ relay install @spaces/bobusan/pm-bot 실행
|
|
170
|
+
→ Space 멤버 확인 → 정상
|
|
171
|
+
→ 패키지 다운로드 및 배치
|
|
172
|
+
→ "✓ 설치 완료!"`,
|
|
164
173
|
},
|
|
165
174
|
{
|
|
166
175
|
id: 'relay-list',
|
|
@@ -176,13 +185,23 @@ ${BUSINESS_CARD_FORMAT}
|
|
|
176
185
|
- 사용 가능한 커맨드
|
|
177
186
|
3. 설치된 팀이 없으면 \`/relay-explore\`로 팀을 탐색해보라고 안내합니다.
|
|
178
187
|
|
|
188
|
+
### Space 팀 목록 확인
|
|
189
|
+
- 특정 Space에서 사용 가능한 팀 목록을 보려면: \`relay list --space <space-slug> --json\`
|
|
190
|
+
- Space에 가입되어 있어야 합니다.
|
|
191
|
+
|
|
179
192
|
## 예시
|
|
180
193
|
|
|
181
194
|
사용자: /relay-list
|
|
182
195
|
→ relay list --json 실행
|
|
183
196
|
→ "2개 팀이 설치되어 있어요:"
|
|
184
197
|
→ " @example/contents-team v1.2.0 (2일 전 설치) - /cardnews, /pdf-report"
|
|
185
|
-
→ " qa-team v0.5.1 (1주 전 설치) - /qa, /qa-only"
|
|
198
|
+
→ " qa-team v0.5.1 (1주 전 설치) - /qa, /qa-only"
|
|
199
|
+
|
|
200
|
+
사용자: /relay-list --space bobusan
|
|
201
|
+
→ relay list --space bobusan --json 실행
|
|
202
|
+
→ "bobusan Space에서 설치 가능한 팀:"
|
|
203
|
+
→ " pm-bot — 프로젝트 관리 봇"
|
|
204
|
+
→ " cs-bot — 고객 응대 봇"`,
|
|
186
205
|
},
|
|
187
206
|
{
|
|
188
207
|
id: 'relay-update',
|
|
@@ -241,7 +260,7 @@ exports.BUILDER_COMMANDS = [
|
|
|
241
260
|
{
|
|
242
261
|
id: 'relay-publish',
|
|
243
262
|
description: '현재 팀 패키지를 포트폴리오와 함께 relay 마켓플레이스에 배포합니다',
|
|
244
|
-
body: `현재 디렉토리의 에이전트 팀(.relay/)을 분석하고, 보안 점검 및 requirements를 구성한 뒤, 포트폴리오를 생성하고 relay 마켓플레이스에 배포합니다.
|
|
263
|
+
body: `현재 디렉토리의 에이전트 팀(.relay/)을 분석하고, 보안 점검 및 requirements를 구성한 뒤, 사용가이드와 포트폴리오를 생성하고 relay 마켓플레이스에 배포합니다.
|
|
245
264
|
|
|
246
265
|
## 실행 단계
|
|
247
266
|
|
|
@@ -327,76 +346,67 @@ requires:
|
|
|
327
346
|
- @example/contents-team
|
|
328
347
|
\`\`\`
|
|
329
348
|
|
|
330
|
-
### 4. 포트폴리오
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
- 규격: 1200x630px, WebP, 최대 500KB
|
|
336
|
-
- 팀 구조를 요약하는 카드 HTML을 생성합니다:
|
|
337
|
-
- 팀 이름, 버전
|
|
338
|
-
- Skills 목록 (이름 + 설명)
|
|
339
|
-
- Commands 목록
|
|
340
|
-
- 주요 기능 요약
|
|
341
|
-
- 생성된 HTML을 Playwright로 1200x630 뷰포트에서 스크린샷 캡처합니다.
|
|
342
|
-
- .relay/portfolio/cover.png에 저장합니다.
|
|
343
|
-
- 사용자에게 "이 cover를 사용할까요?" 확인. 직접 제공도 가능.
|
|
344
|
-
|
|
345
|
-
#### 슬롯 2: demo (자동 생성 — 동작 시연)
|
|
346
|
-
에이전트가 **직접 팀의 커맨드를 실행하여** demo를 자동 생성합니다. 사용자에게 묻기 전에 먼저 만듭니다.
|
|
347
|
-
|
|
348
|
-
**생성 절차:**
|
|
349
|
-
1. .relay/commands/ 의 커맨드 목록을 읽고, 가장 대표적인 커맨드를 선택합니다.
|
|
350
|
-
2. 해당 커맨드를 예시 주제/데이터로 실행합니다.
|
|
351
|
-
- 콘텐츠 팀: "라즈베리파이5 신제품 소개" 같은 예시 주제로 커맨드 실행
|
|
352
|
-
- 크롤링 팀: 예시 URL로 실행하며 브라우저 동작을 녹화
|
|
353
|
-
- 분석 팀: 샘플 데이터로 실행하여 결과 캡처
|
|
354
|
-
3. 실행 결과물로 demo를 생성합니다:
|
|
355
|
-
- 여러 장 이미지 (카드뉴스 등): 슬라이드쇼 GIF (각 장 2초 간격)
|
|
356
|
-
- 브라우저 동작: 동작 녹화 GIF
|
|
357
|
-
- 단일 결과물: 결과 이미지를 demo로 사용
|
|
358
|
-
4. 생성된 demo를 사용자에게 보여줍니다: "이 demo를 사용할까요?"
|
|
359
|
-
- 수정 요청 가능: "다른 주제로 다시 만들어줘", "좀 더 짧게"
|
|
360
|
-
- 확정 시 .relay/portfolio/demo.gif에 저장
|
|
361
|
-
|
|
362
|
-
**예시 주제 선택 기준:**
|
|
363
|
-
- relay.yaml의 description, tags에서 도메인 힌트 추출
|
|
364
|
-
- 기존 output/에 결과물이 있으면 같은 주제 활용
|
|
365
|
-
- 없으면 팀 설명에서 가장 그럴듯한 예시 주제를 자체 생성
|
|
366
|
-
|
|
367
|
-
**GIF 규격:** 최대 5MB, .relay/portfolio/demo.gif
|
|
368
|
-
**대안:** 외부 영상 URL (YouTube, Loom)도 가능 — relay.yaml에 기록
|
|
369
|
-
|
|
370
|
-
#### 슬롯 3: gallery (선택 — 결과물 쇼케이스, 최대 5장)
|
|
371
|
-
- 규격: 800x600px 이하, WebP, 각 500KB 이하
|
|
372
|
-
- output/, results/, examples/ 디렉토리를 스캔합니다.
|
|
373
|
-
- 큰 이미지 → 사용자에게 "어느 영역을 보여줄까요?" 확인 후 핵심 영역 crop
|
|
374
|
-
- HTML 파일 → Playwright 스크린샷으로 변환
|
|
349
|
+
### 4. 포트폴리오 수집
|
|
350
|
+
|
|
351
|
+
- output/, results/, examples/, portfolio/ 디렉토리를 스캔합니다.
|
|
352
|
+
- 발견된 결과물(PNG, JPG, HTML, PDF)을 사용자에게 보여줍니다.
|
|
353
|
+
- HTML 파일은 Playwright 스크린샷으로 변환합니다.
|
|
375
354
|
- 사용자가 포트폴리오에 포함할 항목을 선택합니다.
|
|
376
|
-
- .relay/portfolio/에 저장합니다.
|
|
355
|
+
- 선택된 이미지를 .relay/portfolio/에 저장합니다.
|
|
356
|
+
|
|
357
|
+
### 5. 사용가이드 생성 (GUIDE.html)
|
|
358
|
+
|
|
359
|
+
설치자가 읽을 수 있는 사용가이드를 HTML로 생성합니다.
|
|
360
|
+
|
|
361
|
+
#### 5-1. 팀 소스 분석
|
|
362
|
+
- skills/, agents/, commands/ 디렉토리의 **모든 파일 내용**을 읽습니다.
|
|
363
|
+
- 각 스킬의 SKILL.md, 에이전트 설정, 커맨드 문서를 분석하여 팀의 파이프라인 흐름을 추론합니다.
|
|
364
|
+
|
|
365
|
+
#### 5-2. GUIDE.html 생성
|
|
366
|
+
- \`cli/src/lib/guide.ts\`의 \`GUIDE_HTML_PROMPT\`를 읽고, 해당 프롬프트의 콘텐츠 구조와 디자인 규칙을 따라 GUIDE.html을 생성합니다.
|
|
367
|
+
- 5-1에서 분석한 팀 소스 정보를 프롬프트에 반영합니다.
|
|
368
|
+
- 파이프라인이 없는 단순한 팀은 시작 방법 + 기능 설명 + Q&A만 포함합니다.
|
|
369
|
+
|
|
370
|
+
#### 5-3. 미리보기 + 컨펌
|
|
371
|
+
- 생성된 GUIDE.html을 브라우저에서 열어 빌더에게 미리보기를 보여줍니다.
|
|
372
|
+
- 빌더에게 확인: "사용가이드를 확인해주세요. 이대로 진행할까요?"
|
|
373
|
+
|
|
374
|
+
#### 5-4. 재생성 루프
|
|
375
|
+
- 빌더가 수정을 요청하면 (예: "Q&A 추가해줘", "파이프라인 설명 더 자세히") 요청사항을 반영하여 GUIDE.html을 재생성합니다.
|
|
376
|
+
- 재생성 후 다시 브라우저에서 미리보기를 보여줍니다.
|
|
377
|
+
- 빌더가 컨펌할 때까지 반복합니다.
|
|
378
|
+
|
|
379
|
+
#### 5-5. 저장
|
|
380
|
+
- 컨펌된 GUIDE.html을 \`.relay/GUIDE.html\`에 저장합니다.
|
|
381
|
+
|
|
382
|
+
#### 5-6. GUIDE.html 스크린샷 → gallery 등록
|
|
383
|
+
- GUIDE.html을 Playwright로 열어 첫 화면(뷰포트 1200x630)을 스크린샷 캡처합니다. (gstack 또는 webapp-testing 스킬 활용)
|
|
384
|
+
- 결과 PNG를 \`./portfolio/guide-preview.png\`에 저장합니다.
|
|
385
|
+
- relay.yaml의 portfolio gallery 첫 번째 항목으로 자동 등록합니다.
|
|
386
|
+
- 이 이미지는 마켓플레이스 카드 및 OG 이미지의 fallback으로 사용됩니다 (demo > gallery 순).
|
|
377
387
|
|
|
378
|
-
###
|
|
388
|
+
### 6. 메타데이터 생성
|
|
379
389
|
- description: skills 내용 기반으로 자동 생성합니다.
|
|
380
390
|
- long_description: 팀 소개 마크다운을 자동 생성합니다 (README.md가 있으면 활용).
|
|
381
391
|
- tags: 팀 특성에 맞는 태그를 추천합니다.
|
|
382
392
|
- 사용자에게 확인: "이대로 배포할까요?"
|
|
383
393
|
|
|
384
|
-
###
|
|
394
|
+
### 7. .relay/relay.yaml 업데이트
|
|
385
395
|
- 메타데이터, requires, 포트폴리오 슬롯을 .relay/relay.yaml에 반영합니다.
|
|
386
396
|
|
|
387
397
|
\`\`\`yaml
|
|
388
398
|
portfolio:
|
|
389
|
-
cover:
|
|
390
|
-
path: portfolio/cover.png
|
|
391
399
|
demo: # 선택
|
|
392
400
|
type: gif
|
|
393
401
|
path: portfolio/demo.gif
|
|
394
402
|
gallery: # 선택, 최대 5장
|
|
403
|
+
- path: portfolio/guide-preview.png
|
|
404
|
+
title: "사용가이드 미리보기"
|
|
395
405
|
- path: portfolio/example-1.png
|
|
396
406
|
title: "카드뉴스 예시"
|
|
397
407
|
\`\`\`
|
|
398
408
|
|
|
399
|
-
###
|
|
409
|
+
### 8. 배포
|
|
400
410
|
- \`relay publish\` 명령어를 실행합니다.
|
|
401
411
|
- 배포 결과와 마켓플레이스 URL을 보여줍니다.
|
|
402
412
|
${BUSINESS_CARD_FORMAT}
|
|
@@ -408,9 +418,9 @@ ${BUSINESS_CARD_FORMAT}
|
|
|
408
418
|
→ 보안 스캔: ✓ 시크릿 없음
|
|
409
419
|
→ 환경변수 감지: OPENAI_API_KEY (필수), DATABASE_URL (선택)
|
|
410
420
|
→ requires 업데이트 완료
|
|
411
|
-
→
|
|
412
|
-
→
|
|
413
|
-
→
|
|
421
|
+
→ 포트폴리오: output/ 스캔 → "카드뉴스 예시.png 포함?" → Yes
|
|
422
|
+
→ GUIDE.html 생성 → 브라우저에서 미리보기 → 빌더 컨펌
|
|
423
|
+
→ GUIDE.html 스크린샷 → gallery 첫 번째 이미지로 등록
|
|
414
424
|
→ relay publish 실행
|
|
415
425
|
→ "배포 완료! URL: https://relayax.com/teams/my-team"`,
|
|
416
426
|
},
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GUIDE.html 생성 프롬프트 템플릿.
|
|
4
|
+
* publish slash command에서 Claude Code가 이 프롬프트를 읽고 GUIDE.html을 생성한다.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.GUIDE_HTML_PROMPT = void 0;
|
|
8
|
+
exports.GUIDE_HTML_PROMPT = `
|
|
9
|
+
당신은 에이전트 팀의 사용가이드(GUIDE.html)를 생성하는 에이전트입니다.
|
|
10
|
+
팀의 skills/, agents/, commands/ 파일을 분석한 결과를 바탕으로, 설치자가 읽을 수 있는 standalone HTML 파일을 생성하세요.
|
|
11
|
+
|
|
12
|
+
## 콘텐츠 구조 (순서대로)
|
|
13
|
+
|
|
14
|
+
1. **헤더**: 팀 이름 + 한 줄 소개
|
|
15
|
+
2. **설치 방법 (준비하기)**:
|
|
16
|
+
- Step 1: CLI 설치 — \`npm install -g relayax-cli\`
|
|
17
|
+
- Step 2: 팀 설치 — \`relay install @owner/slug\` (실제 팀의 scoped slug 사용)
|
|
18
|
+
- Step 3: 에이전트에 복사붙여넣기 — "사용하시는 AI 에이전트(Claude Code, Codex 등)의 채팅창에 아래 명령어를 붙여넣기 하세요!"
|
|
19
|
+
3. **안내 Callout**: 노란 배경 박스로 시작 방법 강조 — "채팅창에 '○○○'라고 입력하세요."
|
|
20
|
+
4. **워크플로우 요약**: 전체 흐름을 한 줄로 — \`[0 온보딩 → 1 분석 → 1.5 선택 → 2 추천 → 3 생성]\`
|
|
21
|
+
5. **파이프라인 다이어그램**: 2-column 세로 플로우차트
|
|
22
|
+
- 왼쪽 컬럼: 파이프라인 단계 (노드 + 연결선, 세로 흐름)
|
|
23
|
+
- 오른쪽 컬럼: 해당 단계에서 사용하는 skill 카드 (skill명 + 한 줄 설명)
|
|
24
|
+
- 색상 구분: 유저 개입 단계 = 베이지/주황 톤, AI 자동 단계 = 연두/초록 톤
|
|
25
|
+
- 상단에 범례: ■ 유저 개입 / ■ 자동 실행
|
|
26
|
+
- 선택적 단계(optional)는 점선 테두리 그룹으로 묶기
|
|
27
|
+
6. **단계별 상세 테이블**: 4-column 테이블
|
|
28
|
+
- 컬럼: 단계 | skill명 | 액션 | 상세
|
|
29
|
+
- 각 행에 역할 뱃지: "AI가 알아서 해줘요" (초록 배경) / "직접 아래 액션을 수행해주세요!" (주황 배경, 볼드)
|
|
30
|
+
- 상세 컬럼에 bullet list로 구체적 동작 설명
|
|
31
|
+
- 주의사항은 빨간 이탤릭 (*주의: ...)
|
|
32
|
+
7. **사전 준비사항**: 필요한 환경변수, 접근 권한, 데이터 등
|
|
33
|
+
8. **Q&A**: 접이식 토글(details/summary)로 자주 묻는 질문
|
|
34
|
+
- 커스터마이징 방법 (개수 변경, 타입 고정 등)
|
|
35
|
+
- 중단/재시작 방법
|
|
36
|
+
- 데이터 형식 요구사항
|
|
37
|
+
|
|
38
|
+
파이프라인이 없는 단순한 팀은 시작 방법 + 기능 설명 + Q&A만 포함합니다.
|
|
39
|
+
|
|
40
|
+
## 디자인 규칙 (Notion 스타일)
|
|
41
|
+
|
|
42
|
+
- **라이트 모드**: 배경 #ffffff / 부배경 #fafaf8, 텍스트 다크 #1a1a18
|
|
43
|
+
- **레이아웃**: max-width 900px, padding 40px, Notion 문서형 넓은 여백
|
|
44
|
+
- **타이포그래피**: system-ui, -apple-system, sans-serif. 본문 15px, line-height 1.7
|
|
45
|
+
- **Callout 박스**: 노란 배경 #fffbeb + 좌측 보더 4px solid #fcd34d (중요 안내), 파란 배경 #eff6ff + 보더 #93c5fd (참고)
|
|
46
|
+
- **역할 뱃지**: 둥근 모서리 pill, 초록 배경 #e0f5ea + 텍스트 #0d6b2e (AI) / 주황 배경 #fff0e0 + 텍스트 #b85a00 (유저)
|
|
47
|
+
- **테이블**: 깔끔한 border #e8e8e4, hover 시 행 하이라이트 #f5f5f3, 헤더 회색 배경 #f5f5f3
|
|
48
|
+
- **색상**: 유저 단계 = 베이지/주황 (#fff8f0, #f0c090), AI 단계 = 연두 (#f0faf4, #90d4a8)
|
|
49
|
+
- **톤**: 따뜻하고 친근한 한국어, 이모지 적절히 활용
|
|
50
|
+
- **반응형**: 600px 이하에서 테이블 가로스크롤, 다이어그램 단일 컬럼
|
|
51
|
+
- **기술 제약**: standalone HTML, CSS는 style 태그 내, 외부 CDN/폰트 없음, JS 최소화 (details/summary만)
|
|
52
|
+
`.trim();
|
package/dist/types.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface InstalledTeam {
|
|
|
8
8
|
installed_at: string;
|
|
9
9
|
files: string[];
|
|
10
10
|
type?: 'team' | 'system';
|
|
11
|
+
/** Space 소속 팀인 경우 Space slug */
|
|
12
|
+
space_slug?: string;
|
|
11
13
|
}
|
|
12
14
|
/** 키는 scoped slug 포맷: "@owner/name" */
|
|
13
15
|
export interface InstalledRegistry {
|
|
@@ -30,7 +32,7 @@ export interface TeamRegistryInfo {
|
|
|
30
32
|
tags?: string[];
|
|
31
33
|
install_count?: number;
|
|
32
34
|
requires?: Record<string, unknown>;
|
|
33
|
-
visibility?: "public" | "
|
|
35
|
+
visibility?: "public" | "private";
|
|
34
36
|
welcome?: string | null;
|
|
35
37
|
contact?: Record<string, string> | null;
|
|
36
38
|
author?: {
|