pinokiod 8.0.19 → 8.0.20

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.0.19",
3
+ "version": "8.0.20",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -7632,7 +7632,7 @@ class Server {
7632
7632
  const category = classifyPluginMenuItem(pluginItem)
7633
7633
  const title = pluginItem?.title || pluginItem?.text || pluginItem?.name || "Plugin"
7634
7634
  const hasInstalledCheck = PluginSources.isInstalledCheck(pluginItem?.installed)
7635
- const installed = hasInstalledCheck ? await evaluatePluginInstalled(pluginItem) : null
7635
+ const installed = null
7636
7636
  return {
7637
7637
  index,
7638
7638
  title,
@@ -8262,6 +8262,11 @@ class Server {
8262
8262
  res.status(404).send("Plugin not found.")
8263
8263
  return
8264
8264
  }
8265
+ if (plugin.hasInstalledCheck) {
8266
+ const validation = await contentValidation.validatePluginByPath(plugin.pluginPath)
8267
+ const config = validation.valid && validation.context ? validation.context.config : null
8268
+ plugin.installed = config ? await evaluatePluginInstalled(config) : null
8269
+ }
8265
8270
  const [apps, shareState, sidebarContext] = await Promise.all([
8266
8271
  collectPluginApps(),
8267
8272
  buildPluginShareState(plugin),
@@ -3013,7 +3013,15 @@ if (typeof window !== 'undefined' && !window.__pinokioNavigateListenerInstalled)
3013
3013
  return;
3014
3014
  }
3015
3015
  }
3016
- const frame = document.activeElement;
3016
+ let frame = document.activeElement;
3017
+ if (
3018
+ (!frame || frame.tagName !== 'IFRAME') &&
3019
+ new URLSearchParams(window.location.search).get('mode') === 'explore' &&
3020
+ event.origin === 'https://pinokio.co'
3021
+ ) {
3022
+ const exploreFrame = document.querySelector('main > iframe');
3023
+ if (exploreFrame && event.source === exploreFrame.contentWindow) frame = exploreFrame;
3024
+ }
3017
3025
  if (!frame || frame.tagName !== 'IFRAME') {
3018
3026
  try {
3019
3027
  console.warn('[pinokio:navigate] no active iframe');
@@ -3029,6 +3037,23 @@ if (typeof window !== 'undefined' && !window.__pinokioNavigateListenerInstalled)
3029
3037
  } catch (_) {}
3030
3038
  return;
3031
3039
  }
3040
+ const localInstall =
3041
+ target.origin === 'http://localhost:42000' &&
3042
+ (
3043
+ (target.pathname === '/pinokio/download' && Boolean(target.searchParams.get('uri'))) ||
3044
+ (
3045
+ target.pathname === '/checkpoints' &&
3046
+ Boolean(target.searchParams.get('registry')) &&
3047
+ /^(?:sha256:)?[0-9a-f]{64}$/i.test(target.searchParams.get('hash') || '')
3048
+ )
3049
+ );
3050
+ const lanExplore =
3051
+ new URLSearchParams(window.location.search).get('mode') === 'explore' &&
3052
+ !['localhost', '127.0.0.1', '::1', '[::1]'].includes(window.location.hostname) &&
3053
+ event.origin === 'https://pinokio.co' && event.source === frame.contentWindow;
3054
+ if (lanExplore && localInstall) {
3055
+ target = new URL(`${target.pathname}${target.search}${target.hash}`, window.location.origin);
3056
+ }
3032
3057
  if (target.origin !== window.location.origin) {
3033
3058
  try {
3034
3059
  console.warn('[pinokio:navigate] blocked origin', target.origin);
@@ -590,7 +590,17 @@
590
590
  return `${detailUrl}${separator}next=install`
591
591
  }
592
592
  }
593
- redirectToPluginInstallIfNeeded(tool) {
593
+ async redirectToPluginInstallIfNeeded(tool) {
594
+ if (tool && tool.hasInstall === true && tool.hasInstalledCheck === true && typeof tool.installed !== 'boolean') {
595
+ const pluginPath = typeof tool.pluginPath === 'string' ? tool.pluginPath.trim() : ''
596
+ if (pluginPath) {
597
+ try {
598
+ const response = await fetch(`/api/plugin/install-state?path=${encodeURIComponent(pluginPath)}`)
599
+ const state = response.ok ? await response.json() : null
600
+ tool.installed = state && typeof state.installed === 'boolean' ? state.installed : null
601
+ } catch (_) {}
602
+ }
603
+ }
594
604
  const href = this.pluginInstallHref(tool)
595
605
  if (!href) {
596
606
  return false
@@ -992,7 +1002,7 @@
992
1002
  return
993
1003
  }
994
1004
  error.textContent = ''
995
- if (this.redirectToPluginInstallIfNeeded(tool)) {
1005
+ if (await this.redirectToPluginInstallIfNeeded(tool)) {
996
1006
  close()
997
1007
  return
998
1008
  }
@@ -177,9 +177,9 @@ body.dark .explore-url {
177
177
  <%- include('partials/home_server_popover_assets') %>
178
178
  <main>
179
179
  <% if (theme === "dark") { %>
180
- <iframe name="<%=schema%>" src="<%=discover_dark%>" <%=agent === "electron" ? "no-referrer" : ""%> allow="fullscreen *;" allowfullscreen></iframe>
180
+ <iframe name="pinokio-explore" src="<%=discover_dark%>" <%=agent === "electron" ? "no-referrer" : ""%> allow="fullscreen *;" allowfullscreen></iframe>
181
181
  <% } else { %>
182
- <iframe name="<%=schema%>" src="<%=discover_light%>" <%=agent === "electron" ? "no-referrer" : ""%> allow="fullscreen *;" allowfullscreen></iframe>
182
+ <iframe name="pinokio-explore" src="<%=discover_light%>" <%=agent === "electron" ? "no-referrer" : ""%> allow="fullscreen *;" allowfullscreen></iframe>
183
183
  <% } %>
184
184
  <%- include('partials/main_sidebar', { selected: 'explore' }) %>
185
185
  </main>
Binary file
@@ -0,0 +1,57 @@
1
+ module.exports = {
2
+ title: "Opencode",
3
+ icon: "icon.png",
4
+ description: "The AI coding agent built for the terminal.",
5
+ link: "https://opencode.ai/",
6
+ path: "plugin",
7
+ async installed(kernel) {
8
+ return Boolean(kernel.which("opencode"))
9
+ },
10
+ install: [{
11
+ method: "shell.run",
12
+ params: {
13
+ message: "npm install -g opencode-ai@latest"
14
+ }
15
+ }],
16
+ uninstall: [{
17
+ method: "shell.run",
18
+ params: {
19
+ message: "npm uninstall -g opencode-ai"
20
+ }
21
+ }, {
22
+ method: "fs.rm",
23
+ params: {
24
+ path: "."
25
+ }
26
+ }],
27
+ update: [{
28
+ method: "shell.run",
29
+ params: {
30
+ message: "git pull",
31
+ }
32
+ }, {
33
+ method: "shell.run",
34
+ params: {
35
+ message: "npm install -g opencode-ai@latest"
36
+ }
37
+ }],
38
+ run: [{
39
+ when: "{{platform === 'win32'}}",
40
+ method: "shell.run",
41
+ params: {
42
+ shell: "bash",
43
+ message: "opencode",
44
+ path: "{{args.cwd}}",
45
+ input: true
46
+ }
47
+ }, {
48
+ when: "{{platform !== 'win32'}}",
49
+ id: "run",
50
+ method: "shell.run",
51
+ params: {
52
+ message: "opencode",
53
+ path: "{{args.cwd}}",
54
+ input: true
55
+ }
56
+ }]
57
+ }
@@ -0,0 +1,14 @@
1
+ const opencode = require("../opencode/pinokio")
2
+
3
+ module.exports = {
4
+ ...opencode,
5
+ title: "Opencode Auto",
6
+ description: "OpenCode with permission prompts automatically approved unless explicitly denied.",
7
+ run: opencode.run.map((step) => ({
8
+ ...step,
9
+ params: {
10
+ ...step.params,
11
+ message: "opencode --auto",
12
+ },
13
+ })),
14
+ }
@@ -0,0 +1,145 @@
1
+ const assert = require("node:assert/strict")
2
+ const fs = require("node:fs/promises")
3
+ const path = require("node:path")
4
+ const test = require("node:test")
5
+ const { JSDOM, VirtualConsole } = require("jsdom")
6
+
7
+ const commonScriptPath = path.resolve(__dirname, "../server/public/common.js")
8
+ const exploreTemplatePath = path.resolve(__dirname, "../server/views/explore.ejs")
9
+
10
+ async function createHarness(url = "http://192.168.1.50:42000/home?mode=explore") {
11
+ const source = await fs.readFile(commonScriptPath, "utf8")
12
+ const virtualConsole = new VirtualConsole()
13
+ virtualConsole.on("jsdomError", (error) => {
14
+ throw error
15
+ })
16
+ const dom = new JSDOM("<!doctype html><body><main><iframe></iframe></main></body>", {
17
+ runScripts: "dangerously",
18
+ pretendToBeVisual: true,
19
+ url,
20
+ virtualConsole,
21
+ beforeParse(window) {
22
+ window.fetch = async () => ({ ok: true, json: async () => ({}) })
23
+ window.matchMedia = () => ({
24
+ matches: false,
25
+ addEventListener() {},
26
+ removeEventListener() {},
27
+ })
28
+ window.ResizeObserver = class {
29
+ observe() {}
30
+ disconnect() {}
31
+ }
32
+ },
33
+ })
34
+ const script = dom.window.document.createElement("script")
35
+ script.textContent = source
36
+ dom.window.document.body.appendChild(script)
37
+ const frame = dom.window.document.querySelector("iframe")
38
+ frame.src = "https://pinokio.co/?embed=1&theme=dark"
39
+ frame.focus()
40
+ assert.equal(dom.window.document.activeElement, frame)
41
+ return { dom, frame }
42
+ }
43
+
44
+ function navigate(dom, frame, target, options = {}) {
45
+ dom.window.dispatchEvent(new dom.window.MessageEvent("message", {
46
+ data: { e: "pinokio:navigate", url: target },
47
+ origin: options.origin || "https://pinokio.co",
48
+ source: options.source === undefined ? frame.contentWindow : options.source,
49
+ }))
50
+ }
51
+
52
+ test("Explore uses a stable frame identity instead of the launcher schema", async () => {
53
+ const source = await fs.readFile(exploreTemplatePath, "utf8")
54
+
55
+ assert.equal(source.match(/name="pinokio-explore"/g)?.length, 2)
56
+ assert.doesNotMatch(source, /name="<%=schema%>"/)
57
+ })
58
+
59
+ test("LAN Explore rebases registry download installs onto the Home Server origin", async (t) => {
60
+ const { dom, frame } = await createHarness()
61
+ t.after(() => dom.window.close())
62
+
63
+ navigate(dom, frame, "http://localhost:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo&path=api&branch=dev")
64
+
65
+ assert.equal(
66
+ frame.src,
67
+ "http://192.168.1.50:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo&path=api&branch=dev",
68
+ )
69
+ })
70
+
71
+ test("LAN Explore handles registry installs when mobile Safari leaves the parent body focused", async (t) => {
72
+ const { dom, frame } = await createHarness()
73
+ t.after(() => dom.window.close())
74
+ dom.window.document.body.tabIndex = -1
75
+ dom.window.document.body.focus()
76
+ assert.equal(dom.window.document.activeElement, dom.window.document.body)
77
+
78
+ navigate(dom, frame, "http://localhost:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo&path=api")
79
+
80
+ assert.equal(
81
+ frame.src,
82
+ "http://192.168.1.50:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo&path=api",
83
+ )
84
+ })
85
+
86
+ test("LAN Explore rebases valid checkpoint installs onto the Home Server origin", async (t) => {
87
+ const { dom, frame } = await createHarness()
88
+ t.after(() => dom.window.close())
89
+ const hash = `sha256%3A${"a".repeat(64)}`
90
+
91
+ navigate(dom, frame, `http://localhost:42000/checkpoints?registry=https%3A%2F%2Fapi.pinokio.co&hash=${hash}&path=api`)
92
+
93
+ assert.equal(
94
+ frame.src,
95
+ `http://192.168.1.50:42000/checkpoints?registry=https%3A%2F%2Fapi.pinokio.co&hash=${hash}&path=api`,
96
+ )
97
+ })
98
+
99
+ const blockedCases = [
100
+ ["unrelated local route", "http://localhost:42000/tasker?vars=url", {}],
101
+ ["download without a URI", "http://localhost:42000/pinokio/download", {}],
102
+ ["checkpoint with an invalid hash", "http://localhost:42000/checkpoints?registry=https%3A%2F%2Fapi.pinokio.co&hash=abc", {}],
103
+ ["different message origin", "http://localhost:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo", { origin: "https://example.com" }],
104
+ ["different message source", "http://localhost:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo", { source: null }],
105
+ ]
106
+
107
+ for (const [label, target, options] of blockedCases) {
108
+ test(`LAN Explore leaves ${label} blocked`, async (t) => {
109
+ const { dom, frame } = await createHarness()
110
+ t.after(() => dom.window.close())
111
+ const original = frame.src
112
+
113
+ navigate(dom, frame, target, options)
114
+
115
+ assert.equal(frame.src, original)
116
+ })
117
+ }
118
+
119
+ test("the additive branch does not run outside Explore", async (t) => {
120
+ const { dom, frame } = await createHarness("http://192.168.1.50:42000/home")
121
+ t.after(() => dom.window.close())
122
+ const original = frame.src
123
+
124
+ navigate(dom, frame, "http://localhost:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo")
125
+
126
+ assert.equal(frame.src, original)
127
+ })
128
+
129
+ test("localhost keeps the existing same-origin navigation behavior", async (t) => {
130
+ const { dom, frame } = await createHarness("http://localhost:42000/home?mode=explore")
131
+ t.after(() => dom.window.close())
132
+
133
+ navigate(dom, frame, "http://localhost:42000/existing-local-route?value=1")
134
+
135
+ assert.equal(frame.src, "http://localhost:42000/existing-local-route?value=1")
136
+ })
137
+
138
+ test("LAN keeps the existing same-origin navigation behavior", async (t) => {
139
+ const { dom, frame } = await createHarness()
140
+ t.after(() => dom.window.close())
141
+
142
+ navigate(dom, frame, "http://192.168.1.50:42000/existing-lan-route?value=1")
143
+
144
+ assert.equal(frame.src, "http://192.168.1.50:42000/existing-lan-route?value=1")
145
+ })
@@ -0,0 +1,45 @@
1
+ const assert = require("node:assert/strict")
2
+ const test = require("node:test")
3
+
4
+ const opencode = require("../system/plugin/opencode/pinokio")
5
+ const opencodeAuto = require("../system/plugin/opencode-auto/pinokio")
6
+
7
+ test("OpenCode installed status follows executable availability", async () => {
8
+ const lookups = []
9
+ const kernel = {
10
+ which(name) {
11
+ lookups.push(name)
12
+ return null
13
+ },
14
+ }
15
+
16
+ assert.equal(await opencode.installed(kernel, {}), false)
17
+
18
+ kernel.which = (name) => {
19
+ lookups.push(name)
20
+ return "/pinokio/bin/npm/bin/opencode"
21
+ }
22
+
23
+ assert.equal(await opencode.installed(kernel, {}), true)
24
+ assert.deepEqual(lookups, ["opencode", "opencode"])
25
+ })
26
+
27
+ test("OpenCode Auto shares the managed lifecycle and launches in auto mode", () => {
28
+ for (const action of ["install", "uninstall", "update", "installed"]) {
29
+ assert.equal(opencodeAuto[action], opencode[action])
30
+ }
31
+
32
+ assert.equal(opencodeAuto.run.length, opencode.run.length)
33
+ for (let index = 0; index < opencodeAuto.run.length; index += 1) {
34
+ const normalStep = opencode.run[index]
35
+ const autoStep = opencodeAuto.run[index]
36
+
37
+ assert.equal(autoStep.when, normalStep.when)
38
+ assert.equal(autoStep.id, normalStep.id)
39
+ assert.equal(autoStep.method, normalStep.method)
40
+ assert.equal(autoStep.params.path, normalStep.params.path)
41
+ assert.equal(autoStep.params.input, normalStep.params.input)
42
+ assert.equal(autoStep.params.shell, normalStep.params.shell)
43
+ assert.equal(autoStep.params.message, "opencode --auto")
44
+ }
45
+ })
@@ -208,7 +208,7 @@ module.exports = {
208
208
  const systemPlugins = menu.filter((item) => item.source === "system")
209
209
  const localPlugins = menu.filter((item) => item.source === "local")
210
210
 
211
- assert.strictEqual(systemPlugins.length, 10)
211
+ assert.strictEqual(systemPlugins.length, 12)
212
212
  assert.ok(systemPlugins.every((item) => item.system === true))
213
213
  assert.ok(systemPlugins.every((item) => item.href.startsWith("/pinokio/run/plugin/")))
214
214
  assert.ok(systemPlugins.every((item) => item.image.startsWith("/pinokio/asset/plugin/")))