pinokiod 7.3.1 → 7.3.3
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/kernel/api/github/index.js +444 -0
- package/kernel/api/index.js +199 -11
- package/kernel/api/process/index.js +124 -44
- package/kernel/api/shell_run_template.js +273 -0
- package/kernel/api/uri/index.js +51 -0
- package/kernel/bin/git.js +9 -10
- package/kernel/bin/huggingface.js +1 -1
- package/kernel/bin/zip.js +9 -1
- package/kernel/connect/providers/github/README.md +5 -4
- package/kernel/environment.js +195 -92
- package/kernel/git.js +98 -19
- package/kernel/gitconfig_template +7 -0
- package/kernel/gpu/amd.js +72 -0
- package/kernel/gpu/apple.js +8 -0
- package/kernel/gpu/common.js +12 -0
- package/kernel/gpu/intel.js +47 -0
- package/kernel/gpu/nvidia.js +8 -0
- package/kernel/index.js +11 -1
- package/kernel/managed_skills.js +871 -0
- package/kernel/plugin.js +6 -58
- package/kernel/plugin_sources.js +316 -0
- package/kernel/resource_usage/gpu.js +349 -0
- package/kernel/resource_usage/index.js +322 -0
- package/kernel/resource_usage/macos_footprint.js +197 -0
- package/kernel/resource_usage/preferences.js +92 -0
- package/kernel/resource_usage/process_tree.js +303 -0
- package/kernel/scripts/git/create +4 -4
- package/kernel/scripts/git/fork +7 -8
- package/kernel/shell.js +23 -2
- package/kernel/shells.js +41 -0
- package/kernel/sysinfo.js +62 -9
- package/kernel/util.js +60 -0
- package/package.json +1 -1
- package/server/index.js +984 -156
- package/server/lib/app_log_report.js +543 -0
- package/server/lib/content_validation.js +55 -33
- package/server/lib/launcher_instruction_bootstrap.js +4 -96
- package/server/lib/terminal_session_helpers.js +0 -3
- package/server/public/common.js +77 -31
- package/server/public/create-launcher.js +4 -32
- package/server/public/logs.js +1428 -0
- package/server/public/nav.js +7 -0
- package/server/public/plugin-detail.js +93 -10
- package/server/public/privacy_filter_worker.js +391 -0
- package/server/public/style.css +1104 -154
- package/server/public/task-launcher.js +8 -29
- package/server/public/universal-launcher.css +8 -6
- package/server/public/universal-launcher.js +3 -27
- package/server/routes/apps.js +195 -1
- package/server/views/app.ejs +3041 -717
- package/server/views/autolaunch.ejs +917 -0
- package/server/views/bootstrap.ejs +7 -1
- package/server/views/d.ejs +408 -65
- package/server/views/editor.ejs +85 -19
- package/server/views/index.ejs +661 -111
- package/server/views/init/index.ejs +1 -1
- package/server/views/install.ejs +1 -1
- package/server/views/logs.ejs +164 -86
- package/server/views/net.ejs +7 -1
- package/server/views/partials/d_terminal_column.ejs +2 -2
- package/server/views/partials/d_terminal_options.ejs +0 -8
- package/server/views/partials/fs_status.ejs +47 -0
- package/server/views/partials/home_action_modal.ejs +86 -0
- package/server/views/partials/home_run_menu.ejs +87 -0
- package/server/views/partials/main_sidebar.ejs +2 -0
- package/server/views/partials/menu.ejs +1 -1
- package/server/views/plugin_detail.ejs +19 -4
- package/server/views/plugins.ejs +201 -3
- package/server/views/pre.ejs +1 -1
- package/server/views/pro.ejs +1 -1
- package/server/views/shell.ejs +40 -18
- package/server/views/skills.ejs +506 -0
- package/server/views/terminal.ejs +45 -19
- package/spec/INSTRUCTION_SYNC.md +20 -10
- package/system/plugin/antigravity-cli/antigravity.png +0 -0
- package/system/plugin/antigravity-cli/common.js +155 -0
- package/system/plugin/antigravity-cli/install.js +272 -0
- package/system/plugin/antigravity-cli/pinokio.js +13 -0
- package/system/plugin/antigravity-cli-auto/antigravity.png +0 -0
- package/system/plugin/antigravity-cli-auto/pinokio.js +13 -0
- package/system/plugin/claude/claude.png +0 -0
- package/system/plugin/claude/pinokio.js +47 -0
- package/system/plugin/claude-auto/claude.png +0 -0
- package/system/plugin/claude-auto/pinokio.js +58 -0
- package/system/plugin/claude-desktop/icon.jpeg +0 -0
- package/system/plugin/claude-desktop/pinokio.js +23 -0
- package/system/plugin/codex/openai.webp +0 -0
- package/system/plugin/codex/pinokio.js +42 -0
- package/system/plugin/codex-auto/openai.webp +0 -0
- package/system/plugin/codex-auto/pinokio.js +49 -0
- package/system/plugin/codex-desktop/icon.png +0 -0
- package/system/plugin/codex-desktop/pinokio.js +23 -0
- package/system/plugin/crush/crush.png +0 -0
- package/system/plugin/crush/pinokio.js +15 -0
- package/system/plugin/cursor/cursor.jpeg +0 -0
- package/system/plugin/cursor/pinokio.js +23 -0
- package/system/plugin/qwen/pinokio.js +34 -0
- package/system/plugin/qwen/qwen.png +0 -0
- package/system/plugin/vscode/pinokio.js +20 -0
- package/system/plugin/vscode/vscode.png +0 -0
- package/system/plugin/windsurf/pinokio.js +23 -0
- package/system/plugin/windsurf/windsurf.png +0 -0
- package/test/antigravity-cli-plugin.test.js +185 -0
- package/test/app-api.test.js +239 -0
- package/test/app-log-report.test.js +67 -0
- package/test/environment-cache-preflight.test.js +98 -0
- package/test/git-bin.test.js +59 -0
- package/test/git-defaults.test.js +97 -0
- package/test/github-api.test.js +158 -0
- package/test/github-connection.test.js +117 -0
- package/test/huggingface-bin.test.js +25 -0
- package/test/managed-skills.test.js +351 -0
- package/test/plugin-action-functions.test.js +337 -0
- package/test/plugin-dev-iframe.test.js +17 -0
- package/test/plugin-sources.test.js +203 -0
- package/test/privacy-filter-worker-heuristics.test.js +69 -0
- package/test/process-wait.test.js +169 -0
- package/test/script-api.test.js +97 -0
- package/test/shell-api.test.js +134 -0
- package/test/shell-run-template.test.js +209 -0
- package/test/storage-api.test.js +137 -0
- package/test/uri-api.test.js +100 -0
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
const fs = require("fs")
|
|
2
|
+
const os = require("os")
|
|
3
|
+
const path = require("path")
|
|
4
|
+
|
|
5
|
+
const INDEX_FILENAME = "index.json"
|
|
6
|
+
const INDEX_VERSION = 1
|
|
7
|
+
const MARKER_FILENAME = ".pinokio-managed.json"
|
|
8
|
+
const MANAGER_ID = "pinokio"
|
|
9
|
+
const TEMP_DIRNAME = ".tmp"
|
|
10
|
+
const NON_INTERACTIVE_GIT_ENV = {
|
|
11
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
12
|
+
GIT_ASKPASS: "",
|
|
13
|
+
SSH_ASKPASS: "",
|
|
14
|
+
GCM_INTERACTIVE: "never"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const BUILTIN_SKILLS = {
|
|
18
|
+
pinokio: {
|
|
19
|
+
id: "pinokio",
|
|
20
|
+
publishName: "pinokio",
|
|
21
|
+
source: "builtin",
|
|
22
|
+
removable: false
|
|
23
|
+
},
|
|
24
|
+
gepeto: {
|
|
25
|
+
id: "gepeto",
|
|
26
|
+
publishName: "gepeto",
|
|
27
|
+
source: "builtin",
|
|
28
|
+
removable: false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const normalizeText = (value) => String(value || "").replace(/\r\n/g, "\n")
|
|
33
|
+
|
|
34
|
+
const skillsRoot = (kernel) => {
|
|
35
|
+
if (!kernel || !kernel.homedir || typeof kernel.path !== "function") {
|
|
36
|
+
throw new Error("Pinokio home is not configured.")
|
|
37
|
+
}
|
|
38
|
+
return path.resolve(kernel.path("skills"))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const indexPath = (kernel) => path.resolve(skillsRoot(kernel), INDEX_FILENAME)
|
|
42
|
+
|
|
43
|
+
const publishRoots = (home = os.homedir()) => [
|
|
44
|
+
path.resolve(home, ".agents", "skills"),
|
|
45
|
+
path.resolve(home, ".claude", "skills"),
|
|
46
|
+
path.resolve(home, ".hermes", "skills")
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
const writeFileIfChanged = async (targetPath, content) => {
|
|
50
|
+
let shouldWrite = true
|
|
51
|
+
try {
|
|
52
|
+
const existing = await fs.promises.readFile(targetPath, "utf8")
|
|
53
|
+
shouldWrite = existing !== content
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (!(error && error.code === "ENOENT")) {
|
|
56
|
+
throw error
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (shouldWrite) {
|
|
60
|
+
await fs.promises.mkdir(path.dirname(targetPath), { recursive: true })
|
|
61
|
+
await fs.promises.writeFile(targetPath, content, "utf8")
|
|
62
|
+
}
|
|
63
|
+
return shouldWrite
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const writeJsonFileAtomic = async (targetPath, value) => {
|
|
67
|
+
const content = JSON.stringify(value, null, 2) + "\n"
|
|
68
|
+
const tempPath = path.resolve(
|
|
69
|
+
path.dirname(targetPath),
|
|
70
|
+
`.${path.basename(targetPath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`
|
|
71
|
+
)
|
|
72
|
+
await fs.promises.writeFile(tempPath, content, "utf8")
|
|
73
|
+
try {
|
|
74
|
+
await fs.promises.rename(tempPath, targetPath)
|
|
75
|
+
} catch (error) {
|
|
76
|
+
await fs.promises.rm(tempPath, { force: true }).catch(() => {})
|
|
77
|
+
throw error
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const normalizeSkillId = (value) => {
|
|
82
|
+
const raw = String(value || "")
|
|
83
|
+
.trim()
|
|
84
|
+
.replace(/\.git$/i, "")
|
|
85
|
+
.toLowerCase()
|
|
86
|
+
const normalized = raw
|
|
87
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
88
|
+
.replace(/^[._-]+|[._-]+$/g, "")
|
|
89
|
+
.replace(/-{2,}/g, "-")
|
|
90
|
+
if (!normalized || normalized === "." || normalized === "..") {
|
|
91
|
+
return ""
|
|
92
|
+
}
|
|
93
|
+
return normalized
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const normalizePublishName = normalizeSkillId
|
|
97
|
+
|
|
98
|
+
const deriveSkillIdFromRef = (ref) => {
|
|
99
|
+
const raw = String(ref || "").trim().replace(/[\\/]+$/, "")
|
|
100
|
+
if (!raw) {
|
|
101
|
+
return ""
|
|
102
|
+
}
|
|
103
|
+
let lastSegment = ""
|
|
104
|
+
try {
|
|
105
|
+
const parsed = new URL(raw)
|
|
106
|
+
lastSegment = parsed.pathname.replace(/\/+$/, "").split("/").filter(Boolean).pop() || ""
|
|
107
|
+
} catch (_) {
|
|
108
|
+
}
|
|
109
|
+
if (!lastSegment) {
|
|
110
|
+
lastSegment = raw.split(/[/:]/).filter(Boolean).pop() || ""
|
|
111
|
+
}
|
|
112
|
+
return normalizeSkillId(lastSegment)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const defaultPublishName = (id) => {
|
|
116
|
+
const normalized = normalizeSkillId(id)
|
|
117
|
+
if (normalized === "pinokio" || normalized === "gepeto") {
|
|
118
|
+
return normalized
|
|
119
|
+
}
|
|
120
|
+
return normalized ? `pinokio-${normalized}` : ""
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const parseSimpleFrontmatter = (content) => {
|
|
124
|
+
const normalized = normalizeText(content)
|
|
125
|
+
if (!normalized.startsWith("---\n")) {
|
|
126
|
+
return {
|
|
127
|
+
frontmatter: {},
|
|
128
|
+
bodyWithoutFrontmatter: normalized.trim()
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const end = normalized.indexOf("\n---\n", 4)
|
|
132
|
+
if (end === -1) {
|
|
133
|
+
return {
|
|
134
|
+
frontmatter: {},
|
|
135
|
+
bodyWithoutFrontmatter: normalized.trim()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const rawFrontmatter = normalized.slice(4, end)
|
|
139
|
+
const data = {}
|
|
140
|
+
let currentArrayKey = null
|
|
141
|
+
for (const line of rawFrontmatter.split("\n")) {
|
|
142
|
+
const arrayMatch = /^-\s*(.*)$/.exec(line.trim())
|
|
143
|
+
if (arrayMatch && currentArrayKey) {
|
|
144
|
+
data[currentArrayKey].push(arrayMatch[1].replace(/^["']|["']$/g, ""))
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line)
|
|
148
|
+
if (!match) {
|
|
149
|
+
currentArrayKey = null
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
const key = match[1].trim()
|
|
153
|
+
let value = match[2].trim()
|
|
154
|
+
if (!value) {
|
|
155
|
+
data[key] = []
|
|
156
|
+
currentArrayKey = key
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
currentArrayKey = null
|
|
160
|
+
value = value.replace(/^["']|["']$/g, "")
|
|
161
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
162
|
+
data[key] = value.slice(1, -1).split(",").map((entry) => entry.trim().replace(/^["']|["']$/g, "")).filter(Boolean)
|
|
163
|
+
} else {
|
|
164
|
+
data[key] = value
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
frontmatter: data,
|
|
169
|
+
bodyWithoutFrontmatter: normalized.slice(end + 5).trim()
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const normalizeIndex = (raw) => {
|
|
174
|
+
const source = raw && typeof raw === "object" ? raw : {}
|
|
175
|
+
const rawSkills = source.skills && typeof source.skills === "object" ? source.skills : {}
|
|
176
|
+
const skills = {}
|
|
177
|
+
for (const key of Object.keys(rawSkills)) {
|
|
178
|
+
const entry = rawSkills[key]
|
|
179
|
+
if (!entry || typeof entry !== "object") {
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
const id = normalizeSkillId(entry.id || key)
|
|
183
|
+
if (!id) {
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
const builtin = Object.prototype.hasOwnProperty.call(BUILTIN_SKILLS, id)
|
|
187
|
+
const publishName = normalizePublishName(entry.publishName) || defaultPublishName(id)
|
|
188
|
+
skills[id] = {
|
|
189
|
+
id,
|
|
190
|
+
source: builtin ? "builtin" : (typeof entry.source === "string" && entry.source.trim() ? entry.source.trim() : "local"),
|
|
191
|
+
ref: typeof entry.ref === "string" ? entry.ref : "",
|
|
192
|
+
enabled: entry.enabled === true,
|
|
193
|
+
publishName,
|
|
194
|
+
builtin,
|
|
195
|
+
removable: builtin ? false : entry.removable !== false,
|
|
196
|
+
installedAt: typeof entry.installedAt === "string" ? entry.installedAt : "",
|
|
197
|
+
updatedAt: typeof entry.updatedAt === "string" ? entry.updatedAt : ""
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
version: INDEX_VERSION,
|
|
202
|
+
skills
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const readIndex = async (kernel) => {
|
|
207
|
+
await fs.promises.mkdir(skillsRoot(kernel), { recursive: true })
|
|
208
|
+
try {
|
|
209
|
+
const raw = await fs.promises.readFile(indexPath(kernel), "utf8")
|
|
210
|
+
return normalizeIndex(JSON.parse(raw))
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (error && error.code === "ENOENT") {
|
|
213
|
+
return normalizeIndex({})
|
|
214
|
+
}
|
|
215
|
+
const nextError = new Error(`Failed to read managed skills index: ${error && error.message ? error.message : "unknown error"}`)
|
|
216
|
+
nextError.status = 500
|
|
217
|
+
nextError.cause = error
|
|
218
|
+
throw nextError
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const writeIndex = async (kernel, index) => {
|
|
223
|
+
const normalized = normalizeIndex(index)
|
|
224
|
+
await fs.promises.mkdir(skillsRoot(kernel), { recursive: true })
|
|
225
|
+
await writeJsonFileAtomic(indexPath(kernel), normalized)
|
|
226
|
+
return normalized
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const skillDir = (kernel, id) => path.resolve(skillsRoot(kernel), normalizeSkillId(id))
|
|
230
|
+
const skillPath = (kernel, id) => path.resolve(skillDir(kernel, id), "SKILL.md")
|
|
231
|
+
|
|
232
|
+
const readSkillContent = async (sourcePath) => {
|
|
233
|
+
try {
|
|
234
|
+
return await fs.promises.readFile(sourcePath, "utf8")
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (error && error.code === "ENOENT") {
|
|
237
|
+
return ""
|
|
238
|
+
}
|
|
239
|
+
throw error
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const validateSkillDir = async (dir, fallbackId = "") => {
|
|
244
|
+
const sourcePath = path.resolve(dir, "SKILL.md")
|
|
245
|
+
let content = ""
|
|
246
|
+
const errors = []
|
|
247
|
+
try {
|
|
248
|
+
content = await fs.promises.readFile(sourcePath, "utf8")
|
|
249
|
+
} catch (error) {
|
|
250
|
+
errors.push("Missing SKILL.md at the skill root.")
|
|
251
|
+
}
|
|
252
|
+
const parsed = parseSimpleFrontmatter(content)
|
|
253
|
+
const meta = parsed.frontmatter || {}
|
|
254
|
+
const label = String(meta.title || meta.name || meta.skill || meta.id || fallbackId || path.basename(dir)).trim()
|
|
255
|
+
const description = String(meta.description || meta.summary || "").trim()
|
|
256
|
+
if (content && !content.trim()) {
|
|
257
|
+
errors.push("SKILL.md is empty.")
|
|
258
|
+
}
|
|
259
|
+
if (content && !String(meta.title || meta.name || meta.skill || meta.id || "").trim()) {
|
|
260
|
+
errors.push("SKILL.md frontmatter must include name or title.")
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
valid: errors.length === 0,
|
|
264
|
+
errors,
|
|
265
|
+
label,
|
|
266
|
+
description,
|
|
267
|
+
content,
|
|
268
|
+
path: sourcePath
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const ensureBuiltinIndexEntries = (index) => {
|
|
273
|
+
const now = new Date().toISOString()
|
|
274
|
+
let changed = false
|
|
275
|
+
for (const builtin of Object.values(BUILTIN_SKILLS)) {
|
|
276
|
+
if (!index.skills[builtin.id]) {
|
|
277
|
+
index.skills[builtin.id] = {
|
|
278
|
+
id: builtin.id,
|
|
279
|
+
source: "builtin",
|
|
280
|
+
ref: "",
|
|
281
|
+
enabled: true,
|
|
282
|
+
publishName: builtin.publishName,
|
|
283
|
+
builtin: true,
|
|
284
|
+
removable: false,
|
|
285
|
+
installedAt: now,
|
|
286
|
+
updatedAt: now
|
|
287
|
+
}
|
|
288
|
+
changed = true
|
|
289
|
+
continue
|
|
290
|
+
}
|
|
291
|
+
const entry = index.skills[builtin.id]
|
|
292
|
+
const before = JSON.stringify(entry)
|
|
293
|
+
entry.source = "builtin"
|
|
294
|
+
entry.builtin = true
|
|
295
|
+
entry.removable = false
|
|
296
|
+
entry.publishName = builtin.publishName
|
|
297
|
+
if (JSON.stringify(entry) !== before) {
|
|
298
|
+
changed = true
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return changed
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const composeBuiltinSkillContent = async (kernel, id) => {
|
|
305
|
+
if (id === "pinokio") {
|
|
306
|
+
return readSkillContent(path.resolve(__dirname, "../prototype/system/SKILL_PINOKIO.md"))
|
|
307
|
+
}
|
|
308
|
+
if (id === "gepeto") {
|
|
309
|
+
const agentsContent = await readSkillContent(path.resolve(kernel.homedir, "AGENTS.md"))
|
|
310
|
+
if (!agentsContent.trim()) {
|
|
311
|
+
return ""
|
|
312
|
+
}
|
|
313
|
+
return [
|
|
314
|
+
"---",
|
|
315
|
+
"name: gepeto",
|
|
316
|
+
"description: Guide for building 1-click launchers and building apps with launchers built-in using Pinokio",
|
|
317
|
+
"---",
|
|
318
|
+
"",
|
|
319
|
+
agentsContent.trim(),
|
|
320
|
+
""
|
|
321
|
+
].join("\n")
|
|
322
|
+
}
|
|
323
|
+
return ""
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const syncBuiltinSourceFiles = async (kernel, index) => {
|
|
327
|
+
let changed = false
|
|
328
|
+
for (const builtin of Object.values(BUILTIN_SKILLS)) {
|
|
329
|
+
const content = await composeBuiltinSkillContent(kernel, builtin.id)
|
|
330
|
+
if (!content.trim()) {
|
|
331
|
+
continue
|
|
332
|
+
}
|
|
333
|
+
const target = skillPath(kernel, builtin.id)
|
|
334
|
+
const wrote = await writeFileIfChanged(target, normalizeText(content).trim() + "\n")
|
|
335
|
+
if (wrote && index.skills[builtin.id]) {
|
|
336
|
+
index.skills[builtin.id].updatedAt = new Date().toISOString()
|
|
337
|
+
changed = true
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return changed
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const scanSkillFolders = async (kernel) => {
|
|
344
|
+
let entries = []
|
|
345
|
+
try {
|
|
346
|
+
entries = await fs.promises.readdir(skillsRoot(kernel), { withFileTypes: true })
|
|
347
|
+
} catch (_) {
|
|
348
|
+
return []
|
|
349
|
+
}
|
|
350
|
+
return entries
|
|
351
|
+
.filter((entry) => entry && entry.isDirectory())
|
|
352
|
+
.filter((entry) => !String(entry.name || "").startsWith("."))
|
|
353
|
+
.map((entry) => normalizeSkillId(entry.name))
|
|
354
|
+
.filter(Boolean)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const reconcileIndexWithFolders = async (kernel, index) => {
|
|
358
|
+
const now = new Date().toISOString()
|
|
359
|
+
let changed = false
|
|
360
|
+
const ids = await scanSkillFolders(kernel)
|
|
361
|
+
for (const id of ids) {
|
|
362
|
+
if (index.skills[id]) {
|
|
363
|
+
continue
|
|
364
|
+
}
|
|
365
|
+
index.skills[id] = {
|
|
366
|
+
id,
|
|
367
|
+
source: "local",
|
|
368
|
+
ref: "",
|
|
369
|
+
enabled: false,
|
|
370
|
+
publishName: defaultPublishName(id),
|
|
371
|
+
builtin: false,
|
|
372
|
+
removable: true,
|
|
373
|
+
installedAt: now,
|
|
374
|
+
updatedAt: now
|
|
375
|
+
}
|
|
376
|
+
changed = true
|
|
377
|
+
}
|
|
378
|
+
return changed
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const ensureManagedSkillState = async (kernel) => {
|
|
382
|
+
let index = await readIndex(kernel)
|
|
383
|
+
let changed = ensureBuiltinIndexEntries(index)
|
|
384
|
+
if (await syncBuiltinSourceFiles(kernel, index)) {
|
|
385
|
+
changed = true
|
|
386
|
+
}
|
|
387
|
+
if (await reconcileIndexWithFolders(kernel, index)) {
|
|
388
|
+
changed = true
|
|
389
|
+
}
|
|
390
|
+
if (changed) {
|
|
391
|
+
index = await writeIndex(kernel, index)
|
|
392
|
+
}
|
|
393
|
+
return index
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const readManagedMarker = async (dir) => {
|
|
397
|
+
try {
|
|
398
|
+
const raw = await fs.promises.readFile(path.resolve(dir, MARKER_FILENAME), "utf8")
|
|
399
|
+
const parsed = JSON.parse(raw)
|
|
400
|
+
return parsed && typeof parsed === "object" ? parsed : null
|
|
401
|
+
} catch (_) {
|
|
402
|
+
return null
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const isMarkerForSkill = (marker, skillId) => {
|
|
407
|
+
return marker
|
|
408
|
+
&& marker.manager === MANAGER_ID
|
|
409
|
+
&& normalizeSkillId(marker.skillId) === normalizeSkillId(skillId)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const writeManagedMarker = async (dir, entry, sourcePath) => {
|
|
413
|
+
const marker = {
|
|
414
|
+
manager: MANAGER_ID,
|
|
415
|
+
skillId: entry.id,
|
|
416
|
+
publishName: entry.publishName,
|
|
417
|
+
source: sourcePath,
|
|
418
|
+
ref: entry.ref || ""
|
|
419
|
+
}
|
|
420
|
+
await writeFileIfChanged(path.resolve(dir, MARKER_FILENAME), JSON.stringify(marker, null, 2) + "\n")
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const targetDirFor = (root, publishName) => path.resolve(root, publishName)
|
|
424
|
+
|
|
425
|
+
const hasOnlyManagedPublishedFiles = async (dir) => {
|
|
426
|
+
let entries = []
|
|
427
|
+
try {
|
|
428
|
+
entries = await fs.promises.readdir(dir, { withFileTypes: true })
|
|
429
|
+
} catch (_) {
|
|
430
|
+
return false
|
|
431
|
+
}
|
|
432
|
+
if (!entries.length) {
|
|
433
|
+
return false
|
|
434
|
+
}
|
|
435
|
+
const allowed = new Set(["skill.md", MARKER_FILENAME.toLowerCase()])
|
|
436
|
+
return entries.every((entry) => entry && entry.isFile() && allowed.has(String(entry.name || "").toLowerCase()))
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const removePublishedCopy = async (targetDir, entry, desiredContent = "") => {
|
|
440
|
+
let stat = null
|
|
441
|
+
try {
|
|
442
|
+
stat = await fs.promises.stat(targetDir)
|
|
443
|
+
} catch (_) {
|
|
444
|
+
return { removed: false, skipped: true, reason: "missing" }
|
|
445
|
+
}
|
|
446
|
+
if (!stat.isDirectory()) {
|
|
447
|
+
return { removed: false, skipped: true, reason: "not-directory" }
|
|
448
|
+
}
|
|
449
|
+
const marker = await readManagedMarker(targetDir)
|
|
450
|
+
if (isMarkerForSkill(marker, entry.id)) {
|
|
451
|
+
await fs.promises.rm(targetDir, { recursive: true, force: true })
|
|
452
|
+
return { removed: true }
|
|
453
|
+
}
|
|
454
|
+
if (desiredContent) {
|
|
455
|
+
const existingContent = await readSkillContent(path.resolve(targetDir, "SKILL.md"))
|
|
456
|
+
if (
|
|
457
|
+
normalizeText(existingContent).trim() === normalizeText(desiredContent).trim()
|
|
458
|
+
&& await hasOnlyManagedPublishedFiles(targetDir)
|
|
459
|
+
) {
|
|
460
|
+
await fs.promises.rm(targetDir, { recursive: true, force: true })
|
|
461
|
+
return { removed: true, adoptedLegacy: true }
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return { removed: false, skipped: true, reason: "user-owned" }
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const cleanupOrphanedPublishedCopies = async (index, roots = publishRoots()) => {
|
|
468
|
+
const activeByName = new Map()
|
|
469
|
+
for (const entry of Object.values(index.skills)) {
|
|
470
|
+
if (entry && entry.enabled && entry.publishName) {
|
|
471
|
+
activeByName.set(entry.publishName, entry.id)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
for (const root of roots) {
|
|
475
|
+
let entries = []
|
|
476
|
+
try {
|
|
477
|
+
entries = await fs.promises.readdir(root, { withFileTypes: true })
|
|
478
|
+
} catch (_) {
|
|
479
|
+
continue
|
|
480
|
+
}
|
|
481
|
+
for (const dirent of entries) {
|
|
482
|
+
if (!dirent || !dirent.isDirectory()) {
|
|
483
|
+
continue
|
|
484
|
+
}
|
|
485
|
+
const childDir = path.resolve(root, dirent.name)
|
|
486
|
+
const marker = await readManagedMarker(childDir)
|
|
487
|
+
if (!marker || marker.manager !== MANAGER_ID) {
|
|
488
|
+
continue
|
|
489
|
+
}
|
|
490
|
+
const expectedSkillId = activeByName.get(dirent.name)
|
|
491
|
+
if (!expectedSkillId || normalizeSkillId(expectedSkillId) !== normalizeSkillId(marker.skillId)) {
|
|
492
|
+
await fs.promises.rm(childDir, { recursive: true, force: true })
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const publishSkillToRoot = async (root, entry, validation) => {
|
|
499
|
+
const targetDir = targetDirFor(root, entry.publishName)
|
|
500
|
+
const desiredContent = normalizeText(validation.content).trim() + "\n"
|
|
501
|
+
let exists = false
|
|
502
|
+
let isDirectory = false
|
|
503
|
+
try {
|
|
504
|
+
const stat = await fs.promises.stat(targetDir)
|
|
505
|
+
exists = true
|
|
506
|
+
isDirectory = stat.isDirectory()
|
|
507
|
+
} catch (_) {
|
|
508
|
+
}
|
|
509
|
+
if (exists && !isDirectory) {
|
|
510
|
+
return {
|
|
511
|
+
root,
|
|
512
|
+
path: targetDir,
|
|
513
|
+
status: "conflict",
|
|
514
|
+
message: "A file already exists at the publish path."
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (exists) {
|
|
518
|
+
const marker = await readManagedMarker(targetDir)
|
|
519
|
+
const existingContent = await readSkillContent(path.resolve(targetDir, "SKILL.md"))
|
|
520
|
+
const sameContent = normalizeText(existingContent).trim() === normalizeText(desiredContent).trim()
|
|
521
|
+
const canManage = isMarkerForSkill(marker, entry.id)
|
|
522
|
+
|| (sameContent && await hasOnlyManagedPublishedFiles(targetDir))
|
|
523
|
+
if (!canManage) {
|
|
524
|
+
return {
|
|
525
|
+
root,
|
|
526
|
+
path: targetDir,
|
|
527
|
+
status: "conflict",
|
|
528
|
+
message: "A non-Pinokio skill already exists here."
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
await fs.promises.mkdir(targetDir, { recursive: true })
|
|
533
|
+
await writeFileIfChanged(path.resolve(targetDir, "SKILL.md"), desiredContent)
|
|
534
|
+
await writeManagedMarker(targetDir, entry, validation.path)
|
|
535
|
+
return {
|
|
536
|
+
root,
|
|
537
|
+
path: targetDir,
|
|
538
|
+
status: "published",
|
|
539
|
+
message: ""
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const syncManagedSkills = async (kernel, options = {}) => {
|
|
544
|
+
const roots = Array.isArray(options.publishRoots) ? options.publishRoots : publishRoots()
|
|
545
|
+
const index = await ensureManagedSkillState(kernel)
|
|
546
|
+
await cleanupOrphanedPublishedCopies(index, roots)
|
|
547
|
+
const results = []
|
|
548
|
+
for (const entry of Object.values(index.skills)) {
|
|
549
|
+
const validation = await validateSkillDir(skillDir(kernel, entry.id), entry.id)
|
|
550
|
+
const skillResult = {
|
|
551
|
+
id: entry.id,
|
|
552
|
+
publishName: entry.publishName,
|
|
553
|
+
enabled: entry.enabled === true,
|
|
554
|
+
valid: validation.valid,
|
|
555
|
+
targets: []
|
|
556
|
+
}
|
|
557
|
+
if (entry.enabled && validation.valid) {
|
|
558
|
+
for (const root of roots) {
|
|
559
|
+
skillResult.targets.push(await publishSkillToRoot(root, entry, validation))
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
for (const root of roots) {
|
|
563
|
+
const targetDir = targetDirFor(root, entry.publishName)
|
|
564
|
+
const removal = await removePublishedCopy(targetDir, entry, validation.content)
|
|
565
|
+
skillResult.targets.push({
|
|
566
|
+
root,
|
|
567
|
+
path: targetDir,
|
|
568
|
+
status: removal.removed ? "removed" : (removal.reason === "missing" ? "disabled" : "conflict"),
|
|
569
|
+
message: removal.reason || ""
|
|
570
|
+
})
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
results.push(skillResult)
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
index,
|
|
577
|
+
results
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const publishStatusForSkill = async (entry, validation, roots = publishRoots()) => {
|
|
582
|
+
const targets = []
|
|
583
|
+
for (const root of roots) {
|
|
584
|
+
const target = targetDirFor(root, entry.publishName)
|
|
585
|
+
let exists = false
|
|
586
|
+
let isDirectory = false
|
|
587
|
+
try {
|
|
588
|
+
const stat = await fs.promises.stat(target)
|
|
589
|
+
exists = true
|
|
590
|
+
isDirectory = stat.isDirectory()
|
|
591
|
+
} catch (_) {
|
|
592
|
+
}
|
|
593
|
+
if (!exists) {
|
|
594
|
+
targets.push({
|
|
595
|
+
root,
|
|
596
|
+
path: target,
|
|
597
|
+
status: entry.enabled ? "missing" : "disabled",
|
|
598
|
+
message: ""
|
|
599
|
+
})
|
|
600
|
+
continue
|
|
601
|
+
}
|
|
602
|
+
if (!isDirectory) {
|
|
603
|
+
targets.push({
|
|
604
|
+
root,
|
|
605
|
+
path: target,
|
|
606
|
+
status: "conflict",
|
|
607
|
+
message: "A file exists at this path."
|
|
608
|
+
})
|
|
609
|
+
continue
|
|
610
|
+
}
|
|
611
|
+
const marker = await readManagedMarker(target)
|
|
612
|
+
const existingContent = await readSkillContent(path.resolve(target, "SKILL.md"))
|
|
613
|
+
const sameContent = validation.content
|
|
614
|
+
&& normalizeText(existingContent).trim() === normalizeText(validation.content).trim()
|
|
615
|
+
const legacyManageable = sameContent && await hasOnlyManagedPublishedFiles(target)
|
|
616
|
+
if (isMarkerForSkill(marker, entry.id)) {
|
|
617
|
+
targets.push({
|
|
618
|
+
root,
|
|
619
|
+
path: target,
|
|
620
|
+
status: entry.enabled ? "published" : "stale-managed",
|
|
621
|
+
message: ""
|
|
622
|
+
})
|
|
623
|
+
} else if (legacyManageable) {
|
|
624
|
+
targets.push({
|
|
625
|
+
root,
|
|
626
|
+
path: target,
|
|
627
|
+
status: "legacy-managed",
|
|
628
|
+
message: ""
|
|
629
|
+
})
|
|
630
|
+
} else {
|
|
631
|
+
targets.push({
|
|
632
|
+
root,
|
|
633
|
+
path: target,
|
|
634
|
+
status: "conflict",
|
|
635
|
+
message: "A non-Pinokio skill already exists here."
|
|
636
|
+
})
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return targets
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const listManagedSkills = async (kernel, options = {}) => {
|
|
643
|
+
const shouldSync = options.sync !== false
|
|
644
|
+
if (shouldSync) {
|
|
645
|
+
await syncManagedSkills(kernel, options)
|
|
646
|
+
} else {
|
|
647
|
+
await ensureManagedSkillState(kernel)
|
|
648
|
+
}
|
|
649
|
+
const index = await readIndex(kernel)
|
|
650
|
+
const roots = Array.isArray(options.publishRoots) ? options.publishRoots : publishRoots()
|
|
651
|
+
const items = []
|
|
652
|
+
for (const entry of Object.values(index.skills)) {
|
|
653
|
+
const validation = await validateSkillDir(skillDir(kernel, entry.id), entry.id)
|
|
654
|
+
const targets = await publishStatusForSkill(entry, validation, roots)
|
|
655
|
+
items.push({
|
|
656
|
+
...entry,
|
|
657
|
+
path: skillPath(kernel, entry.id),
|
|
658
|
+
dir: skillDir(kernel, entry.id),
|
|
659
|
+
valid: validation.valid,
|
|
660
|
+
errors: validation.errors,
|
|
661
|
+
label: validation.label,
|
|
662
|
+
description: validation.description,
|
|
663
|
+
targets,
|
|
664
|
+
hasConflict: targets.some((target) => target.status === "conflict")
|
|
665
|
+
})
|
|
666
|
+
}
|
|
667
|
+
items.sort((a, b) => {
|
|
668
|
+
const ab = a.builtin ? 0 : 1
|
|
669
|
+
const bb = b.builtin ? 0 : 1
|
|
670
|
+
if (ab !== bb) return ab - bb
|
|
671
|
+
return String(a.label || a.id).localeCompare(String(b.label || b.id))
|
|
672
|
+
})
|
|
673
|
+
return items
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const getManagedSkill = async (kernel, id, options = {}) => {
|
|
677
|
+
const normalizedId = normalizeSkillId(id)
|
|
678
|
+
const items = await listManagedSkills(kernel, options)
|
|
679
|
+
return items.find((item) => item.id === normalizedId) || null
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const assertUniquePublishName = (index, id, publishName) => {
|
|
683
|
+
for (const entry of Object.values(index.skills)) {
|
|
684
|
+
if (!entry || entry.id === id) {
|
|
685
|
+
continue
|
|
686
|
+
}
|
|
687
|
+
if (entry.publishName === publishName) {
|
|
688
|
+
const error = new Error(`Publish name is already used by ${entry.id}.`)
|
|
689
|
+
error.status = 409
|
|
690
|
+
throw error
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const setSkillEnabled = async (kernel, id, enabled, options = {}) => {
|
|
696
|
+
const normalizedId = normalizeSkillId(id)
|
|
697
|
+
let index = await ensureManagedSkillState(kernel)
|
|
698
|
+
const entry = index.skills[normalizedId]
|
|
699
|
+
if (!entry) {
|
|
700
|
+
const error = new Error("Skill not found.")
|
|
701
|
+
error.status = 404
|
|
702
|
+
throw error
|
|
703
|
+
}
|
|
704
|
+
if (enabled) {
|
|
705
|
+
const validation = await validateSkillDir(skillDir(kernel, normalizedId), normalizedId)
|
|
706
|
+
if (!validation.valid) {
|
|
707
|
+
const error = new Error(validation.errors[0] || "Skill is invalid.")
|
|
708
|
+
error.status = 400
|
|
709
|
+
throw error
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
entry.enabled = enabled === true
|
|
713
|
+
entry.updatedAt = new Date().toISOString()
|
|
714
|
+
index = await writeIndex(kernel, index)
|
|
715
|
+
await syncManagedSkills(kernel, options)
|
|
716
|
+
return getManagedSkill(kernel, normalizedId, { ...options, sync: false })
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const setSkillPublishName = async (kernel, id, publishName, options = {}) => {
|
|
720
|
+
const normalizedId = normalizeSkillId(id)
|
|
721
|
+
const normalizedPublishName = normalizePublishName(publishName)
|
|
722
|
+
if (!normalizedPublishName) {
|
|
723
|
+
const error = new Error("Publish name is invalid.")
|
|
724
|
+
error.status = 400
|
|
725
|
+
throw error
|
|
726
|
+
}
|
|
727
|
+
let index = await ensureManagedSkillState(kernel)
|
|
728
|
+
const entry = index.skills[normalizedId]
|
|
729
|
+
if (!entry) {
|
|
730
|
+
const error = new Error("Skill not found.")
|
|
731
|
+
error.status = 404
|
|
732
|
+
throw error
|
|
733
|
+
}
|
|
734
|
+
if (entry.builtin) {
|
|
735
|
+
const error = new Error("Built-in skill publish names cannot be changed.")
|
|
736
|
+
error.status = 400
|
|
737
|
+
throw error
|
|
738
|
+
}
|
|
739
|
+
assertUniquePublishName(index, normalizedId, normalizedPublishName)
|
|
740
|
+
entry.publishName = normalizedPublishName
|
|
741
|
+
entry.updatedAt = new Date().toISOString()
|
|
742
|
+
index = await writeIndex(kernel, index)
|
|
743
|
+
await syncManagedSkills(kernel, options)
|
|
744
|
+
return getManagedSkill(kernel, normalizedId, { ...options, sync: false })
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const removeSkill = async (kernel, id, options = {}) => {
|
|
748
|
+
const normalizedId = normalizeSkillId(id)
|
|
749
|
+
let index = await ensureManagedSkillState(kernel)
|
|
750
|
+
const entry = index.skills[normalizedId]
|
|
751
|
+
if (!entry) {
|
|
752
|
+
const error = new Error("Skill not found.")
|
|
753
|
+
error.status = 404
|
|
754
|
+
throw error
|
|
755
|
+
}
|
|
756
|
+
if (entry.builtin) {
|
|
757
|
+
const error = new Error("Built-in skills can be disabled, not removed.")
|
|
758
|
+
error.status = 400
|
|
759
|
+
throw error
|
|
760
|
+
}
|
|
761
|
+
entry.enabled = false
|
|
762
|
+
index = await writeIndex(kernel, index)
|
|
763
|
+
await syncManagedSkills(kernel, options)
|
|
764
|
+
await fs.promises.rm(skillDir(kernel, normalizedId), { recursive: true, force: true })
|
|
765
|
+
delete index.skills[normalizedId]
|
|
766
|
+
await writeIndex(kernel, index)
|
|
767
|
+
await syncManagedSkills(kernel, options)
|
|
768
|
+
return { id: normalizedId }
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const tempSkillCloneDir = async (kernel, id) => {
|
|
772
|
+
const tempRoot = path.resolve(skillsRoot(kernel), TEMP_DIRNAME)
|
|
773
|
+
await fs.promises.mkdir(tempRoot, { recursive: true })
|
|
774
|
+
return path.resolve(tempRoot, `${normalizeSkillId(id)}-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`)
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const installSkillFromGit = async (kernel, options = {}) => {
|
|
778
|
+
const ref = typeof options.ref === "string" ? options.ref.trim() : ""
|
|
779
|
+
if (!ref) {
|
|
780
|
+
const error = new Error("Git URL is required.")
|
|
781
|
+
error.status = 400
|
|
782
|
+
throw error
|
|
783
|
+
}
|
|
784
|
+
const id = deriveSkillIdFromRef(ref)
|
|
785
|
+
if (!id) {
|
|
786
|
+
const error = new Error("Skill folder name is invalid.")
|
|
787
|
+
error.status = 400
|
|
788
|
+
throw error
|
|
789
|
+
}
|
|
790
|
+
let index = await ensureManagedSkillState(kernel)
|
|
791
|
+
if (index.skills[id]) {
|
|
792
|
+
const error = new Error("Skill already exists.")
|
|
793
|
+
error.status = 409
|
|
794
|
+
throw error
|
|
795
|
+
}
|
|
796
|
+
const publishName = defaultPublishName(id)
|
|
797
|
+
assertUniquePublishName(index, id, publishName)
|
|
798
|
+
const targetDir = skillDir(kernel, id)
|
|
799
|
+
try {
|
|
800
|
+
await fs.promises.access(targetDir, fs.constants.F_OK)
|
|
801
|
+
const error = new Error("Skill folder already exists.")
|
|
802
|
+
error.status = 409
|
|
803
|
+
throw error
|
|
804
|
+
} catch (error) {
|
|
805
|
+
if (error && error.code !== "ENOENT") {
|
|
806
|
+
throw error
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
await fs.promises.mkdir(skillsRoot(kernel), { recursive: true })
|
|
810
|
+
const tempDir = await tempSkillCloneDir(kernel, id)
|
|
811
|
+
try {
|
|
812
|
+
await kernel.exec({
|
|
813
|
+
message: [{ _: ["git", "clone", "--depth", "1", "--single-branch", ref, tempDir] }],
|
|
814
|
+
path: skillsRoot(kernel),
|
|
815
|
+
env: { ...NON_INTERACTIVE_GIT_ENV }
|
|
816
|
+
}, () => {})
|
|
817
|
+
await fs.promises.rename(tempDir, targetDir)
|
|
818
|
+
} catch (error) {
|
|
819
|
+
await fs.promises.rm(tempDir, { recursive: true, force: true }).catch(() => {})
|
|
820
|
+
const nextError = new Error(error && error.message ? error.message : "Failed to clone skill repository.")
|
|
821
|
+
nextError.status = error && error.status ? error.status : 500
|
|
822
|
+
throw nextError
|
|
823
|
+
}
|
|
824
|
+
const validation = await validateSkillDir(targetDir, id)
|
|
825
|
+
const now = new Date().toISOString()
|
|
826
|
+
index.skills[id] = {
|
|
827
|
+
id,
|
|
828
|
+
source: "git",
|
|
829
|
+
ref,
|
|
830
|
+
enabled: validation.valid,
|
|
831
|
+
publishName,
|
|
832
|
+
builtin: false,
|
|
833
|
+
removable: true,
|
|
834
|
+
installedAt: now,
|
|
835
|
+
updatedAt: now
|
|
836
|
+
}
|
|
837
|
+
index = await writeIndex(kernel, index)
|
|
838
|
+
await syncManagedSkills(kernel, options)
|
|
839
|
+
return getManagedSkill(kernel, id, { ...options, sync: false })
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const readEnabledManagedSkillBody = async (kernel, id) => {
|
|
843
|
+
const skill = await getManagedSkill(kernel, id, { sync: false })
|
|
844
|
+
if (!skill || !skill.enabled || !skill.valid) {
|
|
845
|
+
return ""
|
|
846
|
+
}
|
|
847
|
+
const content = await readSkillContent(skill.path)
|
|
848
|
+
return parseSimpleFrontmatter(content).bodyWithoutFrontmatter
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
module.exports = {
|
|
852
|
+
INDEX_FILENAME,
|
|
853
|
+
MARKER_FILENAME,
|
|
854
|
+
defaultPublishName,
|
|
855
|
+
deriveSkillIdFromRef,
|
|
856
|
+
getManagedSkill,
|
|
857
|
+
indexPath,
|
|
858
|
+
installSkillFromGit,
|
|
859
|
+
listManagedSkills,
|
|
860
|
+
normalizePublishName,
|
|
861
|
+
normalizeSkillId,
|
|
862
|
+
publishRoots,
|
|
863
|
+
readEnabledManagedSkillBody,
|
|
864
|
+
removeSkill,
|
|
865
|
+
setSkillEnabled,
|
|
866
|
+
setSkillPublishName,
|
|
867
|
+
skillPath,
|
|
868
|
+
skillsRoot,
|
|
869
|
+
syncManagedSkills,
|
|
870
|
+
validateSkillDir
|
|
871
|
+
}
|