relayax-cli 0.3.58 → 0.3.59

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.
@@ -1,10 +1,39 @@
1
1
  import { Command } from 'commander';
2
+ type DiffStatus = 'added' | 'modified' | 'deleted' | 'unchanged';
3
+ interface DiffEntry {
4
+ relPath: string;
5
+ status: DiffStatus;
6
+ }
2
7
  import type { ContentType } from '../lib/ai-tools.js';
3
8
  export interface ContentEntry {
4
9
  name: string;
5
10
  type: ContentType;
6
11
  from: string;
7
12
  }
13
+ type ContentDiffStatus = 'modified' | 'unchanged' | 'source_missing';
14
+ interface ContentDiffEntry {
15
+ name: string;
16
+ type: ContentType;
17
+ status: ContentDiffStatus;
18
+ files?: DiffEntry[];
19
+ }
20
+ interface NewItemEntry {
21
+ name: string;
22
+ type: ContentType;
23
+ source: string;
24
+ relativePath: string;
25
+ }
26
+ /**
27
+ * contents 매니페스트 기반으로 각 항목의 원본과 .relay/ 복사본을 비교한다.
28
+ */
29
+ export declare function computeContentsDiff(contents: ContentEntry[], relayDir: string, projectPath: string): {
30
+ diff: ContentDiffEntry[];
31
+ newItems: NewItemEntry[];
32
+ };
33
+ /**
34
+ * contents 항목 단위로 from → .relay/ 동기화한다.
35
+ */
36
+ export declare function syncContentsToRelay(contents: ContentEntry[], contentsDiff: ContentDiffEntry[], relayDir: string, projectPath: string): void;
8
37
  /**
9
38
  * 패키지 홈 디렉토리를 결정한다.
10
39
  * 1. 프로젝트에 .relay/가 있으면 → projectPath/.relay/
@@ -18,3 +47,4 @@ export declare function resolveRelayDir(projectPath: string, slug?: string): str
18
47
  */
19
48
  export declare function initGlobalAgentHome(slug: string, yamlData: Record<string, unknown>): string;
20
49
  export declare function registerPackage(program: Command): void;
50
+ export {};
@@ -3,6 +3,8 @@ 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.computeContentsDiff = computeContentsDiff;
7
+ exports.syncContentsToRelay = syncContentsToRelay;
6
8
  exports.resolveRelayDir = resolveRelayDir;
7
9
  exports.initGlobalAgentHome = initGlobalAgentHome;
8
10
  exports.registerPackage = registerPackage;
@@ -291,6 +291,108 @@ function createMcpServer() {
291
291
  }
292
292
  return { content: [jsonText({ sources })] };
293
293
  });
294
+ server.tool('relay_package', '소스 디렉토리에서 .relay/로 콘텐츠를 패키징합니다. mode: init(최초 소스 탐색), sync(변경 반영), migrate(source→contents 마이그레이션)', {
295
+ mode: zod_1.z.enum(['init', 'sync', 'migrate']).describe('패키징 모드'),
296
+ project_path: zod_1.z.string().optional().describe('프로젝트 경로'),
297
+ }, async ({ mode, project_path }) => {
298
+ try {
299
+ const projectPath = resolveMcpProjectPath(project_path);
300
+ const homeDir = (0, paths_js_1.resolveHome)();
301
+ const relayDir = path_1.default.join(projectPath, '.relay');
302
+ const relayYamlPath = path_1.default.join(relayDir, 'relay.yaml');
303
+ if (mode === 'init') {
304
+ // 최초 패키징: 소스 탐색
305
+ const { detectGlobalCLIs } = await import('../lib/ai-tools.js');
306
+ const localTools = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
307
+ const globalTools = detectGlobalCLIs(homeDir);
308
+ const sources = [];
309
+ for (const tool of localTools) {
310
+ const items = (0, ai_tools_js_1.scanLocalItems)(projectPath, tool);
311
+ if (items.length > 0)
312
+ sources.push({ path: tool.skillsDir, location: 'local', name: tool.name, items: items.map((i) => ({ name: i.name, type: i.type, relativePath: i.relativePath })) });
313
+ }
314
+ for (const tool of globalTools) {
315
+ const items = (0, ai_tools_js_1.scanGlobalItems)(tool, homeDir);
316
+ if (items.length > 0)
317
+ sources.push({ path: `~/${tool.skillsDir}`, location: 'global', name: `${tool.name} (global)`, items: items.map((i) => ({ name: i.name, type: i.type, relativePath: i.relativePath })) });
318
+ }
319
+ for (const { tool, basePath } of (0, ai_tools_js_1.detectMountedCLIs)()) {
320
+ const items = (0, ai_tools_js_1.scanMountedItems)(basePath, tool);
321
+ if (items.length > 0)
322
+ sources.push({ path: `${basePath}/${tool.skillsDir}`, location: 'mounted', name: `${tool.name} (mounted)`, items: items.map((i) => ({ name: i.name, type: i.type, relativePath: i.relativePath })) });
323
+ }
324
+ // 기존 글로벌 에이전트 패키지 스캔
325
+ const globalAgentsDir = path_1.default.join(homeDir, '.relay', 'agents');
326
+ const existingAgents = [];
327
+ if (fs_1.default.existsSync(globalAgentsDir)) {
328
+ for (const entry of fs_1.default.readdirSync(globalAgentsDir, { withFileTypes: true })) {
329
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
330
+ continue;
331
+ const agentYaml = path_1.default.join(globalAgentsDir, entry.name, 'relay.yaml');
332
+ if (fs_1.default.existsSync(agentYaml)) {
333
+ try {
334
+ const cfg = js_yaml_1.default.load(fs_1.default.readFileSync(agentYaml, 'utf-8'));
335
+ existingAgents.push({ slug: cfg.slug ?? entry.name, name: cfg.name ?? entry.name, version: cfg.version ?? '0.0.0', path: `~/.relay/agents/${entry.name}` });
336
+ }
337
+ catch { /* skip */ }
338
+ }
339
+ }
340
+ }
341
+ return { content: [jsonText({ status: 'init_required', sources, existing_agents: existingAgents })] };
342
+ }
343
+ // sync / migrate는 relay.yaml이 필요
344
+ if (!fs_1.default.existsSync(relayYamlPath)) {
345
+ return { content: [jsonText({ error: 'NOT_INITIALIZED', message: '.relay/relay.yaml이 없습니다. mode: init으로 먼저 실행하세요.' })], isError: true };
346
+ }
347
+ if (mode === 'migrate') {
348
+ const yamlMigrate = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
349
+ const cfgMigrate = js_yaml_1.default.load(yamlMigrate);
350
+ if (cfgMigrate.contents) {
351
+ return { content: [jsonText({ status: 'already_migrated', message: '이미 contents 형식입니다.' })] };
352
+ }
353
+ const legacySource = cfgMigrate.source;
354
+ if (!legacySource) {
355
+ return { content: [jsonText({ status: 'no_source', message: 'source 필드가 없습니다.' })], isError: true };
356
+ }
357
+ const localTools = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
358
+ const tool = localTools.find((t) => t.skillsDir === legacySource);
359
+ const migratedContents = [];
360
+ if (tool) {
361
+ const items = (0, ai_tools_js_1.scanLocalItems)(projectPath, tool);
362
+ for (const item of items) {
363
+ migratedContents.push({ name: item.name, type: item.type, from: `${legacySource}/${item.relativePath}` });
364
+ }
365
+ }
366
+ delete cfgMigrate.source;
367
+ cfgMigrate.contents = migratedContents;
368
+ fs_1.default.writeFileSync(relayYamlPath, js_yaml_1.default.dump(cfgMigrate, { lineWidth: 120 }), 'utf-8');
369
+ return { content: [jsonText({ status: 'migrated', contents: migratedContents })] };
370
+ }
371
+ // mode === 'sync'
372
+ const { computeContentsDiff, syncContentsToRelay } = await import('../commands/package.js');
373
+ const yamlContent = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
374
+ const config = js_yaml_1.default.load(yamlContent);
375
+ const contents = config.contents ?? [];
376
+ if (contents.length === 0) {
377
+ return { content: [jsonText({ status: 'no_contents', message: 'relay.yaml에 contents가 없습니다.' })], isError: true };
378
+ }
379
+ const { diff: contentsDiff, newItems } = computeContentsDiff(contents, relayDir, projectPath);
380
+ const hasChanges = contentsDiff.some((d) => d.status === 'modified');
381
+ if (hasChanges) {
382
+ syncContentsToRelay(contents, contentsDiff, relayDir, projectPath);
383
+ }
384
+ const summary = {
385
+ modified: contentsDiff.filter((d) => d.status === 'modified').length,
386
+ unchanged: contentsDiff.filter((d) => d.status === 'unchanged').length,
387
+ source_missing: contentsDiff.filter((d) => d.status === 'source_missing').length,
388
+ new_available: newItems.length,
389
+ };
390
+ return { content: [jsonText({ diff: contentsDiff, new_items: newItems, synced: hasChanges, summary })] };
391
+ }
392
+ catch (err) {
393
+ return { content: [jsonText({ error: String(err) })], isError: true };
394
+ }
395
+ });
294
396
  server.tool('relay_org_list', '소속 Organization 목록을 조회합니다', {}, async () => {
295
397
  try {
296
398
  const token = await (0, config_js_1.getValidToken)();
@@ -59,6 +59,9 @@ cat ~/Library/Application\ Support/Claude/claude_desktop_config.json 2>/dev/null
59
59
  | "relay install X" | `relay install X` | `relay_install` tool 호출 (`slug: "X"`) |
60
60
  | "relay publish" | `relay publish` | `relay_publish` tool 호출 |
61
61
  | "relay login" | `relay login` | `relay_login` tool 호출 |
62
+ | "relay package --init" | `relay package --init --json` | `relay_package` tool 호출 (`mode: "init"`) |
63
+ | "relay package --sync" | `relay package --sync --json` | `relay_package` tool 호출 (`mode: "sync"`) |
64
+ | "relay package --migrate" | `relay package --migrate --json` | `relay_package` tool 호출 (`mode: "migrate"`) |
62
65
  | "relay scan" | `relay package --init` | `relay_scan` tool 호출 |
63
66
  | "relay check-update X" | `relay check-update X` | `relay_check_update` tool 호출 (`slug: "X"`) |
64
67
  | "relay orgs list" | `relay orgs list --json` | `relay_org_list` tool 호출 |
@@ -28,6 +28,13 @@
28
28
 
29
29
  #### A. 최초 배포 (.relay/relay.yaml이 없음)
30
30
 
31
+ **환경 B (MCP)의 경우:**
32
+ `relay_package` MCP tool (`mode: "init"`)을 사용합니다. CLI의 `relay package --init --json`과 동일한 결과를 반환합니다.
33
+ - 결과의 `sources[]`는 배포할 콘텐츠 **후보** 목록입니다. 전부 배포 대상이 아닙니다.
34
+ - 사용자에게 어떤 콘텐츠를 패키지에 포함할지 반드시 물어보세요.
35
+
36
+ **환경 A (터미널)의 경우:**
37
+
31
38
  ##### 1단계: 소스 탐색
32
39
 
33
40
  `relay package --init --json` 실행
@@ -172,6 +179,14 @@ contents:
172
179
 
173
180
  #### B. 재배포 (.relay/relay.yaml이 있음)
174
181
 
182
+ **환경 B (MCP)의 경우:**
183
+ `relay_package` MCP tool을 사용합니다:
184
+ - `mode: "migrate"` — B-0 마이그레이션 (source → contents)
185
+ - `mode: "sync"` — B-1~B-2 동기화 + 변경 반영
186
+ - sync 결과의 `new_items`가 있으면 B-3 새 콘텐츠 추가도 진행합니다.
187
+
188
+ **환경 A (터미널)의 경우:**
189
+
175
190
  ##### B-0. 기존 source 필드 마이그레이션
176
191
 
177
192
  relay.yaml에 기존 `source` 필드만 있고 `contents`가 없으면:
@@ -300,18 +315,19 @@ relay.yaml의 `visibility` 설정을 확인합니다.
300
315
 
301
316
  Org가 선택된 경우:
302
317
  - **사용자 질문 도구 호출:**
303
- - question: "공개 범위를 선택하세요"
304
- - options: `["공개 — 누구나 설치", "링크 공유 접근 링크가 있는 사람만 설치", "비공개Org 멤버만"]`
318
+ - question: "{org_name} Organization에 배포합니다. 공개 범위를 선택하세요"
319
+ - options: `["public외부인 포함 누구나 설치", "internal조직 구성원 누구나 설치", "private조직 내에서도 허가받은 사람만 설치"]`
305
320
 
306
- Org가 없는 경우:
321
+ Org가 없는 경우 (개인 배포):
307
322
  - **사용자 질문 도구 호출:**
308
323
  - question: "공개 범위를 선택하세요"
309
- - options: `["공개 — 누구나 설치", "링크 공유 — 접근 링크가 있는 사람만 설치"]`
324
+ - options: `["public — 누구나 검색하여 설치 가능", "private — 접근 링크를 받은 사람만 설치 가능"]`
310
325
 
311
326
  **응답 처리:**
312
- - "공개" → relay.yaml에 `visibility: public` 저장
313
- - "링크 공유" → relay.yaml에 `visibility: private` 저장. 배포 후 웹 대시보드(/dashboard)에서 접근 링크를 생성하고 구매 안내를 설정할 수 있다고 안내.
314
- - "비공개" → relay.yaml에 `visibility: internal`, `org: <selected_slug>` 저장
327
+
328
+ - "public" → relay.yaml에 `visibility: public` 저장
329
+ - "internal" → relay.yaml에 `visibility: internal` 저장
330
+ - "private" → relay.yaml에 `visibility: private` 저장. 배포 후 웹 대시보드에서 접근 코드를 생성하여 공유할 수 있다고 안내.
315
331
 
316
332
  **재배포 (visibility 이미 설정됨):**
317
333
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relayax-cli",
3
- "version": "0.3.58",
3
+ "version": "0.3.59",
4
4
  "description": "RelayAX Agent Team Marketplace CLI - Install and manage agent teams",
5
5
  "main": "dist/index.js",
6
6
  "bin": {