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/README.md +381 -0
- package/bin/skill.js +348 -0
- package/bin/tui.js +33 -0
- package/package.json +53 -0
- package/scripts/package-lock.json +6 -0
- package/scripts/setup-env.bat +58 -0
- package/scripts/test-scan.js +42 -0
- package/src/actions.js +216 -0
- package/src/api.js +306 -0
- package/src/cache.js +107 -0
- package/src/config.js +220 -0
- package/src/fallback-index.json +6 -0
- package/src/interactive.js +23 -0
- package/src/localCrawler.js +204 -0
- package/src/matcher.js +170 -0
- package/src/store.js +156 -0
- package/src/syncer.js +226 -0
- package/src/theme.js +191 -0
- package/src/tui/ActionModal.js +209 -0
- package/src/tui/AddDelView.js +212 -0
- package/src/tui/App.js +739 -0
- package/src/tui/AsciiHeader.js +35 -0
- package/src/tui/CommandPalette.js +64 -0
- package/src/tui/ConfigView.js +168 -0
- package/src/tui/DetailView.js +139 -0
- package/src/tui/DualPane.js +114 -0
- package/src/tui/PrimaryView.js +163 -0
- package/src/tui/SearchBox.js +26 -0
- package/src/tui/SearchView.js +121 -0
- package/src/tui/SkillList.js +102 -0
- package/src/tui/SyncView.js +143 -0
- package/src/tui/ThemeView.js +116 -0
- package/src/utils.js +83 -0
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
|
+
};
|