devlyn-cli 0.0.1
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/README.md +43 -0
- package/bin/devlyn.js +362 -0
- package/config/commands/devlyn.design-system.md +502 -0
- package/config/commands/devlyn.discover-product.md +116 -0
- package/config/commands/devlyn.feature-spec.md +630 -0
- package/config/commands/devlyn.handoff.md +13 -0
- package/config/commands/devlyn.product-spec.md +603 -0
- package/config/commands/devlyn.recommend-features.md +286 -0
- package/config/commands/devlyn.resolve.md +108 -0
- package/config/commands/devlyn.review.md +99 -0
- package/config/commands/devlyn.ui.md +342 -0
- package/config/commit-conventions.md +28 -0
- package/config/skills/feature-gap-analysis/SKILL.md +111 -0
- package/config/skills/investigate/SKILL.md +71 -0
- package/config/skills/prompt-engineering/SKILL.md +243 -0
- package/config/templates/prompt-templates.md +71 -0
- package/config/templates/template-feature.spec.md +255 -0
- package/config/templates/template-product-spec.md +680 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# devlyn-cli
|
|
2
|
+
|
|
3
|
+
Claude Code 설정을 팀과 프로젝트 간에 공유하는 CLI 도구.
|
|
4
|
+
|
|
5
|
+
## 사용법
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 새 프로젝트에 .claude 설정 설치
|
|
9
|
+
npx devlyn-cli
|
|
10
|
+
|
|
11
|
+
# 프롬프트 없이 설치 (CI용)
|
|
12
|
+
npx devlyn-cli -y
|
|
13
|
+
|
|
14
|
+
# 최신 버전으로 업데이트
|
|
15
|
+
npx devlyn-cli@latest
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 포함된 Core Config
|
|
19
|
+
|
|
20
|
+
- **commands/** - 커스텀 슬래시 커맨드 (devlyn.ui, devlyn.review 등)
|
|
21
|
+
- **skills/** - AI 에이전트 스킬 (investigate, prompt-engineering 등)
|
|
22
|
+
- **templates/** - 코드 템플릿
|
|
23
|
+
- **commit-conventions.md** - 커밋 메시지 컨벤션
|
|
24
|
+
|
|
25
|
+
## Optional Skill Packs
|
|
26
|
+
|
|
27
|
+
설치 시 선택하거나, 나중에 수동 설치:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Vercel - React, Next.js, React Native best practices
|
|
31
|
+
npx skills add vercel-labs/agent-skills
|
|
32
|
+
|
|
33
|
+
# Supabase - Supabase integration patterns
|
|
34
|
+
npx skills add supabase/agent-skills
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 업데이트
|
|
38
|
+
|
|
39
|
+
새 버전이 배포되면:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx devlyn-cli@latest
|
|
43
|
+
```
|
package/bin/devlyn.js
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const CONFIG_SOURCE = path.join(__dirname, '..', 'config');
|
|
9
|
+
const PKG = require('../package.json');
|
|
10
|
+
|
|
11
|
+
function getTargetDir() {
|
|
12
|
+
try {
|
|
13
|
+
return path.join(process.cwd(), '.claude');
|
|
14
|
+
} catch {
|
|
15
|
+
console.error('\n\x1b[33m❌ Current directory no longer exists.\x1b[0m');
|
|
16
|
+
console.error('\x1b[2m Please cd into a valid directory and try again.\x1b[0m\n');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const COLORS = {
|
|
22
|
+
reset: '\x1b[0m',
|
|
23
|
+
green: '\x1b[32m',
|
|
24
|
+
yellow: '\x1b[33m',
|
|
25
|
+
blue: '\x1b[34m',
|
|
26
|
+
cyan: '\x1b[36m',
|
|
27
|
+
magenta: '\x1b[35m',
|
|
28
|
+
dim: '\x1b[2m',
|
|
29
|
+
bold: '\x1b[1m',
|
|
30
|
+
// Extended colors for gradient effect
|
|
31
|
+
purple: '\x1b[38;5;135m',
|
|
32
|
+
violet: '\x1b[38;5;99m',
|
|
33
|
+
pink: '\x1b[38;5;213m',
|
|
34
|
+
gray: '\x1b[38;5;240m',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function showLogo() {
|
|
38
|
+
const p = COLORS.purple;
|
|
39
|
+
const v = COLORS.violet;
|
|
40
|
+
const k = COLORS.pink;
|
|
41
|
+
const g = COLORS.gray;
|
|
42
|
+
const r = COLORS.reset;
|
|
43
|
+
|
|
44
|
+
// 2.5D effect using block shadows and gradient colors
|
|
45
|
+
const logo = `
|
|
46
|
+
${v} ██████╗ ${p}███████╗${k}██╗ ██╗${v}██╗ ${p}██╗ ██╗${k}███╗ ██╗${r}
|
|
47
|
+
${v} ██╔══██╗${p}██╔════╝${k}██║ ██║${v}██║ ${p}╚██╗ ██╔╝${k}████╗ ██║${r}
|
|
48
|
+
${v} ██║ ██║${p}█████╗ ${k}██║ ██║${v}██║ ${p}╚████╔╝ ${k}██╔██╗ ██║${r}
|
|
49
|
+
${v} ██║ ██║${p}██╔══╝ ${k}╚██╗ ██╔╝${v}██║ ${p}╚██╔╝ ${k}██║╚██╗██║${r}
|
|
50
|
+
${v} ██████╔╝${p}███████╗${k} ╚████╔╝ ${v}███████╗ ${p}██║ ${k}██║ ╚████║${r}
|
|
51
|
+
${g} ╚═════╝ ╚══════╝ ╚═══╝ ╚══════╝ ╚═╝ ╚═╝ ╚═══╝${r}
|
|
52
|
+
|
|
53
|
+
${COLORS.dim} Claude Code Config Toolkit${r}
|
|
54
|
+
${g} v${PKG.version} ${COLORS.dim}· ${k}🍩 by Donut Studio${r}
|
|
55
|
+
`;
|
|
56
|
+
console.log(logo);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const SKILL_PACKS = [
|
|
60
|
+
{ name: 'vercel-labs/agent-skills', desc: 'React, Next.js, React Native best practices' },
|
|
61
|
+
{ name: 'supabase/agent-skills', desc: 'Supabase integration patterns' },
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
function log(msg, color = 'reset') {
|
|
65
|
+
console.log(`${COLORS[color]}${msg}${COLORS.reset}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getDescription(filePath) {
|
|
69
|
+
try {
|
|
70
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
71
|
+
const lines = content.split('\n');
|
|
72
|
+
|
|
73
|
+
// 1. Check if first line is a plain description (not header, not frontmatter, not empty)
|
|
74
|
+
const firstLine = lines[0]?.trim();
|
|
75
|
+
if (firstLine && !firstLine.startsWith('#') && !firstLine.startsWith('---') && !firstLine.startsWith('<') && !firstLine.includes('{')) {
|
|
76
|
+
return firstLine.slice(0, 70);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 2. Look for description in frontmatter
|
|
80
|
+
const descMatch = content.match(/description:\s*["']?([^"'\n]+)/i);
|
|
81
|
+
if (descMatch && !descMatch[1].includes('{')) {
|
|
82
|
+
return descMatch[1].trim().slice(0, 70);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 3. Look for purpose field in yaml blocks
|
|
86
|
+
const purposeMatch = content.match(/purpose:\s*["']?([^"'\n{]+)/i);
|
|
87
|
+
if (purposeMatch && !purposeMatch[1].includes('{')) {
|
|
88
|
+
return purposeMatch[1].trim().slice(0, 70);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 4. Get first H1 title as fallback (skip template placeholders)
|
|
92
|
+
const titleMatch = content.match(/^#\s+([^{}\n]+)$/m);
|
|
93
|
+
if (titleMatch && !titleMatch[1].includes('{') && !titleMatch[1].includes('[')) {
|
|
94
|
+
return titleMatch[1].trim().slice(0, 70);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return '';
|
|
98
|
+
} catch {
|
|
99
|
+
return '';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function listContents() {
|
|
104
|
+
showLogo();
|
|
105
|
+
log('─'.repeat(44), 'dim');
|
|
106
|
+
|
|
107
|
+
const commandsDir = path.join(CONFIG_SOURCE, 'commands');
|
|
108
|
+
const templatesDir = path.join(CONFIG_SOURCE, 'templates');
|
|
109
|
+
const skillsDir = path.join(CONFIG_SOURCE, 'skills');
|
|
110
|
+
|
|
111
|
+
// List commands
|
|
112
|
+
if (fs.existsSync(commandsDir)) {
|
|
113
|
+
const commands = fs.readdirSync(commandsDir).filter((f) => f.endsWith('.md'));
|
|
114
|
+
if (commands.length > 0) {
|
|
115
|
+
log('\n📋 Commands:', 'cyan');
|
|
116
|
+
commands.forEach((file) => {
|
|
117
|
+
const name = file.replace('.md', '').replace('devlyn.', '/');
|
|
118
|
+
const desc = getDescription(path.join(commandsDir, file));
|
|
119
|
+
log(` ${COLORS.green}${name}${COLORS.reset}`);
|
|
120
|
+
if (desc) log(` ${COLORS.dim}${desc}${COLORS.reset}`);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// List templates
|
|
126
|
+
if (fs.existsSync(templatesDir)) {
|
|
127
|
+
const templates = fs.readdirSync(templatesDir).filter((f) => f.endsWith('.md'));
|
|
128
|
+
if (templates.length > 0) {
|
|
129
|
+
log('\n📄 Templates:', 'blue');
|
|
130
|
+
templates.forEach((file) => {
|
|
131
|
+
const name = file.replace('.md', '');
|
|
132
|
+
const desc = getDescription(path.join(templatesDir, file));
|
|
133
|
+
log(` ${COLORS.green}${name}${COLORS.reset}`);
|
|
134
|
+
if (desc) log(` ${COLORS.dim}${desc}${COLORS.reset}`);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// List skills
|
|
140
|
+
if (fs.existsSync(skillsDir)) {
|
|
141
|
+
const skills = fs.readdirSync(skillsDir).filter((d) => {
|
|
142
|
+
const stat = fs.statSync(path.join(skillsDir, d));
|
|
143
|
+
return stat.isDirectory() && fs.existsSync(path.join(skillsDir, d, 'SKILL.md'));
|
|
144
|
+
});
|
|
145
|
+
if (skills.length > 0) {
|
|
146
|
+
log('\n🛠️ Skills:', 'magenta');
|
|
147
|
+
skills.forEach((skill) => {
|
|
148
|
+
const desc = getDescription(path.join(skillsDir, skill, 'SKILL.md'));
|
|
149
|
+
log(` ${COLORS.green}${skill}${COLORS.reset}`);
|
|
150
|
+
if (desc) log(` ${COLORS.dim}${desc}${COLORS.reset}`);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
log('');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function copyRecursive(src, dest, baseDir) {
|
|
159
|
+
const stats = fs.statSync(src);
|
|
160
|
+
|
|
161
|
+
if (stats.isDirectory()) {
|
|
162
|
+
if (!fs.existsSync(dest)) {
|
|
163
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const item of fs.readdirSync(src)) {
|
|
167
|
+
copyRecursive(path.join(src, item), path.join(dest, item), baseDir);
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
const destDir = path.dirname(dest);
|
|
171
|
+
if (!fs.existsSync(destDir)) {
|
|
172
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
173
|
+
}
|
|
174
|
+
fs.copyFileSync(src, dest);
|
|
175
|
+
log(` → ${path.relative(baseDir, dest)}`, 'dim');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function multiSelect(items) {
|
|
180
|
+
return new Promise((resolve) => {
|
|
181
|
+
const selected = new Set();
|
|
182
|
+
let cursor = 0;
|
|
183
|
+
let firstRender = true;
|
|
184
|
+
|
|
185
|
+
const render = () => {
|
|
186
|
+
// Move cursor up to redraw (skip on first render)
|
|
187
|
+
const totalLines = items.length * 2 + 2; // 2 lines per item + header + blank
|
|
188
|
+
if (!firstRender) {
|
|
189
|
+
process.stdout.write(`\x1b[${totalLines}A\x1b[0J`); // Move up and clear to end of screen
|
|
190
|
+
}
|
|
191
|
+
firstRender = false;
|
|
192
|
+
|
|
193
|
+
console.log(`${COLORS.dim}(↑↓ navigate, space select, enter confirm)${COLORS.reset}\n`);
|
|
194
|
+
|
|
195
|
+
items.forEach((item, i) => {
|
|
196
|
+
const checkbox = selected.has(i) ? `${COLORS.green}◉${COLORS.reset}` : `${COLORS.dim}○${COLORS.reset}`;
|
|
197
|
+
const pointer = i === cursor ? `${COLORS.cyan}❯${COLORS.reset}` : ' ';
|
|
198
|
+
const name = i === cursor ? `${COLORS.cyan}${item.name}${COLORS.reset}` : item.name;
|
|
199
|
+
console.log(`${pointer} ${checkbox} ${name}`);
|
|
200
|
+
console.log(` ${COLORS.dim}${item.desc}${COLORS.reset}`);
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
render();
|
|
205
|
+
|
|
206
|
+
process.stdin.setRawMode(true);
|
|
207
|
+
process.stdin.resume();
|
|
208
|
+
process.stdin.setEncoding('utf8');
|
|
209
|
+
|
|
210
|
+
const onKeypress = (key) => {
|
|
211
|
+
// Ctrl+C
|
|
212
|
+
if (key === '\u0003') {
|
|
213
|
+
process.stdin.setRawMode(false);
|
|
214
|
+
process.stdin.removeListener('data', onKeypress);
|
|
215
|
+
process.exit();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Enter
|
|
219
|
+
if (key === '\r' || key === '\n') {
|
|
220
|
+
process.stdin.setRawMode(false);
|
|
221
|
+
process.stdin.removeListener('data', onKeypress);
|
|
222
|
+
process.stdin.pause();
|
|
223
|
+
console.log('');
|
|
224
|
+
resolve([...selected].map((i) => items[i]));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Space - toggle selection
|
|
229
|
+
if (key === ' ') {
|
|
230
|
+
if (selected.has(cursor)) {
|
|
231
|
+
selected.delete(cursor);
|
|
232
|
+
} else {
|
|
233
|
+
selected.add(cursor);
|
|
234
|
+
}
|
|
235
|
+
render();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Arrow up or k
|
|
240
|
+
if (key === '\x1b[A' || key === 'k') {
|
|
241
|
+
cursor = cursor > 0 ? cursor - 1 : items.length - 1;
|
|
242
|
+
render();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Arrow down or j
|
|
247
|
+
if (key === '\x1b[B' || key === 'j') {
|
|
248
|
+
cursor = cursor < items.length - 1 ? cursor + 1 : 0;
|
|
249
|
+
render();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 'a' - select all
|
|
254
|
+
if (key === 'a') {
|
|
255
|
+
if (selected.size === items.length) {
|
|
256
|
+
selected.clear();
|
|
257
|
+
} else {
|
|
258
|
+
items.forEach((_, i) => selected.add(i));
|
|
259
|
+
}
|
|
260
|
+
render();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
process.stdin.on('data', onKeypress);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function installSkillPack(packName) {
|
|
270
|
+
try {
|
|
271
|
+
log(`\n📦 Installing ${packName}...`, 'cyan');
|
|
272
|
+
execSync(`npx skills add ${packName}`, { stdio: 'inherit' });
|
|
273
|
+
return true;
|
|
274
|
+
} catch (error) {
|
|
275
|
+
log(` ⚠️ Failed to install ${packName}`, 'yellow');
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function init(skipPrompts = false) {
|
|
281
|
+
showLogo();
|
|
282
|
+
log('─'.repeat(44), 'dim');
|
|
283
|
+
|
|
284
|
+
if (!fs.existsSync(CONFIG_SOURCE)) {
|
|
285
|
+
log('❌ Config source not found', 'yellow');
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Install core config
|
|
290
|
+
const targetDir = getTargetDir();
|
|
291
|
+
log('\n📁 Installing core config to .claude/', 'green');
|
|
292
|
+
copyRecursive(CONFIG_SOURCE, targetDir, targetDir);
|
|
293
|
+
log('\n✅ Core config installed!', 'green');
|
|
294
|
+
|
|
295
|
+
// Skip prompts if -y flag or non-interactive
|
|
296
|
+
if (skipPrompts || !process.stdin.isTTY) {
|
|
297
|
+
log('\n💡 Add skill packs later with:', 'dim');
|
|
298
|
+
SKILL_PACKS.forEach((pack) => {
|
|
299
|
+
log(` npx skills add ${pack.name}`, 'dim');
|
|
300
|
+
});
|
|
301
|
+
log('');
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Ask about skill packs
|
|
306
|
+
log('\n📚 Optional skill packs:\n', 'blue');
|
|
307
|
+
|
|
308
|
+
const selectedPacks = await multiSelect(SKILL_PACKS);
|
|
309
|
+
|
|
310
|
+
if (selectedPacks.length > 0) {
|
|
311
|
+
for (const pack of selectedPacks) {
|
|
312
|
+
installSkillPack(pack.name);
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
log('💡 No skill packs selected', 'dim');
|
|
316
|
+
log(' Add later with: npx skills add <pack-name>\n', 'dim');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
log('\n✨ All done!', 'green');
|
|
320
|
+
log(' Run `npx devlyn-cli` again to update\n', 'dim');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function showHelp() {
|
|
324
|
+
showLogo();
|
|
325
|
+
log('Usage:', 'green');
|
|
326
|
+
log(' npx devlyn-cli Install/update .claude config');
|
|
327
|
+
log(' npx devlyn-cli list List available commands & templates');
|
|
328
|
+
log(' npx devlyn-cli -y Install without prompts');
|
|
329
|
+
log(' npx devlyn-cli --help Show this help\n');
|
|
330
|
+
log('Skill packs:', 'green');
|
|
331
|
+
SKILL_PACKS.forEach((pack) => {
|
|
332
|
+
log(` npx skills add ${pack.name}`);
|
|
333
|
+
});
|
|
334
|
+
log('');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Main
|
|
338
|
+
const args = process.argv.slice(2);
|
|
339
|
+
const command = args[0];
|
|
340
|
+
|
|
341
|
+
switch (command) {
|
|
342
|
+
case '--help':
|
|
343
|
+
case '-h':
|
|
344
|
+
showHelp();
|
|
345
|
+
break;
|
|
346
|
+
case '-y':
|
|
347
|
+
case '--yes':
|
|
348
|
+
init(true);
|
|
349
|
+
break;
|
|
350
|
+
case 'list':
|
|
351
|
+
case 'ls':
|
|
352
|
+
listContents();
|
|
353
|
+
break;
|
|
354
|
+
case 'init':
|
|
355
|
+
case undefined:
|
|
356
|
+
init(false);
|
|
357
|
+
break;
|
|
358
|
+
default:
|
|
359
|
+
log(`Unknown command: ${command}`, 'yellow');
|
|
360
|
+
showHelp();
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|