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 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
+ }