add-skill-kit 3.2.7 → 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 +1 -1
package/bin/lib/helpers.js
CHANGED
|
@@ -1,196 +1,196 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Utility helper functions
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import fs from "fs";
|
|
6
|
-
import path from "path";
|
|
7
|
-
import crypto from "crypto";
|
|
8
|
-
import { BACKUP_DIR, DRY, cwd, GLOBAL, WORKSPACE, GLOBAL_DIR, REGISTRIES_FILE } from "./config.js";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Get directory size recursively
|
|
12
|
-
* @param {string} dir - Directory path
|
|
13
|
-
* @returns {number} Size in bytes
|
|
14
|
-
*/
|
|
15
|
-
export function getDirSize(dir) {
|
|
16
|
-
let size = 0;
|
|
17
|
-
try {
|
|
18
|
-
const walk = (p) => {
|
|
19
|
-
for (const f of fs.readdirSync(p)) {
|
|
20
|
-
const full = path.join(p, f);
|
|
21
|
-
const stat = fs.statSync(full);
|
|
22
|
-
if (stat.isDirectory()) walk(full);
|
|
23
|
-
else size += stat.size;
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
walk(dir);
|
|
27
|
-
} catch (err) {
|
|
28
|
-
// Directory may not exist or be inaccessible
|
|
29
|
-
if (process.env.DEBUG) console.error(`getDirSize error: ${err.message}`);
|
|
30
|
-
}
|
|
31
|
-
return size;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Format bytes to human readable
|
|
36
|
-
* @param {number} bytes
|
|
37
|
-
* @returns {string}
|
|
38
|
-
*/
|
|
39
|
-
export function formatBytes(bytes) {
|
|
40
|
-
if (bytes < 1024) return bytes + " B";
|
|
41
|
-
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
|
42
|
-
return (bytes / 1024 / 1024).toFixed(1) + " MB";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Format ISO date string
|
|
47
|
-
* @param {string} [iso]
|
|
48
|
-
* @returns {string}
|
|
49
|
-
*/
|
|
50
|
-
export function formatDate(iso) {
|
|
51
|
-
return iso ? new Date(iso).toLocaleDateString() : "unknown";
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Calculate merkle hash of directory
|
|
56
|
-
* @param {string} dir - Directory path
|
|
57
|
-
* @returns {string} SHA256 hash
|
|
58
|
-
*/
|
|
59
|
-
export function merkleHash(dir) {
|
|
60
|
-
const files = [];
|
|
61
|
-
const walk = (p) => {
|
|
62
|
-
for (const f of fs.readdirSync(p)) {
|
|
63
|
-
if (f === ".skill-source.json") continue;
|
|
64
|
-
const full = path.join(p, f);
|
|
65
|
-
const stat = fs.statSync(full);
|
|
66
|
-
if (stat.isDirectory()) walk(full);
|
|
67
|
-
else {
|
|
68
|
-
const h = crypto.createHash("sha256").update(fs.readFileSync(full)).digest("hex");
|
|
69
|
-
files.push(`${path.relative(dir, full)}:${h}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
walk(dir);
|
|
74
|
-
files.sort();
|
|
75
|
-
return crypto.createHash("sha256").update(files.join("|")).digest("hex");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Parse skill spec string
|
|
80
|
-
* @param {string} spec - Spec like org/repo#skill@ref
|
|
81
|
-
* @returns {import('./types.js').ParsedSpec}
|
|
82
|
-
*/
|
|
83
|
-
export function parseSkillSpec(spec) {
|
|
84
|
-
if (!spec || typeof spec !== 'string') {
|
|
85
|
-
throw new Error('Skill spec is required');
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const [repoPart, skillPart] = spec.split("#");
|
|
89
|
-
|
|
90
|
-
if (!repoPart.includes('/')) {
|
|
91
|
-
throw new Error(`Invalid spec format: "${spec}". Expected: org/repo or org/repo#skill`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const [org, repo] = repoPart.split("/");
|
|
95
|
-
|
|
96
|
-
if (!org || !repo) {
|
|
97
|
-
throw new Error(`Invalid spec: missing org or repo in "${spec}"`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const [skill, ref] = (skillPart || "").split("@");
|
|
101
|
-
return { org, repo, skill: skill || undefined, ref: ref || undefined };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Resolve scope based on flags and cwd
|
|
106
|
-
* @returns {string} Skills directory path
|
|
107
|
-
*/
|
|
108
|
-
export function resolveScope() {
|
|
109
|
-
if (GLOBAL) return GLOBAL_DIR;
|
|
110
|
-
if (fs.existsSync(path.join(cwd, ".agent"))) return WORKSPACE;
|
|
111
|
-
return GLOBAL_DIR;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Create backup of skill directory
|
|
116
|
-
* @param {string} skillDir - Source directory
|
|
117
|
-
* @param {string} skillName - Skill name for backup naming
|
|
118
|
-
* @returns {string|null} Backup path or null if dry run
|
|
119
|
-
*/
|
|
120
|
-
export function createBackup(skillDir, skillName) {
|
|
121
|
-
if (DRY) return null;
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
125
|
-
const bp = path.join(BACKUP_DIR, `${skillName}_${ts}`);
|
|
126
|
-
fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
|
127
|
-
|
|
128
|
-
// Resolve symlink to real path (Windows compatibility)
|
|
129
|
-
const realPath = fs.realpathSync(skillDir);
|
|
130
|
-
|
|
131
|
-
fs.cpSync(realPath, bp, { recursive: true });
|
|
132
|
-
return bp;
|
|
133
|
-
} catch (err) {
|
|
134
|
-
// Fallback: try direct copy if realpath fails
|
|
135
|
-
try {
|
|
136
|
-
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
137
|
-
const bp = path.join(BACKUP_DIR, `${skillName}_${ts}`);
|
|
138
|
-
fs.cpSync(skillDir, bp, { recursive: true });
|
|
139
|
-
return bp;
|
|
140
|
-
} catch (fallbackErr) {
|
|
141
|
-
if (process.env.DEBUG) {
|
|
142
|
-
console.error(`Backup failed for ${skillName}:`, fallbackErr.message);
|
|
143
|
-
}
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* List backups for a skill
|
|
151
|
-
* @param {string} [skillName] - Filter by skill name
|
|
152
|
-
* @returns {import('./types.js').Backup[]}
|
|
153
|
-
*/
|
|
154
|
-
export function listBackups(skillName = null) {
|
|
155
|
-
if (!fs.existsSync(BACKUP_DIR)) return [];
|
|
156
|
-
const backups = [];
|
|
157
|
-
for (const name of fs.readdirSync(BACKUP_DIR)) {
|
|
158
|
-
if (skillName && !name.startsWith(skillName + "_")) continue;
|
|
159
|
-
const bp = path.join(BACKUP_DIR, name);
|
|
160
|
-
backups.push({ name, path: bp, createdAt: fs.statSync(bp).mtime, size: getDirSize(bp) });
|
|
161
|
-
}
|
|
162
|
-
return backups.sort((a, b) => b.createdAt - a.createdAt);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Load skill lock file
|
|
167
|
-
* @returns {import('./types.js').SkillLock}
|
|
168
|
-
*/
|
|
169
|
-
export function loadSkillLock() {
|
|
170
|
-
const f = path.join(cwd, ".agent", "skill-lock.json");
|
|
171
|
-
if (!fs.existsSync(f)) throw new Error("skill-lock.json not found");
|
|
172
|
-
return JSON.parse(fs.readFileSync(f, "utf-8"));
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Load registries list
|
|
177
|
-
* @returns {string[]}
|
|
178
|
-
*/
|
|
179
|
-
export function loadRegistries() {
|
|
180
|
-
try {
|
|
181
|
-
return fs.existsSync(REGISTRIES_FILE) ? JSON.parse(fs.readFileSync(REGISTRIES_FILE, "utf-8")) : [];
|
|
182
|
-
} catch (err) {
|
|
183
|
-
if (process.env.DEBUG) console.error(`loadRegistries error: ${err.message}`);
|
|
184
|
-
return [];
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Save registries list
|
|
190
|
-
* @param {string[]} regs
|
|
191
|
-
*/
|
|
192
|
-
export function saveRegistries(regs) {
|
|
193
|
-
fs.mkdirSync(path.dirname(REGISTRIES_FILE), { recursive: true });
|
|
194
|
-
fs.writeFileSync(REGISTRIES_FILE, JSON.stringify(regs, null, 2));
|
|
195
|
-
}
|
|
196
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Utility helper functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import crypto from "crypto";
|
|
8
|
+
import { BACKUP_DIR, DRY, cwd, GLOBAL, WORKSPACE, GLOBAL_DIR, REGISTRIES_FILE } from "./config.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get directory size recursively
|
|
12
|
+
* @param {string} dir - Directory path
|
|
13
|
+
* @returns {number} Size in bytes
|
|
14
|
+
*/
|
|
15
|
+
export function getDirSize(dir) {
|
|
16
|
+
let size = 0;
|
|
17
|
+
try {
|
|
18
|
+
const walk = (p) => {
|
|
19
|
+
for (const f of fs.readdirSync(p)) {
|
|
20
|
+
const full = path.join(p, f);
|
|
21
|
+
const stat = fs.statSync(full);
|
|
22
|
+
if (stat.isDirectory()) walk(full);
|
|
23
|
+
else size += stat.size;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
walk(dir);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
// Directory may not exist or be inaccessible
|
|
29
|
+
if (process.env.DEBUG) console.error(`getDirSize error: ${err.message}`);
|
|
30
|
+
}
|
|
31
|
+
return size;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Format bytes to human readable
|
|
36
|
+
* @param {number} bytes
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
export function formatBytes(bytes) {
|
|
40
|
+
if (bytes < 1024) return bytes + " B";
|
|
41
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
|
42
|
+
return (bytes / 1024 / 1024).toFixed(1) + " MB";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Format ISO date string
|
|
47
|
+
* @param {string} [iso]
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
export function formatDate(iso) {
|
|
51
|
+
return iso ? new Date(iso).toLocaleDateString() : "unknown";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Calculate merkle hash of directory
|
|
56
|
+
* @param {string} dir - Directory path
|
|
57
|
+
* @returns {string} SHA256 hash
|
|
58
|
+
*/
|
|
59
|
+
export function merkleHash(dir) {
|
|
60
|
+
const files = [];
|
|
61
|
+
const walk = (p) => {
|
|
62
|
+
for (const f of fs.readdirSync(p)) {
|
|
63
|
+
if (f === ".skill-source.json") continue;
|
|
64
|
+
const full = path.join(p, f);
|
|
65
|
+
const stat = fs.statSync(full);
|
|
66
|
+
if (stat.isDirectory()) walk(full);
|
|
67
|
+
else {
|
|
68
|
+
const h = crypto.createHash("sha256").update(fs.readFileSync(full)).digest("hex");
|
|
69
|
+
files.push(`${path.relative(dir, full)}:${h}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
walk(dir);
|
|
74
|
+
files.sort();
|
|
75
|
+
return crypto.createHash("sha256").update(files.join("|")).digest("hex");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parse skill spec string
|
|
80
|
+
* @param {string} spec - Spec like org/repo#skill@ref
|
|
81
|
+
* @returns {import('./types.js').ParsedSpec}
|
|
82
|
+
*/
|
|
83
|
+
export function parseSkillSpec(spec) {
|
|
84
|
+
if (!spec || typeof spec !== 'string') {
|
|
85
|
+
throw new Error('Skill spec is required');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const [repoPart, skillPart] = spec.split("#");
|
|
89
|
+
|
|
90
|
+
if (!repoPart.includes('/')) {
|
|
91
|
+
throw new Error(`Invalid spec format: "${spec}". Expected: org/repo or org/repo#skill`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const [org, repo] = repoPart.split("/");
|
|
95
|
+
|
|
96
|
+
if (!org || !repo) {
|
|
97
|
+
throw new Error(`Invalid spec: missing org or repo in "${spec}"`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const [skill, ref] = (skillPart || "").split("@");
|
|
101
|
+
return { org, repo, skill: skill || undefined, ref: ref || undefined };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve scope based on flags and cwd
|
|
106
|
+
* @returns {string} Skills directory path
|
|
107
|
+
*/
|
|
108
|
+
export function resolveScope() {
|
|
109
|
+
if (GLOBAL) return GLOBAL_DIR;
|
|
110
|
+
if (fs.existsSync(path.join(cwd, ".agent"))) return WORKSPACE;
|
|
111
|
+
return GLOBAL_DIR;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create backup of skill directory
|
|
116
|
+
* @param {string} skillDir - Source directory
|
|
117
|
+
* @param {string} skillName - Skill name for backup naming
|
|
118
|
+
* @returns {string|null} Backup path or null if dry run
|
|
119
|
+
*/
|
|
120
|
+
export function createBackup(skillDir, skillName) {
|
|
121
|
+
if (DRY) return null;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
125
|
+
const bp = path.join(BACKUP_DIR, `${skillName}_${ts}`);
|
|
126
|
+
fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
|
127
|
+
|
|
128
|
+
// Resolve symlink to real path (Windows compatibility)
|
|
129
|
+
const realPath = fs.realpathSync(skillDir);
|
|
130
|
+
|
|
131
|
+
fs.cpSync(realPath, bp, { recursive: true });
|
|
132
|
+
return bp;
|
|
133
|
+
} catch (err) {
|
|
134
|
+
// Fallback: try direct copy if realpath fails
|
|
135
|
+
try {
|
|
136
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
137
|
+
const bp = path.join(BACKUP_DIR, `${skillName}_${ts}`);
|
|
138
|
+
fs.cpSync(skillDir, bp, { recursive: true });
|
|
139
|
+
return bp;
|
|
140
|
+
} catch (fallbackErr) {
|
|
141
|
+
if (process.env.DEBUG) {
|
|
142
|
+
console.error(`Backup failed for ${skillName}:`, fallbackErr.message);
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* List backups for a skill
|
|
151
|
+
* @param {string} [skillName] - Filter by skill name
|
|
152
|
+
* @returns {import('./types.js').Backup[]}
|
|
153
|
+
*/
|
|
154
|
+
export function listBackups(skillName = null) {
|
|
155
|
+
if (!fs.existsSync(BACKUP_DIR)) return [];
|
|
156
|
+
const backups = [];
|
|
157
|
+
for (const name of fs.readdirSync(BACKUP_DIR)) {
|
|
158
|
+
if (skillName && !name.startsWith(skillName + "_")) continue;
|
|
159
|
+
const bp = path.join(BACKUP_DIR, name);
|
|
160
|
+
backups.push({ name, path: bp, createdAt: fs.statSync(bp).mtime, size: getDirSize(bp) });
|
|
161
|
+
}
|
|
162
|
+
return backups.sort((a, b) => b.createdAt - a.createdAt);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Load skill lock file
|
|
167
|
+
* @returns {import('./types.js').SkillLock}
|
|
168
|
+
*/
|
|
169
|
+
export function loadSkillLock() {
|
|
170
|
+
const f = path.join(cwd, ".agent", "skill-lock.json");
|
|
171
|
+
if (!fs.existsSync(f)) throw new Error("skill-lock.json not found");
|
|
172
|
+
return JSON.parse(fs.readFileSync(f, "utf-8"));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Load registries list
|
|
177
|
+
* @returns {string[]}
|
|
178
|
+
*/
|
|
179
|
+
export function loadRegistries() {
|
|
180
|
+
try {
|
|
181
|
+
return fs.existsSync(REGISTRIES_FILE) ? JSON.parse(fs.readFileSync(REGISTRIES_FILE, "utf-8")) : [];
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (process.env.DEBUG) console.error(`loadRegistries error: ${err.message}`);
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Save registries list
|
|
190
|
+
* @param {string[]} regs
|
|
191
|
+
*/
|
|
192
|
+
export function saveRegistries(regs) {
|
|
193
|
+
fs.mkdirSync(path.dirname(REGISTRIES_FILE), { recursive: true });
|
|
194
|
+
fs.writeFileSync(REGISTRIES_FILE, JSON.stringify(regs, null, 2));
|
|
195
|
+
}
|
|
196
|
+
|
package/bin/lib/helpers.test.js
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Tests for helpers.js
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect } from "vitest";
|
|
5
|
-
import { formatBytes, formatDate, parseSkillSpec } from "./helpers.js";
|
|
6
|
-
|
|
7
|
-
describe("formatBytes", () => {
|
|
8
|
-
it("formats bytes correctly", () => {
|
|
9
|
-
expect(formatBytes(0)).toBe("0 B");
|
|
10
|
-
expect(formatBytes(512)).toBe("512 B");
|
|
11
|
-
expect(formatBytes(1024)).toBe("1.0 KB");
|
|
12
|
-
expect(formatBytes(1536)).toBe("1.5 KB");
|
|
13
|
-
expect(formatBytes(1048576)).toBe("1.0 MB");
|
|
14
|
-
expect(formatBytes(1572864)).toBe("1.5 MB");
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe("formatDate", () => {
|
|
19
|
-
it("returns 'unknown' for undefined", () => {
|
|
20
|
-
expect(formatDate()).toBe("unknown");
|
|
21
|
-
expect(formatDate(undefined)).toBe("unknown");
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("formats ISO date strings", () => {
|
|
25
|
-
const result = formatDate("2026-01-25T00:00:00.000Z");
|
|
26
|
-
expect(result).toBeTruthy();
|
|
27
|
-
expect(typeof result).toBe("string");
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe("parseSkillSpec", () => {
|
|
32
|
-
it("parses org/repo format", () => {
|
|
33
|
-
const result = parseSkillSpec("agentskillkit/agent-skills");
|
|
34
|
-
expect(result.org).toBe("agentskillkit");
|
|
35
|
-
expect(result.repo).toBe("agent-skills");
|
|
36
|
-
expect(result.skill).toBeUndefined();
|
|
37
|
-
expect(result.ref).toBeUndefined();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("parses org/repo#skill format", () => {
|
|
41
|
-
const result = parseSkillSpec("agentskillkit/agent-skills#react-patterns");
|
|
42
|
-
expect(result.org).toBe("agentskillkit");
|
|
43
|
-
expect(result.repo).toBe("agent-skills");
|
|
44
|
-
expect(result.skill).toBe("react-patterns");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("parses org/repo#skill@ref format", () => {
|
|
48
|
-
const result = parseSkillSpec("agentskillkit/agent-skills#react-patterns@v1.0.0");
|
|
49
|
-
expect(result.org).toBe("agentskillkit");
|
|
50
|
-
expect(result.repo).toBe("agent-skills");
|
|
51
|
-
expect(result.skill).toBe("react-patterns");
|
|
52
|
-
expect(result.ref).toBe("v1.0.0");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("throws error for invalid spec", () => {
|
|
56
|
-
expect(() => parseSkillSpec("")).toThrow("Skill spec is required");
|
|
57
|
-
expect(() => parseSkillSpec("invalid")).toThrow("Invalid spec format");
|
|
58
|
-
expect(() => parseSkillSpec(null)).toThrow("Skill spec is required");
|
|
59
|
-
});
|
|
60
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for helpers.js
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from "vitest";
|
|
5
|
+
import { formatBytes, formatDate, parseSkillSpec } from "./helpers.js";
|
|
6
|
+
|
|
7
|
+
describe("formatBytes", () => {
|
|
8
|
+
it("formats bytes correctly", () => {
|
|
9
|
+
expect(formatBytes(0)).toBe("0 B");
|
|
10
|
+
expect(formatBytes(512)).toBe("512 B");
|
|
11
|
+
expect(formatBytes(1024)).toBe("1.0 KB");
|
|
12
|
+
expect(formatBytes(1536)).toBe("1.5 KB");
|
|
13
|
+
expect(formatBytes(1048576)).toBe("1.0 MB");
|
|
14
|
+
expect(formatBytes(1572864)).toBe("1.5 MB");
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("formatDate", () => {
|
|
19
|
+
it("returns 'unknown' for undefined", () => {
|
|
20
|
+
expect(formatDate()).toBe("unknown");
|
|
21
|
+
expect(formatDate(undefined)).toBe("unknown");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("formats ISO date strings", () => {
|
|
25
|
+
const result = formatDate("2026-01-25T00:00:00.000Z");
|
|
26
|
+
expect(result).toBeTruthy();
|
|
27
|
+
expect(typeof result).toBe("string");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("parseSkillSpec", () => {
|
|
32
|
+
it("parses org/repo format", () => {
|
|
33
|
+
const result = parseSkillSpec("agentskillkit/agent-skills");
|
|
34
|
+
expect(result.org).toBe("agentskillkit");
|
|
35
|
+
expect(result.repo).toBe("agent-skills");
|
|
36
|
+
expect(result.skill).toBeUndefined();
|
|
37
|
+
expect(result.ref).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("parses org/repo#skill format", () => {
|
|
41
|
+
const result = parseSkillSpec("agentskillkit/agent-skills#react-patterns");
|
|
42
|
+
expect(result.org).toBe("agentskillkit");
|
|
43
|
+
expect(result.repo).toBe("agent-skills");
|
|
44
|
+
expect(result.skill).toBe("react-patterns");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("parses org/repo#skill@ref format", () => {
|
|
48
|
+
const result = parseSkillSpec("agentskillkit/agent-skills#react-patterns@v1.0.0");
|
|
49
|
+
expect(result.org).toBe("agentskillkit");
|
|
50
|
+
expect(result.repo).toBe("agent-skills");
|
|
51
|
+
expect(result.skill).toBe("react-patterns");
|
|
52
|
+
expect(result.ref).toBe("v1.0.0");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("throws error for invalid spec", () => {
|
|
56
|
+
expect(() => parseSkillSpec("")).toThrow("Skill spec is required");
|
|
57
|
+
expect(() => parseSkillSpec("invalid")).toThrow("Invalid spec format");
|
|
58
|
+
expect(() => parseSkillSpec(null)).toThrow("Skill spec is required");
|
|
59
|
+
});
|
|
60
|
+
});
|