@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.
@@ -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;