skill-search 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/store.js ADDED
@@ -0,0 +1,156 @@
1
+ // src/store.js
2
+ // Local Data Store Management
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const config = require('./config');
7
+
8
+ const paths = config.getPaths();
9
+
10
+ /**
11
+ * Check if local data is empty
12
+ */
13
+ function isEmpty() {
14
+ return !fs.existsSync(paths.indexFile);
15
+ }
16
+
17
+ /**
18
+ * Get local index
19
+ */
20
+ function getIndex() {
21
+ if (!fs.existsSync(paths.indexFile)) {
22
+ return { skills: [], categories: [] };
23
+ }
24
+ try {
25
+ return JSON.parse(fs.readFileSync(paths.indexFile, 'utf8'));
26
+ } catch (error) {
27
+ return { skills: [], categories: [] };
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Set local index
33
+ */
34
+ function setIndex(data) {
35
+ config.ensureDataDir();
36
+ fs.writeFileSync(paths.indexFile, JSON.stringify(data, null, 2));
37
+ }
38
+
39
+ /**
40
+ * Get Sync Metadata
41
+ */
42
+ function getMeta() {
43
+ if (!fs.existsSync(paths.metaFile)) {
44
+ return null;
45
+ }
46
+ try {
47
+ return JSON.parse(fs.readFileSync(paths.metaFile, 'utf8'));
48
+ } catch (error) {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Set Sync Metadata
55
+ */
56
+ function setMeta(data) {
57
+ config.ensureDataDir();
58
+ // Merge with existing meta
59
+ const current = getMeta() || {};
60
+ const merged = { ...current, ...data };
61
+ fs.writeFileSync(paths.metaFile, JSON.stringify(merged, null, 2));
62
+ }
63
+
64
+ /**
65
+ * Get local doc content
66
+ */
67
+ function getDoc(skillId) {
68
+ const docPath = path.join(paths.docsDir, `${skillId}.md`);
69
+ if (fs.existsSync(docPath)) {
70
+ return fs.readFileSync(docPath, 'utf8');
71
+ }
72
+ return null;
73
+ }
74
+
75
+ /**
76
+ * Set local doc content
77
+ */
78
+ function setDoc(skillId, content) {
79
+ const docsDir = paths.docsDir;
80
+ if (!fs.existsSync(docsDir)) {
81
+ fs.mkdirSync(docsDir, { recursive: true });
82
+ }
83
+ const docPath = path.join(docsDir, `${skillId}.md`);
84
+ fs.writeFileSync(docPath, content);
85
+ }
86
+
87
+ /**
88
+ * Check if full skill folder exists
89
+ */
90
+ function hasFullSkill(skillId) {
91
+ const folderName = getSkillFolderName(skillId);
92
+ const skillDir = path.join(paths.skillsDir, folderName);
93
+ // Requirement says folder must contain skill.md. Usually it's SKILL.md or skill.md
94
+ // We check both or just SKILL.md as standardized? Original code checked 'SKILL.md'
95
+ return fs.existsSync(skillDir) &&
96
+ (fs.existsSync(path.join(skillDir, 'SKILL.md')) || fs.existsSync(path.join(skillDir, 'skill.md')));
97
+ }
98
+
99
+ /**
100
+ * Get full skill folder path
101
+ */
102
+ function getFullSkillPath(skillId) {
103
+ const folderName = getSkillFolderName(skillId);
104
+ return path.join(paths.skillsDir, folderName);
105
+ }
106
+
107
+ /**
108
+ * Set skill file (by specific folder name)
109
+ */
110
+ function setSkillFileByFolder(folderName, relativePath, content) {
111
+ const skillDir = path.join(paths.skillsDir, folderName);
112
+ const filePath = path.join(skillDir, relativePath);
113
+ const fileDir = path.dirname(filePath);
114
+
115
+ if (!fs.existsSync(fileDir)) {
116
+ fs.mkdirSync(fileDir, { recursive: true });
117
+ }
118
+
119
+ fs.writeFileSync(filePath, content);
120
+ }
121
+
122
+ /**
123
+ * Helper: Resolve folder name from Skill ID
124
+ */
125
+ function getSkillFolderName(skillId) {
126
+ const index = getIndex();
127
+ const skill = index.skills.find(s => s.id === skillId);
128
+ return (skill && skill.localFolder) ? skill.localFolder : skillId;
129
+ }
130
+
131
+ /**
132
+ * Set skill file (for full sync)
133
+ * @param {string} skillId
134
+ * @param {string} relativePath Relative path (e.g. scripts/test.py)
135
+ * @param {string|Buffer} content
136
+ */
137
+ function setSkillFile(skillId, relativePath, content) {
138
+ // Try to find folder name from index, otherwise use ID
139
+ const folderName = getSkillFolderName(skillId);
140
+ setSkillFileByFolder(folderName, relativePath, content);
141
+ }
142
+
143
+ module.exports = {
144
+ isEmpty,
145
+ getIndex,
146
+ setIndex,
147
+ getMeta,
148
+ setMeta,
149
+ getDoc,
150
+ setDoc,
151
+ hasFullSkill,
152
+ getFullSkillPath,
153
+ setSkillFile,
154
+ setSkillFileByFolder,
155
+ getPaths: config.getPaths // Expose getPaths if needed
156
+ };
package/src/syncer.js ADDED
@@ -0,0 +1,226 @@
1
+ // src/syncer.js
2
+ // Sync Module
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const chalk = require('chalk');
7
+ const ora = require('ora');
8
+ const api = require('./api');
9
+ const store = require('./store');
10
+ const { crawlLocalSkills } = require('./localCrawler');
11
+ const { parseFrontMatter } = require('./utils');
12
+
13
+ /**
14
+ * Execute sync
15
+ * @param {object} options
16
+ * @param {string} options.mode - 'local', 'remote', 'all'
17
+ */
18
+ async function sync(options = {}) {
19
+ // console.log('Starting Sync with options:', options);
20
+ const mode = options.mode || 'all';
21
+ const spinner = ora('Initializing Sync...').start();
22
+ const startTime = Date.now();
23
+
24
+ try {
25
+ let stats = {
26
+ local: 0,
27
+ remote: 0,
28
+ failed: 0
29
+ };
30
+
31
+ // 1. Sync Local Skills (Copy from found locations to cache)
32
+ if (mode === 'local' || mode === 'all') {
33
+ spinner.text = 'Scanning and syncing local skills...';
34
+ const localStats = await syncLocal(spinner);
35
+ stats.local = localStats.success;
36
+ stats.failed += localStats.failed;
37
+ }
38
+
39
+ // 2. Sync Remote Skills (Download from GitHub)
40
+ if (mode === 'remote' || mode === 'all') {
41
+ spinner.text = 'Fetching remote index...';
42
+ const remoteStats = await syncRemote(spinner);
43
+ stats.remote = remoteStats.success;
44
+ stats.failed += remoteStats.failed;
45
+ }
46
+
47
+ // 3. Update Meta
48
+ updateMeta();
49
+
50
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
51
+ spinner.succeed(chalk.green(`Sync completed in ${duration}s`));
52
+ console.log(chalk.cyan(` Local Synced: ${stats.local}`));
53
+ console.log(chalk.cyan(` Remote Synced: ${stats.remote}`));
54
+ if (stats.failed > 0) {
55
+ console.log(chalk.red(` Failed: ${stats.failed}`));
56
+ }
57
+
58
+ } catch (error) {
59
+ spinner.fail(chalk.red('Sync failed'));
60
+ console.error(chalk.red(error.message));
61
+ if (process.env.DEBUG) console.error(error);
62
+ throw error; // Re-throw for UI to catch
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Sync Local Skills
68
+ * Scans for local skills and copies them to ~/.skill-data/skills/
69
+ */
70
+ async function syncLocal(spinner) {
71
+ const results = { success: 0, failed: 0 };
72
+
73
+ // 1. Crawl
74
+ spinner.text = 'Crawling local directories...';
75
+ const localSkills = await crawlLocalSkills();
76
+
77
+ if (localSkills.length === 0) {
78
+ return results;
79
+ }
80
+
81
+ spinner.text = `Found ${localSkills.length} local skills. Syncing to cache...`;
82
+
83
+ // 2. Copy each skill to cache
84
+ for (const skill of localSkills) {
85
+ try {
86
+ const destDir = store.getFullSkillPath(skill.id);
87
+ // Ensure distinct folder for local skills if needed, but ID should be unique enough?
88
+ // User requested "separate folder". Use ID.
89
+
90
+ await copyRecursive(skill.path, destDir);
91
+
92
+ // Ensure metadata is saved
93
+ store.setSkillFileByFolder(skill.id, 'skill.json', JSON.stringify(skill, null, 2));
94
+
95
+ results.success++;
96
+ // spinner.text = `Synced local: ${skill.id}`;
97
+ } catch (err) {
98
+ results.failed++;
99
+ console.error(chalk.yellow(`Failed to sync local skill ${skill.id}: ${err.message}`));
100
+ }
101
+ }
102
+
103
+ return results;
104
+ }
105
+
106
+ /**
107
+ * Sync Remote Skills
108
+ * Fetches index and downloads content for ALL remote skills
109
+ */
110
+ async function syncRemote(spinner) {
111
+ const results = { success: 0, failed: 0 };
112
+
113
+ // 1. Fetch Index
114
+ spinner.text = 'Fetching remote index...';
115
+ const allSkills = await api.fetchAllSkills();
116
+
117
+ // Update Store Index
118
+ store.setIndex({
119
+ skills: allSkills,
120
+ updated: new Date().toISOString()
121
+ });
122
+
123
+ // 2. Download Content
124
+ const skillsToDownload = allSkills.filter(s => s.githubUrl);
125
+
126
+ // Warn if too many? User requested "All". Let's throttle or just do it.
127
+ // For TUI, let's just do it.
128
+
129
+ for (let i = 0; i < skillsToDownload.length; i++) {
130
+ const skill = skillsToDownload[i];
131
+ spinner.text = `Downloading remote [${i + 1}/${skillsToDownload.length}]: ${skill.name || skill.id}`;
132
+
133
+ try {
134
+ // Logic from old syncFull
135
+ // Construct base folder name: owner.repo.skillName
136
+ let folderName = skill.id;
137
+
138
+ // Try to extract better folder structure if GitHub URL is present
139
+ if (skill.githubUrl) {
140
+ const parsed = api.parseGitHubUrl(skill.githubUrl);
141
+ if (parsed) {
142
+ // Keep the original skill folder name from the URL path end
143
+ const originalSkillName = parsed.path.split('/').pop();
144
+ folderName = `${parsed.owner}.${parsed.repo}.${originalSkillName}`;
145
+ }
146
+ }
147
+
148
+ // Fetch contents recursively and preserve relative paths
149
+ await api.fetchGitHubDirectoryRecursive(skill.githubUrl, (file) => {
150
+ // file.path is relative to the skill root (e.g., "SKILL.md", "references/foo.md")
151
+ // file.content is the file content
152
+ store.setSkillFileByFolder(folderName, file.path, file.content);
153
+ });
154
+
155
+ // Also ensure skill.json or similar metadata exists if needed
156
+ // store.setSkillFileByFolder(folderName, 'meta.json', JSON.stringify(skill, null, 2));
157
+
158
+ results.success++;
159
+ } catch (err) {
160
+ results.failed++;
161
+ // Don't spam console in TUI mode unless verbose
162
+ // console.error(chalk.yellow(`Failed to download ${skill.id}: ${err.message}`));
163
+ }
164
+ }
165
+
166
+ return results;
167
+ }
168
+
169
+ // Helper: Copy Recursive
170
+ async function copyRecursive(src, dest) {
171
+ const stats = await fs.promises.stat(src);
172
+ if (stats.isDirectory()) {
173
+ await fs.promises.mkdir(dest, { recursive: true });
174
+ const entries = await fs.promises.readdir(src);
175
+ for (const entry of entries) {
176
+ await copyRecursive(path.join(src, entry), path.join(dest, entry));
177
+ }
178
+ } else {
179
+ await fs.promises.copyFile(src, dest);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Update Metadata
185
+ */
186
+ function updateMeta() {
187
+ const meta = store.getMeta() || {};
188
+ const index = store.getIndex();
189
+
190
+ meta.lastSync = new Date().toISOString();
191
+ meta.totalSkills = index.skills ? index.skills.length : 0;
192
+
193
+ // We can count actual folders in skillsDir to allow correct "Cached" count
194
+ try {
195
+ const skillsDir = store.getPaths().skillsDir;
196
+ if (fs.existsSync(skillsDir)) {
197
+ const folders = fs.readdirSync(skillsDir);
198
+ meta.syncedDocs = folders.length; // Use syncedDocs field to store count of cached skills
199
+ }
200
+ } catch (e) { }
201
+
202
+ store.setMeta(meta);
203
+ }
204
+
205
+ /**
206
+ * Get Sync Status
207
+ */
208
+ function getStatus() {
209
+ const meta = store.getMeta();
210
+ const index = store.getIndex();
211
+
212
+ if (!meta) {
213
+ return null;
214
+ }
215
+
216
+ return {
217
+ lastSync: meta.lastSync,
218
+ totalSkills: index.skills ? index.skills.length : 0,
219
+ syncedDocs: meta.syncedDocs || 0 // Reusing this field name as "Cached Skills"
220
+ };
221
+ }
222
+
223
+ module.exports = {
224
+ sync,
225
+ getStatus
226
+ };
package/src/theme.js ADDED
@@ -0,0 +1,191 @@
1
+ // src/theme.js
2
+ // Theme Management for TUI
3
+
4
+ const config = require('./config');
5
+
6
+ /**
7
+ * Theme Definitions
8
+ */
9
+ const THEMES = {
10
+ dark: {
11
+ name: 'Dark',
12
+ // Primary colors
13
+ primary: 'cyan',
14
+ secondary: 'green',
15
+ accent: 'yellow',
16
+ // Text colors
17
+ text: 'white',
18
+ textDim: 'gray',
19
+ textMuted: 'gray',
20
+ // UI colors
21
+ border: 'cyan',
22
+ borderInactive: 'gray',
23
+ // Status colors
24
+ success: 'green',
25
+ warning: 'yellow',
26
+ error: 'red',
27
+ info: 'cyan',
28
+ // List colors
29
+ localHighlight: 'green',
30
+ remoteHighlight: 'yellow',
31
+ selected: 'cyan',
32
+ // Logo color
33
+ logo: 'cyan',
34
+ // Background (for inverse text)
35
+ bgHighlight: 'cyan'
36
+ },
37
+ light: {
38
+ name: 'Light',
39
+ // Primary colors
40
+ primary: 'blue',
41
+ secondary: 'magenta',
42
+ accent: 'red',
43
+ // Text colors
44
+ text: 'black',
45
+ textDim: 'gray',
46
+ textMuted: 'gray',
47
+ // UI colors
48
+ border: 'blue',
49
+ borderInactive: 'gray',
50
+ // Status colors
51
+ success: 'green',
52
+ warning: 'red',
53
+ error: 'red',
54
+ info: 'blue',
55
+ // List colors
56
+ localHighlight: 'magenta',
57
+ remoteHighlight: 'red',
58
+ selected: 'blue',
59
+ // Logo color
60
+ logo: 'blue',
61
+ // Background (for inverse text)
62
+ bgHighlight: 'blue'
63
+ }
64
+ };
65
+
66
+ // Current theme cache
67
+ let currentTheme = null;
68
+
69
+ /**
70
+ * Detect terminal color scheme
71
+ * Returns 'dark' or 'light' based on environment variables and heuristics
72
+ */
73
+ function detectTerminalTheme() {
74
+ // Check common environment variables
75
+ const colorTerm = process.env.COLORFGBG;
76
+ const termProgram = process.env.TERM_PROGRAM;
77
+ const wtSession = process.env.WT_SESSION; // Windows Terminal
78
+ const iterm = process.env.ITERM_SESSION_ID;
79
+
80
+ // COLORFGBG format: "foreground;background" (e.g., "15;0" = white on black = dark)
81
+ if (colorTerm) {
82
+ const parts = colorTerm.split(';');
83
+ if (parts.length >= 2) {
84
+ const bg = parseInt(parts[parts.length - 1], 10);
85
+ // Background 0-6 or 8 typically means dark, 7 or 9-15 means light
86
+ if (bg === 0 || bg === 8 || (bg >= 0 && bg <= 6)) {
87
+ return 'dark';
88
+ } else if (bg === 7 || bg === 15 || (bg >= 9 && bg <= 15)) {
89
+ return 'light';
90
+ }
91
+ }
92
+ }
93
+
94
+ // macOS Terminal.app and iTerm2 - usually respect system dark mode
95
+ if (process.platform === 'darwin') {
96
+ // Try to detect macOS dark mode via AppleInterfaceStyle
97
+ try {
98
+ const { execSync } = require('child_process');
99
+ const result = execSync('defaults read -g AppleInterfaceStyle 2>/dev/null', { encoding: 'utf8' });
100
+ if (result.trim().toLowerCase() === 'dark') {
101
+ return 'dark';
102
+ }
103
+ } catch (e) {
104
+ // AppleInterfaceStyle not set = light mode
105
+ return 'light';
106
+ }
107
+ }
108
+
109
+ // Windows Terminal detection
110
+ if (wtSession) {
111
+ // Windows Terminal - default to dark as it's the common default
112
+ return 'dark';
113
+ }
114
+
115
+ // VS Code integrated terminal
116
+ if (termProgram === 'vscode') {
117
+ // Check VS Code theme via environment
118
+ const vscodeTheme = process.env.VSCODE_GIT_ASKPASS_NODE;
119
+ // Default to dark as VS Code default is dark
120
+ return 'dark';
121
+ }
122
+
123
+ // Default to dark theme (most terminal users prefer dark)
124
+ return 'dark';
125
+ }
126
+
127
+ /**
128
+ * Get current theme name from config
129
+ */
130
+ function getThemeName() {
131
+ const userConfig = config.getUserConfig();
132
+ if (userConfig.theme) {
133
+ return userConfig.theme;
134
+ }
135
+ // First run - detect and save
136
+ const detected = detectTerminalTheme();
137
+ setThemeName(detected);
138
+ return detected;
139
+ }
140
+
141
+ /**
142
+ * Set theme name in config
143
+ */
144
+ function setThemeName(themeName) {
145
+ const userConfig = config.getUserConfig();
146
+ userConfig.theme = themeName;
147
+ config.setUserConfig(userConfig);
148
+ currentTheme = null; // Reset cache
149
+ }
150
+
151
+ /**
152
+ * Get current theme object
153
+ */
154
+ function getTheme() {
155
+ if (currentTheme) {
156
+ return currentTheme;
157
+ }
158
+ const themeName = getThemeName();
159
+ currentTheme = THEMES[themeName] || THEMES.dark;
160
+ return currentTheme;
161
+ }
162
+
163
+ /**
164
+ * Toggle between dark and light themes
165
+ */
166
+ function toggleTheme() {
167
+ const current = getThemeName();
168
+ const next = current === 'dark' ? 'light' : 'dark';
169
+ setThemeName(next);
170
+ return next;
171
+ }
172
+
173
+ /**
174
+ * Get list of available themes
175
+ */
176
+ function getAvailableThemes() {
177
+ return Object.keys(THEMES).map(key => ({
178
+ key,
179
+ name: THEMES[key].name
180
+ }));
181
+ }
182
+
183
+ module.exports = {
184
+ THEMES,
185
+ detectTerminalTheme,
186
+ getThemeName,
187
+ setThemeName,
188
+ getTheme,
189
+ toggleTheme,
190
+ getAvailableThemes
191
+ };