pinokiod 7.3.0 → 7.3.3

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.
Files changed (125) hide show
  1. package/kernel/api/github/index.js +444 -0
  2. package/kernel/api/index.js +199 -11
  3. package/kernel/api/process/index.js +124 -44
  4. package/kernel/api/shell_run_template.js +273 -0
  5. package/kernel/api/uri/index.js +51 -0
  6. package/kernel/bin/{conda-python.js → conda-pins.js} +23 -0
  7. package/kernel/bin/conda.js +15 -5
  8. package/kernel/bin/git.js +9 -10
  9. package/kernel/bin/huggingface.js +1 -1
  10. package/kernel/bin/index.js +5 -2
  11. package/kernel/bin/zip.js +9 -1
  12. package/kernel/connect/providers/github/README.md +5 -4
  13. package/kernel/environment.js +195 -92
  14. package/kernel/git.js +98 -19
  15. package/kernel/gitconfig_template +7 -0
  16. package/kernel/gpu/amd.js +72 -0
  17. package/kernel/gpu/apple.js +8 -0
  18. package/kernel/gpu/common.js +12 -0
  19. package/kernel/gpu/intel.js +47 -0
  20. package/kernel/gpu/nvidia.js +8 -0
  21. package/kernel/index.js +11 -1
  22. package/kernel/managed_skills.js +871 -0
  23. package/kernel/plugin.js +6 -58
  24. package/kernel/plugin_sources.js +316 -0
  25. package/kernel/resource_usage/gpu.js +349 -0
  26. package/kernel/resource_usage/index.js +322 -0
  27. package/kernel/resource_usage/macos_footprint.js +197 -0
  28. package/kernel/resource_usage/preferences.js +92 -0
  29. package/kernel/resource_usage/process_tree.js +303 -0
  30. package/kernel/scripts/git/create +4 -4
  31. package/kernel/scripts/git/fork +7 -8
  32. package/kernel/shell.js +23 -2
  33. package/kernel/shells.js +41 -0
  34. package/kernel/sysinfo.js +62 -9
  35. package/kernel/util.js +60 -0
  36. package/package.json +1 -1
  37. package/server/index.js +984 -156
  38. package/server/lib/app_log_report.js +543 -0
  39. package/server/lib/content_validation.js +55 -33
  40. package/server/lib/launcher_instruction_bootstrap.js +4 -96
  41. package/server/lib/terminal_session_helpers.js +0 -3
  42. package/server/public/common.js +77 -31
  43. package/server/public/create-launcher.js +4 -32
  44. package/server/public/logs.js +1428 -0
  45. package/server/public/nav.js +7 -0
  46. package/server/public/plugin-detail.js +93 -10
  47. package/server/public/privacy_filter_worker.js +391 -0
  48. package/server/public/style.css +1104 -154
  49. package/server/public/task-launcher.js +8 -29
  50. package/server/public/universal-launcher.css +8 -6
  51. package/server/public/universal-launcher.js +3 -27
  52. package/server/routes/apps.js +195 -1
  53. package/server/views/app.ejs +3041 -717
  54. package/server/views/autolaunch.ejs +917 -0
  55. package/server/views/bootstrap.ejs +7 -1
  56. package/server/views/d.ejs +408 -65
  57. package/server/views/editor.ejs +85 -19
  58. package/server/views/index.ejs +661 -111
  59. package/server/views/init/index.ejs +1 -1
  60. package/server/views/install.ejs +1 -1
  61. package/server/views/logs.ejs +164 -86
  62. package/server/views/net.ejs +7 -1
  63. package/server/views/partials/d_terminal_column.ejs +2 -2
  64. package/server/views/partials/d_terminal_options.ejs +0 -8
  65. package/server/views/partials/fs_status.ejs +47 -0
  66. package/server/views/partials/home_action_modal.ejs +86 -0
  67. package/server/views/partials/home_run_menu.ejs +87 -0
  68. package/server/views/partials/main_sidebar.ejs +2 -0
  69. package/server/views/partials/menu.ejs +1 -1
  70. package/server/views/plugin_detail.ejs +19 -4
  71. package/server/views/plugins.ejs +201 -3
  72. package/server/views/pre.ejs +1 -1
  73. package/server/views/pro.ejs +1 -1
  74. package/server/views/shell.ejs +40 -18
  75. package/server/views/skills.ejs +506 -0
  76. package/server/views/terminal.ejs +45 -19
  77. package/spec/INSTRUCTION_SYNC.md +20 -10
  78. package/system/plugin/antigravity-cli/antigravity.png +0 -0
  79. package/system/plugin/antigravity-cli/common.js +155 -0
  80. package/system/plugin/antigravity-cli/install.js +272 -0
  81. package/system/plugin/antigravity-cli/pinokio.js +13 -0
  82. package/system/plugin/antigravity-cli-auto/antigravity.png +0 -0
  83. package/system/plugin/antigravity-cli-auto/pinokio.js +13 -0
  84. package/system/plugin/claude/claude.png +0 -0
  85. package/system/plugin/claude/pinokio.js +47 -0
  86. package/system/plugin/claude-auto/claude.png +0 -0
  87. package/system/plugin/claude-auto/pinokio.js +58 -0
  88. package/system/plugin/claude-desktop/icon.jpeg +0 -0
  89. package/system/plugin/claude-desktop/pinokio.js +23 -0
  90. package/system/plugin/codex/openai.webp +0 -0
  91. package/system/plugin/codex/pinokio.js +42 -0
  92. package/system/plugin/codex-auto/openai.webp +0 -0
  93. package/system/plugin/codex-auto/pinokio.js +49 -0
  94. package/system/plugin/codex-desktop/icon.png +0 -0
  95. package/system/plugin/codex-desktop/pinokio.js +23 -0
  96. package/system/plugin/crush/crush.png +0 -0
  97. package/system/plugin/crush/pinokio.js +15 -0
  98. package/system/plugin/cursor/cursor.jpeg +0 -0
  99. package/system/plugin/cursor/pinokio.js +23 -0
  100. package/system/plugin/qwen/pinokio.js +34 -0
  101. package/system/plugin/qwen/qwen.png +0 -0
  102. package/system/plugin/vscode/pinokio.js +20 -0
  103. package/system/plugin/vscode/vscode.png +0 -0
  104. package/system/plugin/windsurf/pinokio.js +23 -0
  105. package/system/plugin/windsurf/windsurf.png +0 -0
  106. package/test/antigravity-cli-plugin.test.js +185 -0
  107. package/test/app-api.test.js +239 -0
  108. package/test/app-log-report.test.js +67 -0
  109. package/test/environment-cache-preflight.test.js +98 -0
  110. package/test/git-bin.test.js +59 -0
  111. package/test/git-defaults.test.js +97 -0
  112. package/test/github-api.test.js +158 -0
  113. package/test/github-connection.test.js +117 -0
  114. package/test/huggingface-bin.test.js +25 -0
  115. package/test/managed-skills.test.js +351 -0
  116. package/test/plugin-action-functions.test.js +337 -0
  117. package/test/plugin-dev-iframe.test.js +17 -0
  118. package/test/plugin-sources.test.js +203 -0
  119. package/test/privacy-filter-worker-heuristics.test.js +69 -0
  120. package/test/process-wait.test.js +169 -0
  121. package/test/script-api.test.js +97 -0
  122. package/test/shell-api.test.js +134 -0
  123. package/test/shell-run-template.test.js +209 -0
  124. package/test/storage-api.test.js +137 -0
  125. package/test/uri-api.test.js +100 -0
package/server/index.js CHANGED
@@ -30,6 +30,8 @@ const registerFileRoutes = require('./routes/files')
30
30
  const registerAppRoutes = require('./routes/apps')
31
31
  const Git = require("../kernel/git")
32
32
  const TerminalApi = require('../kernel/api/terminal')
33
+ const PluginSources = require("../kernel/plugin_sources")
34
+ const ManagedSkills = require("../kernel/managed_skills")
33
35
 
34
36
  const git = require('isomorphic-git')
35
37
  const http = require('isomorphic-git/http/node')
@@ -82,8 +84,10 @@ const { createContentValidationService } = require("./lib/content_validation")
82
84
  const { buildSecureRouterDebugSnapshot, createSecureRouterDebugStore } = require("./lib/secure_router_debug")
83
85
  const AppRegistryService = require("./lib/app_registry")
84
86
  const AppLogService = require("./lib/app_logs")
87
+ const AppLogReportService = require("./lib/app_log_report")
85
88
  const AppSearchService = require("./lib/app_search")
86
89
  const AppPreferencesService = require("./lib/app_preferences")
90
+ const ResourceUsageService = require("../kernel/resource_usage")
87
91
 
88
92
  function normalize(str) {
89
93
  if (!str) return '';
@@ -214,7 +218,9 @@ class Server {
214
218
  this.appRegistry = new AppRegistryService({ kernel: this.kernel })
215
219
  this.appPreferences = new AppPreferencesService({ kernel: this.kernel })
216
220
  this.kernel.appPreferences = this.appPreferences
221
+ this.resourceUsage = new ResourceUsageService({ kernel: this.kernel })
217
222
  this.appLogs = new AppLogService({ registry: this.appRegistry })
223
+ this.appLogReports = new AppLogReportService({ registry: this.appRegistry, kernel: this.kernel })
218
224
  this.appSearch = new AppSearchService({
219
225
  kernel: this.kernel,
220
226
  registry: this.appRegistry,
@@ -491,14 +497,12 @@ class Server {
491
497
  assignProjectSlug(obj)
492
498
  running_dynamic.push(obj)
493
499
  }
494
- } else if (href.startsWith("/run")) {
495
- let uri_path = new URL("http://localhost" + href).pathname
496
- let _filepath = uri_path.split("/").filter(x=>x).slice(1)
497
- let filepath = this.kernel.path(..._filepath)
500
+ } else if (PluginSources.isRunPath(href)) {
501
+ let filepath = PluginSources.resolveRunPath(this.kernel, href)
498
502
  let id = `${filepath}?cwd=${cwd}`
499
503
  obj.script_id = id
500
504
  //if (this.kernel.api.running[filepath]) {
501
- if (obj.src.startsWith("/run" + selected_query.plugin)) {
505
+ if (PluginSources.pluginSelectionMatches(obj.src, selected_query && selected_query.plugin)) {
502
506
  obj.running = true
503
507
  obj.display = "indent"
504
508
  obj.default = true
@@ -920,15 +924,8 @@ class Server {
920
924
 
921
925
  const config = await this.kernel.git.config(dir)
922
926
 
923
- let hosts = ""
924
- const hostsFile = this.kernel.path("config/gh/hosts.yml")
925
- if (await this.exists(hostsFile)) {
926
- hosts = await fs.promises.readFile(hostsFile, "utf8")
927
- if (hosts.startsWith("{}")) {
928
- hosts = ""
929
- }
930
- }
931
- const connected = hosts.length > 0
927
+ const githubConnection = await this.get_github_connection()
928
+ const connected = Boolean(githubConnection.connected)
932
929
 
933
930
  let remote = null
934
931
  if (config && config["remote \"origin\""]) {
@@ -990,6 +987,10 @@ class Server {
990
987
  }
991
988
  }
992
989
  async get_github_hosts() {
990
+ const connection = await this.get_github_connection()
991
+ return connection.display
992
+ }
993
+ async get_legacy_github_hosts() {
993
994
  let hosts = ""
994
995
  let hosts_file = this.kernel.path("config/gh/hosts.yml")
995
996
  let e = await this.exists(hosts_file)
@@ -1001,6 +1002,135 @@ class Server {
1001
1002
  }
1002
1003
  return hosts
1003
1004
  }
1005
+ github_command_env(interactive = false) {
1006
+ const env = this.kernel && this.kernel.envs
1007
+ ? { ...this.kernel.envs }
1008
+ : (this.kernel && this.kernel.bin && typeof this.kernel.bin.envs === 'function'
1009
+ ? this.kernel.bin.envs(process.env)
1010
+ : { ...process.env })
1011
+
1012
+ if (interactive) {
1013
+ delete env.GCM_INTERACTIVE
1014
+ delete env.GIT_TERMINAL_PROMPT
1015
+ delete env.GIT_ASKPASS
1016
+ delete env.SSH_ASKPASS
1017
+ } else {
1018
+ Object.assign(env, NON_INTERACTIVE_GIT_ENV)
1019
+ }
1020
+
1021
+ return env
1022
+ }
1023
+ async github_gcm(args, options = {}) {
1024
+ return new Promise((resolve, reject) => {
1025
+ execFile(
1026
+ 'git',
1027
+ ['credential-manager', 'github', ...args],
1028
+ {
1029
+ cwd: this.kernel.homedir,
1030
+ env: this.github_command_env(Boolean(options.interactive)),
1031
+ timeout: Number.isFinite(options.timeout) ? options.timeout : 15000,
1032
+ maxBuffer: 1024 * 1024,
1033
+ },
1034
+ (error, stdout, stderr) => {
1035
+ if (error) {
1036
+ error.stderr = stderr
1037
+ reject(error)
1038
+ return
1039
+ }
1040
+ resolve(stdout || "")
1041
+ }
1042
+ )
1043
+ })
1044
+ }
1045
+ async get_github_connection() {
1046
+ if (this._githubConnectionPromise) {
1047
+ return this._githubConnectionPromise
1048
+ }
1049
+
1050
+ this._githubConnectionPromise = (async () => {
1051
+ const legacyHosts = await this.get_legacy_github_hosts()
1052
+ let accounts = []
1053
+ let gcmError = null
1054
+
1055
+ try {
1056
+ const stdout = await this.github_gcm(['list'], { timeout: 10000 })
1057
+ accounts = stdout
1058
+ .split(/\r?\n/)
1059
+ .map((line) => line.trim())
1060
+ .filter(Boolean)
1061
+ } catch (error) {
1062
+ gcmError = error && (error.stderr || error.message) ? String(error.stderr || error.message).trim() : String(error)
1063
+ }
1064
+
1065
+ const display = accounts.length > 0
1066
+ ? accounts.map((account) => `github.com: ${account}`).join("\n")
1067
+ : ""
1068
+
1069
+ return {
1070
+ accounts,
1071
+ legacyHosts,
1072
+ display,
1073
+ connected: accounts.length > 0,
1074
+ provider: accounts.length > 0 ? "gcm" : null,
1075
+ gcmError,
1076
+ }
1077
+ })()
1078
+
1079
+ try {
1080
+ return await this._githubConnectionPromise
1081
+ } finally {
1082
+ this._githubConnectionPromise = null
1083
+ }
1084
+ }
1085
+ github_login_params() {
1086
+ const doneMarker = "PINOKIO_GITHUB_LOGIN_DONE"
1087
+ const delimiter = this.kernel.platform === "win32" ? " && " : " ; "
1088
+ const verifyCommand = this.kernel.platform === "win32"
1089
+ ? "cmd /C \"set GIT_TERMINAL_PROMPT=0&& set GCM_INTERACTIVE=never&& (echo protocol=https& echo host=github.com& echo.) | git credential fill >NUL\""
1090
+ : "printf 'protocol=https\\nhost=github.com\\n\\n' | GIT_TERMINAL_PROMPT=0 GCM_INTERACTIVE=never git credential fill >/dev/null"
1091
+ const doneCommand = this.kernel.platform === "win32"
1092
+ ? "echo P^INOKIO_GITHUB_LOGIN_DONE"
1093
+ : "(GCM_DONE=INOKIO_GITHUB_LOGIN_DONE; printf 'P%s\\n' \"$GCM_DONE\")"
1094
+ const loginCommand = [
1095
+ "git credential-manager github login --web --force",
1096
+ verifyCommand,
1097
+ doneCommand
1098
+ ].join(" && ")
1099
+
1100
+ return {
1101
+ doneMarker,
1102
+ message: [
1103
+ "git config --global --replace-all credential.helper manager",
1104
+ "git config --global --replace-all credential.gitHubAuthModes oauth",
1105
+ "git config --global --replace-all credential.namespace pinokio",
1106
+ "git config --global --replace-all credential.https://github.com.helper manager",
1107
+ "git config --global --replace-all credential.https://github.com.provider github",
1108
+ loginCommand
1109
+ ].join(delimiter)
1110
+ }
1111
+ }
1112
+ github_logout_command(connection) {
1113
+ const accounts = connection && Array.isArray(connection.accounts) ? connection.accounts : []
1114
+ const safeAccounts = accounts.filter((account) => /^[A-Za-z0-9._-]+$/.test(account))
1115
+ if (safeAccounts.length === 0) {
1116
+ return null
1117
+ }
1118
+ const delimiter = this.kernel.platform === "win32" ? " && " : " ; "
1119
+ return safeAccounts.map((account) => `git credential-manager github logout ${account}`).join(delimiter)
1120
+ }
1121
+ async github_logout_params(connection) {
1122
+ const hadLegacyAuth = Boolean(connection && connection.legacyHosts && connection.legacyHosts.length > 0)
1123
+ if (hadLegacyAuth) {
1124
+ await this.clear_legacy_github_auth()
1125
+ }
1126
+ return {
1127
+ hadLegacyAuth,
1128
+ message: this.github_logout_command(connection)
1129
+ }
1130
+ }
1131
+ async clear_legacy_github_auth() {
1132
+ await fs.promises.rm(this.kernel.path("config/gh/hosts.yml"), { force: true }).catch(() => {})
1133
+ }
1004
1134
  async current_urls(current_path) {
1005
1135
  return {}
1006
1136
  // let router_running = await this.check_router_up()
@@ -1037,6 +1167,296 @@ class Server {
1037
1167
  list: this.getPeers(),
1038
1168
  }
1039
1169
  }
1170
+ normalizeAutolaunchAppId(value) {
1171
+ if (typeof value !== "string") {
1172
+ return ""
1173
+ }
1174
+ const id = value.trim()
1175
+ if (!id || id === "." || id === ".." || id.includes("\0") || /[\\/]/.test(id)) {
1176
+ return ""
1177
+ }
1178
+ return id
1179
+ }
1180
+ normalizeAutolaunchScriptPath(value) {
1181
+ if (typeof value !== "string") {
1182
+ return ""
1183
+ }
1184
+ let script = value.trim().replace(/\\/g, "/")
1185
+ if (!script || script.includes("\0")) {
1186
+ return ""
1187
+ }
1188
+ script = script.split("#")[0].split("?")[0].trim()
1189
+ if (!script || /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(script) || script.startsWith("//")) {
1190
+ return ""
1191
+ }
1192
+ if (script.startsWith("/")) {
1193
+ return ""
1194
+ }
1195
+ const normalized = path.posix.normalize(script).replace(/^\.\/+/, "")
1196
+ if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../") || path.posix.isAbsolute(normalized)) {
1197
+ return ""
1198
+ }
1199
+ return normalized
1200
+ }
1201
+ isAutolaunchScriptFilename(filename) {
1202
+ const ext = path.extname(filename || "").toLowerCase()
1203
+ if (![".js", ".json", ".mjs", ".cjs"].includes(ext)) {
1204
+ return false
1205
+ }
1206
+ const base = path.basename(filename || "").toLowerCase()
1207
+ return !["package.json", "pinokio.js", "pinokio.json", "pinokio_meta.json"].includes(base)
1208
+ }
1209
+ stripAutolaunchLabel(value) {
1210
+ if (typeof value !== "string") {
1211
+ return ""
1212
+ }
1213
+ return value
1214
+ .replace(/<[^>]*>/g, " ")
1215
+ .replace(/\s+/g, " ")
1216
+ .trim()
1217
+ }
1218
+ async getAutolaunchAppById(appId) {
1219
+ const id = this.normalizeAutolaunchAppId(appId)
1220
+ if (!id) {
1221
+ return null
1222
+ }
1223
+ const apps = await this.kernel.api.listApps()
1224
+ return apps.find((app) => app && app.id === id) || null
1225
+ }
1226
+ async getAutolaunchEnvInfo(app) {
1227
+ const appRoot = path.resolve(this.kernel.api.userdir, app.id)
1228
+ const gotRoot = await Environment.get_root({ path: appRoot }, this.kernel)
1229
+ const envRoot = gotRoot && gotRoot.root ? gotRoot.root : appRoot
1230
+ const envPath = path.resolve(envRoot, "ENVIRONMENT")
1231
+ const env = await Util.parse_env(envPath)
1232
+ const value = typeof env.PINOKIO_SCRIPT_AUTOLAUNCH === "string"
1233
+ ? env.PINOKIO_SCRIPT_AUTOLAUNCH.trim()
1234
+ : ""
1235
+ const exists = await this.exists(envPath)
1236
+ return {
1237
+ appRoot,
1238
+ envRoot,
1239
+ envPath,
1240
+ envRelpath: gotRoot && gotRoot.relpath ? gotRoot.relpath : "",
1241
+ exists,
1242
+ value,
1243
+ enabled: value.length > 0
1244
+ }
1245
+ }
1246
+ async buildAutolaunchAppState(app) {
1247
+ const envInfo = await this.getAutolaunchEnvInfo(app)
1248
+ return {
1249
+ id: app.id,
1250
+ name: app.name,
1251
+ title: app.title || app.name || app.id,
1252
+ description: app.description || "",
1253
+ icon: app.icon || "/pinokio-black.png",
1254
+ workspace_path: app.workspace_path,
1255
+ launcher_path: app.launcher_path,
1256
+ launcher_root: app.launcher_root || "",
1257
+ env_path: envInfo.envPath,
1258
+ autolaunch: envInfo.value,
1259
+ autolaunch_enabled: envInfo.enabled
1260
+ }
1261
+ }
1262
+ async buildAutolaunchAppsState() {
1263
+ const apps = await this.kernel.api.listApps()
1264
+ const states = []
1265
+ for (const app of apps) {
1266
+ states.push(await this.buildAutolaunchAppState(app))
1267
+ }
1268
+ return states
1269
+ }
1270
+ async resolveAutolaunchScript(appRoot, script) {
1271
+ const normalized = this.normalizeAutolaunchScriptPath(script)
1272
+ if (!normalized || !this.isAutolaunchScriptFilename(normalized)) {
1273
+ return null
1274
+ }
1275
+ const scriptPath = path.resolve(appRoot, normalized)
1276
+ if (!this.is_subpath(appRoot, scriptPath)) {
1277
+ return null
1278
+ }
1279
+ let stat
1280
+ try {
1281
+ stat = await fs.promises.stat(scriptPath)
1282
+ } catch (_) {
1283
+ return null
1284
+ }
1285
+ if (!stat || !stat.isFile()) {
1286
+ return null
1287
+ }
1288
+ return {
1289
+ script: normalized,
1290
+ path: scriptPath
1291
+ }
1292
+ }
1293
+ flattenAutolaunchMenu(menu, trail = []) {
1294
+ const items = []
1295
+ if (!Array.isArray(menu)) {
1296
+ return items
1297
+ }
1298
+ for (const menuitem of menu) {
1299
+ if (!menuitem || typeof menuitem !== "object") {
1300
+ continue
1301
+ }
1302
+ const label = this.stripAutolaunchLabel(menuitem.text || menuitem.name || menuitem.html || "")
1303
+ const nextTrail = label ? trail.concat(label) : trail
1304
+ if (Array.isArray(menuitem.menu)) {
1305
+ items.push(...this.flattenAutolaunchMenu(menuitem.menu, nextTrail))
1306
+ } else {
1307
+ items.push({ item: menuitem, group: trail.join(" / ") })
1308
+ }
1309
+ }
1310
+ return items
1311
+ }
1312
+ async addAutolaunchCandidate(candidates, seen, candidate, appRoot) {
1313
+ const resolved = await this.resolveAutolaunchScript(appRoot, candidate.script)
1314
+ if (!resolved || seen.has(resolved.script)) {
1315
+ return null
1316
+ }
1317
+ seen.add(resolved.script)
1318
+ const label = this.stripAutolaunchLabel(candidate.label || "") || resolved.script
1319
+ const menuDefault = !!candidate.menu_default
1320
+ const item = {
1321
+ script: resolved.script,
1322
+ label,
1323
+ group: candidate.group || "",
1324
+ icon: candidate.icon || "",
1325
+ source: candidate.source || "local",
1326
+ menu_default: menuDefault,
1327
+ has_params: !!candidate.has_params
1328
+ }
1329
+ candidates.push(item)
1330
+ return item
1331
+ }
1332
+ async collectAutolaunchScriptFiles(root, appRoot) {
1333
+ const results = []
1334
+ const ignoredDirs = new Set([
1335
+ ".git",
1336
+ ".venv",
1337
+ "__pycache__",
1338
+ "app",
1339
+ "cache",
1340
+ "data",
1341
+ "env",
1342
+ "logs",
1343
+ "models",
1344
+ "node_modules",
1345
+ "output",
1346
+ "outputs",
1347
+ "venv"
1348
+ ])
1349
+ const maxResults = 500
1350
+ const walk = async (dir, depth) => {
1351
+ if (depth > 4 || results.length >= maxResults) {
1352
+ return
1353
+ }
1354
+ let entries
1355
+ try {
1356
+ entries = await fs.promises.readdir(dir, { withFileTypes: true })
1357
+ } catch (_) {
1358
+ return
1359
+ }
1360
+ for (const entry of entries) {
1361
+ if (!entry || !entry.name || entry.name.includes("\0")) {
1362
+ continue
1363
+ }
1364
+ const fullPath = path.resolve(dir, entry.name)
1365
+ if (entry.isDirectory()) {
1366
+ if (!ignoredDirs.has(entry.name)) {
1367
+ await walk(fullPath, depth + 1)
1368
+ }
1369
+ continue
1370
+ }
1371
+ if (!entry.isFile() || !this.isAutolaunchScriptFilename(entry.name)) {
1372
+ continue
1373
+ }
1374
+ if (!this.is_subpath(appRoot, fullPath)) {
1375
+ continue
1376
+ }
1377
+ const rel = path.relative(appRoot, fullPath).split(path.sep).join("/")
1378
+ results.push(rel)
1379
+ }
1380
+ }
1381
+ await walk(root, 0)
1382
+ results.sort((a, b) => a.localeCompare(b))
1383
+ return results
1384
+ }
1385
+ async buildAutolaunchCandidates(app) {
1386
+ const envInfo = await this.getAutolaunchEnvInfo(app)
1387
+ const appRoot = envInfo.appRoot
1388
+ const launcher = await this.kernel.api.launcher(app.id)
1389
+ const launcherRoot = launcher && launcher.launcher_root
1390
+ ? path.resolve(appRoot, launcher.launcher_root)
1391
+ : appRoot
1392
+ const menuCandidates = []
1393
+ const otherCandidates = []
1394
+ const seen = new Set()
1395
+
1396
+ try {
1397
+ let config = await this.kernel.api.meta(app.id)
1398
+ config = await this.processMenu(app.id, safeStructuredClone(config || {}))
1399
+ const flat = this.flattenAutolaunchMenu(config && config.menu ? config.menu : [])
1400
+ for (const entry of flat) {
1401
+ const menuitem = entry.item
1402
+ if (!menuitem || typeof menuitem.href !== "string") {
1403
+ continue
1404
+ }
1405
+ let href = menuitem.href.trim()
1406
+ const apiPrefix = `/api/${app.id}/`
1407
+ if (href.startsWith(apiPrefix)) {
1408
+ href = href.slice(apiPrefix.length)
1409
+ } else if (href.startsWith("/")) {
1410
+ continue
1411
+ }
1412
+ const localScript = this.normalizeAutolaunchScriptPath(href)
1413
+ if (!localScript) {
1414
+ continue
1415
+ }
1416
+ const scriptPath = path.resolve(launcherRoot, localScript)
1417
+ if (!this.is_subpath(appRoot, scriptPath)) {
1418
+ continue
1419
+ }
1420
+ const script = path.relative(appRoot, scriptPath).split(path.sep).join("/")
1421
+ await this.addAutolaunchCandidate(menuCandidates, seen, {
1422
+ script,
1423
+ label: menuitem.text || menuitem.name || script,
1424
+ group: entry.group,
1425
+ icon: typeof menuitem.icon === "string" ? menuitem.icon : "",
1426
+ source: "menu",
1427
+ menu_default: !!menuitem.default,
1428
+ has_params: !!(menuitem.params && typeof menuitem.params === "object")
1429
+ }, appRoot)
1430
+ }
1431
+ } catch (error) {
1432
+ console.warn("[autolaunch] failed to resolve menu candidates", app.id, error && error.message ? error.message : error)
1433
+ }
1434
+
1435
+ if (envInfo.value) {
1436
+ await this.addAutolaunchCandidate(menuCandidates, seen, {
1437
+ script: envInfo.value,
1438
+ label: envInfo.value,
1439
+ source: "current"
1440
+ }, appRoot)
1441
+ }
1442
+
1443
+ const localScripts = await this.collectAutolaunchScriptFiles(launcherRoot, appRoot)
1444
+ for (const script of localScripts) {
1445
+ await this.addAutolaunchCandidate(otherCandidates, seen, {
1446
+ script,
1447
+ label: script,
1448
+ source: "local"
1449
+ }, appRoot)
1450
+ }
1451
+
1452
+ return {
1453
+ app: await this.buildAutolaunchAppState(app),
1454
+ launcher_root: path.relative(appRoot, launcherRoot).split(path.sep).join("/"),
1455
+ menu: menuCandidates,
1456
+ other: otherCandidates,
1457
+ current: envInfo.value
1458
+ }
1459
+ }
1040
1460
  async renderInvalidContentPage(req, res, invalid, options = {}) {
1041
1461
  const type = invalid && typeof invalid.type === "string" ? invalid.type : "app"
1042
1462
  const sidebarSelected = options.sidebarSelected || (type === "plugin" ? "plugins" : type === "task" ? "tasks" : "home")
@@ -1239,6 +1659,9 @@ class Server {
1239
1659
  let dev_tab = "/p/" + name + "/dev"
1240
1660
  let review_tab = "/p/" + name + "/review"
1241
1661
  let files_tab = "/p/" + name + "/files"
1662
+ const dev_initial_tab = type === "browse" && req.query && req.query.pinokio_dev_tab === "files"
1663
+ ? "files"
1664
+ : "plugins"
1242
1665
 
1243
1666
  const registryEnabled = await this.isRegistryEnabled().catch(() => false)
1244
1667
  let community_url = ""
@@ -1272,13 +1695,16 @@ class Server {
1272
1695
  }
1273
1696
 
1274
1697
  let dynamic_url = "/pinokio/dynamic/" + name;
1275
- if (Object.values(req.query).length > 0) {
1698
+ const dynamicQueryEntries = Object.entries(req.query || {}).filter(([key]) => {
1699
+ return key !== "pinokio_dev_tab"
1700
+ })
1701
+ if (dynamicQueryEntries.length > 0) {
1276
1702
  let index = 0
1277
- for(let key in req.query) {
1703
+ for (let [key, value] of dynamicQueryEntries) {
1278
1704
  if (index === 0) {
1279
- dynamic_url = dynamic_url + `?${key}=${encodeURIComponent(req.query[key])}`
1705
+ dynamic_url = dynamic_url + `?${key}=${encodeURIComponent(value)}`
1280
1706
  } else {
1281
- dynamic_url = dynamic_url + `&${key}=${encodeURIComponent(req.query[key])}`
1707
+ dynamic_url = dynamic_url + `&${key}=${encodeURIComponent(value)}`
1282
1708
  }
1283
1709
  index++;
1284
1710
  }
@@ -1286,6 +1712,22 @@ class Server {
1286
1712
  const protectionPreference = this.appPreferences && typeof this.appPreferences.getPreference === "function"
1287
1713
  ? await this.appPreferences.getPreference(name)
1288
1714
  : null
1715
+ let autolaunchAppState = null
1716
+ try {
1717
+ const appRoot = this.kernel.path("api", name)
1718
+ autolaunchAppState = await this.buildAutolaunchAppState({
1719
+ id: name,
1720
+ name,
1721
+ title: config && config.title ? config.title : name,
1722
+ description: config && config.description ? config.description : "",
1723
+ icon: config && config.icon ? config.icon : "/pinokio-black.png",
1724
+ workspace_path: appRoot,
1725
+ launcher_path: req.launcher_root ? path.resolve(appRoot, req.launcher_root) : appRoot,
1726
+ launcher_root: req.launcher_root || ""
1727
+ })
1728
+ } catch (error) {
1729
+ console.warn("[autolaunch] failed to build app page state", name, error && error.message ? error.message : error)
1730
+ }
1289
1731
 
1290
1732
  const result = {
1291
1733
  dev_link,
@@ -1317,8 +1759,10 @@ class Server {
1317
1759
  // feed,
1318
1760
  tabs: savedTabs,
1319
1761
  editor_tab: editor_tab,
1762
+ dev_initial_tab,
1320
1763
  config,
1321
1764
  protection_enabled: protectionPreference ? protectionPreference.protection_enabled !== false : false,
1765
+ autolaunch_app: autolaunchAppState,
1322
1766
  // sidebar_url: "/pinokio/sidebar/" + name,
1323
1767
  home: req.originalUrl,
1324
1768
  run_tab,
@@ -2091,15 +2535,8 @@ class Server {
2091
2535
 
2092
2536
  const config = await this.kernel.git.config(dir)
2093
2537
 
2094
- let hosts = ""
2095
- const hostsFile = this.kernel.path("config/gh/hosts.yml")
2096
- if (await this.exists(hostsFile)) {
2097
- hosts = await fs.promises.readFile(hostsFile, "utf8")
2098
- if (hosts.startsWith("{}")) {
2099
- hosts = ""
2100
- }
2101
- }
2102
- const connected = hosts.length > 0
2538
+ const githubConnection = await this.get_github_connection()
2539
+ const connected = Boolean(githubConnection.connected)
2103
2540
 
2104
2541
  let remote = null
2105
2542
  if (config && config["remote \"origin\""]) {
@@ -2393,8 +2830,10 @@ class Server {
2393
2830
  filepath = full_filepath
2394
2831
  }
2395
2832
 
2396
- if ((req.action || req.originalUrl.startsWith("/run/")) && this.contentValidation) {
2397
- const validation = await this.contentValidation.validateRunPath(pathComponents)
2833
+ if ((req.action || PluginSources.isRunPath(req.originalUrl)) && this.contentValidation) {
2834
+ const validation = await this.contentValidation.validateRunPath(pathComponents, {
2835
+ system: req.pinokioSystem === true,
2836
+ })
2398
2837
  if (validation && !validation.valid) {
2399
2838
  await this.renderInvalidContentPage(req, res, validation, {
2400
2839
  sidebarSelected: validation.type === "plugin" ? "plugins" : validation.type === "task" ? "tasks" : "home",
@@ -2753,10 +3192,12 @@ class Server {
2753
3192
  } else {
2754
3193
  resolved = runner(this.kernel, this.kernel.info)
2755
3194
  }
2756
- runnable = resolved && Array.isArray(resolved[actionKey]) && resolved[actionKey].length > 0
3195
+ const action = resolved ? resolved[actionKey] : null
3196
+ runnable = typeof action === "function" || (Array.isArray(action) && action.length > 0)
2757
3197
  } else {
2758
- runnable = runner && Array.isArray(runner[actionKey]) && runner[actionKey].length > 0
2759
3198
  resolved = runner
3199
+ const action = resolved ? resolved[actionKey] : null
3200
+ runnable = typeof action === "function" || (Array.isArray(action) && action.length > 0)
2760
3201
  }
2761
3202
 
2762
3203
  let template = "terminal"
@@ -2865,11 +3306,19 @@ class Server {
2865
3306
  const protectionPreference = protectionAppId && this.appPreferences && typeof this.appPreferences.getPreference === "function"
2866
3307
  ? await this.appPreferences.getPreference(protectionAppId)
2867
3308
  : null
3309
+ const activeProcessWait = this.kernel.activeProcessWaits && this.kernel.activeProcessWaits[filepath]
3310
+ ? this.kernel.activeProcessWaits[filepath]
3311
+ : null
2868
3312
  const result = {
2869
3313
  portal: this.portal,
2870
3314
  projectName: (pathComponents.length > 0 ? pathComponents[0] : ''),
2871
3315
  protection_app_id: protectionAppId,
2872
3316
  protection_enabled: protectionPreference ? protectionPreference.protection_enabled !== false : false,
3317
+ active_process_wait: activeProcessWait ? {
3318
+ title: activeProcessWait.title,
3319
+ description: activeProcessWait.description,
3320
+ message: activeProcessWait.message
3321
+ } : null,
2873
3322
  kill_message,
2874
3323
  callback,
2875
3324
  callback_target,
@@ -2884,6 +3333,7 @@ class Server {
2884
3333
  //run: true, // run mode by default
2885
3334
  run: (req.query && req.query.mode === "source" ? false : true),
2886
3335
  stop: (req.query && req.query.stop ? true : false),
3336
+ readonly: req.pinokioSystem === true,
2887
3337
  pinokioPath,
2888
3338
  action: actionKey,
2889
3339
  runnable,
@@ -3100,6 +3550,7 @@ class Server {
3100
3550
  if (pathComponents.length === 0) {
3101
3551
  const normalizedApiRoot = path.normalize(this.kernel.path("api"))
3102
3552
  const normalizedPluginRoot = path.normalize(this.kernel.path("plugin"))
3553
+ const normalizedSystemPluginRoot = path.normalize(PluginSources.systemPluginRoot(this.kernel))
3103
3554
  const isPathWithinRoot = (candidatePath, rootPath) => {
3104
3555
  if (typeof candidatePath !== "string" || typeof rootPath !== "string") {
3105
3556
  return false
@@ -3125,6 +3576,9 @@ class Server {
3125
3576
  if (isPathWithinRoot(normalizedCandidate, normalizedPluginRoot)) {
3126
3577
  return true
3127
3578
  }
3579
+ if (isPathWithinRoot(normalizedCandidate, normalizedSystemPluginRoot)) {
3580
+ return true
3581
+ }
3128
3582
  const relativeToApiRoot = path.relative(normalizedApiRoot, normalizedCandidate)
3129
3583
  if (!relativeToApiRoot || relativeToApiRoot.startsWith("..") || path.isAbsolute(relativeToApiRoot)) {
3130
3584
  return false
@@ -3138,6 +3592,8 @@ class Server {
3138
3592
  let item = items[i]
3139
3593
  let launcher = await this.kernel.api.launcher(item.name)
3140
3594
  let config = launcher.script
3595
+ req.launcher_root = launcher.launcher_root
3596
+ req.pinokioLauncher = launcher
3141
3597
  await this.kernel.dns({
3142
3598
  name: item.name,
3143
3599
  config
@@ -3145,6 +3601,13 @@ class Server {
3145
3601
 
3146
3602
 
3147
3603
  if (config) {
3604
+ try {
3605
+ config = await this.processMenu(item.name, config)
3606
+ await this.renderMenu(req, this.kernel.path("api"), item.name, config, [])
3607
+ items[i].menu = Array.isArray(config.menu) ? config.menu : []
3608
+ } catch (e) {
3609
+ items[i].menu = []
3610
+ }
3148
3611
 
3149
3612
  if (config.shortcuts) {
3150
3613
  if (typeof config.shortcuts === "function") {
@@ -3786,6 +4249,33 @@ class Server {
3786
4249
  menuitem.href = "/shell/" + shell_id + "?" + params.toString()
3787
4250
  let decoded_shell_id = decodeURIComponent(shell_id)
3788
4251
  const shellPrefixId = "shell/" + decoded_shell_id
4252
+ const appendShellIdentityParams = (href, activeShellId) => {
4253
+ if (!href || typeof href !== "string" || !activeShellId || typeof activeShellId !== "string") {
4254
+ return href
4255
+ }
4256
+ const shellQueryIndex = activeShellId.indexOf("?")
4257
+ if (shellQueryIndex < 0) {
4258
+ return href
4259
+ }
4260
+ let identityParams
4261
+ try {
4262
+ identityParams = new URLSearchParams(activeShellId.slice(shellQueryIndex + 1).replace(/&amp;/g, "&"))
4263
+ } catch (error) {
4264
+ return href
4265
+ }
4266
+ const hrefQueryIndex = href.indexOf("?")
4267
+ const pathPart = hrefQueryIndex >= 0 ? href.slice(0, hrefQueryIndex) : href
4268
+ const queryPart = hrefQueryIndex >= 0 ? href.slice(hrefQueryIndex + 1) : ""
4269
+ const nextParams = new URLSearchParams(queryPart)
4270
+ ;["session", "terminal_id"].forEach((key) => {
4271
+ const value = identityParams.get(key)
4272
+ if (value && !nextParams.has(key)) {
4273
+ nextParams.set(key, value)
4274
+ }
4275
+ })
4276
+ const qs = nextParams.toString()
4277
+ return qs ? `${pathPart}?${qs}` : pathPart
4278
+ }
3789
4279
  let shell = this.kernel.shell.get(shellPrefixId)
3790
4280
  if (!shell && this.kernel.shell && Array.isArray(this.kernel.shell.shells)) {
3791
4281
  shell = this.kernel.shell.shells.find((entry) => {
@@ -3797,6 +4287,8 @@ class Server {
3797
4287
  }
3798
4288
  menuitem.shell_id = shellPrefixId
3799
4289
  if (shell) {
4290
+ menuitem.shell_id = shell.id || shellPrefixId
4291
+ menuitem.href = appendShellIdentityParams(menuitem.href, shell.id)
3800
4292
  menuitem.running = true
3801
4293
  }
3802
4294
  }
@@ -3938,8 +4430,7 @@ class Server {
3938
4430
  if (menuitem.href.startsWith("http")) {
3939
4431
  menuitem.src = menuitem.href
3940
4432
  } else if (menuitem.href.startsWith("/")) {
3941
- let run_path = "/run"
3942
- if (menuitem.href.startsWith(run_path)) {
4433
+ if (PluginSources.isRunPath(menuitem.href)) {
3943
4434
  menuitem.src = menuitem.href
3944
4435
  // u = new URL("http://localhost" + menuitem.href.slice(run_path.length))
3945
4436
  // cwd = u.searchParams.get("cwd")
@@ -3959,7 +4450,14 @@ class Server {
3959
4450
  }
3960
4451
 
3961
4452
  // check running
3962
- let fullpath = this.kernel.path(menuitem.src.slice(1))
4453
+ let srcPathname = menuitem.src
4454
+ try {
4455
+ srcPathname = new URL("http://localhost" + menuitem.src).pathname
4456
+ } catch (_) {
4457
+ }
4458
+ let fullpath = PluginSources.isRunPath(srcPathname)
4459
+ ? PluginSources.resolveRunPath(this.kernel, srcPathname)
4460
+ : this.kernel.path(srcPathname.slice(1))
3963
4461
  let relpath = path.relative(this.kernel.homedir, fullpath)
3964
4462
  if (relpath.startsWith("api")) {
3965
4463
  // api script
@@ -5279,15 +5777,7 @@ class Server {
5279
5777
  return normalized
5280
5778
  }
5281
5779
  isValidBundledPluginConfig(pluginConfig) {
5282
- if (!pluginConfig || !Array.isArray(pluginConfig.run)) {
5283
- return false
5284
- }
5285
- for (const key of Object.keys(pluginConfig)) {
5286
- if (typeof pluginConfig[key] === "function") {
5287
- return false
5288
- }
5289
- }
5290
- return true
5780
+ return PluginSources.isValidPluginConfig(pluginConfig)
5291
5781
  }
5292
5782
  isPathInsideRootForBundledPlugin(candidatePath, rootPath) {
5293
5783
  const relative = path.relative(rootPath, candidatePath)
@@ -5673,7 +6163,6 @@ class Server {
5673
6163
  "prototype/PTERM.md",
5674
6164
  ]
5675
6165
  const managedRefreshTargets = [
5676
- "plugin/code",
5677
6166
  "prototype/system",
5678
6167
  "network/system",
5679
6168
  "prototype/PINOKIO.md",
@@ -5695,6 +6184,15 @@ class Server {
5695
6184
 
5696
6185
  needsManagedRefresh = true
5697
6186
  console.log("[TRY] Updating to the new version")
6187
+ let envPath = path.resolve(home, "ENVIRONMENT")
6188
+ let envExists = await this.kernel.exists(envPath)
6189
+ if (!envExists) {
6190
+ let str = await Environment.ENV("system", home, this.kernel)
6191
+ await fs.promises.writeFile(envPath, str)
6192
+ }
6193
+ await Environment.ensurePinokioCacheDirs(this.kernel, {
6194
+ throwOnFailure: true
6195
+ })
5698
6196
  this.kernel.store.set("version", this.version.pinokiod)
5699
6197
  console.log("[DONE] Updating to the new version")
5700
6198
  console.log("not up to date. update py.")
@@ -5707,9 +6205,6 @@ class Server {
5707
6205
  let p2 = path.resolve(home, "prototype/system")
5708
6206
  await fse.remove(p2)
5709
6207
 
5710
- let p3 = path.resolve(home, "plugin/code")
5711
- await fse.remove(p3)
5712
-
5713
6208
  let p4 = path.resolve(home, "network/system")
5714
6209
  await fse.remove(p4)
5715
6210
 
@@ -5778,10 +6273,6 @@ class Server {
5778
6273
  await this.kernel.proto.init()
5779
6274
  }
5780
6275
  if (needsManagedRefresh) {
5781
- const pluginReady = await this.kernel.exists("plugin/code")
5782
- if (!pluginReady && this.kernel.plugin && typeof this.kernel.plugin.init === "function") {
5783
- await this.kernel.plugin.init()
5784
- }
5785
6276
  const networkReady = await this.kernel.exists("network/system")
5786
6277
  if (!networkReady && this.kernel.router && typeof this.kernel.router.init === "function") {
5787
6278
  await this.kernel.router.init()
@@ -5962,6 +6453,10 @@ class Server {
5962
6453
  })
5963
6454
  this.app.use(express.static(path.resolve(__dirname, 'public')));
5964
6455
  this.app.use("/web", express.static(path.resolve(__dirname, "..", "..", "web")))
6456
+ this.app.use(PluginSources.SYSTEM_ASSET_PREFIX, express.static(PluginSources.systemRoot(this.kernel), {
6457
+ index: false,
6458
+ fallthrough: true,
6459
+ }))
5965
6460
  this.app.set('view engine', 'ejs');
5966
6461
  this.app.use((req, res, next) => {
5967
6462
  const peerForwarded = (req.get('X-Pinokio-Peer') || '').trim().toLowerCase()
@@ -6039,6 +6534,7 @@ class Server {
6039
6534
  preferences: this.appPreferences,
6040
6535
  appSearch: this.appSearch,
6041
6536
  appLogs: this.appLogs,
6537
+ appLogReports: this.appLogReports,
6042
6538
  getTheme: () => this.theme
6043
6539
  })
6044
6540
 
@@ -6103,6 +6599,90 @@ class Server {
6103
6599
  }
6104
6600
  })
6105
6601
  */
6602
+ this.app.get("/autolaunch/candidates", ex(async (req, res) => {
6603
+ const app = await this.getAutolaunchAppById(req.query.app)
6604
+ if (!app) {
6605
+ res.status(404).json({ ok: false, error: "App not found." })
6606
+ return
6607
+ }
6608
+ const state = await this.buildAutolaunchCandidates(app)
6609
+ res.json({ ok: true, ...state })
6610
+ }))
6611
+ this.app.post("/autolaunch", ex(async (req, res) => {
6612
+ const app = await this.getAutolaunchAppById(req.body && req.body.app)
6613
+ if (!app) {
6614
+ res.status(404).json({ ok: false, error: "App not found." })
6615
+ return
6616
+ }
6617
+ const requestedScript = typeof req.body.script === "string" ? req.body.script.trim() : ""
6618
+ if (!requestedScript) {
6619
+ const envInfo = await this.getAutolaunchEnvInfo(app)
6620
+ if (envInfo.exists) {
6621
+ await Util.update_env(envInfo.envPath, {
6622
+ PINOKIO_SCRIPT_AUTOLAUNCH: ""
6623
+ })
6624
+ }
6625
+ res.json({
6626
+ ok: true,
6627
+ app: await this.buildAutolaunchAppState(app)
6628
+ })
6629
+ return
6630
+ }
6631
+
6632
+ const envInfo = await this.getAutolaunchEnvInfo(app)
6633
+ const resolved = await this.resolveAutolaunchScript(envInfo.appRoot, requestedScript)
6634
+ if (!resolved) {
6635
+ res.status(400).json({
6636
+ ok: false,
6637
+ error: "Select an existing local script inside the app."
6638
+ })
6639
+ return
6640
+ }
6641
+ const initialized = await Environment.init({ name: app.id }, this.kernel)
6642
+ await Util.update_env(initialized.env_path, {
6643
+ PINOKIO_SCRIPT_AUTOLAUNCH: resolved.script
6644
+ })
6645
+ res.json({
6646
+ ok: true,
6647
+ app: await this.buildAutolaunchAppState(app)
6648
+ })
6649
+ }))
6650
+ this.app.post("/autolaunch/disable-all", ex(async (req, res) => {
6651
+ const apps = await this.kernel.api.listApps()
6652
+ let disabled = 0
6653
+ for (const app of apps) {
6654
+ const envInfo = await this.getAutolaunchEnvInfo(app)
6655
+ if (envInfo.exists && envInfo.value) {
6656
+ await Util.update_env(envInfo.envPath, {
6657
+ PINOKIO_SCRIPT_AUTOLAUNCH: ""
6658
+ })
6659
+ disabled++
6660
+ }
6661
+ }
6662
+ res.json({
6663
+ ok: true,
6664
+ disabled,
6665
+ apps: await this.buildAutolaunchAppsState()
6666
+ })
6667
+ }))
6668
+ this.app.get("/autolaunch", ex(async (req, res) => {
6669
+ const peerAccess = await this.composePeerAccessPayload()
6670
+ const list = this.getPeers()
6671
+ const apps = await this.buildAutolaunchAppsState()
6672
+ const appsJson = JSON.stringify(apps).replace(/</g, "\\u003c")
6673
+ res.render("autolaunch", {
6674
+ current_host: this.kernel.peer.host,
6675
+ ...peerAccess,
6676
+ apps,
6677
+ appsJson,
6678
+ enabledCount: apps.filter((app) => app.autolaunch_enabled).length,
6679
+ portal: this.portal,
6680
+ logo: this.logo,
6681
+ theme: this.theme,
6682
+ agent: req.agent,
6683
+ list,
6684
+ })
6685
+ }))
6106
6686
  this.app.get("/tools", ex(async (req, res) => {
6107
6687
  const peerAccess = await this.composePeerAccessPayload()
6108
6688
  let list = this.getPeers()
@@ -6744,24 +7324,7 @@ class Server {
6744
7324
  }
6745
7325
  }
6746
7326
  const normalizePluginPath = (value) => {
6747
- let normalized = typeof value === "string" ? value.trim() : ""
6748
- if (!normalized) {
6749
- return ""
6750
- }
6751
- normalized = normalized.replace(/\\/g, "/")
6752
- if (/^https?:\/\//i.test(normalized)) {
6753
- try {
6754
- const parsed = new URL(normalized)
6755
- normalized = parsed.pathname || ""
6756
- } catch (_) {
6757
- }
6758
- }
6759
- normalized = normalized.replace(/^\/run(?=\/)/, "")
6760
- if (!normalized.startsWith("/")) {
6761
- normalized = `/${normalized}`
6762
- }
6763
- normalized = normalized.replace(/\/{2,}/g, "/").replace(/\/+$/, "")
6764
- return normalized
7327
+ return PluginSources.normalizePluginPath(value)
6765
7328
  }
6766
7329
  const normalizePluginLookupKey = (value) => {
6767
7330
  const normalized = normalizePluginPath(value)
@@ -6777,6 +7340,21 @@ class Server {
6777
7340
  : `${trimmed.replace(/\/+$/, "")}/pinokio.js`
6778
7341
  }
6779
7342
  const loadBundledPluginMenu = async () => this.getBundledPluginMenu()
7343
+ const evaluatePluginInstalled = async (pluginItem) => {
7344
+ if (!PluginSources.isInstalledCheck(pluginItem && pluginItem.installed)) {
7345
+ return null
7346
+ }
7347
+ try {
7348
+ const result = await Promise.race([
7349
+ Promise.resolve(pluginItem.installed(this.kernel, this.kernel.info || {})),
7350
+ new Promise((resolve) => setTimeout(() => resolve(null), 1000))
7351
+ ])
7352
+ return typeof result === "boolean" ? result : null
7353
+ } catch (error) {
7354
+ console.warn("Failed to evaluate plugin installed state", pluginItem && pluginItem.title, error)
7355
+ return null
7356
+ }
7357
+ }
6780
7358
  const classifyPluginMenuItem = (pluginItem) => {
6781
7359
  const runs = Array.isArray(pluginItem && pluginItem.run) ? pluginItem.run : []
6782
7360
  const hasExec = runs.some((step) => step && step.method === "exec")
@@ -6793,7 +7371,7 @@ class Server {
6793
7371
  }
6794
7372
  return "cli"
6795
7373
  }
6796
- const serializePluginMenuItem = (pluginItem, index) => {
7374
+ const serializePluginMenuItem = async (pluginItem, index) => {
6797
7375
  const hrefValue = typeof pluginItem?.href === "string" ? pluginItem.href : ""
6798
7376
  let pluginPath = typeof pluginItem?.src === "string" ? pluginItem.src : ""
6799
7377
  const extraParams = []
@@ -6801,7 +7379,9 @@ class Server {
6801
7379
  try {
6802
7380
  const parsed = new URL(hrefValue.startsWith("http") ? hrefValue : `http://localhost${hrefValue}`)
6803
7381
  if (!pluginPath) {
6804
- pluginPath = parsed.pathname.replace(/^\/run/, "") || ""
7382
+ pluginPath = PluginSources.isSystemRunPath(parsed.pathname)
7383
+ ? parsed.pathname
7384
+ : (parsed.pathname.replace(/^\/run/, "") || "")
6805
7385
  }
6806
7386
  parsed.searchParams.forEach((value, key) => {
6807
7387
  if (key === "cwd") {
@@ -6816,6 +7396,8 @@ class Server {
6816
7396
  const normalizedPluginPath = normalizePluginPath(pluginPath)
6817
7397
  const category = classifyPluginMenuItem(pluginItem)
6818
7398
  const title = pluginItem?.title || pluginItem?.text || pluginItem?.name || "Plugin"
7399
+ const hasInstalledCheck = PluginSources.isInstalledCheck(pluginItem?.installed)
7400
+ const installed = hasInstalledCheck ? await evaluatePluginInstalled(pluginItem) : null
6819
7401
  return {
6820
7402
  index,
6821
7403
  title,
@@ -6836,9 +7418,11 @@ class Server {
6836
7418
  cwd: typeof pluginItem.ownerApp.cwd === "string" ? pluginItem.ownerApp.cwd : "",
6837
7419
  }
6838
7420
  : null,
6839
- hasInstall: Array.isArray(pluginItem?.install),
6840
- hasUninstall: Array.isArray(pluginItem?.uninstall),
6841
- hasUpdate: Array.isArray(pluginItem?.update),
7421
+ hasInstall: PluginSources.isAction(pluginItem?.install),
7422
+ hasUninstall: PluginSources.isAction(pluginItem?.uninstall),
7423
+ hasUpdate: PluginSources.isAction(pluginItem?.update),
7424
+ hasInstalledCheck,
7425
+ installed,
6842
7426
  category,
6843
7427
  categoryTitle: category === "ide" ? "Desktop Plugin" : "Terminal Plugin",
6844
7428
  categorySubtitle: category === "ide" ? "Launch externally" : "Launch in Pinokio",
@@ -6868,7 +7452,7 @@ class Server {
6868
7452
  console.warn("Failed to load bundled plugins", bundledError)
6869
7453
  }
6870
7454
  const mergedPluginMenu = pluginMenu.concat(bundledPluginMenu)
6871
- return mergedPluginMenu.map((pluginItem, index) => serializePluginMenuItem(pluginItem, index))
7455
+ return Promise.all(mergedPluginMenu.map((pluginItem, index) => serializePluginMenuItem(pluginItem, index)))
6872
7456
  }
6873
7457
  const buildPluginCategories = (plugins) => {
6874
7458
  const buckets = { ide: [], cli: [] }
@@ -6880,10 +7464,75 @@ class Server {
6880
7464
  }
6881
7465
  })
6882
7466
  return [
6883
- { key: "ide", title: "Desktop Plugins", subtitle: "Launch externally", items: buckets.ide },
6884
7467
  { key: "cli", title: "Terminal Plugins", subtitle: "Launch in Pinokio", items: buckets.cli },
7468
+ { key: "ide", title: "Desktop Plugins", subtitle: "Launch externally", items: buckets.ide },
6885
7469
  ]
6886
7470
  }
7471
+ const resolveProjectShellWorkspace = (value) => {
7472
+ const requestedWorkspace = typeof value === "string" ? value.trim() : ""
7473
+ if (!requestedWorkspace) {
7474
+ return ""
7475
+ }
7476
+ try {
7477
+ return path.resolve(this.kernel.api.filePath(requestedWorkspace))
7478
+ } catch (error) {
7479
+ return ""
7480
+ }
7481
+ }
7482
+ const serializeProjectShellMenu = async (workspacePath) => {
7483
+ if (!workspacePath) {
7484
+ return null
7485
+ }
7486
+ try {
7487
+ const stat = await fs.promises.stat(workspacePath)
7488
+ if (!stat.isDirectory()) {
7489
+ return null
7490
+ }
7491
+ } catch (error) {
7492
+ return null
7493
+ }
7494
+ try {
7495
+ const terminal = await this.devTerminals(workspacePath)
7496
+ const items = []
7497
+ const addShellOption = (item, group = {}) => {
7498
+ if (!item || typeof item !== "object") {
7499
+ return
7500
+ }
7501
+ if (Array.isArray(item.menu) && item.menu.length > 0) {
7502
+ item.menu.forEach((child) => addShellOption(child, item))
7503
+ return
7504
+ }
7505
+ const href = typeof item.href === "string" ? item.href.trim() : ""
7506
+ if (!href || !href.startsWith("/shell/")) {
7507
+ return
7508
+ }
7509
+ const title = typeof item.title === "string" && item.title.trim()
7510
+ ? item.title.trim()
7511
+ : (typeof item.text === "string" && item.text.trim() ? item.text.trim() : "Shell")
7512
+ items.push({
7513
+ title,
7514
+ text: typeof item.text === "string" ? item.text : title,
7515
+ subtitle: typeof item.subtitle === "string" ? item.subtitle : "",
7516
+ icon: typeof item.icon === "string" && item.icon ? item.icon : (group.icon || terminal.icon || "fa-solid fa-terminal"),
7517
+ href,
7518
+ target: `@${href}`,
7519
+ shellId: typeof item.shell_id === "string" ? item.shell_id : "",
7520
+ })
7521
+ }
7522
+ ;(Array.isArray(terminal.menu) ? terminal.menu : []).forEach((item) => addShellOption(item))
7523
+ if (items.length === 0) {
7524
+ return null
7525
+ }
7526
+ return {
7527
+ title: "Project Shell",
7528
+ subtitle: typeof terminal.subtitle === "string" ? terminal.subtitle : "",
7529
+ items,
7530
+ }
7531
+ } catch (error) {
7532
+ console.warn("Failed to load project shell menu for Ask AI", error)
7533
+ return null
7534
+ }
7535
+ }
6887
7536
  const findPluginByPath = (plugins, targetPath) => {
6888
7537
  const targetKey = normalizePluginLookupKey(targetPath)
6889
7538
  if (!targetKey) {
@@ -6908,7 +7557,7 @@ class Server {
6908
7557
  index: -1,
6909
7558
  title: context.title || "Plugin",
6910
7559
  description: typeof config.description === "string" ? config.description : "",
6911
- href: `/run${normalizedPluginPath}`,
7560
+ href: PluginSources.pluginRunHrefForPath(normalizedPluginPath),
6912
7561
  link: typeof config.link === "string" ? config.link : "",
6913
7562
  image: context.image || null,
6914
7563
  icon: null,
@@ -6921,6 +7570,8 @@ class Server {
6921
7570
  hasInstall: !!context.hasInstall,
6922
7571
  hasUninstall: !!context.hasUninstall,
6923
7572
  hasUpdate: !!context.hasUpdate,
7573
+ hasInstalledCheck: !!context.hasInstalledCheck,
7574
+ installed: null,
6924
7575
  category,
6925
7576
  categoryTitle: category === "ide" ? "Desktop Plugin" : "Terminal Plugin",
6926
7577
  categorySubtitle: category === "ide" ? "Launch externally" : "Launch in Pinokio",
@@ -6948,9 +7599,20 @@ class Server {
6948
7599
  localLabel: "",
6949
7600
  managedPrefix: "",
6950
7601
  }
7602
+ if (PluginSources.isSystemPluginPath(normalizedPath)) {
7603
+ const relativeSystemPath = normalizedPath.replace(/^\/pinokio\/run\/+/, "")
7604
+ return {
7605
+ ...emptyState,
7606
+ ownership: "system",
7607
+ localLabel: relativeSystemPath,
7608
+ }
7609
+ }
6951
7610
  if (!normalizedPath.startsWith("/plugin/")) {
6952
7611
  return emptyState
6953
7612
  }
7613
+ if (PluginSources.isLegacyPluginCodePath(normalizedPath)) {
7614
+ return emptyState
7615
+ }
6954
7616
  const pluginFilePath = path.resolve(this.kernel.path(normalizedPath.slice(1)))
6955
7617
  if (!isPathInsideRoot(pluginFilePath, pluginRoot)) {
6956
7618
  return emptyState
@@ -7176,6 +7838,11 @@ class Server {
7176
7838
  label: "Local plugin",
7177
7839
  tone: "accent"
7178
7840
  })
7841
+ } else if (ownership === "system") {
7842
+ badges.push({
7843
+ label: "Built-in plugin",
7844
+ tone: "neutral"
7845
+ })
7179
7846
  } else if (ownership === "managed") {
7180
7847
  badges.push({
7181
7848
  label: "Managed by Pinokio",
@@ -7191,6 +7858,17 @@ class Server {
7191
7858
  label: plugin && plugin.category === "ide" ? "Desktop plugin" : "Terminal plugin",
7192
7859
  tone: "neutral"
7193
7860
  })
7861
+ if (plugin && plugin.installed === true) {
7862
+ badges.push({
7863
+ label: "Installed",
7864
+ tone: "accent"
7865
+ })
7866
+ } else if (plugin && plugin.installed === false) {
7867
+ badges.push({
7868
+ label: "Not installed",
7869
+ tone: "warning"
7870
+ })
7871
+ }
7194
7872
  if (remoteLabel) {
7195
7873
  badges.push({
7196
7874
  label: "Remote linked",
@@ -7228,8 +7906,14 @@ class Server {
7228
7906
  sourceValue = shareState.localLabel
7229
7907
  statusValue = changes.length > 0 ? pluralizeTaskFiles(changes.length) : "Updated with Pinokio"
7230
7908
  githubPanelTitle = "Managed by Pinokio"
7231
- githubPanelCopy = "This plugin lives inside <code>plugin/code</code>, which Pinokio refreshes as part of its managed source. Open the folder if you need to inspect it, but don&apos;t treat it as your own publishable repo."
7909
+ githubPanelCopy = "This plugin lives inside Pinokio-managed source. Open the folder if you need to inspect it, but don&apos;t treat it as your own publishable repo."
7232
7910
  localChangesCopy = "These edits live inside Pinokio-managed source and may be overwritten by future Pinokio updates."
7911
+ } else if (ownership === "system") {
7912
+ sourceLabel = "System plugin"
7913
+ sourceValue = shareState.localLabel || sourceValue
7914
+ statusValue = "Read-only"
7915
+ githubPanelTitle = "Built in to Pinokio"
7916
+ githubPanelCopy = "This plugin ships with Pinokio and is not editable from the local plugin workspace."
7233
7917
  }
7234
7918
  const changePreview = changes.slice(0, 6).map((change) => ({
7235
7919
  file: change && change.file ? change.file : "",
@@ -7272,6 +7956,7 @@ class Server {
7272
7956
  }))
7273
7957
  this.app.get("/plugin", ex(async (req, res) => {
7274
7958
  const requestedPath = typeof req.query.path === "string" ? req.query.path.trim() : ""
7959
+ const requestedCwd = typeof req.query.cwd === "string" ? req.query.cwd.trim() : ""
7275
7960
  if (!requestedPath) {
7276
7961
  res.redirect("/plugins")
7277
7962
  return
@@ -7301,6 +7986,7 @@ class Server {
7301
7986
  ...sidebarContext,
7302
7987
  plugin,
7303
7988
  pluginUi,
7989
+ pluginCwd: requestedCwd,
7304
7990
  shareState,
7305
7991
  apps,
7306
7992
  })
@@ -7337,14 +8023,154 @@ class Server {
7337
8023
  }))
7338
8024
  this.app.get("/api/plugin/menu", ex(async (req, res) => {
7339
8025
  try {
8026
+ const workspacePath = resolveProjectShellWorkspace(req.query.workspace)
7340
8027
  const pluginMenu = await loadSerializedPlugins()
8028
+ const projectShell = await serializeProjectShellMenu(workspacePath)
7341
8029
  res.set("Cache-Control", "no-store")
7342
- res.json({ menu: pluginMenu })
8030
+ res.json({ menu: pluginMenu, projectShell })
7343
8031
  } catch (error) {
7344
8032
  console.warn('Failed to load plugin menu for create launcher modal', error)
7345
8033
  res.json({ menu: [] })
7346
8034
  }
7347
8035
  }))
8036
+ this.app.get("/api/plugin/install-state", ex(async (req, res) => {
8037
+ const requestedPath = typeof req.query.path === "string" ? req.query.path.trim() : ""
8038
+ if (!requestedPath) {
8039
+ res.status(400).json({
8040
+ ok: false,
8041
+ error: "Plugin path is required."
8042
+ })
8043
+ return
8044
+ }
8045
+ const validation = await contentValidation.validatePluginByPath(requestedPath)
8046
+ if (!validation.valid) {
8047
+ res.status(404).json({
8048
+ ok: false,
8049
+ error: validation.message || "Plugin not found."
8050
+ })
8051
+ return
8052
+ }
8053
+ const context = validation.context && typeof validation.context === "object" ? validation.context : {}
8054
+ const config = context.config && typeof context.config === "object" ? context.config : {}
8055
+ const normalizedPluginPath = normalizePluginPath(context.pluginPath || requestedPath)
8056
+ const hasInstall = PluginSources.isAction(config.install)
8057
+ const hasInstalledCheck = PluginSources.isInstalledCheck(config.installed)
8058
+ const installed = hasInstalledCheck ? await evaluatePluginInstalled(config) : null
8059
+ res.set("Cache-Control", "no-store")
8060
+ res.json({
8061
+ ok: true,
8062
+ title: context.title || "Plugin",
8063
+ pluginPath: normalizedPluginPath,
8064
+ detailUrl: `/plugin?path=${encodeURIComponent(normalizedPluginPath)}`,
8065
+ hasInstall,
8066
+ hasInstalledCheck,
8067
+ installed,
8068
+ })
8069
+ }))
8070
+ const redirectSkills = (res, params = {}) => {
8071
+ const query = new URLSearchParams()
8072
+ if (params.notice) query.set("notice", params.notice)
8073
+ if (params.error) query.set("error", params.error)
8074
+ res.redirect(`/skills${query.toString() ? `?${query.toString()}` : ""}`)
8075
+ }
8076
+ const requireSkillsHome = (res) => {
8077
+ if (this.kernel && this.kernel.homedir) {
8078
+ return true
8079
+ }
8080
+ res.redirect("/home?mode=settings")
8081
+ return false
8082
+ }
8083
+ this.app.get("/skills", ex(async (req, res) => {
8084
+ if (!requireSkillsHome(res)) {
8085
+ return
8086
+ }
8087
+ let skills = []
8088
+ let indexError = ""
8089
+ try {
8090
+ skills = await ManagedSkills.listManagedSkills(this.kernel)
8091
+ } catch (error) {
8092
+ indexError = error && error.message ? error.message : "Failed to read managed skills index."
8093
+ }
8094
+ res.render("skills", {
8095
+ theme: this.theme,
8096
+ agent: req.agent,
8097
+ skills,
8098
+ skillsRootPath: ManagedSkills.skillsRoot(this.kernel),
8099
+ indexPath: ManagedSkills.indexPath(this.kernel),
8100
+ publishRoots: ManagedSkills.publishRoots(),
8101
+ notice: typeof req.query.notice === "string" ? req.query.notice : "",
8102
+ error: typeof req.query.error === "string" ? req.query.error : "",
8103
+ indexError
8104
+ })
8105
+ }))
8106
+ this.app.post("/skills/install", ex(async (req, res) => {
8107
+ if (!requireSkillsHome(res)) {
8108
+ return
8109
+ }
8110
+ try {
8111
+ const skill = await ManagedSkills.installSkillFromGit(this.kernel, {
8112
+ ref: typeof req.body.ref === "string" ? req.body.ref : ""
8113
+ })
8114
+ redirectSkills(res, {
8115
+ notice: skill && skill.valid
8116
+ ? `Downloaded ${skill.label || skill.id}.`
8117
+ : `Downloaded ${skill ? skill.id : "skill"}, but it is invalid.`
8118
+ })
8119
+ } catch (error) {
8120
+ redirectSkills(res, {
8121
+ error: error && error.message ? error.message : "Failed to download skill."
8122
+ })
8123
+ }
8124
+ }))
8125
+ this.app.post("/skills/:id/toggle", ex(async (req, res) => {
8126
+ if (!requireSkillsHome(res)) {
8127
+ return
8128
+ }
8129
+ const id = typeof req.params.id === "string" ? req.params.id : ""
8130
+ const enabled = req.body.enabled === "1" || req.body.enabled === "true"
8131
+ try {
8132
+ const skill = await ManagedSkills.setSkillEnabled(this.kernel, id, enabled)
8133
+ redirectSkills(res, {
8134
+ notice: `${skill && skill.label ? skill.label : id} ${enabled ? "turned on" : "turned off"}.`
8135
+ })
8136
+ } catch (error) {
8137
+ redirectSkills(res, {
8138
+ error: error && error.message ? error.message : "Failed to update skill."
8139
+ })
8140
+ }
8141
+ }))
8142
+ this.app.post("/skills/:id/publish-name", ex(async (req, res) => {
8143
+ if (!requireSkillsHome(res)) {
8144
+ return
8145
+ }
8146
+ const id = typeof req.params.id === "string" ? req.params.id : ""
8147
+ try {
8148
+ const skill = await ManagedSkills.setSkillPublishName(this.kernel, id, req.body.publishName)
8149
+ redirectSkills(res, {
8150
+ notice: `${skill && skill.label ? skill.label : id} syncs as ${skill.publishName}.`
8151
+ })
8152
+ } catch (error) {
8153
+ redirectSkills(res, {
8154
+ error: error && error.message ? error.message : "Failed to update sync folder name."
8155
+ })
8156
+ }
8157
+ }))
8158
+ this.app.post("/skills/:id/remove", ex(async (req, res) => {
8159
+ if (!requireSkillsHome(res)) {
8160
+ return
8161
+ }
8162
+ const id = typeof req.params.id === "string" ? req.params.id : ""
8163
+ try {
8164
+ await ManagedSkills.removeSkill(this.kernel, id)
8165
+ redirectSkills(res, {
8166
+ notice: `${id} removed.`
8167
+ })
8168
+ } catch (error) {
8169
+ redirectSkills(res, {
8170
+ error: error && error.message ? error.message : "Failed to remove skill."
8171
+ })
8172
+ }
8173
+ }))
7348
8174
  this.app.get("/api/tasks", ex(async (req, res) => {
7349
8175
  const query = typeof req.query.q === "string" ? req.query.q.trim().toLowerCase() : ""
7350
8176
  const targetFilter = typeof req.query.target === "string"
@@ -7488,8 +8314,13 @@ class Server {
7488
8314
  const peerAccess = await this.composePeerAccessPayload()
7489
8315
  const list = this.getPeers()
7490
8316
  const workspace = typeof req.query.workspace === 'string' ? req.query.workspace.trim() : ''
8317
+ const embedded = req.query.embed === '1' || req.query.embed === 'true'
8318
+ const requestedView = typeof req.query.view === 'string' ? req.query.view.trim().toLowerCase() : ''
8319
+ const initialView = workspace && requestedView !== 'raw' ? 'latest' : 'raw'
7491
8320
  let context
7492
8321
  const downloadUrl = workspace ? `/pinokio/logs.zip?workspace=${encodeURIComponent(workspace)}` : '/pinokio/logs.zip'
8322
+ const reportUrl = workspace ? `/apps/logs/${encodeURIComponent(workspace)}/report` : ''
8323
+ const draftUrl = workspace ? `/apps/logs/${encodeURIComponent(workspace)}/drafts` : ''
7493
8324
  try {
7494
8325
  context = await this.resolveLogsRoot({ workspace })
7495
8326
  } catch (error) {
@@ -7506,6 +8337,10 @@ class Server {
7506
8337
  logsTitle: workspace || null,
7507
8338
  logsError: error && error.message ? error.message : 'Workspace not found',
7508
8339
  logsDownloadUrl: downloadUrl,
8340
+ logsReportUrl: reportUrl,
8341
+ logsDraftUrl: draftUrl,
8342
+ logsEmbedded: embedded,
8343
+ logsInitialView: initialView,
7509
8344
  })
7510
8345
  return
7511
8346
  }
@@ -7522,6 +8357,10 @@ class Server {
7522
8357
  logsTitle: context.title,
7523
8358
  logsError: null,
7524
8359
  logsDownloadUrl: downloadUrl,
8360
+ logsReportUrl: reportUrl,
8361
+ logsDraftUrl: draftUrl,
8362
+ logsEmbedded: embedded,
8363
+ logsInitialView: initialView,
7525
8364
  })
7526
8365
  }))
7527
8366
  this.app.get("/api/logs/tree", ex(async (req, res) => {
@@ -8094,24 +8933,7 @@ class Server {
8094
8933
  }
8095
8934
  return targetPath
8096
8935
  }
8097
- const resolveUniversalLauncherPluginHref = (toolValue) => {
8098
- let normalizedTool = typeof toolValue === "string" ? toolValue.trim() : ""
8099
- normalizedTool = normalizedTool.replace(/^https?:\/\/[^/]+/i, "")
8100
- normalizedTool = normalizedTool.replace(/^\/+|\/+$/g, "")
8101
- normalizedTool = normalizedTool.replace(/^run\//, "")
8102
- if (!normalizedTool || normalizedTool.includes("..") || !/^[A-Za-z0-9._/-]+$/.test(normalizedTool)) {
8103
- const error = new Error("Invalid plugin.")
8104
- error.status = 400
8105
- throw error
8106
- }
8107
- if (normalizedTool.startsWith("plugin/") || normalizedTool.startsWith("api/")) {
8108
- const scriptPath = normalizedTool.endsWith(".js")
8109
- ? normalizedTool
8110
- : `${normalizedTool}/pinokio.js`
8111
- return `/run/${scriptPath}`
8112
- }
8113
- return `/run/plugin/${normalizedTool}/pinokio.js`
8114
- }
8936
+ const resolveUniversalLauncherPluginHref = PluginSources.resolveLauncherPluginHref
8115
8937
  const persistLauncherPromptContext = async (targetPath, options = {}) => {
8116
8938
  const prompt = typeof options.prompt === "string" ? options.prompt.trim() : ""
8117
8939
  if (!prompt) {
@@ -8390,7 +9212,7 @@ class Server {
8390
9212
  type: "create_app",
8391
9213
  name: folderName,
8392
9214
  cwd: path.resolve(this.kernel.path("api"), folderName),
8393
- url: response.success
9215
+ url: PluginSources.normalizeLauncherSuccessPlugin(response.success, selectedTool)
8394
9216
  }
8395
9217
  }
8396
9218
  const taskPackages = createTaskPackageService({
@@ -9377,10 +10199,6 @@ class Server {
9377
10199
  res.redirect("/home?mode=settings")
9378
10200
  return
9379
10201
  }
9380
- const terminalDefaultSkillIds = Array.from(new Set([
9381
- path.resolve(os.homedir(), ".agents", "skills", "pinokio", "SKILL.md"),
9382
- path.resolve(os.homedir(), ".agents", "skills", "gepeto", "SKILL.md")
9383
- ].map((skillPath) => crypto.createHash("sha1").update(skillPath).digest("hex").slice(0, 16))))
9384
10202
  const peerAccess = await this.composePeerAccessPayload()
9385
10203
  res.render("terminals", {
9386
10204
  ...peerAccess,
@@ -9392,7 +10210,7 @@ class Server {
9392
10210
  items: [],
9393
10211
  providers: getTerminalStarterProviders().map((provider) => ({ key: provider.key, label: provider.label })),
9394
10212
  skills: [],
9395
- defaultSkillIds: terminalDefaultSkillIds,
10213
+ defaultSkillIds: [],
9396
10214
  ishome: false
9397
10215
  })
9398
10216
  return
@@ -12114,15 +12932,16 @@ class Server {
12114
12932
  }))
12115
12933
  this.app.get("/settings/docs/:skill/download", ex(async (req, res) => {
12116
12934
  const skill = typeof req.params.skill === "string" ? req.params.skill.trim().toLowerCase() : ""
12117
- const docsBySkill = {
12118
- pinokio: path.resolve(os.homedir(), ".agents", "skills", "pinokio", "SKILL.md"),
12119
- gepeto: path.resolve(os.homedir(), ".agents", "skills", "gepeto", "SKILL.md"),
12935
+ if (!this.kernel || !this.kernel.homedir) {
12936
+ res.status(404).send("Pinokio home not configured")
12937
+ return
12120
12938
  }
12121
- const filepath = docsBySkill[skill]
12122
- if (!filepath) {
12939
+ const managedSkill = await ManagedSkills.getManagedSkill(this.kernel, skill, { sync: false })
12940
+ if (!managedSkill || (skill !== "pinokio" && skill !== "gepeto")) {
12123
12941
  res.status(404).send("Unknown skill")
12124
12942
  return
12125
12943
  }
12944
+ const filepath = managedSkill.path
12126
12945
  try {
12127
12946
  await fs.promises.access(filepath, fs.constants.R_OK)
12128
12947
  } catch (error) {
@@ -12202,10 +13021,11 @@ class Server {
12202
13021
  let md = await fs.promises.readFile(path.resolve(__dirname, "..", "kernel/connect/providers/github/README.md"), "utf8")
12203
13022
  let readme = marked.parse(md)
12204
13023
 
12205
- let hosts = await this.get_github_hosts()
13024
+ const githubConnection = await this.get_github_connection()
13025
+ let hosts = githubConnection.display
12206
13026
 
12207
13027
  let items
12208
- if (hosts.length > 0) {
13028
+ if (githubConnection.connected) {
12209
13029
  // logged in => display logout
12210
13030
  items = [{
12211
13031
  icon: "fa-solid fa-circle-xmark",
@@ -12280,7 +13100,7 @@ class Server {
12280
13100
  this.app.get("/github/status", ex(async (req, res) => {
12281
13101
  let id = "gh_status"
12282
13102
  let params = new URLSearchParams()
12283
- let message = "gh auth status"
13103
+ let message = "git credential-manager github list"
12284
13104
  params.set("message", encodeURIComponent(message))
12285
13105
  params.set("path", this.kernel.homedir)
12286
13106
  // params.set("kill", "/Logged in/i")
@@ -12294,7 +13114,15 @@ class Server {
12294
13114
  this.app.get("/github/logout", ex(async (req, res) => {
12295
13115
  let id = "gh_logout"
12296
13116
  let params = new URLSearchParams()
12297
- let message = "gh auth logout"
13117
+ const githubConnection = await this.get_github_connection()
13118
+ let { hadLegacyAuth, message } = await this.github_logout_params(githubConnection)
13119
+ if (!message && hadLegacyAuth) {
13120
+ res.redirect("/github")
13121
+ return
13122
+ }
13123
+ if (!message) {
13124
+ message = "git credential-manager github list"
13125
+ }
12298
13126
  params.set("message", encodeURIComponent(message))
12299
13127
  params.set("path", this.kernel.homedir)
12300
13128
  // params.set("kill", "/Logged in/i")
@@ -12308,21 +13136,11 @@ class Server {
12308
13136
  this.app.get("/github/login", ex(async (req, res) => {
12309
13137
  let id = "gh_login"
12310
13138
  let params = new URLSearchParams()
12311
- let delimiter
12312
- if (this.kernel.platform === "win32") {
12313
- delimiter = " && "; // must use &&. & doesn't necessariliy wait until the curruent command finishes
12314
- } else {
12315
- delimiter = " ; ";
12316
- }
12317
- let message = [
12318
- "gh auth setup-git --hostname github.com --force",
12319
- "gh auth login --web --clipboard --git-protocol https"
12320
- ].join(delimiter)
13139
+ const { doneMarker, message } = this.github_login_params()
12321
13140
  params.set("message", encodeURIComponent(message))
12322
13141
  params.set("input", true)
12323
13142
  params.set("path", this.kernel.homedir)
12324
- params.set("kill", "/Logged in/i")
12325
- // params.set("kill_message", "Your Github account is now connected.")
13143
+ params.set("kill", `/${doneMarker}/`)
12326
13144
  params.set("callback", encodeURIComponent("/github"))
12327
13145
  params.set("id", id)
12328
13146
  params.set("target", "_top")
@@ -12756,23 +13574,6 @@ class Server {
12756
13574
  })
12757
13575
  }
12758
13576
  }))
12759
- this.app.post("/plugin/update", ex(async (req, res) => {
12760
- try {
12761
- await this.kernel.exec({
12762
- message: "git pull",
12763
- path: this.kernel.path("plugin/code")
12764
- }, (e) => {
12765
- console.log(e)
12766
- })
12767
- res.json({
12768
- success: true
12769
- })
12770
- } catch (e) {
12771
- res.json({
12772
- error: e.stack
12773
- })
12774
- }
12775
- }))
12776
13577
  this.app.post("/network/reset", ex(async (req, res) => {
12777
13578
  let caddy_path = this.kernel.path("cache/XDG_DATA_HOME/caddy")
12778
13579
  await rimraf(caddy_path)
@@ -13549,14 +14350,15 @@ class Server {
13549
14350
  this.app.get("/pre/api/:name", ex(async (req, res) => {
13550
14351
  let launcher = await this.kernel.api.launcher(req.params.name)
13551
14352
  let config = launcher.script
13552
- if (config && config.pre) {
13553
- config.pre.forEach((item) => {
13554
- if (item.icon) {
14353
+ if (config && Array.isArray(config.pre)) {
14354
+ const items = config.pre.filter((item) => item && typeof item === "object")
14355
+ items.forEach((item) => {
14356
+ if (typeof item.icon === "string" && item.icon) {
13555
14357
  item.icon = `/api/${req.params.name}/${item.icon}?raw=true`
13556
14358
  } else {
13557
14359
  item.icon = "/pinokio-black.png"
13558
14360
  }
13559
- if (!item.href.startsWith("http")) {
14361
+ if (typeof item.href === "string" && item.href && !item.href.startsWith("http")) {
13560
14362
  item.href = path.resolve(this.kernel.homedir, "api", req.params.name, item.href)
13561
14363
  }
13562
14364
  })
@@ -13567,7 +14369,7 @@ class Server {
13567
14369
  theme: this.theme,
13568
14370
  agent: req.agent,
13569
14371
  name: req.params.name,
13570
- items: config.pre,
14372
+ items,
13571
14373
  env
13572
14374
  })
13573
14375
  } else {
@@ -13684,6 +14486,18 @@ class Server {
13684
14486
  res.json({ du: 0 })
13685
14487
  }
13686
14488
  }))
14489
+ this.app.post("/resource-usage/preferences", ex(async (req, res) => {
14490
+ const preferences = await this.resourceUsage.updatePreferences(req.body || {})
14491
+ res.json({ preferences })
14492
+ }))
14493
+ this.app.get("/resource-usage/workspace/:name", ex(async (req, res) => {
14494
+ const usage = await this.resourceUsage.getWorkspaceUsage(req.params.name)
14495
+ if (!usage.ok) {
14496
+ res.status(400).json(usage)
14497
+ return
14498
+ }
14499
+ res.json(usage)
14500
+ }))
13687
14501
  this.app.get("/edit/*", ex(async (req, res) => {
13688
14502
  let pathComponents = req.params[0].split("/")
13689
14503
  let filepath = path.resolve(this.kernel.homedir, req.params[0])
@@ -14053,7 +14867,7 @@ class Server {
14053
14867
  mode = "launch_type.desktop"
14054
14868
  } else if (launchType === "terminal") {
14055
14869
  mode = "launch_type.terminal"
14056
- } else {
14870
+ } else if (Array.isArray(item.run)) {
14057
14871
  for(let step of item.run) {
14058
14872
  if (step.method === "exec") {
14059
14873
  mode = "exec"
@@ -14068,6 +14882,8 @@ class Server {
14068
14882
  break
14069
14883
  }
14070
14884
  }
14885
+ } else if (typeof item.run === "function") {
14886
+ mode = "shell"
14071
14887
  }
14072
14888
  if (mode === "launch_type.desktop" || mode === "exec" || mode === "launch") {
14073
14889
  item.type = "Open"
@@ -14195,8 +15011,11 @@ class Server {
14195
15011
  }))
14196
15012
  this.app.get("/action/:action/*", ex(async (req, res) => {
14197
15013
  const action = typeof req.params.action === 'string' ? req.params.action : ''
14198
- const pathComponents = req.params[0] ? req.params[0].split("/") : []
14199
- req.base = this.kernel.homedir
15014
+ const rawComponents = req.params[0] ? req.params[0].split("/").filter(Boolean) : []
15015
+ const actionTarget = PluginSources.normalizeActionPathComponents(rawComponents)
15016
+ const pathComponents = actionTarget.pathComponents
15017
+ req.base = actionTarget.system ? PluginSources.systemRoot(this.kernel) : this.kernel.homedir
15018
+ req.pinokioSystem = actionTarget.system
14200
15019
  req.action = action
14201
15020
  try {
14202
15021
  await this.render(req, res, pathComponents)
@@ -14204,6 +15023,17 @@ class Server {
14204
15023
  res.status(404).send(e.message)
14205
15024
  }
14206
15025
  }))
15026
+ this.app.get(`${PluginSources.SYSTEM_RUN_PREFIX}/*`, ex(async (req, res) => {
15027
+ const runPath = typeof req.params[0] === "string" ? req.params[0] : ""
15028
+ let pathComponents = runPath.split("/")
15029
+ req.base = PluginSources.systemRoot(this.kernel)
15030
+ req.pinokioSystem = true
15031
+ try {
15032
+ await this.render(req, res, pathComponents)
15033
+ } catch (e) {
15034
+ res.status(404).send(e.message)
15035
+ }
15036
+ }))
14207
15037
  this.app.get("/run/*", ex(async (req, res) => {
14208
15038
  const runPath = typeof req.params[0] === "string" ? req.params[0] : ""
14209
15039
  let pathComponents = runPath.split("/")
@@ -14937,7 +15767,7 @@ class Server {
14937
15767
  await this.chrome(req, res, "browse")
14938
15768
  }))
14939
15769
  this.app.get("/p/:name/files", ex(async (req, res) => {
14940
- await this.chrome(req, res, "files")
15770
+ res.redirect(`/p/${encodeURIComponent(req.params.name)}/dev?pinokio_dev_tab=files`)
14941
15771
  }))
14942
15772
  this.app.get("/p/:name/browse", ex(async (req, res) => {
14943
15773
  await this.chrome(req, res, "browse")
@@ -14970,6 +15800,9 @@ class Server {
14970
15800
  let folderPath = this.kernel.path("cache")
14971
15801
  await fse.remove(folderPath)
14972
15802
  await fs.promises.mkdir(folderPath, { recursive: true }).catch((e) => { })
15803
+ await Environment.ensurePinokioCacheDirs(this.kernel, {
15804
+ throwOnFailure: true
15805
+ })
14973
15806
  res.json({ success: true })
14974
15807
  } else if (req.body.type === 'env') {
14975
15808
  let envpath = this.kernel.path("ENVIRONMENT")
@@ -15629,12 +16462,7 @@ class Server {
15629
16462
  }))
15630
16463
  this.app.get("/bin_ready", ex(async (req, res) => {
15631
16464
  if (this.kernel.bin && !this.kernel.bin.requirements_pending) {
15632
- let code_exists = await this.kernel.exists("plugin/code")
15633
- if (code_exists) {
15634
- res.json({ success: true })
15635
- } else {
15636
- res.json({ success: false })
15637
- }
16465
+ res.json({ success: true })
15638
16466
  } else {
15639
16467
  res.json({ success: false })
15640
16468
  }