@wang121ye/skillmanager 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/LICENSE +21 -0
- package/README.md +219 -0
- package/manifests/sources.json +38 -0
- package/package.json +50 -0
- package/src/cli.js +179 -0
- package/src/commands/bootstrap.js +153 -0
- package/src/commands/config.js +96 -0
- package/src/commands/select.js +79 -0
- package/src/commands/source.js +103 -0
- package/src/commands/uninstall.js +63 -0
- package/src/commands/update.js +110 -0
- package/src/commands/webui.js +150 -0
- package/src/commands/where.js +23 -0
- package/src/index.js +2 -0
- package/src/lib/concurrency.js +22 -0
- package/src/lib/config.js +75 -0
- package/src/lib/fs.js +33 -0
- package/src/lib/git.js +57 -0
- package/src/lib/http.js +42 -0
- package/src/lib/installed.js +35 -0
- package/src/lib/local-install.js +35 -0
- package/src/lib/manifest.js +84 -0
- package/src/lib/openskills.js +50 -0
- package/src/lib/paths.js +44 -0
- package/src/lib/profiles.js +31 -0
- package/src/lib/scan.js +50 -0
- package/src/lib/source-utils.js +62 -0
- package/src/ui/server.js +321 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { fileExists, readJson, writeJson } = require('./fs');
|
|
3
|
+
const { getAppPaths } = require('./paths');
|
|
4
|
+
|
|
5
|
+
function getUserConfigPath() {
|
|
6
|
+
const p = getAppPaths();
|
|
7
|
+
return path.join(p.configDir, 'config.json');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function loadConfig() {
|
|
11
|
+
const configPath = getUserConfigPath();
|
|
12
|
+
if (!(await fileExists(configPath))) {
|
|
13
|
+
const initial = { version: 1, defaultProfile: 'default', remoteProfileUrl: null };
|
|
14
|
+
await writeJson(configPath, initial);
|
|
15
|
+
return { configPath, config: initial };
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const config = await readJson(configPath);
|
|
19
|
+
return { configPath, config: config || { version: 1, defaultProfile: 'default', remoteProfileUrl: null } };
|
|
20
|
+
} catch {
|
|
21
|
+
const fallback = { version: 1, defaultProfile: 'default', remoteProfileUrl: null };
|
|
22
|
+
await writeJson(configPath, fallback);
|
|
23
|
+
return { configPath, config: fallback };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function setDefaultProfile(name) {
|
|
28
|
+
const { configPath, config } = await loadConfig();
|
|
29
|
+
const next = { ...(config || {}), version: 1, defaultProfile: String(name || 'default') };
|
|
30
|
+
await writeJson(configPath, next);
|
|
31
|
+
return { configPath, config: next };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function setRemoteProfileUrl(url) {
|
|
35
|
+
const { configPath, config } = await loadConfig();
|
|
36
|
+
const next = { ...(config || {}), version: 1, remoteProfileUrl: url ? String(url) : null };
|
|
37
|
+
await writeJson(configPath, next);
|
|
38
|
+
return { configPath, config: next };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getDefaultProfileFromEnv() {
|
|
42
|
+
const v = process.env.SKILLMANAGER_PROFILE;
|
|
43
|
+
return v && String(v).trim() ? String(v).trim() : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getRemoteProfileUrlFromEnv() {
|
|
47
|
+
const v = process.env.SKILLMANAGER_PROFILE_URL;
|
|
48
|
+
return v && String(v).trim() ? String(v).trim() : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function getEffectiveDefaultProfile() {
|
|
52
|
+
const env = getDefaultProfileFromEnv();
|
|
53
|
+
if (env) return env;
|
|
54
|
+
const { config } = await loadConfig();
|
|
55
|
+
const v = config?.defaultProfile;
|
|
56
|
+
return v && String(v).trim() ? String(v).trim() : 'default';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function getEffectiveRemoteProfileUrl() {
|
|
60
|
+
const env = getRemoteProfileUrlFromEnv();
|
|
61
|
+
if (env) return env;
|
|
62
|
+
const { config } = await loadConfig();
|
|
63
|
+
const v = config?.remoteProfileUrl;
|
|
64
|
+
return v && String(v).trim() ? String(v).trim() : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
loadConfig,
|
|
69
|
+
setDefaultProfile,
|
|
70
|
+
setRemoteProfileUrl,
|
|
71
|
+
getEffectiveDefaultProfile,
|
|
72
|
+
getEffectiveRemoteProfileUrl,
|
|
73
|
+
getUserConfigPath
|
|
74
|
+
};
|
|
75
|
+
|
package/src/lib/fs.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const fsp = require('fs/promises');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
async function ensureDir(dir) {
|
|
6
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function fileExists(p) {
|
|
10
|
+
try {
|
|
11
|
+
await fsp.access(p, fs.constants.F_OK);
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function readJson(p) {
|
|
19
|
+
const raw = await fsp.readFile(p, 'utf8');
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function writeJson(p, obj) {
|
|
24
|
+
await ensureDir(path.dirname(p));
|
|
25
|
+
await fsp.writeFile(p, JSON.stringify(obj, null, 2) + '\n', 'utf8');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function rmDir(p) {
|
|
29
|
+
await fsp.rm(p, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { ensureDir, fileExists, readJson, writeJson, rmDir };
|
|
33
|
+
|
package/src/lib/git.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const simpleGit = require('simple-git');
|
|
3
|
+
const { ensureDir, fileExists, rmDir } = require('./fs');
|
|
4
|
+
|
|
5
|
+
function sanitizeId(id) {
|
|
6
|
+
return String(id).replace(/[^a-zA-Z0-9._-]+/g, '_');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function sleep(ms) {
|
|
10
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function ensureRepo({ reposDir, source }) {
|
|
14
|
+
if (!source?.repo) {
|
|
15
|
+
throw new Error(`Invalid source repo for ${source?.id || '<unknown>'}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const repoDir = path.join(reposDir, sanitizeId(source.id));
|
|
19
|
+
await ensureDir(reposDir);
|
|
20
|
+
|
|
21
|
+
const gitDir = path.join(repoDir, '.git');
|
|
22
|
+
const git = simpleGit();
|
|
23
|
+
|
|
24
|
+
if (!(await fileExists(gitDir))) {
|
|
25
|
+
const cloneArgs = ['--depth', '1', '--single-branch', '--filter=blob:none'];
|
|
26
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
27
|
+
try {
|
|
28
|
+
await rmDir(repoDir);
|
|
29
|
+
await ensureDir(repoDir);
|
|
30
|
+
await git.clone(source.repo, repoDir, cloneArgs);
|
|
31
|
+
return repoDir;
|
|
32
|
+
} catch (err) {
|
|
33
|
+
await rmDir(repoDir);
|
|
34
|
+
if (attempt === 3) throw err;
|
|
35
|
+
await sleep(1000 * attempt);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return repoDir;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const repoGit = simpleGit(repoDir);
|
|
42
|
+
try {
|
|
43
|
+
await repoGit.fetch(['--depth', '1', '--filter=blob:none']);
|
|
44
|
+
} catch {
|
|
45
|
+
// ignore fetch errors; pull may still work
|
|
46
|
+
}
|
|
47
|
+
// Best-effort: try to fast-forward current branch
|
|
48
|
+
try {
|
|
49
|
+
await repoGit.pull(['--ff-only']);
|
|
50
|
+
} catch {
|
|
51
|
+
// ignore ff-only failures (e.g. detached HEAD) — user can clean manually
|
|
52
|
+
}
|
|
53
|
+
return repoDir;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { ensureRepo };
|
|
57
|
+
|
package/src/lib/http.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
let proxyInitialized = false;
|
|
2
|
+
|
|
3
|
+
function getProxyUrlFromEnv() {
|
|
4
|
+
return (
|
|
5
|
+
process.env.SKILLMANAGER_PROXY ||
|
|
6
|
+
process.env.HTTPS_PROXY ||
|
|
7
|
+
process.env.https_proxy ||
|
|
8
|
+
process.env.HTTP_PROXY ||
|
|
9
|
+
process.env.http_proxy ||
|
|
10
|
+
null
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function ensureProxyInitialized() {
|
|
15
|
+
if (proxyInitialized) return;
|
|
16
|
+
proxyInitialized = true;
|
|
17
|
+
|
|
18
|
+
const proxyUrl = getProxyUrlFromEnv();
|
|
19
|
+
if (!proxyUrl) return;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Node 20+ ships undici. Use ProxyAgent so global fetch respects proxy.
|
|
23
|
+
// eslint-disable-next-line global-require
|
|
24
|
+
const { ProxyAgent, setGlobalDispatcher } = require('undici');
|
|
25
|
+
setGlobalDispatcher(new ProxyAgent(proxyUrl));
|
|
26
|
+
// eslint-disable-next-line no-console
|
|
27
|
+
console.log(`使用代理:${proxyUrl}`);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
// eslint-disable-next-line no-console
|
|
30
|
+
console.warn('警告:初始化代理失败,将不使用代理。');
|
|
31
|
+
// eslint-disable-next-line no-console
|
|
32
|
+
console.warn(e?.message || String(e));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function httpFetch(url, options) {
|
|
37
|
+
ensureProxyInitialized();
|
|
38
|
+
return await fetch(url, options);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { httpFetch };
|
|
42
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fsp = require('fs/promises');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const matter = require('gray-matter');
|
|
5
|
+
|
|
6
|
+
async function listInstalledSkills(targetDir) {
|
|
7
|
+
try {
|
|
8
|
+
const entries = await fsp.readdir(targetDir, { withFileTypes: true });
|
|
9
|
+
const skills = [];
|
|
10
|
+
for (const e of entries) {
|
|
11
|
+
if (!e.isDirectory()) continue;
|
|
12
|
+
const skillDir = path.join(targetDir, e.name);
|
|
13
|
+
const skillMd = path.join(skillDir, 'SKILL.md');
|
|
14
|
+
if (!fs.existsSync(skillMd)) continue;
|
|
15
|
+
let description = '';
|
|
16
|
+
try {
|
|
17
|
+
const raw = await fsp.readFile(skillMd, 'utf8');
|
|
18
|
+
const parsed = matter(raw);
|
|
19
|
+
if (parsed?.data?.description) description = String(parsed.data.description).trim();
|
|
20
|
+
} catch {}
|
|
21
|
+
skills.push({
|
|
22
|
+
name: e.name,
|
|
23
|
+
description,
|
|
24
|
+
skillDir
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
28
|
+
return skills;
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { listInstalledSkills };
|
|
35
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const fsp = require('fs/promises');
|
|
4
|
+
|
|
5
|
+
function isPathInside(targetPath, targetDir) {
|
|
6
|
+
const resolvedTargetPath = path.resolve(targetPath);
|
|
7
|
+
const resolvedTargetDir = path.resolve(targetDir);
|
|
8
|
+
const resolvedTargetDirWithSep = resolvedTargetDir.endsWith(path.sep) ? resolvedTargetDir : resolvedTargetDir + path.sep;
|
|
9
|
+
return resolvedTargetPath.startsWith(resolvedTargetDirWithSep);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function installFromLocalSkillDir({ skillDir, targetDir }) {
|
|
13
|
+
const skillMd = path.join(skillDir, 'SKILL.md');
|
|
14
|
+
if (!fs.existsSync(skillMd)) {
|
|
15
|
+
throw new Error(`SKILL.md not found: ${skillMd}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const skillName = path.basename(skillDir);
|
|
19
|
+
const targetPath = path.join(targetDir, skillName);
|
|
20
|
+
|
|
21
|
+
await fsp.mkdir(targetDir, { recursive: true });
|
|
22
|
+
if (!isPathInside(targetPath, targetDir)) {
|
|
23
|
+
throw new Error(`Security error: target path outside targetDir: ${targetPath}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// overwrite for deterministic bootstrap/update
|
|
27
|
+
await fsp.rm(targetPath, { recursive: true, force: true });
|
|
28
|
+
// Node 20: fs.promises.cp exists
|
|
29
|
+
await fsp.cp(skillDir, targetPath, { recursive: true, dereference: true });
|
|
30
|
+
|
|
31
|
+
return { skillName, targetPath };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { installFromLocalSkillDir };
|
|
35
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { readJson, fileExists, writeJson } = require('./fs');
|
|
3
|
+
const { getAppPaths } = require('./paths');
|
|
4
|
+
|
|
5
|
+
function getBuiltinManifestPath() {
|
|
6
|
+
// src/lib -> src -> project root
|
|
7
|
+
return path.resolve(__dirname, '../../manifests/sources.json');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getUserManifestPath() {
|
|
11
|
+
const appPaths = getAppPaths();
|
|
12
|
+
return path.join(appPaths.configDir, 'sources.json');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function loadSourcesManifest() {
|
|
16
|
+
const builtinPath = getBuiltinManifestPath();
|
|
17
|
+
const userPath = getUserManifestPath();
|
|
18
|
+
|
|
19
|
+
const builtin = await readJson(builtinPath);
|
|
20
|
+
|
|
21
|
+
// Ensure user manifest exists (copy builtin on first run)
|
|
22
|
+
if (!(await fileExists(userPath))) {
|
|
23
|
+
await writeJson(userPath, builtin);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let manifestPath = userPath;
|
|
27
|
+
let manifest;
|
|
28
|
+
try {
|
|
29
|
+
manifest = await readJson(manifestPath);
|
|
30
|
+
} catch {
|
|
31
|
+
// fallback to builtin if user file is corrupted
|
|
32
|
+
manifestPath = builtinPath;
|
|
33
|
+
manifest = await readJson(manifestPath);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Merge-in new builtin sources by id (non-destructive):
|
|
37
|
+
// - keep user's existing entries as-is (including enabled flags)
|
|
38
|
+
// - add any builtin sources missing from user
|
|
39
|
+
// - bump version to max(builtin, user)
|
|
40
|
+
if (manifestPath === userPath) {
|
|
41
|
+
const userSources = Array.isArray(manifest?.sources) ? manifest.sources : [];
|
|
42
|
+
const userIds = new Set(userSources.map((s) => s && s.id).filter(Boolean));
|
|
43
|
+
const builtinSources = Array.isArray(builtin?.sources) ? builtin.sources : [];
|
|
44
|
+
|
|
45
|
+
const mergedSources = [...userSources];
|
|
46
|
+
for (const s of builtinSources) {
|
|
47
|
+
if (!s?.id) continue;
|
|
48
|
+
if (!userIds.has(s.id)) mergedSources.push(s);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const mergedVersion = Math.max(Number(manifest?.version || 1), Number(builtin?.version || 1));
|
|
52
|
+
const changed = mergedVersion !== Number(manifest?.version || 1) || mergedSources.length !== userSources.length;
|
|
53
|
+
|
|
54
|
+
if (changed) {
|
|
55
|
+
manifest = { ...(manifest || {}), version: mergedVersion, sources: mergedSources };
|
|
56
|
+
await writeJson(userPath, manifest);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const sources = Array.isArray(manifest?.sources) ? manifest.sources : [];
|
|
61
|
+
return { version: manifest?.version ?? 1, sources, manifestPath };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function readUserSourcesManifest() {
|
|
65
|
+
const builtinPath = getBuiltinManifestPath();
|
|
66
|
+
const userPath = getUserManifestPath();
|
|
67
|
+
|
|
68
|
+
if (!(await fileExists(userPath))) {
|
|
69
|
+
const builtin = await readJson(builtinPath);
|
|
70
|
+
await writeJson(userPath, builtin);
|
|
71
|
+
}
|
|
72
|
+
const manifest = await readJson(userPath);
|
|
73
|
+
const sources = Array.isArray(manifest?.sources) ? manifest.sources : [];
|
|
74
|
+
return { manifest, sources, userPath };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function writeUserSourcesManifest(manifest) {
|
|
78
|
+
const userPath = getUserManifestPath();
|
|
79
|
+
await writeJson(userPath, manifest);
|
|
80
|
+
return userPath;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { loadSourcesManifest, getUserManifestPath, readUserSourcesManifest, writeUserSourcesManifest };
|
|
84
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { spawn } = require('child_process');
|
|
3
|
+
|
|
4
|
+
function getOpenSkillsCliPath() {
|
|
5
|
+
// openskills bin points to dist/cli.js
|
|
6
|
+
return require.resolve('openskills/dist/cli.js');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function runOpenSkills(args, opts = {}) {
|
|
10
|
+
const cliPath = getOpenSkillsCliPath();
|
|
11
|
+
const node = process.execPath;
|
|
12
|
+
return await new Promise((resolve, reject) => {
|
|
13
|
+
const child = spawn(node, [cliPath, ...args], {
|
|
14
|
+
cwd: opts.cwd || process.cwd(),
|
|
15
|
+
stdio: 'inherit',
|
|
16
|
+
windowsHide: true
|
|
17
|
+
});
|
|
18
|
+
child.on('error', reject);
|
|
19
|
+
child.on('exit', (code) => {
|
|
20
|
+
if (code === 0) return resolve();
|
|
21
|
+
reject(new Error(`openskills exited with code ${code}`));
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildOpenSkillsFlags({ globalInstall, universal }) {
|
|
27
|
+
const flags = [];
|
|
28
|
+
if (globalInstall) flags.push('--global');
|
|
29
|
+
if (universal) flags.push('--universal');
|
|
30
|
+
return flags;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function installSkillDir({ skillDir, globalInstall, universal }) {
|
|
34
|
+
const flags = buildOpenSkillsFlags({ globalInstall, universal });
|
|
35
|
+
await runOpenSkills(['install', skillDir, ...flags, '--yes']);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function installSourceRef({ ref, globalInstall, universal }) {
|
|
39
|
+
const flags = buildOpenSkillsFlags({ globalInstall, universal });
|
|
40
|
+
await runOpenSkills(['install', ref, ...flags, '--yes']);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function syncAgents({ output, cwd }) {
|
|
44
|
+
const args = ['sync', '--yes'];
|
|
45
|
+
if (output) args.push('--output', path.resolve(cwd || process.cwd(), output));
|
|
46
|
+
await runOpenSkills(args, { cwd });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { installSkillDir, installSourceRef, syncAgents, runOpenSkills };
|
|
50
|
+
|
package/src/lib/paths.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
|
|
4
|
+
function getAppPaths() {
|
|
5
|
+
const home = os.homedir();
|
|
6
|
+
const platform = process.platform;
|
|
7
|
+
|
|
8
|
+
let configDir;
|
|
9
|
+
let cacheDir;
|
|
10
|
+
let dataDir;
|
|
11
|
+
let logDir;
|
|
12
|
+
let tempDir;
|
|
13
|
+
|
|
14
|
+
if (platform === 'win32') {
|
|
15
|
+
const roaming = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
16
|
+
const local = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
17
|
+
const tmp = process.env.TEMP || process.env.TMP || path.join(local, 'Temp');
|
|
18
|
+
|
|
19
|
+
configDir = path.join(roaming, 'skillmanager');
|
|
20
|
+
cacheDir = path.join(local, 'skillmanager', 'Cache');
|
|
21
|
+
dataDir = path.join(local, 'skillmanager', 'Data');
|
|
22
|
+
logDir = path.join(local, 'skillmanager', 'Logs');
|
|
23
|
+
tempDir = path.join(tmp, 'skillmanager');
|
|
24
|
+
} else {
|
|
25
|
+
configDir = process.env.XDG_CONFIG_HOME ? path.join(process.env.XDG_CONFIG_HOME, 'skillmanager') : path.join(home, '.config', 'skillmanager');
|
|
26
|
+
cacheDir = process.env.XDG_CACHE_HOME ? path.join(process.env.XDG_CACHE_HOME, 'skillmanager') : path.join(home, '.cache', 'skillmanager');
|
|
27
|
+
dataDir = process.env.XDG_DATA_HOME ? path.join(process.env.XDG_DATA_HOME, 'skillmanager') : path.join(home, '.local', 'share', 'skillmanager');
|
|
28
|
+
logDir = path.join(cacheDir, 'logs');
|
|
29
|
+
tempDir = path.join(os.tmpdir(), 'skillmanager');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
cacheDir,
|
|
34
|
+
configDir,
|
|
35
|
+
dataDir,
|
|
36
|
+
logDir,
|
|
37
|
+
tempDir,
|
|
38
|
+
reposDir: path.join(cacheDir, 'repos'),
|
|
39
|
+
profilesDir: path.join(configDir, 'profiles')
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { getAppPaths };
|
|
44
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { fileExists, readJson, writeJson, ensureDir } = require('./fs');
|
|
3
|
+
|
|
4
|
+
function profilePath({ profilesDir, profileName }) {
|
|
5
|
+
const safe = String(profileName || 'default').replace(/[^a-zA-Z0-9._-]+/g, '_');
|
|
6
|
+
return path.join(profilesDir, `${safe}.json`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function loadProfile({ profilesDir, profileName }) {
|
|
10
|
+
const p = profilePath({ profilesDir, profileName });
|
|
11
|
+
if (!(await fileExists(p))) return null;
|
|
12
|
+
try {
|
|
13
|
+
return await readJson(p);
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function saveProfile({ profilesDir, profileName, selectedSkillIds }) {
|
|
20
|
+
await ensureDir(profilesDir);
|
|
21
|
+
const p = profilePath({ profilesDir, profileName });
|
|
22
|
+
await writeJson(p, {
|
|
23
|
+
version: 1,
|
|
24
|
+
updatedAt: new Date().toISOString(),
|
|
25
|
+
selectedSkillIds: Array.isArray(selectedSkillIds) ? selectedSkillIds : []
|
|
26
|
+
});
|
|
27
|
+
return p;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { loadProfile, saveProfile, profilePath };
|
|
31
|
+
|
package/src/lib/scan.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fsp = require('fs/promises');
|
|
3
|
+
const fg = require('fast-glob');
|
|
4
|
+
const matter = require('gray-matter');
|
|
5
|
+
|
|
6
|
+
async function scanSkillsInRepo({ sourceId, sourceName, repoDir }) {
|
|
7
|
+
const entries = await fg(['**/SKILL.md'], {
|
|
8
|
+
cwd: repoDir,
|
|
9
|
+
dot: false,
|
|
10
|
+
onlyFiles: true,
|
|
11
|
+
ignore: ['**/node_modules/**', '**/.git/**']
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const skills = [];
|
|
15
|
+
for (const relSkillMd of entries) {
|
|
16
|
+
const absSkillMd = path.join(repoDir, relSkillMd);
|
|
17
|
+
const skillDir = path.dirname(absSkillMd);
|
|
18
|
+
let name = path.basename(skillDir);
|
|
19
|
+
let description = '';
|
|
20
|
+
try {
|
|
21
|
+
const raw = await fsp.readFile(absSkillMd, 'utf8');
|
|
22
|
+
const parsed = matter(raw);
|
|
23
|
+
if (parsed?.data?.name) name = String(parsed.data.name).trim();
|
|
24
|
+
if (parsed?.data?.description) description = String(parsed.data.description).trim();
|
|
25
|
+
} catch {
|
|
26
|
+
// ignore parse errors
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const relSkillDir = path.relative(repoDir, skillDir).replace(/\\/g, '/');
|
|
30
|
+
skills.push({
|
|
31
|
+
id: `${sourceId}:${relSkillDir}`,
|
|
32
|
+
sourceId,
|
|
33
|
+
sourceName,
|
|
34
|
+
name,
|
|
35
|
+
description,
|
|
36
|
+
skillDir,
|
|
37
|
+
skillMd: absSkillMd
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// stable sort: by source then name
|
|
42
|
+
skills.sort((a, b) => {
|
|
43
|
+
if (a.sourceId !== b.sourceId) return a.sourceId.localeCompare(b.sourceId);
|
|
44
|
+
return a.name.localeCompare(b.name);
|
|
45
|
+
});
|
|
46
|
+
return skills;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { scanSkillsInRepo };
|
|
50
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
function sanitizeId(id) {
|
|
2
|
+
return String(id).replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function parseGitHubRef(input) {
|
|
6
|
+
const raw = String(input || '').trim();
|
|
7
|
+
if (!raw) return null;
|
|
8
|
+
|
|
9
|
+
// owner/repo
|
|
10
|
+
const mRef = raw.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/);
|
|
11
|
+
if (mRef) {
|
|
12
|
+
const owner = mRef[1];
|
|
13
|
+
const repo = mRef[2];
|
|
14
|
+
return {
|
|
15
|
+
kind: 'github',
|
|
16
|
+
owner,
|
|
17
|
+
repo,
|
|
18
|
+
openskillsRef: `${owner}/${repo}`,
|
|
19
|
+
httpsRepo: `https://github.com/${owner}/${repo}.git`
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// https://github.com/owner/repo(.git)?
|
|
24
|
+
const mHttps = raw.match(/^https?:\/\/github\.com\/([^/]+)\/([^/#?]+?)(?:\.git)?\/?$/i);
|
|
25
|
+
if (mHttps) {
|
|
26
|
+
const owner = mHttps[1];
|
|
27
|
+
const repo = mHttps[2];
|
|
28
|
+
return {
|
|
29
|
+
kind: 'github',
|
|
30
|
+
owner,
|
|
31
|
+
repo,
|
|
32
|
+
openskillsRef: `${owner}/${repo}`,
|
|
33
|
+
httpsRepo: `https://github.com/${owner}/${repo}.git`
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// git@github.com:owner/repo(.git)
|
|
38
|
+
const mSsh = raw.match(/^git@github\.com:([^/]+)\/([^/#?]+?)(?:\.git)?$/i);
|
|
39
|
+
if (mSsh) {
|
|
40
|
+
const owner = mSsh[1];
|
|
41
|
+
const repo = mSsh[2];
|
|
42
|
+
return {
|
|
43
|
+
kind: 'github',
|
|
44
|
+
owner,
|
|
45
|
+
repo,
|
|
46
|
+
openskillsRef: `${owner}/${repo}`,
|
|
47
|
+
httpsRepo: `https://github.com/${owner}/${repo}.git`,
|
|
48
|
+
sshRepo: `git@github.com:${owner}/${repo}.git`
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function defaultSourceIdFromInput(input) {
|
|
56
|
+
const gh = parseGitHubRef(input);
|
|
57
|
+
if (gh) return sanitizeId(`${gh.owner}-${gh.repo}`);
|
|
58
|
+
return sanitizeId(input);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { sanitizeId, parseGitHubRef, defaultSourceIdFromInput };
|
|
62
|
+
|