relayax-cli 0.1.992 → 0.1.995
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 +13 -1
- package/dist/commands/install.js +6 -5
- package/dist/commands/login.js +1 -1
- package/dist/commands/uninstall.js +12 -2
- package/dist/commands/update.js +18 -5
- package/dist/lib/api.d.ts +6 -0
- package/dist/lib/api.js +10 -0
- package/dist/lib/command-adapter.js +29 -18
- package/dist/lib/config.d.ts +11 -1
- package/dist/lib/config.js +81 -14
- 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 +8 -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,9 @@ 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");
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
13
|
+
const pkg = require('../../package.json');
|
|
11
14
|
const VALID_TEAM_DIRS = ['skills', 'agents', 'rules', 'commands'];
|
|
12
15
|
function resolveTools(toolsArg) {
|
|
13
16
|
const raw = toolsArg.trim().toLowerCase();
|
|
@@ -121,8 +124,17 @@ function registerInit(program) {
|
|
|
121
124
|
// ── 1. 글로벌 User 커맨드 설치 ──
|
|
122
125
|
let globalStatus = 'already';
|
|
123
126
|
if (opts.update || !hasGlobalUserCommands()) {
|
|
124
|
-
installGlobalUserCommands();
|
|
127
|
+
const { commands } = installGlobalUserCommands();
|
|
125
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);
|
|
126
138
|
}
|
|
127
139
|
// ── 2. 로컬 Builder 커맨드 (팀 프로젝트인 경우) ──
|
|
128
140
|
const localResults = [];
|
package/dist/commands/install.js
CHANGED
|
@@ -9,19 +9,22 @@ 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
|
|
12
|
+
const slug_js_1 = require("../lib/slug.js");
|
|
13
13
|
function registerInstall(program) {
|
|
14
14
|
program
|
|
15
15
|
.command('install <slug>')
|
|
16
16
|
.description('에이전트 팀 패키지를 .relay/teams/에 다운로드합니다')
|
|
17
|
-
.action(async (
|
|
17
|
+
.action(async (slugInput) => {
|
|
18
18
|
const json = program.opts().json ?? false;
|
|
19
19
|
const projectPath = process.cwd();
|
|
20
|
-
const teamDir = path_1.default.join(projectPath, '.relay', 'teams', slug);
|
|
21
20
|
const tempDir = (0, storage_js_1.makeTempDir)();
|
|
22
21
|
try {
|
|
22
|
+
// 0. Resolve scoped slug
|
|
23
|
+
const parsed = await (0, slug_js_1.resolveSlug)(slugInput);
|
|
24
|
+
const slug = parsed.full;
|
|
23
25
|
// 1. Fetch team metadata
|
|
24
26
|
const team = await (0, api_js_1.fetchTeamInfo)(slug);
|
|
27
|
+
const teamDir = path_1.default.join(projectPath, '.relay', 'teams', parsed.owner, parsed.name);
|
|
25
28
|
// 2. Visibility check
|
|
26
29
|
const visibility = team.visibility ?? 'public';
|
|
27
30
|
if (visibility === 'login-only' || visibility === 'invite-only') {
|
|
@@ -62,8 +65,6 @@ function registerInstall(program) {
|
|
|
62
65
|
return count;
|
|
63
66
|
}
|
|
64
67
|
const fileCount = countFiles(teamDir);
|
|
65
|
-
// 5.5. Inject update-check preamble into SKILL.md files
|
|
66
|
-
(0, preamble_js_1.injectPreambleToTeam)(teamDir, slug);
|
|
67
68
|
// 6. Record in installed.json
|
|
68
69
|
const installed = (0, config_js_1.loadInstalled)();
|
|
69
70
|
installed[slug] = {
|
package/dist/commands/login.js
CHANGED
|
@@ -96,7 +96,7 @@ function registerLogin(program) {
|
|
|
96
96
|
.option('--token <token>', '직접 토큰 입력 (브라우저 없이)')
|
|
97
97
|
.action(async (opts) => {
|
|
98
98
|
const json = program.opts().json ?? false;
|
|
99
|
-
(0, config_js_1.
|
|
99
|
+
(0, config_js_1.ensureGlobalRelayDir)();
|
|
100
100
|
let token = opts.token;
|
|
101
101
|
if (!token) {
|
|
102
102
|
try {
|
|
@@ -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,20 +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
|
|
8
|
+
const slug_js_1 = require("../lib/slug.js");
|
|
9
9
|
function registerUpdate(program) {
|
|
10
10
|
program
|
|
11
11
|
.command('update <slug>')
|
|
12
12
|
.description('설치된 에이전트 팀을 최신 버전으로 업데이트합니다')
|
|
13
13
|
.option('--path <install_path>', '설치 경로 지정 (기본: ./.claude)')
|
|
14
14
|
.option('--code <code>', '초대 코드 (invite-only 팀 업데이트 시 필요)')
|
|
15
|
-
.action(async (
|
|
15
|
+
.action(async (slugInput, opts) => {
|
|
16
16
|
const json = program.opts().json ?? false;
|
|
17
17
|
const installPath = (0, config_js_1.getInstallPath)(opts.path);
|
|
18
18
|
const tempDir = (0, storage_js_1.makeTempDir)();
|
|
19
19
|
try {
|
|
20
|
-
//
|
|
20
|
+
// Resolve scoped slug (try installed.json first for offline, then server)
|
|
21
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
|
|
22
37
|
const currentEntry = installed[slug];
|
|
23
38
|
const currentVersion = currentEntry?.version ?? null;
|
|
24
39
|
// Fetch latest team metadata
|
|
@@ -55,8 +70,6 @@ function registerUpdate(program) {
|
|
|
55
70
|
await (0, storage_js_1.extractPackage)(tarPath, extractDir);
|
|
56
71
|
// Copy files to install_path
|
|
57
72
|
const files = (0, installer_js_1.installTeam)(extractDir, installPath);
|
|
58
|
-
// Inject update-check preamble into SKILL.md files
|
|
59
|
-
(0, preamble_js_1.injectPreambleToTeam)(installPath, slug);
|
|
60
73
|
// Update installed.json with new version
|
|
61
74
|
installed[slug] = {
|
|
62
75
|
version: latestVersion,
|
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,6 +44,15 @@ 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
57
|
const token = (0, config_js_1.loadToken)();
|
|
48
58
|
const headers = {
|
|
@@ -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',
|
|
@@ -83,8 +83,14 @@ exports.USER_COMMANDS = [
|
|
|
83
83
|
|
|
84
84
|
## 실행 방법
|
|
85
85
|
|
|
86
|
+
### 0. 업데이트 확인
|
|
87
|
+
- 먼저 \`relay check-update\` 명령어를 실행합니다.
|
|
88
|
+
- CLI 업데이트가 있으면 사용자에게 안내합니다: "relay v{new} available. Run: npm update -g relayax-cli"
|
|
89
|
+
- 팀 업데이트가 있으면 안내합니다.
|
|
90
|
+
- 업데이트 여부와 관계없이 설치를 계속 진행합니다.
|
|
91
|
+
|
|
86
92
|
### 1. 팀 패키지 다운로드
|
|
87
|
-
- \`relay install
|
|
93
|
+
- \`relay install <@author/slug>\` 명령어를 실행합니다.
|
|
88
94
|
- 패키지가 \`.relay/teams/<slug>/\`에 다운로드됩니다.
|
|
89
95
|
|
|
90
96
|
### 2. 패키지 내용 확인
|
|
@@ -109,7 +115,7 @@ exports.USER_COMMANDS = [
|
|
|
109
115
|
- **env**: 환경변수 확인 → required이면 설정 안내, optional이면 알림
|
|
110
116
|
- **mcp**: MCP 서버 설정 — 에이전트의 MCP 설정에 추가 안내
|
|
111
117
|
- **runtime**: Node.js/Python 버전 확인
|
|
112
|
-
- **teams**: 의존하는 다른 팀 → \`relay install
|
|
118
|
+
- **teams**: 의존하는 다른 팀 → \`relay install <@author/team>\`으로 재귀 설치
|
|
113
119
|
${LOGIN_JIT_GUIDE}
|
|
114
120
|
|
|
115
121
|
### 5. 완료 안내
|
|
@@ -118,9 +124,9 @@ ${LOGIN_JIT_GUIDE}
|
|
|
118
124
|
|
|
119
125
|
## 예시
|
|
120
126
|
|
|
121
|
-
사용자: /relay-install contents-team
|
|
122
|
-
→ relay install contents-team 실행 (패키지 다운로드)
|
|
123
|
-
→ .relay/teams/contents-team/ 내용 확인
|
|
127
|
+
사용자: /relay-install @example/contents-team
|
|
128
|
+
→ relay install @example/contents-team 실행 (패키지 다운로드)
|
|
129
|
+
→ .relay/teams/@example/contents-team/ 내용 확인
|
|
124
130
|
→ commands/cardnews.md → .claude/commands/cardnews.md 복사
|
|
125
131
|
→ skills/pdf-gen.md → .claude/skills/pdf-gen.md 복사
|
|
126
132
|
→ requires 확인: ✓ playwright 설치됨, ✓ sharp 설치됨
|
|
@@ -145,7 +151,7 @@ ${LOGIN_JIT_GUIDE}
|
|
|
145
151
|
사용자: /relay-list
|
|
146
152
|
→ relay list --json 실행
|
|
147
153
|
→ "2개 팀이 설치되어 있어요:"
|
|
148
|
-
→ " contents-team v1.2.0 (2일 전 설치) - /cardnews, /pdf-report"
|
|
154
|
+
→ " @example/contents-team v1.2.0 (2일 전 설치) - /cardnews, /pdf-report"
|
|
149
155
|
→ " qa-team v0.5.1 (1주 전 설치) - /qa, /qa-only"`,
|
|
150
156
|
},
|
|
151
157
|
{
|
|
@@ -155,15 +161,20 @@ ${LOGIN_JIT_GUIDE}
|
|
|
155
161
|
|
|
156
162
|
## 실행 방법
|
|
157
163
|
|
|
164
|
+
### 0. CLI 업데이트 확인
|
|
165
|
+
- 먼저 \`relay check-update\` 명령어를 실행합니다.
|
|
166
|
+
- CLI 업데이트가 있으면 사용자에게 안내합니다: "relay v{new} available. Run: npm update -g relayax-cli"
|
|
167
|
+
- 업데이트 여부와 관계없이 팀 업데이트를 계속 진행합니다.
|
|
168
|
+
|
|
158
169
|
### 특정 팀 업데이트
|
|
159
|
-
- 사용자가 팀 이름을 지정한 경우: \`relay update
|
|
170
|
+
- 사용자가 팀 이름을 지정한 경우: \`relay update <@author/slug> --json\` 실행
|
|
160
171
|
- 업데이트 결과를 보여줍니다 (이전 버전 → 새 버전)
|
|
161
172
|
|
|
162
173
|
### 전체 업데이트 확인
|
|
163
174
|
- 팀 이름을 지정하지 않은 경우:
|
|
164
175
|
1. \`relay outdated --json\`으로 업데이트 가능한 팀 목록을 확인합니다.
|
|
165
176
|
2. 업데이트 가능한 팀이 있으면 목록을 보여주고 어떤 팀을 업데이트할지 물어봅니다.
|
|
166
|
-
3. 선택된 팀에 대해 \`relay update
|
|
177
|
+
3. 선택된 팀에 대해 \`relay update <@author/slug> --json\`을 실행합니다.
|
|
167
178
|
4. 모두 최신이면 "모든 팀이 최신 버전입니다"라고 안내합니다.
|
|
168
179
|
|
|
169
180
|
## 예시
|
|
@@ -171,10 +182,10 @@ ${LOGIN_JIT_GUIDE}
|
|
|
171
182
|
사용자: /relay-update
|
|
172
183
|
→ relay outdated --json 실행
|
|
173
184
|
→ "1개 팀 업데이트 가능:"
|
|
174
|
-
→ " contents-team: v1.2.0 → v1.3.0"
|
|
185
|
+
→ " @example/contents-team: v1.2.0 → v1.3.0"
|
|
175
186
|
→ "업데이트할까요?"
|
|
176
|
-
→ relay update contents-team --json 실행
|
|
177
|
-
→ "✓ contents-team v1.3.0으로 업데이트 완료"`,
|
|
187
|
+
→ relay update @example/contents-team --json 실행
|
|
188
|
+
→ "✓ @example/contents-team v1.3.0으로 업데이트 완료"`,
|
|
178
189
|
},
|
|
179
190
|
{
|
|
180
191
|
id: 'relay-uninstall',
|
|
@@ -183,15 +194,15 @@ ${LOGIN_JIT_GUIDE}
|
|
|
183
194
|
|
|
184
195
|
## 실행 방법
|
|
185
196
|
|
|
186
|
-
1. \`relay uninstall
|
|
197
|
+
1. \`relay uninstall <@author/slug> --json\` 명령어를 실행합니다.
|
|
187
198
|
2. 삭제 결과를 보여줍니다 (팀 이름, 제거된 파일 수).
|
|
188
199
|
3. 삭제 완료 후 남아있는 팀 목록을 간단히 안내합니다.
|
|
189
200
|
|
|
190
201
|
## 예시
|
|
191
202
|
|
|
192
|
-
사용자: /relay-uninstall contents-team
|
|
193
|
-
→ relay uninstall contents-team --json 실행
|
|
194
|
-
→ "✓ contents-team 삭제 완료 (12개 파일 제거)"`,
|
|
203
|
+
사용자: /relay-uninstall @example/contents-team
|
|
204
|
+
→ relay uninstall @example/contents-team --json 실행
|
|
205
|
+
→ "✓ @example/contents-team 삭제 완료 (12개 파일 제거)"`,
|
|
195
206
|
},
|
|
196
207
|
];
|
|
197
208
|
// ─── Builder Commands (로컬 설치) ───
|
|
@@ -282,7 +293,7 @@ requires:
|
|
|
282
293
|
- filesystem
|
|
283
294
|
- network
|
|
284
295
|
teams:
|
|
285
|
-
- contents-team
|
|
296
|
+
- @example/contents-team
|
|
286
297
|
\`\`\`
|
|
287
298
|
|
|
288
299
|
### 4. 포트폴리오 생성 (슬롯 기반)
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -7,8 +7,18 @@ export declare const API_URL = "https://www.relayax.com";
|
|
|
7
7
|
* 3. 감지 안 되면 현재 디렉토리에 직접 설치
|
|
8
8
|
*/
|
|
9
9
|
export declare function getInstallPath(override?: string): string;
|
|
10
|
-
|
|
10
|
+
/** ~/.relay/ — 글로벌 (token, CLI cache) */
|
|
11
|
+
export declare function ensureGlobalRelayDir(): void;
|
|
12
|
+
/** cwd/.relay/ — 프로젝트 로컬 (installed.json, teams/) */
|
|
13
|
+
export declare function ensureProjectRelayDir(): void;
|
|
11
14
|
export declare function loadToken(): string | undefined;
|
|
12
15
|
export declare function saveToken(token: string): void;
|
|
16
|
+
/** 프로젝트 로컬 installed.json 읽기 (unscoped 키 자동 마이그레이션) */
|
|
13
17
|
export declare function loadInstalled(): InstalledRegistry;
|
|
18
|
+
/**
|
|
19
|
+
* 비동기 마이그레이션: unscoped 키를 서버 resolve하여 scoped 키로 변환.
|
|
20
|
+
* install/update 등 비동기 커맨드에서 호출.
|
|
21
|
+
*/
|
|
22
|
+
export declare function migrateInstalled(): Promise<void>;
|
|
23
|
+
/** 프로젝트 로컬 installed.json 쓰기 */
|
|
14
24
|
export declare function saveInstalled(registry: InstalledRegistry): void;
|
package/dist/lib/config.js
CHANGED
|
@@ -5,18 +5,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.API_URL = void 0;
|
|
7
7
|
exports.getInstallPath = getInstallPath;
|
|
8
|
-
exports.
|
|
8
|
+
exports.ensureGlobalRelayDir = ensureGlobalRelayDir;
|
|
9
|
+
exports.ensureProjectRelayDir = ensureProjectRelayDir;
|
|
9
10
|
exports.loadToken = loadToken;
|
|
10
11
|
exports.saveToken = saveToken;
|
|
11
12
|
exports.loadInstalled = loadInstalled;
|
|
13
|
+
exports.migrateInstalled = migrateInstalled;
|
|
12
14
|
exports.saveInstalled = saveInstalled;
|
|
13
15
|
const fs_1 = __importDefault(require("fs"));
|
|
14
16
|
const path_1 = __importDefault(require("path"));
|
|
15
17
|
const os_1 = __importDefault(require("os"));
|
|
16
18
|
const ai_tools_js_1 = require("./ai-tools.js");
|
|
19
|
+
const slug_js_1 = require("./slug.js");
|
|
17
20
|
exports.API_URL = 'https://www.relayax.com';
|
|
18
|
-
const
|
|
19
|
-
const INSTALLED_FILE = path_1.default.join(RELAY_DIR, 'installed.json');
|
|
21
|
+
const GLOBAL_RELAY_DIR = path_1.default.join(os_1.default.homedir(), '.relay');
|
|
20
22
|
/**
|
|
21
23
|
* 설치 경로를 결정한다.
|
|
22
24
|
* 1. --path 옵션이 있으면 그대로 사용
|
|
@@ -37,13 +39,21 @@ function getInstallPath(override) {
|
|
|
37
39
|
}
|
|
38
40
|
return cwd;
|
|
39
41
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
/** ~/.relay/ — 글로벌 (token, CLI cache) */
|
|
43
|
+
function ensureGlobalRelayDir() {
|
|
44
|
+
if (!fs_1.default.existsSync(GLOBAL_RELAY_DIR)) {
|
|
45
|
+
fs_1.default.mkdirSync(GLOBAL_RELAY_DIR, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** cwd/.relay/ — 프로젝트 로컬 (installed.json, teams/) */
|
|
49
|
+
function ensureProjectRelayDir() {
|
|
50
|
+
const dir = path_1.default.join(process.cwd(), '.relay');
|
|
51
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
52
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
43
53
|
}
|
|
44
54
|
}
|
|
45
55
|
function loadToken() {
|
|
46
|
-
const tokenFile = path_1.default.join(
|
|
56
|
+
const tokenFile = path_1.default.join(GLOBAL_RELAY_DIR, 'token');
|
|
47
57
|
if (!fs_1.default.existsSync(tokenFile))
|
|
48
58
|
return undefined;
|
|
49
59
|
try {
|
|
@@ -54,22 +64,79 @@ function loadToken() {
|
|
|
54
64
|
}
|
|
55
65
|
}
|
|
56
66
|
function saveToken(token) {
|
|
57
|
-
|
|
58
|
-
fs_1.default.writeFileSync(path_1.default.join(
|
|
67
|
+
ensureGlobalRelayDir();
|
|
68
|
+
fs_1.default.writeFileSync(path_1.default.join(GLOBAL_RELAY_DIR, 'token'), token);
|
|
59
69
|
}
|
|
70
|
+
/** 프로젝트 로컬 installed.json 읽기 (unscoped 키 자동 마이그레이션) */
|
|
60
71
|
function loadInstalled() {
|
|
61
|
-
|
|
72
|
+
const file = path_1.default.join(process.cwd(), '.relay', 'installed.json');
|
|
73
|
+
if (!fs_1.default.existsSync(file)) {
|
|
62
74
|
return {};
|
|
63
75
|
}
|
|
64
76
|
try {
|
|
65
|
-
const raw = fs_1.default.readFileSync(
|
|
66
|
-
|
|
77
|
+
const raw = fs_1.default.readFileSync(file, 'utf-8');
|
|
78
|
+
const registry = JSON.parse(raw);
|
|
79
|
+
return migrateInstalledKeys(registry);
|
|
67
80
|
}
|
|
68
81
|
catch {
|
|
69
82
|
return {};
|
|
70
83
|
}
|
|
71
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* unscoped 키를 감지하여 서버 resolve 없이 가능한 마이그레이션을 수행한다.
|
|
87
|
+
* 서버 resolve가 필요한 경우는 마이그레이션 보류 (다음 기회에 재시도).
|
|
88
|
+
*/
|
|
89
|
+
function migrateInstalledKeys(registry) {
|
|
90
|
+
const unscopedKeys = Object.keys(registry).filter((k) => !(0, slug_js_1.isScopedSlug)(k) && k !== 'relay-core');
|
|
91
|
+
if (unscopedKeys.length === 0)
|
|
92
|
+
return registry;
|
|
93
|
+
// 비동기 서버 resolve 없이는 owner를 알 수 없으므로,
|
|
94
|
+
// loadInstalled는 동기 함수 → 마이그레이션은 비동기 migrateInstalled()로 별도 호출
|
|
95
|
+
return registry;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 비동기 마이그레이션: unscoped 키를 서버 resolve하여 scoped 키로 변환.
|
|
99
|
+
* install/update 등 비동기 커맨드에서 호출.
|
|
100
|
+
*/
|
|
101
|
+
async function migrateInstalled() {
|
|
102
|
+
const { resolveSlugFromServer } = await import('./api.js');
|
|
103
|
+
const registry = loadInstalled();
|
|
104
|
+
const teamsDir = path_1.default.join(process.cwd(), '.relay', 'teams');
|
|
105
|
+
let changed = false;
|
|
106
|
+
for (const key of Object.keys(registry)) {
|
|
107
|
+
if ((0, slug_js_1.isScopedSlug)(key) || key === 'relay-core')
|
|
108
|
+
continue;
|
|
109
|
+
try {
|
|
110
|
+
const results = await resolveSlugFromServer(key);
|
|
111
|
+
if (results.length !== 1)
|
|
112
|
+
continue;
|
|
113
|
+
const { owner, name } = results[0];
|
|
114
|
+
const scopedKey = `@${owner}/${name}`;
|
|
115
|
+
// installed.json 키 변환
|
|
116
|
+
registry[scopedKey] = registry[key];
|
|
117
|
+
delete registry[key];
|
|
118
|
+
// 디렉토리 이동
|
|
119
|
+
const oldDir = path_1.default.join(teamsDir, key);
|
|
120
|
+
const newDir = path_1.default.join(teamsDir, owner, name);
|
|
121
|
+
if (fs_1.default.existsSync(oldDir)) {
|
|
122
|
+
fs_1.default.mkdirSync(path_1.default.dirname(newDir), { recursive: true });
|
|
123
|
+
fs_1.default.renameSync(oldDir, newDir);
|
|
124
|
+
// files 배열 업데이트
|
|
125
|
+
registry[scopedKey].files = registry[scopedKey].files.map((f) => f.replace(`/teams/${key}`, `/teams/${owner}/${name}`));
|
|
126
|
+
}
|
|
127
|
+
changed = true;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// 네트워크 오류 등 — 다음 기회에 재시도
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (changed) {
|
|
134
|
+
saveInstalled(registry);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** 프로젝트 로컬 installed.json 쓰기 */
|
|
72
138
|
function saveInstalled(registry) {
|
|
73
|
-
|
|
74
|
-
|
|
139
|
+
ensureProjectRelayDir();
|
|
140
|
+
const file = path_1.default.join(process.cwd(), '.relay', 'installed.json');
|
|
141
|
+
fs_1.default.writeFileSync(file, JSON.stringify(registry, null, 2));
|
|
75
142
|
}
|
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
|
@@ -2,11 +2,14 @@ export interface InstalledTeam {
|
|
|
2
2
|
version: string;
|
|
3
3
|
installed_at: string;
|
|
4
4
|
files: string[];
|
|
5
|
+
type?: 'team' | 'system';
|
|
5
6
|
}
|
|
7
|
+
/** 키는 scoped slug 포맷: "@owner/name" */
|
|
6
8
|
export interface InstalledRegistry {
|
|
7
|
-
[
|
|
9
|
+
[scopedSlug: string]: InstalledTeam;
|
|
8
10
|
}
|
|
9
11
|
export interface TeamRegistryInfo {
|
|
12
|
+
/** scoped slug 포맷: "@owner/name" */
|
|
10
13
|
slug: string;
|
|
11
14
|
name: string;
|
|
12
15
|
description?: string;
|
|
@@ -16,11 +19,9 @@ export interface TeamRegistryInfo {
|
|
|
16
19
|
name: string;
|
|
17
20
|
description: string;
|
|
18
21
|
}[];
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
skills: number;
|
|
23
|
-
};
|
|
22
|
+
component_agents: number;
|
|
23
|
+
component_rules: number;
|
|
24
|
+
component_skills: number;
|
|
24
25
|
tags?: string[];
|
|
25
26
|
install_count?: number;
|
|
26
27
|
requires?: Record<string, unknown>;
|
|
@@ -38,6 +39,7 @@ export interface TeamRegistryInfo {
|
|
|
38
39
|
} | null;
|
|
39
40
|
}
|
|
40
41
|
export interface SearchResult {
|
|
42
|
+
/** scoped slug 포맷: "@owner/name" */
|
|
41
43
|
slug: string;
|
|
42
44
|
name: string;
|
|
43
45
|
description: string;
|