relayax-cli 0.2.14 → 0.2.18

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.
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerChangelog(program: Command): void;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerChangelog = registerChangelog;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const js_yaml_1 = __importDefault(require("js-yaml"));
10
+ function registerChangelog(program) {
11
+ const changelog = program
12
+ .command('changelog')
13
+ .description('팀 패키지의 changelog를 관리합니다');
14
+ changelog
15
+ .command('add')
16
+ .description('relay.yaml에 changelog 엔트리를 추가합니다')
17
+ .argument('[message]', 'changelog 메시지 (없으면 에디터에서 입력)')
18
+ .action(async (message) => {
19
+ const yamlPath = path_1.default.resolve('relay.yaml');
20
+ if (!fs_1.default.existsSync(yamlPath)) {
21
+ console.error('relay.yaml을 찾을 수 없습니다. 팀 패키지 디렉토리에서 실행하세요.');
22
+ process.exit(1);
23
+ }
24
+ const content = fs_1.default.readFileSync(yamlPath, 'utf-8');
25
+ const doc = js_yaml_1.default.load(content) ?? {};
26
+ if (!message) {
27
+ // Read from stdin if piped, otherwise prompt
28
+ const readline = await import('readline');
29
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
30
+ message = await new Promise((resolve) => {
31
+ rl.question('Changelog 메시지: ', (answer) => {
32
+ rl.close();
33
+ resolve(answer);
34
+ });
35
+ });
36
+ }
37
+ if (!message || message.trim() === '') {
38
+ console.error('changelog 메시지가 비어있습니다.');
39
+ process.exit(1);
40
+ }
41
+ const version = String(doc.version ?? '1.0.0');
42
+ const date = new Date().toISOString().split('T')[0];
43
+ const entry = `## v${version} (${date})\n\n- ${message.trim()}`;
44
+ const existing = doc.changelog ? String(doc.changelog) : '';
45
+ doc.changelog = existing ? `${entry}\n\n${existing}` : entry;
46
+ fs_1.default.writeFileSync(yamlPath, js_yaml_1.default.dump(doc, { lineWidth: -1, noRefs: true }), 'utf-8');
47
+ console.log(`\x1b[32m✓\x1b[0m changelog 추가됨 (v${version})`);
48
+ console.log(` ${message.trim()}`);
49
+ });
50
+ changelog
51
+ .command('show')
52
+ .description('현재 relay.yaml의 changelog를 표시합니다')
53
+ .action(() => {
54
+ const yamlPath = path_1.default.resolve('relay.yaml');
55
+ if (!fs_1.default.existsSync(yamlPath)) {
56
+ console.error('relay.yaml을 찾을 수 없습니다.');
57
+ process.exit(1);
58
+ }
59
+ const content = fs_1.default.readFileSync(yamlPath, 'utf-8');
60
+ const doc = js_yaml_1.default.load(content) ?? {};
61
+ if (!doc.changelog) {
62
+ console.log('changelog가 없습니다. `relay changelog add "메시지"`로 추가하세요.');
63
+ return;
64
+ }
65
+ console.log(String(doc.changelog));
66
+ });
67
+ }
@@ -11,6 +11,7 @@ const js_yaml_1 = __importDefault(require("js-yaml"));
11
11
  const tar_1 = require("tar");
12
12
  const config_js_1 = require("../lib/config.js");
13
13
  const contact_format_js_1 = require("../lib/contact-format.js");
14
+ const preamble_js_1 = require("../lib/preamble.js");
14
15
  const version_check_js_1 = require("../lib/version-check.js");
15
16
  // eslint-disable-next-line @typescript-eslint/no-var-requires
16
17
  const cliPkg = require('../../package.json');
@@ -104,6 +105,118 @@ function detectCommands(teamDir) {
104
105
  }
105
106
  return entries;
106
107
  }
108
+ function detectSkills(teamDir) {
109
+ const skillsDir = path_1.default.join(teamDir, 'skills');
110
+ if (!fs_1.default.existsSync(skillsDir))
111
+ return [];
112
+ const entries = [];
113
+ for (const entry of fs_1.default.readdirSync(skillsDir, { withFileTypes: true })) {
114
+ if (!entry.isDirectory())
115
+ continue;
116
+ const skillMd = path_1.default.join(skillsDir, entry.name, 'SKILL.md');
117
+ if (!fs_1.default.existsSync(skillMd))
118
+ continue;
119
+ let description = entry.name;
120
+ try {
121
+ const content = fs_1.default.readFileSync(skillMd, 'utf-8');
122
+ const m = content.match(/^---\n[\s\S]*?description:\s*[|>]?\s*\n?\s*(.+)\n[\s\S]*?---/m)
123
+ ?? content.match(/^---\n[\s\S]*?description:\s*(.+)\n[\s\S]*?---/m);
124
+ if (m)
125
+ description = m[1].trim();
126
+ }
127
+ catch {
128
+ // ignore
129
+ }
130
+ entries.push({ name: entry.name, description });
131
+ }
132
+ return entries;
133
+ }
134
+ function generateRootSkillMd(config, commands, skills, scopedSlug) {
135
+ const lines = [];
136
+ // Frontmatter
137
+ lines.push('---');
138
+ lines.push(`name: ${config.name}`);
139
+ lines.push(`description: ${config.description}`);
140
+ lines.push('---');
141
+ lines.push('');
142
+ // Preamble
143
+ lines.push((0, preamble_js_1.generatePreamble)(scopedSlug));
144
+ lines.push('');
145
+ // Skills
146
+ if (skills.length > 0) {
147
+ lines.push('## 포함된 스킬');
148
+ lines.push('');
149
+ for (const s of skills) {
150
+ lines.push(`- **${s.name}**: ${s.description}`);
151
+ }
152
+ lines.push('');
153
+ }
154
+ // Commands
155
+ if (commands.length > 0) {
156
+ lines.push('## 포함된 커맨드');
157
+ lines.push('');
158
+ for (const c of commands) {
159
+ lines.push(`- **/${c.name}**: ${c.description}`);
160
+ }
161
+ lines.push('');
162
+ }
163
+ // Requires
164
+ const req = config.requires;
165
+ if (req) {
166
+ lines.push('## 요구사항');
167
+ lines.push('');
168
+ if (req.env && req.env.length > 0) {
169
+ lines.push('### 환경변수');
170
+ for (const e of req.env) {
171
+ const name = typeof e === 'string' ? e : e.name;
172
+ const desc = typeof e === 'string' ? '' : e.description ?? '';
173
+ const required = typeof e === 'string' ? true : e.required !== false;
174
+ lines.push(`- \`${name}\` ${required ? '(필수)' : '(선택)'}${desc ? ' — ' + desc : ''}`);
175
+ }
176
+ lines.push('');
177
+ }
178
+ if (req.cli && req.cli.length > 0) {
179
+ lines.push('### CLI 도구');
180
+ for (const c of req.cli) {
181
+ const install = c.install ? ` — 설치: \`${c.install}\`` : '';
182
+ lines.push(`- \`${c.name}\`${install}`);
183
+ }
184
+ lines.push('');
185
+ }
186
+ if (req.npm && req.npm.length > 0) {
187
+ lines.push('### npm 패키지');
188
+ for (const n of req.npm) {
189
+ const name = typeof n === 'string' ? n : n.name;
190
+ lines.push(`- \`${name}\``);
191
+ }
192
+ lines.push('');
193
+ }
194
+ if (req.mcp && req.mcp.length > 0) {
195
+ lines.push('### MCP 서버');
196
+ for (const m of req.mcp) {
197
+ const pkg = m.package ? ` (\`${m.package}\`)` : '';
198
+ lines.push(`- **${m.name}**${pkg}`);
199
+ }
200
+ lines.push('');
201
+ }
202
+ if (req.runtime) {
203
+ lines.push('### 런타임');
204
+ if (req.runtime.node)
205
+ lines.push(`- Node.js ${req.runtime.node}`);
206
+ if (req.runtime.python)
207
+ lines.push(`- Python ${req.runtime.python}`);
208
+ lines.push('');
209
+ }
210
+ if (req.teams && req.teams.length > 0) {
211
+ lines.push('### 의존 팀');
212
+ for (const t of req.teams) {
213
+ lines.push(`- \`${t}\``);
214
+ }
215
+ lines.push('');
216
+ }
217
+ }
218
+ return lines.join('\n');
219
+ }
107
220
  function countDir(teamDir, dirName) {
108
221
  const dirPath = path_1.default.join(teamDir, dirName);
109
222
  if (!fs_1.default.existsSync(dirPath))
@@ -186,11 +299,16 @@ function resolveLongDescription(teamDir, yamlValue) {
186
299
  async function createTarball(teamDir) {
187
300
  const tmpFile = path_1.default.join(os_1.default.tmpdir(), `relay-publish-${Date.now()}.tar.gz`);
188
301
  const dirsToInclude = VALID_DIRS.filter((d) => fs_1.default.existsSync(path_1.default.join(teamDir, d)));
302
+ // Include root SKILL.md if it exists
303
+ const entries = [...dirsToInclude];
304
+ if (fs_1.default.existsSync(path_1.default.join(teamDir, 'SKILL.md'))) {
305
+ entries.push('SKILL.md');
306
+ }
189
307
  await (0, tar_1.create)({
190
308
  gzip: true,
191
309
  file: tmpFile,
192
310
  cwd: teamDir,
193
- }, [...dirsToInclude]);
311
+ }, entries);
194
312
  return tmpFile;
195
313
  }
196
314
  async function publishToApi(token, tarPath, metadata, teamDir, portfolioEntries) {
@@ -417,6 +535,10 @@ function registerPublish(program) {
417
535
  console.error(`포트폴리오 이미지: ${portfolioEntries.length}개`);
418
536
  }
419
537
  }
538
+ // Generate root SKILL.md for skill discovery and update checks
539
+ const detectedSkills = detectSkills(relayDir);
540
+ const rootSkillContent = generateRootSkillMd(config, detectedCommands, detectedSkills, config.slug);
541
+ fs_1.default.writeFileSync(path_1.default.join(relayDir, 'SKILL.md'), rootSkillContent);
420
542
  let tarPath = null;
421
543
  try {
422
544
  tarPath = await createTarball(relayDir);
@@ -101,6 +101,21 @@ function registerUpdate(program) {
101
101
  const authorDisplayName = team.author?.display_name ?? authorUsername ?? '';
102
102
  const contactParts = (0, contact_format_js_1.formatContactParts)(team.author?.contact_links);
103
103
  const hasCard = team.welcome || contactParts.length > 0 || authorUsername;
104
+ // Show changelog for this version
105
+ try {
106
+ const versions = await (0, api_js_1.fetchTeamVersions)(slug);
107
+ const thisVersion = versions.find((v) => v.version === latestVersion);
108
+ if (thisVersion?.changelog) {
109
+ console.log(`\n \x1b[90m── Changelog ──────────────────────────────\x1b[0m`);
110
+ for (const line of thisVersion.changelog.split('\n').slice(0, 5)) {
111
+ console.log(` ${line}`);
112
+ }
113
+ console.log(` \x1b[90m───────────────────────────────────────────\x1b[0m`);
114
+ }
115
+ }
116
+ catch {
117
+ // Non-critical: skip changelog display
118
+ }
104
119
  if (hasCard) {
105
120
  console.log(`\n \x1b[90m┌─ ${authorDisplayName || '빌더'}의 명함 ${'─'.repeat(Math.max(0, 34 - (authorDisplayName || '빌더').length))}┐\x1b[0m`);
106
121
  if (team.welcome) {
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ const update_js_1 = require("./commands/update.js");
15
15
  const outdated_js_1 = require("./commands/outdated.js");
16
16
  const check_update_js_1 = require("./commands/check-update.js");
17
17
  const follow_js_1 = require("./commands/follow.js");
18
+ const changelog_js_1 = require("./commands/changelog.js");
18
19
  // eslint-disable-next-line @typescript-eslint/no-var-requires
19
20
  const pkg = require('../package.json');
20
21
  const program = new commander_1.Command();
@@ -36,4 +37,5 @@ program
36
37
  (0, outdated_js_1.registerOutdated)(program);
37
38
  (0, check_update_js_1.registerCheckUpdate)(program);
38
39
  (0, follow_js_1.registerFollow)(program);
40
+ (0, changelog_js_1.registerChangelog)(program);
39
41
  program.parse();
@@ -54,23 +54,20 @@ const LOGIN_JIT_GUIDE = `
54
54
  // ─── 명함 표시 포맷 ───
55
55
  const BUSINESS_CARD_FORMAT = `
56
56
  ### 빌더 명함 표시
57
- CLI 출력에 포함된 빌더 명함 정보를 아래 포맷으로 사용자에게 보여줍니다:
57
+ CLI 출력에 포함된 빌더 명함 정보를 반드시 아래 예시와 동일한 형태로 출력합니다.
58
+ 불릿 리스트(- 또는 *)로 나열하지 마세요. 반드시 인용 블록(>) 안에 넣어야 합니다.
58
59
 
59
- \`\`\`
60
- ┌─ {빌더이름}의 명함 ─────────────────────────┐
61
- │ │
62
- │ 💬 "{환영 메시지}" │
63
- │ │
64
- │ 📧 {이메일} │
65
- │ 🔗 {웹사이트 또는 기타 연락처} │
66
- 👤 relayax.com/@{username} │
67
- │ │
68
- └──────────────────────────────────────────────┘
69
- \`\`\`
60
+ **예시 (이 형태를 그대로 따르세요):**
61
+
62
+ > **🪪 devhaemin의 명함**
63
+ >
64
+ > 💬 "안녕하세요!"
65
+ >
66
+ > 📧 haemin@musibe.com
67
+ > 👤 relayax.com/@devhaemin
70
68
 
71
- - CLI 출력의 ┌─ ... ┘ 박스를 그대로 복사하지 말고, 위 마크다운 포맷으로 다시 렌더링합니다.
72
- - 연락처가 여러 개면 각각 한 줄씩 표시합니다.
73
69
  - 환영 메시지가 없으면 💬 줄을 생략합니다.
70
+ - 연락처가 여러 개면 각각 한 줄씩 표시합니다.
74
71
  - 명함이 비어있으면 명함 블록 전체를 생략합니다.`;
75
72
  // ─── User Commands (글로벌 설치) ───
76
73
  exports.USER_COMMANDS = [
@@ -122,14 +119,22 @@ exports.USER_COMMANDS = [
122
119
  - rules/ — 룰 파일들
123
120
  - relay.yaml — 팀 메타데이터 및 requirements
124
121
 
125
- ### 3. 에이전트 환경에 맞게 배치
122
+ ### 3. 기존 파일 충돌 확인
123
+ 배치 대상 디렉토리(\`.claude/commands/\`, \`.claude/skills/\` 등)에 **같은 이름의 파일이 이미 존재하는지** 확인합니다.
124
+ - 충돌하는 파일이 있으면 사용자에게 반드시 물어봅니다:
125
+ - "다음 파일이 이미 존재합니다: {파일 목록}. 덮어쓸까요, 건너뛸까요?"
126
+ - 사용자가 선택할 때까지 진행하지 않습니다.
127
+ - 충돌이 없으면 그대로 진행합니다.
128
+ - **주의**: 팀에 포함되지 않은 기존 파일은 절대 삭제하지 않습니다.
129
+
130
+ ### 4. 에이전트 환경에 맞게 배치
126
131
  현재 에이전트의 디렉토리 구조에 맞게 파일을 복사합니다:
127
132
  - Claude Code: \`.relay/teams/<slug>/commands/\` → \`.claude/commands/\`에 복사
128
133
  - Claude Code: \`.relay/teams/<slug>/skills/\` → \`.claude/skills/\`에 복사
129
134
  - 다른 에이전트(Cursor, Cline 등): 해당 에이전트의 규칙에 맞는 디렉토리에 복사
130
135
  - 에이전트 설정이나 룰은 적절한 위치에 배치
131
136
 
132
- ### 4. Requirements 확인 및 설치
137
+ ### 5. Requirements 확인 및 설치
133
138
  \`.relay/teams/<slug>/relay.yaml\`의 \`requires\` 섹션을 읽고 처리합니다:
134
139
  - **cli**: \`which <name>\`으로 확인 → 없으면 install 명령 실행 또는 안내
135
140
  - **npm**: \`npm list <package>\`로 확인 → 없으면 \`npm install\`
@@ -139,7 +144,7 @@ exports.USER_COMMANDS = [
139
144
  - **teams**: 의존하는 다른 팀 → \`relay install <@author/team>\`으로 재귀 설치
140
145
  ${LOGIN_JIT_GUIDE}
141
146
 
142
- ### 5. 완료 안내
147
+ ### 6. 완료 안내
143
148
  - 배치된 파일과 활성화된 커맨드 목록을 보여줍니다.
144
149
  ${BUSINESS_CARD_FORMAT}
145
150
  - **팔로우 제안**: 사용자에게 직접 질문하는 인터랙티브 도구(예: AskUserQuestion 등)를 사용하여 "@{username}을 팔로우할까요? 새 버전 알림을 받을 수 있습니다."라고 반드시 물어봅니다. 인터랙티브 도구가 없으면 텍스트로 물어봅니다.
@@ -1,3 +1,11 @@
1
1
  export declare function generatePreamble(slug: string): string;
2
+ /**
3
+ * frontmatter(---...---) 뒤에 preamble을 삽입한다.
4
+ * frontmatter가 없으면 파일 맨 앞에 삽입한다.
5
+ */
2
6
  export declare function injectPreamble(filePath: string, slug: string): void;
7
+ /**
8
+ * 팀의 루트 SKILL.md와 commands/*.md에 preamble을 주입한다.
9
+ * 서브 스킬의 SKILL.md는 건드리지 않는다 (frontmatter 검색에 영향).
10
+ */
3
11
  export declare function injectPreambleToTeam(teamDir: string, slug: string): number;
@@ -12,7 +12,7 @@ const PREAMBLE_START = '<!-- RELAY_PREAMBLE_START - DO NOT EDIT -->';
12
12
  const PREAMBLE_END = '<!-- RELAY_PREAMBLE_END -->';
13
13
  function generatePreamble(slug) {
14
14
  return `${PREAMBLE_START}
15
- ## Preamble (auto-generated by relay)
15
+ ## Preamble (run first)
16
16
 
17
17
  \`\`\`bash
18
18
  relay check-update ${slug} --quiet 2>/dev/null
@@ -22,39 +22,58 @@ If output shows \`CLI_UPGRADE_AVAILABLE <old> <new>\`: tell user "relay v{new} a
22
22
  If output shows \`TEAM_UPGRADE_AVAILABLE\`: tell user "${slug} v{new} available (by @{author}). Run: relay update ${slug}"
23
23
  ${PREAMBLE_END}`;
24
24
  }
25
+ /**
26
+ * frontmatter(---...---) 뒤에 preamble을 삽입한다.
27
+ * frontmatter가 없으면 파일 맨 앞에 삽입한다.
28
+ */
25
29
  function injectPreamble(filePath, slug) {
26
30
  const content = fs_1.default.readFileSync(filePath, 'utf-8');
27
31
  const preamble = generatePreamble(slug);
28
- const startIdx = content.indexOf(PREAMBLE_START);
29
- const endIdx = content.indexOf(PREAMBLE_END);
30
- let newContent;
32
+ // 기존 preamble 제거
33
+ let cleaned = content;
34
+ const startIdx = cleaned.indexOf(PREAMBLE_START);
35
+ const endIdx = cleaned.indexOf(PREAMBLE_END);
31
36
  if (startIdx !== -1 && endIdx !== -1) {
32
- newContent =
33
- content.slice(0, startIdx) +
34
- preamble +
35
- content.slice(endIdx + PREAMBLE_END.length);
37
+ cleaned =
38
+ cleaned.slice(0, startIdx).trimEnd() +
39
+ '\n' +
40
+ cleaned.slice(endIdx + PREAMBLE_END.length).trimStart();
41
+ }
42
+ // frontmatter 뒤에 삽입
43
+ const fmMatch = cleaned.match(/^---\n[\s\S]*?\n---\n/);
44
+ if (fmMatch) {
45
+ const fmEnd = fmMatch[0].length;
46
+ cleaned =
47
+ cleaned.slice(0, fmEnd) +
48
+ '\n' + preamble + '\n\n' +
49
+ cleaned.slice(fmEnd).trimStart();
36
50
  }
37
51
  else {
38
- newContent = preamble + '\n\n' + content;
52
+ cleaned = preamble + '\n\n' + cleaned.trimStart();
39
53
  }
40
- fs_1.default.writeFileSync(filePath, newContent);
54
+ fs_1.default.writeFileSync(filePath, cleaned);
41
55
  }
56
+ /**
57
+ * 팀의 루트 SKILL.md와 commands/*.md에 preamble을 주입한다.
58
+ * 서브 스킬의 SKILL.md는 건드리지 않는다 (frontmatter 검색에 영향).
59
+ */
42
60
  function injectPreambleToTeam(teamDir, slug) {
43
61
  let count = 0;
44
- function walk(dir) {
45
- if (!fs_1.default.existsSync(dir))
46
- return;
47
- for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
48
- const fullPath = path_1.default.join(dir, entry.name);
49
- if (entry.isDirectory()) {
50
- walk(fullPath);
51
- }
52
- else if (entry.name === 'SKILL.md') {
53
- injectPreamble(fullPath, slug);
54
- count++;
55
- }
62
+ // 1. 루트 SKILL.md (skills/<slug>/SKILL.md 레벨)
63
+ const rootSkill = path_1.default.join(teamDir, 'SKILL.md');
64
+ if (fs_1.default.existsSync(rootSkill)) {
65
+ injectPreamble(rootSkill, slug);
66
+ count++;
67
+ }
68
+ // 2. commands/*.md
69
+ const commandsDir = path_1.default.join(teamDir, 'commands');
70
+ if (fs_1.default.existsSync(commandsDir)) {
71
+ for (const entry of fs_1.default.readdirSync(commandsDir, { withFileTypes: true })) {
72
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
73
+ continue;
74
+ injectPreamble(path_1.default.join(commandsDir, entry.name), slug);
75
+ count++;
56
76
  }
57
77
  }
58
- walk(teamDir);
59
78
  return count;
60
79
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relayax-cli",
3
- "version": "0.2.14",
3
+ "version": "0.2.18",
4
4
  "description": "RelayAX Agent Team Marketplace CLI - Install and manage agent teams",
5
5
  "main": "dist/index.js",
6
6
  "bin": {