pinokiod 7.5.20 → 7.5.23

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.
@@ -75,6 +75,13 @@ const normalizePluginPath = (value) => {
75
75
  return normalized
76
76
  }
77
77
 
78
+ const systemPluginPathForLocalPath = (value) => {
79
+ const normalized = normalizePluginPath(value)
80
+ const localPrefix = "/plugin/"
81
+ if (!normalized.startsWith(localPrefix)) return ""
82
+ return `${SYSTEM_PLUGIN_RUN_PREFIX}/${normalized.slice(localPrefix.length)}`
83
+ }
84
+
78
85
  const pluginSelectionMatches = (src, selectedValue) => {
79
86
  const selectedPlugin = normalizeSlashes(typeof selectedValue === "string" ? selectedValue.trim() : "")
80
87
  if (!selectedPlugin || !src) return false
@@ -297,6 +304,7 @@ module.exports = {
297
304
  isSystemPluginPath,
298
305
  isLegacyPluginCodePath,
299
306
  normalizePluginPath,
307
+ systemPluginPathForLocalPath,
300
308
  pluginSelectionMatches,
301
309
  resolveRunPath,
302
310
  pluginPathToAbsolute,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.5.20",
3
+ "version": "7.5.23",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -78,6 +78,7 @@ const { createTerminalSessionHelpers } = require("./lib/terminal_session_helpers
78
78
  const { createLauncherInstructionBootstrap } = require("./lib/launcher_instruction_bootstrap")
79
79
  const { createTerminalGitResetHandler } = require("./lib/terminal_git_reset")
80
80
  const { createDesktopEventRouter } = require("./lib/desktop_event_router")
81
+ const { deletePluginFolder } = require("./lib/plugin_delete")
81
82
  const { createInjectRouter, resolveInjectList } = require("./lib/inject_router")
82
83
  const { createTaskPackageService } = require("./lib/task_packages")
83
84
  const { createTaskWorkspaceLinkService } = require("./lib/task_workspace_links")
@@ -7203,6 +7204,8 @@ class Server {
7203
7204
  link: pluginItem?.link || "",
7204
7205
  image: pluginItem?.image || null,
7205
7206
  icon: pluginItem?.icon || null,
7207
+ source: pluginItem?.source || "",
7208
+ system: pluginItem?.system === true,
7206
7209
  default: pluginItem?.default === true,
7207
7210
  pluginPath: normalizedPluginPath,
7208
7211
  pluginKey: normalizePluginLookupKey(normalizedPluginPath),
@@ -7335,7 +7338,18 @@ class Server {
7335
7338
  if (!targetKey) {
7336
7339
  return null
7337
7340
  }
7338
- return plugins.find((plugin) => plugin && plugin.pluginKey === targetKey) || null
7341
+ const exact = plugins.find((plugin) => plugin && plugin.pluginKey === targetKey)
7342
+ if (exact) {
7343
+ return exact
7344
+ }
7345
+ return findSystemPluginFallbackByPath(plugins, targetPath, targetKey)
7346
+ }
7347
+ const findSystemPluginFallbackByPath = (plugins, targetPath, targetKey = "") => {
7348
+ const systemFallbackKey = normalizePluginLookupKey(PluginSources.systemPluginPathForLocalPath(targetPath))
7349
+ if (!systemFallbackKey || systemFallbackKey === targetKey) {
7350
+ return null
7351
+ }
7352
+ return plugins.find((plugin) => plugin && plugin.system === true && plugin.pluginKey === systemFallbackKey) || null
7339
7353
  }
7340
7354
  const buildSerializedPluginFromValidation = (validation) => {
7341
7355
  const context = validation && validation.context && typeof validation.context === "object"
@@ -7383,6 +7397,10 @@ class Server {
7383
7397
  const pluginRoot = path.resolve(this.kernel.path("plugin"))
7384
7398
  const managedRoot = path.resolve(pluginRoot, "code")
7385
7399
  const normalizedPath = normalizePluginPath(plugin && plugin.pluginPath ? plugin.pluginPath : "")
7400
+ const isSystemPlugin = Boolean(plugin && (plugin.system === true || plugin.source === "system"))
7401
+ const systemPluginPath = PluginSources.isSystemPluginPath(normalizedPath)
7402
+ ? normalizedPath
7403
+ : (isSystemPlugin ? PluginSources.systemPluginPathForLocalPath(normalizedPath) : "")
7386
7404
  const emptyState = {
7387
7405
  ownership: "bundled",
7388
7406
  manageable: false,
@@ -7396,8 +7414,8 @@ class Server {
7396
7414
  localLabel: "",
7397
7415
  managedPrefix: "",
7398
7416
  }
7399
- if (PluginSources.isSystemPluginPath(normalizedPath)) {
7400
- const relativeSystemPath = normalizedPath.replace(/^\/pinokio\/run\/+/, "")
7417
+ if (isSystemPlugin || systemPluginPath) {
7418
+ const relativeSystemPath = (systemPluginPath || normalizedPath).replace(/^\/pinokio\/run\/+/, "")
7401
7419
  return {
7402
7420
  ...emptyState,
7403
7421
  ownership: "system",
@@ -7511,6 +7529,27 @@ class Server {
7511
7529
  }
7512
7530
  }
7513
7531
 
7532
+ if (localState.ownership === "system") {
7533
+ return {
7534
+ ownership: "system",
7535
+ manageable: false,
7536
+ canOpenFolder: false,
7537
+ dir: "",
7538
+ localLabel: localState.localLabel,
7539
+ remoteUrl: "",
7540
+ remoteWebUrl: "",
7541
+ githubConnected: false,
7542
+ gitInitialized: false,
7543
+ hasCommit: false,
7544
+ changeCount: 0,
7545
+ changes: [],
7546
+ branch: "HEAD",
7547
+ commitUrl: "",
7548
+ createUrl: "",
7549
+ pushUrl: "",
7550
+ }
7551
+ }
7552
+
7514
7553
  if (localState.ownership === "managed") {
7515
7554
  let changes = []
7516
7555
  try {
@@ -7726,6 +7765,7 @@ class Server {
7726
7765
  changePreview,
7727
7766
  extraChangeCount: Math.max(changes.length - changePreview.length, 0),
7728
7767
  canManageSource: ownership === "local",
7768
+ showSidebar: ownership !== "system",
7729
7769
  canOpenFolder: Boolean(shareState && shareState.canOpenFolder && shareState.dir),
7730
7770
  openFolderPath: shareState && shareState.canOpenFolder ? shareState.dir : "",
7731
7771
  isManagedSource: ownership === "managed",
@@ -7758,17 +7798,29 @@ class Server {
7758
7798
  res.redirect("/plugins")
7759
7799
  return
7760
7800
  }
7761
- const validation = await contentValidation.validatePluginByPath(requestedPath)
7762
- if (!validation.valid) {
7763
- await this.renderInvalidContentPage(req, res, validation, {
7764
- sidebarSelected: "plugins",
7765
- backHref: "/plugins",
7766
- backLabel: "Back to Plugins",
7767
- })
7768
- return
7801
+ let plugins = null
7802
+ const loadPluginsOnce = async () => {
7803
+ if (!plugins) {
7804
+ plugins = await loadSerializedPlugins()
7805
+ }
7806
+ return plugins
7807
+ }
7808
+ let plugin = null
7809
+ if (PluginSources.systemPluginPathForLocalPath(requestedPath)) {
7810
+ plugin = findPluginByPath(await loadPluginsOnce(), requestedPath)
7811
+ }
7812
+ if (!plugin) {
7813
+ const validation = await contentValidation.validatePluginByPath(requestedPath)
7814
+ if (!validation.valid) {
7815
+ await this.renderInvalidContentPage(req, res, validation, {
7816
+ sidebarSelected: "plugins",
7817
+ backHref: "/plugins",
7818
+ backLabel: "Back to Plugins",
7819
+ })
7820
+ return
7821
+ }
7822
+ plugin = findPluginByPath(await loadPluginsOnce(), requestedPath) || buildSerializedPluginFromValidation(validation)
7769
7823
  }
7770
- const plugins = await loadSerializedPlugins()
7771
- const plugin = findPluginByPath(plugins, requestedPath) || buildSerializedPluginFromValidation(validation)
7772
7824
  if (!plugin) {
7773
7825
  res.status(404).send("Plugin not found.")
7774
7826
  return
@@ -7818,6 +7870,54 @@ class Server {
7818
7870
  share: shareState
7819
7871
  })
7820
7872
  }))
7873
+ this.app.post("/plugin/delete", ex(async (req, res) => {
7874
+ const requestedPath = typeof req.body.path === "string" ? req.body.path.trim() : ""
7875
+ if (!requestedPath) {
7876
+ res.status(400).json({
7877
+ ok: false,
7878
+ error: "Plugin path is required."
7879
+ })
7880
+ return
7881
+ }
7882
+ try {
7883
+ const plugins = await loadSerializedPlugins()
7884
+ let plugin = findPluginByPath(plugins, requestedPath)
7885
+ if (!plugin) {
7886
+ const validation = await contentValidation.validatePluginByPath(requestedPath)
7887
+ if (!validation.valid) {
7888
+ res.status(404).json({
7889
+ ok: false,
7890
+ error: validation.message || "Plugin not found."
7891
+ })
7892
+ return
7893
+ }
7894
+ plugin = buildSerializedPluginFromValidation(validation)
7895
+ }
7896
+ if (!plugin) {
7897
+ res.status(404).json({
7898
+ ok: false,
7899
+ error: "Plugin not found."
7900
+ })
7901
+ return
7902
+ }
7903
+
7904
+ const target = await deletePluginFolder({ kernel: this.kernel, plugin })
7905
+ res.set("Cache-Control", "no-store")
7906
+ res.json({
7907
+ ok: true,
7908
+ redirect: "/plugins",
7909
+ deleted: {
7910
+ pluginPath: target.pluginPath,
7911
+ localLabel: target.localLabel,
7912
+ }
7913
+ })
7914
+ } catch (error) {
7915
+ res.status(error && error.status ? error.status : 500).json({
7916
+ ok: false,
7917
+ error: error && error.message ? error.message : "Failed to delete plugin."
7918
+ })
7919
+ }
7920
+ }))
7821
7921
  this.app.get("/api/plugin/menu", ex(async (req, res) => {
7822
7922
  try {
7823
7923
  const workspacePath = resolveProjectShellWorkspace(req.query.workspace)
@@ -0,0 +1,89 @@
1
+ const fs = require("fs")
2
+ const path = require("path")
3
+ const PluginSources = require("../../kernel/plugin_sources")
4
+
5
+ const isPathInsideRoot = (candidatePath, rootPath) => {
6
+ const relative = path.relative(rootPath, candidatePath)
7
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))
8
+ }
9
+
10
+ const httpError = (status, message) => {
11
+ const error = new Error(message)
12
+ error.status = status
13
+ return error
14
+ }
15
+
16
+ const assertFinalRmTargetInsidePluginRoot = async (target) => {
17
+ const pluginRoot = path.resolve(target.pluginRoot)
18
+ const rmPath = path.resolve(target.pluginDir)
19
+ if (!isPathInsideRoot(rmPath, pluginRoot) || rmPath === pluginRoot) {
20
+ throw httpError(403, "Plugin folder is outside the downloaded plugin folder.")
21
+ }
22
+
23
+ const [realPluginRoot, realRmPath] = await Promise.all([
24
+ fs.promises.realpath(pluginRoot),
25
+ fs.promises.realpath(rmPath),
26
+ ])
27
+ if (!isPathInsideRoot(realRmPath, realPluginRoot) || realRmPath === realPluginRoot) {
28
+ throw httpError(403, "Plugin folder is outside the downloaded plugin folder.")
29
+ }
30
+ }
31
+
32
+ const resolvePluginDeleteTarget = ({ kernel, plugin }) => {
33
+ if (!kernel || typeof kernel.path !== "function") {
34
+ throw httpError(500, "Plugin home path is unavailable.")
35
+ }
36
+
37
+ const normalizedPath = PluginSources.normalizePluginPath(plugin && plugin.pluginPath ? plugin.pluginPath : "")
38
+ if (!normalizedPath) {
39
+ throw httpError(400, "Plugin path is required.")
40
+ }
41
+ if ((plugin && (plugin.system === true || plugin.source === "system")) || PluginSources.isSystemPluginPath(normalizedPath)) {
42
+ throw httpError(403, "Built-in plugins cannot be deleted.")
43
+ }
44
+ if (!normalizedPath.startsWith("/plugin/") || PluginSources.isLegacyPluginCodePath(normalizedPath)) {
45
+ throw httpError(403, "Only downloaded plugin folders can be deleted.")
46
+ }
47
+
48
+ const pluginRoot = path.resolve(kernel.path("plugin"))
49
+ const pluginFilePath = path.resolve(kernel.path(normalizedPath.slice(1)))
50
+ if (!isPathInsideRoot(pluginFilePath, pluginRoot)) {
51
+ throw httpError(403, "Plugin path is outside the downloaded plugin folder.")
52
+ }
53
+
54
+ const pluginDir = path.dirname(pluginFilePath)
55
+ if (pluginDir === pluginRoot) {
56
+ throw httpError(403, "The plugin root folder cannot be deleted from this page.")
57
+ }
58
+
59
+ const relativeDir = path.relative(pluginRoot, pluginDir).split(path.sep).join("/")
60
+ return {
61
+ pluginPath: normalizedPath,
62
+ pluginRoot,
63
+ pluginFilePath,
64
+ pluginDir,
65
+ relativeDir,
66
+ localLabel: relativeDir ? `plugin/${relativeDir}` : "plugin",
67
+ }
68
+ }
69
+
70
+ const deletePluginFolder = async ({ kernel, plugin }) => {
71
+ const target = resolvePluginDeleteTarget({ kernel, plugin })
72
+
73
+ const [fileStat, dirStat] = await Promise.all([
74
+ fs.promises.stat(target.pluginFilePath).catch(() => null),
75
+ fs.promises.stat(target.pluginDir).catch(() => null),
76
+ ])
77
+ if (!fileStat || !fileStat.isFile() || !dirStat || !dirStat.isDirectory()) {
78
+ throw httpError(404, "Plugin folder was not found.")
79
+ }
80
+
81
+ await assertFinalRmTargetInsidePluginRoot(target)
82
+ await fs.promises.rm(target.pluginDir, { recursive: true, force: true })
83
+ return target
84
+ }
85
+
86
+ module.exports = {
87
+ deletePluginFolder,
88
+ resolvePluginDeleteTarget,
89
+ }
@@ -3699,9 +3699,13 @@ document.addEventListener("DOMContentLoaded", () => {
3699
3699
  const type = trigger.dataset && typeof trigger.dataset.universalLauncherOpen === 'string'
3700
3700
  ? trigger.dataset.universalLauncherOpen.trim()
3701
3701
  : '';
3702
- createLauncherDebugLog('universal launcher trigger clicked', { type });
3702
+ const mode = trigger.dataset && typeof trigger.dataset.universalLauncherMode === 'string'
3703
+ ? trigger.dataset.universalLauncherMode.trim()
3704
+ : '';
3705
+ createLauncherDebugLog('universal launcher trigger clicked', { type, mode });
3703
3706
  guardUniversalLauncher(api, {
3704
- type: type || 'create_app'
3707
+ type: type || 'create_app',
3708
+ mode: mode || ''
3705
3709
  });
3706
3710
  });
3707
3711
  });
@@ -34,6 +34,7 @@
34
34
  uninstall: "fa-solid fa-trash-can",
35
35
  update: "fa-solid fa-rotate-right"
36
36
  };
37
+ const EVENT_PANEL_STATUS_EVENT = "pinokio:event-panel-status";
37
38
 
38
39
  const noteEl = document.querySelector("[data-plugin-share-note]");
39
40
  const nextTitleEl = document.querySelector("[data-plugin-share-next-title]");
@@ -43,6 +44,7 @@
43
44
  const createCancel = document.querySelector("[data-plugin-share-create-cancel]");
44
45
  const repoNameInput = document.querySelector("[data-plugin-share-repo-name]");
45
46
  const visibilitySelect = document.querySelector("[data-plugin-share-visibility]");
47
+ const staticActions = document.querySelector("[data-plugin-share-static-actions]");
46
48
  const remoteLink = document.querySelector("[data-plugin-share-remote-link]");
47
49
  const refreshButton = document.querySelector("[data-plugin-share-refresh]");
48
50
  const shareCopyEl = document.querySelector("[data-plugin-share-copy]");
@@ -59,9 +61,11 @@
59
61
  const aiPermissionClear = document.querySelector("[data-plugin-ai-permission-clear]");
60
62
  const aiPermissionPath = document.querySelector("[data-plugin-ai-permission-path]");
61
63
  const aiPermissionStatus = document.querySelector("[data-plugin-ai-permission-status]");
64
+ const deleteButton = document.querySelector("[data-plugin-delete]");
62
65
 
63
66
  let refreshInFlight = false;
64
67
  let createFormOpen = false;
68
+ let activeActionModal = null;
65
69
  const AI_CONSENT_PREFIX = "pinokio:ai-consent:";
66
70
 
67
71
  function readDownloadState() {
@@ -182,6 +186,72 @@
182
186
  }
183
187
  }
184
188
 
189
+ async function confirmPluginDelete() {
190
+ if (!deleteButton || !plugin || !plugin.pluginPath) {
191
+ return;
192
+ }
193
+
194
+ const pluginTitle = plugin.title || "this plugin";
195
+ const localLabel = share && share.localLabel ? share.localLabel : "this plugin folder";
196
+ let confirmed = false;
197
+ if (window.Swal && typeof Swal.fire === "function") {
198
+ const result = await Swal.fire({
199
+ title: `Delete ${pluginTitle}?`,
200
+ html: `This permanently deletes <code>${escapeHtml(localLabel)}</code>. Only this plugin folder will be removed.`,
201
+ showCancelButton: true,
202
+ confirmButtonText: "Delete plugin",
203
+ cancelButtonText: "Cancel",
204
+ focusCancel: true,
205
+ reverseButtons: true,
206
+ buttonsStyling: false,
207
+ customClass: {
208
+ confirmButton: "task-button danger",
209
+ cancelButton: "task-button"
210
+ }
211
+ });
212
+ confirmed = !!(result && result.isConfirmed);
213
+ } else {
214
+ confirmed = window.confirm(`Delete ${pluginTitle}? This permanently deletes ${localLabel}.`);
215
+ }
216
+ if (!confirmed) {
217
+ return;
218
+ }
219
+
220
+ const originalHtml = deleteButton.innerHTML;
221
+ deleteButton.disabled = true;
222
+ deleteButton.classList.add("is-busy");
223
+ try {
224
+ const response = await fetch("/plugin/delete", {
225
+ method: "POST",
226
+ headers: { "Content-Type": "application/json" },
227
+ body: JSON.stringify({ path: plugin.pluginPath })
228
+ });
229
+ const payload = await response.json().catch(() => ({}));
230
+ if (!response.ok || !payload.ok) {
231
+ throw new Error(payload && payload.error ? payload.error : `HTTP ${response.status}`);
232
+ }
233
+ window.location.href = payload.redirect || "/plugins";
234
+ } catch (error) {
235
+ deleteButton.disabled = false;
236
+ deleteButton.classList.remove("is-busy");
237
+ deleteButton.innerHTML = originalHtml;
238
+ const message = error && error.message ? error.message : "Failed to delete plugin.";
239
+ if (window.Swal && typeof Swal.fire === "function") {
240
+ Swal.fire({
241
+ title: "Delete failed",
242
+ text: message,
243
+ confirmButtonText: "Close",
244
+ buttonsStyling: false,
245
+ customClass: {
246
+ confirmButton: "task-button"
247
+ }
248
+ });
249
+ } else {
250
+ alert(message);
251
+ }
252
+ }
253
+ }
254
+
185
255
  function clearNode(node) {
186
256
  while (node && node.firstChild) {
187
257
  node.removeChild(node.firstChild);
@@ -233,10 +303,38 @@
233
303
  if (plugin.defaultCwd) {
234
304
  params.set("cwd", plugin.defaultCwd);
235
305
  }
306
+ params.set("__pinokio_event_panel", "1");
236
307
  params.set("ts", String(Date.now()));
237
308
  return `/action/${encodeURIComponent(actionType)}/${encodedPath}?${params.toString()}`;
238
309
  }
239
310
 
311
+ function finishActionModal(modalState) {
312
+ if (!modalState || modalState.completed) {
313
+ return;
314
+ }
315
+ modalState.completed = true;
316
+ Swal.close();
317
+ }
318
+
319
+ function handleActionCompleteMessage(event) {
320
+ const data = event && event.data && typeof event.data === "object" ? event.data : null;
321
+ if (!data || data.e !== EVENT_PANEL_STATUS_EVENT || !activeActionModal) {
322
+ return;
323
+ }
324
+ if (event.origin !== window.location.origin) {
325
+ return;
326
+ }
327
+ const modalState = activeActionModal;
328
+ if (data.success === false) {
329
+ return;
330
+ }
331
+ const iframe = modalState.iframe;
332
+ if (!iframe || event.source !== iframe.contentWindow) {
333
+ return;
334
+ }
335
+ finishActionModal(modalState);
336
+ }
337
+
240
338
  function showActionModal(actionType) {
241
339
  const targetUrl = buildActionUrl(actionType);
242
340
  if (!targetUrl) {
@@ -261,6 +359,11 @@
261
359
  </div>
262
360
  </div>
263
361
  `;
362
+ const modalState = {
363
+ iframe: null,
364
+ completed: false
365
+ };
366
+ activeActionModal = modalState;
264
367
 
265
368
  Swal.fire({
266
369
  html: modalHtml,
@@ -278,10 +381,19 @@
278
381
  didOpen: (popup) => {
279
382
  const iframe = popup.querySelector("iframe");
280
383
  if (iframe) {
384
+ modalState.iframe = iframe;
281
385
  iframe.dataset.forceVisible = "true";
282
386
  iframe.classList.remove("hidden");
283
387
  iframe.removeAttribute("hidden");
284
388
  }
389
+ },
390
+ didClose: () => {
391
+ if (activeActionModal === modalState) {
392
+ activeActionModal = null;
393
+ }
394
+ if (modalState.completed) {
395
+ window.location.reload();
396
+ }
285
397
  }
286
398
  });
287
399
  }
@@ -761,6 +873,9 @@
761
873
  }
762
874
 
763
875
  function renderRemoteState() {
876
+ const hasRemote = !!share.remoteUrl;
877
+ const hasRemoteWebUrl = !!share.remoteWebUrl;
878
+ const showStaticRefresh = hasRemote && !!share.githubConnected;
764
879
  if (noteEl) {
765
880
  if (!share.manageable) {
766
881
  noteEl.textContent = "Read only";
@@ -776,8 +891,14 @@
776
891
  noteEl.textContent = "Setup needed";
777
892
  }
778
893
  }
894
+ if (staticActions) {
895
+ staticActions.classList.toggle("task-hidden", !hasRemoteWebUrl && !showStaticRefresh);
896
+ }
897
+ if (refreshButton) {
898
+ refreshButton.classList.toggle("task-hidden", !showStaticRefresh);
899
+ }
779
900
  if (remoteLink) {
780
- if (share.remoteWebUrl) {
901
+ if (hasRemoteWebUrl) {
781
902
  remoteLink.href = share.remoteWebUrl;
782
903
  remoteLink.classList.remove("task-hidden", "is-disabled");
783
904
  remoteLink.removeAttribute("aria-disabled");
@@ -936,6 +1057,8 @@
936
1057
  });
937
1058
  }
938
1059
 
1060
+ window.addEventListener("message", handleActionCompleteMessage);
1061
+
939
1062
  document.querySelectorAll("[data-plugin-action]").forEach((button) => {
940
1063
  button.addEventListener("click", (event) => {
941
1064
  event.preventDefault();
@@ -998,6 +1121,13 @@
998
1121
  updateAiPermissionStatus();
999
1122
  }
1000
1123
 
1124
+ if (deleteButton) {
1125
+ deleteButton.addEventListener("click", (event) => {
1126
+ event.preventDefault();
1127
+ confirmPluginDelete();
1128
+ });
1129
+ }
1130
+
1001
1131
  render();
1002
1132
  applyDownloadedState();
1003
1133
  })();