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.
- package/dist/commands/changelog.d.ts +2 -0
- package/dist/commands/changelog.js +67 -0
- package/dist/commands/publish.js +123 -1
- package/dist/commands/update.js +15 -0
- package/dist/index.js +2 -0
- package/dist/lib/command-adapter.js +22 -17
- package/dist/lib/preamble.d.ts +8 -0
- package/dist/lib/preamble.js +42 -23
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/commands/publish.js
CHANGED
|
@@ -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
|
-
},
|
|
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);
|
package/dist/commands/update.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
###
|
|
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
|
-
###
|
|
147
|
+
### 6. 완료 안내
|
|
143
148
|
- 배치된 파일과 활성화된 커맨드 목록을 보여줍니다.
|
|
144
149
|
${BUSINESS_CARD_FORMAT}
|
|
145
150
|
- **팔로우 제안**: 사용자에게 직접 질문하는 인터랙티브 도구(예: AskUserQuestion 등)를 사용하여 "@{username}을 팔로우할까요? 새 버전 알림을 받을 수 있습니다."라고 반드시 물어봅니다. 인터랙티브 도구가 없으면 텍스트로 물어봅니다.
|
package/dist/lib/preamble.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/preamble.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
52
|
+
cleaned = preamble + '\n\n' + cleaned.trimStart();
|
|
39
53
|
}
|
|
40
|
-
fs_1.default.writeFileSync(filePath,
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
}
|