add-ai-tools 1.0.0

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/index.mjs ADDED
@@ -0,0 +1,1906 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from "commander";
4
+ import inquirer from "inquirer";
5
+ import ora from "ora";
6
+ import { homedir } from "os";
7
+ import * as path from "path";
8
+ import { join } from "path";
9
+ import { parse } from "yaml";
10
+ import { basename, dirname, join as join$1 } from "node:path";
11
+ import { existsSync } from "node:fs";
12
+ import { copyFile, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
13
+ import { createHash, randomBytes } from "node:crypto";
14
+ import { createTwoFilesPatch } from "diff";
15
+ import chalk from "chalk";
16
+ import archiver from "archiver";
17
+ import * as fs from "fs";
18
+
19
+ //#region src/data/agents.ts
20
+ /**
21
+ * Agent 설정 데이터
22
+ * 각 AI 코딩 어시스턴트의 리소스 설치 경로 및 지원 타입 정의
23
+ */
24
+ const agents = {
25
+ "claude-code": {
26
+ name: "Claude Code",
27
+ supportedTypes: [
28
+ "skills",
29
+ "rules",
30
+ "agents"
31
+ ],
32
+ paths: {
33
+ project: {
34
+ skills: ".claude/skills/",
35
+ rules: ".claude/rules/",
36
+ agents: ".claude/agents/"
37
+ },
38
+ global: {
39
+ skills: "~/.claude/skills/",
40
+ rules: "~/.claude/rules/",
41
+ agents: "~/.claude/agents/"
42
+ }
43
+ }
44
+ },
45
+ cursor: {
46
+ name: "Cursor",
47
+ supportedTypes: ["skills", "rules"],
48
+ paths: {
49
+ project: {
50
+ skills: ".cursor/skills/",
51
+ rules: ".cursor/rules/",
52
+ agents: null
53
+ },
54
+ global: {
55
+ skills: "~/.cursor/skills/",
56
+ rules: "~/.cursor/rules/",
57
+ agents: null
58
+ }
59
+ }
60
+ },
61
+ "github-copilot": {
62
+ name: "GitHub Copilot",
63
+ supportedTypes: ["skills", "rules"],
64
+ paths: {
65
+ project: {
66
+ skills: ".github/skills/",
67
+ rules: ".github/instructions/",
68
+ agents: null
69
+ },
70
+ global: {
71
+ skills: "~/.copilot/skills/",
72
+ rules: "~/.copilot/instructions/",
73
+ agents: null
74
+ }
75
+ }
76
+ },
77
+ antigravity: {
78
+ name: "Antigravity",
79
+ supportedTypes: ["skills", "rules"],
80
+ paths: {
81
+ project: {
82
+ skills: ".agent/skills/",
83
+ rules: ".agent/rules/",
84
+ agents: null
85
+ },
86
+ global: {
87
+ skills: "~/.gemini/antigravity/skills/",
88
+ rules: "~/.gemini/antigravity/rules/",
89
+ agents: null
90
+ }
91
+ }
92
+ }
93
+ };
94
+
95
+ //#endregion
96
+ //#region src/path/PathResolver.ts
97
+ /**
98
+ * PathResolver - Agent별 경로 해석 및 지원 타입 관리
99
+ *
100
+ * 주요 기능:
101
+ * - Agent가 지원하는 리소스 타입 조회
102
+ * - Agent별 설치 경로 해석 (project/global scope)
103
+ * - 타입 지원 여부 확인
104
+ */
105
+ var PathResolver = class {
106
+ agents;
107
+ constructor() {
108
+ this.agents = agents;
109
+ }
110
+ /**
111
+ * Agent가 지원하는 리소스 타입 목록 반환
112
+ * @param agent - 대상 에이전트
113
+ * @returns 지원하는 리소스 타입 배열
114
+ */
115
+ getSupportedTypes(agent) {
116
+ const agentConfig = this.agents[agent];
117
+ if (!agentConfig) return [];
118
+ return agentConfig.supportedTypes;
119
+ }
120
+ /**
121
+ * Agent별 설치 경로 해석
122
+ * @param agent - 대상 에이전트
123
+ * @param type - 리소스 타입
124
+ * @param scope - 설치 범위 (project | global)
125
+ * @returns 설치 경로 또는 null (미지원)
126
+ */
127
+ resolveAgentPath(agent, type, scope) {
128
+ const agentConfig = this.agents[agent];
129
+ if (!agentConfig) throw new Error(`Unknown agent: ${agent}`);
130
+ const paths = agentConfig.paths[scope];
131
+ if (!paths) throw new Error(`Unknown scope: ${scope}`);
132
+ const pathTemplate = paths[type];
133
+ if (pathTemplate === null || pathTemplate === void 0) return null;
134
+ return this.expandTilde(pathTemplate);
135
+ }
136
+ /**
137
+ * 타입이 Agent에서 지원되는지 확인
138
+ * @param agent - 대상 에이전트
139
+ * @param type - 확인할 리소스 타입
140
+ * @returns 지원 여부
141
+ */
142
+ isTypeSupported(agent, type) {
143
+ const agentConfig = this.agents[agent];
144
+ if (!agentConfig) return false;
145
+ return agentConfig.supportedTypes.includes(type);
146
+ }
147
+ /**
148
+ * 모든 Agent 목록 반환
149
+ * @returns Agent 키 배열
150
+ */
151
+ getAgents() {
152
+ return Object.keys(this.agents);
153
+ }
154
+ /**
155
+ * Agent 설정 전체 반환
156
+ * @param agent - 대상 에이전트
157
+ * @returns Agent 설정 객체
158
+ */
159
+ getAgentConfig(agent) {
160
+ const agentConfig = this.agents[agent];
161
+ if (!agentConfig) throw new Error(`Unknown agent: ${agent}`);
162
+ return agentConfig;
163
+ }
164
+ /**
165
+ * Agent 표시 이름 반환
166
+ * @param agent - 대상 에이전트
167
+ * @returns 표시용 이름
168
+ */
169
+ getAgentName(agent) {
170
+ return this.getAgentConfig(agent).name;
171
+ }
172
+ /**
173
+ * ~ 를 $HOME으로 확장
174
+ * @param path - 경로 문자열
175
+ * @returns 확장된 경로
176
+ */
177
+ expandTilde(path) {
178
+ if (path.startsWith("~/")) return join(homedir(), path.slice(2));
179
+ return path;
180
+ }
181
+ };
182
+ const pathResolver = new PathResolver();
183
+
184
+ //#endregion
185
+ //#region src/source/SourceParser.ts
186
+ /**
187
+ * GitHub URL 매칭 정규식
188
+ * 예: https://github.com/owner/repo
189
+ * https://github.com/owner/repo/tree/branch/path/to/skill
190
+ * https://github.com/owner/repo/blob/branch/path/to/file.md
191
+ */
192
+ const GITHUB_URL_REGEX = /^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/(?:tree|blob)\/([^/]+)(?:\/(.+))?)?$/;
193
+ /**
194
+ * GitLab URL 매칭 정규식
195
+ * 예: https://gitlab.com/owner/repo
196
+ * https://gitlab.com/owner/repo/-/tree/branch/path
197
+ */
198
+ const GITLAB_URL_REGEX = /^https?:\/\/(?:www\.)?gitlab\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/-\/(?:tree|blob)\/([^/]+)(?:\/(.+))?)?$/;
199
+ /**
200
+ * GitHub shorthand 매칭 정규식
201
+ * 예: owner/repo
202
+ * vercel-labs/agent-skills
203
+ */
204
+ const GITHUB_SHORTHAND_REGEX = /^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)$/;
205
+ /**
206
+ * Git URL 매칭 정규식 (SSH 또는 git:// 프로토콜)
207
+ * 예: git@github.com:owner/repo.git
208
+ * git://github.com/owner/repo.git
209
+ */
210
+ const GIT_URL_REGEX = /^(?:git@([^:]+):([^/]+)\/([^/]+?)(?:\.git)?|git:\/\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?)$/;
211
+ /**
212
+ * 입력이 직접 SKILL.md URL인지 확인
213
+ * 예: https://raw.githubusercontent.com/.../SKILL.md
214
+ */
215
+ function isDirectSkillUrl(input) {
216
+ if (!input.startsWith("http://") && !input.startsWith("https://")) return false;
217
+ const lowerInput = input.toLowerCase();
218
+ return lowerInput.endsWith("/skill.md") || lowerInput.endsWith("/rules.md") || lowerInput.endsWith("/commands.md") || lowerInput.endsWith("/agent.md");
219
+ }
220
+ /**
221
+ * 소스 문자열을 파싱하여 ParsedSource 객체로 변환
222
+ *
223
+ * 지원하는 소스 포맷:
224
+ * - GitHub shorthand: owner/repo
225
+ * - GitHub URL: https://github.com/owner/repo
226
+ * - GitHub URL with path: https://github.com/owner/repo/tree/main/skills/frontend-design
227
+ * - GitLab URL: https://gitlab.com/owner/repo
228
+ * - Git URL: git@github.com:owner/repo.git
229
+ * - Direct URL: https://raw.githubusercontent.com/.../SKILL.md
230
+ */
231
+ function parseSource(input) {
232
+ const trimmed = input.trim();
233
+ const githubMatch = trimmed.match(GITHUB_URL_REGEX);
234
+ if (githubMatch) {
235
+ const [, owner, repo, ref, subpath] = githubMatch;
236
+ return {
237
+ type: "github",
238
+ url: `https://github.com/${owner}/${repo}`,
239
+ owner,
240
+ repo,
241
+ ref: ref || void 0,
242
+ subpath: subpath || void 0,
243
+ raw: input
244
+ };
245
+ }
246
+ const gitlabMatch = trimmed.match(GITLAB_URL_REGEX);
247
+ if (gitlabMatch) {
248
+ const [, owner, repo, ref, subpath] = gitlabMatch;
249
+ return {
250
+ type: "gitlab",
251
+ url: `https://gitlab.com/${owner}/${repo}`,
252
+ owner,
253
+ repo,
254
+ ref: ref || void 0,
255
+ subpath: subpath || void 0,
256
+ raw: input
257
+ };
258
+ }
259
+ const gitMatch = trimmed.match(GIT_URL_REGEX);
260
+ if (gitMatch) {
261
+ if (gitMatch[1]) {
262
+ const [, host, owner, repo] = gitMatch;
263
+ return {
264
+ type: host === "github.com" ? "github" : host === "gitlab.com" ? "gitlab" : "git",
265
+ url: `https://${host}/${owner}/${repo}`,
266
+ owner,
267
+ repo,
268
+ raw: input
269
+ };
270
+ }
271
+ if (gitMatch[4]) {
272
+ const [, , , , host, owner, repo] = gitMatch;
273
+ return {
274
+ type: host === "github.com" ? "github" : host === "gitlab.com" ? "gitlab" : "git",
275
+ url: `https://${host}/${owner}/${repo}`,
276
+ owner,
277
+ repo,
278
+ raw: input
279
+ };
280
+ }
281
+ }
282
+ const shorthandMatch = trimmed.match(GITHUB_SHORTHAND_REGEX);
283
+ if (shorthandMatch) {
284
+ const [, owner, repo] = shorthandMatch;
285
+ return {
286
+ type: "github",
287
+ url: `https://github.com/${owner}/${repo}`,
288
+ owner,
289
+ repo,
290
+ raw: input
291
+ };
292
+ }
293
+ if (isDirectSkillUrl(trimmed)) return {
294
+ type: "direct-url",
295
+ url: trimmed,
296
+ raw: input
297
+ };
298
+ if (trimmed.startsWith("https://") || trimmed.startsWith("http://")) return {
299
+ type: "git",
300
+ url: trimmed,
301
+ raw: input
302
+ };
303
+ throw new Error(`Invalid source format: ${input}. Please use GitHub URL (https://github.com/owner/repo) or owner/repo format.`);
304
+ }
305
+ /**
306
+ * ParsedSource에서 owner/repo 식별자 추출 (텔레메트리용)
307
+ */
308
+ function getOwnerRepo(parsed) {
309
+ if (parsed.owner && parsed.repo) return `${parsed.owner}/${parsed.repo}`;
310
+ if (parsed.url) {
311
+ const match = parsed.url.match(/(?:github|gitlab)\.com\/([^/]+)\/([^/]+)/);
312
+ if (match) return `${match[1]}/${match[2]}`;
313
+ }
314
+ return null;
315
+ }
316
+ /**
317
+ * ParsedSource가 특정 스킬/리소스를 직접 가리키는지 확인
318
+ */
319
+ function isDirectResourcePath(parsed) {
320
+ if (parsed.type === "direct-url") return true;
321
+ if (parsed.subpath) return true;
322
+ return false;
323
+ }
324
+ /**
325
+ * 소스 유형에 따른 표시 문자열 반환
326
+ */
327
+ function getSourceDisplayName(parsed) {
328
+ switch (parsed.type) {
329
+ case "github": return parsed.owner && parsed.repo ? `GitHub: ${parsed.owner}/${parsed.repo}${parsed.subpath ? `/${parsed.subpath}` : ""}` : parsed.url || parsed.raw;
330
+ case "gitlab": return parsed.owner && parsed.repo ? `GitLab: ${parsed.owner}/${parsed.repo}${parsed.subpath ? `/${parsed.subpath}` : ""}` : parsed.url || parsed.raw;
331
+ case "git": return `Git: ${parsed.url || parsed.raw}`;
332
+ case "direct-url": return `URL: ${parsed.url}`;
333
+ default: return parsed.raw;
334
+ }
335
+ }
336
+
337
+ //#endregion
338
+ //#region src/parser/ResourceParser.ts
339
+ /**
340
+ * ResourceParser
341
+ *
342
+ * Converts source files to Resource objects by:
343
+ * - Parsing YAML frontmatter
344
+ * - Detecting resource type from filename
345
+ * - Extracting metadata
346
+ */
347
+ var ResourceParser = class {
348
+ /**
349
+ * Parse source file to resource
350
+ */
351
+ parseResource(file, type) {
352
+ const frontmatter = this.parseYAMLFrontmatter(file.content);
353
+ const detectedType = this.detectType(file.path) || type;
354
+ const resource = {
355
+ name: frontmatter.name || this.extractNameFromPath(file.path),
356
+ type: detectedType,
357
+ description: frontmatter.description || "",
358
+ path: file.path,
359
+ content: file.content,
360
+ metadata: {
361
+ author: frontmatter.author,
362
+ version: frontmatter.version,
363
+ license: frontmatter.license,
364
+ category: frontmatter.category
365
+ }
366
+ };
367
+ if (file.siblingFiles && file.siblingFiles.length > 0) resource.directory = { files: file.siblingFiles };
368
+ return resource;
369
+ }
370
+ /**
371
+ * Parse multiple source files
372
+ */
373
+ parseResources(files, type) {
374
+ return files.map((file) => this.parseResource(file, type));
375
+ }
376
+ /**
377
+ * Parse YAML frontmatter
378
+ * Extracts content between --- delimiters
379
+ */
380
+ parseYAMLFrontmatter(content) {
381
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
382
+ if (!match) return {};
383
+ try {
384
+ return parse(match[1]) || {};
385
+ } catch (error) {
386
+ console.warn("Failed to parse YAML frontmatter:", error);
387
+ return {};
388
+ }
389
+ }
390
+ /**
391
+ * Detect resource type from file path
392
+ */
393
+ detectType(filePath) {
394
+ const filename = basename(filePath);
395
+ return {
396
+ "SKILL.md": "skills",
397
+ "RULES.md": "rules",
398
+ "AGENT.md": "agents"
399
+ }[filename] || null;
400
+ }
401
+ /**
402
+ * Extract resource name from file path
403
+ * Example: skills/commit/SKILL.md -> commit
404
+ */
405
+ extractNameFromPath(filePath) {
406
+ const parts = dirname(filePath).split("/").filter(Boolean);
407
+ if (parts.length > 0) {
408
+ const lastName = parts[parts.length - 1];
409
+ if (![
410
+ "skills",
411
+ "rules",
412
+ "commands",
413
+ "agents"
414
+ ].includes(lastName)) return lastName;
415
+ if (parts.length > 1) return parts[parts.length - 2];
416
+ }
417
+ return basename(filePath, ".md").toLowerCase();
418
+ }
419
+ };
420
+
421
+ //#endregion
422
+ //#region src/fetch/GitHubFetcher.ts
423
+ /**
424
+ * GitHub API 에러
425
+ */
426
+ var GitHubApiError = class extends Error {
427
+ constructor(message, status, path) {
428
+ super(message);
429
+ this.status = status;
430
+ this.path = path;
431
+ this.name = "GitHubApiError";
432
+ }
433
+ };
434
+ /**
435
+ * GitHub 리소스를 찾을 수 없음
436
+ */
437
+ var GitHubNotFoundError = class extends GitHubApiError {
438
+ constructor(path) {
439
+ super(`Resource not found: ${path}`, 404, path);
440
+ this.name = "GitHubNotFoundError";
441
+ }
442
+ };
443
+ /**
444
+ * GitHub API 레이트 제한
445
+ */
446
+ var GitHubRateLimitError = class extends GitHubApiError {
447
+ constructor(path) {
448
+ super("GitHub API rate limit exceeded. Try again later or use authenticated requests.", 403, path);
449
+ this.name = "GitHubRateLimitError";
450
+ }
451
+ };
452
+ /**
453
+ * GitHubFetcher - GitHub 레포지토리에서 리소스를 가져옵니다
454
+ */
455
+ var GitHubFetcher = class {
456
+ parser;
457
+ baseApiUrl = "https://api.github.com";
458
+ baseRawUrl = "https://raw.githubusercontent.com";
459
+ constructor() {
460
+ this.parser = new ResourceParser();
461
+ }
462
+ /**
463
+ * GitHub 소스에서 리소스 목록을 가져옵니다
464
+ */
465
+ async fetchResources(source, types) {
466
+ if (source.type !== "github" || !source.owner || !source.repo) throw new Error("Invalid GitHub source");
467
+ const resources = [];
468
+ const ref = source.ref || "main";
469
+ for (const type of types) {
470
+ const dirPath = this.getResourceDirPath(type, source.subpath);
471
+ const typeResources = await this.fetchResourcesFromDir(source.owner, source.repo, dirPath, type, ref);
472
+ resources.push(...typeResources);
473
+ }
474
+ return resources;
475
+ }
476
+ /**
477
+ * 디렉토리에서 리소스를 가져옵니다
478
+ */
479
+ async fetchResourcesFromDir(owner, repo, dirPath, type, ref) {
480
+ try {
481
+ const contents = await this.listDirectory(owner, repo, dirPath, ref);
482
+ const resources = [];
483
+ for (const item of contents) if (item.type === "dir") {
484
+ const resource = await this.fetchResourceFromSubdir(owner, repo, item.path, type, ref);
485
+ if (resource) resources.push(resource);
486
+ } else if (item.type === "file" && item.name.endsWith(".md")) {
487
+ const resource = await this.fetchSingleResource(owner, repo, item.path, type, ref);
488
+ if (resource) resources.push(resource);
489
+ }
490
+ return resources;
491
+ } catch (error) {
492
+ if (error instanceof GitHubNotFoundError) return [];
493
+ throw error;
494
+ }
495
+ }
496
+ /**
497
+ * 하위 디렉토리에서 리소스를 가져옵니다 (디렉토리 전체 복제)
498
+ */
499
+ async fetchResourceFromSubdir(owner, repo, dirPath, type, ref) {
500
+ try {
501
+ const contents = await this.listDirectory(owner, repo, dirPath, ref);
502
+ const mainFile = this.findMainResourceFile(contents, type);
503
+ if (!mainFile) return null;
504
+ const content = await this.fetchFileContent(owner, repo, mainFile.path, ref);
505
+ const siblingFiles = await this.fetchAllFilesInDir(owner, repo, dirPath, contents, ref, mainFile.name);
506
+ const sourceFile = {
507
+ path: mainFile.path,
508
+ content,
509
+ isDirectory: false,
510
+ siblingFiles
511
+ };
512
+ return this.parser.parseResource(sourceFile, type);
513
+ } catch (error) {
514
+ if (error instanceof GitHubNotFoundError) return null;
515
+ console.warn(`Failed to fetch resource from ${dirPath}:`, error instanceof Error ? error.message : error);
516
+ return null;
517
+ }
518
+ }
519
+ /**
520
+ * 디렉토리 내 모든 파일을 가져옵니다 (메인 파일 제외, 상대 경로)
521
+ */
522
+ async fetchAllFilesInDir(owner, repo, basePath, contents, ref, excludeFile) {
523
+ const files = [];
524
+ for (const item of contents) {
525
+ if (item.type === "file" && item.name === excludeFile) continue;
526
+ if (item.type === "file") try {
527
+ const content = await this.fetchFileContent(owner, repo, item.path, ref);
528
+ files.push({
529
+ path: item.name,
530
+ content,
531
+ isDirectory: false
532
+ });
533
+ } catch (error) {
534
+ console.warn(`Failed to fetch file ${item.path}:`, error instanceof Error ? error.message : error);
535
+ }
536
+ else if (item.type === "dir") {
537
+ const dirFiles = await this.fetchDirectoryFilesRecursive(owner, repo, item.path, ref, basePath);
538
+ files.push(...dirFiles);
539
+ }
540
+ }
541
+ return files;
542
+ }
543
+ /**
544
+ * 단일 리소스 파일을 가져옵니다
545
+ */
546
+ async fetchSingleResource(owner, repo, filePath, type, ref) {
547
+ try {
548
+ const sourceFile = {
549
+ path: filePath,
550
+ content: await this.fetchFileContent(owner, repo, filePath, ref),
551
+ isDirectory: false
552
+ };
553
+ return this.parser.parseResource(sourceFile, type);
554
+ } catch (error) {
555
+ if (error instanceof GitHubNotFoundError) return null;
556
+ console.warn(`Failed to fetch resource ${filePath}:`, error instanceof Error ? error.message : error);
557
+ return null;
558
+ }
559
+ }
560
+ /**
561
+ * 디렉토리 내 모든 파일을 재귀적으로 가져옵니다 (상대 경로 반환)
562
+ */
563
+ async fetchDirectoryFilesRecursive(owner, repo, dirPath, ref, basePath) {
564
+ try {
565
+ const contents = await this.listDirectory(owner, repo, dirPath, ref);
566
+ const files = [];
567
+ for (const item of contents) {
568
+ const relativePath = item.path.startsWith(`${basePath}/`) ? item.path.slice(basePath.length + 1) : item.path;
569
+ if (item.type === "file") try {
570
+ const content = await this.fetchFileContent(owner, repo, item.path, ref);
571
+ files.push({
572
+ path: relativePath,
573
+ content,
574
+ isDirectory: false
575
+ });
576
+ } catch (error) {
577
+ console.warn(`Failed to fetch file ${item.path}:`, error instanceof Error ? error.message : error);
578
+ }
579
+ else if (item.type === "dir") {
580
+ const subFiles = await this.fetchDirectoryFilesRecursive(owner, repo, item.path, ref, basePath);
581
+ files.push(...subFiles);
582
+ }
583
+ }
584
+ return files;
585
+ } catch (error) {
586
+ if (error instanceof GitHubNotFoundError) return [];
587
+ console.warn(`Failed to fetch directory ${dirPath}:`, error instanceof Error ? error.message : error);
588
+ return [];
589
+ }
590
+ }
591
+ /**
592
+ * GitHub API로 디렉토리 목록을 가져옵니다
593
+ */
594
+ async listDirectory(owner, repo, path, ref) {
595
+ const url = `${this.baseApiUrl}/repos/${owner}/${repo}/contents/${path}?ref=${ref}`;
596
+ const response = await fetch(url, { headers: {
597
+ "Accept": "application/vnd.github.v3+json",
598
+ "User-Agent": "ai-toolkit"
599
+ } });
600
+ if (!response.ok) this.handleHttpError(response.status, path);
601
+ const data = await response.json();
602
+ return Array.isArray(data) ? data : [];
603
+ }
604
+ /**
605
+ * 파일 내용을 가져옵니다
606
+ */
607
+ async fetchFileContent(owner, repo, path, ref) {
608
+ const url = `${this.baseRawUrl}/${owner}/${repo}/${ref}/${path}`;
609
+ const response = await fetch(url);
610
+ if (!response.ok) this.handleHttpError(response.status, path);
611
+ return response.text();
612
+ }
613
+ /**
614
+ * HTTP 에러를 처리합니다
615
+ */
616
+ handleHttpError(status, path) {
617
+ switch (status) {
618
+ case 404: throw new GitHubNotFoundError(path);
619
+ case 403: throw new GitHubRateLimitError(path);
620
+ case 401: throw new GitHubApiError("Authentication required", status, path);
621
+ case 500:
622
+ case 502:
623
+ case 503: throw new GitHubApiError("GitHub server error. Please try again later.", status, path);
624
+ default: throw new GitHubApiError(`GitHub API error: ${status}`, status, path);
625
+ }
626
+ }
627
+ /**
628
+ * 리소스 타입에 맞는 디렉토리 경로를 반환합니다
629
+ */
630
+ getResourceDirPath(type, subpath) {
631
+ if (subpath) return subpath;
632
+ return type;
633
+ }
634
+ /**
635
+ * 메인 리소스 파일을 찾습니다
636
+ */
637
+ findMainResourceFile(contents, type) {
638
+ const candidates = {
639
+ skills: ["SKILL.md", "skill.md"],
640
+ rules: [
641
+ "RULE.md",
642
+ "RULES.md",
643
+ "rule.md",
644
+ "rules.md",
645
+ "README.md"
646
+ ],
647
+ agents: ["AGENT.md", "agent.md"]
648
+ }[type] || [];
649
+ for (const candidate of candidates) {
650
+ const found = contents.find((item) => item.type === "file" && item.name.toLowerCase() === candidate.toLowerCase());
651
+ if (found) return found;
652
+ }
653
+ return contents.find((item) => item.type === "file" && item.name.endsWith(".md")) || null;
654
+ }
655
+ };
656
+ const githubFetcher = new GitHubFetcher();
657
+
658
+ //#endregion
659
+ //#region src/prompts/InteractivePrompt.ts
660
+ /**
661
+ * InteractivePrompt - 외부 소스 기반 설치 플로우
662
+ *
663
+ * 플로우: Agent → Source URL → Type 선택 → Resources 선택 → Scope → Confirm
664
+ */
665
+ var InteractivePrompt = class {
666
+ /**
667
+ * Interactive 플로우 실행
668
+ */
669
+ async run() {
670
+ console.log("Welcome to AI Toolkit!\n");
671
+ const agent = await this.selectAgent();
672
+ const parsedSource = parseSource(await this.inputSource());
673
+ console.log(`\nSource: ${getSourceDisplayName(parsedSource)}`);
674
+ const types = await this.selectTypes(agent);
675
+ const spinner = ora("Fetching resources from source...").start();
676
+ let availableResources = [];
677
+ try {
678
+ if (parsedSource.type === "github") availableResources = await githubFetcher.fetchResources(parsedSource, types);
679
+ else spinner.warn(`Source type "${parsedSource.type}" is not yet supported.`);
680
+ spinner.succeed(`Found ${availableResources.length} resource(s)`);
681
+ } catch (error) {
682
+ spinner.fail(`Failed to fetch resources: ${error instanceof Error ? error.message : String(error)}`);
683
+ return {
684
+ agent,
685
+ types,
686
+ resources: [],
687
+ scope: "project",
688
+ source: parsedSource
689
+ };
690
+ }
691
+ return {
692
+ agent,
693
+ types,
694
+ resources: await this.selectResources(availableResources, types),
695
+ scope: await this.selectScope(),
696
+ source: parsedSource
697
+ };
698
+ }
699
+ /**
700
+ * Agent 선택
701
+ */
702
+ async selectAgent() {
703
+ const agents = pathResolver.getAgents();
704
+ const { agent } = await inquirer.prompt([{
705
+ type: "list",
706
+ name: "agent",
707
+ message: "Select target agent:",
708
+ choices: agents.map((key) => ({
709
+ name: pathResolver.getAgentConfig(key).name,
710
+ value: key
711
+ }))
712
+ }]);
713
+ return agent;
714
+ }
715
+ /**
716
+ * Source URL 입력
717
+ */
718
+ async inputSource() {
719
+ const { source } = await inquirer.prompt([{
720
+ type: "input",
721
+ name: "source",
722
+ message: "Enter source (GitHub URL or owner/repo):",
723
+ validate: (input) => {
724
+ if (!input.trim()) return "Please enter a source";
725
+ return true;
726
+ }
727
+ }]);
728
+ return source.trim();
729
+ }
730
+ /**
731
+ * Type 선택 (Agent 지원 타입만 표시)
732
+ */
733
+ async selectTypes(agent) {
734
+ const supportedTypes = pathResolver.getSupportedTypes(agent);
735
+ const typeDescriptions = {
736
+ skills: "Skills - Reusable prompts and instructions",
737
+ rules: "Rules - Project guidelines and standards",
738
+ agents: "Agents - Specialized agent configurations"
739
+ };
740
+ const { types } = await inquirer.prompt([{
741
+ type: "checkbox",
742
+ name: "types",
743
+ message: `Select resource types to install (${pathResolver.getAgentConfig(agent).name} supports):`,
744
+ choices: supportedTypes.map((type) => ({
745
+ name: typeDescriptions[type],
746
+ value: type,
747
+ checked: type === "skills"
748
+ })),
749
+ validate: (input) => {
750
+ if (input.length === 0) return "Please select at least one type";
751
+ return true;
752
+ }
753
+ }]);
754
+ return types;
755
+ }
756
+ /**
757
+ * Scope 선택
758
+ */
759
+ async selectScope() {
760
+ const { scope } = await inquirer.prompt([{
761
+ type: "list",
762
+ name: "scope",
763
+ message: "Select installation scope:",
764
+ choices: [{
765
+ name: "Project - Install in current directory",
766
+ value: "project"
767
+ }, {
768
+ name: "Global - Install in home directory",
769
+ value: "global"
770
+ }],
771
+ default: "project"
772
+ }]);
773
+ return scope;
774
+ }
775
+ /**
776
+ * 리소스 선택 (가져온 리소스 목록에서)
777
+ */
778
+ async selectResources(availableResources, types) {
779
+ const filteredResources = availableResources.filter((r) => types.includes(r.type));
780
+ if (filteredResources.length === 0) {
781
+ console.log("\nNo resources found for selected types.");
782
+ return [];
783
+ }
784
+ const { resources } = await inquirer.prompt([{
785
+ type: "checkbox",
786
+ name: "resources",
787
+ message: "Select resources to install:",
788
+ choices: filteredResources.map((r) => ({
789
+ name: `[${r.type}] ${r.name} - ${r.description || "No description"}`,
790
+ value: r,
791
+ checked: true
792
+ })),
793
+ validate: (input) => {
794
+ if (input.length === 0) return "Please select at least one resource";
795
+ return true;
796
+ }
797
+ }]);
798
+ return resources;
799
+ }
800
+ /**
801
+ * 설치 확인
802
+ */
803
+ async confirmInstallation(agent, resources, scope) {
804
+ console.log("\n--- Installation Summary ---");
805
+ console.log(`Agent: ${pathResolver.getAgentConfig(agent).name}`);
806
+ console.log(`Scope: ${scope}`);
807
+ console.log(`Resources (${resources.length}):`);
808
+ resources.forEach((r) => {
809
+ const targetPath = pathResolver.resolveAgentPath(agent, r.type, scope);
810
+ console.log(` - [${r.type}] ${r.name} → ${targetPath || "Not supported"}`);
811
+ });
812
+ console.log("");
813
+ const { confirmed } = await inquirer.prompt([{
814
+ type: "confirm",
815
+ name: "confirmed",
816
+ message: "Proceed with installation?",
817
+ default: true
818
+ }]);
819
+ return confirmed;
820
+ }
821
+ /**
822
+ * Handle single duplicate
823
+ */
824
+ async handleDuplicate(resourceName) {
825
+ const { action } = await inquirer.prompt([{
826
+ type: "list",
827
+ name: "action",
828
+ message: `"${resourceName}" already exists. What do you want to do?`,
829
+ choices: [
830
+ {
831
+ name: "Skip - Keep existing file",
832
+ value: "skip"
833
+ },
834
+ {
835
+ name: "Overwrite - Replace with new version",
836
+ value: "overwrite"
837
+ },
838
+ {
839
+ name: "Rename - Save as new (e.g., skill-2)",
840
+ value: "rename"
841
+ },
842
+ {
843
+ name: "Backup - Backup existing and overwrite",
844
+ value: "backup"
845
+ },
846
+ {
847
+ name: "Compare - View differences first",
848
+ value: "compare"
849
+ }
850
+ ]
851
+ }]);
852
+ return action;
853
+ }
854
+ /**
855
+ * Handle batch duplicates
856
+ */
857
+ async handleBatchDuplicates(duplicateCount) {
858
+ const { action } = await inquirer.prompt([{
859
+ type: "list",
860
+ name: "action",
861
+ message: `${duplicateCount} files already exist. How do you want to handle them?`,
862
+ choices: [
863
+ {
864
+ name: "Ask for each file",
865
+ value: "ask-each"
866
+ },
867
+ {
868
+ name: "Skip all - Keep all existing files",
869
+ value: "skip-all"
870
+ },
871
+ {
872
+ name: "Overwrite all - Replace all with new versions",
873
+ value: "overwrite-all"
874
+ },
875
+ {
876
+ name: "Backup all - Backup existing and install new",
877
+ value: "backup-all"
878
+ }
879
+ ]
880
+ }]);
881
+ return action;
882
+ }
883
+ };
884
+ const interactivePrompt = new InteractivePrompt();
885
+
886
+ //#endregion
887
+ //#region src/utils/fs-safe.ts
888
+ /**
889
+ * Atomic file write
890
+ * Write to temp file first, then rename (atomic operation)
891
+ */
892
+ async function atomicWrite(filePath, content) {
893
+ const dir = dirname(filePath);
894
+ const tempFile = join$1(dir, `.${randomBytes(8).toString("hex")}.tmp`);
895
+ try {
896
+ await mkdir(dir, { recursive: true });
897
+ await writeFile(tempFile, content, "utf-8");
898
+ await rename(tempFile, filePath);
899
+ } catch (error) {
900
+ try {
901
+ await unlink(tempFile);
902
+ } catch {}
903
+ throw error;
904
+ }
905
+ }
906
+
907
+ //#endregion
908
+ //#region src/utils/hash.ts
909
+ /**
910
+ * Calculate SHA-256 hash of content
911
+ */
912
+ function calculateHash(content) {
913
+ return createHash("sha256").update(content, "utf-8").digest("hex");
914
+ }
915
+ /**
916
+ * Check if two contents are identical
917
+ */
918
+ function isSameContent(content1, content2) {
919
+ return calculateHash(content1) === calculateHash(content2);
920
+ }
921
+
922
+ //#endregion
923
+ //#region src/utils/diff.ts
924
+ /**
925
+ * Generate unified diff between two contents
926
+ */
927
+ function generateDiff(oldContent, newContent, filename = "file") {
928
+ return createTwoFilesPatch(`${filename} (existing)`, `${filename} (new)`, oldContent, newContent, "", "");
929
+ }
930
+ /**
931
+ * Format diff with colors
932
+ */
933
+ function formatDiff(diffText) {
934
+ return diffText.split("\n").map((line) => {
935
+ if (line.startsWith("+") && !line.startsWith("+++")) return chalk.green(line);
936
+ if (line.startsWith("-") && !line.startsWith("---")) return chalk.red(line);
937
+ if (line.startsWith("@@")) return chalk.cyan(line);
938
+ return line;
939
+ }).join("\n");
940
+ }
941
+ /**
942
+ * Display diff to console
943
+ */
944
+ function displayDiff(oldContent, newContent, filename) {
945
+ const formatted = formatDiff(generateDiff(oldContent, newContent, filename));
946
+ console.log("\n" + formatted + "\n");
947
+ }
948
+
949
+ //#endregion
950
+ //#region src/install/DuplicateHandler.ts
951
+ /**
952
+ * DuplicateHandler
953
+ *
954
+ * Handles various duplicate resolution strategies for resource installation:
955
+ * - skip: Do nothing, keep existing file
956
+ * - overwrite: Replace existing file with new content
957
+ * - rename: Create new file with incremented number (skill-2, skill-3)
958
+ * - backup: Create .backup file before overwriting
959
+ */
960
+ var DuplicateHandler = class {
961
+ /**
962
+ * Handle rename - Find next available number
963
+ * Examples: skill-2, skill-3, skill-4
964
+ *
965
+ * @param targetPath - The original target path (e.g., /path/to/my-skill/SKILL.md)
966
+ * @param content - The new content to write
967
+ * @returns The new path where the file was written
968
+ */
969
+ async rename(targetPath, content) {
970
+ const dir = dirname(targetPath);
971
+ const filename = basename(targetPath);
972
+ const baseName = basename(dir);
973
+ const parentDir = dirname(dir);
974
+ let counter = 2;
975
+ let newPath;
976
+ while (true) {
977
+ const newDirName = `${baseName}-${counter}`;
978
+ newPath = join$1(parentDir, newDirName, filename);
979
+ if (!existsSync(join$1(parentDir, newDirName))) break;
980
+ counter++;
981
+ }
982
+ await atomicWrite(newPath, content);
983
+ return newPath;
984
+ }
985
+ /**
986
+ * Handle backup - Create .backup file and overwrite
987
+ *
988
+ * If .backup already exists, creates numbered backups: .backup.1, .backup.2, etc.
989
+ *
990
+ * @param targetPath - The target path to backup and overwrite
991
+ * @param content - The new content to write
992
+ * @returns The backup path where the original content was saved
993
+ */
994
+ async backup(targetPath, content) {
995
+ const baseBackupPath = `${targetPath}.backup`;
996
+ let backupPath;
997
+ if (existsSync(baseBackupPath)) {
998
+ let counter = 1;
999
+ while (true) {
1000
+ backupPath = `${baseBackupPath}.${counter}`;
1001
+ if (!existsSync(backupPath)) break;
1002
+ counter++;
1003
+ }
1004
+ } else backupPath = baseBackupPath;
1005
+ await copyFile(targetPath, backupPath);
1006
+ await atomicWrite(targetPath, content);
1007
+ return backupPath;
1008
+ }
1009
+ /**
1010
+ * Skip - Do nothing
1011
+ */
1012
+ async skip() {}
1013
+ /**
1014
+ * Overwrite - Replace file
1015
+ *
1016
+ * @param targetPath - The target path to overwrite
1017
+ * @param content - The new content to write
1018
+ */
1019
+ async overwrite(targetPath, content) {
1020
+ await atomicWrite(targetPath, content);
1021
+ }
1022
+ /**
1023
+ * Handle compare - Show diff and let user choose action
1024
+ *
1025
+ * Displays unified diff between existing and new content,
1026
+ * then prompts user to choose skip, overwrite, or backup.
1027
+ *
1028
+ * @param targetPath - The target file path
1029
+ * @param existingContent - Current content of the file
1030
+ * @param newContent - New content to potentially install
1031
+ * @param resourceName - Name of the resource for display
1032
+ * @returns The chosen action: 'skip', 'overwrite', or 'backup'
1033
+ */
1034
+ async compare(targetPath, existingContent, newContent, resourceName) {
1035
+ console.log(`\nComparing "${resourceName}":`);
1036
+ displayDiff(existingContent, newContent, resourceName);
1037
+ const { action } = await inquirer.prompt([{
1038
+ type: "list",
1039
+ name: "action",
1040
+ message: "What do you want to do after seeing the diff?",
1041
+ choices: [
1042
+ {
1043
+ name: "Skip - Keep existing",
1044
+ value: "skip"
1045
+ },
1046
+ {
1047
+ name: "Overwrite - Use new version",
1048
+ value: "overwrite"
1049
+ },
1050
+ {
1051
+ name: "Backup - Backup and overwrite",
1052
+ value: "backup"
1053
+ }
1054
+ ]
1055
+ }]);
1056
+ return action;
1057
+ }
1058
+ /**
1059
+ * Prompt user for single duplicate action
1060
+ *
1061
+ * Displays interactive menu for handling a single duplicate file.
1062
+ *
1063
+ * @param resourceName - Name of the resource
1064
+ * @param existingPath - Path where file already exists
1065
+ * @returns The chosen duplicate action
1066
+ */
1067
+ async promptForAction(resourceName, existingPath) {
1068
+ const { action } = await inquirer.prompt([{
1069
+ type: "list",
1070
+ name: "action",
1071
+ message: `File "${resourceName}" already exists at ${existingPath}. What do you want to do?`,
1072
+ choices: [
1073
+ {
1074
+ name: "Skip - Keep existing file",
1075
+ value: "skip"
1076
+ },
1077
+ {
1078
+ name: "Overwrite - Replace with new version",
1079
+ value: "overwrite"
1080
+ },
1081
+ {
1082
+ name: "Rename - Save new version with different name",
1083
+ value: "rename"
1084
+ },
1085
+ {
1086
+ name: "Backup - Backup existing and install new",
1087
+ value: "backup"
1088
+ },
1089
+ {
1090
+ name: "Compare - Show differences first",
1091
+ value: "compare"
1092
+ }
1093
+ ]
1094
+ }]);
1095
+ return action;
1096
+ }
1097
+ };
1098
+
1099
+ //#endregion
1100
+ //#region src/install/InstallManager.ts
1101
+ /**
1102
+ * InstallManager
1103
+ *
1104
+ * Handles resource installation with duplicate detection and handling strategies:
1105
+ * - Skip: Keep existing file
1106
+ * - Overwrite: Replace existing file
1107
+ * - Rename: Create new file with incremented number (skill-2, skill-3)
1108
+ * - Backup: Create .backup file before overwriting
1109
+ * - Auto-skip: Automatically skip if content is identical
1110
+ */
1111
+ var InstallManager = class {
1112
+ pathResolver;
1113
+ duplicateHandler;
1114
+ constructor() {
1115
+ this.pathResolver = new PathResolver();
1116
+ this.duplicateHandler = new DuplicateHandler();
1117
+ }
1118
+ /**
1119
+ * Install multiple resources
1120
+ */
1121
+ async install(requests) {
1122
+ const results = [];
1123
+ for (const request of requests) try {
1124
+ const result = await this.installOne(request);
1125
+ results.push(result);
1126
+ } catch (error) {
1127
+ results.push({
1128
+ resourceName: request.resource.name,
1129
+ agent: request.agent,
1130
+ success: false,
1131
+ action: "failed",
1132
+ path: "",
1133
+ error: error.message
1134
+ });
1135
+ }
1136
+ return results;
1137
+ }
1138
+ /**
1139
+ * Install single resource
1140
+ */
1141
+ async installOne(request) {
1142
+ const targetPath = this.resolveTargetPath(request.resource, request.agent, request.scope);
1143
+ const duplicate = await this.checkDuplicate(targetPath, request.resource.content);
1144
+ if (duplicate && duplicate.isSameContent) return {
1145
+ resourceName: request.resource.name,
1146
+ agent: request.agent,
1147
+ success: true,
1148
+ action: "skipped",
1149
+ path: targetPath
1150
+ };
1151
+ if (duplicate) return await this.handleDuplicate(request, duplicate, targetPath);
1152
+ await atomicWrite(targetPath, request.resource.content);
1153
+ if (request.resource.directory?.files) {
1154
+ const targetDir = dirname(targetPath);
1155
+ await this.copySiblingFiles(request.resource.directory.files, targetDir);
1156
+ }
1157
+ return {
1158
+ resourceName: request.resource.name,
1159
+ agent: request.agent,
1160
+ success: true,
1161
+ action: "created",
1162
+ path: targetPath
1163
+ };
1164
+ }
1165
+ /**
1166
+ * Copy sibling files to target directory
1167
+ */
1168
+ async copySiblingFiles(files, targetDir) {
1169
+ for (const file of files) await atomicWrite(join$1(targetDir, file.path), file.content);
1170
+ }
1171
+ /**
1172
+ * Resolve target installation path
1173
+ */
1174
+ resolveTargetPath(resource, agent, scope) {
1175
+ const basePath = this.pathResolver.resolveAgentPath(agent, resource.type, scope);
1176
+ const filename = basename(resource.path);
1177
+ return join$1(basePath, resource.name, filename);
1178
+ }
1179
+ /**
1180
+ * Check if file exists and get duplicate info
1181
+ */
1182
+ async checkDuplicate(path, newContent) {
1183
+ if (!existsSync(path)) return null;
1184
+ try {
1185
+ const existingContent = await readFile(path, "utf-8");
1186
+ return {
1187
+ resourceName: path.split("/").slice(-2, -1)[0],
1188
+ path,
1189
+ existingContent,
1190
+ newContent,
1191
+ isSameContent: isSameContent(existingContent, newContent)
1192
+ };
1193
+ } catch (error) {
1194
+ return null;
1195
+ }
1196
+ }
1197
+ /**
1198
+ * Handle duplicate file
1199
+ */
1200
+ async handleDuplicate(request, duplicate, targetPath) {
1201
+ const { onDuplicate } = request;
1202
+ switch (onDuplicate) {
1203
+ case "skip":
1204
+ await this.duplicateHandler.skip();
1205
+ return {
1206
+ resourceName: request.resource.name,
1207
+ agent: request.agent,
1208
+ success: true,
1209
+ action: "skipped",
1210
+ path: targetPath
1211
+ };
1212
+ case "overwrite":
1213
+ await this.duplicateHandler.overwrite(targetPath, request.resource.content);
1214
+ return {
1215
+ resourceName: request.resource.name,
1216
+ agent: request.agent,
1217
+ success: true,
1218
+ action: "overwritten",
1219
+ path: targetPath
1220
+ };
1221
+ case "rename": {
1222
+ const newPath = await this.duplicateHandler.rename(targetPath, request.resource.content);
1223
+ return {
1224
+ resourceName: request.resource.name,
1225
+ agent: request.agent,
1226
+ success: true,
1227
+ action: "renamed",
1228
+ path: newPath,
1229
+ renamedTo: newPath
1230
+ };
1231
+ }
1232
+ case "backup": {
1233
+ const backupPath = await this.duplicateHandler.backup(targetPath, request.resource.content);
1234
+ return {
1235
+ resourceName: request.resource.name,
1236
+ agent: request.agent,
1237
+ success: true,
1238
+ action: "backed-up",
1239
+ path: targetPath,
1240
+ backupPath
1241
+ };
1242
+ }
1243
+ case "fail": throw new Error(`File already exists: ${targetPath}`);
1244
+ case "compare": {
1245
+ const action = await this.duplicateHandler.compare(targetPath, duplicate.existingContent, request.resource.content, request.resource.name);
1246
+ const newRequest = {
1247
+ ...request,
1248
+ onDuplicate: action
1249
+ };
1250
+ return await this.handleDuplicate(newRequest, duplicate, targetPath);
1251
+ }
1252
+ default: throw new Error(`Unknown duplicate action: ${onDuplicate}`);
1253
+ }
1254
+ }
1255
+ };
1256
+
1257
+ //#endregion
1258
+ //#region src/utils/Logger.ts
1259
+ /**
1260
+ * Logger class for CLI output with progress spinner and colored messages
1261
+ *
1262
+ * Provides:
1263
+ * - Progress spinner (ora)
1264
+ * - Colored log messages (chalk)
1265
+ * - Installation results summary
1266
+ * - Welcome/completion messages
1267
+ */
1268
+ var Logger = class {
1269
+ spinner = null;
1270
+ /**
1271
+ * Start progress spinner
1272
+ */
1273
+ startProgress(message) {
1274
+ this.spinner = ora(message).start();
1275
+ }
1276
+ /**
1277
+ * Update progress message
1278
+ */
1279
+ updateProgress(message) {
1280
+ if (this.spinner) this.spinner.text = message;
1281
+ }
1282
+ /**
1283
+ * Stop progress with success
1284
+ */
1285
+ succeedProgress(message) {
1286
+ if (this.spinner) {
1287
+ this.spinner.succeed(message);
1288
+ this.spinner = null;
1289
+ }
1290
+ }
1291
+ /**
1292
+ * Stop progress with failure
1293
+ */
1294
+ failProgress(message) {
1295
+ if (this.spinner) {
1296
+ this.spinner.fail(message);
1297
+ this.spinner = null;
1298
+ }
1299
+ }
1300
+ /**
1301
+ * Stop progress with warning
1302
+ */
1303
+ warnProgress(message) {
1304
+ if (this.spinner) {
1305
+ this.spinner.warn(message);
1306
+ this.spinner = null;
1307
+ }
1308
+ }
1309
+ /**
1310
+ * Stop progress (generic)
1311
+ */
1312
+ stopProgress() {
1313
+ if (this.spinner) {
1314
+ this.spinner.stop();
1315
+ this.spinner = null;
1316
+ }
1317
+ }
1318
+ /**
1319
+ * Log info message
1320
+ */
1321
+ info(message) {
1322
+ console.log(chalk.blue("i"), message);
1323
+ }
1324
+ /**
1325
+ * Log success message
1326
+ */
1327
+ success(message) {
1328
+ console.log(chalk.green("v"), message);
1329
+ }
1330
+ /**
1331
+ * Log warning message
1332
+ */
1333
+ warn(message) {
1334
+ console.log(chalk.yellow("!"), message);
1335
+ }
1336
+ /**
1337
+ * Log error message
1338
+ */
1339
+ error(message) {
1340
+ console.log(chalk.red("x"), message);
1341
+ }
1342
+ /**
1343
+ * Display installation results summary
1344
+ */
1345
+ displayResults(results) {
1346
+ console.log("\n" + chalk.bold("Installation Results:") + "\n");
1347
+ const summary = this.summarizeResults(results);
1348
+ if (summary.created > 0) console.log(chalk.green(` v Created: ${summary.created}`));
1349
+ if (summary.skipped > 0) console.log(chalk.gray(` - Skipped: ${summary.skipped}`));
1350
+ if (summary.overwritten > 0) console.log(chalk.yellow(` ~ Overwritten: ${summary.overwritten}`));
1351
+ if (summary.renamed > 0) console.log(chalk.cyan(` > Renamed: ${summary.renamed}`));
1352
+ if (summary.backedUp > 0) console.log(chalk.magenta(` ^ Backed up: ${summary.backedUp}`));
1353
+ if (summary.failed > 0) console.log(chalk.red(` x Failed: ${summary.failed}`));
1354
+ console.log(chalk.bold(`\n Total: ${results.length}`));
1355
+ const failures = results.filter((r) => !r.success);
1356
+ if (failures.length > 0) {
1357
+ console.log("\n" + chalk.red.bold("Failures:"));
1358
+ failures.forEach((f) => {
1359
+ console.log(chalk.red(` - ${f.resourceName} (${f.agent}): ${f.error}`));
1360
+ });
1361
+ }
1362
+ if (results.length > 0) {
1363
+ console.log("\n" + chalk.bold("Details:"));
1364
+ results.forEach((r) => {
1365
+ const icon = this.getActionIcon(r.action);
1366
+ const color = this.getActionColor(r.action);
1367
+ const pathInfo = r.renamedTo || r.path;
1368
+ console.log(color(` ${icon} ${r.resourceName} -> ${pathInfo}`));
1369
+ if (r.backupPath) console.log(chalk.gray(` (backup: ${r.backupPath})`));
1370
+ });
1371
+ }
1372
+ console.log("");
1373
+ }
1374
+ /**
1375
+ * Summarize results by action type
1376
+ */
1377
+ summarizeResults(results) {
1378
+ return {
1379
+ created: results.filter((r) => r.action === "created").length,
1380
+ skipped: results.filter((r) => r.action === "skipped").length,
1381
+ overwritten: results.filter((r) => r.action === "overwritten").length,
1382
+ renamed: results.filter((r) => r.action === "renamed").length,
1383
+ backedUp: results.filter((r) => r.action === "backed-up").length,
1384
+ failed: results.filter((r) => r.action === "failed").length
1385
+ };
1386
+ }
1387
+ /**
1388
+ * Get icon for action
1389
+ */
1390
+ getActionIcon(action) {
1391
+ return {
1392
+ created: "v",
1393
+ skipped: "-",
1394
+ overwritten: "~",
1395
+ renamed: ">",
1396
+ "backed-up": "^",
1397
+ failed: "x"
1398
+ }[action] || "*";
1399
+ }
1400
+ /**
1401
+ * Get color function for action
1402
+ */
1403
+ getActionColor(action) {
1404
+ return {
1405
+ created: chalk.green,
1406
+ skipped: chalk.gray,
1407
+ overwritten: chalk.yellow,
1408
+ renamed: chalk.cyan,
1409
+ "backed-up": chalk.magenta,
1410
+ failed: chalk.red
1411
+ }[action] || chalk.white;
1412
+ }
1413
+ /**
1414
+ * Display welcome message
1415
+ */
1416
+ displayWelcome() {
1417
+ console.log(chalk.bold.cyan("\nAI Toolkit - Universal Resource Installer\n"));
1418
+ }
1419
+ /**
1420
+ * Display completion message
1421
+ */
1422
+ displayCompletion() {
1423
+ console.log(chalk.green.bold("\nInstallation complete!\n"));
1424
+ }
1425
+ };
1426
+
1427
+ //#endregion
1428
+ //#region src/commands/CommandHandler.ts
1429
+ /**
1430
+ * CommandHandler - 외부 소스 기반 설치
1431
+ *
1432
+ * 지원 소스 포맷:
1433
+ * - GitHub shorthand: owner/repo
1434
+ * - GitHub URL: https://github.com/owner/repo
1435
+ * - GitHub URL with path: https://github.com/owner/repo/tree/main/skills/frontend-design
1436
+ * - GitLab URL: https://gitlab.com/owner/repo
1437
+ * - Git URL: git@github.com:owner/repo.git
1438
+ * - Direct URL: https://raw.githubusercontent.com/.../SKILL.md
1439
+ */
1440
+ var CommandHandler = class {
1441
+ installManager;
1442
+ logger;
1443
+ constructor() {
1444
+ this.installManager = new InstallManager();
1445
+ this.logger = new Logger();
1446
+ }
1447
+ /**
1448
+ * CLI 실행 진입점
1449
+ */
1450
+ async run(options = {}) {
1451
+ try {
1452
+ this.logger.displayWelcome();
1453
+ if (options.source) await this.runWithSource(options);
1454
+ else await this.runInteractive(options);
1455
+ this.logger.displayCompletion();
1456
+ } catch (error) {
1457
+ if (error instanceof Error && error.message === "Installation cancelled") {
1458
+ this.logger.info("Installation cancelled");
1459
+ return;
1460
+ }
1461
+ this.logger.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
1462
+ throw error;
1463
+ }
1464
+ }
1465
+ /**
1466
+ * 소스가 제공된 경우 실행
1467
+ */
1468
+ async runWithSource(options) {
1469
+ const parsed = parseSource(options.source);
1470
+ this.logger.info(`Source: ${getSourceDisplayName(parsed)}`);
1471
+ const agent = options.agent || "claude-code";
1472
+ const scope = options.scope || "project";
1473
+ const types = pathResolver.getSupportedTypes(agent);
1474
+ this.logger.startProgress("Fetching resources from source...");
1475
+ try {
1476
+ let resources;
1477
+ if (parsed.type === "github") resources = await githubFetcher.fetchResources(parsed, types);
1478
+ else {
1479
+ this.logger.failProgress(`Source type "${parsed.type}" is not yet supported.`);
1480
+ return;
1481
+ }
1482
+ if (resources.length === 0) {
1483
+ this.logger.warnProgress("No resources found in source.");
1484
+ return;
1485
+ }
1486
+ this.logger.succeedProgress(`Found ${resources.length} resource(s)`);
1487
+ let selectedResources = resources;
1488
+ if (!options.yes) {
1489
+ selectedResources = await interactivePrompt.selectResources(resources, types);
1490
+ if (selectedResources.length === 0) {
1491
+ this.logger.info("No resources selected.");
1492
+ return;
1493
+ }
1494
+ }
1495
+ const installRequests = selectedResources.map((resource) => ({
1496
+ resource,
1497
+ agent,
1498
+ scope,
1499
+ onDuplicate: "compare"
1500
+ }));
1501
+ this.logger.startProgress(`Installing ${installRequests.length} resource(s)...`);
1502
+ const results = await this.installManager.install(installRequests);
1503
+ this.logger.succeedProgress("Installation complete");
1504
+ this.printResults(results);
1505
+ } catch (error) {
1506
+ this.logger.failProgress(`Failed to fetch resources: ${error instanceof Error ? error.message : String(error)}`);
1507
+ throw error;
1508
+ }
1509
+ }
1510
+ /**
1511
+ * 인터랙티브 모드 실행
1512
+ */
1513
+ async runInteractive(options) {
1514
+ const result = await interactivePrompt.run();
1515
+ if (result.resources.length === 0) {
1516
+ this.logger.info("No resources selected.");
1517
+ return;
1518
+ }
1519
+ const installRequests = result.resources.map((resource) => ({
1520
+ resource,
1521
+ agent: result.agent,
1522
+ scope: result.scope,
1523
+ onDuplicate: "compare"
1524
+ }));
1525
+ this.logger.startProgress(`Installing ${installRequests.length} resource(s)...`);
1526
+ const results = await this.installManager.install(installRequests);
1527
+ this.logger.succeedProgress("Installation complete");
1528
+ this.printResults(results);
1529
+ }
1530
+ /**
1531
+ * 파싱된 소스 정보 로깅 (디버깅용)
1532
+ */
1533
+ logParsedSource(parsed) {
1534
+ console.log("\nParsed source details:");
1535
+ console.log(JSON.stringify(parsed, null, 2));
1536
+ }
1537
+ /**
1538
+ * 설치 결과 출력
1539
+ */
1540
+ printResults(results) {
1541
+ this.logger.displayResults(results);
1542
+ }
1543
+ };
1544
+ const commandHandler = new CommandHandler();
1545
+
1546
+ //#endregion
1547
+ //#region src/prompts/ZipPrompt.ts
1548
+ /**
1549
+ * ZipPrompt - ZIP 내보내기용 선택 플로우
1550
+ *
1551
+ * 플로우: Types → Resources → Confirm
1552
+ */
1553
+ var ZipPrompt = class {
1554
+ /**
1555
+ * ZIP 플로우 실행
1556
+ */
1557
+ async run(availableResources = []) {
1558
+ console.log("AI Toolkit - ZIP Export Mode\n");
1559
+ const types = await this.selectTypes();
1560
+ const resources = await this.selectResources(availableResources, types);
1561
+ if (!await this.confirmExport(resources)) throw new Error("Export cancelled");
1562
+ return {
1563
+ types,
1564
+ resources
1565
+ };
1566
+ }
1567
+ /**
1568
+ * 타입 복수 선택
1569
+ */
1570
+ async selectTypes() {
1571
+ const allTypes = [
1572
+ "skills",
1573
+ "rules",
1574
+ "agents"
1575
+ ];
1576
+ const descriptions = {
1577
+ skills: "Skills - Reusable prompts and instructions",
1578
+ rules: "Rules - Project guidelines and standards",
1579
+ agents: "Agents - Specialized agent configurations"
1580
+ };
1581
+ const { selected } = await inquirer.prompt([{
1582
+ type: "checkbox",
1583
+ name: "selected",
1584
+ message: "Select resource types to export:",
1585
+ choices: allTypes.map((type) => ({
1586
+ name: descriptions[type],
1587
+ value: type,
1588
+ checked: type === "skills"
1589
+ })),
1590
+ validate: (input) => {
1591
+ if (input.length === 0) return "Please select at least one type";
1592
+ return true;
1593
+ }
1594
+ }]);
1595
+ return selected;
1596
+ }
1597
+ /**
1598
+ * 리소스 복수 선택
1599
+ */
1600
+ async selectResources(availableResources, types) {
1601
+ const filteredResources = availableResources.filter((r) => types.includes(r.type));
1602
+ if (filteredResources.length === 0) {
1603
+ console.log("\nNo resources found for selected types.");
1604
+ return [];
1605
+ }
1606
+ const { selected } = await inquirer.prompt([{
1607
+ type: "checkbox",
1608
+ name: "selected",
1609
+ message: "Select resources to export:",
1610
+ choices: filteredResources.map((r) => ({
1611
+ name: `[${r.type}] ${r.name} - ${r.description || "No description"}`,
1612
+ value: r
1613
+ })),
1614
+ validate: (input) => {
1615
+ if (input.length === 0) return "Please select at least one resource";
1616
+ return true;
1617
+ }
1618
+ }]);
1619
+ return selected;
1620
+ }
1621
+ /**
1622
+ * 내보내기 확인
1623
+ */
1624
+ async confirmExport(resources) {
1625
+ console.log("\n--- Export Summary ---");
1626
+ console.log(`Resources (${resources.length}):`);
1627
+ resources.forEach((r) => {
1628
+ console.log(` - [${r.type}] ${r.name}`);
1629
+ });
1630
+ console.log("");
1631
+ const { confirmed } = await inquirer.prompt([{
1632
+ type: "confirm",
1633
+ name: "confirmed",
1634
+ message: "Proceed with export?",
1635
+ default: true
1636
+ }]);
1637
+ return confirmed;
1638
+ }
1639
+ };
1640
+ const zipPrompt = new ZipPrompt();
1641
+
1642
+ //#endregion
1643
+ //#region src/export/ZipExporter.ts
1644
+ /**
1645
+ * ZipExporter - 리소스를 ZIP으로 내보내기
1646
+ *
1647
+ * ZIP 구조:
1648
+ * frontend/skills/my-skill/
1649
+ * SKILL.md
1650
+ * scripts/
1651
+ * references/
1652
+ */
1653
+ var ZipExporter = class {
1654
+ /**
1655
+ * 리소스를 ZIP으로 내보내기
1656
+ */
1657
+ async export(resources, outputPath) {
1658
+ return new Promise((resolve, reject) => {
1659
+ const output = fs.createWriteStream(outputPath);
1660
+ const archive = archiver("zip", { zlib: { level: 9 } });
1661
+ output.on("close", () => {
1662
+ resolve({
1663
+ success: true,
1664
+ outputPath,
1665
+ resourceCount: resources.length
1666
+ });
1667
+ });
1668
+ archive.on("error", (err) => {
1669
+ reject(err);
1670
+ });
1671
+ archive.pipe(output);
1672
+ for (const resource of resources) {
1673
+ const basePath = this.getResourceBasePath(resource);
1674
+ const mainFileName = this.getMainFileName(resource.type);
1675
+ archive.append(resource.content, { name: path.posix.join(basePath, mainFileName) });
1676
+ if (resource.directory?.files) {
1677
+ for (const file of resource.directory.files) if (!file.isDirectory) archive.append(file.content, { name: path.posix.join(basePath, file.path) });
1678
+ }
1679
+ }
1680
+ archive.finalize();
1681
+ });
1682
+ }
1683
+ /**
1684
+ * 리소스의 기본 경로 추출
1685
+ * 예: /path/to/registry/frontend/skills/my-skill → frontend/skills/my-skill
1686
+ */
1687
+ getResourceBasePath(resource) {
1688
+ const parts = resource.path.split(path.sep);
1689
+ const resourcesIndex = parts.findIndex((p) => p === "resources");
1690
+ if (resourcesIndex !== -1) return parts.slice(resourcesIndex + 1).join("/");
1691
+ return `${resource.type}/${resource.name}`;
1692
+ }
1693
+ /**
1694
+ * 리소스 타입별 메인 파일명
1695
+ */
1696
+ getMainFileName(type) {
1697
+ return {
1698
+ skills: "SKILL.md",
1699
+ rules: "RULES.md",
1700
+ commands: "COMMANDS.md",
1701
+ agents: "AGENT.md"
1702
+ }[type];
1703
+ }
1704
+ };
1705
+ const zipExporter = new ZipExporter();
1706
+
1707
+ //#endregion
1708
+ //#region src/commands/ZipHandler.ts
1709
+ /**
1710
+ * ZipHandler - ZIP 내보내기 워크플로우 오케스트레이션
1711
+ */
1712
+ var ZipHandler = class {
1713
+ logger;
1714
+ constructor() {
1715
+ this.logger = new Logger();
1716
+ }
1717
+ /**
1718
+ * ZIP 내보내기 실행
1719
+ */
1720
+ async run(options = {}) {
1721
+ try {
1722
+ console.log("AI Toolkit - ZIP Export Mode\n");
1723
+ const source = options.source || await this.promptSource();
1724
+ if (!source) {
1725
+ this.logger.warn("No source provided. Exiting.");
1726
+ return;
1727
+ }
1728
+ const parsed = parseSource(source);
1729
+ this.logger.info(`Source: ${getSourceDisplayName(parsed)}`);
1730
+ this.logger.startProgress("Fetching resources from source...");
1731
+ const types = [
1732
+ "skills",
1733
+ "rules",
1734
+ "agents"
1735
+ ];
1736
+ let resources;
1737
+ if (parsed.type === "github") resources = await githubFetcher.fetchResources(parsed, types);
1738
+ else {
1739
+ this.logger.failProgress(`Source type "${parsed.type}" is not yet supported.`);
1740
+ return;
1741
+ }
1742
+ if (resources.length === 0) {
1743
+ this.logger.warnProgress("No resources found in source.");
1744
+ return;
1745
+ }
1746
+ this.logger.succeedProgress(`Found ${resources.length} resource(s)`);
1747
+ let selectedResources = resources;
1748
+ if (!options.yes) {
1749
+ selectedResources = (await zipPrompt.run(resources)).resources;
1750
+ if (selectedResources.length === 0) {
1751
+ this.logger.info("No resources selected.");
1752
+ return;
1753
+ }
1754
+ }
1755
+ const outputPath = this.generateOutputPath();
1756
+ this.logger.startProgress("Creating ZIP...");
1757
+ const zipResult = await zipExporter.export(selectedResources, outputPath);
1758
+ if (zipResult.success) {
1759
+ this.logger.succeedProgress("ZIP created successfully");
1760
+ this.printResult(zipResult);
1761
+ } else {
1762
+ this.logger.failProgress("ZIP creation failed");
1763
+ if (zipResult.error) this.logger.error(zipResult.error);
1764
+ }
1765
+ } catch (error) {
1766
+ if (error instanceof Error && error.message === "Export cancelled") {
1767
+ this.logger.info("Export cancelled");
1768
+ return;
1769
+ }
1770
+ this.logger.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
1771
+ throw error;
1772
+ }
1773
+ }
1774
+ /**
1775
+ * 소스 입력 프롬프트
1776
+ */
1777
+ async promptSource() {
1778
+ const { source } = await inquirer.prompt([{
1779
+ type: "input",
1780
+ name: "source",
1781
+ message: "Enter source (GitHub shorthand or URL):",
1782
+ validate: (input) => {
1783
+ if (!input.trim()) return "Please enter a source";
1784
+ return true;
1785
+ }
1786
+ }]);
1787
+ return source.trim();
1788
+ }
1789
+ /**
1790
+ * 출력 파일 경로 생성
1791
+ */
1792
+ generateOutputPath() {
1793
+ return `ai-toolkit-export-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.zip`;
1794
+ }
1795
+ /**
1796
+ * 결과 출력
1797
+ */
1798
+ printResult(result) {
1799
+ console.log("");
1800
+ console.log("--- Export Complete ---");
1801
+ console.log(`File: ${result.outputPath}`);
1802
+ console.log(`Resources: ${result.resourceCount}`);
1803
+ console.log("");
1804
+ }
1805
+ };
1806
+ const zipHandler = new ZipHandler();
1807
+
1808
+ //#endregion
1809
+ //#region src/install/BatchHandler.ts
1810
+ /**
1811
+ * BatchHandler
1812
+ *
1813
+ * Handles batch operations for multiple install requests:
1814
+ * - Apply batch actions to all requests
1815
+ * - Summarize installation results
1816
+ */
1817
+ var BatchHandler = class {
1818
+ /**
1819
+ * Apply batch action to all requests
1820
+ * Converts batch action to individual duplicate actions
1821
+ */
1822
+ applyBatchAction(requests, batchAction) {
1823
+ if (batchAction === "ask-each") return requests;
1824
+ const onDuplicate = {
1825
+ "skip-all": "skip",
1826
+ "overwrite-all": "overwrite",
1827
+ "backup-all": "backup"
1828
+ }[batchAction];
1829
+ return requests.map((req) => ({
1830
+ ...req,
1831
+ onDuplicate
1832
+ }));
1833
+ }
1834
+ /**
1835
+ * Group and count results by action type
1836
+ */
1837
+ summarizeResults(results) {
1838
+ return {
1839
+ created: results.filter((r) => r.action === "created").length,
1840
+ skipped: results.filter((r) => r.action === "skipped").length,
1841
+ overwritten: results.filter((r) => r.action === "overwritten").length,
1842
+ renamed: results.filter((r) => r.action === "renamed").length,
1843
+ backedUp: results.filter((r) => r.action === "backed-up").length,
1844
+ failed: results.filter((r) => r.action === "failed").length
1845
+ };
1846
+ }
1847
+ /**
1848
+ * Format summary as human-readable string
1849
+ */
1850
+ formatSummary(summary) {
1851
+ const parts = [];
1852
+ if (summary.created > 0) parts.push(`${summary.created} created`);
1853
+ if (summary.skipped > 0) parts.push(`${summary.skipped} skipped`);
1854
+ if (summary.overwritten > 0) parts.push(`${summary.overwritten} overwritten`);
1855
+ if (summary.renamed > 0) parts.push(`${summary.renamed} renamed`);
1856
+ if (summary.backedUp > 0) parts.push(`${summary.backedUp} backed up`);
1857
+ if (summary.failed > 0) parts.push(`${summary.failed} failed`);
1858
+ if (parts.length === 0) return "No operations performed";
1859
+ return parts.join(", ");
1860
+ }
1861
+ /**
1862
+ * Check if any results have failed
1863
+ */
1864
+ hasFailures(results) {
1865
+ return results.some((r) => r.action === "failed");
1866
+ }
1867
+ /**
1868
+ * Get failed results only
1869
+ */
1870
+ getFailedResults(results) {
1871
+ return results.filter((r) => r.action === "failed");
1872
+ }
1873
+ /**
1874
+ * Get successful results only
1875
+ */
1876
+ getSuccessfulResults(results) {
1877
+ return results.filter((r) => r.success);
1878
+ }
1879
+ };
1880
+
1881
+ //#endregion
1882
+ //#region src/index.ts
1883
+ program.name("add-ai-tools").description("AI Toolkit - Install and manage AI resources from various sources").argument("[source]", "Source to install from (GitHub shorthand or URL)").option("--zip", "Export resources as ZIP file").option("--agent <agent>", "Target agent (claude-code, cursor, github-copilot, antigravity)").option("--scope <scope>", "Installation scope (project, global)", "project").option("-y, --yes", "Skip confirmation prompts").parse();
1884
+ const options = program.opts();
1885
+ const args = program.args;
1886
+ async function main() {
1887
+ const source = args[0];
1888
+ if (options.zip) await zipHandler.run({
1889
+ source,
1890
+ yes: options.yes
1891
+ });
1892
+ else await commandHandler.run({
1893
+ source,
1894
+ agent: options.agent,
1895
+ scope: options.scope,
1896
+ yes: options.yes
1897
+ });
1898
+ }
1899
+ if (import.meta.url === `file://${process.argv[1]}`) main().catch((error) => {
1900
+ console.error("Error:", error instanceof Error ? error.message : String(error));
1901
+ process.exit(1);
1902
+ });
1903
+
1904
+ //#endregion
1905
+ export { BatchHandler, CommandHandler, DuplicateHandler, InstallManager, PathResolver, ZipExporter, ZipHandler, agents, commandHandler, displayDiff, formatDiff, generateDiff, getOwnerRepo, getSourceDisplayName, isDirectResourcePath, isDirectSkillUrl, main, parseSource, pathResolver, zipExporter, zipHandler };
1906
+ //# sourceMappingURL=index.mjs.map