pinokiod 3.311.0 → 3.312.0

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.
@@ -795,6 +795,9 @@ class Api {
795
795
  let port = await this.kernel.port()
796
796
 
797
797
  let { cwd, script } = await this.resolveScript(request.path)
798
+ const actionKey = request.action || 'run'
799
+ const steps = (script && Array.isArray(script[actionKey])) ? script[actionKey] : []
800
+ const totalSteps = steps.length
798
801
 
799
802
  let name = path.relative(this.kernel.path("api"), cwd)
800
803
 
@@ -827,7 +830,7 @@ class Api {
827
830
  ...this.kernel.vars,
828
831
  }
829
832
 
830
- if (i < script.run.length-1) {
833
+ if (i < totalSteps - 1) {
831
834
  memory.next = i+1
832
835
  } else {
833
836
  memory.next = null
@@ -911,7 +914,7 @@ class Api {
911
914
 
912
915
  rpc.current = i
913
916
 
914
- rpc.total = script.run.length
917
+ rpc.total = totalSteps
915
918
 
916
919
  rpc.input = input
917
920
 
@@ -926,7 +929,7 @@ class Api {
926
929
  if (rpc.hasOwnProperty("next")) {
927
930
  console.log("next already exists, don't touch", rpc.next)
928
931
  if (typeof rpc.next === "string") {
929
- let run = script.run
932
+ let run = steps
930
933
  console.log("run", run)
931
934
  for(let i=0; i<run.length; i++) {
932
935
  let step = run[i]
@@ -937,7 +940,7 @@ class Api {
937
940
  }
938
941
  }
939
942
  } else {
940
- if (i < script.run.length-1) {
943
+ if (i < totalSteps-1) {
941
944
  rpc.next = i+1
942
945
  } else {
943
946
  rpc.next = null
@@ -961,7 +964,7 @@ class Api {
961
964
 
962
965
  if (!should_run) {
963
966
  // the current step is skipped, therefore the next value should be ignored as well
964
- if (i < script.run.length-1) {
967
+ if (i < totalSteps-1) {
965
968
  rpc.next = i+1
966
969
  } else {
967
970
  rpc.next = null
@@ -1007,12 +1010,12 @@ class Api {
1007
1010
  })
1008
1011
  }
1009
1012
  }
1010
- return { request, input: null, step: rpc.next, total: script.run.length, args }
1013
+ return { request, input: null, step: rpc.next, total: totalSteps, args }
1011
1014
  } else {
1012
1015
  // still ongoing
1013
- let next_rpc = script.run[rpc.next]
1016
+ let next_rpc = steps[rpc.next]
1014
1017
  if (next_rpc) {
1015
- return { request, rawrpc: next_rpc, input: null, step: rpc.next, total: script.run.length, args }
1018
+ return { request, rawrpc: next_rpc, input: null, step: rpc.next, total: totalSteps, args }
1016
1019
  }
1017
1020
  }
1018
1021
  }
@@ -1213,7 +1216,7 @@ class Api {
1213
1216
  description: "All scripts finished running. Running in daemon mode..."
1214
1217
  }
1215
1218
  })
1216
- return { request, input: result, step: rpc.next, total: script.run.length, args }
1219
+ return { request, input: result, step: rpc.next, total: totalSteps, args }
1217
1220
  } else {
1218
1221
  // no next rpc to execute. Finish
1219
1222
  this.kernel.memory.local[id] = {}
@@ -1237,13 +1240,13 @@ class Api {
1237
1240
  }
1238
1241
  })
1239
1242
  }
1240
- return { request, input: result, step: rpc.next, total: script.run.length, args }
1243
+ return { request, input: result, step: rpc.next, total: totalSteps, args }
1241
1244
  }
1242
1245
 
1243
1246
  } else {
1244
1247
  // still going
1245
- let next_rpc = script.run[rpc.next]
1246
- return { request, rawrpc: next_rpc, input: result, step: rpc.next, total: script.run.length, args }
1248
+ let next_rpc = steps[rpc.next]
1249
+ return { request, rawrpc: next_rpc, input: result, step: rpc.next, total: totalSteps, args }
1247
1250
  }
1248
1251
 
1249
1252
  } catch (e) {
@@ -1491,9 +1494,12 @@ class Api {
1491
1494
  data: "the endpoint does not exist: " + request.uri,
1492
1495
  })
1493
1496
  } else {
1494
- // 3. Check if the resolved endpoint has the "run" attribute and it's an array
1495
- //if (script.run && Array.isArray(script.run)) {
1496
- if (script.run) {
1497
+ const actionKey = request.action || 'run'
1498
+ request.action = actionKey
1499
+ const steps = script ? script[actionKey] : null
1500
+
1501
+ // 3. Check if the resolved endpoint has the requested action attribute and it's an array
1502
+ if (Array.isArray(steps) && steps.length > 0) {
1497
1503
 
1498
1504
  if (request.id) {
1499
1505
  this.running[request.id] = true
@@ -1514,14 +1520,13 @@ class Api {
1514
1520
  }
1515
1521
 
1516
1522
 
1517
-
1518
- this.queue(request, script.run[0], request.input, 0, script.run.length, cwd, request.input)
1523
+ this.queue(request, steps[0], request.input, 0, steps.length, cwd, request.input)
1519
1524
 
1520
1525
  } else {
1521
1526
  this.ondata({
1522
1527
  id: request.id || request.path,
1523
1528
  type: "error",
1524
- data: "missing attribute: run"
1529
+ data: `missing or invalid attribute: ${actionKey}`
1525
1530
  })
1526
1531
  }
1527
1532
  }
package/kernel/index.js CHANGED
@@ -56,7 +56,7 @@ const VARS = {
56
56
 
57
57
  //const memwatch = require('@airbnb/node-memwatch');
58
58
  class Kernel {
59
- schema = "<=4.0.0"
59
+ schema = "<=5.0.0"
60
60
  constructor(store) {
61
61
  this.fetch = fetch
62
62
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "3.311.0",
3
+ "version": "3.312.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -1650,10 +1650,6 @@ class Server {
1650
1650
  }
1651
1651
  let configArray = [{
1652
1652
  key: "home",
1653
- description: [
1654
- "* NO white spaces (' ')",
1655
- "* NO exFAT drives",
1656
- ],
1657
1653
  val: this.kernel.homedir,
1658
1654
  placeholder: "Enter the absolute path to use as your Pinokio home folder (D:\\pinokio, /Users/alice/pinokiofs, etc.)"
1659
1655
  // }, {
@@ -1834,6 +1830,7 @@ class Server {
1834
1830
  schemaPath = ""
1835
1831
  }
1836
1832
 
1833
+ const actionKey = req.action || 'run'
1837
1834
  let runnable
1838
1835
  let resolved
1839
1836
  if (typeof runner === "function") {
@@ -1842,9 +1839,9 @@ class Server {
1842
1839
  } else {
1843
1840
  resolved = runner(this.kernel, this.kernel.info)
1844
1841
  }
1845
- runnable = resolved && resolved.run ? true : false
1842
+ runnable = resolved && Array.isArray(resolved[actionKey]) && resolved[actionKey].length > 0
1846
1843
  } else {
1847
- runnable = runner && runner.run ? true : false
1844
+ runnable = runner && Array.isArray(runner[actionKey]) && runner[actionKey].length > 0
1848
1845
  resolved = runner
1849
1846
  }
1850
1847
 
@@ -1965,6 +1962,7 @@ class Server {
1965
1962
  run: (req.query && req.query.mode === "source" ? false : true),
1966
1963
  stop: (req.query && req.query.stop ? true : false),
1967
1964
  pinokioPath,
1965
+ action: actionKey,
1968
1966
  runnable,
1969
1967
  agent: req.agent,
1970
1968
  rawpath,
@@ -3579,65 +3577,7 @@ class Server {
3579
3577
  }
3580
3578
  logHomeCheck({ step: 'resolved', resolvedHome, ancestor })
3581
3579
 
3582
- const mounts = await system.fsSize().catch(() => [])
3583
- const blockDeviceMounts = []
3584
- const mountTypeLookup = new Map()
3585
- try {
3586
- // fsSize() on some platforms can obscure the actual fs type; pull blockDevices for more accurate mount FS info (e.g. exFAT on macOS/Windows)
3587
- const blockDevices = await system.blockDevices()
3588
- for (const device of blockDevices || []) {
3589
- const mountPath = normalizeMountPath(device.mount)
3590
- if (!mountPath) continue
3591
- const fsType = (device.fsType || device.type || '').toLowerCase()
3592
- blockDeviceMounts.push({ mount: mountPath, type: fsType })
3593
- if (fsType) {
3594
- mountTypeLookup.set(mountPath.toLowerCase(), fsType)
3595
- }
3596
- }
3597
- } catch (_) {
3598
- // ignore - fallback to fsSize data only
3599
- }
3600
- logHomeCheck({ step: 'mountSources', fsSizeCount: mounts.length, blockDevicesCount: blockDeviceMounts.length })
3601
-
3602
- const normalizedAncestor = normalizeMountPath(ancestor)
3603
- const isParentMount = (mountPath) => {
3604
- if (!mountPath || !normalizedAncestor) return false
3605
- if (mountPath === "/") return normalizedAncestor.startsWith("/")
3606
- return normalizedAncestor === mountPath || normalizedAncestor.startsWith(mountPath + "/")
3607
- }
3608
- const isExfat = (fsType) => {
3609
- const normalized = (fsType || '').toLowerCase().replace(/[^a-z0-9]/g, '')
3610
- return normalized.includes('exfat')
3611
- }
3612
- let bestMount = null
3613
- for (const volume of mounts) {
3614
- const mountPath = normalizeMountPath(volume.mount)
3615
- if (!isParentMount(mountPath)) continue
3616
- const blockType = mountTypeLookup.get((mountPath || '').toLowerCase())
3617
- const detectedType = (blockType || volume.type || '').toLowerCase()
3618
- if (!bestMount || mountPath.length > bestMount.mount.length) {
3619
- bestMount = { mount: mountPath, type: detectedType }
3620
- logHomeCheck({ step: 'candidate', source: 'fsSize', mount: mountPath, type: detectedType })
3621
- }
3622
- }
3623
-
3624
- if (!bestMount) {
3625
- for (const deviceMount of blockDeviceMounts) {
3626
- if (!isParentMount(deviceMount.mount)) continue
3627
- if (!bestMount || deviceMount.mount.length > bestMount.mount.length) {
3628
- bestMount = { ...deviceMount }
3629
- logHomeCheck({ step: 'candidate', source: 'blockDevices', mount: deviceMount.mount, type: deviceMount.type })
3630
- }
3631
- }
3632
- }
3633
-
3634
- logHomeCheck({ step: 'bestMount', bestMount })
3635
-
3636
- if (bestMount && bestMount.type && isExfat(bestMount.type)) {
3637
- logHomeCheck({ step: 'reject', reason: 'exfat', bestMount })
3638
- throw new Error("Pinokio home cannot be located on an exFAT drive. Please choose a different location.")
3639
- }
3640
- logHomeCheck({ step: 'accept', bestMount })
3580
+ logHomeCheck({ step: 'accept' })
3641
3581
 
3642
3582
  // // check if the destination already exists => throw error
3643
3583
  // let exists = await fse.pathExists(config.home)
@@ -4559,16 +4499,20 @@ class Server {
4559
4499
  })
4560
4500
  }))
4561
4501
  this.app.get("/agents", ex(async (req, res) => {
4562
- if (!this.kernel.plugin.config) {
4563
- try {
4502
+ let pluginMenu = []
4503
+ try {
4504
+ if (!this.kernel.plugin.config) {
4564
4505
  await this.kernel.plugin.init()
4565
- } catch (err) {
4566
- console.warn('Failed to initialize plugins', err)
4506
+ } else {
4507
+ // Refresh the plugin list so newly downloaded plugins show up immediately
4508
+ await this.kernel.plugin.setConfig()
4567
4509
  }
4510
+ if (this.kernel.plugin && this.kernel.plugin.config && Array.isArray(this.kernel.plugin.config.menu)) {
4511
+ pluginMenu = this.kernel.plugin.config.menu
4512
+ }
4513
+ } catch (err) {
4514
+ console.warn('Failed to initialize plugins', err)
4568
4515
  }
4569
- const pluginMenu = this.kernel.plugin && this.kernel.plugin.config && Array.isArray(this.kernel.plugin.config.menu)
4570
- ? this.kernel.plugin.config.menu
4571
- : []
4572
4516
 
4573
4517
  const apps = []
4574
4518
  try {
@@ -5172,10 +5116,6 @@ class Server {
5172
5116
  }
5173
5117
  let configArray = [{
5174
5118
  key: "home",
5175
- description: [
5176
- "* NO white spaces (' ')",
5177
- "* NO exFAT drives",
5178
- ],
5179
5119
  val: this.kernel.homedir ? this.kernel.homedir : _home,
5180
5120
  placeholder: "Enter the absolute path to use as your Pinokio home folder (D\\pinokio, /Users/alice/pinokiofs, etc.)"
5181
5121
  // }, {
@@ -7676,6 +7616,17 @@ class Server {
7676
7616
  res.status(404).send(e.message)
7677
7617
  }
7678
7618
  }))
7619
+ this.app.get("/action/:action/*", ex(async (req, res) => {
7620
+ const action = typeof req.params.action === 'string' ? req.params.action : ''
7621
+ const pathComponents = req.params[0] ? req.params[0].split("/") : []
7622
+ req.base = this.kernel.homedir
7623
+ req.action = action
7624
+ try {
7625
+ await this.render(req, res, pathComponents)
7626
+ } catch (e) {
7627
+ res.status(404).send(e.message)
7628
+ }
7629
+ }))
7679
7630
  this.app.get("/run/*", ex(async (req, res) => {
7680
7631
  let pathComponents = req.params[0].split("/")
7681
7632
  req.base = this.kernel.homedir
@@ -59,26 +59,30 @@
59
59
  const href = typeof plugin.href === 'string' ? plugin.href.trim() : '';
60
60
  const label = plugin.title || plugin.text || plugin.name || href || '';
61
61
 
62
- let slug = '';
62
+ let value = '';
63
63
  if (href) {
64
- const segments = href.split('/').filter(Boolean);
65
- if (segments.length >= 2) {
66
- slug = segments[segments.length - 2] || '';
64
+ // Normalize href to a plugin-relative path for the backend (e.g., code/codex)
65
+ const normalized = href.replace(/^\/run/, '').replace(/^\/+/, '');
66
+ const parts = normalized.split('/').filter(Boolean);
67
+ // Expect /plugin/<path...>/pinokio.js -> want <path...>
68
+ if (parts[0] === 'plugin' && parts.length >= 3) {
69
+ value = parts.slice(1, -1).join('/');
70
+ } else {
71
+ value = normalized;
67
72
  }
68
- if (!slug && segments.length) {
69
- slug = segments[segments.length - 1] || '';
70
- }
71
- if (slug.endsWith('.js')) {
72
- slug = slug.replace(/\.js$/i, '');
73
+ if (value.endsWith('/pinokio.js')) {
74
+ value = value.replace(/\/pinokio\.js$/i, '');
73
75
  }
74
76
  }
75
- if (!slug && label) {
76
- slug = label
77
+ if (!value && label) {
78
+ value = label
77
79
  .toLowerCase()
78
- .replace(/[^a-z0-9]+/g, '-')
80
+ .replace(/[^a-z0-9/]+/g, '-')
79
81
  .replace(/^-+|-+$/g, '');
80
82
  }
81
- const value = slug || href || (typeof plugin.link === 'string' ? plugin.link.trim() : '');
83
+ if (!value && typeof plugin.link === 'string') {
84
+ value = plugin.link.trim();
85
+ }
82
86
  if (!value) {
83
87
  return null;
84
88
  }
@@ -620,17 +624,6 @@
620
624
  return ui && ui.templateManager ? ui.templateManager.getTemplateValues() : new Map();
621
625
  }
622
626
 
623
- function getSelectedTool(ui) {
624
- if (!ui || !Array.isArray(ui.toolEntries)) {
625
- return '';
626
- }
627
- const checked = ui.toolEntries.find((entry) => entry.input.checked);
628
- if (checked && checked.input && checked.input.value) {
629
- return checked.input.value;
630
- }
631
- return ui.toolEntries.length > 0 ? (ui.toolEntries[0].input.value || '') : '';
632
- }
633
-
634
627
  async function submitFromUi(ui) {
635
628
  if (!ui) return;
636
629
  ui.error.textContent = '';
@@ -641,9 +634,13 @@
641
634
  const targetProject = isAskVariant ? (ui.projectName || folderName) : folderName;
642
635
  const rawPrompt = ui.promptTextarea.value;
643
636
  const templateValues = readTemplateValues(ui);
644
- const selectedTool = getSelectedTool(ui);
637
+ const selectedEntry = ui && Array.isArray(ui.toolEntries)
638
+ ? (ui.toolEntries.find((entry) => entry.input.checked) || ui.toolEntries[0])
639
+ : null
640
+ const selectedTool = selectedEntry && selectedEntry.input ? selectedEntry.input.value : ''
641
+ const selectedHref = selectedEntry && selectedEntry.input ? selectedEntry.input.dataset.agentHref : ''
645
642
 
646
- if (!selectedTool) {
643
+ if (!selectedEntry || !selectedHref) {
647
644
  ui.error.textContent = 'Please select an agent.';
648
645
  return;
649
646
  }
@@ -689,7 +686,8 @@
689
686
 
690
687
  if (isAskVariant) {
691
688
  const params = new URLSearchParams();
692
- params.set('plugin', `/plugin/code/${selectedTool}/pinokio.js`);
689
+ const pluginPath = selectedHref.replace(/^\/run/, '')
690
+ params.set('plugin', pluginPath);
693
691
  if (prompt) {
694
692
  params.set('prompt', prompt);
695
693
  }
@@ -146,7 +146,8 @@ const install = async (name, url, term, socket, options) => {
146
146
  text: `Downloaded to ~/${normalizedPath}/${name}`,
147
147
  timeout: 4000
148
148
  })
149
- location.href = "/agents"
149
+ const relativePluginPath = `${normalizedPath}/${name}`
150
+ location.href = `/agents?path=${encodeURIComponent(relativePluginPath)}`
150
151
  }
151
152
  }
152
153
  }
@@ -102,31 +102,28 @@ body.plugin-page .btn-tab .btn {
102
102
  }
103
103
  .plugin-card {
104
104
  display: flex;
105
- align-items: center;
105
+ flex-direction: column;
106
+ align-items: stretch;
106
107
  gap: 12px;
107
108
  padding: 12px;
108
109
  border-radius: 10px;
109
110
  background: rgba(0, 0, 0, 0.03);
110
- cursor: pointer;
111
- transition: background 0.15s ease, transform 0.15s ease;
111
+ cursor: default;
112
+ transition: background 0.15s ease;
112
113
  text-decoration: none;
113
114
  color: inherit;
114
115
  min-height: 68px;
115
116
  }
116
117
  .plugin-card:hover,
117
- .plugin-card:focus-visible {
118
+ .plugin-card:focus-within {
118
119
  background: rgba(0, 0, 0, 0.08);
119
120
  }
120
- .plugin-card:focus-visible {
121
- outline: 2px solid rgba(127, 91, 243, 0.6);
122
- outline-offset: 2px;
123
- }
124
121
  body.dark .plugin-card {
125
122
  background: rgba(255, 255, 255, 0.05);
126
123
  color: white;
127
124
  }
128
125
  body.dark .plugin-card:hover,
129
- body.dark .plugin-card:focus-visible {
126
+ body.dark .plugin-card:focus-within {
130
127
  background: rgba(255, 255, 255, 0.12);
131
128
  }
132
129
  .plugin-card img,
@@ -156,6 +153,16 @@ body.dark .plugin-card .plugin-icon {
156
153
  display: flex;
157
154
  flex-direction: column;
158
155
  gap: 4px;
156
+ flex: 1;
157
+ }
158
+ .plugin-card .plugin-top {
159
+ display: flex;
160
+ align-items: center;
161
+ gap: 12px;
162
+ }
163
+ .plugin-card .plugin-footer {
164
+ display: flex;
165
+ justify-content: flex-end;
159
166
  }
160
167
  .plugin-card h2 {
161
168
  margin: 0;
@@ -191,6 +198,171 @@ body.dark .plugin-card .plugin-info:focus-visible {
191
198
  background: rgba(255, 255, 255, 0.18);
192
199
  color: rgba(255, 255, 255, 0.95);
193
200
  }
201
+ .plugin-card .plugin-actions {
202
+ margin-left: auto;
203
+ display: flex;
204
+ flex-wrap: wrap;
205
+ gap: 8px;
206
+ justify-content: flex-end;
207
+ }
208
+ .plugin-card .plugin-action-button {
209
+ padding: 8px 12px;
210
+ border-radius: 8px;
211
+ border: 1px solid rgba(0, 0, 0, 0.08);
212
+ background: white;
213
+ color: rgba(0, 0, 0, 0.8);
214
+ cursor: pointer;
215
+ font-weight: 600;
216
+ transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
217
+ }
218
+ body.dark .plugin-card .plugin-action-button {
219
+ background: rgba(255, 255, 255, 0.08);
220
+ border-color: rgba(255, 255, 255, 0.16);
221
+ color: white;
222
+ }
223
+ .plugin-card .plugin-action-button:hover,
224
+ .plugin-card .plugin-action-button:focus-visible {
225
+ background: rgba(127, 91, 243, 0.12);
226
+ border-color: rgba(127, 91, 243, 0.45);
227
+ color: rgba(0, 0, 0, 0.9);
228
+ outline: none;
229
+ }
230
+ body.dark .plugin-card .plugin-action-button:hover,
231
+ body.dark .plugin-card .plugin-action-button:focus-visible {
232
+ color: white;
233
+ background: rgba(127, 91, 243, 0.25);
234
+ border-color: rgba(127, 91, 243, 0.65);
235
+ }
236
+ .plugin-card .plugin-action-button:active {
237
+ transform: translateY(1px);
238
+ }
239
+ :root {
240
+ --pinokio-modal-bg: #ffffff;
241
+ --pinokio-modal-text: #0f172a;
242
+ --pinokio-modal-icon-bg: linear-gradient(135deg, rgba(59, 130, 246, 0.18), rgba(59, 130, 246, 0.03));
243
+ --pinokio-modal-icon-color: #2563eb;
244
+ --pinokio-modal-subtitle: rgba(71, 85, 105, 0.75);
245
+ --pinokio-modal-body-bg: rgba(241, 245, 249, 0.9);
246
+ }
247
+ body.dark {
248
+ --pinokio-modal-bg: #0f172a;
249
+ --pinokio-modal-text: #e2e8f0;
250
+ --pinokio-modal-icon-bg: linear-gradient(135deg, rgba(59, 130, 246, 0.25), rgba(59, 130, 246, 0.05));
251
+ --pinokio-modal-icon-color: #60a5fa;
252
+ --pinokio-modal-subtitle: rgba(148, 163, 184, 0.85);
253
+ --pinokio-modal-body-bg: rgba(15, 23, 42, 0.6);
254
+ }
255
+ .pinokio-modern-modal.swal2-popup {
256
+ padding: 0 !important;
257
+ border-radius: 20px !important;
258
+ background: var(--pinokio-modal-bg) !important;
259
+ box-shadow: 0 32px 80px rgba(15, 23, 42, 0.25);
260
+ }
261
+ body.dark .pinokio-modern-modal.swal2-popup {
262
+ box-shadow: 0 40px 120px rgba(2, 8, 23, 0.7);
263
+ }
264
+ .pinokio-modern-html {
265
+ padding: 0 !important;
266
+ }
267
+ .pinokio-modern-close {
268
+ color: rgba(71, 85, 105, 0.65) !important;
269
+ top: 12px !important;
270
+ right: 12px !important;
271
+ }
272
+ body.dark .pinokio-modern-close {
273
+ color: rgba(226, 232, 240, 0.6) !important;
274
+ }
275
+ .pinokio-modern-close:hover {
276
+ background: rgba(148, 163, 184, 0.18) !important;
277
+ color: #0f172a !important;
278
+ }
279
+ body.dark .pinokio-modern-close:hover {
280
+ background: rgba(148, 163, 184, 0.12) !important;
281
+ color: #f8fafc !important;
282
+ }
283
+ .pinokio-modal-surface {
284
+ background: var(--pinokio-modal-bg);
285
+ color: var(--pinokio-modal-text);
286
+ border-radius: 20px;
287
+ overflow: hidden;
288
+ }
289
+ .pinokio-modal-header {
290
+ display: flex;
291
+ align-items: center;
292
+ gap: 12px;
293
+ padding: 22px 24px 10px;
294
+ }
295
+ .pinokio-modal-icon {
296
+ width: 48px;
297
+ height: 48px;
298
+ border-radius: 14px;
299
+ background: var(--pinokio-modal-icon-bg);
300
+ color: var(--pinokio-modal-icon-color);
301
+ display: grid;
302
+ place-items: center;
303
+ font-size: 22px;
304
+ }
305
+ .pinokio-modal-heading {
306
+ display: flex;
307
+ flex-direction: column;
308
+ gap: 4px;
309
+ }
310
+ .pinokio-modal-title {
311
+ font-size: 20px;
312
+ font-weight: 700;
313
+ color: var(--pinokio-modal-text);
314
+ }
315
+ .pinokio-modal-subtitle {
316
+ font-size: 13px;
317
+ color: var(--pinokio-modal-subtitle);
318
+ }
319
+ .pinokio-modal-body {
320
+ padding: 14px 24px 12px;
321
+ background: var(--pinokio-modal-body-bg);
322
+ }
323
+ .pinokio-modal-body--iframe {
324
+ padding: 14px 24px 12px;
325
+ }
326
+ .pinokio-modal-body--iframe iframe {
327
+ width: 100%;
328
+ height: 440px;
329
+ border: none;
330
+ border-radius: 12px;
331
+ background: inherit;
332
+ }
333
+ .pinokio-modal-footer {
334
+ display: flex;
335
+ justify-content: flex-end;
336
+ gap: 10px;
337
+ padding: 0 24px 18px;
338
+ background: var(--pinokio-modal-bg);
339
+ }
340
+ .pinokio-modal-footer--publish {
341
+ align-items: center;
342
+ }
343
+ .pinokio-publish-close-btn,
344
+ .pinokio-modal-secondary-btn {
345
+ border: none;
346
+ border-radius: 10px;
347
+ padding: 9px 14px;
348
+ font-weight: 600;
349
+ cursor: pointer;
350
+ background: rgba(0, 0, 0, 0.08);
351
+ color: var(--pinokio-modal-text);
352
+ }
353
+ body.dark .pinokio-publish-close-btn,
354
+ body.dark .pinokio-modal-secondary-btn {
355
+ background: rgba(255, 255, 255, 0.12);
356
+ color: var(--pinokio-modal-text);
357
+ }
358
+ .pinokio-publish-close-btn:hover,
359
+ .pinokio-modal-secondary-btn:hover {
360
+ background: rgba(127, 91, 243, 0.18);
361
+ }
362
+ body.dark .pinokio-publish-close-btn:hover,
363
+ body.dark .pinokio-modal-secondary-btn:hover {
364
+ background: rgba(127, 91, 243, 0.25);
365
+ }
194
366
  .plugin-card .disclosure-indicator {
195
367
  display: flex;
196
368
  align-items: center;
@@ -201,7 +373,7 @@ body.dark .plugin-card .plugin-info:focus-visible {
201
373
  transition: transform 0.15s ease, color 0.15s ease;
202
374
  }
203
375
  .plugin-card:hover .disclosure-indicator,
204
- .plugin-card:focus-visible .disclosure-indicator {
376
+ .plugin-card:focus-within .disclosure-indicator {
205
377
  transform: translateX(2px);
206
378
  color: rgba(0, 0, 0, 0.65);
207
379
  }
@@ -209,7 +381,7 @@ body.dark .plugin-card .disclosure-indicator {
209
381
  color: rgba(255, 255, 255, 0.4);
210
382
  }
211
383
  body.dark .plugin-card:hover .disclosure-indicator,
212
- body.dark .plugin-card:focus-visible .disclosure-indicator {
384
+ body.dark .plugin-card:focus-within .disclosure-indicator {
213
385
  color: rgba(255, 255, 255, 0.85);
214
386
  }
215
387
  .plugin-empty {
@@ -403,7 +575,8 @@ body.dark .plugin-option:hover {
403
575
  image: plugin.image || null,
404
576
  icon: plugin.icon || null,
405
577
  pluginPath,
406
- extraParams
578
+ extraParams,
579
+ hasInstall: Array.isArray(plugin.install)
407
580
  }
408
581
  }) %>
409
582
  </head>
@@ -471,19 +644,35 @@ body.dark .plugin-option:hover {
471
644
  <% if (items.length) { %>
472
645
  <div class='plugin-grid'>
473
646
  <% items.forEach(({ pluginItem, index }) => { %>
474
- <div class='plugin-card' role="button" tabindex="0" data-plugin-index="<%=index%>">
475
- <% if (pluginItem.image) { %>
476
- <img src="<%=pluginItem.image%>" alt="<%=pluginItem.title || pluginItem.text || 'Plugin'%> icon">
477
- <% } else if (pluginItem.icon) { %>
478
- <div class='plugin-icon'><i class="<%=pluginItem.icon%>"></i></div>
479
- <% } else { %>
480
- <div class='plugin-icon'><i class="fa-solid fa-robot"></i></div>
481
- <% } %>
482
- <div class='plugin-details'>
483
- <h2><%=pluginItem.title || pluginItem.text || pluginItem.name || 'Plugin'%></h2>
484
- <% if (pluginItem.description) { %>
485
- <div class='subtitle'><%=pluginItem.description%></div>
647
+ <div class='plugin-card' data-plugin-index="<%=index%>">
648
+ <div class="plugin-top">
649
+ <% if (pluginItem.image) { %>
650
+ <img src="<%=pluginItem.image%>" alt="<%=pluginItem.title || pluginItem.text || 'Plugin'%> icon">
651
+ <% } else if (pluginItem.icon) { %>
652
+ <div class='plugin-icon'><i class="<%=pluginItem.icon%>"></i></div>
653
+ <% } else { %>
654
+ <div class='plugin-icon'><i class="fa-solid fa-robot"></i></div>
486
655
  <% } %>
656
+ <div class='plugin-details'>
657
+ <h2><%=pluginItem.title || pluginItem.text || pluginItem.name || 'Plugin'%></h2>
658
+ <% if (pluginItem.description) { %>
659
+ <div class='subtitle'><%=pluginItem.description%></div>
660
+ <% } %>
661
+ </div>
662
+ </div>
663
+ <div class="plugin-footer">
664
+ <div class="plugin-actions">
665
+ <button type="button" class="plugin-action-button" data-action="open">Open in</button>
666
+ <% if (Array.isArray(pluginItem.install)) { %>
667
+ <button type="button" class="plugin-action-button" data-plugin-action="install">Install</button>
668
+ <% } %>
669
+ <% if (Array.isArray(pluginItem.uninstall)) { %>
670
+ <button type="button" class="plugin-action-button" data-plugin-action="uninstall">Uninstall</button>
671
+ <% } %>
672
+ <% if (Array.isArray(pluginItem.update)) { %>
673
+ <button type="button" class="plugin-action-button" data-plugin-action="update">Update</button>
674
+ <% } %>
675
+ </div>
487
676
  </div>
488
677
  </div>
489
678
  <% }) %>
@@ -828,6 +1017,119 @@ body.dark .plugin-option:hover {
828
1017
 
829
1018
  const modal = createPluginModal(apps)
830
1019
 
1020
+ const ACTION_LABELS = {
1021
+ install: 'Install',
1022
+ uninstall: 'Uninstall',
1023
+ update: 'Update'
1024
+ }
1025
+ const ACTION_ICONS = {
1026
+ install: 'fa-solid fa-download',
1027
+ uninstall: 'fa-solid fa-trash-can',
1028
+ update: 'fa-solid fa-rotate-right'
1029
+ }
1030
+
1031
+ function buildActionUrl(plugin, actionType) {
1032
+ if (!plugin || !plugin.pluginPath) return null
1033
+ const normalizedPath = plugin.pluginPath.startsWith('/') ? plugin.pluginPath.slice(1) : plugin.pluginPath
1034
+ if (!normalizedPath) return null
1035
+ const encodedPath = normalizedPath.split('/').map((segment) => encodeURIComponent(segment)).join('/')
1036
+ const ts = Date.now()
1037
+ return `/action/${encodeURIComponent(actionType)}/${encodedPath}?ts=${ts}`
1038
+ }
1039
+
1040
+ function showActionModal(actionType, plugin) {
1041
+ const targetUrl = buildActionUrl(plugin, actionType)
1042
+ if (!targetUrl) {
1043
+ alert('This action is missing a target script.')
1044
+ return
1045
+ }
1046
+ const pluginTitle = plugin && (plugin.title || plugin.text || plugin.name) ? (plugin.title || plugin.text || plugin.name) : 'Plugin'
1047
+ const title = `${ACTION_LABELS[actionType] || 'Run'} ${escapeHtml(pluginTitle)}`
1048
+ const subtitle = plugin && plugin.description ? escapeHtml(plugin.description) : ''
1049
+ const iconClass = ACTION_ICONS[actionType] || 'fa-solid fa-terminal'
1050
+ const modalHtml = `
1051
+ <div class="pinokio-modal-surface">
1052
+ <div class="pinokio-modal-header">
1053
+ <div class="pinokio-modal-icon"><i class="${iconClass}"></i></div>
1054
+ <div class="pinokio-modal-heading">
1055
+ <div class="pinokio-modal-title">${title}</div>
1056
+ <div class="pinokio-modal-subtitle">${subtitle}</div>
1057
+ </div>
1058
+ </div>
1059
+ <div class="pinokio-modal-body pinokio-modal-body--iframe">
1060
+ <iframe src="${targetUrl}"></iframe>
1061
+ </div>
1062
+ <div class="pinokio-modal-footer pinokio-modal-footer--publish" data-publish-footer>
1063
+ <button type="button" class="pinokio-publish-close-btn" data-publish-close>Close</button>
1064
+ </div>
1065
+ </div>
1066
+ `
1067
+
1068
+ Swal.fire({
1069
+ html: modalHtml,
1070
+ customClass: {
1071
+ popup: 'pinokio-modern-modal',
1072
+ htmlContainer: 'pinokio-modern-html',
1073
+ closeButton: 'pinokio-modern-close'
1074
+ },
1075
+ backdrop: 'rgba(9,11,15,0.65)',
1076
+ width: 'min(760px, 90vw)',
1077
+ showConfirmButton: false,
1078
+ showCloseButton: true,
1079
+ buttonsStyling: false,
1080
+ focusConfirm: false,
1081
+ didOpen: (popup) => {
1082
+ const iframe = popup.querySelector('iframe')
1083
+ const closeBtn = popup.querySelector('[data-publish-close]')
1084
+ if (iframe) {
1085
+ iframe.dataset.forceVisible = 'true'
1086
+ iframe.classList.remove('hidden')
1087
+ iframe.removeAttribute('hidden')
1088
+ }
1089
+ if (closeBtn) {
1090
+ closeBtn.addEventListener('click', () => {
1091
+ Swal.close()
1092
+ })
1093
+ }
1094
+ }
1095
+ })
1096
+ }
1097
+
1098
+ function normalizePluginPath(value) {
1099
+ if (typeof value !== 'string') return ''
1100
+ return value.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+$/, '')
1101
+ }
1102
+
1103
+ function findPluginByPath(targetPath) {
1104
+ if (!targetPath) return null
1105
+ const normalizedTarget = normalizePluginPath(targetPath)
1106
+ const targetWithFile = normalizedTarget.endsWith('pinokio.js')
1107
+ ? normalizedTarget
1108
+ : `${normalizedTarget}/pinokio.js`
1109
+ return plugins.find((plugin) => {
1110
+ const pluginPath = normalizePluginPath(plugin.pluginPath || '')
1111
+ if (!pluginPath) return false
1112
+ return pluginPath === targetWithFile || pluginPath === `/${targetWithFile}`
1113
+ }) || null
1114
+ }
1115
+
1116
+ function autoActivateFromQuery() {
1117
+ const params = new URLSearchParams(window.location.search)
1118
+ const targetPath = params.get('path')
1119
+ if (!targetPath) {
1120
+ return
1121
+ }
1122
+ const plugin = findPluginByPath(targetPath)
1123
+ if (!plugin) {
1124
+ return
1125
+ }
1126
+ if (plugin.hasInstall) {
1127
+ showActionModal('install', plugin)
1128
+ } else {
1129
+ modal.open(plugin)
1130
+ }
1131
+ }
1132
+
831
1133
  function attachHandlers() {
832
1134
  const cards = document.querySelectorAll('.plugin-card')
833
1135
  if (!cards.length) return
@@ -849,6 +1151,7 @@ body.dark .plugin-option:hover {
849
1151
  }
850
1152
  })
851
1153
  }
1154
+ const openButton = card.querySelector('[data-action="open"]')
852
1155
  const open = (event) => {
853
1156
  if (event) {
854
1157
  event.preventDefault()
@@ -859,11 +1162,17 @@ body.dark .plugin-option:hover {
859
1162
  }
860
1163
  modal.open(plugin)
861
1164
  }
862
- card.addEventListener('click', open)
863
- card.addEventListener('keydown', (event) => {
864
- if (event.key === 'Enter' || event.key === ' ') {
865
- open(event)
866
- }
1165
+ if (openButton) {
1166
+ openButton.addEventListener('click', open)
1167
+ }
1168
+ const actionButtons = card.querySelectorAll('[data-plugin-action]')
1169
+ actionButtons.forEach((button) => {
1170
+ const actionType = button.getAttribute('data-plugin-action')
1171
+ if (!actionType) return
1172
+ button.addEventListener('click', (event) => {
1173
+ event.preventDefault()
1174
+ showActionModal(actionType, plugin)
1175
+ })
867
1176
  })
868
1177
  })
869
1178
  }
@@ -871,6 +1180,7 @@ body.dark .plugin-option:hover {
871
1180
  document.addEventListener('DOMContentLoaded', () => {
872
1181
  initUrlFeatures()
873
1182
  attachHandlers()
1183
+ autoActivateFromQuery()
874
1184
  })
875
1185
  })()
876
1186
  </script>
@@ -163,6 +163,78 @@ body.dark .item select {
163
163
  .timestamp {
164
164
  color: rgba(0,0,0,0.5);
165
165
  }
166
+ .home-warning {
167
+ margin-top: 8px;
168
+ padding: 10px 12px;
169
+ border: 1px solid rgba(0,0,0,0.08);
170
+ border-radius: 6px;
171
+ font-size: 12px;
172
+ line-height: 1.4;
173
+ background: rgba(0,0,0,0.03);
174
+ }
175
+ body.dark .home-warning {
176
+ border-color: rgba(255,255,255,0.12);
177
+ background: rgba(255,255,255,0.04);
178
+ color: rgba(255,255,255,0.8);
179
+ }
180
+ .home-warning ul {
181
+ margin: 0;
182
+ padding-left: 18px;
183
+ }
184
+ .swal2-popup.custom-home-modal {
185
+ border-radius: 10px;
186
+ padding: 18px 18px 14px 18px;
187
+ background: #fdfdfd;
188
+ box-shadow: 0 8px 28px rgba(0,0,0,0.12);
189
+ text-align: left;
190
+ }
191
+ body.dark .swal2-popup.custom-home-modal {
192
+ background: #1f1f25;
193
+ box-shadow: 0 10px 36px rgba(0,0,0,0.45);
194
+ }
195
+ .swal2-popup.custom-home-modal .swal2-title {
196
+ text-align: left !important;
197
+ font-size: 16px;
198
+ font-weight: 700;
199
+ color: #111;
200
+ margin: 0 0 6px 0;
201
+ text-align: left;
202
+ }
203
+ body.dark .swal2-popup.custom-home-modal .swal2-title {
204
+ color: #f5f5f5;
205
+ }
206
+ .swal2-popup.custom-home-modal .swal2-html-container {
207
+ font-size: 14px;
208
+ color: #333;
209
+ margin: 0 0 10px 0;
210
+ text-align: left;
211
+ }
212
+ body.dark .swal2-popup.custom-home-modal .swal2-html-container {
213
+ color: rgba(255,255,255,0.8);
214
+ }
215
+ .swal2-popup.custom-home-modal .swal2-actions {
216
+ margin-top: 4px;
217
+ gap: 8px;
218
+ justify-content: flex-end !important;
219
+ }
220
+ .swal2-popup.custom-home-modal .swal2-styled.swal2-confirm {
221
+ background: #4a4ae0;
222
+ color: #fff;
223
+ border-radius: 6px;
224
+ padding: 8px 16px;
225
+ font-weight: 600;
226
+ }
227
+ .swal2-popup.custom-home-modal .swal2-styled.swal2-cancel {
228
+ background: transparent;
229
+ color: #555;
230
+ border: 1px solid rgba(0,0,0,0.15);
231
+ border-radius: 6px;
232
+ padding: 8px 16px;
233
+ }
234
+ body.dark .swal2-popup.custom-home-modal .swal2-styled.swal2-cancel {
235
+ color: rgba(255,255,255,0.8);
236
+ border-color: rgba(255,255,255,0.2);
237
+ }
166
238
  body.dark .loading {
167
239
  background: rgba(255,255,255,0.06);
168
240
  }
@@ -578,6 +650,28 @@ document.addEventListener('DOMContentLoaded', function() {
578
650
  document.addEventListener("DOMContentLoaded", async () => {
579
651
  //Reporter()
580
652
  const n = new N()
653
+ const homeInput = document.querySelector('input.homepath[name="home"]')
654
+ let showHomeWarning = () => {}
655
+ const originalHome = homeInput ? (homeInput.defaultValue || '') : ''
656
+ if (homeInput) {
657
+ const warning = document.createElement('div')
658
+ warning.className = 'home-warning hidden'
659
+ warning.innerHTML = `
660
+ <ul>
661
+ <li>Avoid exFAT; it can cause permission/metadata issues.</li>
662
+ <li>Avoid spaces in the path; they can break tooling.</li>
663
+ </ul>
664
+ `
665
+ const parent = homeInput.parentElement
666
+ if (parent) {
667
+ parent.appendChild(warning)
668
+ }
669
+ const revealWarning = () => warning.classList.remove('hidden')
670
+ showHomeWarning = revealWarning
671
+ homeInput.addEventListener('focus', revealWarning)
672
+ homeInput.addEventListener('input', revealWarning)
673
+ homeInput.addEventListener('click', revealWarning)
674
+ }
581
675
  const updateThemeClassAcrossShells = (nextTheme) => {
582
676
  if (!nextTheme) {
583
677
  return
@@ -722,6 +816,7 @@ document.addEventListener("DOMContentLoaded", async () => {
722
816
  document.querySelector("main form").addEventListener("submit", async (e) => {
723
817
  e.preventDefault()
724
818
  e.stopPropagation()
819
+ showHomeWarning()
725
820
  let val = document.querySelector(`[name=home]`).value
726
821
  if (/.*\s+.*/.test(val)) {
727
822
  alert("Please use a home path that does NOT include a space")
@@ -733,6 +828,25 @@ document.addEventListener("DOMContentLoaded", async () => {
733
828
  document.querySelector("[name=home]").focus()
734
829
  return
735
830
  }
831
+ const isChanged = val !== originalHome
832
+ if (homeInput && isChanged) {
833
+ const confirmResult = await Swal.fire({
834
+ title: 'Reminder',
835
+ html: 'exFAT drives often break permissions/metadata.<br>Please double-check the path isn\u2019t on exFAT. If it looks good, click \u201cSelect\u201d to continue.',
836
+ showCancelButton: true,
837
+ confirmButtonText: 'Select',
838
+ cancelButtonText: 'Cancel',
839
+ reverseButtons: true,
840
+ allowOutsideClick: false,
841
+ allowEscapeKey: false,
842
+ customClass: {
843
+ popup: 'custom-home-modal'
844
+ }
845
+ })
846
+ if (!confirmResult.isConfirmed) {
847
+ return
848
+ }
849
+ }
736
850
 
737
851
  // let drive = document.querySelector(`[name=drive]`).value
738
852
  // if (/.*\s+.*/.test(drive)) {
@@ -304,6 +304,7 @@ document.addEventListener("DOMContentLoaded", async () => {
304
304
  const baseScriptId = <% if (script_id) { %><%- JSON.stringify(script_id) %><% } else { %>null<% } %>
305
305
  const scriptUri = <% if (script_path) { %><%- JSON.stringify(script_path) %><% } else { %>null<% } %>
306
306
  const scriptCwd = <% if (cwd) { %><%- JSON.stringify(cwd) %><% } else { %>null<% } %>
307
+ const scriptAction = <% if (typeof action !== 'undefined') { %><%- JSON.stringify(action) %><% } else { %>null<% } %>
307
308
  class RPC {
308
309
  constructor() {
309
310
  this.socket = new Socket()
@@ -458,6 +459,9 @@ document.addEventListener("DOMContentLoaded", async () => {
458
459
  rows: this.term.rows,
459
460
  }
460
461
  }
462
+ if (scriptAction) {
463
+ payload.action = scriptAction
464
+ }
461
465
  if (scriptCwd) {
462
466
  payload.cwd = scriptCwd
463
467
  }