relayax-cli 0.2.22 → 0.2.24

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,6 +1,11 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.registerUninstall = registerUninstall;
7
+ const os_1 = __importDefault(require("os"));
8
+ const path_1 = __importDefault(require("path"));
4
9
  const config_js_1 = require("../lib/config.js");
5
10
  const installer_js_1 = require("../lib/installer.js");
6
11
  const slug_js_1 = require("../lib/slug.js");
@@ -10,17 +15,24 @@ function registerUninstall(program) {
10
15
  .description('에이전트 팀 제거')
11
16
  .action((slugInput) => {
12
17
  const json = program.opts().json ?? false;
13
- const installed = (0, config_js_1.loadInstalled)();
14
- // Resolve slug from installed.json
18
+ const localInstalled = (0, config_js_1.loadInstalled)();
19
+ const globalInstalled = (0, config_js_1.loadGlobalInstalled)();
20
+ // Resolve slug — support short names like "cardnews-team"
15
21
  let slug;
16
22
  if ((0, slug_js_1.isScopedSlug)(slugInput)) {
17
23
  slug = slugInput;
18
24
  }
19
25
  else {
20
- const found = (0, slug_js_1.findInstalledByName)(installed, slugInput);
21
- slug = found ?? slugInput;
26
+ const allKeys = [...Object.keys(localInstalled), ...Object.keys(globalInstalled)];
27
+ const match = allKeys.find((key) => {
28
+ const parsed = (0, slug_js_1.parseSlug)(key);
29
+ return parsed && parsed.name === slugInput;
30
+ });
31
+ slug = match ?? slugInput;
22
32
  }
23
- if (!installed[slug]) {
33
+ const localEntry = localInstalled[slug];
34
+ const globalEntry = globalInstalled[slug];
35
+ if (!localEntry && !globalEntry) {
24
36
  const msg = { error: 'NOT_INSTALLED', message: `'${slugInput}'는 설치되어 있지 않습니다.` };
25
37
  if (json) {
26
38
  console.error(JSON.stringify(msg));
@@ -30,21 +42,55 @@ function registerUninstall(program) {
30
42
  }
31
43
  process.exit(1);
32
44
  }
33
- const { files } = installed[slug];
34
- const removed = (0, installer_js_1.uninstallTeam)(files);
35
- delete installed[slug];
36
- (0, config_js_1.saveInstalled)(installed);
45
+ let totalRemoved = 0;
46
+ // Remove from local registry
47
+ if (localEntry) {
48
+ const removed = (0, installer_js_1.uninstallTeam)(localEntry.files);
49
+ totalRemoved += removed.length;
50
+ // Remove deployed files
51
+ if (localEntry.deployed_files && localEntry.deployed_files.length > 0) {
52
+ const deployedRemoved = (0, installer_js_1.uninstallTeam)(localEntry.deployed_files);
53
+ totalRemoved += deployedRemoved.length;
54
+ // Clean empty parent directories
55
+ const boundary = path_1.default.join(process.cwd(), '.claude');
56
+ for (const f of deployedRemoved) {
57
+ (0, installer_js_1.cleanEmptyParents)(f, boundary);
58
+ }
59
+ }
60
+ delete localInstalled[slug];
61
+ (0, config_js_1.saveInstalled)(localInstalled);
62
+ }
63
+ // Remove from global registry
64
+ if (globalEntry) {
65
+ // Only remove files if not already handled by local entry
66
+ if (!localEntry) {
67
+ const removed = (0, installer_js_1.uninstallTeam)(globalEntry.files);
68
+ totalRemoved += removed.length;
69
+ }
70
+ // Remove globally deployed files
71
+ if (globalEntry.deployed_files && globalEntry.deployed_files.length > 0) {
72
+ const deployedRemoved = (0, installer_js_1.uninstallTeam)(globalEntry.deployed_files);
73
+ totalRemoved += deployedRemoved.length;
74
+ // Clean empty parent directories
75
+ const boundary = path_1.default.join(os_1.default.homedir(), '.claude');
76
+ for (const f of deployedRemoved) {
77
+ (0, installer_js_1.cleanEmptyParents)(f, boundary);
78
+ }
79
+ }
80
+ delete globalInstalled[slug];
81
+ (0, config_js_1.saveGlobalInstalled)(globalInstalled);
82
+ }
37
83
  const result = {
38
84
  status: 'ok',
39
85
  team: slug,
40
- files_removed: removed.length,
86
+ files_removed: totalRemoved,
41
87
  };
42
88
  if (json) {
43
89
  console.log(JSON.stringify(result));
44
90
  }
45
91
  else {
46
92
  console.log(`\n\x1b[32m✓ ${slug} 제거 완료\x1b[0m`);
47
- console.log(` 삭제된 파일: ${removed.length}개`);
93
+ console.log(` 삭제된 파일: ${totalRemoved}개`);
48
94
  }
49
95
  });
50
96
  }
@@ -26,14 +26,8 @@ function registerUpdate(program) {
26
26
  slug = slugInput;
27
27
  }
28
28
  else {
29
- const found = (0, slug_js_1.findInstalledByName)(installed, slugInput);
30
- if (found) {
31
- slug = found;
32
- }
33
- else {
34
- const parsed = await (0, slug_js_1.resolveSlug)(slugInput);
35
- slug = parsed.full;
36
- }
29
+ const parsed = await (0, slug_js_1.resolveSlug)(slugInput);
30
+ slug = parsed.full;
37
31
  }
38
32
  // Check installed.json for current version
39
33
  const currentEntry = installed[slug];
@@ -68,11 +62,17 @@ function registerUpdate(program) {
68
62
  (0, preamble_js_1.injectPreambleToTeam)(extractDir, slug);
69
63
  // Copy files to install_path
70
64
  const files = (0, installer_js_1.installTeam)(extractDir, installPath);
65
+ // Preserve deploy info but clear deployed_files (agent needs to re-deploy)
66
+ const previousDeployScope = currentEntry?.deploy_scope;
67
+ const hadDeployedFiles = (currentEntry?.deployed_files?.length ?? 0) > 0;
71
68
  // Update installed.json with new version
72
69
  installed[slug] = {
73
70
  version: latestVersion,
74
71
  installed_at: new Date().toISOString(),
75
72
  files,
73
+ // Keep deploy_scope so agent knows where to re-deploy
74
+ ...(previousDeployScope ? { deploy_scope: previousDeployScope } : {}),
75
+ // Clear deployed_files — agent must re-deploy and call deploy-record
76
76
  };
77
77
  (0, config_js_1.saveInstalled)(installed);
78
78
  // Report install (non-blocking)
@@ -84,6 +84,7 @@ function registerUpdate(program) {
84
84
  version: latestVersion,
85
85
  files_installed: files.length,
86
86
  install_path: installPath,
87
+ ...(hadDeployedFiles ? { needs_redeploy: true, previous_deploy_scope: previousDeployScope } : {}),
87
88
  };
88
89
  if (json) {
89
90
  console.log(JSON.stringify(result));
package/dist/index.js CHANGED
@@ -17,6 +17,8 @@ const check_update_js_1 = require("./commands/check-update.js");
17
17
  const follow_js_1 = require("./commands/follow.js");
18
18
  const changelog_js_1 = require("./commands/changelog.js");
19
19
  const join_js_1 = require("./commands/join.js");
20
+ const spaces_js_1 = require("./commands/spaces.js");
21
+ const deploy_record_js_1 = require("./commands/deploy-record.js");
20
22
  // eslint-disable-next-line @typescript-eslint/no-var-requires
21
23
  const pkg = require('../package.json');
22
24
  const program = new commander_1.Command();
@@ -40,4 +42,6 @@ program
40
42
  (0, follow_js_1.registerFollow)(program);
41
43
  (0, changelog_js_1.registerChangelog)(program);
42
44
  (0, join_js_1.registerJoin)(program);
45
+ (0, spaces_js_1.registerSpaces)(program);
46
+ (0, deploy_record_js_1.registerDeployRecord)(program);
43
47
  program.parse();
@@ -29,5 +29,3 @@ export declare function getGlobalCommandDir(): string;
29
29
  export declare function formatCommandFile(content: CommandContent): string;
30
30
  export declare const USER_COMMANDS: CommandContent[];
31
31
  export declare const BUILDER_COMMANDS: CommandContent[];
32
- /** 하위 호환 — 기존 코드에서 RELAY_COMMANDS를 참조하는 경우 */
33
- export declare const RELAY_COMMANDS: CommandContent[];
@@ -3,7 +3,7 @@ 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.RELAY_COMMANDS = exports.BUILDER_COMMANDS = exports.USER_COMMANDS = void 0;
6
+ exports.BUILDER_COMMANDS = exports.USER_COMMANDS = void 0;
7
7
  exports.createAdapter = createAdapter;
8
8
  exports.getGlobalCommandPath = getGlobalCommandPath;
9
9
  exports.getGlobalCommandDir = getGlobalCommandDir;
@@ -20,9 +20,7 @@ function createAdapter(tool) {
20
20
  getFilePath(commandId) {
21
21
  return path_1.default.join(tool.skillsDir, 'commands', 'relay', `${commandId}.md`);
22
22
  },
23
- formatFile(content) {
24
- return `---\ndescription: ${content.description}\n---\n\n${content.body}\n`;
25
- },
23
+ formatFile: formatCommandFile,
26
24
  };
27
25
  }
28
26
  /**
@@ -101,44 +99,46 @@ exports.USER_COMMANDS = [
101
99
 
102
100
  ## 실행 방법
103
101
 
104
- ### 0. 업데이트 확인
105
- - 먼저 \`relay check-update\` 명령어를 실행합니다.
106
- - CLI 업데이트가 있으면 사용자에게 안내합니다: "relay v{new} available. Run: npm update -g relayax-cli"
107
- - 업데이트가 있으면 안내합니다.
108
- - 업데이트 여부와 관계없이 설치를 계속 진행합니다.
109
-
110
- ### 1. 팀 패키지 다운로드
111
- - Public 마켓 팀: \`relay install <@author/slug>\` 명령어를 실행합니다.
112
- - Space 팀: \`relay install @spaces/<space-slug>/<team-slug>\` 명령어를 실행합니다.
102
+ ### 1. 패키지 다운로드
103
+ \`relay install <@author/slug> --json\` 명령어를 실행합니다.
104
+ - Public 마켓 팀: \`relay install <@author/slug> --json\`
105
+ - Space 팀: \`relay install @spaces/<space-slug>/<team-slug> --json\`
113
106
  - Space 가입이 필요하면: \`relay join <space-slug> --code <invite-code>\` 를 먼저 실행합니다.
114
- - 또는 \`relay install @spaces/<space-slug>/<team-slug> --join-code <code>\` 가입+설치를 한번에 할 수 있습니다.
115
- - 패키지가 \`.relay/teams/<slug>/\`에 다운로드됩니다.
116
-
117
- ### 2. 패키지 내용 확인
118
- 다운로드된 \`.relay/teams/<slug>/\` 디렉토리를 읽어 구조를 파악합니다:
119
- - skills/ 스킬 파일들
120
- - commands/ — 슬래시 커맨드 파일들
121
- - agents/ 에이전트 설정 파일들
122
- - rules/ 파일들
123
- - relay.yaml — 팀 메타데이터 및 requirements
124
-
125
- ### 3. 기존 파일 충돌 확인
126
- 배치 대상 디렉토리(\`.claude/commands/\`, \`.claude/skills/\` 등)에 **같은 이름의 파일이 이미 존재하는지** 확인합니다.
127
- - 충돌하는 파일이 있으면 사용자에게 반드시 물어봅니다:
128
- - "다음 파일이 이미 존재합니다: {파일 목록}. 덮어쓸까요, 건너뛸까요?"
129
- - 사용자가 선택할 때까지 진행하지 않습니다.
130
- - 충돌이 없으면 그대로 진행합니다.
131
- - **주의**: 팀에 포함되지 않은 기존 파일은 절대 삭제하지 않습니다.
132
-
133
- ### 4. 에이전트 환경에 맞게 배치
134
- 현재 에이전트의 디렉토리 구조에 맞게 파일을 복사합니다:
135
- - Claude Code: \`.relay/teams/<slug>/commands/\` → \`.claude/commands/\`에 복사
136
- - Claude Code: \`.relay/teams/<slug>/skills/\` → \`.claude/skills/\`에 복사
137
- - 다른 에이전트(Cursor, Cline 등): 해당 에이전트의 규칙에 맞는 디렉토리에 복사
138
- - 에이전트 설정이나 룰은 적절한 위치에 배치
107
+ - 또는 \`--join-code <code>\`로 가입+설치를 한번에 할 수 있습니다.
108
+ - CLI가 init과 login을 자동으로 처리합니다 (사용자가 별도 실행할 필요 없음).
109
+ - JSON 출력에서 \`install_path\` (패키지 경로)를 확인합니다.
110
+
111
+ ### 2. 배치 범위 선택
112
+ 사용자에게 설치 범위를 물어봅니다:
113
+
114
+ - **글로벌** (\`~/.claude/\`): 모든 프로젝트에서 사용 가능
115
+ - **로컬** (현재 프로젝트 \`.claude/\`): 이 프로젝트에서만 사용
116
+
117
+ 판단 기준:
118
+ - 범용 도구 (카드뉴스, PDF 생성 등): 글로벌 추천
119
+ - 프로젝트 전용 팀: 로컬 추천
120
+ - Space 비공개 팀: 로컬 추천
121
+
122
+ 사용자가 별도 지정하지 않으면 글로벌로 진행합니다.
123
+
124
+ ### 3. 에이전트 환경에 맞게 배치
125
+ 다운로드된 패키지(\`install_path\`)에서 파일을 읽고 선택된 범위에 배치합니다:
126
+ - Claude Code 글로벌: \`<install_path>/commands/\` → \`~/.claude/commands/\`에 복사
127
+ - Claude Code 글로벌: \`<install_path>/skills/\` \`~/.claude/skills/\`에 복사
128
+ - Claude Code 로컬: \`<install_path>/commands/\` → \`.claude/commands/\`에 복사
129
+ - Claude Code 로컬: \`<install_path>/skills/\` → \`.claude/skills/\`에 복사
130
+ - agents/, rules/ 파일도 같은 방식으로 배치합니다.
131
+ - **충돌 확인**: 같은 이름의 파일이 이미 있으면 사용자에게 덮어쓸지 물어봅니다.
132
+
133
+ ### 4. 배치 정보 기록 (필수)
134
+ 배치 완료 후 반드시 \`relay deploy-record\`를 실행하여 배치 정보를 기록합니다:
135
+ \`\`\`
136
+ relay deploy-record <slug> --scope <global|local> --files <배치된_파일1> <배치된_파일2> ...
137
+ \`\`\`
138
+ 이 정보는 \`relay uninstall\` 시 배치된 파일까지 정리하는 데 사용됩니다.
139
139
 
140
140
  ### 5. Requirements 확인 및 설치
141
- \`.relay/teams/<slug>/relay.yaml\`의 \`requires\` 섹션을 읽고 처리합니다:
141
+ \`<install_path>/relay.yaml\`의 \`requires\` 섹션을 읽고 처리합니다:
142
142
  - **cli**: \`which <name>\`으로 확인 → 없으면 install 명령 실행 또는 안내
143
143
  - **npm**: \`npm list <package>\`로 확인 → 없으면 \`npm install\`
144
144
  - **env**: 환경변수 확인 → required이면 설정 안내, optional이면 알림
@@ -155,21 +155,20 @@ ${BUSINESS_CARD_FORMAT}
155
155
  - 거절하면: 건너뜁니다
156
156
  - "바로 사용해볼까요?" 제안
157
157
 
158
+ ### 7. 업데이트 확인 (설치 완료 후)
159
+ - \`relay check-update\` 명령어를 실행합니다.
160
+ - CLI 업데이트가 있으면 안내합니다: "relay v{new} available. Run: npm update -g relayax-cli"
161
+ - 다른 팀 업데이트가 있으면 안내합니다.
162
+
158
163
  ## 예시
159
164
 
160
165
  사용자: /relay-install @example/contents-team
161
- → relay install @example/contents-team 실행 (패키지 다운로드)
162
- .relay/teams/@example/contents-team/ 내용 확인
163
- commands/cardnews.md .claude/commands/cardnews.md 복사
164
- skills/pdf-gen.md .claude/skills/pdf-gen.md 복사
166
+ → relay install @example/contents-team --json 실행 (패키지 다운로드)
167
+ 사용자에게 "글로벌 vs 로컬" 선택 질문 → 글로벌
168
+ .relay/teams/ 내용을 ~/.claude/에 배치
169
+ relay deploy-record @example/contents-team --scope global --files ~/.claude/commands/cardnews.md ...
165
170
  → requires 확인: ✓ playwright 설치됨, ✓ sharp 설치됨
166
- → "✓ 설치 완료! /cardnews를 사용해볼까요?"
167
-
168
- 사용자: /relay-install @spaces/bobusan/pm-bot
169
- → relay install @spaces/bobusan/pm-bot 실행
170
- → Space 멤버 확인 → 정상
171
- → 패키지 다운로드 및 배치
172
- → "✓ 설치 완료!"`,
171
+ → "✓ 설치 완료! /cardnews를 사용해볼까요?"`,
173
172
  },
174
173
  {
175
174
  id: 'relay-list',
@@ -218,6 +217,9 @@ ${BUSINESS_CARD_FORMAT}
218
217
  ### 특정 팀 업데이트
219
218
  - 사용자가 팀 이름을 지정한 경우: \`relay update <@author/slug> --json\` 실행
220
219
  - 업데이트 결과를 보여줍니다 (이전 버전 → 새 버전)
220
+ - **재배치 필요 확인**: JSON 출력에 \`needs_redeploy: true\`가 있으면:
221
+ 1. \`previous_deploy_scope\`를 참고하여 같은 범위(글로벌/로컬)로 파일을 다시 배치합니다.
222
+ 2. 배치 후 \`relay deploy-record <slug> --scope <scope> --files <...>\`를 실행하여 기록합니다.
221
223
  ${BUSINESS_CARD_FORMAT}
222
224
 
223
225
  ### 전체 업데이트 확인
@@ -235,18 +237,50 @@ ${BUSINESS_CARD_FORMAT}
235
237
  → " @example/contents-team: v1.2.0 → v1.3.0"
236
238
  → "업데이트할까요?"
237
239
  → relay update @example/contents-team --json 실행
240
+ → needs_redeploy: true → 글로벌로 재배치
241
+ → relay deploy-record @example/contents-team --scope global --files ...
238
242
  → "✓ @example/contents-team v1.3.0으로 업데이트 완료"`,
243
+ },
244
+ {
245
+ id: 'relay-spaces',
246
+ description: '내 Space 목록을 확인합니다',
247
+ body: `사용자의 Space 목록을 조회하고 보여줍니다.
248
+
249
+ ## 실행 방법
250
+
251
+ 1. \`relay spaces --json\` 명령어를 실행합니다.
252
+ 2. 결과를 사용자에게 보기 좋게 정리합니다:
253
+ - 개인 스페이스
254
+ - 팀 스페이스 (이름, 역할, 설명)
255
+ 3. Space가 있으면 관련 활용법을 안내합니다:
256
+ - 팀 목록 보기: \`relay list --space <slug>\`
257
+ - 비공개 팀 설치: \`relay install @spaces/<slug>/<team>\`
258
+ - Space 관리: www.relayax.com/spaces/<slug>
259
+ ${LOGIN_JIT_GUIDE}
260
+
261
+ ## 예시
262
+
263
+ 사용자: /relay-spaces
264
+ → relay spaces --json 실행
265
+ → "2개 Space가 있어요:"
266
+ → " bobusan — 보부산 (소유자)"
267
+ → " design-lab — 디자인 랩 (멤버)"
268
+ → "💡 Space 팀 보기: relay list --space bobusan"`,
239
269
  },
240
270
  {
241
271
  id: 'relay-uninstall',
242
272
  description: '설치된 에이전트 팀을 삭제합니다',
243
- body: `설치된 에이전트 팀을 프로젝트에서 제거합니다.
273
+ body: `설치된 에이전트 팀을 제거합니다. CLI가 패키지와 배치된 파일을 모두 정리합니다.
244
274
 
245
275
  ## 실행 방법
246
276
 
247
277
  1. \`relay uninstall <@author/slug> --json\` 명령어를 실행합니다.
248
- 2. 삭제 결과를 보여줍니다 (팀 이름, 제거된 파일 수).
249
- 3. 삭제 완료 후 남아있는 팀 목록을 간단히 안내합니다.
278
+ 2. CLI가 자동으로 처리하는 것:
279
+ - \`.relay/teams/\` 패키지 삭제
280
+ - \`deployed_files\`에 기록된 배치 파일 삭제 (\`~/.claude/\` 또는 \`.claude/\`)
281
+ - 빈 상위 디렉토리 정리
282
+ - installed.json에서 항목 제거 (글로벌/로컬 양쪽)
283
+ 3. 삭제 결과를 보여줍니다 (팀 이름, 제거된 파일 수).
250
284
 
251
285
  ## 예시
252
286
 
@@ -265,9 +299,9 @@ exports.BUILDER_COMMANDS = [
265
299
  ## 실행 단계
266
300
 
267
301
  ### 1. 인증 확인 (가장 먼저)
268
- - \`cat ~/.relay/token\` 또는 환경변수 RELAY_TOKEN으로 토큰 존재 여부를 확인합니다.
269
- - 미인증이면 즉시 안내: "먼저 \`relay login\`으로 로그인이 필요합니다." → 로그인 후 재실행 안내.
270
- - 인증되어 있으면 다음 단계로 진행합니다.
302
+ - \`relay status --json\` 명령어를 실행하여 로그인 상태를 확인합니다.
303
+ - 미인증이면 즉시 \`relay login\`을 실행합니다.
304
+ - 로그인 완료 다음 단계로 진행합니다.
271
305
  ${LOGIN_JIT_GUIDE}
272
306
 
273
307
  ### 2. 팀 구조 분석
@@ -363,8 +397,9 @@ requires:
363
397
  - 각 스킬의 SKILL.md, 에이전트 설정, 커맨드 문서를 분석하여 팀의 파이프라인 흐름을 추론합니다.
364
398
 
365
399
  #### 5-2. GUIDE.html 생성
366
- - \`cli/src/lib/guide.ts\`의 \`GUIDE_HTML_PROMPT\`를 읽고, 해당 프롬프트의 콘텐츠 구조와 디자인 규칙을 따라 GUIDE.html을 생성합니다.
367
- - 5-1에서 분석한 소스 정보를 프롬프트에 반영합니다.
400
+ - 팀의 핵심 기능, 시작 방법, 파이프라인 흐름, Q&A를 포함하는 단일 HTML 가이드를 생성합니다.
401
+ - 디자인: 깔끔한 단일 페이지, 시스템 폰트, 최대 1200px 너비, 라이트 테마.
402
+ - 5-1에서 분석한 팀 소스 정보를 기반으로 콘텐츠를 구성합니다.
368
403
  - 파이프라인이 없는 단순한 팀은 시작 방법 + 기능 설명 + Q&A만 포함합니다.
369
404
 
370
405
  #### 5-3. 미리보기 + 컨펌
@@ -391,7 +426,17 @@ requires:
391
426
  - tags: 팀 특성에 맞는 태그를 추천합니다.
392
427
  - 사용자에게 확인: "이대로 배포할까요?"
393
428
 
394
- ### 7. .relay/relay.yaml 업데이트
429
+ ### 7. 공개 범위 확인 (필수)
430
+ - .relay/relay.yaml에 \`visibility\`가 반드시 설정되어 있어야 합니다.
431
+ - 설정되어 있지 않으면 빌더에게 반드시 물어봅니다:
432
+ - "공개 (마켓플레이스에 누구나 검색·설치 가능)" vs "비공개 (Space 멤버만 접근)"
433
+ - 선택 결과를 relay.yaml에 저장합니다.
434
+ - 이미 설정되어 있으면 현재 값을 확인합니다:
435
+ - 공개인 경우: "⚠ 이 팀은 **공개**로 설정되어 있어 마켓플레이스에 노출됩니다. 맞나요?"
436
+ - 비공개인 경우: "이 팀은 **비공개**로 설정되어 Space 멤버만 접근 가능합니다."
437
+ - 빌더가 변경을 원하면 relay.yaml을 업데이트합니다.
438
+
439
+ ### 8. .relay/relay.yaml 업데이트
395
440
  - 메타데이터, requires, 포트폴리오 슬롯을 .relay/relay.yaml에 반영합니다.
396
441
 
397
442
  \`\`\`yaml
@@ -406,9 +451,23 @@ portfolio:
406
451
  title: "카드뉴스 예시"
407
452
  \`\`\`
408
453
 
409
- ### 8. 배포
454
+ ### 9. 배포
410
455
  - \`relay publish\` 명령어를 실행합니다.
411
456
  - 배포 결과와 마켓플레이스 URL을 보여줍니다.
457
+
458
+ ### 10. 공유용 온보딩 가이드 제공
459
+ - \`relay publish\` 출력 끝에 코드블록 형태의 온보딩 가이드가 포함됩니다.
460
+ - 이 코드블록을 사용자에게 그대로 보여줍니다.
461
+ - 출력에 코드블록이 없으면 아래 형태로 직접 생성합니다:
462
+
463
+ \\\`\\\`\\\`
464
+ npm install -g relayax-cli
465
+ relay login
466
+ relay install <slug>
467
+ \\\`\\\`\\\`
468
+
469
+ - \`<slug>\`는 배포된 팀의 실제 슬러그로 치환합니다.
470
+ - "이 블록을 팀원에게 공유하면 바로 설치할 수 있습니다"라고 안내합니다.
412
471
  ${BUSINESS_CARD_FORMAT}
413
472
 
414
473
  ## 예시
@@ -422,8 +481,8 @@ ${BUSINESS_CARD_FORMAT}
422
481
  → GUIDE.html 생성 → 브라우저에서 미리보기 → 빌더 컨펌
423
482
  → GUIDE.html 스크린샷 → gallery 첫 번째 이미지로 등록
424
483
  → relay publish 실행
425
- → "배포 완료! URL: https://relayax.com/teams/my-team"`,
484
+ → "배포 완료! URL: https://relayax.com/teams/my-team"
485
+ → 온보딩 가이드 코드블록 표시
486
+ → "이 블록을 팀원에게 공유하면 바로 설치할 수 있습니다"`,
426
487
  },
427
488
  ];
428
- /** 하위 호환 — 기존 코드에서 RELAY_COMMANDS를 참조하는 경우 */
429
- exports.RELAY_COMMANDS = [...exports.USER_COMMANDS, ...exports.BUILDER_COMMANDS];
@@ -28,12 +28,16 @@ export declare function saveToken(token: string): void;
28
28
  * 4. 갱신 실패 시 undefined (재로그인 필요)
29
29
  */
30
30
  export declare function getValidToken(): Promise<string | undefined>;
31
- /** 프로젝트 로컬 installed.json 읽기 (unscoped 키 자동 마이그레이션) */
31
+ /** 프로젝트 로컬 installed.json 읽기 */
32
32
  export declare function loadInstalled(): InstalledRegistry;
33
- /**
34
- * 비동기 마이그레이션: unscoped 키를 서버 resolve하여 scoped 키로 변환.
35
- * install/update 등 비동기 커맨드에서 호출.
36
- */
37
- export declare function migrateInstalled(): Promise<void>;
38
33
  /** 프로젝트 로컬 installed.json 쓰기 */
39
34
  export declare function saveInstalled(registry: InstalledRegistry): void;
35
+ /** 글로벌 installed.json 읽기 (~/.relay/installed.json) */
36
+ export declare function loadGlobalInstalled(): InstalledRegistry;
37
+ /** 글로벌 installed.json 쓰기 (~/.relay/installed.json) */
38
+ export declare function saveGlobalInstalled(registry: InstalledRegistry): void;
39
+ /** 글로벌 + 로컬 레지스트리 병합 뷰 */
40
+ export declare function loadMergedInstalled(): {
41
+ global: InstalledRegistry;
42
+ local: InstalledRegistry;
43
+ };
@@ -13,13 +13,14 @@ exports.saveTokenData = saveTokenData;
13
13
  exports.saveToken = saveToken;
14
14
  exports.getValidToken = getValidToken;
15
15
  exports.loadInstalled = loadInstalled;
16
- exports.migrateInstalled = migrateInstalled;
17
16
  exports.saveInstalled = saveInstalled;
17
+ exports.loadGlobalInstalled = loadGlobalInstalled;
18
+ exports.saveGlobalInstalled = saveGlobalInstalled;
19
+ exports.loadMergedInstalled = loadMergedInstalled;
18
20
  const fs_1 = __importDefault(require("fs"));
19
21
  const path_1 = __importDefault(require("path"));
20
22
  const os_1 = __importDefault(require("os"));
21
23
  const ai_tools_js_1 = require("./ai-tools.js");
22
- const slug_js_1 = require("./slug.js");
23
24
  exports.API_URL = 'https://www.relayax.com';
24
25
  const GLOBAL_RELAY_DIR = path_1.default.join(os_1.default.homedir(), '.relay');
25
26
  /**
@@ -63,7 +64,6 @@ function loadTokenData() {
63
64
  const raw = fs_1.default.readFileSync(tokenFile, 'utf-8').trim();
64
65
  if (!raw)
65
66
  return undefined;
66
- // JSON 형식 (새 포맷)
67
67
  if (raw.startsWith('{')) {
68
68
  return JSON.parse(raw);
69
69
  }
@@ -121,76 +121,45 @@ async function getValidToken() {
121
121
  return undefined;
122
122
  }
123
123
  }
124
- /** 프로젝트 로컬 installed.json 읽기 (unscoped 키 자동 마이그레이션) */
124
+ /** 프로젝트 로컬 installed.json 읽기 */
125
125
  function loadInstalled() {
126
126
  const file = path_1.default.join(process.cwd(), '.relay', 'installed.json');
127
127
  if (!fs_1.default.existsSync(file)) {
128
128
  return {};
129
129
  }
130
130
  try {
131
- const raw = fs_1.default.readFileSync(file, 'utf-8');
132
- const registry = JSON.parse(raw);
133
- return migrateInstalledKeys(registry);
131
+ return JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
134
132
  }
135
133
  catch {
136
134
  return {};
137
135
  }
138
136
  }
139
- /**
140
- * unscoped 키를 감지하여 서버 resolve 없이 가능한 마이그레이션을 수행한다.
141
- * 서버 resolve가 필요한 경우는 마이그레이션 보류 (다음 기회에 재시도).
142
- */
143
- function migrateInstalledKeys(registry) {
144
- const unscopedKeys = Object.keys(registry).filter((k) => !(0, slug_js_1.isScopedSlug)(k) && k !== 'relay-core');
145
- if (unscopedKeys.length === 0)
146
- return registry;
147
- // 비동기 서버 resolve 없이는 owner를 알 수 없으므로,
148
- // loadInstalled는 동기 함수 → 마이그레이션은 비동기 migrateInstalled()로 별도 호출
149
- return registry;
150
- }
151
- /**
152
- * 비동기 마이그레이션: unscoped 키를 서버 resolve하여 scoped 키로 변환.
153
- * install/update 등 비동기 커맨드에서 호출.
154
- */
155
- async function migrateInstalled() {
156
- const { resolveSlugFromServer } = await import('./api.js');
157
- const registry = loadInstalled();
158
- const teamsDir = path_1.default.join(process.cwd(), '.relay', 'teams');
159
- let changed = false;
160
- for (const key of Object.keys(registry)) {
161
- if ((0, slug_js_1.isScopedSlug)(key) || key === 'relay-core')
162
- continue;
163
- try {
164
- const results = await resolveSlugFromServer(key);
165
- if (results.length !== 1)
166
- continue;
167
- const { owner, name } = results[0];
168
- const scopedKey = `@${owner}/${name}`;
169
- // installed.json 키 변환
170
- registry[scopedKey] = registry[key];
171
- delete registry[key];
172
- // 디렉토리 이동
173
- const oldDir = path_1.default.join(teamsDir, key);
174
- const newDir = path_1.default.join(teamsDir, owner, name);
175
- if (fs_1.default.existsSync(oldDir)) {
176
- fs_1.default.mkdirSync(path_1.default.dirname(newDir), { recursive: true });
177
- fs_1.default.renameSync(oldDir, newDir);
178
- // files 배열 업데이트
179
- registry[scopedKey].files = registry[scopedKey].files.map((f) => f.replace(`/teams/${key}`, `/teams/${owner}/${name}`));
180
- }
181
- changed = true;
182
- }
183
- catch {
184
- // 네트워크 오류 등 — 다음 기회에 재시도
185
- }
186
- }
187
- if (changed) {
188
- saveInstalled(registry);
189
- }
190
- }
191
137
  /** 프로젝트 로컬 installed.json 쓰기 */
192
138
  function saveInstalled(registry) {
193
139
  ensureProjectRelayDir();
194
140
  const file = path_1.default.join(process.cwd(), '.relay', 'installed.json');
195
141
  fs_1.default.writeFileSync(file, JSON.stringify(registry, null, 2));
196
142
  }
143
+ // ─── 글로벌 레지스트리 ───
144
+ /** 글로벌 installed.json 읽기 (~/.relay/installed.json) */
145
+ function loadGlobalInstalled() {
146
+ const file = path_1.default.join(GLOBAL_RELAY_DIR, 'installed.json');
147
+ if (!fs_1.default.existsSync(file))
148
+ return {};
149
+ try {
150
+ return JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
151
+ }
152
+ catch {
153
+ return {};
154
+ }
155
+ }
156
+ /** 글로벌 installed.json 쓰기 (~/.relay/installed.json) */
157
+ function saveGlobalInstalled(registry) {
158
+ ensureGlobalRelayDir();
159
+ const file = path_1.default.join(GLOBAL_RELAY_DIR, 'installed.json');
160
+ fs_1.default.writeFileSync(file, JSON.stringify(registry, null, 2));
161
+ }
162
+ /** 글로벌 + 로컬 레지스트리 병합 뷰 */
163
+ function loadMergedInstalled() {
164
+ return { global: loadGlobalInstalled(), local: loadInstalled() };
165
+ }
@@ -1,2 +1,7 @@
1
1
  export declare function installTeam(extractedDir: string, installPath: string): string[];
2
2
  export declare function uninstallTeam(files: string[]): string[];
3
+ /**
4
+ * 빈 상위 디렉토리를 boundary까지 정리한다.
5
+ * 예: /home/.claude/skills/cardnews/ 가 비었으면 삭제, /home/.claude/skills/는 유지
6
+ */
7
+ export declare function cleanEmptyParents(filePath: string, boundary: string): void;
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.installTeam = installTeam;
7
7
  exports.uninstallTeam = uninstallTeam;
8
+ exports.cleanEmptyParents = cleanEmptyParents;
8
9
  const fs_1 = __importDefault(require("fs"));
9
10
  const path_1 = __importDefault(require("path"));
10
11
  const COPY_DIRS = ['skills', 'agents', 'rules', 'commands'];
@@ -39,10 +40,16 @@ function uninstallTeam(files) {
39
40
  const removed = [];
40
41
  for (const file of files) {
41
42
  try {
42
- if (fs_1.default.existsSync(file)) {
43
+ if (!fs_1.default.existsSync(file))
44
+ continue;
45
+ const stat = fs_1.default.statSync(file);
46
+ if (stat.isDirectory()) {
47
+ fs_1.default.rmSync(file, { recursive: true, force: true });
48
+ }
49
+ else {
43
50
  fs_1.default.unlinkSync(file);
44
- removed.push(file);
45
51
  }
52
+ removed.push(file);
46
53
  }
47
54
  catch {
48
55
  // best-effort removal
@@ -50,3 +57,22 @@ function uninstallTeam(files) {
50
57
  }
51
58
  return removed;
52
59
  }
60
+ /**
61
+ * 빈 상위 디렉토리를 boundary까지 정리한다.
62
+ * 예: /home/.claude/skills/cardnews/ 가 비었으면 삭제, /home/.claude/skills/는 유지
63
+ */
64
+ function cleanEmptyParents(filePath, boundary) {
65
+ let dir = path_1.default.dirname(filePath);
66
+ while (dir.length > boundary.length && dir.startsWith(boundary)) {
67
+ try {
68
+ const entries = fs_1.default.readdirSync(dir);
69
+ if (entries.length > 0)
70
+ break;
71
+ fs_1.default.rmdirSync(dir);
72
+ dir = path_1.default.dirname(dir);
73
+ }
74
+ catch {
75
+ break;
76
+ }
77
+ }
78
+ }