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/README.md +122 -0
- package/dist/index.d.mts +501 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1906 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +44 -0
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
|