relayax-cli 0.1.994 → 0.1.996
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/check-update.js +24 -1
- package/dist/commands/init.js +11 -1
- package/dist/commands/install.js +8 -4
- package/dist/commands/login.js +18 -6
- package/dist/commands/publish.js +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/uninstall.js +12 -2
- package/dist/commands/update.js +19 -3
- package/dist/lib/api.d.ts +6 -0
- package/dist/lib/api.js +11 -1
- package/dist/lib/command-adapter.js +18 -18
- package/dist/lib/config.d.ts +21 -1
- package/dist/lib/config.js +112 -5
- package/dist/lib/preamble.js +0 -3
- package/dist/lib/slug.d.ts +24 -0
- package/dist/lib/slug.js +71 -0
- package/dist/lib/version-check.js +4 -8
- package/dist/types.d.ts +7 -6
- package/package.json +1 -1
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerCheckUpdate = registerCheckUpdate;
|
|
4
4
|
const version_check_js_1 = require("../lib/version-check.js");
|
|
5
|
+
const slug_js_1 = require("../lib/slug.js");
|
|
6
|
+
const config_js_1 = require("../lib/config.js");
|
|
5
7
|
function registerCheckUpdate(program) {
|
|
6
8
|
program
|
|
7
9
|
.command('check-update [slug]')
|
|
@@ -24,7 +26,28 @@ function registerCheckUpdate(program) {
|
|
|
24
26
|
}
|
|
25
27
|
// Team version check
|
|
26
28
|
if (slug) {
|
|
27
|
-
|
|
29
|
+
// Resolve to scoped slug
|
|
30
|
+
let scopedSlug;
|
|
31
|
+
if ((0, slug_js_1.isScopedSlug)(slug)) {
|
|
32
|
+
scopedSlug = slug;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const installed = (0, config_js_1.loadInstalled)();
|
|
36
|
+
const found = (0, slug_js_1.findInstalledByName)(installed, slug);
|
|
37
|
+
if (found) {
|
|
38
|
+
scopedSlug = found;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = await (0, slug_js_1.resolveSlug)(slug);
|
|
43
|
+
scopedSlug = parsed.full;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
scopedSlug = slug; // fallback to original
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const teamResult = await (0, version_check_js_1.checkTeamVersion)(scopedSlug, force);
|
|
28
51
|
if (teamResult) {
|
|
29
52
|
if (quiet) {
|
|
30
53
|
const byAuthor = teamResult.author ? ` ${teamResult.author}` : '';
|
package/dist/commands/init.js
CHANGED
|
@@ -8,6 +8,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const ai_tools_js_1 = require("../lib/ai-tools.js");
|
|
10
10
|
const command_adapter_js_1 = require("../lib/command-adapter.js");
|
|
11
|
+
const config_js_1 = require("../lib/config.js");
|
|
11
12
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
12
13
|
const pkg = require('../../package.json');
|
|
13
14
|
const VALID_TEAM_DIRS = ['skills', 'agents', 'rules', 'commands'];
|
|
@@ -123,8 +124,17 @@ function registerInit(program) {
|
|
|
123
124
|
// ── 1. 글로벌 User 커맨드 설치 ──
|
|
124
125
|
let globalStatus = 'already';
|
|
125
126
|
if (opts.update || !hasGlobalUserCommands()) {
|
|
126
|
-
installGlobalUserCommands();
|
|
127
|
+
const { commands } = installGlobalUserCommands();
|
|
127
128
|
globalStatus = opts.update ? 'updated' : 'installed';
|
|
129
|
+
// Register relay-core in installed.json
|
|
130
|
+
const installed = (0, config_js_1.loadInstalled)();
|
|
131
|
+
installed['relay-core'] = {
|
|
132
|
+
version: pkg.version,
|
|
133
|
+
installed_at: new Date().toISOString(),
|
|
134
|
+
files: commands.map((c) => (0, command_adapter_js_1.getGlobalCommandPath)(c)),
|
|
135
|
+
type: 'system',
|
|
136
|
+
};
|
|
137
|
+
(0, config_js_1.saveInstalled)(installed);
|
|
128
138
|
}
|
|
129
139
|
// ── 2. 로컬 Builder 커맨드 (팀 프로젝트인 경우) ──
|
|
130
140
|
const localResults = [];
|
package/dist/commands/install.js
CHANGED
|
@@ -9,22 +9,26 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const api_js_1 = require("../lib/api.js");
|
|
10
10
|
const storage_js_1 = require("../lib/storage.js");
|
|
11
11
|
const config_js_1 = require("../lib/config.js");
|
|
12
|
+
const slug_js_1 = require("../lib/slug.js");
|
|
12
13
|
function registerInstall(program) {
|
|
13
14
|
program
|
|
14
15
|
.command('install <slug>')
|
|
15
16
|
.description('에이전트 팀 패키지를 .relay/teams/에 다운로드합니다')
|
|
16
|
-
.action(async (
|
|
17
|
+
.action(async (slugInput) => {
|
|
17
18
|
const json = program.opts().json ?? false;
|
|
18
19
|
const projectPath = process.cwd();
|
|
19
|
-
const teamDir = path_1.default.join(projectPath, '.relay', 'teams', slug);
|
|
20
20
|
const tempDir = (0, storage_js_1.makeTempDir)();
|
|
21
21
|
try {
|
|
22
|
+
// 0. Resolve scoped slug
|
|
23
|
+
const parsed = await (0, slug_js_1.resolveSlug)(slugInput);
|
|
24
|
+
const slug = parsed.full;
|
|
22
25
|
// 1. Fetch team metadata
|
|
23
26
|
const team = await (0, api_js_1.fetchTeamInfo)(slug);
|
|
27
|
+
const teamDir = path_1.default.join(projectPath, '.relay', 'teams', parsed.owner, parsed.name);
|
|
24
28
|
// 2. Visibility check
|
|
25
29
|
const visibility = team.visibility ?? 'public';
|
|
26
30
|
if (visibility === 'login-only' || visibility === 'invite-only') {
|
|
27
|
-
const token = (0, config_js_1.
|
|
31
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
28
32
|
if (!token) {
|
|
29
33
|
console.error(JSON.stringify({
|
|
30
34
|
error: 'LOGIN_REQUIRED',
|
|
@@ -119,7 +123,7 @@ function registerInstall(program) {
|
|
|
119
123
|
console.log(` \x1b[90m└${'─'.repeat(44)}┘\x1b[0m`);
|
|
120
124
|
}
|
|
121
125
|
// Follow prompt (only when logged in)
|
|
122
|
-
const token = (0, config_js_1.
|
|
126
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
123
127
|
if (authorUsername && token) {
|
|
124
128
|
try {
|
|
125
129
|
const { confirm } = await import('@inquirer/prompts');
|
package/dist/commands/login.js
CHANGED
|
@@ -43,6 +43,9 @@ function waitForToken(port) {
|
|
|
43
43
|
const url = new URL(req.url ?? '/', `http://localhost:${port}`);
|
|
44
44
|
if (url.pathname === '/callback') {
|
|
45
45
|
const token = url.searchParams.get('token');
|
|
46
|
+
const refresh_token = url.searchParams.get('refresh_token') ?? undefined;
|
|
47
|
+
const expires_at_raw = url.searchParams.get('expires_at');
|
|
48
|
+
const expires_at = expires_at_raw ? Number(expires_at_raw) : undefined;
|
|
46
49
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
47
50
|
res.end(`<!DOCTYPE html>
|
|
48
51
|
<html><head><title>RelayAX</title></head>
|
|
@@ -55,7 +58,7 @@ function waitForToken(port) {
|
|
|
55
58
|
</body></html>`);
|
|
56
59
|
server.close();
|
|
57
60
|
if (token) {
|
|
58
|
-
resolve(token);
|
|
61
|
+
resolve({ token, refresh_token, expires_at });
|
|
59
62
|
}
|
|
60
63
|
else {
|
|
61
64
|
reject(new Error('토큰이 전달되지 않았습니다'));
|
|
@@ -97,14 +100,19 @@ function registerLogin(program) {
|
|
|
97
100
|
.action(async (opts) => {
|
|
98
101
|
const json = program.opts().json ?? false;
|
|
99
102
|
(0, config_js_1.ensureGlobalRelayDir)();
|
|
100
|
-
let
|
|
101
|
-
|
|
103
|
+
let accessToken = opts.token;
|
|
104
|
+
let refreshToken;
|
|
105
|
+
let expiresAt;
|
|
106
|
+
if (!accessToken) {
|
|
102
107
|
try {
|
|
103
108
|
const port = await findAvailablePort();
|
|
104
109
|
const loginUrl = `${config_js_1.API_URL}/auth/cli-login?port=${port}`;
|
|
105
110
|
console.error('브라우저에서 로그인을 진행합니다...');
|
|
106
111
|
openBrowser(loginUrl);
|
|
107
|
-
|
|
112
|
+
const loginResult = await waitForToken(port);
|
|
113
|
+
accessToken = loginResult.token;
|
|
114
|
+
refreshToken = loginResult.refresh_token;
|
|
115
|
+
expiresAt = loginResult.expires_at;
|
|
108
116
|
}
|
|
109
117
|
catch (err) {
|
|
110
118
|
const msg = err instanceof Error ? err.message : '로그인 실패';
|
|
@@ -112,8 +120,12 @@ function registerLogin(program) {
|
|
|
112
120
|
process.exit(1);
|
|
113
121
|
}
|
|
114
122
|
}
|
|
115
|
-
const user = await verifyToken(
|
|
116
|
-
(0, config_js_1.
|
|
123
|
+
const user = await verifyToken(accessToken);
|
|
124
|
+
(0, config_js_1.saveTokenData)({
|
|
125
|
+
access_token: accessToken,
|
|
126
|
+
...(refreshToken ? { refresh_token: refreshToken } : {}),
|
|
127
|
+
...(expiresAt ? { expires_at: expiresAt } : {}),
|
|
128
|
+
});
|
|
117
129
|
const result = {
|
|
118
130
|
status: 'ok',
|
|
119
131
|
message: '로그인 성공',
|
package/dist/commands/publish.js
CHANGED
|
@@ -379,7 +379,7 @@ function registerPublish(program) {
|
|
|
379
379
|
process.exit(1);
|
|
380
380
|
}
|
|
381
381
|
// Get token (checked before tarball creation)
|
|
382
|
-
const token = opts.token ?? process.env.RELAY_TOKEN ?? (0, config_js_1.
|
|
382
|
+
const token = opts.token ?? process.env.RELAY_TOKEN ?? await (0, config_js_1.getValidToken)();
|
|
383
383
|
if (!token) {
|
|
384
384
|
console.error(JSON.stringify({
|
|
385
385
|
error: 'NO_TOKEN',
|
package/dist/commands/status.js
CHANGED
|
@@ -31,7 +31,7 @@ function registerStatus(program) {
|
|
|
31
31
|
const json = program.opts().json ?? false;
|
|
32
32
|
const projectPath = process.cwd();
|
|
33
33
|
// 1. 로그인 상태
|
|
34
|
-
const token = (0, config_js_1.
|
|
34
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
35
35
|
let username;
|
|
36
36
|
if (token) {
|
|
37
37
|
username = await resolveUsername(token);
|
|
@@ -3,15 +3,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.registerUninstall = registerUninstall;
|
|
4
4
|
const config_js_1 = require("../lib/config.js");
|
|
5
5
|
const installer_js_1 = require("../lib/installer.js");
|
|
6
|
+
const slug_js_1 = require("../lib/slug.js");
|
|
6
7
|
function registerUninstall(program) {
|
|
7
8
|
program
|
|
8
9
|
.command('uninstall <slug>')
|
|
9
10
|
.description('에이전트 팀 제거')
|
|
10
|
-
.action((
|
|
11
|
+
.action((slugInput) => {
|
|
11
12
|
const json = program.opts().json ?? false;
|
|
12
13
|
const installed = (0, config_js_1.loadInstalled)();
|
|
14
|
+
// Resolve slug from installed.json
|
|
15
|
+
let slug;
|
|
16
|
+
if ((0, slug_js_1.isScopedSlug)(slugInput)) {
|
|
17
|
+
slug = slugInput;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
const found = (0, slug_js_1.findInstalledByName)(installed, slugInput);
|
|
21
|
+
slug = found ?? slugInput;
|
|
22
|
+
}
|
|
13
23
|
if (!installed[slug]) {
|
|
14
|
-
const msg = { error: 'NOT_INSTALLED', message: `'${
|
|
24
|
+
const msg = { error: 'NOT_INSTALLED', message: `'${slugInput}'는 설치되어 있지 않습니다.` };
|
|
15
25
|
if (json) {
|
|
16
26
|
console.error(JSON.stringify(msg));
|
|
17
27
|
}
|
package/dist/commands/update.js
CHANGED
|
@@ -5,19 +5,35 @@ const api_js_1 = require("../lib/api.js");
|
|
|
5
5
|
const storage_js_1 = require("../lib/storage.js");
|
|
6
6
|
const installer_js_1 = require("../lib/installer.js");
|
|
7
7
|
const config_js_1 = require("../lib/config.js");
|
|
8
|
+
const slug_js_1 = require("../lib/slug.js");
|
|
8
9
|
function registerUpdate(program) {
|
|
9
10
|
program
|
|
10
11
|
.command('update <slug>')
|
|
11
12
|
.description('설치된 에이전트 팀을 최신 버전으로 업데이트합니다')
|
|
12
13
|
.option('--path <install_path>', '설치 경로 지정 (기본: ./.claude)')
|
|
13
14
|
.option('--code <code>', '초대 코드 (invite-only 팀 업데이트 시 필요)')
|
|
14
|
-
.action(async (
|
|
15
|
+
.action(async (slugInput, opts) => {
|
|
15
16
|
const json = program.opts().json ?? false;
|
|
16
17
|
const installPath = (0, config_js_1.getInstallPath)(opts.path);
|
|
17
18
|
const tempDir = (0, storage_js_1.makeTempDir)();
|
|
18
19
|
try {
|
|
19
|
-
//
|
|
20
|
+
// Resolve scoped slug (try installed.json first for offline, then server)
|
|
20
21
|
const installed = (0, config_js_1.loadInstalled)();
|
|
22
|
+
let slug;
|
|
23
|
+
if ((0, slug_js_1.isScopedSlug)(slugInput)) {
|
|
24
|
+
slug = slugInput;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const found = (0, slug_js_1.findInstalledByName)(installed, slugInput);
|
|
28
|
+
if (found) {
|
|
29
|
+
slug = found;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const parsed = await (0, slug_js_1.resolveSlug)(slugInput);
|
|
33
|
+
slug = parsed.full;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Check installed.json for current version
|
|
21
37
|
const currentEntry = installed[slug];
|
|
22
38
|
const currentVersion = currentEntry?.version ?? null;
|
|
23
39
|
// Fetch latest team metadata
|
|
@@ -35,7 +51,7 @@ function registerUpdate(program) {
|
|
|
35
51
|
// Visibility check
|
|
36
52
|
const visibility = team.visibility ?? 'public';
|
|
37
53
|
if (visibility === 'login-only') {
|
|
38
|
-
const token = (0, config_js_1.
|
|
54
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
39
55
|
if (!token) {
|
|
40
56
|
console.error('이 팀은 로그인이 필요합니다. `relay login`을 먼저 실행하세요.');
|
|
41
57
|
process.exit(1);
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -8,4 +8,10 @@ export interface TeamVersionInfo {
|
|
|
8
8
|
}
|
|
9
9
|
export declare function fetchTeamVersions(slug: string): Promise<TeamVersionInfo[]>;
|
|
10
10
|
export declare function reportInstall(slug: string): Promise<void>;
|
|
11
|
+
export interface ResolvedSlug {
|
|
12
|
+
owner: string;
|
|
13
|
+
name: string;
|
|
14
|
+
full: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function resolveSlugFromServer(name: string): Promise<ResolvedSlug[]>;
|
|
11
17
|
export declare function followBuilder(username: string): Promise<void>;
|
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.resolveSlugFromServer = resolveSlugFromServer;
|
|
7
8
|
exports.followBuilder = followBuilder;
|
|
8
9
|
const config_js_1 = require("./config.js");
|
|
9
10
|
async function fetchTeamInfo(slug) {
|
|
@@ -43,8 +44,17 @@ async function reportInstall(slug) {
|
|
|
43
44
|
// non-critical: ignore errors
|
|
44
45
|
});
|
|
45
46
|
}
|
|
47
|
+
async function resolveSlugFromServer(name) {
|
|
48
|
+
const url = `${config_js_1.API_URL}/api/registry/resolve?name=${encodeURIComponent(name)}`;
|
|
49
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
throw new Error(`slug resolve 실패 (${res.status})`);
|
|
52
|
+
}
|
|
53
|
+
const data = (await res.json());
|
|
54
|
+
return data.results;
|
|
55
|
+
}
|
|
46
56
|
async function followBuilder(username) {
|
|
47
|
-
const token = (0, config_js_1.
|
|
57
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
48
58
|
const headers = {
|
|
49
59
|
'Content-Type': 'application/json',
|
|
50
60
|
};
|
|
@@ -66,7 +66,7 @@ exports.USER_COMMANDS = [
|
|
|
66
66
|
- 팀 이름과 설명
|
|
67
67
|
- 제공하는 커맨드 목록
|
|
68
68
|
- 왜 이 팀이 지금 프로젝트에 맞는지 설명
|
|
69
|
-
4. 관심 있는 팀이 있다면 \`/relay-install
|
|
69
|
+
4. 관심 있는 팀이 있다면 \`/relay-install <@author/slug>\`로 바로 설치할 수 있다고 안내합니다.
|
|
70
70
|
|
|
71
71
|
## 예시
|
|
72
72
|
|
|
@@ -74,7 +74,7 @@ exports.USER_COMMANDS = [
|
|
|
74
74
|
→ relay search 콘텐츠 실행
|
|
75
75
|
→ 결과 해석: "contents-team이 카드뉴스, PDF, PPT를 만들 수 있어요"
|
|
76
76
|
→ 프로젝트 맥락 기반 추천
|
|
77
|
-
→ "/relay-install contents-team으로 설치할 수 있어요"`,
|
|
77
|
+
→ "/relay-install @example/contents-team으로 설치할 수 있어요"`,
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
id: 'relay-install',
|
|
@@ -90,7 +90,7 @@ exports.USER_COMMANDS = [
|
|
|
90
90
|
- 업데이트 여부와 관계없이 설치를 계속 진행합니다.
|
|
91
91
|
|
|
92
92
|
### 1. 팀 패키지 다운로드
|
|
93
|
-
- \`relay install
|
|
93
|
+
- \`relay install <@author/slug>\` 명령어를 실행합니다.
|
|
94
94
|
- 패키지가 \`.relay/teams/<slug>/\`에 다운로드됩니다.
|
|
95
95
|
|
|
96
96
|
### 2. 패키지 내용 확인
|
|
@@ -115,7 +115,7 @@ exports.USER_COMMANDS = [
|
|
|
115
115
|
- **env**: 환경변수 확인 → required이면 설정 안내, optional이면 알림
|
|
116
116
|
- **mcp**: MCP 서버 설정 — 에이전트의 MCP 설정에 추가 안내
|
|
117
117
|
- **runtime**: Node.js/Python 버전 확인
|
|
118
|
-
- **teams**: 의존하는 다른 팀 → \`relay install
|
|
118
|
+
- **teams**: 의존하는 다른 팀 → \`relay install <@author/team>\`으로 재귀 설치
|
|
119
119
|
${LOGIN_JIT_GUIDE}
|
|
120
120
|
|
|
121
121
|
### 5. 완료 안내
|
|
@@ -124,9 +124,9 @@ ${LOGIN_JIT_GUIDE}
|
|
|
124
124
|
|
|
125
125
|
## 예시
|
|
126
126
|
|
|
127
|
-
사용자: /relay-install contents-team
|
|
128
|
-
→ relay install contents-team 실행 (패키지 다운로드)
|
|
129
|
-
→ .relay/teams/contents-team/ 내용 확인
|
|
127
|
+
사용자: /relay-install @example/contents-team
|
|
128
|
+
→ relay install @example/contents-team 실행 (패키지 다운로드)
|
|
129
|
+
→ .relay/teams/@example/contents-team/ 내용 확인
|
|
130
130
|
→ commands/cardnews.md → .claude/commands/cardnews.md 복사
|
|
131
131
|
→ skills/pdf-gen.md → .claude/skills/pdf-gen.md 복사
|
|
132
132
|
→ requires 확인: ✓ playwright 설치됨, ✓ sharp 설치됨
|
|
@@ -151,7 +151,7 @@ ${LOGIN_JIT_GUIDE}
|
|
|
151
151
|
사용자: /relay-list
|
|
152
152
|
→ relay list --json 실행
|
|
153
153
|
→ "2개 팀이 설치되어 있어요:"
|
|
154
|
-
→ " contents-team v1.2.0 (2일 전 설치) - /cardnews, /pdf-report"
|
|
154
|
+
→ " @example/contents-team v1.2.0 (2일 전 설치) - /cardnews, /pdf-report"
|
|
155
155
|
→ " qa-team v0.5.1 (1주 전 설치) - /qa, /qa-only"`,
|
|
156
156
|
},
|
|
157
157
|
{
|
|
@@ -167,14 +167,14 @@ ${LOGIN_JIT_GUIDE}
|
|
|
167
167
|
- 업데이트 여부와 관계없이 팀 업데이트를 계속 진행합니다.
|
|
168
168
|
|
|
169
169
|
### 특정 팀 업데이트
|
|
170
|
-
- 사용자가 팀 이름을 지정한 경우: \`relay update
|
|
170
|
+
- 사용자가 팀 이름을 지정한 경우: \`relay update <@author/slug> --json\` 실행
|
|
171
171
|
- 업데이트 결과를 보여줍니다 (이전 버전 → 새 버전)
|
|
172
172
|
|
|
173
173
|
### 전체 업데이트 확인
|
|
174
174
|
- 팀 이름을 지정하지 않은 경우:
|
|
175
175
|
1. \`relay outdated --json\`으로 업데이트 가능한 팀 목록을 확인합니다.
|
|
176
176
|
2. 업데이트 가능한 팀이 있으면 목록을 보여주고 어떤 팀을 업데이트할지 물어봅니다.
|
|
177
|
-
3. 선택된 팀에 대해 \`relay update
|
|
177
|
+
3. 선택된 팀에 대해 \`relay update <@author/slug> --json\`을 실행합니다.
|
|
178
178
|
4. 모두 최신이면 "모든 팀이 최신 버전입니다"라고 안내합니다.
|
|
179
179
|
|
|
180
180
|
## 예시
|
|
@@ -182,10 +182,10 @@ ${LOGIN_JIT_GUIDE}
|
|
|
182
182
|
사용자: /relay-update
|
|
183
183
|
→ relay outdated --json 실행
|
|
184
184
|
→ "1개 팀 업데이트 가능:"
|
|
185
|
-
→ " contents-team: v1.2.0 → v1.3.0"
|
|
185
|
+
→ " @example/contents-team: v1.2.0 → v1.3.0"
|
|
186
186
|
→ "업데이트할까요?"
|
|
187
|
-
→ relay update contents-team --json 실행
|
|
188
|
-
→ "✓ contents-team v1.3.0으로 업데이트 완료"`,
|
|
187
|
+
→ relay update @example/contents-team --json 실행
|
|
188
|
+
→ "✓ @example/contents-team v1.3.0으로 업데이트 완료"`,
|
|
189
189
|
},
|
|
190
190
|
{
|
|
191
191
|
id: 'relay-uninstall',
|
|
@@ -194,15 +194,15 @@ ${LOGIN_JIT_GUIDE}
|
|
|
194
194
|
|
|
195
195
|
## 실행 방법
|
|
196
196
|
|
|
197
|
-
1. \`relay uninstall
|
|
197
|
+
1. \`relay uninstall <@author/slug> --json\` 명령어를 실행합니다.
|
|
198
198
|
2. 삭제 결과를 보여줍니다 (팀 이름, 제거된 파일 수).
|
|
199
199
|
3. 삭제 완료 후 남아있는 팀 목록을 간단히 안내합니다.
|
|
200
200
|
|
|
201
201
|
## 예시
|
|
202
202
|
|
|
203
|
-
사용자: /relay-uninstall contents-team
|
|
204
|
-
→ relay uninstall contents-team --json 실행
|
|
205
|
-
→ "✓ contents-team 삭제 완료 (12개 파일 제거)"`,
|
|
203
|
+
사용자: /relay-uninstall @example/contents-team
|
|
204
|
+
→ relay uninstall @example/contents-team --json 실행
|
|
205
|
+
→ "✓ @example/contents-team 삭제 완료 (12개 파일 제거)"`,
|
|
206
206
|
},
|
|
207
207
|
];
|
|
208
208
|
// ─── Builder Commands (로컬 설치) ───
|
|
@@ -293,7 +293,7 @@ requires:
|
|
|
293
293
|
- filesystem
|
|
294
294
|
- network
|
|
295
295
|
teams:
|
|
296
|
-
- contents-team
|
|
296
|
+
- @example/contents-team
|
|
297
297
|
\`\`\`
|
|
298
298
|
|
|
299
299
|
### 4. 포트폴리오 생성 (슬롯 기반)
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -11,9 +11,29 @@ export declare function getInstallPath(override?: string): string;
|
|
|
11
11
|
export declare function ensureGlobalRelayDir(): void;
|
|
12
12
|
/** cwd/.relay/ — 프로젝트 로컬 (installed.json, teams/) */
|
|
13
13
|
export declare function ensureProjectRelayDir(): void;
|
|
14
|
+
export interface TokenData {
|
|
15
|
+
access_token: string;
|
|
16
|
+
refresh_token?: string;
|
|
17
|
+
expires_at?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function loadTokenData(): TokenData | undefined;
|
|
14
20
|
export declare function loadToken(): string | undefined;
|
|
21
|
+
export declare function saveTokenData(data: TokenData): void;
|
|
15
22
|
export declare function saveToken(token: string): void;
|
|
16
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* 유효한 access_token을 반환한다.
|
|
25
|
+
* 1. 저장된 토큰이 없으면 undefined
|
|
26
|
+
* 2. expires_at이 아직 유효하면 access_token 반환
|
|
27
|
+
* 3. 만료되었으면 refresh_token으로 갱신 시도
|
|
28
|
+
* 4. 갱신 실패 시 undefined (재로그인 필요)
|
|
29
|
+
*/
|
|
30
|
+
export declare function getValidToken(): Promise<string | undefined>;
|
|
31
|
+
/** 프로젝트 로컬 installed.json 읽기 (unscoped 키 자동 마이그레이션) */
|
|
17
32
|
export declare function loadInstalled(): InstalledRegistry;
|
|
33
|
+
/**
|
|
34
|
+
* 비동기 마이그레이션: unscoped 키를 서버 resolve하여 scoped 키로 변환.
|
|
35
|
+
* install/update 등 비동기 커맨드에서 호출.
|
|
36
|
+
*/
|
|
37
|
+
export declare function migrateInstalled(): Promise<void>;
|
|
18
38
|
/** 프로젝트 로컬 installed.json 쓰기 */
|
|
19
39
|
export declare function saveInstalled(registry: InstalledRegistry): void;
|
package/dist/lib/config.js
CHANGED
|
@@ -7,14 +7,19 @@ exports.API_URL = void 0;
|
|
|
7
7
|
exports.getInstallPath = getInstallPath;
|
|
8
8
|
exports.ensureGlobalRelayDir = ensureGlobalRelayDir;
|
|
9
9
|
exports.ensureProjectRelayDir = ensureProjectRelayDir;
|
|
10
|
+
exports.loadTokenData = loadTokenData;
|
|
10
11
|
exports.loadToken = loadToken;
|
|
12
|
+
exports.saveTokenData = saveTokenData;
|
|
11
13
|
exports.saveToken = saveToken;
|
|
14
|
+
exports.getValidToken = getValidToken;
|
|
12
15
|
exports.loadInstalled = loadInstalled;
|
|
16
|
+
exports.migrateInstalled = migrateInstalled;
|
|
13
17
|
exports.saveInstalled = saveInstalled;
|
|
14
18
|
const fs_1 = __importDefault(require("fs"));
|
|
15
19
|
const path_1 = __importDefault(require("path"));
|
|
16
20
|
const os_1 = __importDefault(require("os"));
|
|
17
21
|
const ai_tools_js_1 = require("./ai-tools.js");
|
|
22
|
+
const slug_js_1 = require("./slug.js");
|
|
18
23
|
exports.API_URL = 'https://www.relayax.com';
|
|
19
24
|
const GLOBAL_RELAY_DIR = path_1.default.join(os_1.default.homedir(), '.relay');
|
|
20
25
|
/**
|
|
@@ -50,22 +55,71 @@ function ensureProjectRelayDir() {
|
|
|
50
55
|
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
51
56
|
}
|
|
52
57
|
}
|
|
53
|
-
function
|
|
58
|
+
function loadTokenData() {
|
|
54
59
|
const tokenFile = path_1.default.join(GLOBAL_RELAY_DIR, 'token');
|
|
55
60
|
if (!fs_1.default.existsSync(tokenFile))
|
|
56
61
|
return undefined;
|
|
57
62
|
try {
|
|
58
|
-
|
|
63
|
+
const raw = fs_1.default.readFileSync(tokenFile, 'utf-8').trim();
|
|
64
|
+
if (!raw)
|
|
65
|
+
return undefined;
|
|
66
|
+
// JSON 형식 (새 포맷)
|
|
67
|
+
if (raw.startsWith('{')) {
|
|
68
|
+
return JSON.parse(raw);
|
|
69
|
+
}
|
|
70
|
+
// plain text (기존 포맷) — 호환성 유지
|
|
71
|
+
return { access_token: raw };
|
|
59
72
|
}
|
|
60
73
|
catch {
|
|
61
74
|
return undefined;
|
|
62
75
|
}
|
|
63
76
|
}
|
|
77
|
+
function loadToken() {
|
|
78
|
+
return loadTokenData()?.access_token;
|
|
79
|
+
}
|
|
80
|
+
function saveTokenData(data) {
|
|
81
|
+
ensureGlobalRelayDir();
|
|
82
|
+
fs_1.default.writeFileSync(path_1.default.join(GLOBAL_RELAY_DIR, 'token'), JSON.stringify(data));
|
|
83
|
+
}
|
|
64
84
|
function saveToken(token) {
|
|
65
85
|
ensureGlobalRelayDir();
|
|
66
|
-
fs_1.default.writeFileSync(path_1.default.join(GLOBAL_RELAY_DIR, 'token'), token);
|
|
86
|
+
fs_1.default.writeFileSync(path_1.default.join(GLOBAL_RELAY_DIR, 'token'), JSON.stringify({ access_token: token }));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 유효한 access_token을 반환한다.
|
|
90
|
+
* 1. 저장된 토큰이 없으면 undefined
|
|
91
|
+
* 2. expires_at이 아직 유효하면 access_token 반환
|
|
92
|
+
* 3. 만료되었으면 refresh_token으로 갱신 시도
|
|
93
|
+
* 4. 갱신 실패 시 undefined (재로그인 필요)
|
|
94
|
+
*/
|
|
95
|
+
async function getValidToken() {
|
|
96
|
+
const data = loadTokenData();
|
|
97
|
+
if (!data)
|
|
98
|
+
return undefined;
|
|
99
|
+
// expires_at이 없거나 아직 유효하면 (30초 여유) 그대로 사용
|
|
100
|
+
if (!data.expires_at || data.expires_at > Date.now() / 1000 + 30) {
|
|
101
|
+
return data.access_token;
|
|
102
|
+
}
|
|
103
|
+
// 만료됨 — refresh 시도
|
|
104
|
+
if (!data.refresh_token)
|
|
105
|
+
return undefined;
|
|
106
|
+
try {
|
|
107
|
+
const res = await fetch(`${exports.API_URL}/api/auth/refresh`, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: { 'Content-Type': 'application/json' },
|
|
110
|
+
body: JSON.stringify({ refresh_token: data.refresh_token }),
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok)
|
|
113
|
+
return undefined;
|
|
114
|
+
const refreshed = (await res.json());
|
|
115
|
+
saveTokenData(refreshed);
|
|
116
|
+
return refreshed.access_token;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
67
121
|
}
|
|
68
|
-
/** 프로젝트 로컬 installed.json 읽기 */
|
|
122
|
+
/** 프로젝트 로컬 installed.json 읽기 (unscoped 키 자동 마이그레이션) */
|
|
69
123
|
function loadInstalled() {
|
|
70
124
|
const file = path_1.default.join(process.cwd(), '.relay', 'installed.json');
|
|
71
125
|
if (!fs_1.default.existsSync(file)) {
|
|
@@ -73,12 +127,65 @@ function loadInstalled() {
|
|
|
73
127
|
}
|
|
74
128
|
try {
|
|
75
129
|
const raw = fs_1.default.readFileSync(file, 'utf-8');
|
|
76
|
-
|
|
130
|
+
const registry = JSON.parse(raw);
|
|
131
|
+
return migrateInstalledKeys(registry);
|
|
77
132
|
}
|
|
78
133
|
catch {
|
|
79
134
|
return {};
|
|
80
135
|
}
|
|
81
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* unscoped 키를 감지하여 서버 resolve 없이 가능한 마이그레이션을 수행한다.
|
|
139
|
+
* 서버 resolve가 필요한 경우는 마이그레이션 보류 (다음 기회에 재시도).
|
|
140
|
+
*/
|
|
141
|
+
function migrateInstalledKeys(registry) {
|
|
142
|
+
const unscopedKeys = Object.keys(registry).filter((k) => !(0, slug_js_1.isScopedSlug)(k) && k !== 'relay-core');
|
|
143
|
+
if (unscopedKeys.length === 0)
|
|
144
|
+
return registry;
|
|
145
|
+
// 비동기 서버 resolve 없이는 owner를 알 수 없으므로,
|
|
146
|
+
// loadInstalled는 동기 함수 → 마이그레이션은 비동기 migrateInstalled()로 별도 호출
|
|
147
|
+
return registry;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 비동기 마이그레이션: unscoped 키를 서버 resolve하여 scoped 키로 변환.
|
|
151
|
+
* install/update 등 비동기 커맨드에서 호출.
|
|
152
|
+
*/
|
|
153
|
+
async function migrateInstalled() {
|
|
154
|
+
const { resolveSlugFromServer } = await import('./api.js');
|
|
155
|
+
const registry = loadInstalled();
|
|
156
|
+
const teamsDir = path_1.default.join(process.cwd(), '.relay', 'teams');
|
|
157
|
+
let changed = false;
|
|
158
|
+
for (const key of Object.keys(registry)) {
|
|
159
|
+
if ((0, slug_js_1.isScopedSlug)(key) || key === 'relay-core')
|
|
160
|
+
continue;
|
|
161
|
+
try {
|
|
162
|
+
const results = await resolveSlugFromServer(key);
|
|
163
|
+
if (results.length !== 1)
|
|
164
|
+
continue;
|
|
165
|
+
const { owner, name } = results[0];
|
|
166
|
+
const scopedKey = `@${owner}/${name}`;
|
|
167
|
+
// installed.json 키 변환
|
|
168
|
+
registry[scopedKey] = registry[key];
|
|
169
|
+
delete registry[key];
|
|
170
|
+
// 디렉토리 이동
|
|
171
|
+
const oldDir = path_1.default.join(teamsDir, key);
|
|
172
|
+
const newDir = path_1.default.join(teamsDir, owner, name);
|
|
173
|
+
if (fs_1.default.existsSync(oldDir)) {
|
|
174
|
+
fs_1.default.mkdirSync(path_1.default.dirname(newDir), { recursive: true });
|
|
175
|
+
fs_1.default.renameSync(oldDir, newDir);
|
|
176
|
+
// files 배열 업데이트
|
|
177
|
+
registry[scopedKey].files = registry[scopedKey].files.map((f) => f.replace(`/teams/${key}`, `/teams/${owner}/${name}`));
|
|
178
|
+
}
|
|
179
|
+
changed = true;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// 네트워크 오류 등 — 다음 기회에 재시도
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (changed) {
|
|
186
|
+
saveInstalled(registry);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
82
189
|
/** 프로젝트 로컬 installed.json 쓰기 */
|
|
83
190
|
function saveInstalled(registry) {
|
|
84
191
|
ensureProjectRelayDir();
|
package/dist/lib/preamble.js
CHANGED
|
@@ -25,19 +25,16 @@ ${PREAMBLE_END}`;
|
|
|
25
25
|
function injectPreamble(filePath, slug) {
|
|
26
26
|
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
27
27
|
const preamble = generatePreamble(slug);
|
|
28
|
-
// Replace existing preamble or prepend
|
|
29
28
|
const startIdx = content.indexOf(PREAMBLE_START);
|
|
30
29
|
const endIdx = content.indexOf(PREAMBLE_END);
|
|
31
30
|
let newContent;
|
|
32
31
|
if (startIdx !== -1 && endIdx !== -1) {
|
|
33
|
-
// Replace existing
|
|
34
32
|
newContent =
|
|
35
33
|
content.slice(0, startIdx) +
|
|
36
34
|
preamble +
|
|
37
35
|
content.slice(endIdx + PREAMBLE_END.length);
|
|
38
36
|
}
|
|
39
37
|
else {
|
|
40
|
-
// Prepend
|
|
41
38
|
newContent = preamble + '\n\n' + content;
|
|
42
39
|
}
|
|
43
40
|
fs_1.default.writeFileSync(filePath, newContent);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface ParsedSlug {
|
|
2
|
+
owner: string;
|
|
3
|
+
name: string;
|
|
4
|
+
full: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Scoped slug(`@owner/name`)를 동기적으로 파싱한다.
|
|
8
|
+
* 단순 slug는 파싱할 수 없으므로 null을 반환한다.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseSlug(input: string): ParsedSlug | null;
|
|
11
|
+
/** input이 scoped slug인지 확인 */
|
|
12
|
+
export declare function isScopedSlug(input: string): boolean;
|
|
13
|
+
/** input이 단순 slug인지 확인 */
|
|
14
|
+
export declare function isSimpleSlug(input: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Scoped 또는 단순 slug를 받아 ParsedSlug를 반환한다.
|
|
17
|
+
* 단순 slug는 서버에 resolve를 요청한다.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveSlug(input: string): Promise<ParsedSlug>;
|
|
20
|
+
/**
|
|
21
|
+
* installed.json에서 단순 slug로 매칭되는 scoped 키를 찾는다.
|
|
22
|
+
* 네트워크 없이 로컬에서만 동작.
|
|
23
|
+
*/
|
|
24
|
+
export declare function findInstalledByName(installed: Record<string, unknown>, name: string): string | null;
|
package/dist/lib/slug.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseSlug = parseSlug;
|
|
4
|
+
exports.isScopedSlug = isScopedSlug;
|
|
5
|
+
exports.isSimpleSlug = isSimpleSlug;
|
|
6
|
+
exports.resolveSlug = resolveSlug;
|
|
7
|
+
exports.findInstalledByName = findInstalledByName;
|
|
8
|
+
const api_js_1 = require("./api.js");
|
|
9
|
+
const SCOPED_SLUG_RE = /^@([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\/([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)$/;
|
|
10
|
+
const SIMPLE_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
11
|
+
/**
|
|
12
|
+
* Scoped slug(`@owner/name`)를 동기적으로 파싱한다.
|
|
13
|
+
* 단순 slug는 파싱할 수 없으므로 null을 반환한다.
|
|
14
|
+
*/
|
|
15
|
+
function parseSlug(input) {
|
|
16
|
+
const m = input.match(SCOPED_SLUG_RE);
|
|
17
|
+
if (!m)
|
|
18
|
+
return null;
|
|
19
|
+
return { owner: m[1], name: m[2], full: input };
|
|
20
|
+
}
|
|
21
|
+
/** input이 scoped slug인지 확인 */
|
|
22
|
+
function isScopedSlug(input) {
|
|
23
|
+
return SCOPED_SLUG_RE.test(input);
|
|
24
|
+
}
|
|
25
|
+
/** input이 단순 slug인지 확인 */
|
|
26
|
+
function isSimpleSlug(input) {
|
|
27
|
+
return SIMPLE_SLUG_RE.test(input);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Scoped 또는 단순 slug를 받아 ParsedSlug를 반환한다.
|
|
31
|
+
* 단순 slug는 서버에 resolve를 요청한다.
|
|
32
|
+
*/
|
|
33
|
+
async function resolveSlug(input) {
|
|
34
|
+
// scoped slug면 바로 파싱
|
|
35
|
+
const parsed = parseSlug(input);
|
|
36
|
+
if (parsed)
|
|
37
|
+
return parsed;
|
|
38
|
+
// 단순 slug인지 검증
|
|
39
|
+
if (!isSimpleSlug(input)) {
|
|
40
|
+
throw new Error(`잘못된 slug 형식입니다: '${input}'. @owner/name 또는 name 형태로 입력하세요.`);
|
|
41
|
+
}
|
|
42
|
+
// 서버에 resolve 요청
|
|
43
|
+
const results = await (0, api_js_1.resolveSlugFromServer)(input);
|
|
44
|
+
if (results.length === 0) {
|
|
45
|
+
throw new Error(`'${input}' 팀을 찾을 수 없습니다.`);
|
|
46
|
+
}
|
|
47
|
+
if (results.length === 1) {
|
|
48
|
+
const r = results[0];
|
|
49
|
+
return { owner: r.owner, name: r.name, full: r.full };
|
|
50
|
+
}
|
|
51
|
+
// 여러 개 매칭
|
|
52
|
+
const list = results.map((r) => ` ${r.full}`).join('\n');
|
|
53
|
+
throw new Error(`'${input}'에 해당하는 팀이 여러 개입니다. 전체 slug를 지정해주세요:\n${list}`);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* installed.json에서 단순 slug로 매칭되는 scoped 키를 찾는다.
|
|
57
|
+
* 네트워크 없이 로컬에서만 동작.
|
|
58
|
+
*/
|
|
59
|
+
function findInstalledByName(installed, name) {
|
|
60
|
+
// 정확한 키가 있으면 그대로 (하위 호환)
|
|
61
|
+
if (name in installed)
|
|
62
|
+
return name;
|
|
63
|
+
// scoped 키 중 name 부분이 매칭되는 것 찾기
|
|
64
|
+
const matches = Object.keys(installed).filter((key) => {
|
|
65
|
+
const parsed = parseSlug(key);
|
|
66
|
+
return parsed && parsed.name === name;
|
|
67
|
+
});
|
|
68
|
+
if (matches.length === 1)
|
|
69
|
+
return matches[0];
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
@@ -6,7 +6,6 @@ exports.checkAllTeams = checkAllTeams;
|
|
|
6
6
|
const config_js_1 = require("./config.js");
|
|
7
7
|
const api_js_1 = require("./api.js");
|
|
8
8
|
const update_cache_js_1 = require("./update-cache.js");
|
|
9
|
-
const device_hash_js_1 = require("./device-hash.js");
|
|
10
9
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
11
10
|
const pkg = require('../../package.json');
|
|
12
11
|
async function checkCliVersion(force) {
|
|
@@ -37,15 +36,12 @@ async function checkTeamVersion(slug, force) {
|
|
|
37
36
|
const entry = installed[slug];
|
|
38
37
|
if (!entry?.version)
|
|
39
38
|
return null;
|
|
39
|
+
// system 타입(relay-core)은 CLI 버전 체크로 대체
|
|
40
|
+
if (entry.type === 'system') {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
40
43
|
const team = await (0, api_js_1.fetchTeamInfo)(slug);
|
|
41
44
|
(0, update_cache_js_1.updateCacheTimestamp)(slug);
|
|
42
|
-
// Send usage ping (fire-and-forget)
|
|
43
|
-
fetch(`${config_js_1.API_URL}/api/registry/${slug}/ping`, {
|
|
44
|
-
method: 'POST',
|
|
45
|
-
headers: { 'Content-Type': 'application/json' },
|
|
46
|
-
body: JSON.stringify({ device_hash: (0, device_hash_js_1.getDeviceHash)() }),
|
|
47
|
-
signal: AbortSignal.timeout(3000),
|
|
48
|
-
}).catch(() => { });
|
|
49
45
|
if (team.version !== entry.version) {
|
|
50
46
|
return {
|
|
51
47
|
type: 'team',
|
package/dist/types.d.ts
CHANGED
|
@@ -4,10 +4,12 @@ export interface InstalledTeam {
|
|
|
4
4
|
files: string[];
|
|
5
5
|
type?: 'team' | 'system';
|
|
6
6
|
}
|
|
7
|
+
/** 키는 scoped slug 포맷: "@owner/name" */
|
|
7
8
|
export interface InstalledRegistry {
|
|
8
|
-
[
|
|
9
|
+
[scopedSlug: string]: InstalledTeam;
|
|
9
10
|
}
|
|
10
11
|
export interface TeamRegistryInfo {
|
|
12
|
+
/** scoped slug 포맷: "@owner/name" */
|
|
11
13
|
slug: string;
|
|
12
14
|
name: string;
|
|
13
15
|
description?: string;
|
|
@@ -17,11 +19,9 @@ export interface TeamRegistryInfo {
|
|
|
17
19
|
name: string;
|
|
18
20
|
description: string;
|
|
19
21
|
}[];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
skills: number;
|
|
24
|
-
};
|
|
22
|
+
component_agents: number;
|
|
23
|
+
component_rules: number;
|
|
24
|
+
component_skills: number;
|
|
25
25
|
tags?: string[];
|
|
26
26
|
install_count?: number;
|
|
27
27
|
requires?: Record<string, unknown>;
|
|
@@ -39,6 +39,7 @@ export interface TeamRegistryInfo {
|
|
|
39
39
|
} | null;
|
|
40
40
|
}
|
|
41
41
|
export interface SearchResult {
|
|
42
|
+
/** scoped slug 포맷: "@owner/name" */
|
|
42
43
|
slug: string;
|
|
43
44
|
name: string;
|
|
44
45
|
description: string;
|