pinokiod 7.1.50 → 7.1.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.1.50",
3
+ "version": "7.1.52",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -47,6 +47,12 @@ const LOG_STREAM_INITIAL_BYTES = 512 * 1024
47
47
  const LOG_STREAM_KEEPALIVE_MS = 25000
48
48
  const DEFAULT_REGISTRY_URL = 'https://beta.pinokio.co'
49
49
  const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]'])
50
+ const NON_INTERACTIVE_GIT_ENV = {
51
+ GIT_TERMINAL_PROMPT: "0",
52
+ GIT_ASKPASS: "",
53
+ SSH_ASKPASS: "",
54
+ GCM_INTERACTIVE: "never"
55
+ }
50
56
 
51
57
  const ex = fn => (req, res, next) => {
52
58
  Promise.resolve(fn(req, res, next)).catch(next);
@@ -72,7 +78,7 @@ const { createDesktopEventRouter } = require("./lib/desktop_event_router")
72
78
  const { createInjectRouter, resolveInjectList } = require("./lib/inject_router")
73
79
  const { createTaskPackageService } = require("./lib/task_packages")
74
80
  const { createTaskWorkspaceLinkService } = require("./lib/task_workspace_links")
75
- const { createInstallValidationService } = require("./lib/install_validation")
81
+ const { createContentValidationService } = require("./lib/content_validation")
76
82
  const AppRegistryService = require("./lib/app_registry")
77
83
  const AppLogService = require("./lib/app_logs")
78
84
  const AppSearchService = require("./lib/app_search")
@@ -1016,11 +1022,74 @@ class Server {
1016
1022
  //
1017
1023
  // return current_urls
1018
1024
  }
1025
+ async buildShellSidebarContext(req) {
1026
+ const peerAccess = await this.composePeerAccessPayload()
1027
+ return {
1028
+ current_host: this.kernel.peer.host,
1029
+ ...peerAccess,
1030
+ portal: this.portal,
1031
+ logo: this.logo,
1032
+ theme: this.theme,
1033
+ agent: req.agent,
1034
+ list: this.getPeers(),
1035
+ }
1036
+ }
1037
+ async renderInvalidContentPage(req, res, invalid, options = {}) {
1038
+ const type = invalid && typeof invalid.type === "string" ? invalid.type : "app"
1039
+ const sidebarSelected = options.sidebarSelected || (type === "plugin" ? "plugins" : type === "task" ? "tasks" : "home")
1040
+ const sidebarContext = await this.buildShellSidebarContext(req)
1041
+ const backHref = typeof options.backHref === "string"
1042
+ ? options.backHref
1043
+ : (type === "plugin" ? "/plugins" : type === "task" ? "/tasks" : "/home")
1044
+ const backLabel = typeof options.backLabel === "string"
1045
+ ? options.backLabel
1046
+ : "Back"
1047
+ const folderPath = invalid && typeof invalid.folderPath === "string" ? invalid.folderPath : ""
1048
+ const manifestPath = invalid && typeof invalid.manifestPath === "string" ? invalid.manifestPath : ""
1049
+ let folderExists = false
1050
+ let manifestExists = false
1051
+ if (folderPath) {
1052
+ try {
1053
+ await fs.promises.stat(folderPath)
1054
+ folderExists = true
1055
+ } catch (_) {}
1056
+ }
1057
+ if (manifestPath) {
1058
+ try {
1059
+ await fs.promises.stat(manifestPath)
1060
+ manifestExists = true
1061
+ } catch (_) {}
1062
+ }
1063
+ res.status(Number.isInteger(options.status) ? options.status : 422).render("invalid_content", {
1064
+ ...sidebarContext,
1065
+ theme: this.theme,
1066
+ agent: req.agent,
1067
+ invalid: {
1068
+ ...invalid,
1069
+ folderExists,
1070
+ manifestExists,
1071
+ },
1072
+ sidebarSelected,
1073
+ backHref,
1074
+ backLabel,
1075
+ })
1076
+ }
1019
1077
 
1020
1078
  async chrome(req, res, type, options) {
1021
1079
  console.log("Chrome")
1022
1080
 
1023
1081
  let name = req.params.name
1082
+ if (this.contentValidation) {
1083
+ const validation = await this.contentValidation.validateAppByName(name)
1084
+ if (validation && !validation.valid) {
1085
+ await this.renderInvalidContentPage(req, res, validation, {
1086
+ sidebarSelected: "home",
1087
+ backHref: "/home",
1088
+ backLabel: "Back to Home",
1089
+ })
1090
+ return
1091
+ }
1092
+ }
1024
1093
  console.time("bin check")
1025
1094
  let { requirements, install_required, requirements_pending, error } = await this.kernel.bin.check({
1026
1095
  bin: this.kernel.bin.preset("dev"),
@@ -1188,9 +1257,10 @@ class Server {
1188
1257
  }
1189
1258
 
1190
1259
  let editor_tab = `/pinokio/fileview/${encodeURIComponent(name)}`
1260
+ const tabsStorageKey = `${name}:${type}`
1191
1261
  let savedTabs = []
1192
- if (Array.isArray(this.tabs[name])) {
1193
- savedTabs = this.tabs[name].filter((entry) => {
1262
+ if (Array.isArray(this.tabs[tabsStorageKey])) {
1263
+ savedTabs = this.tabs[tabsStorageKey].filter((entry) => {
1194
1264
  const href = typeof entry === "string"
1195
1265
  ? entry
1196
1266
  : (entry && typeof entry.href === "string" ? entry.href : "")
@@ -2320,6 +2390,26 @@ class Server {
2320
2390
  filepath = full_filepath
2321
2391
  }
2322
2392
 
2393
+ if ((req.action || req.originalUrl.startsWith("/run/")) && this.contentValidation) {
2394
+ const validation = await this.contentValidation.validateRunPath(pathComponents)
2395
+ if (validation && !validation.valid) {
2396
+ await this.renderInvalidContentPage(req, res, validation, {
2397
+ sidebarSelected: validation.type === "plugin" ? "plugins" : validation.type === "task" ? "tasks" : "home",
2398
+ backHref: validation.type === "plugin"
2399
+ ? (validation.detailUrl || "/plugins")
2400
+ : validation.type === "task"
2401
+ ? (validation.detailUrl || "/tasks")
2402
+ : (validation.detailUrl || "/home"),
2403
+ backLabel: validation.type === "plugin"
2404
+ ? "Back to Plugin"
2405
+ : validation.type === "task"
2406
+ ? "Back to Task"
2407
+ : "Back to App",
2408
+ })
2409
+ return
2410
+ }
2411
+ }
2412
+
2323
2413
  // check if it's a folder or a file
2324
2414
  let p = "/api" // run mode
2325
2415
  let _p = "/_api" // edit mode
@@ -6523,7 +6613,8 @@ class Server {
6523
6613
  try {
6524
6614
  await this.kernel.exec({
6525
6615
  message: [`git clone "${safeRemote}" "${folder}"`],
6526
- path: apiRoot
6616
+ path: apiRoot,
6617
+ env: { ...NON_INTERACTIVE_GIT_ENV }
6527
6618
  }, () => {})
6528
6619
  } catch (err) {
6529
6620
  await fs.promises.rm(targetPath, { recursive: true, force: true }).catch(() => {})
@@ -6785,6 +6876,42 @@ class Server {
6785
6876
  }
6786
6877
  return plugins.find((plugin) => plugin && plugin.pluginKey === targetKey) || null
6787
6878
  }
6879
+ const buildSerializedPluginFromValidation = (validation) => {
6880
+ const context = validation && validation.context && typeof validation.context === "object"
6881
+ ? validation.context
6882
+ : null
6883
+ if (!context || !context.pluginPath) {
6884
+ return null
6885
+ }
6886
+ const normalizedPluginPath = normalizePluginPath(context.pluginPath)
6887
+ if (!normalizedPluginPath) {
6888
+ return null
6889
+ }
6890
+ const config = context.config && typeof context.config === "object" ? context.config : {}
6891
+ const category = classifyPluginMenuItem(config)
6892
+ return {
6893
+ index: -1,
6894
+ title: context.title || "Plugin",
6895
+ description: typeof config.description === "string" ? config.description : "",
6896
+ href: `/run${normalizedPluginPath}`,
6897
+ link: typeof config.link === "string" ? config.link : "",
6898
+ image: context.image || null,
6899
+ icon: null,
6900
+ default: false,
6901
+ pluginPath: normalizedPluginPath,
6902
+ pluginKey: normalizePluginLookupKey(normalizedPluginPath),
6903
+ extraParams: [],
6904
+ defaultCwd: "",
6905
+ ownerApp: null,
6906
+ hasInstall: !!context.hasInstall,
6907
+ hasUninstall: !!context.hasUninstall,
6908
+ hasUpdate: !!context.hasUpdate,
6909
+ category,
6910
+ categoryTitle: category === "ide" ? "Desktop Plugin" : "Terminal Plugin",
6911
+ categorySubtitle: category === "ide" ? "Launch externally" : "Launch in Pinokio",
6912
+ detailUrl: `/plugin?path=${encodeURIComponent(normalizedPluginPath)}`,
6913
+ }
6914
+ }
6788
6915
  const isPathInsideRoot = (candidatePath, rootPath) => {
6789
6916
  const relative = path.relative(rootPath, candidatePath)
6790
6917
  return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))
@@ -7134,8 +7261,17 @@ class Server {
7134
7261
  res.redirect("/plugins")
7135
7262
  return
7136
7263
  }
7264
+ const validation = await contentValidation.validatePluginByPath(requestedPath)
7265
+ if (!validation.valid) {
7266
+ await this.renderInvalidContentPage(req, res, validation, {
7267
+ sidebarSelected: "plugins",
7268
+ backHref: "/plugins",
7269
+ backLabel: "Back to Plugins",
7270
+ })
7271
+ return
7272
+ }
7137
7273
  const plugins = await loadSerializedPlugins()
7138
- const plugin = findPluginByPath(plugins, requestedPath)
7274
+ const plugin = findPluginByPath(plugins, requestedPath) || buildSerializedPluginFromValidation(validation)
7139
7275
  if (!plugin) {
7140
7276
  res.status(404).send("Plugin not found.")
7141
7277
  return
@@ -7994,8 +8130,19 @@ class Server {
7994
8130
  try {
7995
8131
  await this.kernel.exec({
7996
8132
  message: [`git clone --depth 1 --single-branch ${shellQuote(normalizedRef)} ${shellQuote(targetPath)}`],
7997
- path: path.resolve(rootDir)
8133
+ path: path.resolve(rootDir),
8134
+ env: { ...NON_INTERACTIVE_GIT_ENV }
7998
8135
  }, () => {})
8136
+ let cloned = false
8137
+ try {
8138
+ const stat = await fs.promises.stat(targetPath)
8139
+ cloned = stat.isDirectory()
8140
+ } catch (_) {}
8141
+ if (!cloned) {
8142
+ const cloneError = new Error("Failed to clone repository.")
8143
+ cloneError.status = 500
8144
+ throw cloneError
8145
+ }
7999
8146
  return targetPath
8000
8147
  } catch (error) {
8001
8148
  await fs.promises.rm(targetPath, { recursive: true, force: true }).catch(() => {})
@@ -8004,6 +8151,112 @@ class Server {
8004
8151
  throw nextError
8005
8152
  }
8006
8153
  }
8154
+ const prepareLauncherDownload = async ({ intent, ref, name }) => {
8155
+ const normalizedIntent = typeof intent === "string" ? intent.trim().toLowerCase() : ""
8156
+ if (normalizedIntent !== "create_app" && normalizedIntent !== "create_plugin" && normalizedIntent !== "ask") {
8157
+ const error = new Error("Unsupported download target.")
8158
+ error.status = 400
8159
+ throw error
8160
+ }
8161
+ if (normalizedIntent === "ask") {
8162
+ const preparedTask = await taskPackages.prepareRemoteTaskPackageInstall({ ref })
8163
+ if (preparedTask.existing) {
8164
+ return {
8165
+ existing: true,
8166
+ url: buildTaskPath({ id: preparedTask.id })
8167
+ }
8168
+ }
8169
+ return {
8170
+ existing: false,
8171
+ clone: {
8172
+ message: `git clone --depth 1 --single-branch ${shellQuote(ref)} ${shellQuote(preparedTask.dir)}`,
8173
+ path: taskPackages.tasksRoot(),
8174
+ env: { ...NON_INTERACTIVE_GIT_ENV }
8175
+ },
8176
+ finalize: {
8177
+ intent: normalizedIntent,
8178
+ id: preparedTask.id,
8179
+ ref: preparedTask.ref
8180
+ }
8181
+ }
8182
+ }
8183
+
8184
+ const folderName = typeof name === "string" ? name.trim() : ""
8185
+ const normalizedRef = normalizeLauncherDownloadRef(ref)
8186
+ if (!normalizedRef) {
8187
+ const error = new Error("Git URL is required.")
8188
+ error.status = 400
8189
+ throw error
8190
+ }
8191
+ if (!folderName) {
8192
+ const error = new Error("Folder name is required.")
8193
+ error.status = 400
8194
+ throw error
8195
+ }
8196
+
8197
+ const rootDir = normalizedIntent === "create_plugin"
8198
+ ? path.resolve(this.kernel.path("plugin"))
8199
+ : path.resolve(this.kernel.path("api"))
8200
+ const targetPath = await createLauncherTargetFolder(rootDir, folderName, {
8201
+ createDirectory: false,
8202
+ initializeGit: false
8203
+ })
8204
+ return {
8205
+ existing: false,
8206
+ clone: {
8207
+ message: `git clone --depth 1 --single-branch ${shellQuote(normalizedRef)} ${shellQuote(targetPath)}`,
8208
+ path: path.resolve(rootDir),
8209
+ env: { ...NON_INTERACTIVE_GIT_ENV }
8210
+ },
8211
+ finalize: {
8212
+ intent: normalizedIntent,
8213
+ name: folderName
8214
+ }
8215
+ }
8216
+ }
8217
+ const finalizeLauncherDownload = async ({ intent, ref, name, id }) => {
8218
+ const normalizedIntent = typeof intent === "string" ? intent.trim().toLowerCase() : ""
8219
+ if (normalizedIntent === "ask") {
8220
+ const task = await taskPackages.finalizeRemoteTaskPackageInstall({ id, ref })
8221
+ return {
8222
+ url: buildTaskPath({ id: task.id })
8223
+ }
8224
+ }
8225
+ if (normalizedIntent !== "create_app" && normalizedIntent !== "create_plugin") {
8226
+ const error = new Error("Unsupported download target.")
8227
+ error.status = 400
8228
+ throw error
8229
+ }
8230
+ const folderName = typeof name === "string" ? name.trim() : ""
8231
+ if (!folderName) {
8232
+ const error = new Error("Folder name is required.")
8233
+ error.status = 400
8234
+ throw error
8235
+ }
8236
+ const rootDir = normalizedIntent === "create_plugin"
8237
+ ? path.resolve(this.kernel.path("plugin"))
8238
+ : path.resolve(this.kernel.path("api"))
8239
+ const targetPath = path.resolve(rootDir, folderName)
8240
+ let cloned = false
8241
+ try {
8242
+ const stat = await fs.promises.stat(targetPath)
8243
+ cloned = stat.isDirectory()
8244
+ } catch (_) {}
8245
+ if (!cloned) {
8246
+ const error = new Error("Failed to clone repository.")
8247
+ error.status = 500
8248
+ throw error
8249
+ }
8250
+ if (normalizedIntent === "create_app") {
8251
+ return {
8252
+ url: `/initialize/${encodeURIComponent(folderName)}`
8253
+ }
8254
+ }
8255
+ const relativePluginPath = `plugin/${folderName}`
8256
+ return {
8257
+ url: `/plugin?path=${encodeURIComponent(relativePluginPath)}&downloaded=1`
8258
+ }
8259
+ }
8007
8260
  const createUniversalLauncherSessionId = () => {
8008
8261
  if (typeof crypto.randomUUID === "function") {
8009
8262
  return crypto.randomUUID()
@@ -8067,9 +8320,10 @@ class Server {
8067
8320
  const taskPackages = createTaskPackageService({
8068
8321
  kernel: this.kernel
8069
8322
  })
8070
- const installValidation = createInstallValidationService({
8323
+ const contentValidation = createContentValidationService({
8071
8324
  kernel: this.kernel
8072
8325
  })
8326
+ this.contentValidation = contentValidation
8073
8327
  const taskWorkspaceLinks = createTaskWorkspaceLinkService({
8074
8328
  kernel: this.kernel
8075
8329
  })
@@ -8299,6 +8553,52 @@ class Server {
8299
8553
  list: this.getPeers(),
8300
8554
  }
8301
8555
  }
8556
+ const resolveTaskForOpen = async ({ id, ref }) => {
8557
+ const normalizedId = typeof id === "string" ? taskPackages.normalizeTaskId(id) : ""
8558
+ if (normalizedId) {
8559
+ const index = await taskPackages.readTaskIndex()
8560
+ const existsInIndex = Array.isArray(index && index.items) && index.items.some((entry) => entry && entry.id === normalizedId)
8561
+ if (!existsInIndex) {
8562
+ return { missing: true }
8563
+ }
8564
+ const validation = await contentValidation.validateTaskById(normalizedId)
8565
+ if (!validation.valid) {
8566
+ return { invalid: validation }
8567
+ }
8568
+ const task = await taskPackages.readTaskPackageById(normalizedId)
8569
+ return {
8570
+ task: {
8571
+ ...task,
8572
+ ref: ""
8573
+ }
8574
+ }
8575
+ }
8576
+
8577
+ const normalizedRef = typeof ref === "string" ? taskPackages.normalizeTaskRef(ref) : ""
8578
+ if (!normalizedRef) {
8579
+ return { missing: true }
8580
+ }
8581
+ const match = await taskPackages.findTaskIndexEntryByRef(normalizedRef)
8582
+ if (!match || !match.entry) {
8583
+ return { missing: true }
8584
+ }
8585
+ const validation = await contentValidation.validateTaskById(match.entry.id)
8586
+ if (!validation.valid) {
8587
+ return {
8588
+ invalid: {
8589
+ ...validation,
8590
+ detailUrl: buildTaskPath({ ref: match.ref })
8591
+ }
8592
+ }
8593
+ }
8594
+ const task = await taskPackages.readTaskPackageById(match.entry.id)
8595
+ return {
8596
+ task: {
8597
+ ...task,
8598
+ ref: match.ref
8599
+ }
8600
+ }
8601
+ }
8302
8602
  const renderTaskBuilderPage = async (req, res, options = {}) => {
8303
8603
  const defaults = options.defaults && typeof options.defaults === "object" ? options.defaults : {}
8304
8604
  const sidebarContext = await buildTaskSidebarContext()
@@ -9528,11 +9828,19 @@ class Server {
9528
9828
  return
9529
9829
  }
9530
9830
 
9531
- const task = await taskPackages.resolveTaskPackage({
9831
+ const resolvedTask = await resolveTaskForOpen({
9532
9832
  id: requestedId,
9533
9833
  ref: requestedRef
9534
9834
  })
9535
- if (!task) {
9835
+ if (resolvedTask.invalid) {
9836
+ await this.renderInvalidContentPage(req, res, resolvedTask.invalid, {
9837
+ sidebarSelected: "tasks",
9838
+ backHref: "/tasks",
9839
+ backLabel: "Back to Tasks",
9840
+ })
9841
+ return
9842
+ }
9843
+ if (!resolvedTask.task) {
9536
9844
  if (requestedId && !requestedRef) {
9537
9845
  res.status(404).send("Task not found.")
9538
9846
  return
@@ -9546,6 +9854,7 @@ class Server {
9546
9854
  return
9547
9855
  }
9548
9856
 
9857
+ const task = resolvedTask.task
9549
9858
  await renderTaskLaunchPage(req, res, task, {
9550
9859
  selectedTool,
9551
9860
  inputValues,
@@ -9589,14 +9898,23 @@ class Server {
9589
9898
  return
9590
9899
  }
9591
9900
 
9592
- const task = await taskPackages.resolveTaskPackage({
9901
+ const resolvedTask = await resolveTaskForOpen({
9593
9902
  id: requestedId,
9594
9903
  ref: requestedRef
9595
9904
  })
9596
- if (!task) {
9905
+ if (resolvedTask.invalid) {
9906
+ await this.renderInvalidContentPage(req, res, resolvedTask.invalid, {
9907
+ sidebarSelected: "tasks",
9908
+ backHref: "/tasks",
9909
+ backLabel: "Back to Tasks",
9910
+ })
9911
+ return
9912
+ }
9913
+ if (!resolvedTask.task) {
9597
9914
  res.status(404).send("Task not found.")
9598
9915
  return
9599
9916
  }
9917
+ const task = resolvedTask.task
9600
9918
 
9601
9919
  const missingRequired = task.inputs.filter((input) => {
9602
9920
  if (!input.required) {
@@ -10238,20 +10556,6 @@ class Server {
10238
10556
  folderName: requestedFolderName,
10239
10557
  ref
10240
10558
  })
10241
- const installRoot = intent === "create_plugin" ? "plugin" : "api"
10242
- const validation = await installValidation.validateInstalledDirectory({
10243
- absolutePath: targetPath,
10244
- installRoot
10245
- })
10246
- if (!validation.valid) {
10247
- await fs.promises.rm(targetPath, { recursive: true, force: true }).catch(() => {})
10248
- res.status(422).json({
10249
- ok: false,
10250
- error: validation.message || "Downloaded content is invalid.",
10251
- validation
10252
- })
10253
- return
10254
- }
10255
10559
 
10256
10560
  if (intent === "create_app") {
10257
10561
  res.json({
@@ -10270,8 +10574,48 @@ class Server {
10270
10574
  const status = Number.isInteger(error && error.status) ? error.status : 500
10271
10575
  res.status(status).json({
10272
10576
  ok: false,
10273
- error: error && error.message ? error.message : "Failed to download from Git URL.",
10274
- validation: error && error.validation ? error.validation : null
10577
+ error: error && error.message ? error.message : "Failed to download from Git URL."
10578
+ })
10579
+ }
10580
+ }))
10581
+ this.app.post("/launcher/download/prepare", ex(async (req, res) => {
10582
+ try {
10583
+ const body = req.body && typeof req.body === "object" ? req.body : {}
10584
+ const prepared = await prepareLauncherDownload({
10585
+ intent: typeof body.intent === "string" ? body.intent : "",
10586
+ ref: typeof body.ref === "string" ? body.ref : "",
10587
+ name: typeof body.name === "string" ? body.name : ""
10588
+ })
10589
+ res.json({
10590
+ ok: true,
10591
+ ...prepared
10592
+ })
10593
+ } catch (error) {
10594
+ const status = Number.isInteger(error && error.status) ? error.status : 500
10595
+ res.status(status).json({
10596
+ ok: false,
10597
+ error: error && error.message ? error.message : "Failed to prepare download."
10598
+ })
10599
+ }
10600
+ }))
10601
+ this.app.post("/launcher/download/finalize", ex(async (req, res) => {
10602
+ try {
10603
+ const body = req.body && typeof req.body === "object" ? req.body : {}
10604
+ const finalized = await finalizeLauncherDownload({
10605
+ intent: typeof body.intent === "string" ? body.intent : "",
10606
+ ref: typeof body.ref === "string" ? body.ref : "",
10607
+ name: typeof body.name === "string" ? body.name : "",
10608
+ id: typeof body.id === "string" ? body.id : ""
10609
+ })
10610
+ res.json({
10611
+ ok: true,
10612
+ ...finalized
10613
+ })
10614
+ } catch (error) {
10615
+ const status = Number.isInteger(error && error.status) ? error.status : 500
10616
+ res.status(status).json({
10617
+ ok: false,
10618
+ error: error && error.message ? error.message : "Failed to finalize download."
10275
10619
  })
10276
10620
  }
10277
10621
  }))
@@ -13078,6 +13422,17 @@ class Server {
13078
13422
  }
13079
13423
  }))
13080
13424
  this.app.get("/initialize/:name", ex(async (req, res) => {
13425
+ if (this.contentValidation) {
13426
+ const validation = await this.contentValidation.validateAppByName(req.params.name)
13427
+ if (validation && !validation.valid) {
13428
+ await this.renderInvalidContentPage(req, res, validation, {
13429
+ sidebarSelected: "home",
13430
+ backHref: "/home",
13431
+ backLabel: "Back to Home",
13432
+ })
13433
+ return
13434
+ }
13435
+ }
13081
13436
  let launcher = await this.kernel.api.launcher(req.params.name)
13082
13437
  let config = launcher.script
13083
13438
  if (config) {
@@ -13884,7 +14239,6 @@ class Server {
13884
14239
  } else {
13885
14240
  config = { menu: [] }
13886
14241
  }
13887
- err = e.stack
13888
14242
  }
13889
14243
  await this.renderMenu(req, uri, name, config, [])
13890
14244
 
@@ -14359,7 +14713,12 @@ class Server {
14359
14713
  res.json(mem)
14360
14714
  }))
14361
14715
  this.app.post("/pinokio/tabs", ex(async (req, res) => {
14362
- this.tabs[req.body.name] = req.body.tabs
14716
+ const workspaceName = typeof req.body.name === "string" ? req.body.name : ""
14717
+ const viewName = typeof req.body.view === "string" ? req.body.view : ""
14718
+ const storageKey = workspaceName && viewName ? `${workspaceName}:${viewName}` : workspaceName
14719
+ if (storageKey) {
14720
+ this.tabs[storageKey] = req.body.tabs
14721
+ }
14363
14722
  res.json({ success: true })
14364
14723
  }))
14365
14724
  this.app.get("/pinokio/browser", ex(async (req, res) => {
@@ -14680,40 +15039,8 @@ class Server {
14680
15039
  res.redirect("/pinokio/install")
14681
15040
  }))
14682
15041
  this.app.post("/pinokio/install/validate", ex(async (req, res) => {
14683
- const body = req.body && typeof req.body === "object" ? req.body : {}
14684
- const normalizedPath = installValidation.normalizeRelativeInstallPath(
14685
- typeof body.relativePath === "string" ? body.relativePath : "",
14686
- "api"
14687
- )
14688
- const normalizedFolderName = installValidation.validateInstallFolderName(
14689
- typeof body.folderName === "string" ? body.folderName : ""
14690
- )
14691
-
14692
- if (!normalizedPath || !normalizedFolderName) {
14693
- return res.status(400).json({
14694
- ok: false,
14695
- error: "Invalid install destination."
14696
- })
14697
- }
14698
-
14699
- const validation = await installValidation.validateInstalledFolder({
14700
- relativePath: normalizedPath,
14701
- folderName: normalizedFolderName,
14702
- fallbackRoot: "api"
14703
- })
14704
- if (!validation.valid) {
14705
- const targetPath = path.resolve(this.kernel.homedir, normalizedPath, normalizedFolderName)
14706
- await fs.promises.rm(targetPath, { recursive: true, force: true }).catch(() => {})
14707
- return res.status(422).json({
14708
- ok: false,
14709
- error: validation.message || "Downloaded content is invalid.",
14710
- validation
14711
- })
14712
- }
14713
-
14714
15042
  res.json({
14715
- ok: true,
14716
- validation
15043
+ ok: true
14717
15044
  })
14718
15045
  }))
14719
15046
  this.app.get("/pinokio/install", ex((req, res) => {