pinokiod 7.3.1 → 7.3.4

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 (122) hide show
  1. package/kernel/api/github/index.js +444 -0
  2. package/kernel/api/index.js +199 -11
  3. package/kernel/api/process/index.js +124 -44
  4. package/kernel/api/shell_run_template.js +273 -0
  5. package/kernel/api/uri/index.js +51 -0
  6. package/kernel/bin/git.js +9 -10
  7. package/kernel/bin/huggingface.js +1 -1
  8. package/kernel/bin/zip.js +9 -1
  9. package/kernel/connect/providers/github/README.md +5 -4
  10. package/kernel/environment.js +195 -92
  11. package/kernel/git.js +126 -19
  12. package/kernel/gitconfig_template +7 -0
  13. package/kernel/gpu/amd.js +72 -0
  14. package/kernel/gpu/apple.js +8 -0
  15. package/kernel/gpu/common.js +12 -0
  16. package/kernel/gpu/intel.js +47 -0
  17. package/kernel/gpu/nvidia.js +8 -0
  18. package/kernel/index.js +11 -1
  19. package/kernel/managed_skills.js +871 -0
  20. package/kernel/plugin.js +6 -58
  21. package/kernel/plugin_sources.js +316 -0
  22. package/kernel/resource_usage/gpu.js +349 -0
  23. package/kernel/resource_usage/index.js +322 -0
  24. package/kernel/resource_usage/macos_footprint.js +197 -0
  25. package/kernel/resource_usage/preferences.js +92 -0
  26. package/kernel/resource_usage/process_tree.js +303 -0
  27. package/kernel/scripts/git/create +4 -4
  28. package/kernel/scripts/git/fork +7 -8
  29. package/kernel/shell.js +23 -2
  30. package/kernel/shells.js +41 -0
  31. package/kernel/sysinfo.js +62 -9
  32. package/kernel/util.js +60 -0
  33. package/package.json +1 -1
  34. package/server/index.js +984 -156
  35. package/server/lib/app_log_report.js +543 -0
  36. package/server/lib/content_validation.js +55 -33
  37. package/server/lib/launcher_instruction_bootstrap.js +4 -96
  38. package/server/lib/terminal_session_helpers.js +0 -3
  39. package/server/public/common.js +77 -31
  40. package/server/public/create-launcher.js +4 -32
  41. package/server/public/logs.js +1428 -0
  42. package/server/public/nav.js +7 -0
  43. package/server/public/plugin-detail.js +93 -10
  44. package/server/public/privacy_filter_worker.js +391 -0
  45. package/server/public/style.css +1104 -154
  46. package/server/public/task-launcher.js +8 -29
  47. package/server/public/universal-launcher.css +8 -6
  48. package/server/public/universal-launcher.js +3 -27
  49. package/server/routes/apps.js +195 -1
  50. package/server/views/app.ejs +3041 -717
  51. package/server/views/autolaunch.ejs +917 -0
  52. package/server/views/bootstrap.ejs +7 -1
  53. package/server/views/d.ejs +408 -65
  54. package/server/views/editor.ejs +85 -19
  55. package/server/views/index.ejs +661 -111
  56. package/server/views/init/index.ejs +1 -1
  57. package/server/views/install.ejs +1 -1
  58. package/server/views/logs.ejs +164 -86
  59. package/server/views/net.ejs +7 -1
  60. package/server/views/partials/d_terminal_column.ejs +2 -2
  61. package/server/views/partials/d_terminal_options.ejs +0 -8
  62. package/server/views/partials/fs_status.ejs +47 -0
  63. package/server/views/partials/home_action_modal.ejs +86 -0
  64. package/server/views/partials/home_run_menu.ejs +87 -0
  65. package/server/views/partials/main_sidebar.ejs +2 -0
  66. package/server/views/partials/menu.ejs +1 -1
  67. package/server/views/plugin_detail.ejs +19 -4
  68. package/server/views/plugins.ejs +201 -3
  69. package/server/views/pre.ejs +1 -1
  70. package/server/views/pro.ejs +1 -1
  71. package/server/views/shell.ejs +40 -18
  72. package/server/views/skills.ejs +506 -0
  73. package/server/views/terminal.ejs +45 -19
  74. package/spec/INSTRUCTION_SYNC.md +20 -10
  75. package/system/plugin/antigravity-cli/antigravity.png +0 -0
  76. package/system/plugin/antigravity-cli/common.js +155 -0
  77. package/system/plugin/antigravity-cli/install.js +272 -0
  78. package/system/plugin/antigravity-cli/pinokio.js +13 -0
  79. package/system/plugin/antigravity-cli-auto/antigravity.png +0 -0
  80. package/system/plugin/antigravity-cli-auto/pinokio.js +13 -0
  81. package/system/plugin/claude/claude.png +0 -0
  82. package/system/plugin/claude/pinokio.js +47 -0
  83. package/system/plugin/claude-auto/claude.png +0 -0
  84. package/system/plugin/claude-auto/pinokio.js +58 -0
  85. package/system/plugin/claude-desktop/icon.jpeg +0 -0
  86. package/system/plugin/claude-desktop/pinokio.js +23 -0
  87. package/system/plugin/codex/openai.webp +0 -0
  88. package/system/plugin/codex/pinokio.js +42 -0
  89. package/system/plugin/codex-auto/openai.webp +0 -0
  90. package/system/plugin/codex-auto/pinokio.js +49 -0
  91. package/system/plugin/codex-desktop/icon.png +0 -0
  92. package/system/plugin/codex-desktop/pinokio.js +23 -0
  93. package/system/plugin/crush/crush.png +0 -0
  94. package/system/plugin/crush/pinokio.js +15 -0
  95. package/system/plugin/cursor/cursor.jpeg +0 -0
  96. package/system/plugin/cursor/pinokio.js +23 -0
  97. package/system/plugin/qwen/pinokio.js +34 -0
  98. package/system/plugin/qwen/qwen.png +0 -0
  99. package/system/plugin/vscode/pinokio.js +20 -0
  100. package/system/plugin/vscode/vscode.png +0 -0
  101. package/system/plugin/windsurf/pinokio.js +23 -0
  102. package/system/plugin/windsurf/windsurf.png +0 -0
  103. package/test/antigravity-cli-plugin.test.js +185 -0
  104. package/test/app-api.test.js +239 -0
  105. package/test/app-log-report.test.js +67 -0
  106. package/test/environment-cache-preflight.test.js +98 -0
  107. package/test/git-bin.test.js +59 -0
  108. package/test/git-defaults.test.js +150 -0
  109. package/test/github-api.test.js +158 -0
  110. package/test/github-connection.test.js +117 -0
  111. package/test/huggingface-bin.test.js +25 -0
  112. package/test/managed-skills.test.js +351 -0
  113. package/test/plugin-action-functions.test.js +337 -0
  114. package/test/plugin-dev-iframe.test.js +17 -0
  115. package/test/plugin-sources.test.js +203 -0
  116. package/test/privacy-filter-worker-heuristics.test.js +69 -0
  117. package/test/process-wait.test.js +169 -0
  118. package/test/script-api.test.js +97 -0
  119. package/test/shell-api.test.js +134 -0
  120. package/test/shell-run-template.test.js +209 -0
  121. package/test/storage-api.test.js +137 -0
  122. 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
+ }