pinokiod 7.2.12 → 7.2.13

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 (53) hide show
  1. package/Dockerfile +0 -2
  2. package/kernel/api/index.js +14 -1
  3. package/kernel/api/process/index.js +99 -44
  4. package/kernel/api/uri/index.js +51 -0
  5. package/kernel/git.js +0 -13
  6. package/kernel/index.js +4 -0
  7. package/kernel/plugin.js +6 -58
  8. package/kernel/plugin_sources.js +236 -0
  9. package/kernel/util.js +60 -0
  10. package/package.json +1 -1
  11. package/server/features/drafts/public/drafts.js +35 -12
  12. package/server/index.js +110 -142
  13. package/server/lib/content_validation.js +28 -25
  14. package/server/public/common.js +95 -29
  15. package/server/public/create-launcher.js +4 -31
  16. package/server/public/electron.css +1 -1
  17. package/server/public/style.css +337 -0
  18. package/server/public/task-launcher.js +5 -32
  19. package/server/public/universal-launcher.js +3 -26
  20. package/server/views/app.ejs +8 -29
  21. package/server/views/d.ejs +0 -33
  22. package/server/views/editor.ejs +25 -4
  23. package/server/views/shell.ejs +11 -3
  24. package/server/views/terminal.ejs +15 -3
  25. package/spec/INSTRUCTION_SYNC.md +5 -5
  26. package/system/plugin/antigravity/antigravity.png +0 -0
  27. package/system/plugin/antigravity/pinokio.js +37 -0
  28. package/system/plugin/claude/claude.png +0 -0
  29. package/system/plugin/claude/pinokio.js +63 -0
  30. package/system/plugin/claude-auto/claude.png +0 -0
  31. package/system/plugin/claude-auto/pinokio.js +74 -0
  32. package/system/plugin/claude-desktop/icon.jpeg +0 -0
  33. package/system/plugin/claude-desktop/pinokio.js +39 -0
  34. package/system/plugin/codex/openai.webp +0 -0
  35. package/system/plugin/codex/pinokio.js +58 -0
  36. package/system/plugin/codex-auto/openai.webp +0 -0
  37. package/system/plugin/codex-auto/pinokio.js +65 -0
  38. package/system/plugin/codex-desktop/icon.png +0 -0
  39. package/system/plugin/codex-desktop/pinokio.js +39 -0
  40. package/system/plugin/crush/crush.png +0 -0
  41. package/system/plugin/crush/pinokio.js +31 -0
  42. package/system/plugin/cursor/cursor.jpeg +0 -0
  43. package/system/plugin/cursor/pinokio.js +39 -0
  44. package/system/plugin/gemini/gemini.jpeg +0 -0
  45. package/system/plugin/gemini/pinokio.js +40 -0
  46. package/system/plugin/gemini-auto/gemini.jpeg +0 -0
  47. package/system/plugin/gemini-auto/pinokio.js +43 -0
  48. package/system/plugin/qwen/pinokio.js +50 -0
  49. package/system/plugin/qwen/qwen.png +0 -0
  50. package/system/plugin/vscode/pinokio.js +36 -0
  51. package/system/plugin/vscode/vscode.png +0 -0
  52. package/system/plugin/windsurf/pinokio.js +39 -0
  53. package/system/plugin/windsurf/windsurf.png +0 -0
package/Dockerfile CHANGED
@@ -37,8 +37,6 @@ RUN mkdir -p /app/.pinokio-seed \
37
37
  && rm -rf /app/.pinokio-seed/network/system/.git \
38
38
  && rm -rf /app/.pinokio-seed/plugin \
39
39
  && mkdir -p /app/.pinokio-seed/plugin \
40
- && git clone --depth 1 https://github.com/pinokiocomputer/code /app/.pinokio-seed/plugin/code \
41
- && rm -rf /app/.pinokio-seed/plugin/code/.git \
42
40
  && rm -rf /app/.pinokio-seed/prototype/system \
43
41
  && mkdir -p /app/.pinokio-seed/prototype \
44
42
  && git clone --depth 1 https://github.com/pinokiocomputer/proto /app/.pinokio-seed/prototype/system \
@@ -29,6 +29,19 @@ class Api {
29
29
  this.child_procs = {}
30
30
  this.lproxy = new Lproxy()
31
31
  }
32
+ startData(rpc) {
33
+ if (rpc && rpc.method === "process.wait" && rpc.params && typeof rpc.params === "object" && !Array.isArray(rpc.params)) {
34
+ let data = rpc
35
+ if (typeof data.title === "undefined" && typeof rpc.params.title !== "undefined") {
36
+ data = { ...data, title: rpc.params.title }
37
+ }
38
+ if (typeof data.description === "undefined" && typeof rpc.params.description !== "undefined") {
39
+ data = { ...data, description: rpc.params.description }
40
+ }
41
+ return data
42
+ }
43
+ return rpc
44
+ }
32
45
  async launcher_path(name) {
33
46
  let root_path = this.kernel.path("api", name)
34
47
  let primary_path = path.resolve(root_path, "pinokio.js")
@@ -1186,7 +1199,7 @@ class Api {
1186
1199
  this.ondata({
1187
1200
  id: request.id || request.path,
1188
1201
  type: "start",
1189
- data: rpc
1202
+ data: this.startData(rpc)
1190
1203
  })
1191
1204
 
1192
1205
  // DEPRECATED APIS
@@ -34,6 +34,16 @@ class Process {
34
34
  constructor() {
35
35
  this.appApi = new AppAPI()
36
36
  }
37
+ async waitIndefinitely(req, kernel) {
38
+ await new Promise((resolve, reject) => {
39
+ this.resolve = resolve
40
+
41
+ // register the process with the root uri so it can be manually resolved (with this.resolve()) later
42
+ if (kernel && kernel.procs && req && req.parent && req.parent.path) {
43
+ kernel.procs[req.parent.path] = this
44
+ }
45
+ })
46
+ }
37
47
  // async start(req, ondata, kernel) {
38
48
  // /*
39
49
  // req := {
@@ -159,6 +169,8 @@ class Process {
159
169
  /*
160
170
  params := {
161
171
  sec: <SECONDS>,
172
+ title: (optional) Title to display in the footer while waiting,
173
+ description: (optional) Description to display in the footer while waiting,
162
174
  message: (optional) Description to display while waiting,
163
175
  menu: (optional) menu to display in the modal while waiting,
164
176
  // ok: (optional) <ok button text> (if not specified, no ok button),
@@ -169,6 +181,8 @@ class Process {
169
181
 
170
182
  params := {
171
183
  min: <MINUTES>,
184
+ title: (optional) Title to display in the footer while waiting,
185
+ description: (optional) Description to display in the footer while waiting,
172
186
  message: (optional) Description to display while waiting,
173
187
  menu: (optional) menu to display in the modal while waiting,
174
188
  // ok: (optional) <ok button text> (if not specified, no ok button),
@@ -180,6 +194,8 @@ class Process {
180
194
  params := {
181
195
  url: <URL>,
182
196
  interval: (optional) how often to retry checking (in seconds)
197
+ title: (optional) Title to display in the footer while waiting,
198
+ description: (optional) Description to display in the footer while waiting,
183
199
  message: (optional) the message to display while retrying (default: no message)
184
200
  }
185
201
 
@@ -198,9 +214,21 @@ class Process {
198
214
 
199
215
  or
200
216
 
217
+ params := {
218
+ title: (optional) Title to display in the footer while waiting,
219
+ description: (optional) Description to display in the footer while waiting,
220
+ }
221
+
222
+ This waits indefinitely until the script is stopped or the wait is manually
223
+ resolved.
224
+
225
+ or
226
+
201
227
 
202
228
  params := {
203
229
  on: <wait-on condition https://github.com/jeffbski/wait-on>,
230
+ title: (optional) Title to display in the footer while waiting,
231
+ description: (optional) Description to display in the footer while waiting,
204
232
  message: (optional) Description to display while waiting,
205
233
  menu: (optional) menu to display in the modal while waiting,
206
234
  // ok: (optional) <ok button text> (if not specified, no ok button),
@@ -212,57 +240,84 @@ class Process {
212
240
  if 'cancel' is pressed before the condition is met, stops the script
213
241
 
214
242
  */
215
- let ms
216
- if (req.params) {
217
- if (req.params.app || req.params.id || req.params.name) {
218
- await this.appApi.waitForAppPresence(req, ondata, kernel)
219
- return
243
+ let ms
244
+ const waitPath = req && req.parent && req.parent.path
245
+ if (waitPath && kernel) {
246
+ if (!kernel.activeProcessWaits) {
247
+ kernel.activeProcessWaits = {}
220
248
  }
221
- // Display modal
222
- if (req.params.sec || req.params.min) {
223
- // Wait
224
- if (req.params.sec) {
225
- ms = req.params.sec * 1000
226
- } else if (req.params.min) {
227
- ms = req.params.min * 60 * 1000
249
+ kernel.activeProcessWaits[waitPath] = {
250
+ path: waitPath,
251
+ params: req.params || {},
252
+ title: req.params && req.params.title,
253
+ description: req.params && req.params.description,
254
+ message: req.params && req.params.message,
255
+ started: Date.now()
256
+ }
257
+ }
258
+ const showFooter = req.params && (req.params.title || req.params.description)
259
+ if (showFooter && typeof ondata === "function") {
260
+ ondata(req.params, "process.wait.start")
261
+ }
262
+ try {
263
+ if (req.params) {
264
+ if (req.params.app || req.params.id || req.params.name) {
265
+ await this.appApi.waitForAppPresence(req, ondata, kernel)
266
+ return
228
267
  }
229
- await new Promise((resolve, reject) => {
230
- this.resolve = resolve
268
+ // Display modal
269
+ if (req.params.sec || req.params.min) {
270
+ // Wait
271
+ if (req.params.sec) {
272
+ ms = req.params.sec * 1000
273
+ } else if (req.params.min) {
274
+ ms = req.params.min * 60 * 1000
275
+ }
276
+ await new Promise((resolve, reject) => {
277
+ this.resolve = resolve
231
278
 
232
- // register the process with the root uri so it can be manually resolved (with this.resolve()) later
233
- kernel.procs[req.parent.path] = this
279
+ // register the process with the root uri so it can be manually resolved (with this.resolve()) later
280
+ kernel.procs[req.parent.path] = this
234
281
 
235
- setTimeout(() => {
236
- this.resolve()
237
- }, ms)
238
- })
239
- } else if (req.params.uri) {
240
- let interval = req.params.interval ? req.params.interval * 1000 : 1000
241
- ondata(req.params, "loading.start")
242
- await waitForUrl(req.params.uri, req.params.message, interval, ondata)
243
- ondata(req.params, "loading.end")
244
- } else if (req.params.url) {
245
- let interval = req.params.interval ? req.params.interval * 1000 : 1000
246
- ondata(req.params, "loading.start")
247
- await waitForUrl(req.params.url, req.params.message, interval, ondata)
248
- ondata(req.params, "loading.end")
249
- } else if (req.params.on) {
250
- // Wait
251
- if (req.params.message) {
252
- ondata({
253
- raw: `\r\nWaiting: ${JSON.stringify(req.params.on)}\r\n`
282
+ setTimeout(() => {
283
+ this.resolve()
284
+ }, ms)
254
285
  })
255
- ondata(req.params, "wait")
286
+ } else if (req.params.uri) {
287
+ let interval = req.params.interval ? req.params.interval * 1000 : 1000
288
+ ondata(req.params, "loading.start")
289
+ await waitForUrl(req.params.uri, req.params.message, interval, ondata)
290
+ ondata(req.params, "loading.end")
291
+ } else if (req.params.url) {
292
+ let interval = req.params.interval ? req.params.interval * 1000 : 1000
293
+ ondata(req.params, "loading.start")
294
+ await waitForUrl(req.params.url, req.params.message, interval, ondata)
295
+ ondata(req.params, "loading.end")
296
+ } else if (req.params.on) {
297
+ // Wait
298
+ if (req.params.message) {
299
+ ondata({
300
+ raw: `\r\nWaiting: ${JSON.stringify(req.params.on)}\r\n`
301
+ })
302
+ ondata(req.params, "wait")
303
+ }
304
+ console.log("Wait", req.params.on)
305
+ await waitOn(req.params.on)
306
+ console.log("Wait finished")
307
+ ondata(req.params, "wait.end")
308
+ } else {
309
+ await this.waitIndefinitely(req, kernel)
256
310
  }
257
- console.log("Wait", req.params.on)
258
- await waitOn(req.params.on)
259
- console.log("Wait finished")
260
- ondata(req.params, "wait.end")
311
+ } else {
312
+ await this.waitIndefinitely(req, kernel)
313
+ }
314
+ } finally {
315
+ if (waitPath && kernel && kernel.activeProcessWaits) {
316
+ delete kernel.activeProcessWaits[waitPath]
317
+ }
318
+ if (showFooter && typeof ondata === "function") {
319
+ ondata(req.params, "process.wait.end")
261
320
  }
262
- } else {
263
- await new Promise((resolve, reject) => {
264
- this.resolve = resolve
265
- })
266
321
  }
267
322
  }
268
323
  }
@@ -0,0 +1,51 @@
1
+ const Util = require('../../util')
2
+
3
+ const appendQueryParams = (uri, params) => {
4
+ if (!params || typeof params !== 'object' || Array.isArray(params)) {
5
+ return uri
6
+ }
7
+
8
+ const entries = []
9
+ for (const [key, value] of Object.entries(params)) {
10
+ const values = Array.isArray(value) ? value : [value]
11
+ for (const item of values) {
12
+ if (item === undefined || item === null) {
13
+ continue
14
+ }
15
+ const serialized = typeof item === 'object' ? JSON.stringify(item) : String(item)
16
+ entries.push(`${encodeURIComponent(key)}=${encodeURIComponent(serialized)}`)
17
+ }
18
+ }
19
+
20
+ if (entries.length === 0) {
21
+ return uri
22
+ }
23
+
24
+ const hashIndex = uri.indexOf('#')
25
+ const base = hashIndex === -1 ? uri : uri.slice(0, hashIndex)
26
+ const hash = hashIndex === -1 ? '' : uri.slice(hashIndex)
27
+ const separator = base.includes('?')
28
+ ? (base.endsWith('?') || base.endsWith('&') ? '' : '&')
29
+ : '?'
30
+
31
+ return `${base}${separator}${entries.join('&')}${hash}`
32
+ }
33
+
34
+ class URI {
35
+ build(params = {}) {
36
+ const uri = typeof params.uri === 'string' ? params.uri.trim() : ''
37
+ if (!uri) {
38
+ throw new Error('uri.open requires params.uri')
39
+ }
40
+ return appendQueryParams(uri, params.params)
41
+ }
42
+
43
+ async open(req, ondata, kernel) {
44
+ const uri = this.build(req.params)
45
+ ondata({ raw: `\r\nopening uri: ${uri}\r\n` })
46
+ const result = await Util.openURI(uri)
47
+ return { uri, result }
48
+ }
49
+ }
50
+
51
+ module.exports = URI
package/kernel/git.js CHANGED
@@ -1378,19 +1378,6 @@ class Git {
1378
1378
  const absoluteTarget = path.resolve(targetPath)
1379
1379
  const home = path.resolve(this.kernel.homedir)
1380
1380
  const managedTargets = [
1381
- {
1382
- kind: "plugin",
1383
- root: path.resolve(home, "plugin/code"),
1384
- matches: (root, target) => target === root || target.startsWith(`${root}${path.sep}`),
1385
- bootstrap: async () => {
1386
- await fs.promises.rm(path.resolve(home, "plugin/code"), { recursive: true, force: true })
1387
- this.dirs.delete(path.resolve(home, "plugin/code"))
1388
- if (this.kernel.plugin && typeof this.kernel.plugin.init === "function") {
1389
- await this.kernel.plugin.init()
1390
- }
1391
- },
1392
- exists: async () => this.kernel.exists("plugin/code")
1393
- },
1394
1381
  {
1395
1382
  kind: "prototype",
1396
1383
  root: path.resolve(home, "prototype/system"),
package/kernel/index.js CHANGED
@@ -415,6 +415,9 @@ class Kernel {
415
415
  path(...args) {
416
416
  return path.resolve(this.homedir, ...args)
417
417
  }
418
+ systemPath(...args) {
419
+ return path.resolve(__dirname, "..", "system", ...args)
420
+ }
418
421
  exists(...args) {
419
422
  if (args) {
420
423
  let abspath = this.path(...args)
@@ -1060,6 +1063,7 @@ class Kernel {
1060
1063
  args: {},
1061
1064
  }
1062
1065
  this.procs = {}
1066
+ this.activeProcessWaits = {}
1063
1067
  this.template = new Template()
1064
1068
  try {
1065
1069
  if (this.homedir) {
package/kernel/plugin.js CHANGED
@@ -1,39 +1,14 @@
1
- const path = require('path')
2
- const { glob } = require('glob')
3
- const Info = require("./info")
1
+ const fs = require('fs')
2
+ const PluginSources = require("./plugin_sources")
3
+
4
4
  class Plugin {
5
5
  constructor(kernel) {
6
6
  this.kernel = kernel
7
7
  }
8
8
  async setConfig() {
9
- let plugin_dir = path.resolve(this.kernel.homedir, "plugin")
10
9
  this.cache = {}
11
10
 
12
- let plugin_paths = await glob('**/pinokio.js', { cwd: plugin_dir })
13
-
14
- let plugins = []
15
- for(let plugin_path of plugin_paths) {
16
- let config = await this.kernel.require(path.resolve(plugin_dir, plugin_path))
17
- if (config && config.run && Array.isArray(config.run)) {
18
- let invalid
19
- for(let key in config) {
20
- if (typeof config[key] === "function") {
21
- invalid = true
22
- }
23
- }
24
- if (invalid) {
25
- continue
26
- }
27
-
28
- let chunks = plugin_path.split(path.sep)
29
- let cwd = chunks.slice(0, -1).join("/")
30
- config.image = "/asset/plugin/" + cwd + "/" + config.icon
31
- plugins.push({
32
- href: "/run/plugin/" + chunks.join("/"),
33
- ...config
34
- })
35
- }
36
- }
11
+ const plugins = await PluginSources.loadPluginMenu(this.kernel)
37
12
 
38
13
  this.config = {
39
14
  menu: plugins.map((plugin) => {
@@ -48,37 +23,10 @@ class Plugin {
48
23
  if (!exists) {
49
24
  await fs.promises.mkdir(this.kernel.path("plugin"), { recursive: true }).catch((e) => {})
50
25
  }
51
- let code_exists = await this.kernel.exists("plugin/code")
52
- console.log({ code_exists })
53
- if (!code_exists) {
54
- if (this.kernel.bin.installed && this.kernel.bin.installed.conda && this.kernel.bin.installed.conda.has("git")) {
55
- await this.kernel.exec({
56
- //message: "git clone https://github.com/peanutcocktail/plugin",
57
- //message: "git clone https://github.com/pinokiocomputer/plugin",
58
- message: "git clone https://github.com/pinokiocomputer/code",
59
- path: this.kernel.path("plugin")
60
- }, (e) => {
61
- process.stdout.write(e.raw)
62
- })
63
- await this.setConfig()
64
- return
65
- }
66
- } else {
67
- await this.setConfig()
68
- }
26
+ await this.setConfig()
69
27
  }
70
28
  async update() {
71
- if (this.kernel.bin.installed && this.kernel.bin.installed.conda && this.kernel.bin.installed.conda.has("git")) {
72
- let exists = await this.kernel.exists("plugin")
73
- if (!exists) {
74
- await this.kernel.exec({
75
- message: "git pull",
76
- path: this.kernel.path("plugin")
77
- }, (e) => {
78
- process.stdout.write(e.raw)
79
- })
80
- }
81
- }
29
+ await this.setConfig()
82
30
  }
83
31
  }
84
32
  module.exports = Plugin
@@ -0,0 +1,236 @@
1
+ const fs = require("fs")
2
+ const path = require("path")
3
+ const { glob } = require("glob")
4
+
5
+ const LOCAL_RUN_PREFIX = "/run"
6
+ const LOCAL_ASSET_PREFIX = "/asset"
7
+ const SYSTEM_RUN_PREFIX = "/pinokio/run"
8
+ const SYSTEM_ASSET_PREFIX = "/pinokio/asset"
9
+ const LOCAL_PLUGIN_RUN_PREFIX = `${LOCAL_RUN_PREFIX}/plugin`
10
+ const LOCAL_PLUGIN_ASSET_PREFIX = `${LOCAL_ASSET_PREFIX}/plugin`
11
+ const SYSTEM_PLUGIN_RUN_PREFIX = `${SYSTEM_RUN_PREFIX}/plugin`
12
+ const SYSTEM_PLUGIN_ASSET_PREFIX = `${SYSTEM_ASSET_PREFIX}/plugin`
13
+
14
+ const toPathname = (value) => {
15
+ const raw = typeof value === "string" ? value.trim() : ""
16
+ if (!raw) return ""
17
+ try {
18
+ if (/^https?:\/\//i.test(raw)) return new URL(raw).pathname
19
+ if (raw.startsWith("/")) return new URL(`http://localhost${raw}`).pathname
20
+ } catch (_) {
21
+ }
22
+ return raw.split("?")[0].split("#")[0]
23
+ }
24
+
25
+ const normalizeSlashes = (value) => String(value || "").replace(/\\/g, "/").replace(/\/{2,}/g, "/")
26
+
27
+ const systemRoot = (kernel) => {
28
+ if (kernel && typeof kernel.systemPath === "function") {
29
+ return kernel.systemPath()
30
+ }
31
+ return path.resolve(__dirname, "..", "system")
32
+ }
33
+
34
+ const systemPluginRoot = (kernel) => {
35
+ if (kernel && typeof kernel.systemPath === "function") {
36
+ return kernel.systemPath("plugin")
37
+ }
38
+ return path.resolve(__dirname, "..", "system", "plugin")
39
+ }
40
+
41
+ const isSystemRunPath = (value) => toPathname(value).startsWith(`${SYSTEM_RUN_PREFIX}/`)
42
+ const isLocalRunPath = (value) => toPathname(value).startsWith(`${LOCAL_RUN_PREFIX}/`)
43
+ const isRunPath = (value) => isLocalRunPath(value) || isSystemRunPath(value)
44
+ const isSystemPluginPath = (value) => normalizeSlashes(value).startsWith(`${SYSTEM_PLUGIN_RUN_PREFIX}/`)
45
+ const isLegacyPluginCodePath = (value) => {
46
+ const normalized = normalizeSlashes(value).replace(/^\/+/, "")
47
+ return normalized.startsWith("plugin/code/") || normalized.startsWith("code/")
48
+ }
49
+
50
+ const normalizePluginPath = (value) => {
51
+ let normalized = toPathname(value)
52
+ if (!normalized) return ""
53
+ normalized = normalizeSlashes(normalized)
54
+ if (!normalized.startsWith(`${SYSTEM_RUN_PREFIX}/`)) {
55
+ normalized = normalized.replace(/^\/run(?=\/)/, "")
56
+ }
57
+ if (!normalized.startsWith("/")) {
58
+ normalized = `/${normalized}`
59
+ }
60
+ normalized = normalized.replace(/\/+$/, "")
61
+ if (!normalized.endsWith("/pinokio.js")) {
62
+ normalized = `${normalized}/pinokio.js`
63
+ }
64
+ return normalized
65
+ }
66
+
67
+ const pluginSelectionMatches = (src, selectedValue) => {
68
+ const selectedPlugin = normalizeSlashes(typeof selectedValue === "string" ? selectedValue.trim() : "")
69
+ if (!selectedPlugin || !src) return false
70
+ const selectedRunPath = selectedPlugin.startsWith(`${SYSTEM_RUN_PREFIX}/`)
71
+ ? selectedPlugin
72
+ : `${LOCAL_RUN_PREFIX}${selectedPlugin}`
73
+ return src === selectedPlugin || src.startsWith(selectedRunPath)
74
+ }
75
+
76
+ const resolveRunPath = (kernel, value) => {
77
+ const pathname = toPathname(value)
78
+ if (isSystemRunPath(pathname)) {
79
+ const parts = pathname.split("/").filter(Boolean).slice(2)
80
+ return kernel.systemPath(...parts)
81
+ }
82
+ if (isLocalRunPath(pathname)) {
83
+ const parts = pathname.split("/").filter(Boolean).slice(1)
84
+ return kernel.path(...parts)
85
+ }
86
+ return ""
87
+ }
88
+
89
+ const resolvePinokioPath = (kernel, value) => {
90
+ const runPath = resolveRunPath(kernel, value)
91
+ if (runPath) return runPath
92
+ const pathname = toPathname(value)
93
+ return pathname ? kernel.path(pathname.replace(/^\/+/, "")) : ""
94
+ }
95
+
96
+ const systemRelativeFromPluginPath = (normalizedPath) => {
97
+ return normalizeSlashes(normalizedPath).replace(/^\/pinokio\/run\/+/, "")
98
+ }
99
+
100
+ const pluginPathToAbsolute = (kernel, normalizedPath) => {
101
+ if (isSystemPluginPath(normalizedPath)) {
102
+ return kernel.systemPath(systemRelativeFromPluginPath(normalizedPath))
103
+ }
104
+ return kernel.path(normalizeSlashes(normalizedPath).replace(/^\/+/, ""))
105
+ }
106
+
107
+ const pluginRunHrefForPath = (normalizedPath) => {
108
+ if (typeof normalizedPath !== "string" || !normalizedPath) return ""
109
+ if (isSystemPluginPath(normalizedPath)) return normalizedPath
110
+ return `${LOCAL_RUN_PREFIX}${normalizedPath}`
111
+ }
112
+
113
+ const pluginAssetHrefForIcon = (normalizedPath, icon) => {
114
+ const trimmedIcon = typeof icon === "string" ? icon.trim() : ""
115
+ if (!trimmedIcon) return ""
116
+ if (isSystemPluginPath(normalizedPath)) {
117
+ const relativeDir = path.posix.dirname(systemRelativeFromPluginPath(normalizedPath))
118
+ return `${SYSTEM_ASSET_PREFIX}/${relativeDir}/${trimmedIcon}`
119
+ }
120
+ if (normalizeSlashes(normalizedPath).startsWith("/plugin/")) {
121
+ const relativeDir = path.posix.dirname(normalizeSlashes(normalizedPath).slice(1))
122
+ return `${LOCAL_ASSET_PREFIX}/${relativeDir}/${trimmedIcon}`
123
+ }
124
+ return ""
125
+ }
126
+
127
+ const normalizeLauncherTool = (toolValue) => {
128
+ let normalizedTool = typeof toolValue === "string" ? toolValue.trim() : ""
129
+ normalizedTool = normalizedTool.replace(/^https?:\/\/[^/]+/i, "")
130
+ normalizedTool = normalizedTool.replace(/^\/+|\/+$/g, "")
131
+ if (!normalizedTool || normalizedTool.includes("..") || !/^[A-Za-z0-9._/-]+$/.test(normalizedTool)) {
132
+ const error = new Error("Invalid plugin.")
133
+ error.status = 400
134
+ throw error
135
+ }
136
+ if (!normalizedTool.startsWith("pinokio/run/")) {
137
+ normalizedTool = normalizedTool.replace(/^run\//, "")
138
+ }
139
+ if (isLegacyPluginCodePath(normalizedTool)) {
140
+ const error = new Error("The managed plugin/code path is no longer used.")
141
+ error.status = 400
142
+ throw error
143
+ }
144
+ return normalizedTool
145
+ }
146
+
147
+ const resolveLauncherPluginHref = (toolValue) => {
148
+ const normalizedTool = normalizeLauncherTool(toolValue)
149
+ if (normalizedTool.startsWith("pinokio/run/")) {
150
+ const scriptPath = normalizedTool.endsWith(".js")
151
+ ? normalizedTool
152
+ : `${normalizedTool}/pinokio.js`
153
+ return `/${scriptPath}`
154
+ }
155
+ if (normalizedTool.startsWith("plugin/") || normalizedTool.startsWith("api/")) {
156
+ const scriptPath = normalizedTool.endsWith(".js")
157
+ ? normalizedTool
158
+ : `${normalizedTool}/pinokio.js`
159
+ return `${LOCAL_RUN_PREFIX}/${scriptPath}`
160
+ }
161
+ return `${LOCAL_PLUGIN_RUN_PREFIX}/${normalizedTool}/pinokio.js`
162
+ }
163
+
164
+ const loadPluginsFromRoot = async ({ kernel, root, runPrefix, assetPrefix, source, ignore = [] }) => {
165
+ const exists = await fs.promises.stat(root).then((stat) => stat.isDirectory()).catch(() => false)
166
+ if (!exists) return []
167
+
168
+ const pluginPaths = await glob("**/pinokio.js", { cwd: root, ignore })
169
+ const plugins = []
170
+ for (const pluginPath of pluginPaths) {
171
+ const normalizedPluginPath = normalizeSlashes(pluginPath)
172
+ const config = await kernel.require(path.resolve(root, pluginPath))
173
+ if (!config || !Array.isArray(config.run)) continue
174
+ const invalid = Object.keys(config).some((key) => typeof config[key] === "function")
175
+ if (invalid) continue
176
+
177
+ const cwd = normalizedPluginPath.split("/").slice(0, -1).join("/")
178
+ const href = `${runPrefix}/${normalizedPluginPath}`
179
+ const image = config.icon ? `${assetPrefix}/${cwd}/${config.icon}` : config.image
180
+ plugins.push({
181
+ ...config,
182
+ href,
183
+ src: href,
184
+ image,
185
+ source,
186
+ system: source === "system",
187
+ })
188
+ }
189
+ return plugins
190
+ }
191
+
192
+ const loadPluginMenu = async (kernel) => {
193
+ const systemPlugins = await loadPluginsFromRoot({
194
+ kernel,
195
+ root: systemPluginRoot(kernel),
196
+ runPrefix: SYSTEM_PLUGIN_RUN_PREFIX,
197
+ assetPrefix: SYSTEM_PLUGIN_ASSET_PREFIX,
198
+ source: "system",
199
+ })
200
+ const localPlugins = await loadPluginsFromRoot({
201
+ kernel,
202
+ root: path.resolve(kernel.homedir, "plugin"),
203
+ runPrefix: LOCAL_PLUGIN_RUN_PREFIX,
204
+ assetPrefix: LOCAL_PLUGIN_ASSET_PREFIX,
205
+ source: "local",
206
+ ignore: ["code/**"],
207
+ })
208
+ return systemPlugins.concat(localPlugins)
209
+ }
210
+
211
+ module.exports = {
212
+ LOCAL_RUN_PREFIX,
213
+ LOCAL_ASSET_PREFIX,
214
+ SYSTEM_RUN_PREFIX,
215
+ SYSTEM_ASSET_PREFIX,
216
+ LOCAL_PLUGIN_RUN_PREFIX,
217
+ SYSTEM_PLUGIN_RUN_PREFIX,
218
+ SYSTEM_PLUGIN_ASSET_PREFIX,
219
+ systemRoot,
220
+ systemPluginRoot,
221
+ toPathname,
222
+ isSystemRunPath,
223
+ isLocalRunPath,
224
+ isRunPath,
225
+ isSystemPluginPath,
226
+ isLegacyPluginCodePath,
227
+ normalizePluginPath,
228
+ pluginSelectionMatches,
229
+ resolveRunPath,
230
+ resolvePinokioPath,
231
+ pluginPathToAbsolute,
232
+ pluginRunHrefForPath,
233
+ pluginAssetHrefForIcon,
234
+ resolveLauncherPluginHref,
235
+ loadPluginMenu,
236
+ }