@sstar/skill-install 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/LICENSE +21 -0
- package/README.md +296 -0
- package/dist/archive/archive-extractor.d.ts +29 -0
- package/dist/archive/archive-extractor.js +123 -0
- package/dist/auth/auth-service.d.ts +21 -0
- package/dist/auth/auth-service.js +260 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +459 -0
- package/dist/core/errors.d.ts +19 -0
- package/dist/core/errors.js +29 -0
- package/dist/core/logger.d.ts +14 -0
- package/dist/core/logger.js +68 -0
- package/dist/download/download-manager.d.ts +41 -0
- package/dist/download/download-manager.js +184 -0
- package/dist/downloader.d.ts +29 -0
- package/dist/downloader.js +163 -0
- package/dist/http/http-client.d.ts +25 -0
- package/dist/http/http-client.js +172 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +14 -0
- package/dist/installer/install-service.d.ts +40 -0
- package/dist/installer/install-service.js +184 -0
- package/dist/skills/skill-validator.d.ts +33 -0
- package/dist/skills/skill-validator.js +124 -0
- package/dist/skills/skills-manager.d.ts +54 -0
- package/dist/skills/skills-manager.js +127 -0
- package/dist/source/source-detector.d.ts +29 -0
- package/dist/source/source-detector.js +88 -0
- package/package.json +62 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InstallService = void 0;
|
|
4
|
+
const promises_1 = require("fs/promises");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const os_1 = require("os");
|
|
7
|
+
const logger_1 = require("../core/logger");
|
|
8
|
+
const errors_1 = require("../core/errors");
|
|
9
|
+
const source_detector_1 = require("../source/source-detector");
|
|
10
|
+
const download_manager_1 = require("../download/download-manager");
|
|
11
|
+
const archive_extractor_1 = require("../archive/archive-extractor");
|
|
12
|
+
const skill_validator_1 = require("../skills/skill-validator");
|
|
13
|
+
const skills_manager_1 = require("../skills/skills-manager");
|
|
14
|
+
class InstallService {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.logger = new logger_1.Logger('InstallService');
|
|
17
|
+
this.sourceDetector = new source_detector_1.SourceDetector();
|
|
18
|
+
this.downloadManager = new download_manager_1.DownloadManager();
|
|
19
|
+
this.extractor = new archive_extractor_1.ArchiveExtractor();
|
|
20
|
+
this.validator = new skill_validator_1.SkillValidator();
|
|
21
|
+
this.skillsManager = new skills_manager_1.SkillsManager();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Install a skill from a source (URL or local file)
|
|
25
|
+
*/
|
|
26
|
+
async install(options) {
|
|
27
|
+
const tempDir = await (0, promises_1.mkdtemp)((0, path_1.join)((0, os_1.tmpdir)(), 'skill-install-'));
|
|
28
|
+
try {
|
|
29
|
+
this.logger.info(`Installing skill from: ${options.source}`);
|
|
30
|
+
// Detect source type
|
|
31
|
+
const sourceInfo = this.sourceDetector.detect(options.source);
|
|
32
|
+
if (sourceInfo.type === 'unknown') {
|
|
33
|
+
throw new errors_1.WikiError(errors_1.ErrorType.INVALID_INPUT, `Unknown source type: ${options.source}. Please provide a valid URL or file path.`);
|
|
34
|
+
}
|
|
35
|
+
// Get the archive file path
|
|
36
|
+
let archivePath;
|
|
37
|
+
if (sourceInfo.type === 'local-file') {
|
|
38
|
+
// Use local file directly
|
|
39
|
+
archivePath = options.source;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// Download the file
|
|
43
|
+
const filename = this.downloadManager.extractFilenameFromUrl(options.source) || 'skill-archive.zip';
|
|
44
|
+
archivePath = (0, path_1.join)(tempDir, filename);
|
|
45
|
+
const downloadResult = await this.downloadManager.download({
|
|
46
|
+
url: options.source,
|
|
47
|
+
outputPath: archivePath,
|
|
48
|
+
username: options.username,
|
|
49
|
+
password: options.password,
|
|
50
|
+
baseUrl: sourceInfo.baseUrl,
|
|
51
|
+
allowSelfSigned: options.allowSelfSigned,
|
|
52
|
+
requireAuth: sourceInfo.type === 'wiki-url'
|
|
53
|
+
});
|
|
54
|
+
if (!downloadResult.success) {
|
|
55
|
+
throw new errors_1.WikiError(errors_1.ErrorType.NETWORK, `Failed to download: ${downloadResult.error || 'Unknown error'}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Extract archive
|
|
59
|
+
const extractDir = (0, path_1.join)(tempDir, 'extracted');
|
|
60
|
+
await (0, promises_1.mkdir)(extractDir, { recursive: true });
|
|
61
|
+
try {
|
|
62
|
+
await this.extractor.extract(archivePath, extractDir);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw new errors_1.WikiError(errors_1.ErrorType.EXTRACTION_FAILED, `Failed to extract archive: ${error.message}`, error);
|
|
66
|
+
}
|
|
67
|
+
// Find the skill directory (may be the root or a subdirectory)
|
|
68
|
+
const skillDir = await this.findSkillDirectory(extractDir);
|
|
69
|
+
if (!skillDir) {
|
|
70
|
+
throw new errors_1.WikiError(errors_1.ErrorType.INVALID_SKILL, 'No valid skill found in archive. A valid skill must contain SKILL.md with name and description.');
|
|
71
|
+
}
|
|
72
|
+
// Validate the skill
|
|
73
|
+
const metadata = await this.validator.validateOrThrow(skillDir);
|
|
74
|
+
// Check if already installed
|
|
75
|
+
const targetSkillsDir = this.skillsManager.getEffectiveSkillsDir(options.skillsDir);
|
|
76
|
+
const targetPath = (0, path_1.join)(targetSkillsDir, metadata.name);
|
|
77
|
+
if (!options.force && await this.skillsManager.isInstalled(metadata.name, options.skillsDir)) {
|
|
78
|
+
throw new errors_1.WikiError(errors_1.ErrorType.ALREADY_INSTALLED, `Skill "${metadata.name}" is already installed. Use --force to reinstall.`);
|
|
79
|
+
}
|
|
80
|
+
// Install to final location
|
|
81
|
+
await (0, promises_1.mkdir)(targetSkillsDir, { recursive: true });
|
|
82
|
+
// If the skill already exists and force is true, remove it first
|
|
83
|
+
if (options.force) {
|
|
84
|
+
try {
|
|
85
|
+
await (0, promises_1.rm)(targetPath, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Ignore if directory doesn't exist
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Move the skill directory to the final location
|
|
92
|
+
await this.moveDirectory(skillDir, targetPath);
|
|
93
|
+
this.logger.info(`Skill installed successfully: ${metadata.name} -> ${targetPath}`);
|
|
94
|
+
return {
|
|
95
|
+
success: true,
|
|
96
|
+
skillName: metadata.name,
|
|
97
|
+
skillPath: targetPath
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
102
|
+
this.logger.error(`Installation failed: ${errorMsg}`);
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
error: errorMsg
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
// Clean up temp directory
|
|
110
|
+
try {
|
|
111
|
+
await (0, promises_1.rm)(tempDir, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Ignore cleanup errors
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Find the skill directory in the extracted files
|
|
120
|
+
* The archive may contain a single root directory or have SKILL.md at root
|
|
121
|
+
*/
|
|
122
|
+
async findSkillDirectory(extractDir) {
|
|
123
|
+
// First, check if SKILL.md exists at the root
|
|
124
|
+
const entries = await (0, promises_1.readdir)(extractDir, { withFileTypes: true });
|
|
125
|
+
// Check for SKILL.md at root level
|
|
126
|
+
const hasSkillMdAtRoot = entries.some(e => e.name === 'SKILL.md');
|
|
127
|
+
if (hasSkillMdAtRoot) {
|
|
128
|
+
const isValid = await this.validator.validate(extractDir);
|
|
129
|
+
if (isValid.valid) {
|
|
130
|
+
return extractDir;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Check subdirectories
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
if (!entry.isDirectory()) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const subDirPath = (0, path_1.join)(extractDir, entry.name);
|
|
139
|
+
const isValid = await this.validator.validate(subDirPath);
|
|
140
|
+
if (isValid.valid) {
|
|
141
|
+
return subDirPath;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Move a directory from one location to another
|
|
148
|
+
* Handles cross-device moves by copying and then deleting
|
|
149
|
+
*/
|
|
150
|
+
async moveDirectory(source, target) {
|
|
151
|
+
try {
|
|
152
|
+
// Try rename first (fastest, works on same device)
|
|
153
|
+
await (0, promises_1.rename)(source, target);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
// If rename fails, try copy + delete
|
|
157
|
+
if (error.code === 'EXDEV') {
|
|
158
|
+
await this.copyDirectory(source, target);
|
|
159
|
+
await (0, promises_1.rm)(source, { recursive: true, force: true });
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Recursively copy a directory
|
|
168
|
+
*/
|
|
169
|
+
async copyDirectory(source, target) {
|
|
170
|
+
await (0, promises_1.mkdir)(target, { recursive: true });
|
|
171
|
+
const entries = await (0, promises_1.readdir)(source, { withFileTypes: true });
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
const sourcePath = (0, path_1.join)(source, entry.name);
|
|
174
|
+
const targetPath = (0, path_1.join)(target, entry.name);
|
|
175
|
+
if (entry.isDirectory()) {
|
|
176
|
+
await this.copyDirectory(sourcePath, targetPath);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
await (0, promises_1.copyFile)(sourcePath, targetPath);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
exports.InstallService = InstallService;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface SkillMetadata {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
}
|
|
5
|
+
export interface SkillFrontmatter {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
[key: string]: any;
|
|
9
|
+
}
|
|
10
|
+
export interface ValidationResult {
|
|
11
|
+
valid: boolean;
|
|
12
|
+
metadata?: SkillMetadata;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class SkillValidator {
|
|
16
|
+
private readonly logger;
|
|
17
|
+
/**
|
|
18
|
+
* Validate that a directory contains a valid skill
|
|
19
|
+
* @param skillPath Path to the skill directory
|
|
20
|
+
* @returns Validation result with metadata if valid
|
|
21
|
+
*/
|
|
22
|
+
validate(skillPath: string): Promise<ValidationResult>;
|
|
23
|
+
private readSkillMd;
|
|
24
|
+
private parseFrontmatter;
|
|
25
|
+
private validateFrontmatter;
|
|
26
|
+
/**
|
|
27
|
+
* Validate and throw WikiError if invalid
|
|
28
|
+
* @param skillPath Path to the skill directory
|
|
29
|
+
* @returns Skill metadata if valid
|
|
30
|
+
* @throws WikiError if validation fails
|
|
31
|
+
*/
|
|
32
|
+
validateOrThrow(skillPath: string): Promise<SkillMetadata>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SkillValidator = void 0;
|
|
4
|
+
const promises_1 = require("fs/promises");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const js_yaml_1 = require("js-yaml");
|
|
7
|
+
const logger_1 = require("../core/logger");
|
|
8
|
+
const errors_1 = require("../core/errors");
|
|
9
|
+
class SkillValidator {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.logger = new logger_1.Logger('SkillValidator');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Validate that a directory contains a valid skill
|
|
15
|
+
* @param skillPath Path to the skill directory
|
|
16
|
+
* @returns Validation result with metadata if valid
|
|
17
|
+
*/
|
|
18
|
+
async validate(skillPath) {
|
|
19
|
+
try {
|
|
20
|
+
const skillMdPath = (0, path_1.join)(skillPath, 'SKILL.md');
|
|
21
|
+
// Check if SKILL.md exists
|
|
22
|
+
const content = await this.readSkillMd(skillMdPath);
|
|
23
|
+
if (!content) {
|
|
24
|
+
this.logger.error(`SKILL.md not found at ${skillMdPath}`);
|
|
25
|
+
return {
|
|
26
|
+
valid: false,
|
|
27
|
+
error: 'SKILL.md not found'
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Parse YAML frontmatter
|
|
31
|
+
const frontmatter = this.parseFrontmatter(content);
|
|
32
|
+
if (!frontmatter) {
|
|
33
|
+
this.logger.error('No YAML frontmatter found in SKILL.md');
|
|
34
|
+
return {
|
|
35
|
+
valid: false,
|
|
36
|
+
error: 'SKILL.md must contain YAML frontmatter (wrapped in ---)'
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// Validate required fields
|
|
40
|
+
const metadata = this.validateFrontmatter(frontmatter);
|
|
41
|
+
if (!metadata) {
|
|
42
|
+
return {
|
|
43
|
+
valid: false,
|
|
44
|
+
error: 'SKILL.md frontmatter must contain "name" and "description" fields'
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
this.logger.info(`Valid skill found: ${metadata.name}`);
|
|
48
|
+
return {
|
|
49
|
+
valid: true,
|
|
50
|
+
metadata
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
55
|
+
this.logger.error(`Validation error: ${errorMsg}`);
|
|
56
|
+
return {
|
|
57
|
+
valid: false,
|
|
58
|
+
error: errorMsg
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async readSkillMd(path) {
|
|
63
|
+
try {
|
|
64
|
+
const content = await (0, promises_1.readFile)(path, 'utf-8');
|
|
65
|
+
return content;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error.code === 'ENOENT') {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
parseFrontmatter(content) {
|
|
75
|
+
// Check for YAML frontmatter wrapped in ---
|
|
76
|
+
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---/;
|
|
77
|
+
const match = content.match(frontmatterRegex);
|
|
78
|
+
if (!match || !match[1]) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const parsed = (0, js_yaml_1.load)(match[1]);
|
|
83
|
+
return parsed;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
this.logger.error(`Failed to parse YAML frontmatter: ${error}`);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
validateFrontmatter(frontmatter) {
|
|
91
|
+
if (!frontmatter.name || typeof frontmatter.name !== 'string') {
|
|
92
|
+
this.logger.error('Missing or invalid "name" field in frontmatter');
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
if (!frontmatter.description || typeof frontmatter.description !== 'string') {
|
|
96
|
+
this.logger.error('Missing or invalid "description" field in frontmatter');
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
// Sanitize name (remove special characters, spaces)
|
|
100
|
+
const sanitizedName = frontmatter.name
|
|
101
|
+
.toLowerCase()
|
|
102
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
103
|
+
.replace(/-+/g, '-')
|
|
104
|
+
.replace(/^-|-$/g, '');
|
|
105
|
+
return {
|
|
106
|
+
name: sanitizedName,
|
|
107
|
+
description: frontmatter.description
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Validate and throw WikiError if invalid
|
|
112
|
+
* @param skillPath Path to the skill directory
|
|
113
|
+
* @returns Skill metadata if valid
|
|
114
|
+
* @throws WikiError if validation fails
|
|
115
|
+
*/
|
|
116
|
+
async validateOrThrow(skillPath) {
|
|
117
|
+
const result = await this.validate(skillPath);
|
|
118
|
+
if (!result.valid) {
|
|
119
|
+
throw new errors_1.WikiError(errors_1.ErrorType.INVALID_SKILL, result.error || 'Invalid skill', result.error ? new Error(result.error) : undefined);
|
|
120
|
+
}
|
|
121
|
+
return result.metadata;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
exports.SkillValidator = SkillValidator;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type AiTool = 'claude' | 'codex';
|
|
2
|
+
export interface InstalledSkill {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
path: string;
|
|
6
|
+
tool: AiTool;
|
|
7
|
+
}
|
|
8
|
+
export declare class SkillsManager {
|
|
9
|
+
private readonly logger;
|
|
10
|
+
private readonly validator;
|
|
11
|
+
/**
|
|
12
|
+
* Get the global skills directory for the specified AI tool
|
|
13
|
+
*/
|
|
14
|
+
getGlobalSkillsDir(aiTool?: AiTool): string;
|
|
15
|
+
/**
|
|
16
|
+
* Get the local skills directory for the specified AI tool
|
|
17
|
+
*/
|
|
18
|
+
getLocalSkillsDir(aiTool?: AiTool): string;
|
|
19
|
+
/**
|
|
20
|
+
* Get both global and local directories for an AI tool
|
|
21
|
+
*/
|
|
22
|
+
getSkillsDirs(aiTool?: AiTool): {
|
|
23
|
+
global: string;
|
|
24
|
+
local: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Get the effective skills directory
|
|
28
|
+
* Returns the custom directory if provided, otherwise global directory
|
|
29
|
+
*/
|
|
30
|
+
getEffectiveSkillsDir(customDir?: string, aiTool?: AiTool): string;
|
|
31
|
+
/**
|
|
32
|
+
* List all installed skills
|
|
33
|
+
* @param skillsDir Path to skills directory (uses default if not provided)
|
|
34
|
+
* @param aiTool AI tool to use (default: 'claude')
|
|
35
|
+
* @returns Array of installed skill metadata
|
|
36
|
+
*/
|
|
37
|
+
listInstalled(skillsDir?: string, aiTool?: AiTool): Promise<InstalledSkill[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Uninstall a skill by name
|
|
40
|
+
* @param name Name of the skill to uninstall
|
|
41
|
+
* @param skillsDir Path to skills directory (uses default if not provided)
|
|
42
|
+
* @param aiTool AI tool to use (default: 'claude')
|
|
43
|
+
* @throws WikiError if skill not found or deletion fails
|
|
44
|
+
*/
|
|
45
|
+
uninstall(name: string, skillsDir?: string, aiTool?: AiTool): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Check if a skill is already installed
|
|
48
|
+
* @param name Name of the skill to check
|
|
49
|
+
* @param skillsDir Path to skills directory (uses default if not provided)
|
|
50
|
+
* @param aiTool AI tool to use (default: 'claude')
|
|
51
|
+
* @returns true if skill is installed, false otherwise
|
|
52
|
+
*/
|
|
53
|
+
isInstalled(name: string, skillsDir?: string, aiTool?: AiTool): Promise<boolean>;
|
|
54
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SkillsManager = void 0;
|
|
4
|
+
const promises_1 = require("fs/promises");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const os_1 = require("os");
|
|
7
|
+
const logger_1 = require("../core/logger");
|
|
8
|
+
const errors_1 = require("../core/errors");
|
|
9
|
+
const skill_validator_1 = require("./skill-validator");
|
|
10
|
+
class SkillsManager {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.logger = new logger_1.Logger('SkillsManager');
|
|
13
|
+
this.validator = new skill_validator_1.SkillValidator();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Get the global skills directory for the specified AI tool
|
|
17
|
+
*/
|
|
18
|
+
getGlobalSkillsDir(aiTool = 'claude') {
|
|
19
|
+
return (0, path_1.join)((0, os_1.homedir)(), `.${aiTool}`, 'skills');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get the local skills directory for the specified AI tool
|
|
23
|
+
*/
|
|
24
|
+
getLocalSkillsDir(aiTool = 'claude') {
|
|
25
|
+
return (0, path_1.join)(process.cwd(), `.${aiTool}`, 'skills');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Get both global and local directories for an AI tool
|
|
29
|
+
*/
|
|
30
|
+
getSkillsDirs(aiTool = 'claude') {
|
|
31
|
+
return {
|
|
32
|
+
global: this.getGlobalSkillsDir(aiTool),
|
|
33
|
+
local: this.getLocalSkillsDir(aiTool)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get the effective skills directory
|
|
38
|
+
* Returns the custom directory if provided, otherwise global directory
|
|
39
|
+
*/
|
|
40
|
+
getEffectiveSkillsDir(customDir, aiTool = 'claude') {
|
|
41
|
+
if (customDir) {
|
|
42
|
+
return customDir;
|
|
43
|
+
}
|
|
44
|
+
// Prefer global directory
|
|
45
|
+
return this.getGlobalSkillsDir(aiTool);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* List all installed skills
|
|
49
|
+
* @param skillsDir Path to skills directory (uses default if not provided)
|
|
50
|
+
* @param aiTool AI tool to use (default: 'claude')
|
|
51
|
+
* @returns Array of installed skill metadata
|
|
52
|
+
*/
|
|
53
|
+
async listInstalled(skillsDir, aiTool = 'claude') {
|
|
54
|
+
const dir = this.getEffectiveSkillsDir(skillsDir, aiTool);
|
|
55
|
+
const skills = [];
|
|
56
|
+
this.logger.info(`Listing skills in: ${dir}`);
|
|
57
|
+
try {
|
|
58
|
+
const entries = await (0, promises_1.readdir)(dir, { withFileTypes: true });
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (!entry.isDirectory()) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const skillPath = (0, path_1.join)(dir, entry.name);
|
|
64
|
+
try {
|
|
65
|
+
const metadata = await this.validator.validateOrThrow(skillPath);
|
|
66
|
+
skills.push({
|
|
67
|
+
name: metadata.name,
|
|
68
|
+
description: metadata.description,
|
|
69
|
+
path: skillPath,
|
|
70
|
+
tool: aiTool
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
// Skip invalid entries but log a warning
|
|
75
|
+
this.logger.warn(`Skipping invalid skill directory: ${entry.name}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return skills;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error.code === 'ENOENT') {
|
|
82
|
+
// Directory doesn't exist yet, return empty array
|
|
83
|
+
this.logger.info('Skills directory does not exist yet');
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Uninstall a skill by name
|
|
91
|
+
* @param name Name of the skill to uninstall
|
|
92
|
+
* @param skillsDir Path to skills directory (uses default if not provided)
|
|
93
|
+
* @param aiTool AI tool to use (default: 'claude')
|
|
94
|
+
* @throws WikiError if skill not found or deletion fails
|
|
95
|
+
*/
|
|
96
|
+
async uninstall(name, skillsDir, aiTool = 'claude') {
|
|
97
|
+
const dir = this.getEffectiveSkillsDir(skillsDir, aiTool);
|
|
98
|
+
const skillPath = (0, path_1.join)(dir, name);
|
|
99
|
+
this.logger.info(`Uninstalling skill: ${name} from ${skillPath}`);
|
|
100
|
+
// Check if skill exists
|
|
101
|
+
const skills = await this.listInstalled(dir, aiTool);
|
|
102
|
+
const skill = skills.find(s => s.name === name);
|
|
103
|
+
if (!skill) {
|
|
104
|
+
throw new errors_1.WikiError(errors_1.ErrorType.NOT_FOUND, `Skill "${name}" not found in ${dir}`);
|
|
105
|
+
}
|
|
106
|
+
// Delete the skill directory
|
|
107
|
+
try {
|
|
108
|
+
await (0, promises_1.rm)(skill.path, { recursive: true, force: true });
|
|
109
|
+
this.logger.info(`Successfully uninstalled skill: ${name}`);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
throw new errors_1.WikiError(errors_1.ErrorType.UNKNOWN, `Failed to delete skill directory: ${skill.path}`, error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Check if a skill is already installed
|
|
117
|
+
* @param name Name of the skill to check
|
|
118
|
+
* @param skillsDir Path to skills directory (uses default if not provided)
|
|
119
|
+
* @param aiTool AI tool to use (default: 'claude')
|
|
120
|
+
* @returns true if skill is installed, false otherwise
|
|
121
|
+
*/
|
|
122
|
+
async isInstalled(name, skillsDir, aiTool = 'claude') {
|
|
123
|
+
const skills = await this.listInstalled(skillsDir, aiTool);
|
|
124
|
+
return skills.some(s => s.name === name);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
exports.SkillsManager = SkillsManager;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type SourceType = 'local-file' | 'wiki-url' | 'public-url' | 'unknown';
|
|
2
|
+
export interface SourceInfo {
|
|
3
|
+
type: SourceType;
|
|
4
|
+
path?: string;
|
|
5
|
+
url?: string;
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class SourceDetector {
|
|
9
|
+
private readonly logger;
|
|
10
|
+
/**
|
|
11
|
+
* Detect the type of source from a string
|
|
12
|
+
* @param source URL or file path
|
|
13
|
+
* @returns SourceInfo with detected type and relevant information
|
|
14
|
+
*/
|
|
15
|
+
detect(source: string): SourceInfo;
|
|
16
|
+
/**
|
|
17
|
+
* Check if the source is a local file path
|
|
18
|
+
*/
|
|
19
|
+
private isLocalFile;
|
|
20
|
+
/**
|
|
21
|
+
* Check if the URL is a Confluence Wiki download URL
|
|
22
|
+
* Typical pattern: /download/attachments/{pageId}/{filename}
|
|
23
|
+
*/
|
|
24
|
+
private isWikiUrl;
|
|
25
|
+
/**
|
|
26
|
+
* Check if a source requires authentication
|
|
27
|
+
*/
|
|
28
|
+
requiresAuthentication(source: string): boolean;
|
|
29
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SourceDetector = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const url_1 = require("url");
|
|
6
|
+
const logger_1 = require("../core/logger");
|
|
7
|
+
class SourceDetector {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.logger = new logger_1.Logger('SourceDetector');
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Detect the type of source from a string
|
|
13
|
+
* @param source URL or file path
|
|
14
|
+
* @returns SourceInfo with detected type and relevant information
|
|
15
|
+
*/
|
|
16
|
+
detect(source) {
|
|
17
|
+
this.logger.debug(`Detecting source type for: ${source}`);
|
|
18
|
+
// Check if it's a local file
|
|
19
|
+
if (this.isLocalFile(source)) {
|
|
20
|
+
this.logger.info('Detected source type: local file');
|
|
21
|
+
return {
|
|
22
|
+
type: 'local-file',
|
|
23
|
+
path: source
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
// Try to parse as URL
|
|
27
|
+
try {
|
|
28
|
+
const url = new url_1.URL(source);
|
|
29
|
+
// Check if it's a wiki URL (Confluence-style download URL)
|
|
30
|
+
if (this.isWikiUrl(source)) {
|
|
31
|
+
this.logger.info('Detected source type: wiki URL');
|
|
32
|
+
return {
|
|
33
|
+
type: 'wiki-url',
|
|
34
|
+
url: source,
|
|
35
|
+
baseUrl: `${url.protocol}//${url.host}`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// Otherwise, treat as public URL
|
|
39
|
+
this.logger.info('Detected source type: public URL');
|
|
40
|
+
return {
|
|
41
|
+
type: 'public-url',
|
|
42
|
+
url: source
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Not a valid URL or file path
|
|
47
|
+
this.logger.warn('Unknown source type');
|
|
48
|
+
return {
|
|
49
|
+
type: 'unknown'
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Check if the source is a local file path
|
|
55
|
+
*/
|
|
56
|
+
isLocalFile(source) {
|
|
57
|
+
// Check if file exists
|
|
58
|
+
if ((0, fs_1.existsSync)(source)) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
// Check if it looks like a file path (contains path separators, no protocol)
|
|
62
|
+
if (!source.includes('://') && (source.includes('/') || source.includes('\\'))) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Check if the URL is a Confluence Wiki download URL
|
|
69
|
+
* Typical pattern: /download/attachments/{pageId}/{filename}
|
|
70
|
+
*/
|
|
71
|
+
isWikiUrl(url) {
|
|
72
|
+
const wikiPatterns = [
|
|
73
|
+
'/download/attachments/',
|
|
74
|
+
'/download/thumbnails/',
|
|
75
|
+
'/wiki/download/',
|
|
76
|
+
'/confluence/download/'
|
|
77
|
+
];
|
|
78
|
+
return wikiPatterns.some(pattern => url.includes(pattern));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Check if a source requires authentication
|
|
82
|
+
*/
|
|
83
|
+
requiresAuthentication(source) {
|
|
84
|
+
const info = this.detect(source);
|
|
85
|
+
return info.type === 'wiki-url';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.SourceDetector = SourceDetector;
|