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.
- 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 +126 -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 +150 -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,351 @@
|
|
|
1
|
+
const assert = require("node:assert/strict")
|
|
2
|
+
const fs = require("node:fs/promises")
|
|
3
|
+
const os = require("node:os")
|
|
4
|
+
const path = require("node:path")
|
|
5
|
+
const test = require("node:test")
|
|
6
|
+
|
|
7
|
+
const ManagedSkills = require("../kernel/managed_skills")
|
|
8
|
+
|
|
9
|
+
async function withTempHome(fn) {
|
|
10
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-managed-skills-"))
|
|
11
|
+
const homedir = path.resolve(root, "home")
|
|
12
|
+
const userhome = path.resolve(root, "user")
|
|
13
|
+
await fs.mkdir(homedir, { recursive: true })
|
|
14
|
+
await fs.mkdir(userhome, { recursive: true })
|
|
15
|
+
try {
|
|
16
|
+
await fs.writeFile(path.resolve(homedir, "AGENTS.md"), "# Home instructions\n\nUse Pinokio carefully.\n")
|
|
17
|
+
await fn({
|
|
18
|
+
root,
|
|
19
|
+
homedir,
|
|
20
|
+
userhome,
|
|
21
|
+
kernel: {
|
|
22
|
+
homedir,
|
|
23
|
+
path: (...parts) => path.resolve(homedir, ...parts),
|
|
24
|
+
exec: async () => {}
|
|
25
|
+
},
|
|
26
|
+
publishRoots: ManagedSkills.publishRoots(userhome)
|
|
27
|
+
})
|
|
28
|
+
} finally {
|
|
29
|
+
await fs.rm(root, { recursive: true, force: true })
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function readJson(filepath) {
|
|
34
|
+
return JSON.parse(await fs.readFile(filepath, "utf8"))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function cloneTargetFromMessage(message) {
|
|
38
|
+
const command = Array.isArray(message) ? message[0] : message
|
|
39
|
+
assert.ok(command && command._ && Array.isArray(command._), "Clone command should use structured shell arguments.")
|
|
40
|
+
return command._[command._.length - 1]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cloneRefFromMessage(message) {
|
|
44
|
+
const command = Array.isArray(message) ? message[0] : message
|
|
45
|
+
assert.ok(command && command._ && Array.isArray(command._), "Clone command should use structured shell arguments.")
|
|
46
|
+
return command._[command._.length - 2]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test("managed skills publish built-ins by default and respect disable state", async () => {
|
|
50
|
+
await withTempHome(async ({ kernel, publishRoots }) => {
|
|
51
|
+
await ManagedSkills.syncManagedSkills(kernel, { publishRoots })
|
|
52
|
+
|
|
53
|
+
const index = await readJson(ManagedSkills.indexPath(kernel))
|
|
54
|
+
assert.equal(index.skills.pinokio.enabled, true)
|
|
55
|
+
assert.equal(index.skills.gepeto.enabled, true)
|
|
56
|
+
assert.equal(index.skills.pinokio.publishName, "pinokio")
|
|
57
|
+
assert.equal(index.skills.gepeto.publishName, "gepeto")
|
|
58
|
+
|
|
59
|
+
for (const root of publishRoots) {
|
|
60
|
+
assert.match(await fs.readFile(path.resolve(root, "pinokio", "SKILL.md"), "utf8"), /name: pinokio/)
|
|
61
|
+
assert.match(await fs.readFile(path.resolve(root, "gepeto", "SKILL.md"), "utf8"), /name: gepeto/)
|
|
62
|
+
const marker = await readJson(path.resolve(root, "gepeto", ManagedSkills.MARKER_FILENAME))
|
|
63
|
+
assert.equal(marker.manager, "pinokio")
|
|
64
|
+
assert.equal(marker.skillId, "gepeto")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await ManagedSkills.setSkillEnabled(kernel, "gepeto", false, { publishRoots })
|
|
68
|
+
|
|
69
|
+
for (const root of publishRoots) {
|
|
70
|
+
await assert.rejects(
|
|
71
|
+
fs.stat(path.resolve(root, "gepeto")),
|
|
72
|
+
{ code: "ENOENT" }
|
|
73
|
+
)
|
|
74
|
+
assert.match(await fs.readFile(path.resolve(root, "pinokio", "SKILL.md"), "utf8"), /name: pinokio/)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test("built-in publish names are fixed", async () => {
|
|
80
|
+
await withTempHome(async ({ kernel, publishRoots }) => {
|
|
81
|
+
await ManagedSkills.syncManagedSkills(kernel, { publishRoots })
|
|
82
|
+
|
|
83
|
+
await assert.rejects(
|
|
84
|
+
ManagedSkills.setSkillPublishName(kernel, "gepeto", "custom-gepeto", { publishRoots }),
|
|
85
|
+
/Built-in skill publish names cannot be changed/
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const index = await readJson(ManagedSkills.indexPath(kernel))
|
|
89
|
+
index.skills.gepeto.publishName = "custom-gepeto"
|
|
90
|
+
await fs.writeFile(ManagedSkills.indexPath(kernel), JSON.stringify(index, null, 2))
|
|
91
|
+
|
|
92
|
+
await ManagedSkills.syncManagedSkills(kernel, { publishRoots })
|
|
93
|
+
|
|
94
|
+
const updated = await readJson(ManagedSkills.indexPath(kernel))
|
|
95
|
+
assert.equal(updated.skills.gepeto.publishName, "gepeto")
|
|
96
|
+
for (const root of publishRoots) {
|
|
97
|
+
assert.match(await fs.readFile(path.resolve(root, "gepeto", "SKILL.md"), "utf8"), /name: gepeto/)
|
|
98
|
+
await assert.rejects(
|
|
99
|
+
fs.stat(path.resolve(root, "custom-gepeto")),
|
|
100
|
+
{ code: "ENOENT" }
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test("enabled managed skills repair deleted published copies on sync", async () => {
|
|
107
|
+
await withTempHome(async ({ kernel, publishRoots }) => {
|
|
108
|
+
await ManagedSkills.syncManagedSkills(kernel, { publishRoots })
|
|
109
|
+
await fs.rm(path.resolve(publishRoots[0], "pinokio"), { recursive: true, force: true })
|
|
110
|
+
|
|
111
|
+
await ManagedSkills.syncManagedSkills(kernel, { publishRoots })
|
|
112
|
+
|
|
113
|
+
assert.match(await fs.readFile(path.resolve(publishRoots[0], "pinokio", "SKILL.md"), "utf8"), /name: pinokio/)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test("managed skills do not overwrite user-owned conflicts", async () => {
|
|
118
|
+
await withTempHome(async ({ kernel, publishRoots }) => {
|
|
119
|
+
const conflictDir = path.resolve(publishRoots[0], "gepeto")
|
|
120
|
+
await fs.mkdir(conflictDir, { recursive: true })
|
|
121
|
+
await fs.writeFile(path.resolve(conflictDir, "SKILL.md"), "---\nname: custom-gepeto\n---\n\nCustom content.\n")
|
|
122
|
+
|
|
123
|
+
const result = await ManagedSkills.syncManagedSkills(kernel, { publishRoots })
|
|
124
|
+
const gepeto = result.results.find((entry) => entry.id === "gepeto")
|
|
125
|
+
const conflict = gepeto.targets.find((target) => target.path === conflictDir)
|
|
126
|
+
|
|
127
|
+
assert.equal(conflict.status, "conflict")
|
|
128
|
+
assert.match(await fs.readFile(path.resolve(conflictDir, "SKILL.md"), "utf8"), /custom-gepeto/)
|
|
129
|
+
await assert.rejects(
|
|
130
|
+
fs.stat(path.resolve(conflictDir, ManagedSkills.MARKER_FILENAME)),
|
|
131
|
+
{ code: "ENOENT" }
|
|
132
|
+
)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test("same-content legacy folders with extra files are not adopted or removed", async () => {
|
|
137
|
+
await withTempHome(async ({ kernel, publishRoots }) => {
|
|
138
|
+
await ManagedSkills.syncManagedSkills(kernel, { publishRoots })
|
|
139
|
+
await ManagedSkills.setSkillEnabled(kernel, "gepeto", false, { publishRoots })
|
|
140
|
+
|
|
141
|
+
const sourceContent = await fs.readFile(ManagedSkills.skillPath(kernel, "gepeto"), "utf8")
|
|
142
|
+
const legacyDir = path.resolve(publishRoots[0], "gepeto")
|
|
143
|
+
await fs.mkdir(legacyDir, { recursive: true })
|
|
144
|
+
await fs.writeFile(path.resolve(legacyDir, "SKILL.md"), sourceContent)
|
|
145
|
+
await fs.writeFile(path.resolve(legacyDir, "USER_NOTES.md"), "keep this\n")
|
|
146
|
+
|
|
147
|
+
const result = await ManagedSkills.syncManagedSkills(kernel, { publishRoots })
|
|
148
|
+
const gepeto = result.results.find((entry) => entry.id === "gepeto")
|
|
149
|
+
const conflict = gepeto.targets.find((target) => target.path === legacyDir)
|
|
150
|
+
|
|
151
|
+
assert.equal(conflict.status, "conflict")
|
|
152
|
+
assert.match(await fs.readFile(path.resolve(legacyDir, "USER_NOTES.md"), "utf8"), /keep this/)
|
|
153
|
+
await assert.rejects(
|
|
154
|
+
fs.stat(path.resolve(legacyDir, ManagedSkills.MARKER_FILENAME)),
|
|
155
|
+
{ code: "ENOENT" }
|
|
156
|
+
)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test("same-content legacy folders without extra files are adopted", async () => {
|
|
161
|
+
await withTempHome(async ({ kernel, publishRoots }) => {
|
|
162
|
+
await ManagedSkills.syncManagedSkills(kernel, { publishRoots })
|
|
163
|
+
await fs.rm(path.resolve(publishRoots[0], "gepeto"), { recursive: true, force: true })
|
|
164
|
+
|
|
165
|
+
const sourceContent = await fs.readFile(ManagedSkills.skillPath(kernel, "gepeto"), "utf8")
|
|
166
|
+
const legacyDir = path.resolve(publishRoots[0], "gepeto")
|
|
167
|
+
await fs.mkdir(legacyDir, { recursive: true })
|
|
168
|
+
await fs.writeFile(path.resolve(legacyDir, "SKILL.md"), sourceContent)
|
|
169
|
+
|
|
170
|
+
await ManagedSkills.syncManagedSkills(kernel, { publishRoots })
|
|
171
|
+
|
|
172
|
+
const marker = await readJson(path.resolve(legacyDir, ManagedSkills.MARKER_FILENAME))
|
|
173
|
+
assert.equal(marker.manager, "pinokio")
|
|
174
|
+
assert.equal(marker.skillId, "gepeto")
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test("downloaded skill publish-name changes clean up old managed copies", async () => {
|
|
179
|
+
await withTempHome(async ({ kernel, publishRoots }) => {
|
|
180
|
+
kernel.exec = async ({ message }) => {
|
|
181
|
+
const dir = cloneTargetFromMessage(message)
|
|
182
|
+
await fs.mkdir(dir, { recursive: true })
|
|
183
|
+
await fs.writeFile(
|
|
184
|
+
path.resolve(dir, "SKILL.md"),
|
|
185
|
+
"---\nname: Music Generation\ndescription: Make music.\n---\n\nUse the music tool.\n"
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
await ManagedSkills.installSkillFromGit(kernel, {
|
|
189
|
+
ref: "https://example.com/music-generation.git",
|
|
190
|
+
publishRoots
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
await ManagedSkills.setSkillPublishName(kernel, "music-generation", "pinokio-music-alt", { publishRoots })
|
|
194
|
+
|
|
195
|
+
for (const root of publishRoots) {
|
|
196
|
+
await assert.rejects(
|
|
197
|
+
fs.stat(path.resolve(root, "pinokio-music-generation")),
|
|
198
|
+
{ code: "ENOENT" }
|
|
199
|
+
)
|
|
200
|
+
assert.match(
|
|
201
|
+
await fs.readFile(path.resolve(root, "pinokio-music-alt", "SKILL.md"), "utf8"),
|
|
202
|
+
/name: Music Generation/
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test("downloaded valid skills publish with the pinokio prefix", async () => {
|
|
209
|
+
await withTempHome(async ({ kernel, publishRoots }) => {
|
|
210
|
+
kernel.exec = async ({ message }) => {
|
|
211
|
+
const dir = cloneTargetFromMessage(message)
|
|
212
|
+
await fs.mkdir(dir, { recursive: true })
|
|
213
|
+
await fs.writeFile(
|
|
214
|
+
path.resolve(dir, "SKILL.md"),
|
|
215
|
+
"---\nname: Music Generation\ndescription: Make music.\n---\n\nUse the music tool.\n"
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const skill = await ManagedSkills.installSkillFromGit(kernel, {
|
|
220
|
+
ref: "https://example.com/music-generation.git",
|
|
221
|
+
publishRoots
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
assert.equal(skill.id, "music-generation")
|
|
225
|
+
assert.equal(skill.enabled, true)
|
|
226
|
+
assert.equal(skill.publishName, "pinokio-music-generation")
|
|
227
|
+
|
|
228
|
+
for (const root of publishRoots) {
|
|
229
|
+
assert.match(
|
|
230
|
+
await fs.readFile(path.resolve(root, "pinokio-music-generation", "SKILL.md"), "utf8"),
|
|
231
|
+
/name: Music Generation/
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test("downloaded invalid skills stay installed but disabled", async () => {
|
|
238
|
+
await withTempHome(async ({ kernel, publishRoots }) => {
|
|
239
|
+
kernel.exec = async ({ message }) => {
|
|
240
|
+
const dir = cloneTargetFromMessage(message)
|
|
241
|
+
await fs.mkdir(dir, { recursive: true })
|
|
242
|
+
await fs.writeFile(path.resolve(dir, "README.md"), "No root skill document.\n")
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const skill = await ManagedSkills.installSkillFromGit(kernel, {
|
|
246
|
+
ref: "https://example.com/bad-skill.git",
|
|
247
|
+
publishRoots
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
assert.equal(skill.id, "bad-skill")
|
|
251
|
+
assert.equal(skill.enabled, false)
|
|
252
|
+
assert.equal(skill.valid, false)
|
|
253
|
+
assert.deepEqual(skill.errors, ["Missing SKILL.md at the skill root."])
|
|
254
|
+
assert.equal(skill.publishName, "pinokio-bad-skill")
|
|
255
|
+
await fs.stat(path.resolve(kernel.homedir, "skills", "bad-skill"))
|
|
256
|
+
|
|
257
|
+
for (const root of publishRoots) {
|
|
258
|
+
await assert.rejects(
|
|
259
|
+
fs.stat(path.resolve(root, "pinokio-bad-skill")),
|
|
260
|
+
{ code: "ENOENT" }
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
test("clone failure cleanup only removes the temporary clone directory", async () => {
|
|
267
|
+
await withTempHome(async ({ kernel, publishRoots }) => {
|
|
268
|
+
const finalDir = path.resolve(kernel.homedir, "skills", "race-skill")
|
|
269
|
+
kernel.exec = async ({ message }) => {
|
|
270
|
+
const tempDir = cloneTargetFromMessage(message)
|
|
271
|
+
await fs.mkdir(tempDir, { recursive: true })
|
|
272
|
+
await fs.mkdir(finalDir, { recursive: true })
|
|
273
|
+
await fs.writeFile(path.resolve(finalDir, "KEEP.txt"), "do not remove\n")
|
|
274
|
+
throw new Error("clone failed")
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
await assert.rejects(
|
|
278
|
+
ManagedSkills.installSkillFromGit(kernel, {
|
|
279
|
+
ref: "https://example.com/race-skill.git",
|
|
280
|
+
publishRoots
|
|
281
|
+
}),
|
|
282
|
+
/clone failed/
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
assert.equal(await fs.readFile(path.resolve(finalDir, "KEEP.txt"), "utf8"), "do not remove\n")
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test("skill clone uses structured shell arguments for untrusted git refs", async () => {
|
|
290
|
+
await withTempHome(async ({ kernel, publishRoots }) => {
|
|
291
|
+
const maliciousRef = "https://example.com/evil-$(id).git"
|
|
292
|
+
kernel.exec = async ({ message }) => {
|
|
293
|
+
assert.equal(cloneRefFromMessage(message), maliciousRef)
|
|
294
|
+
const dir = cloneTargetFromMessage(message)
|
|
295
|
+
await fs.mkdir(dir, { recursive: true })
|
|
296
|
+
await fs.writeFile(
|
|
297
|
+
path.resolve(dir, "SKILL.md"),
|
|
298
|
+
"---\nname: Suspicious Ref\ndescription: Test.\n---\n\nUse carefully.\n"
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const skill = await ManagedSkills.installSkillFromGit(kernel, {
|
|
303
|
+
ref: maliciousRef,
|
|
304
|
+
publishRoots
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
assert.equal(skill.id, "evil-id")
|
|
308
|
+
assert.equal(skill.enabled, true)
|
|
309
|
+
})
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
test("read-only skill lookup does not publish external copies", async () => {
|
|
313
|
+
await withTempHome(async ({ kernel, publishRoots }) => {
|
|
314
|
+
const skill = await ManagedSkills.getManagedSkill(kernel, "pinokio", { sync: false, publishRoots })
|
|
315
|
+
|
|
316
|
+
assert.equal(skill.id, "pinokio")
|
|
317
|
+
await assert.rejects(
|
|
318
|
+
fs.stat(path.resolve(publishRoots[0], "pinokio")),
|
|
319
|
+
{ code: "ENOENT" }
|
|
320
|
+
)
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
test("no-op sync does not rewrite managed markers", async () => {
|
|
325
|
+
await withTempHome(async ({ kernel, publishRoots }) => {
|
|
326
|
+
await ManagedSkills.syncManagedSkills(kernel, { publishRoots })
|
|
327
|
+
const markerPath = path.resolve(publishRoots[0], "pinokio", ManagedSkills.MARKER_FILENAME)
|
|
328
|
+
const first = await fs.readFile(markerPath, "utf8")
|
|
329
|
+
|
|
330
|
+
await ManagedSkills.syncManagedSkills(kernel, { publishRoots })
|
|
331
|
+
|
|
332
|
+
assert.equal(await fs.readFile(markerPath, "utf8"), first)
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
test("malformed index fails closed without republishing disabled skills", async () => {
|
|
337
|
+
await withTempHome(async ({ kernel, publishRoots }) => {
|
|
338
|
+
await ManagedSkills.syncManagedSkills(kernel, { publishRoots })
|
|
339
|
+
await ManagedSkills.setSkillEnabled(kernel, "gepeto", false, { publishRoots })
|
|
340
|
+
await fs.writeFile(ManagedSkills.indexPath(kernel), "{ invalid json\n")
|
|
341
|
+
|
|
342
|
+
await assert.rejects(
|
|
343
|
+
ManagedSkills.syncManagedSkills(kernel, { publishRoots }),
|
|
344
|
+
/Failed to read managed skills index/
|
|
345
|
+
)
|
|
346
|
+
await assert.rejects(
|
|
347
|
+
fs.stat(path.resolve(publishRoots[0], "gepeto")),
|
|
348
|
+
{ code: "ENOENT" }
|
|
349
|
+
)
|
|
350
|
+
})
|
|
351
|
+
})
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
const assert = require("node:assert/strict")
|
|
2
|
+
const fs = require("node:fs/promises")
|
|
3
|
+
const os = require("node:os")
|
|
4
|
+
const path = require("node:path")
|
|
5
|
+
const test = require("node:test")
|
|
6
|
+
|
|
7
|
+
const Api = require("../kernel/api")
|
|
8
|
+
|
|
9
|
+
function createKernel() {
|
|
10
|
+
return {
|
|
11
|
+
info: { platform: "test" },
|
|
12
|
+
vars: { extra: "value" },
|
|
13
|
+
envs: {},
|
|
14
|
+
memory: {
|
|
15
|
+
global: {},
|
|
16
|
+
local: {},
|
|
17
|
+
key: {},
|
|
18
|
+
rpc: {},
|
|
19
|
+
args: {},
|
|
20
|
+
input: {},
|
|
21
|
+
},
|
|
22
|
+
script: {},
|
|
23
|
+
template: {
|
|
24
|
+
update: () => {},
|
|
25
|
+
render: (value) => value,
|
|
26
|
+
istemplate: () => false,
|
|
27
|
+
flatten: (value) => value,
|
|
28
|
+
},
|
|
29
|
+
port: async () => 42001,
|
|
30
|
+
path: (...parts) => ["/pinokio"].concat(parts).join("/"),
|
|
31
|
+
update_sysinfo: async () => {},
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function withStepApi(fn) {
|
|
36
|
+
const homedir = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-plugin-action-"))
|
|
37
|
+
try {
|
|
38
|
+
const appDir = path.join(homedir, "api", "demo")
|
|
39
|
+
await fs.mkdir(appDir, { recursive: true })
|
|
40
|
+
await fs.writeFile(path.join(homedir, "ENVIRONMENT"), "PINOKIO_TEST_ENV=1\n")
|
|
41
|
+
await fs.writeFile(path.join(appDir, "ENVIRONMENT"), "PINOKIO_APP_TEST_ENV=1\n")
|
|
42
|
+
|
|
43
|
+
const kernel = createKernel()
|
|
44
|
+
kernel.homedir = homedir
|
|
45
|
+
kernel.path = (...parts) => path.join(homedir, ...parts)
|
|
46
|
+
kernel.exists = async (targetPath) => {
|
|
47
|
+
return fs.access(targetPath).then(() => true).catch(() => false)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const api = new Api(kernel)
|
|
51
|
+
api.init = async () => {}
|
|
52
|
+
await fn({ api, appDir })
|
|
53
|
+
} finally {
|
|
54
|
+
await fs.rm(homedir, { recursive: true, force: true })
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
test("resolveActionSteps preserves array actions without changing launcher syntax", async () => {
|
|
59
|
+
const api = new Api(createKernel())
|
|
60
|
+
const request = { id: "array-request", path: "/pinokio/api/demo/start.js" }
|
|
61
|
+
const steps = [{ method: "shell.run", params: { message: "echo ok" } }]
|
|
62
|
+
const script = { run: steps }
|
|
63
|
+
|
|
64
|
+
const resolved = await api.resolveActionSteps({
|
|
65
|
+
request,
|
|
66
|
+
script,
|
|
67
|
+
scriptDir: "/pinokio/api/demo",
|
|
68
|
+
actionKey: "run",
|
|
69
|
+
input: {},
|
|
70
|
+
args: {},
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
assert.strictEqual(resolved, steps)
|
|
74
|
+
assert.deepEqual(api.resolved_actions["array-request"], {
|
|
75
|
+
actionKey: "run",
|
|
76
|
+
steps,
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test("resolveActionSteps executes function actions once per request and clears cached steps", async () => {
|
|
81
|
+
const api = new Api(createKernel())
|
|
82
|
+
const request = { id: "function-request", path: "/pinokio/api/demo/plugin/pinokio.js" }
|
|
83
|
+
let calls = 0
|
|
84
|
+
const script = {
|
|
85
|
+
run: async function (kernel, info, context) {
|
|
86
|
+
calls += 1
|
|
87
|
+
assert.strictEqual(this, script)
|
|
88
|
+
assert.equal(kernel.info.platform, "test")
|
|
89
|
+
assert.equal(info.platform, "test")
|
|
90
|
+
assert.equal(context.extra, "context")
|
|
91
|
+
return [{
|
|
92
|
+
method: "shell.run",
|
|
93
|
+
params: { message: `echo ${context.input.prompt}` },
|
|
94
|
+
}]
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
api.actionContext = async ({ input, args, actionKey }) => ({
|
|
99
|
+
input,
|
|
100
|
+
args,
|
|
101
|
+
action: actionKey,
|
|
102
|
+
extra: "context",
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const first = await api.resolveActionSteps({
|
|
106
|
+
request,
|
|
107
|
+
script,
|
|
108
|
+
scriptDir: "/pinokio/api/demo/plugin",
|
|
109
|
+
actionKey: "run",
|
|
110
|
+
input: { prompt: "first" },
|
|
111
|
+
args: { prompt: "first" },
|
|
112
|
+
})
|
|
113
|
+
const second = await api.resolveActionSteps({
|
|
114
|
+
request,
|
|
115
|
+
script,
|
|
116
|
+
scriptDir: "/pinokio/api/demo/plugin",
|
|
117
|
+
actionKey: "run",
|
|
118
|
+
input: { prompt: "second" },
|
|
119
|
+
args: { prompt: "second" },
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
assert.equal(calls, 1)
|
|
123
|
+
assert.strictEqual(second, first)
|
|
124
|
+
assert.equal(second[0].params.message, "echo first")
|
|
125
|
+
|
|
126
|
+
api.clearResolvedAction(request)
|
|
127
|
+
const third = await api.resolveActionSteps({
|
|
128
|
+
request,
|
|
129
|
+
script,
|
|
130
|
+
scriptDir: "/pinokio/api/demo/plugin",
|
|
131
|
+
actionKey: "run",
|
|
132
|
+
input: { prompt: "third" },
|
|
133
|
+
args: { prompt: "third" },
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
assert.equal(calls, 2)
|
|
137
|
+
assert.equal(third[0].params.message, "echo third")
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test("resolveActionSteps prompts plugin-managed setup before first run", async () => {
|
|
141
|
+
const api = new Api(createKernel())
|
|
142
|
+
const request = { id: "plugin-run-install", path: "/pinokio/plugin/demo/pinokio.js" }
|
|
143
|
+
let runCalls = 0
|
|
144
|
+
let installCalls = 0
|
|
145
|
+
let installedCalls = 0
|
|
146
|
+
const installSteps = [{ method: "shell.run", params: { message: "install" } }]
|
|
147
|
+
const runSteps = [{ method: "shell.run", params: { message: "run" } }]
|
|
148
|
+
const script = {
|
|
149
|
+
installed: async () => {
|
|
150
|
+
installedCalls += 1
|
|
151
|
+
return false
|
|
152
|
+
},
|
|
153
|
+
install: async () => {
|
|
154
|
+
installCalls += 1
|
|
155
|
+
return installSteps
|
|
156
|
+
},
|
|
157
|
+
run: async () => {
|
|
158
|
+
runCalls += 1
|
|
159
|
+
return runSteps
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
api.actionContext = async ({ input, args, actionKey }) => ({
|
|
164
|
+
input,
|
|
165
|
+
args,
|
|
166
|
+
action: actionKey,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const steps = await api.resolveActionSteps({
|
|
170
|
+
request,
|
|
171
|
+
script,
|
|
172
|
+
scriptDir: "/pinokio/plugin/demo",
|
|
173
|
+
actionKey: "run",
|
|
174
|
+
input: { prompt: "first" },
|
|
175
|
+
args: { prompt: "first" },
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
assert.equal(installedCalls, 1)
|
|
179
|
+
assert.equal(installCalls, 0)
|
|
180
|
+
assert.equal(runCalls, 0)
|
|
181
|
+
assert.deepEqual(steps, [{
|
|
182
|
+
method: "notify",
|
|
183
|
+
params: {
|
|
184
|
+
html: "This plugin is not installed. Open the plugin page and click Install.",
|
|
185
|
+
href: "/plugin?path=%2Fplugin%2Fdemo%2Fpinokio.js&next=install",
|
|
186
|
+
target: "_parent",
|
|
187
|
+
type: "warning",
|
|
188
|
+
}
|
|
189
|
+
}])
|
|
190
|
+
|
|
191
|
+
api.clearResolvedAction(request)
|
|
192
|
+
const restarted = await api.resolveActionSteps({
|
|
193
|
+
request,
|
|
194
|
+
script,
|
|
195
|
+
scriptDir: "/pinokio/plugin/demo",
|
|
196
|
+
actionKey: "run",
|
|
197
|
+
input: { prompt: "first" },
|
|
198
|
+
args: { prompt: "first" },
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
assert.equal(installedCalls, 2)
|
|
202
|
+
assert.equal(installCalls, 0)
|
|
203
|
+
assert.equal(runCalls, 0)
|
|
204
|
+
assert.deepEqual(restarted, steps)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test("resolveActionSteps links system plugin setup prompts to plugin detail", async () => {
|
|
208
|
+
const kernel = createKernel()
|
|
209
|
+
kernel.systemPath = (...parts) => ["/pinokio-system"].concat(parts).join("/")
|
|
210
|
+
const api = new Api(kernel)
|
|
211
|
+
const request = { id: "system-plugin-run-install", path: "/pinokio-system/plugin/demo/pinokio.js" }
|
|
212
|
+
const script = {
|
|
213
|
+
title: "Demo System Plugin",
|
|
214
|
+
installed: async () => false,
|
|
215
|
+
install: async () => [{ method: "shell.run", params: { message: "install" } }],
|
|
216
|
+
run: async () => [{ method: "shell.run", params: { message: "run" } }],
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
api.actionContext = async ({ input, args, actionKey }) => ({
|
|
220
|
+
input,
|
|
221
|
+
args,
|
|
222
|
+
action: actionKey,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
const steps = await api.resolveActionSteps({
|
|
226
|
+
request,
|
|
227
|
+
script,
|
|
228
|
+
scriptDir: "/pinokio-system/plugin/demo",
|
|
229
|
+
actionKey: "run",
|
|
230
|
+
input: {},
|
|
231
|
+
args: {},
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
assert.equal(steps[0].method, "notify")
|
|
235
|
+
assert.equal(steps[0].params.html, "Demo System Plugin is not installed. Open the plugin page and click Install.")
|
|
236
|
+
assert.equal(steps[0].params.href, "/plugin?path=%2Fpinokio%2Frun%2Fplugin%2Fdemo%2Fpinokio.js&next=install")
|
|
237
|
+
assert.equal(steps[0].params.target, "_parent")
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test("resolveActionSteps runs plugin action directly when installed check passes", async () => {
|
|
241
|
+
const api = new Api(createKernel())
|
|
242
|
+
const request = { id: "plugin-run-installed", path: "/pinokio/plugin/demo/pinokio.js" }
|
|
243
|
+
let installCalls = 0
|
|
244
|
+
let runCalls = 0
|
|
245
|
+
const runSteps = [{ method: "shell.run", params: { message: "run" } }]
|
|
246
|
+
const script = {
|
|
247
|
+
installed: async () => true,
|
|
248
|
+
install: async () => {
|
|
249
|
+
installCalls += 1
|
|
250
|
+
return [{ method: "shell.run", params: { message: "install" } }]
|
|
251
|
+
},
|
|
252
|
+
run: async () => {
|
|
253
|
+
runCalls += 1
|
|
254
|
+
return runSteps
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
api.actionContext = async ({ input, args, actionKey }) => ({
|
|
259
|
+
input,
|
|
260
|
+
args,
|
|
261
|
+
action: actionKey,
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
const steps = await api.resolveActionSteps({
|
|
265
|
+
request,
|
|
266
|
+
script,
|
|
267
|
+
scriptDir: "/pinokio/plugin/demo",
|
|
268
|
+
actionKey: "run",
|
|
269
|
+
input: {},
|
|
270
|
+
args: {},
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
assert.equal(installCalls, 0)
|
|
274
|
+
assert.equal(runCalls, 1)
|
|
275
|
+
assert.strictEqual(steps, runSteps)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
test("step clears resolved function actions when an RPC returns an error", async () => {
|
|
279
|
+
await withStepApi(async ({ api, appDir }) => {
|
|
280
|
+
const request = {
|
|
281
|
+
id: "function-error",
|
|
282
|
+
path: path.join(appDir, "plugin", "pinokio.js"),
|
|
283
|
+
}
|
|
284
|
+
const steps = [{ method: "test.fail" }]
|
|
285
|
+
api.running[request.id] = true
|
|
286
|
+
api.resolved_actions[request.id] = { actionKey: "run", steps }
|
|
287
|
+
api.resolveScript = async () => ({
|
|
288
|
+
cwd: appDir,
|
|
289
|
+
script: { run: steps }
|
|
290
|
+
})
|
|
291
|
+
api.resolveMethod = async () => ({
|
|
292
|
+
method: async () => {},
|
|
293
|
+
dirname: appDir
|
|
294
|
+
})
|
|
295
|
+
api.run = async () => ({
|
|
296
|
+
error: "boom",
|
|
297
|
+
response: "failed"
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
await api.step(request, steps[0], {}, 0, 1, {})
|
|
301
|
+
|
|
302
|
+
assert.equal(api.resolved_actions[request.id], undefined)
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
test("step clears resolved function actions when an RPC throws", async () => {
|
|
307
|
+
await withStepApi(async ({ api, appDir }) => {
|
|
308
|
+
const request = {
|
|
309
|
+
id: "function-throw",
|
|
310
|
+
path: path.join(appDir, "plugin", "pinokio.js"),
|
|
311
|
+
}
|
|
312
|
+
const steps = [{ method: "test.throw" }]
|
|
313
|
+
api.running[request.id] = true
|
|
314
|
+
api.resolved_actions[request.id] = { actionKey: "run", steps }
|
|
315
|
+
api.resolveScript = async () => ({
|
|
316
|
+
cwd: appDir,
|
|
317
|
+
script: { run: steps }
|
|
318
|
+
})
|
|
319
|
+
api.resolveMethod = async () => ({
|
|
320
|
+
method: async () => {},
|
|
321
|
+
dirname: appDir
|
|
322
|
+
})
|
|
323
|
+
api.run = async () => {
|
|
324
|
+
throw new Error("boom")
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const originalLog = console.log
|
|
328
|
+
console.log = () => {}
|
|
329
|
+
try {
|
|
330
|
+
await api.step(request, steps[0], {}, 0, 1, {})
|
|
331
|
+
} finally {
|
|
332
|
+
console.log = originalLog
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
assert.equal(api.resolved_actions[request.id], undefined)
|
|
336
|
+
})
|
|
337
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const assert = require("assert")
|
|
2
|
+
const fs = require("node:fs")
|
|
3
|
+
const path = require("node:path")
|
|
4
|
+
const test = require("node:test")
|
|
5
|
+
|
|
6
|
+
test("dev iframe checks plugin install state before posting a launch request", () => {
|
|
7
|
+
const view = fs.readFileSync(path.join(__dirname, "..", "server", "views", "d.ejs"), "utf8")
|
|
8
|
+
const launchStart = view.indexOf("const launchTab = async (tab) =>")
|
|
9
|
+
const redirectIndex = view.indexOf("if (await redirectToPluginInstallIfNeeded(href))", launchStart)
|
|
10
|
+
const postMessageIndex = view.indexOf("window.parent.postMessage", launchStart)
|
|
11
|
+
|
|
12
|
+
assert.notStrictEqual(launchStart, -1)
|
|
13
|
+
assert.ok(redirectIndex > launchStart)
|
|
14
|
+
assert.ok(postMessageIndex > redirectIndex)
|
|
15
|
+
assert.ok(view.includes("window.top.location.href = redirectHref"))
|
|
16
|
+
assert.ok(view.includes("/api/plugin/install-state?path="))
|
|
17
|
+
})
|