pinokiod 3.180.0 → 3.182.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/kernel/favicon.js +91 -34
  2. package/kernel/peer.js +73 -0
  3. package/kernel/util.js +28 -4
  4. package/package.json +1 -1
  5. package/server/index.js +237 -35
  6. package/server/public/common.js +677 -240
  7. package/server/public/files-app/app.css +64 -0
  8. package/server/public/files-app/app.js +87 -0
  9. package/server/public/install.js +8 -1
  10. package/server/public/layout.js +124 -0
  11. package/server/public/nav.js +227 -64
  12. package/server/public/sound/beep.mp3 +0 -0
  13. package/server/public/sound/bell.mp3 +0 -0
  14. package/server/public/sound/bright-ring.mp3 +0 -0
  15. package/server/public/sound/clap.mp3 +0 -0
  16. package/server/public/sound/deep-ring.mp3 +0 -0
  17. package/server/public/sound/gasp.mp3 +0 -0
  18. package/server/public/sound/hehe.mp3 +0 -0
  19. package/server/public/sound/levelup.mp3 +0 -0
  20. package/server/public/sound/light-pop.mp3 +0 -0
  21. package/server/public/sound/light-ring.mp3 +0 -0
  22. package/server/public/sound/meow.mp3 +0 -0
  23. package/server/public/sound/piano.mp3 +0 -0
  24. package/server/public/sound/pop.mp3 +0 -0
  25. package/server/public/sound/uhoh.mp3 +0 -0
  26. package/server/public/sound/whistle.mp3 +0 -0
  27. package/server/public/style.css +195 -4
  28. package/server/public/tab-idle-notifier.js +700 -4
  29. package/server/public/terminal-settings.js +1131 -0
  30. package/server/public/urldropdown.css +28 -1
  31. package/server/socket.js +71 -4
  32. package/server/views/{terminals.ejs → agents.ejs} +108 -32
  33. package/server/views/app.ejs +321 -104
  34. package/server/views/bootstrap.ejs +8 -0
  35. package/server/views/connect.ejs +10 -1
  36. package/server/views/d.ejs +172 -18
  37. package/server/views/editor.ejs +8 -0
  38. package/server/views/file_browser.ejs +4 -0
  39. package/server/views/index.ejs +10 -1
  40. package/server/views/init/index.ejs +18 -3
  41. package/server/views/install.ejs +8 -0
  42. package/server/views/layout.ejs +2 -0
  43. package/server/views/net.ejs +10 -1
  44. package/server/views/network.ejs +10 -1
  45. package/server/views/pro.ejs +8 -0
  46. package/server/views/prototype/index.ejs +8 -0
  47. package/server/views/screenshots.ejs +10 -2
  48. package/server/views/settings.ejs +10 -2
  49. package/server/views/shell.ejs +8 -0
  50. package/server/views/terminal.ejs +8 -0
  51. package/server/views/tools.ejs +10 -2
package/server/index.js CHANGED
@@ -38,6 +38,7 @@ const ini = require('ini')
38
38
  const ejs = require('ejs');
39
39
 
40
40
  const DEFAULT_PORT = 42000
41
+ const NOTIFICATION_SOUND_EXTENSIONS = new Set(['.aac', '.flac', '.m4a', '.mp3', '.ogg', '.wav', '.webm'])
41
42
 
42
43
  const ex = fn => (req, res, next) => {
43
44
  Promise.resolve(fn(req, res, next)).catch(next);
@@ -1509,10 +1510,15 @@ class Server {
1509
1510
  drive: path.resolve(this.kernel.homedir, "drive"),
1510
1511
  }
1511
1512
  }
1513
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
1514
+ let peer_qr = null
1515
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
1512
1516
  let list = this.getPeers()
1513
1517
  res.render("settings", {
1514
1518
  list,
1515
1519
  current_host: this.kernel.peer.host,
1520
+ peer_url,
1521
+ peer_qr,
1516
1522
  platform,
1517
1523
  version: this.version,
1518
1524
  logo: this.logo,
@@ -2234,6 +2240,10 @@ class Server {
2234
2240
  qr_cloudflare = await QRCode.toDataURL(this.cloudflare_pub)
2235
2241
  }
2236
2242
 
2243
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
2244
+ let peer_qr = null
2245
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
2246
+
2237
2247
  // custom theme
2238
2248
  let exists = await fse.pathExists(this.kernel.path("web"))
2239
2249
  if (exists) {
@@ -2263,6 +2273,8 @@ class Server {
2263
2273
  res.render("index", {
2264
2274
  list,
2265
2275
  current_host: this.kernel.peer.host,
2276
+ peer_url,
2277
+ peer_qr,
2266
2278
  current_urls,
2267
2279
  portal: this.portal,
2268
2280
  install: this.install,
@@ -3433,8 +3445,8 @@ class Server {
3433
3445
  }
3434
3446
  terminal = {
3435
3447
  icon: "fa-solid fa-terminal",
3436
- title: "User Terminal",
3437
- subtitle: "Open the terminal in the browser",
3448
+ title: "Shell",
3449
+ subtitle: "Open an interactive terminal in the browser",
3438
3450
  menu: terminals
3439
3451
  }
3440
3452
  } else {
@@ -3901,6 +3913,46 @@ class Server {
3901
3913
  getTheme: () => this.theme,
3902
3914
  exists: (target) => this.exists(target),
3903
3915
  });
3916
+
3917
+ this.app.get('/pinokio/notification-sounds', ex(async (req, res) => {
3918
+ const soundRoot = path.resolve(__dirname, 'public', 'sound');
3919
+ let entries = [];
3920
+ try {
3921
+ const dirEntries = await fs.promises.readdir(soundRoot, { withFileTypes: true });
3922
+ entries = dirEntries
3923
+ .filter((entry) => entry.isFile())
3924
+ .map((entry) => entry.name)
3925
+ .filter((name) => NOTIFICATION_SOUND_EXTENSIONS.has(path.extname(name).toLowerCase()))
3926
+ .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
3927
+ } catch (error) {
3928
+ if (error && error.code === 'ENOENT') {
3929
+ return res.json({ sounds: [] });
3930
+ }
3931
+ return res.status(500).json({
3932
+ error: 'Failed to enumerate notification sounds',
3933
+ details: error && error.message ? error.message : String(error || ''),
3934
+ });
3935
+ }
3936
+
3937
+ const normalizeLabel = (filename) => {
3938
+ const withoutExt = filename.replace(/\.[^.]+$/, '');
3939
+ return withoutExt
3940
+ .replace(/[-_]+/g, ' ')
3941
+ .replace(/\b\w/g, (char) => char.toUpperCase());
3942
+ };
3943
+
3944
+ const sounds = entries.map((filename) => {
3945
+ const encoded = filename.split('/').map(encodeURIComponent).join('/');
3946
+ return {
3947
+ id: filename,
3948
+ label: normalizeLabel(filename),
3949
+ url: `/sound/${encoded}`,
3950
+ filename,
3951
+ };
3952
+ });
3953
+
3954
+ res.json({ sounds });
3955
+ }));
3904
3956
  /*
3905
3957
  this.app.get("/asset/*", ex((req, res) => {
3906
3958
  let pathComponents = req.params[0].split("/")
@@ -3924,6 +3976,9 @@ class Server {
3924
3976
  }))
3925
3977
  */
3926
3978
  this.app.get("/tools", ex(async (req, res) => {
3979
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
3980
+ let peer_qr = null
3981
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
3927
3982
  let list = this.getPeers()
3928
3983
  let installs = []
3929
3984
  for(let key in this.kernel.bin.installed) {
@@ -3955,6 +4010,8 @@ class Server {
3955
4010
  }
3956
4011
  res.render("tools", {
3957
4012
  current_host: this.kernel.peer.host,
4013
+ peer_url,
4014
+ peer_qr,
3958
4015
  pending,
3959
4016
  installs,
3960
4017
  bundles,
@@ -3966,7 +4023,7 @@ class Server {
3966
4023
  list,
3967
4024
  })
3968
4025
  }))
3969
- this.app.get("/terminals", ex(async (req, res) => {
4026
+ this.app.get("/agents", ex(async (req, res) => {
3970
4027
  if (!this.kernel.plugin.config) {
3971
4028
  try {
3972
4029
  await this.kernel.plugin.init()
@@ -4039,9 +4096,14 @@ class Server {
4039
4096
  return (a.name || '').localeCompare(b.name || '')
4040
4097
  })
4041
4098
 
4099
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
4100
+ let peer_qr = null
4101
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
4042
4102
  const list = this.getPeers()
4043
- res.render("terminals", {
4103
+ res.render("agents", {
4044
4104
  current_host: this.kernel.peer.host,
4105
+ peer_url,
4106
+ peer_qr,
4045
4107
  pluginMenu,
4046
4108
  apps,
4047
4109
  portal: this.portal,
@@ -4051,13 +4113,35 @@ class Server {
4051
4113
  list,
4052
4114
  })
4053
4115
  }))
4116
+ this.app.get("/api/plugin/menu", ex(async (req, res) => {
4117
+ try {
4118
+ if (!this.kernel.plugin.config) {
4119
+ await this.kernel.plugin.init()
4120
+ }
4121
+ const pluginMenu = this.kernel.plugin && this.kernel.plugin.config && Array.isArray(this.kernel.plugin.config.menu)
4122
+ ? this.kernel.plugin.config.menu
4123
+ : []
4124
+ res.json({ menu: pluginMenu })
4125
+ } catch (error) {
4126
+ console.warn('Failed to load plugin menu for create launcher modal', error)
4127
+ res.json({ menu: [] })
4128
+ }
4129
+ }))
4054
4130
  this.app.get("/plugins", (req, res) => {
4055
- res.redirect(301, "/terminals")
4131
+ res.redirect(301, "/agents")
4132
+ })
4133
+ this.app.get("/terminals", (req, res) => {
4134
+ res.redirect(301, "/agents")
4056
4135
  })
4057
4136
  this.app.get("/screenshots", ex(async (req, res) => {
4137
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
4138
+ let peer_qr = null
4139
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
4058
4140
  let list = this.getPeers()
4059
4141
  res.render("screenshots", {
4060
4142
  current_host: this.kernel.peer.host,
4143
+ peer_url,
4144
+ peer_qr,
4061
4145
  version: this.version,
4062
4146
  portal: this.portal,
4063
4147
  logo: this.logo,
@@ -4144,6 +4228,7 @@ class Server {
4144
4228
  // if <app_name>.localhost
4145
4229
  // otherwise => redirect
4146
4230
 
4231
+ console.log("Chunks", chunks)
4147
4232
 
4148
4233
  if (chunks.length >= 2) {
4149
4234
 
@@ -4190,29 +4275,31 @@ class Server {
4190
4275
  } else {
4191
4276
  nameChunks = chunks
4192
4277
  }
4193
- let name = nameChunks.join(".")
4194
- let api_path = this.kernel.path("api", name)
4195
- let exists = await this.exists(api_path)
4196
- if (exists) {
4197
- let meta = await this.kernel.api.meta(name)
4198
- let launcher = await this.kernel.api.launcher(name)
4199
- let pinokio = launcher.script
4200
- let launchable = false
4201
- if (pinokio && pinokio.menu && pinokio.menu.length > 0) {
4202
- launchable = true
4203
- }
4204
- res.render("start", {
4205
- url,
4206
- launchable,
4207
- autolaunch,
4208
- logo: this.logo,
4209
- theme: this.theme,
4210
- agent: req.agent,
4211
- name: meta.title,
4212
- image: meta.icon,
4213
- link: `/p/${name}?autolaunch=${autolaunch ? "1" : "0"}`,
4214
- })
4215
- return
4278
+ if (nameChunks) {
4279
+ let name = nameChunks.join(".")
4280
+ let api_path = this.kernel.path("api", name)
4281
+ let exists = await this.exists(api_path)
4282
+ if (exists) {
4283
+ let meta = await this.kernel.api.meta(name)
4284
+ let launcher = await this.kernel.api.launcher(name)
4285
+ let pinokio = launcher.script
4286
+ let launchable = false
4287
+ if (pinokio && pinokio.menu && pinokio.menu.length > 0) {
4288
+ launchable = true
4289
+ }
4290
+ res.render("start", {
4291
+ url,
4292
+ launchable,
4293
+ autolaunch,
4294
+ logo: this.logo,
4295
+ theme: this.theme,
4296
+ agent: req.agent,
4297
+ name: meta.title,
4298
+ image: meta.icon,
4299
+ link: `/p/${name}?autolaunch=${autolaunch ? "1" : "0"}`,
4300
+ })
4301
+ return
4302
+ }
4216
4303
  }
4217
4304
  }
4218
4305
  res.render("start", {
@@ -4325,9 +4412,14 @@ class Server {
4325
4412
  drive: path.resolve(this.kernel.homedir, "drive"),
4326
4413
  }
4327
4414
  }
4415
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
4416
+ let peer_qr = null
4417
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
4328
4418
  let list = this.getPeers()
4329
4419
  res.render("settings", {
4330
4420
  current_host: this.kernel.peer.host,
4421
+ peer_url,
4422
+ peer_qr,
4331
4423
  list,
4332
4424
  platform,
4333
4425
  version: this.version,
@@ -4443,6 +4535,9 @@ class Server {
4443
4535
  return
4444
4536
  }
4445
4537
 
4538
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
4539
+ let peer_qr = null
4540
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
4446
4541
  let list = this.getPeers()
4447
4542
  let ai = await this.kernel.proto.ai()
4448
4543
  ai = [{
@@ -4456,6 +4551,8 @@ class Server {
4456
4551
  list,
4457
4552
  ai,
4458
4553
  current_host: this.kernel.peer.host,
4554
+ peer_url,
4555
+ peer_qr,
4459
4556
  cwd: this.kernel.path("api"),
4460
4557
  name: null,
4461
4558
  // name: req.params.name,
@@ -4556,9 +4653,14 @@ class Server {
4556
4653
  } catch (e) {
4557
4654
  }
4558
4655
  }
4656
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
4657
+ let peer_qr = null
4658
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
4559
4659
  res.render(`connect`, {
4560
4660
  current_urls,
4561
4661
  current_host: this.kernel.peer.host,
4662
+ peer_url,
4663
+ peer_qr,
4562
4664
  list,
4563
4665
  portal: this.portal,
4564
4666
  logo: this.logo,
@@ -4700,6 +4802,13 @@ class Server {
4700
4802
  this.app.post("/push", ex(async (req, res) => {
4701
4803
  try {
4702
4804
  const payload = { ...(req.body || {}) }
4805
+ // Normalise audience and device targeting
4806
+ if (typeof payload.audience === 'string') {
4807
+ payload.audience = payload.audience.trim() || undefined
4808
+ }
4809
+ if (typeof payload.device_id === 'string') {
4810
+ payload.device_id = payload.device_id.trim() || undefined
4811
+ }
4703
4812
  const resolveAssetPath = (raw) => {
4704
4813
  if (typeof raw !== 'string') {
4705
4814
  return null
@@ -4782,6 +4891,19 @@ class Server {
4782
4891
  }
4783
4892
  delete payload.soundUrl
4784
4893
  delete payload.soundPath
4894
+ // For device-scoped notifications, suppress host OS notifier for remote origins,
4895
+ // but allow it when the request originates from the local machine
4896
+ if (payload.audience === 'device' && typeof payload.device_id === 'string' && payload.device_id) {
4897
+ try {
4898
+ if (this.socket && typeof this.socket.isLocalDevice === 'function') {
4899
+ payload.host = !!this.socket.isLocalDevice(payload.device_id)
4900
+ } else {
4901
+ payload.host = false
4902
+ }
4903
+ } catch (_) {
4904
+ payload.host = false
4905
+ }
4906
+ }
4785
4907
  Util.push(payload)
4786
4908
  res.json({ success: true })
4787
4909
  } catch (e) {
@@ -5343,6 +5465,9 @@ class Server {
5343
5465
  let static_routes = Object.keys(this.kernel.router.rewrite_mapping).map((key) => {
5344
5466
  return this.kernel.router.rewrite_mapping[key]
5345
5467
  })
5468
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
5469
+ let peer_qr = null
5470
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
5346
5471
  res.render("net", {
5347
5472
  static_routes,
5348
5473
  selected_name: req.params.name,
@@ -5361,6 +5486,8 @@ class Server {
5361
5486
  peer,
5362
5487
  protocol,
5363
5488
  current_host: this.kernel.peer.host,
5489
+ peer_url,
5490
+ peer_qr,
5364
5491
  })
5365
5492
  }))
5366
5493
  this.app.get("/network", ex(async (req, res) => {
@@ -5502,6 +5629,9 @@ class Server {
5502
5629
  let static_routes = Object.keys(this.kernel.router.rewrite_mapping).map((key) => {
5503
5630
  return this.kernel.router.rewrite_mapping[key]
5504
5631
  })
5632
+ const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
5633
+ let peer_qr = null
5634
+ try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
5505
5635
  res.render("network", {
5506
5636
  static_routes,
5507
5637
  host,
@@ -5527,6 +5657,8 @@ class Server {
5527
5657
  peer_active: this.kernel.peer.active,
5528
5658
  port_mapping: this.kernel.router.port_mapping,
5529
5659
  // port_mapping: this.kernel.caddy.port_mapping,
5660
+ peer_url,
5661
+ peer_qr,
5530
5662
  // ip_mapping: this.kernel.caddy.ip_mapping,
5531
5663
  lan: this.kernel.router.local_network_mapping,
5532
5664
  agent: req.agent,
@@ -6417,6 +6549,40 @@ class Server {
6417
6549
  let exec_menus = []
6418
6550
  let shell_menus = []
6419
6551
  let href_menus = []
6552
+ const normalizeForSort = (value) => {
6553
+ if (typeof value !== 'string') {
6554
+ return ''
6555
+ }
6556
+ return value.trim().toLocaleLowerCase()
6557
+ }
6558
+ const compareMenuItems = (a = {}, b = {}) => {
6559
+ const titleDiff = normalizeForSort(a.title).localeCompare(normalizeForSort(b.title))
6560
+ if (titleDiff !== 0) {
6561
+ return titleDiff
6562
+ }
6563
+ const subtitleDiff = normalizeForSort(a.subtitle).localeCompare(normalizeForSort(b.subtitle))
6564
+ if (subtitleDiff !== 0) {
6565
+ return subtitleDiff
6566
+ }
6567
+ return normalizeForSort(a.href || a.link).localeCompare(normalizeForSort(b.href || b.link))
6568
+ }
6569
+ const sortMenuEntries = (menuArray) => {
6570
+ if (!Array.isArray(menuArray) || menuArray.length < 2) {
6571
+ return
6572
+ }
6573
+ menuArray.sort(compareMenuItems)
6574
+ }
6575
+ const sortNestedMenus = (menuArray) => {
6576
+ if (!Array.isArray(menuArray)) {
6577
+ return
6578
+ }
6579
+ sortMenuEntries(menuArray)
6580
+ for (const entry of menuArray) {
6581
+ if (entry && Array.isArray(entry.menu)) {
6582
+ sortNestedMenus(entry.menu)
6583
+ }
6584
+ }
6585
+ }
6420
6586
  if (plugin_menu.length > 0) {
6421
6587
  for(let item of plugin_menu) {
6422
6588
  // if shell.run method exists
@@ -6444,30 +6610,37 @@ class Server {
6444
6610
  href_menus.push(item)
6445
6611
  }
6446
6612
  }
6447
- exec_menus.sort((a, b) => { return a > b })
6448
- shell_menus.sort((a, b) => { return a > b })
6449
- href_menus.sort((a, b) => { return a > b })
6613
+ sortNestedMenus(exec_menus)
6614
+ sortNestedMenus(shell_menus)
6615
+ sortNestedMenus(href_menus)
6450
6616
  }
6451
6617
 
6452
6618
  // let terminal = await this.terminals(filepath)
6453
6619
  // let online_terminal = await this.getPluginGlobal(req, terminal, filepath)
6454
6620
  // console.log("online_terminal", online_terminal)
6455
6621
  terminal.menus = href_menus
6622
+ sortNestedMenus(terminal.menu)
6623
+ sortNestedMenus(terminal.menus)
6456
6624
  let dynamic = [
6457
6625
  terminal,
6458
6626
  {
6459
6627
  icon: "fa-solid fa-robot",
6460
- title: "AI Terminal",
6461
- subtitle: "Let AI work on this app",
6628
+ title: "Terminal Agents",
6629
+ subtitle: "Start a session in Pinokio",
6462
6630
  menu: shell_menus
6463
6631
  },
6464
6632
  {
6465
6633
  icon: "fa-solid fa-arrow-up-right-from-square",
6466
- title: "External apps",
6467
- subtitle: "Open this project in 3rd party apps",
6634
+ title: "IDE Agents",
6635
+ subtitle: "Open the project in external IDEs",
6468
6636
  menu: exec_menus
6469
6637
  },
6470
6638
  ]
6639
+ for (const item of dynamic) {
6640
+ if (item && Array.isArray(item.menu)) {
6641
+ sortNestedMenus(item.menu)
6642
+ }
6643
+ }
6471
6644
 
6472
6645
  let spec = ""
6473
6646
  try {
@@ -7059,6 +7232,35 @@ class Server {
7059
7232
  let current_peer_info = await this.kernel.peer.current_host()
7060
7233
  res.json(current_peer_info)
7061
7234
  }))
7235
+ this.app.get("/info/router", ex(async (req, res) => {
7236
+ try {
7237
+ // Lightweight router mapping without favicon or installed scans
7238
+ const https_active = this.kernel.peer.https_active
7239
+ const router_info = await this.kernel.peer.router_info_lite()
7240
+ const rewrite_mapping = this.kernel.router.rewrite_mapping
7241
+ const router = this.kernel.router.published()
7242
+ res.json({ https_active, router_info, rewrite_mapping, router })
7243
+ } catch (err) {
7244
+ res.json({ https_active: false, router_info: [], rewrite_mapping: {}, router: {} })
7245
+ }
7246
+ }))
7247
+ this.app.get("/qr", ex(async (req, res) => {
7248
+ try {
7249
+ const data = typeof req.query.data === 'string' ? req.query.data : ''
7250
+ if (!data) {
7251
+ res.status(400).json({ error: 'Missing data parameter' })
7252
+ return
7253
+ }
7254
+ const scale = Math.max(2, Math.min(10, parseInt(req.query.s || '4', 10) || 4))
7255
+ const margin = Math.max(0, Math.min(4, parseInt(req.query.m || '0', 10) || 0))
7256
+ const buf = await QRCode.toBuffer(data, { type: 'png', scale, margin })
7257
+ res.setHeader('Content-Type', 'image/png')
7258
+ res.setHeader('Cache-Control', 'no-store')
7259
+ res.send(buf)
7260
+ } catch (err) {
7261
+ res.status(500).json({ error: 'Failed to generate QR' })
7262
+ }
7263
+ }))
7062
7264
  this.app.get("/info/api", ex(async (req,res) => {
7063
7265
  // api related info
7064
7266
  let repo = this.kernel.git.find(req.query.git)