add-skill-kit 3.2.6 → 3.2.8
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/bin/kit.js +89 -89
- package/bin/lib/agents.js +208 -208
- package/bin/lib/commands/analyze.js +70 -70
- package/bin/lib/commands/cache.js +65 -65
- package/bin/lib/commands/doctor.js +75 -75
- package/bin/lib/commands/help.js +155 -155
- package/bin/lib/commands/info.js +38 -38
- package/bin/lib/commands/init.js +39 -39
- package/bin/lib/commands/install.js +803 -803
- package/bin/lib/commands/list.js +43 -43
- package/bin/lib/commands/lock.js +57 -57
- package/bin/lib/commands/uninstall.js +307 -307
- package/bin/lib/commands/update.js +55 -55
- package/bin/lib/commands/validate.js +69 -69
- package/bin/lib/commands/verify.js +56 -56
- package/bin/lib/config.js +81 -81
- package/bin/lib/helpers.js +196 -196
- package/bin/lib/helpers.test.js +60 -60
- package/bin/lib/installer.js +164 -164
- package/bin/lib/skills.js +119 -119
- package/bin/lib/skills.test.js +109 -109
- package/bin/lib/types.js +82 -82
- package/bin/lib/ui.js +329 -329
- package/package.json +3 -2
package/bin/lib/installer.js
CHANGED
|
@@ -1,164 +1,164 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { homedir } from "os";
|
|
4
|
-
import { GLOBAL_DIR } from "./config.js";
|
|
5
|
-
import { merkleHash } from "./helpers.js";
|
|
6
|
-
import { AGENTS } from "./agents.js";
|
|
7
|
-
|
|
8
|
-
const home = homedir();
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Install a skill to the destination using the specified method.
|
|
12
|
-
* @param {string} src - Source directory (temp)
|
|
13
|
-
* @param {string} dest - Destination directory (project)
|
|
14
|
-
* @param {string} method - 'symlink' or 'copy'
|
|
15
|
-
* @param {Object} metadata - Metadata for .skill-source.json
|
|
16
|
-
*/
|
|
17
|
-
export async function installSkill(src, dest, method, metadata) {
|
|
18
|
-
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
|
|
19
|
-
|
|
20
|
-
if (method === "symlink") {
|
|
21
|
-
// For symlink: Move to global persistent storage first
|
|
22
|
-
// Storage path: ~/.gemini/antigravity/skills/storage/<org>/<repo>/<skill>
|
|
23
|
-
// Metadata must contain org, repo, skill
|
|
24
|
-
const { repo: repoStr, skill } = metadata;
|
|
25
|
-
const [org, repo] = repoStr.split("/");
|
|
26
|
-
|
|
27
|
-
const storageBase = path.join(GLOBAL_DIR, "storage", org, repo, skill);
|
|
28
|
-
|
|
29
|
-
// Ensure fresh copy in storage
|
|
30
|
-
if (fs.existsSync(storageBase)) fs.rmSync(storageBase, { recursive: true, force: true });
|
|
31
|
-
fs.mkdirSync(path.dirname(storageBase), { recursive: true });
|
|
32
|
-
|
|
33
|
-
// Copy from tmp to storage
|
|
34
|
-
await fs.promises.cp(src, storageBase, { recursive: true });
|
|
35
|
-
|
|
36
|
-
// Create junction
|
|
37
|
-
fs.symlinkSync(storageBase, dest, "junction");
|
|
38
|
-
} else {
|
|
39
|
-
// Copy directly
|
|
40
|
-
await fs.promises.cp(src, dest, { recursive: true });
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Write metadata
|
|
44
|
-
const hash = merkleHash(dest);
|
|
45
|
-
const metaFile = path.join(dest, ".skill-source.json");
|
|
46
|
-
|
|
47
|
-
fs.writeFileSync(metaFile, JSON.stringify({
|
|
48
|
-
...metadata,
|
|
49
|
-
checksum: hash,
|
|
50
|
-
installedAt: new Date().toISOString(),
|
|
51
|
-
method: method
|
|
52
|
-
}, null, 2));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Install a skill to multiple agents
|
|
57
|
-
* @param {string} src - Source directory containing the skill
|
|
58
|
-
* @param {string} skillName - Name of the skill
|
|
59
|
-
* @param {Array<{name: string, displayName: string, skillsDir: string, globalSkillsDir: string}>} agents - Agents to install to
|
|
60
|
-
* @param {Object} options - Installation options
|
|
61
|
-
* @param {string} options.method - 'symlink' or 'copy'
|
|
62
|
-
* @param {string} options.scope - 'project' or 'global'
|
|
63
|
-
* @param {Object} options.metadata - Metadata for tracking
|
|
64
|
-
* @returns {Promise<{success: Array, failed: Array}>}
|
|
65
|
-
*/
|
|
66
|
-
export async function installSkillForAgents(src, skillName, agents, options = {}) {
|
|
67
|
-
const { method = "symlink", scope = "project", metadata = {} } = options;
|
|
68
|
-
const results = { success: [], failed: [] };
|
|
69
|
-
|
|
70
|
-
// For symlink mode: first copy to canonical location
|
|
71
|
-
let canonicalPath = null;
|
|
72
|
-
|
|
73
|
-
if (method === "symlink") {
|
|
74
|
-
// Canonical: .agents/skills/<skill-name> or ~/.agents/skills/<skill-name>
|
|
75
|
-
const baseDir = scope === "global" ? home : process.cwd();
|
|
76
|
-
canonicalPath = path.join(baseDir, ".agents", "skills", skillName);
|
|
77
|
-
|
|
78
|
-
// Ensure fresh copy in canonical
|
|
79
|
-
if (fs.existsSync(canonicalPath)) {
|
|
80
|
-
fs.rmSync(canonicalPath, { recursive: true, force: true });
|
|
81
|
-
}
|
|
82
|
-
fs.mkdirSync(path.dirname(canonicalPath), { recursive: true });
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
await fs.promises.cp(src, canonicalPath, { recursive: true });
|
|
86
|
-
|
|
87
|
-
// Write metadata to canonical location
|
|
88
|
-
const hash = merkleHash(canonicalPath);
|
|
89
|
-
const metaFile = path.join(canonicalPath, ".skill-source.json");
|
|
90
|
-
fs.writeFileSync(metaFile, JSON.stringify({
|
|
91
|
-
...metadata,
|
|
92
|
-
skillName,
|
|
93
|
-
checksum: hash,
|
|
94
|
-
installedAt: new Date().toISOString(),
|
|
95
|
-
method: method,
|
|
96
|
-
scope: scope,
|
|
97
|
-
agents: agents.map(a => a.name)
|
|
98
|
-
}, null, 2));
|
|
99
|
-
} catch (err) {
|
|
100
|
-
// If canonical copy fails, abort
|
|
101
|
-
return {
|
|
102
|
-
success: [],
|
|
103
|
-
failed: agents.map(a => ({ agent: a.displayName, error: `Canonical copy failed: ${err.message}` }))
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Install to each agent
|
|
109
|
-
for (const agent of agents) {
|
|
110
|
-
const agentConfig = AGENTS[agent.name];
|
|
111
|
-
if (!agentConfig) {
|
|
112
|
-
results.failed.push({ agent: agent.displayName, error: "Unknown agent" });
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Determine destination path
|
|
117
|
-
const baseDir = scope === "global" ? agentConfig.globalSkillsDir : path.join(process.cwd(), agentConfig.skillsDir);
|
|
118
|
-
const destPath = path.join(baseDir, skillName);
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
// Ensure parent directory exists
|
|
122
|
-
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
123
|
-
|
|
124
|
-
// Remove existing if any
|
|
125
|
-
if (fs.existsSync(destPath)) {
|
|
126
|
-
fs.rmSync(destPath, { recursive: true, force: true });
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (method === "symlink" && canonicalPath) {
|
|
130
|
-
// Create symlink to canonical location
|
|
131
|
-
try {
|
|
132
|
-
fs.symlinkSync(canonicalPath, destPath, "junction");
|
|
133
|
-
results.success.push({ agent: agent.displayName, path: destPath, mode: "symlink" });
|
|
134
|
-
} catch (symlinkErr) {
|
|
135
|
-
// Fallback to copy if symlink fails (Windows permissions)
|
|
136
|
-
await fs.promises.cp(canonicalPath, destPath, { recursive: true });
|
|
137
|
-
results.success.push({ agent: agent.displayName, path: destPath, mode: "copy (symlink failed)" });
|
|
138
|
-
}
|
|
139
|
-
} else {
|
|
140
|
-
// Direct copy
|
|
141
|
-
await fs.promises.cp(src, destPath, { recursive: true });
|
|
142
|
-
|
|
143
|
-
// Write metadata
|
|
144
|
-
const hash = merkleHash(destPath);
|
|
145
|
-
const metaFile = path.join(destPath, ".skill-source.json");
|
|
146
|
-
fs.writeFileSync(metaFile, JSON.stringify({
|
|
147
|
-
...metadata,
|
|
148
|
-
skillName,
|
|
149
|
-
checksum: hash,
|
|
150
|
-
installedAt: new Date().toISOString(),
|
|
151
|
-
method: "copy",
|
|
152
|
-
scope: scope
|
|
153
|
-
}, null, 2));
|
|
154
|
-
|
|
155
|
-
results.success.push({ agent: agent.displayName, path: destPath, mode: "copy" });
|
|
156
|
-
}
|
|
157
|
-
} catch (err) {
|
|
158
|
-
results.failed.push({ agent: agent.displayName, error: err.message });
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return results;
|
|
163
|
-
}
|
|
164
|
-
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { GLOBAL_DIR } from "./config.js";
|
|
5
|
+
import { merkleHash } from "./helpers.js";
|
|
6
|
+
import { AGENTS } from "./agents.js";
|
|
7
|
+
|
|
8
|
+
const home = homedir();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Install a skill to the destination using the specified method.
|
|
12
|
+
* @param {string} src - Source directory (temp)
|
|
13
|
+
* @param {string} dest - Destination directory (project)
|
|
14
|
+
* @param {string} method - 'symlink' or 'copy'
|
|
15
|
+
* @param {Object} metadata - Metadata for .skill-source.json
|
|
16
|
+
*/
|
|
17
|
+
export async function installSkill(src, dest, method, metadata) {
|
|
18
|
+
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
|
|
19
|
+
|
|
20
|
+
if (method === "symlink") {
|
|
21
|
+
// For symlink: Move to global persistent storage first
|
|
22
|
+
// Storage path: ~/.gemini/antigravity/skills/storage/<org>/<repo>/<skill>
|
|
23
|
+
// Metadata must contain org, repo, skill
|
|
24
|
+
const { repo: repoStr, skill } = metadata;
|
|
25
|
+
const [org, repo] = repoStr.split("/");
|
|
26
|
+
|
|
27
|
+
const storageBase = path.join(GLOBAL_DIR, "storage", org, repo, skill);
|
|
28
|
+
|
|
29
|
+
// Ensure fresh copy in storage
|
|
30
|
+
if (fs.existsSync(storageBase)) fs.rmSync(storageBase, { recursive: true, force: true });
|
|
31
|
+
fs.mkdirSync(path.dirname(storageBase), { recursive: true });
|
|
32
|
+
|
|
33
|
+
// Copy from tmp to storage
|
|
34
|
+
await fs.promises.cp(src, storageBase, { recursive: true });
|
|
35
|
+
|
|
36
|
+
// Create junction
|
|
37
|
+
fs.symlinkSync(storageBase, dest, "junction");
|
|
38
|
+
} else {
|
|
39
|
+
// Copy directly
|
|
40
|
+
await fs.promises.cp(src, dest, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Write metadata
|
|
44
|
+
const hash = merkleHash(dest);
|
|
45
|
+
const metaFile = path.join(dest, ".skill-source.json");
|
|
46
|
+
|
|
47
|
+
fs.writeFileSync(metaFile, JSON.stringify({
|
|
48
|
+
...metadata,
|
|
49
|
+
checksum: hash,
|
|
50
|
+
installedAt: new Date().toISOString(),
|
|
51
|
+
method: method
|
|
52
|
+
}, null, 2));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Install a skill to multiple agents
|
|
57
|
+
* @param {string} src - Source directory containing the skill
|
|
58
|
+
* @param {string} skillName - Name of the skill
|
|
59
|
+
* @param {Array<{name: string, displayName: string, skillsDir: string, globalSkillsDir: string}>} agents - Agents to install to
|
|
60
|
+
* @param {Object} options - Installation options
|
|
61
|
+
* @param {string} options.method - 'symlink' or 'copy'
|
|
62
|
+
* @param {string} options.scope - 'project' or 'global'
|
|
63
|
+
* @param {Object} options.metadata - Metadata for tracking
|
|
64
|
+
* @returns {Promise<{success: Array, failed: Array}>}
|
|
65
|
+
*/
|
|
66
|
+
export async function installSkillForAgents(src, skillName, agents, options = {}) {
|
|
67
|
+
const { method = "symlink", scope = "project", metadata = {} } = options;
|
|
68
|
+
const results = { success: [], failed: [] };
|
|
69
|
+
|
|
70
|
+
// For symlink mode: first copy to canonical location
|
|
71
|
+
let canonicalPath = null;
|
|
72
|
+
|
|
73
|
+
if (method === "symlink") {
|
|
74
|
+
// Canonical: .agents/skills/<skill-name> or ~/.agents/skills/<skill-name>
|
|
75
|
+
const baseDir = scope === "global" ? home : process.cwd();
|
|
76
|
+
canonicalPath = path.join(baseDir, ".agents", "skills", skillName);
|
|
77
|
+
|
|
78
|
+
// Ensure fresh copy in canonical
|
|
79
|
+
if (fs.existsSync(canonicalPath)) {
|
|
80
|
+
fs.rmSync(canonicalPath, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
fs.mkdirSync(path.dirname(canonicalPath), { recursive: true });
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await fs.promises.cp(src, canonicalPath, { recursive: true });
|
|
86
|
+
|
|
87
|
+
// Write metadata to canonical location
|
|
88
|
+
const hash = merkleHash(canonicalPath);
|
|
89
|
+
const metaFile = path.join(canonicalPath, ".skill-source.json");
|
|
90
|
+
fs.writeFileSync(metaFile, JSON.stringify({
|
|
91
|
+
...metadata,
|
|
92
|
+
skillName,
|
|
93
|
+
checksum: hash,
|
|
94
|
+
installedAt: new Date().toISOString(),
|
|
95
|
+
method: method,
|
|
96
|
+
scope: scope,
|
|
97
|
+
agents: agents.map(a => a.name)
|
|
98
|
+
}, null, 2));
|
|
99
|
+
} catch (err) {
|
|
100
|
+
// If canonical copy fails, abort
|
|
101
|
+
return {
|
|
102
|
+
success: [],
|
|
103
|
+
failed: agents.map(a => ({ agent: a.displayName, error: `Canonical copy failed: ${err.message}` }))
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Install to each agent
|
|
109
|
+
for (const agent of agents) {
|
|
110
|
+
const agentConfig = AGENTS[agent.name];
|
|
111
|
+
if (!agentConfig) {
|
|
112
|
+
results.failed.push({ agent: agent.displayName, error: "Unknown agent" });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Determine destination path
|
|
117
|
+
const baseDir = scope === "global" ? agentConfig.globalSkillsDir : path.join(process.cwd(), agentConfig.skillsDir);
|
|
118
|
+
const destPath = path.join(baseDir, skillName);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
// Ensure parent directory exists
|
|
122
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
123
|
+
|
|
124
|
+
// Remove existing if any
|
|
125
|
+
if (fs.existsSync(destPath)) {
|
|
126
|
+
fs.rmSync(destPath, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (method === "symlink" && canonicalPath) {
|
|
130
|
+
// Create symlink to canonical location
|
|
131
|
+
try {
|
|
132
|
+
fs.symlinkSync(canonicalPath, destPath, "junction");
|
|
133
|
+
results.success.push({ agent: agent.displayName, path: destPath, mode: "symlink" });
|
|
134
|
+
} catch (symlinkErr) {
|
|
135
|
+
// Fallback to copy if symlink fails (Windows permissions)
|
|
136
|
+
await fs.promises.cp(canonicalPath, destPath, { recursive: true });
|
|
137
|
+
results.success.push({ agent: agent.displayName, path: destPath, mode: "copy (symlink failed)" });
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
// Direct copy
|
|
141
|
+
await fs.promises.cp(src, destPath, { recursive: true });
|
|
142
|
+
|
|
143
|
+
// Write metadata
|
|
144
|
+
const hash = merkleHash(destPath);
|
|
145
|
+
const metaFile = path.join(destPath, ".skill-source.json");
|
|
146
|
+
fs.writeFileSync(metaFile, JSON.stringify({
|
|
147
|
+
...metadata,
|
|
148
|
+
skillName,
|
|
149
|
+
checksum: hash,
|
|
150
|
+
installedAt: new Date().toISOString(),
|
|
151
|
+
method: "copy",
|
|
152
|
+
scope: scope
|
|
153
|
+
}, null, 2));
|
|
154
|
+
|
|
155
|
+
results.success.push({ agent: agent.displayName, path: destPath, mode: "copy" });
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
results.failed.push({ agent: agent.displayName, error: err.message });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return results;
|
|
163
|
+
}
|
|
164
|
+
|
package/bin/lib/skills.js
CHANGED
|
@@ -1,119 +1,119 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Skill detection and parsing
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import fs from "fs";
|
|
6
|
-
import path from "path";
|
|
7
|
-
import { resolveScope } from "./helpers.js";
|
|
8
|
-
import { getDirSize } from "./helpers.js";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Parse SKILL.md YAML frontmatter
|
|
12
|
-
* @param {string} p - Path to SKILL.md
|
|
13
|
-
* @returns {import('./types.js').SkillMeta}
|
|
14
|
-
*/
|
|
15
|
-
export function parseSkillMdFrontmatter(p) {
|
|
16
|
-
try {
|
|
17
|
-
const content = fs.readFileSync(p, "utf-8");
|
|
18
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
19
|
-
if (!match) return {};
|
|
20
|
-
|
|
21
|
-
/** @type {import('./types.js').SkillMeta} */
|
|
22
|
-
const meta = {};
|
|
23
|
-
|
|
24
|
-
for (const line of match[1].split(/\r?\n/)) {
|
|
25
|
-
const i = line.indexOf(":");
|
|
26
|
-
if (i === -1) continue;
|
|
27
|
-
const key = line.substring(0, i).trim();
|
|
28
|
-
const val = line.substring(i + 1).trim();
|
|
29
|
-
if (key === "tags") meta.tags = val.split(",").map(t => t.trim()).filter(Boolean);
|
|
30
|
-
else if (key && val) meta[key] = val;
|
|
31
|
-
}
|
|
32
|
-
return meta;
|
|
33
|
-
} catch (err) {
|
|
34
|
-
if (process.env.DEBUG) console.error(`parseSkillMdFrontmatter error: ${err.message}`);
|
|
35
|
-
return {};
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Detect skill directory structure
|
|
41
|
-
* @param {string} dir - Skill directory
|
|
42
|
-
* @returns {import('./types.js').SkillStructure}
|
|
43
|
-
*/
|
|
44
|
-
export function detectSkillStructure(dir) {
|
|
45
|
-
/** @type {import('./types.js').SkillStructure} */
|
|
46
|
-
const s = {
|
|
47
|
-
hasResources: false,
|
|
48
|
-
hasExamples: false,
|
|
49
|
-
hasScripts: false,
|
|
50
|
-
hasConstitution: false,
|
|
51
|
-
hasDoctrines: false,
|
|
52
|
-
hasEnforcement: false,
|
|
53
|
-
hasProposals: false,
|
|
54
|
-
directories: [],
|
|
55
|
-
files: []
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
for (const item of fs.readdirSync(dir)) {
|
|
60
|
-
const full = path.join(dir, item);
|
|
61
|
-
if (fs.statSync(full).isDirectory()) {
|
|
62
|
-
s.directories.push(item);
|
|
63
|
-
const l = item.toLowerCase();
|
|
64
|
-
if (l === "resources") s.hasResources = true;
|
|
65
|
-
if (l === "examples") s.hasExamples = true;
|
|
66
|
-
if (l === "scripts") s.hasScripts = true;
|
|
67
|
-
if (l === "constitution") s.hasConstitution = true;
|
|
68
|
-
if (l === "doctrines") s.hasDoctrines = true;
|
|
69
|
-
if (l === "enforcement") s.hasEnforcement = true;
|
|
70
|
-
if (l === "proposals") s.hasProposals = true;
|
|
71
|
-
} else {
|
|
72
|
-
s.files.push(item);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
} catch (err) {
|
|
76
|
-
if (process.env.DEBUG) console.error(`detectSkillStructure error: ${err.message}`);
|
|
77
|
-
}
|
|
78
|
-
return s;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Get all installed skills
|
|
83
|
-
* @returns {import('./types.js').Skill[]}
|
|
84
|
-
*/
|
|
85
|
-
export function getInstalledSkills() {
|
|
86
|
-
const scope = resolveScope();
|
|
87
|
-
/** @type {import('./types.js').Skill[]} */
|
|
88
|
-
const skills = [];
|
|
89
|
-
|
|
90
|
-
if (!fs.existsSync(scope)) return skills;
|
|
91
|
-
|
|
92
|
-
for (const name of fs.readdirSync(scope)) {
|
|
93
|
-
const dir = path.join(scope, name);
|
|
94
|
-
if (!fs.statSync(dir).isDirectory()) continue;
|
|
95
|
-
|
|
96
|
-
const metaFile = path.join(dir, ".skill-source.json");
|
|
97
|
-
const skillFile = path.join(dir, "SKILL.md");
|
|
98
|
-
|
|
99
|
-
if (fs.existsSync(metaFile) || fs.existsSync(skillFile)) {
|
|
100
|
-
const meta = fs.existsSync(metaFile) ? JSON.parse(fs.readFileSync(metaFile, "utf-8")) : {};
|
|
101
|
-
const hasSkillMd = fs.existsSync(skillFile);
|
|
102
|
-
const skillMeta = hasSkillMd ? parseSkillMdFrontmatter(skillFile) : {};
|
|
103
|
-
|
|
104
|
-
skills.push({
|
|
105
|
-
name,
|
|
106
|
-
path: dir,
|
|
107
|
-
...meta,
|
|
108
|
-
hasSkillMd,
|
|
109
|
-
description: skillMeta.description || meta.description || "",
|
|
110
|
-
tags: skillMeta.tags || [],
|
|
111
|
-
author: skillMeta.author || meta.publisher || "",
|
|
112
|
-
version: skillMeta.version || meta.ref || "unknown",
|
|
113
|
-
structure: detectSkillStructure(dir),
|
|
114
|
-
size: getDirSize(dir)
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return skills;
|
|
119
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Skill detection and parsing
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { resolveScope } from "./helpers.js";
|
|
8
|
+
import { getDirSize } from "./helpers.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse SKILL.md YAML frontmatter
|
|
12
|
+
* @param {string} p - Path to SKILL.md
|
|
13
|
+
* @returns {import('./types.js').SkillMeta}
|
|
14
|
+
*/
|
|
15
|
+
export function parseSkillMdFrontmatter(p) {
|
|
16
|
+
try {
|
|
17
|
+
const content = fs.readFileSync(p, "utf-8");
|
|
18
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
19
|
+
if (!match) return {};
|
|
20
|
+
|
|
21
|
+
/** @type {import('./types.js').SkillMeta} */
|
|
22
|
+
const meta = {};
|
|
23
|
+
|
|
24
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
25
|
+
const i = line.indexOf(":");
|
|
26
|
+
if (i === -1) continue;
|
|
27
|
+
const key = line.substring(0, i).trim();
|
|
28
|
+
const val = line.substring(i + 1).trim();
|
|
29
|
+
if (key === "tags") meta.tags = val.split(",").map(t => t.trim()).filter(Boolean);
|
|
30
|
+
else if (key && val) meta[key] = val;
|
|
31
|
+
}
|
|
32
|
+
return meta;
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (process.env.DEBUG) console.error(`parseSkillMdFrontmatter error: ${err.message}`);
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Detect skill directory structure
|
|
41
|
+
* @param {string} dir - Skill directory
|
|
42
|
+
* @returns {import('./types.js').SkillStructure}
|
|
43
|
+
*/
|
|
44
|
+
export function detectSkillStructure(dir) {
|
|
45
|
+
/** @type {import('./types.js').SkillStructure} */
|
|
46
|
+
const s = {
|
|
47
|
+
hasResources: false,
|
|
48
|
+
hasExamples: false,
|
|
49
|
+
hasScripts: false,
|
|
50
|
+
hasConstitution: false,
|
|
51
|
+
hasDoctrines: false,
|
|
52
|
+
hasEnforcement: false,
|
|
53
|
+
hasProposals: false,
|
|
54
|
+
directories: [],
|
|
55
|
+
files: []
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
for (const item of fs.readdirSync(dir)) {
|
|
60
|
+
const full = path.join(dir, item);
|
|
61
|
+
if (fs.statSync(full).isDirectory()) {
|
|
62
|
+
s.directories.push(item);
|
|
63
|
+
const l = item.toLowerCase();
|
|
64
|
+
if (l === "resources") s.hasResources = true;
|
|
65
|
+
if (l === "examples") s.hasExamples = true;
|
|
66
|
+
if (l === "scripts") s.hasScripts = true;
|
|
67
|
+
if (l === "constitution") s.hasConstitution = true;
|
|
68
|
+
if (l === "doctrines") s.hasDoctrines = true;
|
|
69
|
+
if (l === "enforcement") s.hasEnforcement = true;
|
|
70
|
+
if (l === "proposals") s.hasProposals = true;
|
|
71
|
+
} else {
|
|
72
|
+
s.files.push(item);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (process.env.DEBUG) console.error(`detectSkillStructure error: ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
return s;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get all installed skills
|
|
83
|
+
* @returns {import('./types.js').Skill[]}
|
|
84
|
+
*/
|
|
85
|
+
export function getInstalledSkills() {
|
|
86
|
+
const scope = resolveScope();
|
|
87
|
+
/** @type {import('./types.js').Skill[]} */
|
|
88
|
+
const skills = [];
|
|
89
|
+
|
|
90
|
+
if (!fs.existsSync(scope)) return skills;
|
|
91
|
+
|
|
92
|
+
for (const name of fs.readdirSync(scope)) {
|
|
93
|
+
const dir = path.join(scope, name);
|
|
94
|
+
if (!fs.statSync(dir).isDirectory()) continue;
|
|
95
|
+
|
|
96
|
+
const metaFile = path.join(dir, ".skill-source.json");
|
|
97
|
+
const skillFile = path.join(dir, "SKILL.md");
|
|
98
|
+
|
|
99
|
+
if (fs.existsSync(metaFile) || fs.existsSync(skillFile)) {
|
|
100
|
+
const meta = fs.existsSync(metaFile) ? JSON.parse(fs.readFileSync(metaFile, "utf-8")) : {};
|
|
101
|
+
const hasSkillMd = fs.existsSync(skillFile);
|
|
102
|
+
const skillMeta = hasSkillMd ? parseSkillMdFrontmatter(skillFile) : {};
|
|
103
|
+
|
|
104
|
+
skills.push({
|
|
105
|
+
name,
|
|
106
|
+
path: dir,
|
|
107
|
+
...meta,
|
|
108
|
+
hasSkillMd,
|
|
109
|
+
description: skillMeta.description || meta.description || "",
|
|
110
|
+
tags: skillMeta.tags || [],
|
|
111
|
+
author: skillMeta.author || meta.publisher || "",
|
|
112
|
+
version: skillMeta.version || meta.ref || "unknown",
|
|
113
|
+
structure: detectSkillStructure(dir),
|
|
114
|
+
size: getDirSize(dir)
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return skills;
|
|
119
|
+
}
|