skill-base 2.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.
Files changed (51) hide show
  1. package/README.md +141 -0
  2. package/bin/skill-base.js +53 -0
  3. package/data/.gitkeep +0 -0
  4. package/package.json +36 -0
  5. package/src/database.js +119 -0
  6. package/src/index.js +88 -0
  7. package/src/middleware/.gitkeep +0 -0
  8. package/src/middleware/admin.js +23 -0
  9. package/src/middleware/auth.js +96 -0
  10. package/src/middleware/error.js +23 -0
  11. package/src/models/.gitkeep +0 -0
  12. package/src/models/skill.js +57 -0
  13. package/src/models/user.js +130 -0
  14. package/src/models/version.js +57 -0
  15. package/src/routes/.gitkeep +0 -0
  16. package/src/routes/auth.js +173 -0
  17. package/src/routes/collaborators.js +260 -0
  18. package/src/routes/init.js +86 -0
  19. package/src/routes/publish.js +108 -0
  20. package/src/routes/skills.js +119 -0
  21. package/src/routes/users.js +169 -0
  22. package/src/utils/.gitkeep +0 -0
  23. package/src/utils/crypto.js +35 -0
  24. package/src/utils/permission.js +45 -0
  25. package/src/utils/zip.js +35 -0
  26. package/static/admin/users.html +593 -0
  27. package/static/cli-code.html +203 -0
  28. package/static/css/.gitkeep +0 -0
  29. package/static/css/style.css +1567 -0
  30. package/static/diff.html +466 -0
  31. package/static/file.html +443 -0
  32. package/static/index.html +251 -0
  33. package/static/js/.gitkeep +0 -0
  34. package/static/js/admin/users.js +346 -0
  35. package/static/js/app.js +508 -0
  36. package/static/js/auth.js +151 -0
  37. package/static/js/cli-code.js +184 -0
  38. package/static/js/collaborators.js +283 -0
  39. package/static/js/diff.js +540 -0
  40. package/static/js/file.js +619 -0
  41. package/static/js/i18n.js +739 -0
  42. package/static/js/index.js +168 -0
  43. package/static/js/publish.js +718 -0
  44. package/static/js/settings.js +124 -0
  45. package/static/js/setup.js +157 -0
  46. package/static/js/skill.js +808 -0
  47. package/static/login.html +82 -0
  48. package/static/publish.html +459 -0
  49. package/static/settings.html +163 -0
  50. package/static/setup.html +101 -0
  51. package/static/skill.html +851 -0
@@ -0,0 +1,86 @@
1
+ const bcrypt = require('bcryptjs');
2
+ const db = require('../database');
3
+
4
+ /**
5
+ * 系统初始化路由
6
+ * @param {import('fastify').FastifyInstance} fastify
7
+ */
8
+ async function initRoutes(fastify) {
9
+ /**
10
+ * GET /api/v1/init/status
11
+ * 检查系统是否需要初始化(是否存在管理员用户)
12
+ */
13
+ fastify.get('/status', async (request, reply) => {
14
+ const adminCount = db.prepare(
15
+ "SELECT COUNT(*) as count FROM users WHERE role = 'admin'"
16
+ ).get();
17
+
18
+ return {
19
+ initialized: adminCount.count > 0
20
+ };
21
+ });
22
+
23
+ /**
24
+ * POST /api/v1/init/setup
25
+ * 初始化系统管理员账号
26
+ * Body: { username, password }
27
+ */
28
+ fastify.post('/setup', async (request, reply) => {
29
+ // 检查是否已经初始化
30
+ const adminCount = db.prepare(
31
+ "SELECT COUNT(*) as count FROM users WHERE role = 'admin'"
32
+ ).get();
33
+
34
+ if (adminCount.count > 0) {
35
+ return reply.code(400).send({
36
+ error: 'System already initialized'
37
+ });
38
+ }
39
+
40
+ const { username, password } = request.body || {};
41
+
42
+ // 验证输入
43
+ if (!username || !password) {
44
+ return reply.code(400).send({
45
+ error: 'Username and password are required'
46
+ });
47
+ }
48
+
49
+ if (username.length < 3 || username.length > 50) {
50
+ return reply.code(400).send({
51
+ error: 'Username must be 3-50 characters'
52
+ });
53
+ }
54
+
55
+ if (password.length < 6) {
56
+ return reply.code(400).send({
57
+ error: 'Password must be at least 6 characters'
58
+ });
59
+ }
60
+
61
+ // 检查用户名是否已存在
62
+ const existingUser = db.prepare(
63
+ 'SELECT id FROM users WHERE username = ?'
64
+ ).get(username);
65
+
66
+ if (existingUser) {
67
+ return reply.code(400).send({
68
+ error: 'Username already exists'
69
+ });
70
+ }
71
+
72
+ // 创建管理员账号
73
+ const passwordHash = bcrypt.hashSync(password, 10);
74
+ const result = db.prepare(
75
+ 'INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)'
76
+ ).run(username, passwordHash, 'admin');
77
+
78
+ return {
79
+ success: true,
80
+ message: 'Admin account created successfully',
81
+ userId: result.lastInsertRowid
82
+ };
83
+ });
84
+ }
85
+
86
+ module.exports = initRoutes;
@@ -0,0 +1,108 @@
1
+ const fs = require('fs');
2
+ const db = require('../database');
3
+ const SkillModel = require('../models/skill');
4
+ const VersionModel = require('../models/version');
5
+ const { ensureSkillDir, generateVersionNumber, getZipPath, getZipRelativePath } = require('../utils/zip');
6
+ const { canPublishSkill } = require('../utils/permission');
7
+
8
+ async function publishRoutes(fastify, options) {
9
+ // POST /publish - 发布新版本
10
+ fastify.post('/publish', {
11
+ preHandler: [fastify.authenticate]
12
+ }, async (request, reply) => {
13
+ const fields = {};
14
+ let zipBuffer = null;
15
+ let zipFilename = null;
16
+
17
+ // 解析 multipart 数据
18
+ const parts = request.parts();
19
+ for await (const part of parts) {
20
+ if (part.type === 'file') {
21
+ if (part.fieldname === 'zip_file') {
22
+ // 读取文件到 buffer
23
+ const chunks = [];
24
+ for await (const chunk of part.file) {
25
+ chunks.push(chunk);
26
+ }
27
+ zipBuffer = Buffer.concat(chunks);
28
+ zipFilename = part.filename;
29
+ }
30
+ } else if (part.type === 'field') {
31
+ fields[part.fieldname] = part.value;
32
+ }
33
+ }
34
+
35
+ // 检查必须的 zip 文件
36
+ if (!zipBuffer) {
37
+ return reply.code(400).send({ detail: 'zip_file is required' });
38
+ }
39
+
40
+ const { skill_id, name, description, changelog } = fields;
41
+
42
+ // 检查 skill_id
43
+ if (!skill_id) {
44
+ return reply.code(400).send({ detail: 'skill_id is required' });
45
+ }
46
+
47
+ // 检查发布权限
48
+ if (!canPublishSkill(request.user, skill_id)) {
49
+ return reply.code(403).send({
50
+ ok: false,
51
+ error: 'permission_denied',
52
+ detail: 'You do not have publish permission for this Skill'
53
+ });
54
+ }
55
+
56
+ // 检查 skill 是否存在
57
+ const skillExists = SkillModel.exists(skill_id);
58
+
59
+ // 如果 skill 不存在,需要 name 字段来创建新 skill
60
+ if (!skillExists) {
61
+ if (!name) {
62
+ return reply.code(400).send({ detail: 'name is required for new skill' });
63
+ }
64
+ // 使用事务创建新 skill 和添加 owner 协作者记录
65
+ const createSkillTx = db.transaction(() => {
66
+ SkillModel.create(skill_id, name, description || '', request.user.id);
67
+ db.prepare(
68
+ 'INSERT INTO skill_collaborators (skill_id, user_id, role, created_by) VALUES (?, ?, ?, ?)'
69
+ ).run(skill_id, request.user.id, 'owner', request.user.id);
70
+ });
71
+ createSkillTx();
72
+ }
73
+
74
+ // 生成版本号
75
+ const version = generateVersionNumber();
76
+
77
+ // 确保目录存在
78
+ ensureSkillDir(skill_id);
79
+
80
+ // 写入 zip 文件
81
+ const zipPath = getZipPath(skill_id, version);
82
+ fs.writeFileSync(zipPath, zipBuffer);
83
+
84
+ // 获取相对路径(存入数据库)
85
+ const zipRelativePath = getZipRelativePath(skill_id, version);
86
+
87
+ // 创建版本记录
88
+ const versionRecord = VersionModel.create(
89
+ skill_id,
90
+ version,
91
+ changelog || '',
92
+ zipRelativePath,
93
+ request.user.id
94
+ );
95
+
96
+ // 更新 skill 的最新版本
97
+ SkillModel.updateLatestVersion(skill_id, version);
98
+
99
+ return {
100
+ ok: true,
101
+ skill_id,
102
+ version,
103
+ created_at: versionRecord.created_at
104
+ };
105
+ });
106
+ }
107
+
108
+ module.exports = publishRoutes;
@@ -0,0 +1,119 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const SkillModel = require('../models/skill');
4
+ const VersionModel = require('../models/version');
5
+ const { getZipPath } = require('../utils/zip');
6
+
7
+ // 格式化 skill,将 owner 转为对象
8
+ function formatSkill(skill) {
9
+ if (!skill) return null;
10
+ return {
11
+ id: skill.id,
12
+ name: skill.name,
13
+ description: skill.description,
14
+ latest_version: skill.latest_version,
15
+ owner: {
16
+ id: skill.owner_id,
17
+ username: skill.owner_username,
18
+ name: skill.owner_name
19
+ },
20
+ created_at: skill.created_at,
21
+ updated_at: skill.updated_at
22
+ };
23
+ }
24
+
25
+ // 格式化 version,将 uploader 转为对象
26
+ function formatVersion(version) {
27
+ if (!version) return null;
28
+ return {
29
+ id: version.id,
30
+ skill_id: version.skill_id,
31
+ version: version.version,
32
+ changelog: version.changelog,
33
+ zip_path: version.zip_path,
34
+ uploader: {
35
+ id: version.uploader_id,
36
+ username: version.uploader_username,
37
+ name: version.uploader_name
38
+ },
39
+ created_at: version.created_at
40
+ };
41
+ }
42
+
43
+ async function skillsRoutes(fastify, options) {
44
+ // GET / - 获取 skills 列表
45
+ fastify.get('/', async (request, reply) => {
46
+ const { q } = request.query;
47
+ const skills = SkillModel.search(q);
48
+ const formattedSkills = skills.map(formatSkill);
49
+
50
+ return {
51
+ skills: formattedSkills,
52
+ total: formattedSkills.length
53
+ };
54
+ });
55
+
56
+ // GET /:skill_id - 获取单个 skill
57
+ fastify.get('/:skill_id', async (request, reply) => {
58
+ const { skill_id } = request.params;
59
+ const skill = SkillModel.findById(skill_id);
60
+
61
+ if (!skill) {
62
+ return reply.code(404).send({ detail: 'Skill not found' });
63
+ }
64
+
65
+ return formatSkill(skill);
66
+ });
67
+
68
+ // GET /:skill_id/versions - 获取 skill 的所有版本
69
+ fastify.get('/:skill_id/versions', async (request, reply) => {
70
+ const { skill_id } = request.params;
71
+
72
+ // 先检查 skill 是否存在
73
+ if (!SkillModel.exists(skill_id)) {
74
+ return reply.code(404).send({ detail: 'Skill not found' });
75
+ }
76
+
77
+ const versions = VersionModel.listBySkillId(skill_id);
78
+ const formattedVersions = versions.map(formatVersion);
79
+
80
+ return {
81
+ skill_id,
82
+ versions: formattedVersions
83
+ };
84
+ });
85
+
86
+ // GET /:skill_id/versions/:version/download - 下载版本 zip 文件
87
+ fastify.get('/:skill_id/versions/:version/download', async (request, reply) => {
88
+ const { skill_id, version } = request.params;
89
+
90
+ let versionRecord;
91
+
92
+ if (version === 'latest') {
93
+ versionRecord = VersionModel.getLatest(skill_id);
94
+ } else {
95
+ versionRecord = VersionModel.findByVersion(skill_id, version);
96
+ }
97
+
98
+ if (!versionRecord) {
99
+ return reply.code(404).send({ detail: 'Version not found' });
100
+ }
101
+
102
+ // 获取文件路径
103
+ const zipPath = getZipPath(skill_id, versionRecord.version);
104
+
105
+ // 检查文件是否存在
106
+ if (!fs.existsSync(zipPath)) {
107
+ return reply.code(404).send({ detail: 'Version not found' });
108
+ }
109
+
110
+ // 设置响应头并返回文件流
111
+ const fileName = `${skill_id}-${versionRecord.version}.zip`;
112
+ reply.header('Content-Type', 'application/zip');
113
+ reply.header('Content-Disposition', `attachment; filename="${fileName}"`);
114
+
115
+ return fs.createReadStream(zipPath);
116
+ });
117
+ }
118
+
119
+ module.exports = skillsRoutes;
@@ -0,0 +1,169 @@
1
+ const UserModel = require('../models/user');
2
+ const { hashPassword } = require('../utils/crypto');
3
+ const db = require('../database');
4
+
5
+ async function usersRoutes(fastify, options) {
6
+ // GET /search - 用户搜索(仅需登录,不需要管理员权限)
7
+ // 注意:必须在 /:user_id 之前注册
8
+ fastify.get('/search', {
9
+ preHandler: [fastify.authenticate]
10
+ }, async (request, reply) => {
11
+ const { q } = request.query;
12
+
13
+ if (!q || q.trim().length < 1) {
14
+ return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'Search keyword must be at least 1 character' });
15
+ }
16
+
17
+ const pattern = `%${q.trim()}%`;
18
+ const users = db.prepare(`
19
+ SELECT id, username, name, status
20
+ FROM users
21
+ WHERE (username LIKE ? OR name LIKE ?) AND status = 'active'
22
+ LIMIT 10
23
+ `).all(pattern, pattern);
24
+
25
+ return reply.send({ users });
26
+ });
27
+
28
+ // 以下路由都需要管理员权限
29
+ fastify.register(async function adminRoutes(fastify) {
30
+ fastify.addHook('preHandler', fastify.requireAdmin);
31
+
32
+ // GET / - 用户列表
33
+ fastify.get('/', async (request, reply) => {
34
+ const { q, status, page = 1, limit = 20 } = request.query;
35
+ const result = UserModel.list({
36
+ q,
37
+ status,
38
+ page: parseInt(page) || 1,
39
+ limit: Math.min(parseInt(limit) || 20, 100)
40
+ });
41
+ return reply.send(result);
42
+ });
43
+
44
+ // POST / - 创建用户
45
+ fastify.post('/', async (request, reply) => {
46
+ const { username, password, role = 'developer', name } = request.body || {};
47
+
48
+ if (!username || !password) {
49
+ return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'Username and password are required' });
50
+ }
51
+ if (password.length < 6) {
52
+ return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'Password must be at least 6 characters' });
53
+ }
54
+ if (!['admin', 'developer'].includes(role)) {
55
+ return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'Role must be admin or developer' });
56
+ }
57
+
58
+ // Check if username already exists
59
+ const existing = UserModel.findByUsername(username.trim());
60
+ if (existing) {
61
+ return reply.code(400).send({ ok: false, error: 'username_exists', detail: 'Username already exists' });
62
+ }
63
+
64
+ const passwordHash = await hashPassword(password);
65
+ // 创建用户时记录 created_by
66
+ const result = db.prepare(
67
+ "INSERT INTO users (username, password_hash, role, name, status, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, 'active', ?, datetime('now'), datetime('now'))"
68
+ ).run(username.trim(), passwordHash, role, name || null, request.user.id);
69
+
70
+ const user = UserModel.findById(result.lastInsertRowid);
71
+ return reply.code(201).send({ ok: true, user });
72
+ });
73
+
74
+ // GET /:user_id - 用户详情
75
+ fastify.get('/:user_id', async (request, reply) => {
76
+ const { user_id } = request.params;
77
+ const user = UserModel.findByIdWithCreator(parseInt(user_id));
78
+
79
+ if (!user) {
80
+ return reply.code(404).send({ ok: false, error: 'not_found', detail: 'User not found' });
81
+ }
82
+
83
+ // Format created_by
84
+ const result = {
85
+ id: user.id,
86
+ username: user.username,
87
+ name: user.name,
88
+ role: user.role,
89
+ status: user.status,
90
+ created_at: user.created_at,
91
+ updated_at: user.updated_at
92
+ };
93
+ if (user.creator_id) {
94
+ result.created_by = { id: user.creator_id, username: user.creator_username };
95
+ }
96
+
97
+ return reply.send(result);
98
+ });
99
+
100
+ // PATCH /:user_id - 更新用户
101
+ fastify.patch('/:user_id', async (request, reply) => {
102
+ const userId = parseInt(request.params.user_id);
103
+ const { role, status, name } = request.body || {};
104
+
105
+ const user = UserModel.findById(userId);
106
+ if (!user) {
107
+ return reply.code(404).send({ ok: false, error: 'not_found', detail: 'User not found' });
108
+ }
109
+
110
+ // Self-protection for admins
111
+ if (userId === request.user.id) {
112
+ if (status === 'disabled') {
113
+ return reply.code(400).send({ ok: false, error: 'self_protection', detail: 'Cannot disable your own account' });
114
+ }
115
+ if (role === 'developer') {
116
+ return reply.code(400).send({ ok: false, error: 'self_protection', detail: 'Cannot downgrade your own role' });
117
+ }
118
+ }
119
+
120
+ // Validate field values
121
+ const fields = {};
122
+ if (role !== undefined) {
123
+ if (!['admin', 'developer'].includes(role)) {
124
+ return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'Role must be admin or developer' });
125
+ }
126
+ fields.role = role;
127
+ }
128
+ if (status !== undefined) {
129
+ if (!['active', 'disabled'].includes(status)) {
130
+ return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'Status must be active or disabled' });
131
+ }
132
+ fields.status = status;
133
+ }
134
+ if (name !== undefined) {
135
+ fields.name = name;
136
+ }
137
+
138
+ if (Object.keys(fields).length === 0) {
139
+ return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'No fields to update' });
140
+ }
141
+
142
+ UserModel.update(userId, fields);
143
+ const updated = UserModel.findById(userId);
144
+ return reply.send({ ok: true, user: updated });
145
+ });
146
+
147
+ // POST /:user_id/reset-password - 重置密码
148
+ fastify.post('/:user_id/reset-password', async (request, reply) => {
149
+ const userId = parseInt(request.params.user_id);
150
+ const { new_password } = request.body || {};
151
+
152
+ if (!new_password || new_password.length < 6) {
153
+ return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'New password must be at least 6 characters' });
154
+ }
155
+
156
+ const user = UserModel.findById(userId);
157
+ if (!user) {
158
+ return reply.code(404).send({ ok: false, error: 'not_found', detail: 'User not found' });
159
+ }
160
+
161
+ const passwordHash = await hashPassword(new_password);
162
+ UserModel.resetPassword(userId, passwordHash);
163
+
164
+ return reply.send({ ok: true, message: 'Password has been reset' });
165
+ });
166
+ });
167
+ }
168
+
169
+ module.exports = usersRoutes;
File without changes
@@ -0,0 +1,35 @@
1
+ const bcrypt = require('bcryptjs');
2
+ const { v4: uuidv4 } = require('uuid');
3
+
4
+ // 密码哈希(10 rounds)
5
+ function hashPassword(password) {
6
+ return bcrypt.hashSync(password, 10);
7
+ }
8
+
9
+ // 验证密码
10
+ function verifyPassword(password, hash) {
11
+ return bcrypt.compareSync(password, hash);
12
+ }
13
+
14
+ // 生成 PAT Token,格式:sk-base-{uuid去掉横线}
15
+ function generatePAT() {
16
+ return 'sk-base-' + uuidv4().replace(/-/g, '');
17
+ }
18
+
19
+ // 生成 CLI 验证码,格式:XXXX-XXXX(大写字母+数字)
20
+ function generateCliCode() {
21
+ const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // 排除易混淆字符
22
+ let code = '';
23
+ for (let i = 0; i < 8; i++) {
24
+ if (i === 4) code += '-';
25
+ code += chars[Math.floor(Math.random() * chars.length)];
26
+ }
27
+ return code;
28
+ }
29
+
30
+ // 生成 Session ID
31
+ function generateSessionId() {
32
+ return uuidv4();
33
+ }
34
+
35
+ module.exports = { hashPassword, verifyPassword, generatePAT, generateCliCode, generateSessionId };
@@ -0,0 +1,45 @@
1
+ const db = require('../database');
2
+
3
+ /**
4
+ * 检查用户是否有 Skill 的指定权限
5
+ * @param {object} user - request.user 对象
6
+ * @param {string} skillId - Skill ID
7
+ * @param {string} requiredRole - 'owner' | 'collaborator' | 'any'
8
+ * @returns {boolean}
9
+ */
10
+ function hasSkillPermission(user, skillId, requiredRole = 'any') {
11
+ // 管理员拥有所有权限
12
+ if (user.role === 'admin') return true;
13
+
14
+ const collaborator = db.prepare(
15
+ 'SELECT role FROM skill_collaborators WHERE skill_id = ? AND user_id = ?'
16
+ ).get(skillId, user.id);
17
+
18
+ if (!collaborator) return false;
19
+ if (requiredRole === 'any') return true;
20
+ if (requiredRole === 'owner') return collaborator.role === 'owner';
21
+ if (requiredRole === 'collaborator') return true; // owner 也有 collaborator 权限
22
+ return false;
23
+ }
24
+
25
+ /**
26
+ * 检查用户是否可以发布 Skill
27
+ */
28
+ function canPublishSkill(user, skillId) {
29
+ const skill = db.prepare('SELECT id FROM skills WHERE id = ?').get(skillId);
30
+ if (!skill) return true; // 新 Skill,任何登录用户都可创建
31
+ return hasSkillPermission(user, skillId, 'any');
32
+ }
33
+
34
+ /**
35
+ * 检查用户是否可以管理协作者/删除 Skill
36
+ */
37
+ function canManageSkill(user, skillId) {
38
+ return hasSkillPermission(user, skillId, 'owner');
39
+ }
40
+
41
+ module.exports = {
42
+ hasSkillPermission,
43
+ canPublishSkill,
44
+ canManageSkill
45
+ };
@@ -0,0 +1,35 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ // 获取 zip 存储目录
5
+ function getDataDir() {
6
+ return process.env.DATA_DIR || path.join(__dirname, '../../data');
7
+ }
8
+
9
+ // 确保 skill 的存储目录存在
10
+ function ensureSkillDir(skillId) {
11
+ const dir = path.join(getDataDir(), skillId);
12
+ if (!fs.existsSync(dir)) {
13
+ fs.mkdirSync(dir, { recursive: true });
14
+ }
15
+ return dir;
16
+ }
17
+
18
+ // 生成时间戳版本号 vYYYYMMDD.HHMMSS
19
+ function generateVersionNumber() {
20
+ const now = new Date();
21
+ const pad = (n) => String(n).padStart(2, '0');
22
+ return `v${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}.${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
23
+ }
24
+
25
+ // 获取 zip 文件的完整路径
26
+ function getZipPath(skillId, version) {
27
+ return path.join(getDataDir(), skillId, `${version}.zip`);
28
+ }
29
+
30
+ // 获取 zip 文件相对路径(存入数据库)
31
+ function getZipRelativePath(skillId, version) {
32
+ return `${skillId}/${version}.zip`;
33
+ }
34
+
35
+ module.exports = { getDataDir, ensureSkillDir, generateVersionNumber, getZipPath, getZipRelativePath };