relayax-cli 0.2.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.
Files changed (62) hide show
  1. package/dist/commands/access.js +12 -12
  2. package/dist/commands/changelog.js +2 -2
  3. package/dist/commands/check-update.js +12 -12
  4. package/dist/commands/create.js +46 -19
  5. package/dist/commands/deploy-record.js +2 -2
  6. package/dist/commands/diff.d.ts +2 -0
  7. package/dist/commands/diff.js +72 -0
  8. package/dist/commands/grant.d.ts +33 -0
  9. package/dist/commands/grant.js +190 -0
  10. package/dist/commands/init.js +10 -10
  11. package/dist/commands/install.js +125 -256
  12. package/dist/commands/join.d.ts +3 -2
  13. package/dist/commands/join.js +18 -69
  14. package/dist/commands/list.js +23 -26
  15. package/dist/commands/login.js +10 -3
  16. package/dist/commands/orgs.d.ts +10 -0
  17. package/dist/commands/orgs.js +128 -0
  18. package/dist/commands/outdated.js +7 -7
  19. package/dist/commands/package.d.ts +18 -0
  20. package/dist/commands/package.js +355 -146
  21. package/dist/commands/ping.js +5 -5
  22. package/dist/commands/publish.d.ts +1 -1
  23. package/dist/commands/publish.js +105 -103
  24. package/dist/commands/search.js +2 -2
  25. package/dist/commands/status.js +11 -11
  26. package/dist/commands/uninstall.js +7 -7
  27. package/dist/commands/update.js +22 -22
  28. package/dist/commands/versions.d.ts +2 -0
  29. package/dist/commands/versions.js +44 -0
  30. package/dist/index.js +8 -2
  31. package/dist/lib/ai-tools.d.ts +15 -0
  32. package/dist/lib/ai-tools.js +48 -1
  33. package/dist/lib/api.d.ts +13 -12
  34. package/dist/lib/api.js +24 -39
  35. package/dist/lib/command-adapter.js +41 -693
  36. package/dist/lib/config.d.ts +10 -5
  37. package/dist/lib/config.js +106 -24
  38. package/dist/lib/guide.js +34 -79
  39. package/dist/lib/installer.d.ts +2 -2
  40. package/dist/lib/installer.js +4 -4
  41. package/dist/lib/preamble.d.ts +4 -4
  42. package/dist/lib/preamble.js +14 -15
  43. package/dist/lib/slug.d.ts +5 -1
  44. package/dist/lib/slug.js +52 -9
  45. package/dist/lib/update-cache.js +4 -4
  46. package/dist/lib/version-check.d.ts +3 -3
  47. package/dist/lib/version-check.js +13 -13
  48. package/dist/prompts/_business-card.md +41 -0
  49. package/dist/prompts/_error-handling.md +38 -0
  50. package/dist/prompts/_requirements-check.md +59 -0
  51. package/dist/prompts/_setup-cli.md +19 -0
  52. package/dist/prompts/_setup-login.md +7 -0
  53. package/dist/prompts/_setup-org.md +27 -0
  54. package/dist/prompts/business-card.md +41 -0
  55. package/dist/prompts/error-handling.md +38 -0
  56. package/dist/prompts/index.d.ts +7 -0
  57. package/dist/prompts/index.js +28 -0
  58. package/dist/prompts/install.md +187 -0
  59. package/dist/prompts/publish.md +444 -0
  60. package/dist/prompts/requirements-check.md +59 -0
  61. package/dist/types.d.ts +10 -10
  62. package/package.json +3 -3
@@ -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
- const detected = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
164
- // 도구의 콘텐츠 스캔
165
- const scans = detected
166
- .map((tool) => scanSource(projectPath, tool))
167
- .filter((s) => s.files.length > 0);
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
- detected: scans.map((s) => ({
172
- source: s.tool.skillsDir,
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 (scans.length === 0) {
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
- console.error('\n프로젝트에서 발견된 에이전트 콘텐츠:\n');
186
- for (const scan of scans) {
187
- const parts = Object.entries(scan.summary)
188
- .map(([dir, count]) => `${dir} ${count}개`)
189
- .join(', ');
190
- console.error(` 📁 ${scan.tool.skillsDir}/ ${parts}`);
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
- // ─── 재패키징 (source 기반 동기화) ───
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 source = opts.source ?? config.source;
200
- if (!source) {
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: 'no_source',
204
- message: 'relay.yaml source 필드가 없습니다. --source 옵션으로 지정하거나 relay.yaml에 source를 추가하세요.',
417
+ status: 'migration_required',
418
+ message: `relay.yaml source 필드를 contents로 마이그레이션해야 합니다.`,
419
+ legacy_source: legacySource,
205
420
  }));
206
421
  }
207
422
  else {
208
- console.error('relay.yaml에 source 필드가 없습니다.');
209
- console.error('--source <dir> 옵션으로 지정하거나 relay.yaml에 source를 추가하세요.');
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({ error: 'SOURCE_NOT_FOUND', message: msg }));
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(msg);
436
+ console.error('relay.yaml에 contents가 없습니다.');
437
+ console.error('relay package --init으로 패키지를 초기화하세요.');
222
438
  }
223
439
  process.exit(1);
224
440
  }
225
- // 소스에서 해당 도구 찾기
226
- const allTools = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
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
- added: diff.filter((d) => d.status === 'added').length,
246
- modified: diff.filter((d) => d.status === 'modified').length,
247
- deleted: diff.filter((d) => d.status === 'deleted').length,
248
- unchanged: diff.filter((d) => d.status === 'unchanged').length,
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.added + summary.modified + summary.deleted > 0;
251
- // --sync: 즉시 동기화
449
+ const hasChanges = summary.modified > 0;
450
+ // --sync: contents 단위 동기화
252
451
  if (opts.sync && hasChanges) {
253
- syncToRelay(sourceBase, relayDir, diff);
452
+ syncContentsToRelay(contents, contentsDiff, relayDir, projectPath);
254
453
  }
255
454
  const result = {
256
- source,
257
- sourceName: toolName,
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(`✓ 소스(${source})와 .relay/가 동기화 상태입니다.`);
464
+ if (!hasChanges && newItems.length === 0 && summary.source_missing === 0) {
465
+ console.error('✓ 모든 콘텐츠가 동기화 상태입니다.');
268
466
  return;
269
467
  }
270
- console.error(`\n📦 소스 동기화 (${source}/ → .relay/)\n`);
271
- for (const entry of diff) {
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 === 'added' ? ' 신규' : entry.status === 'modified' ? ' 변경' : ' 삭제';
275
- console.error(`${icon}: ${entry.relPath}`);
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(` 합계: 신규 ${summary.added}, 변경 ${summary.modified}, 삭제 ${summary.deleted}, 유지 ${summary.unchanged}`);
487
+ console.error(` 합계: 변경 ${summary.modified}, 유지 ${summary.unchanged}, 원본 없음 ${summary.source_missing}, 신규 ${summary.new_available}`);
279
488
  if (opts.sync) {
280
- console.error(`\n✓ .relay/에 반영 완료`);
489
+ console.error('\n✓ .relay/에 반영 완료');
281
490
  }
282
- else {
283
- console.error(`\n반영하려면: relay package --sync`);
491
+ else if (hasChanges) {
492
+ console.error('\n반영하려면: relay package --sync');
284
493
  }
285
494
  }
286
495
  });
@@ -25,15 +25,15 @@ function registerPing(program) {
25
25
  });
26
26
  slug = match ?? slugInput;
27
27
  }
28
- // Resolve version and team_id from installed registry
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 teamId = entry?.team_id;
34
- // Fire-and-forget ping (team_id 기반, 없으면 skip)
35
- if (teamId) {
36
- await (0, api_js_1.sendUsagePing)(teamId, slug, version);
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}`);
@@ -28,7 +28,7 @@ export interface Requires {
28
28
  mcp?: RequiresMcp[];
29
29
  npm?: (string | RequiresNpm)[];
30
30
  env?: RequiresEnv[];
31
- teams?: string[];
31
+ agents?: string[];
32
32
  runtime?: {
33
33
  node?: string;
34
34
  python?: string;