pinokiod 2.15.8 → 2.15.9

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.
@@ -4,7 +4,7 @@ const path = require('path')
4
4
  const _ = require('lodash')
5
5
  const git = require('isomorphic-git')
6
6
  const http = require('isomorphic-git/http/node')
7
- const Lproxy = require('lproxy')
7
+ const Lproxy = require('../lproxy')
8
8
  //const { glob, globSync, globStream, globStreamSync, Glob, } = require('glob')
9
9
  const { glob, sync, hasMagic } = require('glob-gitignore')
10
10
  const fastq = require('fastq')
package/kernel/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const fs = require('fs')
2
- const {JSONPath} = require('jsonpath-plus');
2
+ //const {JSONPath} = require('jsonpath-plus');
3
3
  const os = require("os")
4
4
  const jsdom = require("jsdom");
5
5
  const path = require('path')
@@ -0,0 +1,76 @@
1
+ /*
2
+ const proxy = new Minproxy()
3
+
4
+ // 1. start the proxy for url
5
+ const generated_url = await proxy.start(url)
6
+
7
+ // 2. stop the proxy for url
8
+ proxy.stop(url)
9
+
10
+ // 3. get the proxy url for a given url
11
+ const proxy_url = proxy.url(url)
12
+
13
+ // 4. get all proxies
14
+ const proxies = proxy.all()
15
+
16
+ */
17
+ const httpProxy = require('http-proxy');
18
+ const portfinder = require('portfinder-cp');
19
+ const os = require('os')
20
+ class Lproxy {
21
+ constructor() {
22
+ this.proxies = {}
23
+ this.ip = this.local()
24
+ }
25
+ async start (url, options) {
26
+ let port
27
+ if (options && options.port) {
28
+ port = options.port
29
+ console.log("start proxy at port", port)
30
+ delete options.port
31
+ }
32
+ let o = Object.assign({
33
+ target: url,
34
+ ws: true,
35
+ localAddress: '0.0.0.0',
36
+ }, options)
37
+ console.log("proxy object", o)
38
+ const proxy = httpProxy.createProxyServer(o)
39
+ // get a new port
40
+ if (!port) {
41
+ port = await portfinder.getPortPromise({
42
+ //port: 42420
43
+ port: 44000
44
+ })
45
+ }
46
+ proxy.listen(port)
47
+ proxy.on('close', (res, socket, head) => {
48
+ console.log('Client disconnected');
49
+ });
50
+ proxy.on('error', (e) => {
51
+ console.log('Proxy Error', e)
52
+ });
53
+
54
+ this.proxies[url] = proxy
55
+ return `http://${this.ip}:${port}`
56
+ }
57
+ async stop (url) {
58
+ console.log("proxies", this.proxies, url)
59
+ let p = this.proxies[url]
60
+ if (p) {
61
+ p.close()
62
+ }
63
+ }
64
+ local() {
65
+ const interfaces = Object.values(os.networkInterfaces())
66
+ let addr
67
+ for (let iface of interfaces) {
68
+ for (let alias of iface) {
69
+ if (alias.family === "IPv4" && alias.address !== "127.0.0.1" && !alias.internal) {
70
+ return alias.address
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+ module.exports = Lproxy
package/kernel/shell.js CHANGED
@@ -439,7 +439,13 @@ class Shell {
439
439
  return params.message
440
440
  } else if (Array.isArray(params.message)) {
441
441
  // if params.message is empty, filter out
442
- let delimiter = " && "
442
+ //let delimiter = " && "
443
+ let delimiter
444
+ if (this.platform === "win32") {
445
+ delimiter = " & ";
446
+ } else {
447
+ delimiter = " ; ";
448
+ }
443
449
  return params.message.filter((m) => {
444
450
  return m && !/^\s+$/.test(m)
445
451
  }).join(delimiter)
package/kernel/shells.js CHANGED
@@ -43,11 +43,17 @@ class Shells {
43
43
  params.on = []
44
44
  }
45
45
  // default error
46
- params.on.push({
46
+ const defaultHandlers = [{
47
47
  event: "/error:/i",
48
- debug: true
49
- })
50
- let errors = new Set()
48
+ break: true
49
+ }, {
50
+ event: "/errno /i",
51
+ break: true
52
+ }, {
53
+ event: "/error:.*triton/i",
54
+ break: false
55
+ }]
56
+ params.on = params.on.concat(defaultHandlers)
51
57
  let response = await sh.start(params, async (stream) => {
52
58
  /*
53
59
  {
@@ -61,32 +67,6 @@ class Shells {
61
67
  }
62
68
  }
63
69
  */
64
- // if (params.error && Array.isArray(params.error)) {
65
- // for(let handler of params.error) {
66
- // if (handler.event) {
67
- // let matches = /^\/(.+)\/([dgimsuy]*)$/gs.exec(handler.event)
68
- // if (!/g/.test(matches[2])) {
69
- // matches[2] += "g" // if g option is not included, include it (need it for matchAll)
70
- // }
71
- // let re = new RegExp(matches[1], matches[2])
72
- // let line = stream.cleaned.replaceAll(/[\r\n]/g, "")
73
- // //let rendered_event = [...stream.cleaned.matchAll(re)]
74
- //
75
- // let rendered_event = [...line.matchAll(re)]
76
- // // 3. if the rendered expression is truthy, run the "run" script
77
- // if (rendered_event.length > 0) {
78
- //
79
- // // if error matched, shouldn't move forard
80
- //// error = rendered_event
81
- // console.log({ rendered_event })
82
- // for(let e of rendered_event) {
83
- // errors.add(e[0])
84
- // }
85
- //
86
- // }
87
- // }
88
- // }
89
- // }
90
70
  if (params.on && Array.isArray(params.on)) {
91
71
  for(let handler of params.on) {
92
72
  // regexify
@@ -113,39 +93,104 @@ class Shells {
113
93
  m = rendered_event[0]
114
94
  sh.continue()
115
95
  }
116
- if (handler.debug) {
117
- for(let e of rendered_event) {
118
- errors.add(e[0])
119
- }
120
- }
121
96
  }
122
- // } else if (handler.pattern === "url") {
123
- // let re = /https?:\/\/[a-zA-Z0-9.:]+/g
124
- // let line = stream.cleaned.replaceAll(/[\r\n]/g, "")
125
- // let rendered_event = [...line.matchAll(re)]
126
- // if (rendered_event.length > 0) {
127
- // stream.matches = rendered_event
128
- // if (handler.kill) {
129
- // m = new URL(rendered_event[0])
130
- // console.log("MM", m)
131
- // sh.kill()
132
- // }
133
- // if (handler.done) {
134
- // m = new URL(rendered_event[0])
135
- // console.log("MM", m)
136
- // sh.continue()
137
- // }
138
- // }
139
97
  }
140
98
  }
141
99
  }
142
100
  ondata(stream)
143
101
  })
144
102
 
103
+ /*
104
+ {
105
+ method: "shell.run",
106
+ params: {
107
+ message,
108
+ on: [{
109
+ event: <regex>,
110
+ break: true|false
111
+ }],
112
+ }
113
+ }
114
+
115
+ - true: break with matched
116
+ - false: do not break (default)
117
+ - TEMPLATE: parse the template
118
+ */
119
+
120
+ let errors = new Set()
121
+ if (response) {
122
+ if (params.on && Array.isArray(params.on)) {
123
+
124
+ let line = response.replaceAll(/[\r\n]/g, "")
125
+ // 1. find all break event handlers
126
+ let breakPoints = params.on.filter((x) => {
127
+ return x.event && x.hasOwnProperty("break")
128
+ })
129
+
130
+ // 2. first find the `break: false` handlers and replace the patterns with blank => so they won't be matched in the next step
131
+ //let line = response.replaceAll(/[\r\n]/g, "")
132
+ for(let handler of breakPoints) {
133
+ if (handler.event) {
134
+ let matches = /^\/(.+)\/([dgimsuy]*)$/gs.exec(handler.event)
135
+ if (!/g/.test(matches[2])) {
136
+ matches[2] += "g" // if g option is not included, include it (need it for matchAll)
137
+ }
138
+ let re = new RegExp(matches[1], matches[2])
139
+
140
+ // only the "break: false" ones => these need to be ignored
141
+ if (!handler.break) {
142
+ line = line.replaceAll(re, "")
143
+ }
144
+ }
145
+ }
146
+
147
+ // 3. Now with all the `break: false` (ignored patterns) gone, look for the `break: true` patterns
148
+ for(let handler of breakPoints) {
149
+ if (handler.event) {
150
+ let matches = /^\/(.+)\/([dgimsuy]*)$/gs.exec(handler.event)
151
+ if (!/g/.test(matches[2])) {
152
+ matches[2] += "g" // if g option is not included, include it (need it for matchAll)
153
+ }
154
+ let re = new RegExp(matches[1], matches[2])
155
+
156
+ // only the "break: true" ones
157
+ if (handler.break) {
158
+
159
+ let match;
160
+ // Keep executing the regex on the text until no more matches are found
161
+ while ((match = re.exec(line)) !== null) {
162
+ errors.add(match[0])
163
+ // errors.add(match.slice(1));
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+
145
172
  if (errors.size > 0) {
173
+ // try replacing the shortest pattern from the text one by one and narrow down the errors
174
+ // so it doesn't accidently contain a very long match
175
+
176
+ let errs = Array.from(errors)
177
+ errs.sort((x, y) => {
178
+ return x.length - y.length
179
+ })
180
+ let line = response.replaceAll(/[\r\n]/g, "")
181
+ let shortened_errs = []
182
+ // at this point, errs is sorted in ascending order of length, so the shortest match is the first item
183
+ for(let err of errs) {
184
+ // 1. check the err still matches the line => The first time this is run, it will always be included. But from the next time it might not be
185
+ if (line.includes(err)) {
186
+ // 2. remove the first pattern
187
+ shortened_errs.push(err)
188
+ line = line.replaceAll(err, "")
189
+ }
190
+ }
191
+
146
192
  // don't return if there's an error
147
- console.log("errors", Array.from(errors))
148
- return { id: sh.id, response, stdout: response, event: m, error: Array.from(errors) }
193
+ return { id: sh.id, response, stdout: response, event: m, error: shortened_errs }
149
194
  } else {
150
195
  // need to make a request
151
196
  return { id: sh.id, response, stdout: response, event: m }
package/kernel/util.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const fs = require('fs')
2
2
  const os = require('os')
3
+ const net = require('node:net')
3
4
  const path = require('path')
4
5
  const dotenv = require('dotenv')
5
6
  const child_process = require('node:child_process');
@@ -13,6 +14,31 @@ const h = "[^\\S\\r\\n]"; // simulate `\h`
13
14
  const returnPattern = /\r/g;
14
15
  const returnReplacement = "\\r";
15
16
 
17
+
18
+ const port_running = async (host, port) => {
19
+ const timeout = 1000
20
+ const promise = new Promise((resolve, reject) => {
21
+ const socket = new net.Socket();
22
+ const onError = (e) => {
23
+ socket.destroy();
24
+ reject();
25
+ };
26
+ socket.setTimeout(timeout);
27
+ socket.once('error', onError);
28
+ socket.once('timeout', onError);
29
+ socket.connect(port, host, () => {
30
+ socket.end();
31
+ resolve();
32
+ });
33
+ });
34
+ try {
35
+ await promise;
36
+ return true
37
+ } catch (e) {
38
+ return false
39
+ }
40
+ }
41
+
16
42
  const parse_env = async (filename) => {
17
43
  try {
18
44
  const buf = await fs.promises.readFile(filename)
@@ -142,5 +168,5 @@ const update_env = async (filepath, changes) => {
142
168
  await fs.promises.writeFile(filepath, newval)
143
169
  };
144
170
  module.exports = {
145
- parse_env, api_path, update_env, parse_env_detail, openfs
171
+ parse_env, api_path, update_env, parse_env_detail, openfs, port_running
146
172
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "2.15.8",
3
+ "version": "2.15.9",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -31,16 +31,15 @@
31
31
  "fs-extra": "^11.1.1",
32
32
  "glob": "^10.3.3",
33
33
  "glob-gitignore": "^1.0.14",
34
+ "http-proxy": "^1.18.1",
34
35
  "http-proxy-middleware": "^3.0.0",
35
36
  "http-terminator": "^3.2.0",
36
37
  "isomorphic-git": "^1.23.0",
37
38
  "jimini": "^0.0.6",
38
39
  "jsdom": "^24.0.0",
39
- "jsonpath-plus": "^7.2.0",
40
40
  "key-store": "^1.2.0",
41
41
  "kleur": "^4.1.5",
42
42
  "lodash": "^4.17.21",
43
- "lproxy": "^0.0.13",
44
43
  "marked": "^5.0.1",
45
44
  "mime-types": "^2.1.35",
46
45
  "multer": "^1.4.5-lts.1",
package/server/index.js CHANGED
@@ -71,7 +71,6 @@ class Server {
71
71
  process.env[PATH_KEY]
72
72
  ].join(path.delimiter)
73
73
  }
74
-
75
74
  }
76
75
  stop() {
77
76
  this.server.close()
@@ -257,11 +256,11 @@ class Server {
257
256
 
258
257
  await this.init_env("api/" + name)
259
258
 
259
+
260
260
  let mode = "run"
261
261
  if (req.query && req.query.mode) {
262
262
  mode = req.query.mode
263
263
  }
264
-
265
264
  const env = await this.kernel.env("api/" + name)
266
265
 
267
266
  //res.render("browser", {
@@ -1868,7 +1867,11 @@ class Server {
1868
1867
  } else {
1869
1868
  const primary_port = 80
1870
1869
  const secondary_port = 42000
1871
- const available = await portfinder.isAvailablePromise({ host: "0.0.0.0", port: primary_port })
1870
+ const running1 = await Util.port_running("localhost", primary_port)
1871
+ const running2 = await Util.port_running("127.0.0.1", primary_port)
1872
+ const running = running1 || running2
1873
+ const available = !running
1874
+ //const available = await portfinder.isAvailablePromise({ host: "0.0.0.0", port: primary_port })
1872
1875
  console.log("check available", { primary_port, available })
1873
1876
  if (available) {
1874
1877
  this.port = primary_port
@@ -2105,7 +2108,83 @@ class Server {
2105
2108
  Util.openfs(req.body.path)
2106
2109
  res.json({ success: true })
2107
2110
  }))
2111
+ this.app.get("/proxy", ex(async (req, res) => {
2112
+ let live_proxies = this.kernel.api.proxies["/proxy"]
2113
+ if (!live_proxies) live_proxies = []
2114
+ let proxies = [{
2115
+ icon: "ollama.webp",
2116
+ name: "Ollama",
2117
+ target: 'http://127.0.0.1:11434',
2118
+ port: 42420
2119
+ // }, {
2120
+ // name: "LMStudio",
2121
+ // target: 'http://127.0.0.1:1234',
2122
+ // port: 42421
2123
+ }]
2124
+ for(let i=0; i<proxies.length; i++) {
2125
+ proxies[i].running = false
2126
+ for(let live_proxy of live_proxies) {
2127
+ if (live_proxy.name === proxies[i].name) {
2128
+ proxies[i].running = true
2129
+ proxies[i].proxy = live_proxy.proxy
2130
+ proxies[i].qr = await QRCode.toDataURL(live_proxy.proxy)
2131
+ }
2132
+ }
2133
+ }
2134
+
2135
+ let pinokio_proxy = this.kernel.api.proxies["/"]
2136
+ let pinokio_cloudflare = this.cloudflare_pub
2137
+
2138
+ let qr = null
2139
+ let qr_cloudflare = null
2140
+ let home_proxy = null
2141
+ if (pinokio_proxy && pinokio_proxy.length > 0) {
2142
+ qr = await QRCode.toDataURL(pinokio_proxy[0].proxy)
2143
+ home_proxy = pinokio_proxy[0]
2144
+ }
2108
2145
 
2146
+ let icon
2147
+ if (this.theme === "dark") {
2148
+ icon = "pinokio-white.png"
2149
+ } else {
2150
+ icon = "pinokio-black.png"
2151
+ }
2152
+
2153
+ res.render("proxy", {
2154
+ agent: this.agent,
2155
+ theme: this.theme,
2156
+ items: proxies,
2157
+ qr,
2158
+ proxy: home_proxy,
2159
+ localhost: `http://localhost:${this.port}`,
2160
+ icon
2161
+ })
2162
+ }))
2163
+ this.app.post("/proxy", ex(async (req, res) => {
2164
+ /*
2165
+ req.body := {
2166
+ action: "start"|"stop",
2167
+ name: <name>,
2168
+ target: <target url>,
2169
+ port: <proxy port>
2170
+ }
2171
+ */
2172
+ if (req.body && req.body.target && req.body.action && req.body.name) {
2173
+ //let port = new URL(req.body.target).port
2174
+ //let port = await this.kernel.port()
2175
+ if (req.body.action === "start") {
2176
+ let port = req.body.port
2177
+ console.log("start proxy")
2178
+ await this.kernel.api.startProxy("/proxy", req.body.target, req.body.name, { port })
2179
+ console.log(this.kernel.api.proxies)
2180
+ } else if (req.body.action === "stop") {
2181
+ await this.kernel.api.stopProxy({
2182
+ uri: req.body.target
2183
+ })
2184
+ }
2185
+ }
2186
+ res.json({ success: true })
2187
+ }))
2109
2188
  this.app.post("/unpublish", ex(async (req, res) => {
2110
2189
  /*
2111
2190
  req.body := {
@@ -2667,8 +2746,8 @@ class Server {
2667
2746
  fs.uri("api", "https://github.com/cocktailpeanut/llamacpp.pinokio.git/icon.png")
2668
2747
  fs.uri("bin", "python/bin")
2669
2748
 
2670
- 1. Git URI: http://localhost:4200/pinokio/fs?drive=api&path=https://github.com/cocktailpeanut/llamacpp.pinokio.git/icon.png
2671
- 2. Local path: http://localhost:4200/pinokio/fs?drive=api&path=test/icon.png
2749
+ 1. Git URI: http://localhost/pinokio/fs?drive=api&path=https://github.com/cocktailpeanut/llamacpp.pinokio.git/icon.png
2750
+ 2. Local path: http://localhost/pinokio/fs?drive=api&path=test/icon.png
2672
2751
  */
2673
2752
  this.app.get("/pinokio/fs", ex((req, res) => {
2674
2753
  // serve reaw files
Binary file
@@ -1601,17 +1601,36 @@ footer.status {
1601
1601
  bottom: 0;
1602
1602
  right: 0;
1603
1603
  z-index: 100000000;
1604
+ display: flex;
1605
+ flex-direction: column;
1604
1606
  }
1605
1607
  #error-screen pre {
1606
1608
  padding: 20px;
1607
1609
  box-sizing: border-box;
1608
1610
  width: 100%;
1611
+ /*
1609
1612
  height: 100%;
1613
+ */
1610
1614
  margin: 0;
1611
1615
  background: royalblue;
1612
1616
  color: white;
1613
1617
  overflow: auto;
1618
+ flex-grow: 1;
1619
+ }
1620
+ #error-screen-footer {
1621
+ padding: 10px;
1622
+ box-sizing: border-box;
1623
+ text-align: center;
1624
+ background: royalblue;
1625
+ }
1626
+ #error-screen-close {
1627
+ background: rgba(0, 0, 0, 0.6);
1628
+ font-size: 16px;
1629
+ padding: 10px 20px;
1630
+ font-weight: bold;
1631
+ width: 100%;
1614
1632
  }
1633
+
1615
1634
  mark{
1616
1635
  background: red;
1617
1636
  color: white;
@@ -676,9 +676,6 @@ const subUrlOf = (s, f) => {
676
676
  if (!fullParsed.searchParams.has(key)) {
677
677
  return false
678
678
  }
679
- console.log("key", key)
680
- console.log("f", fullParsed.searchParams.get(key))
681
- console.log("s", val)
682
679
  if (fullParsed.searchParams.get(key) !== val) {
683
680
  return false
684
681
  }
@@ -691,7 +688,6 @@ const renderCursor = () => {
691
688
  el.classList.remove("cursor")
692
689
  })
693
690
  const target = document.querySelector(".selectable[data-index='" + cursorIndex + "']")
694
- console.log({ cursorIndex, target })
695
691
  target.classList.add("cursor")
696
692
  }
697
693
  const nav = (type) => {
@@ -794,7 +790,6 @@ const renderSelection = (e, force) => {
794
790
  //
795
791
  // => select that tab and frame
796
792
 
797
- console.log("target", target)
798
793
  if (target) {
799
794
 
800
795
  // display the menu button at the top
@@ -809,12 +804,10 @@ const renderSelection = (e, force) => {
809
804
  }
810
805
 
811
806
  if (target) {
812
- console.log("# 3")
813
807
 
814
808
 
815
809
  // Instantiate a frame with the selected target's href
816
810
  let url = target.href
817
- console.log("> url", url)
818
811
  document.querySelector("#open-browser").href = url
819
812
  document.querySelector("#clone-tab").setAttribute("data-href", url)
820
813
  document.querySelector("#location").value = url
@@ -1409,6 +1402,11 @@ const refresh = async (silent) => {
1409
1402
  // render the selected frame only if not silent
1410
1403
  if (silent) {
1411
1404
  } else {
1405
+ await new Promise((resolve, reject) => {
1406
+ setTimeout(() => {
1407
+ resolve()
1408
+ }, 1000)
1409
+ })
1412
1410
  renderSelection()
1413
1411
  }
1414
1412
  }
@@ -490,6 +490,11 @@ document.addEventListener("DOMContentLoaded", async () => {
490
490
  separateWordSearch: false,
491
491
  accuracy: "exactly"
492
492
  })
493
+ /*
494
+ document.querySelector("#error-screen").addEventListener("click", (e) => {
495
+ document.querySelector("#error-screen").classList.add("hidden")
496
+ })
497
+ */
493
498
  console.log("instance", instance)
494
499
  console.log("EVENT", packet.event)
495
500
  instance.mark(packet.event)
@@ -882,6 +887,9 @@ const reloadMemory = async () => {
882
887
  <% } %>
883
888
  <div id='error-screen' class='hidden'>
884
889
  <pre></pre>
890
+ <div id='error-screen-footer'>
891
+ <button id='error-screen-close' class='btn'>Close</button>
892
+ </div>
885
893
  </div>
886
894
  </body>
887
895
  </html>
@@ -449,6 +449,11 @@ document.addEventListener("DOMContentLoaded", async () => {
449
449
  separateWordSearch: false,
450
450
  accuracy: "exactly"
451
451
  })
452
+ /*
453
+ document.querySelector("#error-screen").addEventListener("click", (e) => {
454
+ document.querySelector("#error-screen").classList.add("hidden")
455
+ })
456
+ */
452
457
  console.log("instance", instance)
453
458
  console.log("EVENT", packet.event)
454
459
  instance.mark(packet.event)
@@ -500,7 +505,7 @@ document.addEventListener("DOMContentLoaded", async () => {
500
505
  document.querySelector(".run .starting").classList.add("hidden")
501
506
  document.querySelector(".run .stop").classList.add("hidden")
502
507
  }, 0)
503
- this.socket.close()
508
+ //this.socket.close()
504
509
  }
505
510
  }
506
511
 
@@ -820,6 +825,9 @@ const reloadMemory = async () => {
820
825
  <% } %>
821
826
  <div id='error-screen' class='hidden'>
822
827
  <pre></pre>
828
+ <div id='error-screen-footer' class='hidden'>
829
+ <button id='error-screen-close' class='btn'>Close</button>
830
+ </div>
823
831
  </div>
824
832
  </body>
825
833
  </html>
@@ -182,6 +182,7 @@ body.dark .browser-options-row {
182
182
  </form>
183
183
  <% if (ishome) { %>
184
184
  <div class='nav-btns'>
185
+ <a class='btn2' href='/proxy'><div><i class="fa-solid fa-wifi"></i></div><div>Share</div></a>
185
186
  <a class='btn2' id='explore'><div><i class="fa-solid fa-globe"></i></div><div>Discover</div></a>
186
187
  <a class='btn2' href="https://portal.pinokio.computer/" target="_blank"><div><i class="fa-solid fa-face-smile-beam"></i></div><div>Community</div></a>
187
188
  <% if (agent === "electron") { %>
@@ -235,24 +236,32 @@ body.dark .browser-options-row {
235
236
  </div>
236
237
  </div>
237
238
  <% } %>
239
+ <!--
238
240
  <div class='browser-options-row'>
239
- <% if (proxy) { %>
240
- <a id='wifi-stop' data-proxy="<%=proxy.proxy%>" class='btn'><div><i class="fa-solid fa-stop"></i> Stop</div></a>
241
- <div class='flexible'>
242
- <h3><i class="fa-solid fa-wifi"></i> Local Share</h3>
243
- <div>Access Pinokio from any device on the same network. By default it's off.</div>
244
- <span class='badge'><i class="fa-solid fa-circle-notch fa-spin"></i> Running at <a href="<%=proxy.proxy%>" target="_blank" features="browser"><%=proxy.proxy%></a></span>
245
- <a id='wifi-qr' class='badge'><i class="fa-solid fa-qrcode"></i> Scan QR Code</a>
246
- </div>
247
- <% } else { %>
248
- <a id='wifi' class='btn'><div><i class="fa-solid fa-play"></i> Start</div></a>
249
- <a id='wifi-starting' class='btn disabled hidden'><div><i class="fa-solid fa-circle-notch fa-spin"></i> Starting</div></a>
250
- <div class='flexible'>
251
- <h3><i class="fa-solid fa-wifi"></i> Local Share</h3>
252
- <div>Access Pinokio from any device on the same network. By default it's off.</div>
253
- </div>
241
+ <a href="/proxy" class='flexible'>
242
+ <h3><i class="fa-solid fa-wifi"></i> Local Share</h3>
243
+ <div>Access Pinokio, Ollama, and other 3rd party apps from any device on the same network. By default it's off.</div>
244
+ </a>
245
+ <% if (false) { %>
246
+ <% if (proxy) { %>
247
+ <a id='wifi-stop' data-proxy="<%=proxy.proxy%>" class='btn'><div><i class="fa-solid fa-stop"></i> Stop</div></a>
248
+ <div class='flexible'>
249
+ <h3><i class="fa-solid fa-wifi"></i> Local Share</h3>
250
+ <div>Access Pinokio from any device on the same network. By default it's off.</div>
251
+ <span class='badge'><i class="fa-solid fa-circle-notch fa-spin"></i> Running at <a href="<%=proxy.proxy%>" target="_blank" features="browser"><%=proxy.proxy%></a></span>
252
+ <a id='wifi-qr' class='badge'><i class="fa-solid fa-qrcode"></i> Scan QR Code</a>
253
+ </div>
254
+ <% } else { %>
255
+ <a id='wifi' class='btn'><div><i class="fa-solid fa-play"></i> Start</div></a>
256
+ <a id='wifi-starting' class='btn disabled hidden'><div><i class="fa-solid fa-circle-notch fa-spin"></i> Starting</div></a>
257
+ <div class='flexible'>
258
+ <h3><i class="fa-solid fa-wifi"></i> Local Share</h3>
259
+ <div>Access Pinokio from any device on the same network. By default it's off.</div>
260
+ </div>
261
+ <% } %>
254
262
  <% } %>
255
263
  </div>
264
+ -->
256
265
  <div class='browser-options-row hidden'>
257
266
  <% if (cloudflare_pub) { %>
258
267
  <a id='cloudflare-stop' class='btn'><div><i class="fa-solid fa-stop"></i> Stop</div></a>
@@ -0,0 +1,357 @@
1
+ <html>
2
+ <head>
3
+ <title>Pinokio</title>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
6
+ <link href="/xterm.min.css" rel="stylesheet" />
7
+ <link href="/css/fontawesome.min.css" rel="stylesheet">
8
+ <link href="/css/solid.min.css" rel="stylesheet">
9
+ <link href="/css/regular.min.css" rel="stylesheet">
10
+ <link href="/css/brands.min.css" rel="stylesheet">
11
+ <link href="/markdown.css" rel="stylesheet"/>
12
+ <link href="/noty.css" rel="stylesheet"/>
13
+ <link href="/style.css" rel="stylesheet"/>
14
+ <script src="/sweetalert2.js"></script>
15
+ <% if (agent === "electron") { %>
16
+ <link href="/electron.css" rel="stylesheet"/>
17
+ <% } %>
18
+ <style>
19
+ .line2 {
20
+ display: flex;
21
+ align-items: center;
22
+ cursor: pointer;
23
+ background: rgba(0,0,100,0.04);
24
+ }
25
+ .line2 a {
26
+ text-decoration: none;
27
+ color: black;
28
+ }
29
+ .status {
30
+ padding: 10px;
31
+ margin: 10px;
32
+ border-radius: 10px;
33
+ }
34
+ .status.offline {
35
+ background: silver;
36
+ }
37
+ .status.online {
38
+ background: yellowgreen;
39
+ }
40
+ .switch {
41
+ padding: 10px;
42
+ margin: 10px 0;
43
+ }
44
+ .switch[data-online=true] {
45
+ color: yellowgreen;
46
+ }
47
+ .button {
48
+ padding: 10px;
49
+ }
50
+ .on, .off {
51
+ display: flex;
52
+ align-items: center;
53
+ }
54
+ header .runner {
55
+ padding: 10px 0 0;
56
+ }
57
+ .proxy {
58
+ padding: 0 10px;
59
+ text-align: right;
60
+ }
61
+ .proxy a {
62
+ text-decoration: none;
63
+ display: inline-block;
64
+ padding: 5px 10px;
65
+ border-radius: 2px;
66
+ background: black;
67
+ color: white;
68
+ }
69
+ .browser-options-btns {
70
+ display: flex;
71
+ padding-top: 5px;
72
+ }
73
+ .browser-options-row {
74
+ border-top: 1px solid rgba(0,0,0,0.1);
75
+ border-bottom: 1px solid rgba(0,0,0,0.1);
76
+ display: flex;
77
+ padding: 20px 0;
78
+ align-items: flex-start;
79
+ }
80
+ .browser-options-row h3 {
81
+ margin: 0;
82
+ font-size: 14px;
83
+ margin-bottom: 3px;
84
+ }
85
+ .browser-options-row .btn.disabled {
86
+ opacity: 0.7;
87
+ }
88
+ .browser-options-row .btn {
89
+ font-weight: bold;
90
+ width: 100px;
91
+ text-align: center;
92
+ display: block;
93
+ margin-right: 10px;
94
+ flex-shrink: 0;
95
+ padding: 5px;
96
+ }
97
+ .badge {
98
+ font-size: 12px;
99
+ padding: 5px 10px;
100
+ background: rgba(0,0,100,0.1);
101
+ border-radius: 5px;
102
+ display: inline-block;
103
+ margin-top: 5px;
104
+ }
105
+ a.badge {
106
+ color: royalblue;
107
+ text-decoration: none;
108
+ cursor: pointer;
109
+ }
110
+ .badge a {
111
+ color: royalblue;
112
+ text-decoration: none;
113
+ }
114
+ body.dark .btn {
115
+ color: white;
116
+ border: 1px solid rgba(255,255,255,0.3);
117
+ }
118
+ body.dark .badge {
119
+ background: rgba(255,255,255,0.1);
120
+ }
121
+ body.dark .browser-options-row {
122
+ border-top: 1px solid rgba(255,255,255,0.05);
123
+ border-bottom: 1px solid rgba(255,255,255,0.05);
124
+ }
125
+ .description a {
126
+ color: royalblue;
127
+ text-decoration: none;
128
+ }
129
+ .line.align-top h3 {
130
+ align-items: flex-start;
131
+ }
132
+ .line a:hover {
133
+ color: red;
134
+ }
135
+ .btn-container {
136
+ margin-top: 10px;
137
+ }
138
+ .header-label {
139
+ font-size: 30px;
140
+ letter-spacing: -1px;
141
+ padding: 15px;
142
+ text-align: center;
143
+ }
144
+ @media only screen and (max-width: 480px) {
145
+ .btn2 {
146
+ padding: 5px;
147
+ font-size: 11px;
148
+ }
149
+ .nav-btns {
150
+ flex-grow: 1;
151
+ justify-content: center;
152
+ padding: 0;
153
+ }
154
+ }
155
+ </style>
156
+ <body class='<%=theme%>'>
157
+ <header class='navheader grabbable'>
158
+ <h1>
159
+ <a class='home' href="/"><i class="fa-solid fa-house"></i></a>
160
+ <button class='btn2' id='back'><i class="fa-solid fa-chevron-left"></i></button>
161
+ <button class='btn2' id='forward'><i class="fa-solid fa-chevron-right"></i></button>
162
+ <div class='flexible'></div>
163
+ <div class='nav-btns'>
164
+ <a class='btn2' id='explore'><div><i class="fa-solid fa-globe"></i></div><div>Discover</div></a>
165
+ <a class='btn2' href="https://portal.pinokio.computer/" target="_blank"><div><i class="fa-solid fa-face-smile-beam"></i></div><div>Community</div></a>
166
+ <button class='btn2' id='genlog'><div><i class="fa-solid fa-laptop-code"></i></div><div>Debug</div></button>
167
+ <a id='downloadlogs' download class='hidden btn2' href="/pinokio/logs.zip"><div><i class="fa-solid fa-download"></i></div><div>Download logs</div></a>
168
+ <a class='btn2' href="/?mode=settings"><div><i class="fa-solid fa-gear"></i></div><div>Settings</div></a>
169
+ <button id='new-window' title='open a new window' class='btn2'><div><i class="fa-solid fa-plus"></i></div><div>Window</div></button>
170
+ </div>
171
+ </h1>
172
+ </header>
173
+ <main>
174
+ <div class='container'>
175
+ <div class='not-running-apps'>
176
+ <div class='header-label'><i class='fa-solid fa-wifi'></i> Platform Local Sharing</div>
177
+ <ol>
178
+ <li><strong>PLATFORM LOCAL SHARING:</strong> Press "Start" to make the app accessible from any device on the same network</li>
179
+ <li><strong>APP LOCAL SHARING:</strong> This dashboard only lets you locally share the platforms. To do this for apps, look inside each app's Configure tab</li>
180
+ </ol>
181
+ <div class='line align-top'>
182
+ <h3>
183
+ <img class='icon' src="<%=icon%>">
184
+ <div class='col'>
185
+ <div class='title'>Pinokio</div>
186
+ <% if (proxy) { %>
187
+ <div class='description'><a href="<%=proxy.proxy%>" target="_blank"><%=proxy.proxy%></a> → <a href="<%=localhost%>" target="_blank"><%=localhost%></a></div>
188
+ <a class='wifi-qr badge' data-qr="<%=qr%>" data-url="<%=proxy.proxy%>"><i class="fa-solid fa-qrcode"></i> Scan QR Code</a>
189
+ <% } else { %>
190
+ <div class='description'>Share Pinokio <a href="<%=localhost%>" target="_blank"><%=localhost%></a> with other devices on the same network</div>
191
+ <% } %>
192
+ <div class='btn-container'>
193
+ <% if (proxy) { %>
194
+ <div id='wifi-stop' data-proxy="<%=proxy.proxy%>" class='btn'><div><i class="fa-solid fa-stop"></i> Stop</div></div>
195
+ <% } else { %>
196
+ <div id='wifi' class='btn'><div><i class="fa-solid fa-play"></i> Start</div></div>
197
+ <div id='wifi-starting' class='btn disabled hidden'><div><i class="fa-solid fa-circle-notch fa-spin"></i> Starting</div></div>
198
+ <% } %>
199
+ </div>
200
+ </div>
201
+ </h3>
202
+ </div>
203
+ <% items.forEach((item) => { %>
204
+ <div class='line align-top'>
205
+ <h3>
206
+ <img class='icon' src="<%=item.icon%>">
207
+ <div class='col'>
208
+ <div class='title'><%=item.name%></div>
209
+ <% if (item.proxy) { %>
210
+ <div class='description'><a href="<%=item.proxy%>" target="_blank"><%=item.proxy%></a> → <a href="<%=item.target%>" target="_blank"><%=item.target%></a></div>
211
+ <a class='wifi-qr badge' data-qr="<%=item.qr%>" data-url="<%=item.proxy%>"><i class="fa-solid fa-qrcode"></i> Scan QR Code</a>
212
+ <% } else { %>
213
+ <div class='description'>Share <%=item.target%> with other devices on the same network</div>
214
+ <% } %>
215
+ <div class='btn-container'>
216
+ <% if (item.running) { %>
217
+ <div data-port="<%=item.port%>" data-name="<%=item.name%>" data-target="<%=item.target%>" class='stop btn'><i class="fa-solid fa-stop"></i> Stop</div>
218
+ <% } else { %>
219
+ <div data-port="<%=item.port%>" data-name="<%=item.name%>" data-target="<%=item.target%>" class='start btn'><i class="fa-solid fa-play"></i> Start</div>
220
+ <% } %>
221
+ </div>
222
+ </div>
223
+ </h3>
224
+ </div>
225
+ <% }) %>
226
+ </div>
227
+ </div>
228
+ </div>
229
+ </main>
230
+ <script>
231
+ document.querySelector(".container").addEventListener("click", async (e) => {
232
+ let target
233
+ if (e.target.classList.contains("stop")) {
234
+ target = e.target
235
+ } else {
236
+ target = e.target.closest(".stop")
237
+ }
238
+ if (target) {
239
+ // stop
240
+ let res = await fetch("/proxy", {
241
+ method: "post",
242
+ headers: {
243
+ "Content-Type": "application/json"
244
+ },
245
+ body: JSON.stringify({
246
+ action: "stop",
247
+ target: target.getAttribute("data-target"),
248
+ name: target.getAttribute("data-name")
249
+ })
250
+ }).then((res) => {
251
+ return res.json()
252
+ })
253
+ location.href = location.href
254
+ return
255
+ }
256
+
257
+ if (e.target.classList.contains("start")) {
258
+ target = e.target
259
+ } else {
260
+ target = e.target.closest(".start")
261
+ }
262
+ if (target) {
263
+ let res = await fetch("/proxy", {
264
+ method: "post",
265
+ headers: {
266
+ "Content-Type": "application/json"
267
+ },
268
+ body: JSON.stringify({
269
+ action: "start",
270
+ target: target.getAttribute("data-target"),
271
+ name: target.getAttribute("data-name"),
272
+ port: target.getAttribute("data-port")
273
+ })
274
+ }).then((res) => {
275
+ return res.json()
276
+ })
277
+ location.href = location.href
278
+ }
279
+
280
+ if (e.target.classList.contains("wifi-qr")) {
281
+ target = e.target
282
+ } else {
283
+ target = e.target.closest(".wifi-qr")
284
+ }
285
+ if (target) {
286
+ let url = target.getAttribute("data-url")
287
+ let qr = target.getAttribute("data-qr")
288
+ console.log({ url, qr })
289
+ await Swal.fire({
290
+ title: "Local Share",
291
+ customClass: {
292
+ popup: "min-popup",
293
+ title: "min-title"
294
+ },
295
+ showConfirmButton: false,
296
+ html: `<div class='min-modal'>
297
+ <div>Access Pinokio from</div>
298
+ <div>any local network device</div>
299
+ <div><img src="${qr}"></div>
300
+ <a href="${url}" target="_blank">${url}</a>
301
+ </div>`
302
+ })
303
+ }
304
+
305
+ })
306
+ if (document.querySelector("#back")) {
307
+ document.querySelector("#back").addEventListener("click", (e) => {
308
+ history.back()
309
+ })
310
+ }
311
+ if (document.querySelector("#forward")) {
312
+ document.querySelector("#forward").addEventListener("click", (e) => {
313
+ history.forward()
314
+ })
315
+ }
316
+ <% if (proxy) { %>
317
+ if (document.querySelector("#wifi-stop")) {
318
+ document.querySelector("#wifi-stop").addEventListener("click", async (e) => {
319
+ let res = await fetch("/unpublish", {
320
+ method: "post",
321
+ headers: {
322
+ "Content-Type": "application/json"
323
+ },
324
+ body: JSON.stringify({
325
+ type: "local"
326
+ })
327
+ }).then((res) => {
328
+ return res.json()
329
+ })
330
+ console.log(res)
331
+ location.href = location.href
332
+ })
333
+ }
334
+ <% } else { %>
335
+ if (document.querySelector("#wifi")) {
336
+ document.querySelector("#wifi").addEventListener("click", async (e) => {
337
+ document.querySelector("#wifi").classList.add("hidden")
338
+ document.querySelector("#wifi-starting").classList.remove("hidden")
339
+ let res = await fetch("/publish", {
340
+ method: "post",
341
+ headers: {
342
+ "Content-Type": "application/json"
343
+ },
344
+ body: JSON.stringify({
345
+ type: "local"
346
+ })
347
+ }).then((res) => {
348
+ return res.json()
349
+ })
350
+ console.log(res)
351
+ location.href = location.href
352
+ })
353
+ }
354
+ <% } %>
355
+ </script>
356
+ </body>
357
+ </html>