relayax-cli 0.3.41 → 0.3.42
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/access.js +12 -12
- package/dist/commands/changelog.js +2 -2
- package/dist/commands/check-update.js +12 -12
- package/dist/commands/create.js +46 -19
- package/dist/commands/deploy-record.js +2 -2
- package/dist/commands/diff.js +2 -2
- package/dist/commands/grant.d.ts +33 -0
- package/dist/commands/grant.js +190 -0
- package/dist/commands/init.js +10 -10
- package/dist/commands/install.js +69 -68
- package/dist/commands/join.js +3 -3
- package/dist/commands/list.js +15 -15
- package/dist/commands/login.js +10 -3
- package/dist/commands/orgs.js +1 -1
- package/dist/commands/outdated.js +7 -7
- package/dist/commands/package.d.ts +18 -0
- package/dist/commands/package.js +355 -146
- package/dist/commands/ping.js +5 -5
- package/dist/commands/publish.d.ts +1 -1
- package/dist/commands/publish.js +56 -48
- package/dist/commands/search.js +2 -2
- package/dist/commands/status.js +11 -11
- package/dist/commands/uninstall.js +7 -7
- package/dist/commands/update.js +22 -22
- package/dist/commands/versions.js +2 -2
- package/dist/index.js +2 -0
- package/dist/lib/ai-tools.d.ts +15 -0
- package/dist/lib/ai-tools.js +48 -1
- package/dist/lib/api.d.ts +7 -7
- package/dist/lib/api.js +11 -11
- package/dist/lib/command-adapter.js +30 -682
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/config.js +2 -2
- package/dist/lib/guide.js +34 -79
- package/dist/lib/installer.d.ts +2 -2
- package/dist/lib/installer.js +4 -4
- package/dist/lib/preamble.d.ts +4 -4
- package/dist/lib/preamble.js +14 -14
- package/dist/lib/slug.d.ts +5 -0
- package/dist/lib/slug.js +49 -2
- package/dist/lib/update-cache.js +4 -4
- package/dist/lib/version-check.d.ts +3 -3
- package/dist/lib/version-check.js +13 -13
- package/dist/prompts/_business-card.md +41 -0
- package/dist/prompts/_error-handling.md +38 -0
- package/dist/prompts/_requirements-check.md +59 -0
- package/dist/prompts/_setup-cli.md +19 -0
- package/dist/prompts/_setup-login.md +7 -0
- package/dist/prompts/_setup-org.md +27 -0
- package/dist/prompts/business-card.md +41 -0
- package/dist/prompts/error-handling.md +38 -0
- package/dist/prompts/index.d.ts +7 -0
- package/dist/prompts/index.js +28 -0
- package/dist/prompts/install.md +187 -0
- package/dist/prompts/publish.md +444 -0
- package/dist/prompts/requirements-check.md +59 -0
- package/dist/types.d.ts +9 -9
- package/package.json +3 -3
package/dist/commands/package.js
CHANGED
|
@@ -3,94 +3,21 @@ 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.resolveRelayDir = resolveRelayDir;
|
|
7
|
+
exports.initGlobalAgentHome = initGlobalAgentHome;
|
|
6
8
|
exports.registerPackage = registerPackage;
|
|
7
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const os_1 = __importDefault(require("os"));
|
|
8
11
|
const path_1 = __importDefault(require("path"));
|
|
9
12
|
const crypto_1 = __importDefault(require("crypto"));
|
|
10
13
|
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
11
14
|
const ai_tools_js_1 = require("../lib/ai-tools.js");
|
|
12
15
|
const SYNC_DIRS = ['skills', 'commands', 'agents', 'rules'];
|
|
13
|
-
const EXCLUDE_SUBDIRS = ['relay']; // relay CLI 전용 하위 디렉토리 제외
|
|
14
16
|
// ─── Helpers ───
|
|
15
17
|
function fileHash(filePath) {
|
|
16
18
|
const content = fs_1.default.readFileSync(filePath);
|
|
17
19
|
return crypto_1.default.createHash('md5').update(content).digest('hex');
|
|
18
20
|
}
|
|
19
|
-
/**
|
|
20
|
-
* 디렉토리를 재귀 탐색하여 파일 목록을 반환한다.
|
|
21
|
-
* baseDir 기준 상대 경로 + 해시.
|
|
22
|
-
*/
|
|
23
|
-
function scanDir(baseDir, subDir) {
|
|
24
|
-
const fullDir = path_1.default.join(baseDir, subDir);
|
|
25
|
-
if (!fs_1.default.existsSync(fullDir))
|
|
26
|
-
return [];
|
|
27
|
-
const entries = [];
|
|
28
|
-
function walk(dir) {
|
|
29
|
-
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
30
|
-
if (entry.name.startsWith('.'))
|
|
31
|
-
continue;
|
|
32
|
-
const fullPath = path_1.default.join(dir, entry.name);
|
|
33
|
-
if (entry.isDirectory()) {
|
|
34
|
-
walk(fullPath);
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
const relPath = path_1.default.relative(baseDir, fullPath);
|
|
38
|
-
entries.push({ relPath, hash: fileHash(fullPath) });
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
walk(fullDir);
|
|
43
|
-
return entries;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* 소스 디렉토리(예: .claude/)에서 배포 가능한 콘텐츠를 스캔한다.
|
|
47
|
-
* relay/ 하위 디렉토리는 제외.
|
|
48
|
-
*/
|
|
49
|
-
function scanSource(projectPath, tool) {
|
|
50
|
-
const sourceBase = path_1.default.join(projectPath, tool.skillsDir);
|
|
51
|
-
const files = [];
|
|
52
|
-
const summary = {};
|
|
53
|
-
for (const dir of SYNC_DIRS) {
|
|
54
|
-
const fullDir = path_1.default.join(sourceBase, dir);
|
|
55
|
-
if (!fs_1.default.existsSync(fullDir))
|
|
56
|
-
continue;
|
|
57
|
-
// 제외 대상 필터링 (예: commands/relay/)
|
|
58
|
-
const dirEntries = fs_1.default.readdirSync(fullDir, { withFileTypes: true });
|
|
59
|
-
let count = 0;
|
|
60
|
-
for (const entry of dirEntries) {
|
|
61
|
-
if (entry.name.startsWith('.'))
|
|
62
|
-
continue;
|
|
63
|
-
if (entry.isDirectory() && EXCLUDE_SUBDIRS.includes(entry.name))
|
|
64
|
-
continue;
|
|
65
|
-
const entryPath = path_1.default.join(fullDir, entry.name);
|
|
66
|
-
if (entry.isDirectory()) {
|
|
67
|
-
// 하위 파일 재귀 탐색
|
|
68
|
-
const subFiles = scanDir(sourceBase, path_1.default.join(dir, entry.name));
|
|
69
|
-
// relPath를 sourceBase 기준 → SYNC_DIRS 기준으로 유지
|
|
70
|
-
files.push(...subFiles);
|
|
71
|
-
count += subFiles.length > 0 ? 1 : 0; // 디렉토리 단위로 카운트
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
const relPath = path_1.default.relative(sourceBase, entryPath);
|
|
75
|
-
files.push({ relPath, hash: fileHash(entryPath) });
|
|
76
|
-
count++;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
if (count > 0)
|
|
80
|
-
summary[dir] = count;
|
|
81
|
-
}
|
|
82
|
-
return { tool, files, summary };
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* .relay/ 디렉토리의 현재 콘텐츠를 스캔한다.
|
|
86
|
-
*/
|
|
87
|
-
function scanRelay(relayDir) {
|
|
88
|
-
const files = [];
|
|
89
|
-
for (const dir of SYNC_DIRS) {
|
|
90
|
-
files.push(...scanDir(relayDir, dir));
|
|
91
|
-
}
|
|
92
|
-
return files;
|
|
93
|
-
}
|
|
94
21
|
/**
|
|
95
22
|
* 소스와 .relay/를 비교하여 diff를 생성한다.
|
|
96
23
|
*/
|
|
@@ -145,6 +72,186 @@ function syncToRelay(sourceBase, relayDir, diff) {
|
|
|
145
72
|
}
|
|
146
73
|
}
|
|
147
74
|
}
|
|
75
|
+
// ─── Contents-based Helpers ───
|
|
76
|
+
/**
|
|
77
|
+
* from 경로를 절대 경로로 해석한다.
|
|
78
|
+
* ~/로 시작하면 홈 디렉토리, 그 외는 projectPath 기준 상대 경로.
|
|
79
|
+
*/
|
|
80
|
+
function resolveFromPath(fromPath, projectPath) {
|
|
81
|
+
if (fromPath.startsWith('~/')) {
|
|
82
|
+
return path_1.default.join(os_1.default.homedir(), fromPath.slice(2));
|
|
83
|
+
}
|
|
84
|
+
return path_1.default.join(projectPath, fromPath);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 파일 또는 디렉토리의 모든 파일을 재귀 스캔하여 FileEntry[]를 반환한다.
|
|
88
|
+
* relPath는 baseDir 기준.
|
|
89
|
+
*/
|
|
90
|
+
function scanPath(absPath) {
|
|
91
|
+
if (!fs_1.default.existsSync(absPath))
|
|
92
|
+
return [];
|
|
93
|
+
const stat = fs_1.default.statSync(absPath);
|
|
94
|
+
if (stat.isFile()) {
|
|
95
|
+
return [{ relPath: path_1.default.basename(absPath), hash: fileHash(absPath) }];
|
|
96
|
+
}
|
|
97
|
+
// 디렉토리
|
|
98
|
+
const entries = [];
|
|
99
|
+
function walk(dir) {
|
|
100
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
101
|
+
if (entry.name.startsWith('.'))
|
|
102
|
+
continue;
|
|
103
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
104
|
+
if (entry.isDirectory()) {
|
|
105
|
+
walk(fullPath);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
entries.push({ relPath: path_1.default.relative(absPath, fullPath), hash: fileHash(fullPath) });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
walk(absPath);
|
|
113
|
+
return entries;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* contents 매니페스트 기반으로 각 항목의 원본과 .relay/ 복사본을 비교한다.
|
|
117
|
+
*/
|
|
118
|
+
function computeContentsDiff(contents, relayDir, projectPath) {
|
|
119
|
+
const diff = [];
|
|
120
|
+
for (const entry of contents) {
|
|
121
|
+
const absFrom = resolveFromPath(entry.from, projectPath);
|
|
122
|
+
if (!fs_1.default.existsSync(absFrom)) {
|
|
123
|
+
diff.push({ name: entry.name, type: entry.type, status: 'source_missing' });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// from 경로에서 .relay/ 내 대응 위치 결정
|
|
127
|
+
// from: .claude/skills/code-review → .relay/skills/code-review
|
|
128
|
+
// from: ~/.claude/skills/code-review → .relay/skills/code-review
|
|
129
|
+
const relaySubPath = deriveRelaySubPath(entry);
|
|
130
|
+
const relayItemDir = path_1.default.join(relayDir, relaySubPath);
|
|
131
|
+
const sourceFiles = scanPath(absFrom);
|
|
132
|
+
const relayFiles = scanPath(relayItemDir);
|
|
133
|
+
const fileDiff = computeDiff(sourceFiles, relayFiles);
|
|
134
|
+
const hasChanges = fileDiff.some((d) => d.status !== 'unchanged');
|
|
135
|
+
diff.push({
|
|
136
|
+
name: entry.name,
|
|
137
|
+
type: entry.type,
|
|
138
|
+
status: hasChanges ? 'modified' : 'unchanged',
|
|
139
|
+
files: hasChanges ? fileDiff.filter((d) => d.status !== 'unchanged') : undefined,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
// 소스 디렉토리를 다시 스캔하여 contents에 없는 새 항목 탐지
|
|
143
|
+
const newItems = discoverNewItems(contents, projectPath);
|
|
144
|
+
return { diff, newItems };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* contents 항목의 from 경로에서 .relay/ 내 서브경로를 유도한다.
|
|
148
|
+
* 예: .claude/skills/code-review → skills/code-review
|
|
149
|
+
* ~/.claude/agents/dev-lead.md → agents/dev-lead.md
|
|
150
|
+
*/
|
|
151
|
+
function deriveRelaySubPath(entry) {
|
|
152
|
+
const from = entry.from.startsWith('~/') ? entry.from.slice(2) : entry.from;
|
|
153
|
+
// skills/xxx, agents/xxx 등의 패턴을 추출
|
|
154
|
+
for (const dir of SYNC_DIRS) {
|
|
155
|
+
const idx = from.indexOf(`/${dir}/`);
|
|
156
|
+
if (idx !== -1) {
|
|
157
|
+
return from.slice(idx + 1); // /skills/code-review → skills/code-review
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// fallback: type + name
|
|
161
|
+
return `${entry.type}s/${entry.name}`;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* contents에 등록되지 않은 새 항목을 소스 디렉토리에서 찾는다.
|
|
165
|
+
*/
|
|
166
|
+
function discoverNewItems(contents, projectPath) {
|
|
167
|
+
const existingNames = new Set(contents.map((c) => `${c.type}:${c.name}`));
|
|
168
|
+
const newItems = [];
|
|
169
|
+
// 로컬 소스 스캔
|
|
170
|
+
const localTools = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
|
|
171
|
+
for (const tool of localTools) {
|
|
172
|
+
const items = (0, ai_tools_js_1.scanLocalItems)(projectPath, tool);
|
|
173
|
+
for (const item of items) {
|
|
174
|
+
if (!existingNames.has(`${item.type}:${item.name}`)) {
|
|
175
|
+
newItems.push({
|
|
176
|
+
name: item.name,
|
|
177
|
+
type: item.type,
|
|
178
|
+
source: tool.skillsDir,
|
|
179
|
+
relativePath: item.relativePath,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// 글로벌 소스 스캔
|
|
185
|
+
const globalTools = (0, ai_tools_js_1.detectGlobalCLIs)();
|
|
186
|
+
for (const tool of globalTools) {
|
|
187
|
+
const items = (0, ai_tools_js_1.scanGlobalItems)(tool);
|
|
188
|
+
for (const item of items) {
|
|
189
|
+
if (!existingNames.has(`${item.type}:${item.name}`)) {
|
|
190
|
+
newItems.push({
|
|
191
|
+
name: item.name,
|
|
192
|
+
type: item.type,
|
|
193
|
+
source: `~/${tool.skillsDir}`,
|
|
194
|
+
relativePath: item.relativePath,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return newItems;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* contents 항목 단위로 from → .relay/ 동기화한다.
|
|
203
|
+
*/
|
|
204
|
+
function syncContentsToRelay(contents, contentsDiff, relayDir, projectPath) {
|
|
205
|
+
for (const diffEntry of contentsDiff) {
|
|
206
|
+
if (diffEntry.status !== 'modified')
|
|
207
|
+
continue;
|
|
208
|
+
const content = contents.find((c) => c.name === diffEntry.name && c.type === diffEntry.type);
|
|
209
|
+
if (!content)
|
|
210
|
+
continue;
|
|
211
|
+
const absFrom = resolveFromPath(content.from, projectPath);
|
|
212
|
+
const relaySubPath = deriveRelaySubPath(content);
|
|
213
|
+
const relayItemDir = path_1.default.join(relayDir, relaySubPath);
|
|
214
|
+
// 소스 파일을 .relay/로 복사
|
|
215
|
+
const sourceFiles = scanPath(absFrom);
|
|
216
|
+
const relayFiles = scanPath(relayItemDir);
|
|
217
|
+
const fileDiff = computeDiff(sourceFiles, relayFiles);
|
|
218
|
+
syncToRelay(absFrom, relayItemDir, fileDiff);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// ─── Global Agent Home ───
|
|
222
|
+
/**
|
|
223
|
+
* 패키지 홈 디렉토리를 결정한다.
|
|
224
|
+
* 1. 프로젝트에 .relay/가 있으면 → projectPath/.relay/
|
|
225
|
+
* 2. 없으면 → ~/.relay/agents/<slug>/ (slug 필요)
|
|
226
|
+
*
|
|
227
|
+
* slug가 없고 프로젝트에도 .relay/가 없으면 null 반환.
|
|
228
|
+
*/
|
|
229
|
+
function resolveRelayDir(projectPath, slug) {
|
|
230
|
+
const projectRelay = path_1.default.join(projectPath, '.relay');
|
|
231
|
+
if (fs_1.default.existsSync(path_1.default.join(projectRelay, 'relay.yaml'))) {
|
|
232
|
+
return projectRelay;
|
|
233
|
+
}
|
|
234
|
+
// .relay/ 디렉토리는 있지만 relay.yaml이 없는 경우도 프로젝트 모드
|
|
235
|
+
if (fs_1.default.existsSync(projectRelay)) {
|
|
236
|
+
return projectRelay;
|
|
237
|
+
}
|
|
238
|
+
// 글로벌 에이전트 홈
|
|
239
|
+
if (slug) {
|
|
240
|
+
return path_1.default.join(os_1.default.homedir(), '.relay', 'agents', slug);
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* 글로벌 에이전트 홈에 패키지 구조를 초기화한다.
|
|
246
|
+
*/
|
|
247
|
+
function initGlobalAgentHome(slug, yamlData) {
|
|
248
|
+
const agentDir = path_1.default.join(os_1.default.homedir(), '.relay', 'agents', slug);
|
|
249
|
+
fs_1.default.mkdirSync(agentDir, { recursive: true });
|
|
250
|
+
fs_1.default.mkdirSync(path_1.default.join(agentDir, 'skills'), { recursive: true });
|
|
251
|
+
fs_1.default.mkdirSync(path_1.default.join(agentDir, 'agents'), { recursive: true });
|
|
252
|
+
fs_1.default.writeFileSync(path_1.default.join(agentDir, 'relay.yaml'), js_yaml_1.default.dump(yamlData, { lineWidth: 120 }), 'utf-8');
|
|
253
|
+
return agentDir;
|
|
254
|
+
}
|
|
148
255
|
// ─── Command ───
|
|
149
256
|
function registerPackage(program) {
|
|
150
257
|
program
|
|
@@ -153,6 +260,7 @@ function registerPackage(program) {
|
|
|
153
260
|
.option('--source <dir>', '소스 디렉토리 지정 (예: .claude)')
|
|
154
261
|
.option('--sync', '변경사항을 .relay/에 즉시 반영', false)
|
|
155
262
|
.option('--init', '최초 패키징: 소스 감지 → .relay/ 초기화', false)
|
|
263
|
+
.option('--migrate', '기존 source 필드를 contents로 마이그레이션', false)
|
|
156
264
|
.action(async (opts) => {
|
|
157
265
|
const json = program.opts().json ?? false;
|
|
158
266
|
const projectPath = process.cwd();
|
|
@@ -160,127 +268,228 @@ function registerPackage(program) {
|
|
|
160
268
|
const relayYamlPath = path_1.default.join(relayDir, 'relay.yaml');
|
|
161
269
|
// ─── 최초 패키징 (--init) ───
|
|
162
270
|
if (opts.init || !fs_1.default.existsSync(relayYamlPath)) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
271
|
+
// 로컬 + 글로벌 소스를 모두 스캔하여 개별 항목 목록 생성
|
|
272
|
+
const localTools = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
|
|
273
|
+
const globalTools = (0, ai_tools_js_1.detectGlobalCLIs)();
|
|
274
|
+
const sources = [];
|
|
275
|
+
for (const tool of localTools) {
|
|
276
|
+
const items = (0, ai_tools_js_1.scanLocalItems)(projectPath, tool);
|
|
277
|
+
if (items.length > 0) {
|
|
278
|
+
sources.push({
|
|
279
|
+
path: tool.skillsDir,
|
|
280
|
+
location: 'local',
|
|
281
|
+
name: tool.name,
|
|
282
|
+
items,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
for (const tool of globalTools) {
|
|
287
|
+
const items = (0, ai_tools_js_1.scanGlobalItems)(tool);
|
|
288
|
+
if (items.length > 0) {
|
|
289
|
+
sources.push({
|
|
290
|
+
path: `~/${tool.skillsDir}`,
|
|
291
|
+
location: 'global',
|
|
292
|
+
name: `${tool.name} (global)`,
|
|
293
|
+
items,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// ~/.relay/agents/ 에 기존 에이전트 패키지가 있는지 스캔
|
|
298
|
+
const globalAgentsDir = path_1.default.join(os_1.default.homedir(), '.relay', 'agents');
|
|
299
|
+
const existingAgents = [];
|
|
300
|
+
if (fs_1.default.existsSync(globalAgentsDir)) {
|
|
301
|
+
for (const entry of fs_1.default.readdirSync(globalAgentsDir, { withFileTypes: true })) {
|
|
302
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
303
|
+
continue;
|
|
304
|
+
const agentYaml = path_1.default.join(globalAgentsDir, entry.name, 'relay.yaml');
|
|
305
|
+
if (fs_1.default.existsSync(agentYaml)) {
|
|
306
|
+
try {
|
|
307
|
+
const cfg = js_yaml_1.default.load(fs_1.default.readFileSync(agentYaml, 'utf-8'));
|
|
308
|
+
existingAgents.push({
|
|
309
|
+
slug: cfg.slug ?? entry.name,
|
|
310
|
+
name: cfg.name ?? entry.name,
|
|
311
|
+
version: cfg.version ?? '0.0.0',
|
|
312
|
+
path: `~/.relay/agents/${entry.name}`,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
catch { /* skip invalid yaml */ }
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
168
319
|
if (json) {
|
|
169
320
|
console.log(JSON.stringify({
|
|
170
321
|
status: 'init_required',
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
name: s.tool.name,
|
|
174
|
-
summary: s.summary,
|
|
175
|
-
fileCount: s.files.length,
|
|
176
|
-
})),
|
|
322
|
+
sources,
|
|
323
|
+
existing_agents: existingAgents,
|
|
177
324
|
}));
|
|
178
325
|
}
|
|
179
326
|
else {
|
|
180
|
-
if (
|
|
327
|
+
if (sources.length === 0 && existingAgents.length === 0) {
|
|
181
328
|
console.error('배포 가능한 에이전트 콘텐츠를 찾지 못했습니다.');
|
|
182
329
|
console.error('skills/, commands/, agents/, rules/ 중 하나를 만들어주세요.');
|
|
183
330
|
process.exit(1);
|
|
184
331
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
332
|
+
if (sources.length > 0) {
|
|
333
|
+
console.error('\n발견된 에이전트 콘텐츠:\n');
|
|
334
|
+
for (const src of sources) {
|
|
335
|
+
const typeCounts = new Map();
|
|
336
|
+
for (const item of src.items) {
|
|
337
|
+
typeCounts.set(item.type, (typeCounts.get(item.type) ?? 0) + 1);
|
|
338
|
+
}
|
|
339
|
+
const parts = Array.from(typeCounts.entries())
|
|
340
|
+
.map(([t, c]) => `${t} ${c}개`)
|
|
341
|
+
.join(', ');
|
|
342
|
+
const label = src.location === 'global' ? '🌐' : '📁';
|
|
343
|
+
console.error(` ${label} ${src.path}/ — ${parts}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (existingAgents.length > 0) {
|
|
347
|
+
console.error('\n기존 글로벌 에이전트:\n');
|
|
348
|
+
for (const agent of existingAgents) {
|
|
349
|
+
console.error(` 📦 ${agent.name} (v${agent.version}) — ${agent.path}`);
|
|
350
|
+
}
|
|
191
351
|
}
|
|
192
352
|
console.error('');
|
|
193
353
|
}
|
|
194
354
|
return;
|
|
195
355
|
}
|
|
196
|
-
// ───
|
|
356
|
+
// ─── 마이그레이션 (--migrate) ───
|
|
357
|
+
if (opts.migrate) {
|
|
358
|
+
const yamlMigrate = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
|
|
359
|
+
const cfgMigrate = js_yaml_1.default.load(yamlMigrate);
|
|
360
|
+
if (cfgMigrate.contents) {
|
|
361
|
+
if (json) {
|
|
362
|
+
console.log(JSON.stringify({ status: 'already_migrated', message: '이미 contents 형식입니다.' }));
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
console.error('✓ 이미 contents 형식입니다.');
|
|
366
|
+
}
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const legacySource = cfgMigrate.source;
|
|
370
|
+
if (!legacySource) {
|
|
371
|
+
if (json) {
|
|
372
|
+
console.log(JSON.stringify({ status: 'no_source', message: 'source 필드가 없습니다.' }));
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
console.error('source 필드가 없습니다. relay package --init으로 초기화하세요.');
|
|
376
|
+
}
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
// source 디렉토리를 스캔하여 모든 항목을 contents[]로 변환
|
|
380
|
+
const sourceBase = path_1.default.join(projectPath, legacySource);
|
|
381
|
+
const migratedContents = [];
|
|
382
|
+
if (fs_1.default.existsSync(sourceBase)) {
|
|
383
|
+
const localTools = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
|
|
384
|
+
const tool = localTools.find((t) => t.skillsDir === legacySource);
|
|
385
|
+
if (tool) {
|
|
386
|
+
const items = (0, ai_tools_js_1.scanLocalItems)(projectPath, tool);
|
|
387
|
+
for (const item of items) {
|
|
388
|
+
migratedContents.push({
|
|
389
|
+
name: item.name,
|
|
390
|
+
type: item.type,
|
|
391
|
+
from: `${legacySource}/${item.relativePath}`,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// relay.yaml에서 source 제거, contents 저장
|
|
397
|
+
delete cfgMigrate.source;
|
|
398
|
+
cfgMigrate.contents = migratedContents;
|
|
399
|
+
fs_1.default.writeFileSync(relayYamlPath, js_yaml_1.default.dump(cfgMigrate, { lineWidth: 120 }), 'utf-8');
|
|
400
|
+
if (json) {
|
|
401
|
+
console.log(JSON.stringify({ status: 'migrated', contents: migratedContents }));
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
console.error(`✓ source(${legacySource}) → contents(${migratedContents.length}개 항목)로 마이그레이션 완료`);
|
|
405
|
+
}
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
// ─── 재패키징 (contents 매니페스트 기반 동기화) ───
|
|
197
409
|
const yamlContent = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
|
|
198
410
|
const config = js_yaml_1.default.load(yamlContent);
|
|
199
|
-
const
|
|
200
|
-
|
|
411
|
+
const contents = config.contents ?? [];
|
|
412
|
+
// 기존 source 필드 → contents 마이그레이션 안내
|
|
413
|
+
if (!config.contents && config.source) {
|
|
414
|
+
const legacySource = config.source;
|
|
201
415
|
if (json) {
|
|
202
416
|
console.log(JSON.stringify({
|
|
203
|
-
status: '
|
|
204
|
-
message:
|
|
417
|
+
status: 'migration_required',
|
|
418
|
+
message: `relay.yaml의 source 필드를 contents로 마이그레이션해야 합니다.`,
|
|
419
|
+
legacy_source: legacySource,
|
|
205
420
|
}));
|
|
206
421
|
}
|
|
207
422
|
else {
|
|
208
|
-
console.error(
|
|
209
|
-
console.error(
|
|
423
|
+
console.error(`relay.yaml에 기존 source 필드(${legacySource})가 있습니다.`);
|
|
424
|
+
console.error(`contents 형식으로 마이그레이션하려면: relay package --migrate`);
|
|
210
425
|
}
|
|
211
426
|
process.exit(1);
|
|
212
427
|
}
|
|
213
|
-
|
|
214
|
-
const sourceBase = path_1.default.join(projectPath, source);
|
|
215
|
-
if (!fs_1.default.existsSync(sourceBase)) {
|
|
216
|
-
const msg = `소스 디렉토리 '${source}'를 찾을 수 없습니다.`;
|
|
428
|
+
if (contents.length === 0) {
|
|
217
429
|
if (json) {
|
|
218
|
-
console.log(JSON.stringify({
|
|
430
|
+
console.log(JSON.stringify({
|
|
431
|
+
status: 'no_contents',
|
|
432
|
+
message: 'relay.yaml에 contents가 없습니다. relay package --init으로 패키지를 초기화하세요.',
|
|
433
|
+
}));
|
|
219
434
|
}
|
|
220
435
|
else {
|
|
221
|
-
console.error(
|
|
436
|
+
console.error('relay.yaml에 contents가 없습니다.');
|
|
437
|
+
console.error('relay package --init으로 패키지를 초기화하세요.');
|
|
222
438
|
}
|
|
223
439
|
process.exit(1);
|
|
224
440
|
}
|
|
225
|
-
//
|
|
226
|
-
const
|
|
227
|
-
const tool = allTools.find((t) => t.skillsDir === source);
|
|
228
|
-
const toolName = tool?.name ?? source;
|
|
229
|
-
// diff 계산
|
|
230
|
-
const sourceScan = tool
|
|
231
|
-
? scanSource(projectPath, tool)
|
|
232
|
-
: { tool: { name: source, value: source, skillsDir: source }, files: [], summary: {} };
|
|
233
|
-
// tool이 없으면 직접 스캔
|
|
234
|
-
if (!tool) {
|
|
235
|
-
for (const dir of SYNC_DIRS) {
|
|
236
|
-
const files = scanDir(sourceBase, dir);
|
|
237
|
-
sourceScan.files.push(...files);
|
|
238
|
-
if (files.length > 0)
|
|
239
|
-
sourceScan.summary[dir] = files.length;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
const relayFiles = scanRelay(relayDir);
|
|
243
|
-
const diff = computeDiff(sourceScan.files, relayFiles);
|
|
441
|
+
// contents 기반 diff 계산
|
|
442
|
+
const { diff: contentsDiff, newItems } = computeContentsDiff(contents, relayDir, projectPath);
|
|
244
443
|
const summary = {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
444
|
+
modified: contentsDiff.filter((d) => d.status === 'modified').length,
|
|
445
|
+
unchanged: contentsDiff.filter((d) => d.status === 'unchanged').length,
|
|
446
|
+
source_missing: contentsDiff.filter((d) => d.status === 'source_missing').length,
|
|
447
|
+
new_available: newItems.length,
|
|
249
448
|
};
|
|
250
|
-
const hasChanges = summary.
|
|
251
|
-
// --sync:
|
|
449
|
+
const hasChanges = summary.modified > 0;
|
|
450
|
+
// --sync: contents 단위 동기화
|
|
252
451
|
if (opts.sync && hasChanges) {
|
|
253
|
-
|
|
452
|
+
syncContentsToRelay(contents, contentsDiff, relayDir, projectPath);
|
|
254
453
|
}
|
|
255
454
|
const result = {
|
|
256
|
-
|
|
257
|
-
|
|
455
|
+
diff: contentsDiff.filter((d) => d.status !== 'unchanged'),
|
|
456
|
+
new_items: newItems,
|
|
258
457
|
synced: opts.sync === true && hasChanges,
|
|
259
|
-
diff: diff.filter((d) => d.status !== 'unchanged'),
|
|
260
458
|
summary,
|
|
261
459
|
};
|
|
262
460
|
if (json) {
|
|
263
461
|
console.log(JSON.stringify(result));
|
|
264
462
|
}
|
|
265
463
|
else {
|
|
266
|
-
if (!hasChanges) {
|
|
267
|
-
console.error(
|
|
464
|
+
if (!hasChanges && newItems.length === 0 && summary.source_missing === 0) {
|
|
465
|
+
console.error('✓ 모든 콘텐츠가 동기화 상태입니다.');
|
|
268
466
|
return;
|
|
269
467
|
}
|
|
270
|
-
console.error(
|
|
271
|
-
for (const entry of
|
|
468
|
+
console.error('\n📦 콘텐츠 동기화 상태\n');
|
|
469
|
+
for (const entry of contentsDiff) {
|
|
272
470
|
if (entry.status === 'unchanged')
|
|
273
471
|
continue;
|
|
274
|
-
const icon = entry.status === '
|
|
275
|
-
console.error(`${icon}: ${entry.
|
|
472
|
+
const icon = entry.status === 'modified' ? ' 변경' : ' ⚠ 원본 없음';
|
|
473
|
+
console.error(`${icon}: ${entry.name} (${entry.type})`);
|
|
474
|
+
if (entry.files) {
|
|
475
|
+
for (const f of entry.files) {
|
|
476
|
+
console.error(` ${f.status}: ${f.relPath}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (newItems.length > 0) {
|
|
481
|
+
console.error('\n 새로 발견된 콘텐츠:');
|
|
482
|
+
for (const item of newItems) {
|
|
483
|
+
console.error(` + ${item.name} (${item.type}) — ${item.source}`);
|
|
484
|
+
}
|
|
276
485
|
}
|
|
277
486
|
console.error('');
|
|
278
|
-
console.error(` 합계:
|
|
487
|
+
console.error(` 합계: 변경 ${summary.modified}, 유지 ${summary.unchanged}, 원본 없음 ${summary.source_missing}, 신규 ${summary.new_available}`);
|
|
279
488
|
if (opts.sync) {
|
|
280
|
-
console.error(
|
|
489
|
+
console.error('\n✓ .relay/에 반영 완료');
|
|
281
490
|
}
|
|
282
|
-
else {
|
|
283
|
-
console.error(
|
|
491
|
+
else if (hasChanges) {
|
|
492
|
+
console.error('\n반영하려면: relay package --sync');
|
|
284
493
|
}
|
|
285
494
|
}
|
|
286
495
|
});
|
package/dist/commands/ping.js
CHANGED
|
@@ -25,15 +25,15 @@ function registerPing(program) {
|
|
|
25
25
|
});
|
|
26
26
|
slug = match ?? slugInput;
|
|
27
27
|
}
|
|
28
|
-
// Resolve version and
|
|
28
|
+
// Resolve version and agent_id from installed registry
|
|
29
29
|
const local = (0, config_js_1.loadInstalled)();
|
|
30
30
|
const global = (0, config_js_1.loadGlobalInstalled)();
|
|
31
31
|
const entry = local[slug] ?? global[slug];
|
|
32
32
|
const version = entry?.version;
|
|
33
|
-
const
|
|
34
|
-
// Fire-and-forget ping (
|
|
35
|
-
if (
|
|
36
|
-
await (0, api_js_1.sendUsagePing)(
|
|
33
|
+
const agentId = entry?.agent_id;
|
|
34
|
+
// Fire-and-forget ping (agent_id 기반, 없으면 skip)
|
|
35
|
+
if (agentId) {
|
|
36
|
+
await (0, api_js_1.sendUsagePing)(agentId, slug, version);
|
|
37
37
|
}
|
|
38
38
|
if (!opts.quiet) {
|
|
39
39
|
console.log(`RELAY_READY: ${slug}`);
|