pinokiod 3.271.0 → 3.273.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 (55) hide show
  1. package/kernel/ansi_stream_tracker.js +115 -0
  2. package/kernel/api/app/index.js +422 -0
  3. package/kernel/api/htmlmodal/index.js +94 -0
  4. package/kernel/app_launcher/index.js +115 -0
  5. package/kernel/app_launcher/platform/base.js +276 -0
  6. package/kernel/app_launcher/platform/linux.js +229 -0
  7. package/kernel/app_launcher/platform/macos.js +163 -0
  8. package/kernel/app_launcher/platform/unsupported.js +34 -0
  9. package/kernel/app_launcher/platform/windows.js +247 -0
  10. package/kernel/bin/conda-meta.js +93 -0
  11. package/kernel/bin/conda.js +2 -4
  12. package/kernel/bin/index.js +2 -4
  13. package/kernel/index.js +7 -0
  14. package/kernel/shell.js +212 -1
  15. package/package.json +1 -1
  16. package/server/index.js +491 -6
  17. package/server/public/common.js +224 -741
  18. package/server/public/create-launcher.js +754 -0
  19. package/server/public/htmlmodal.js +292 -0
  20. package/server/public/logs.js +715 -0
  21. package/server/public/resizeSync.js +117 -0
  22. package/server/public/style.css +653 -8
  23. package/server/public/tab-idle-notifier.js +34 -59
  24. package/server/public/tab-link-popover.js +7 -10
  25. package/server/public/terminal-settings.js +723 -9
  26. package/server/public/terminal_input_utils.js +72 -0
  27. package/server/public/terminal_key_caption.js +187 -0
  28. package/server/public/urldropdown.css +120 -3
  29. package/server/public/xterm-inline-bridge.js +116 -0
  30. package/server/socket.js +29 -0
  31. package/server/views/agents.ejs +1 -2
  32. package/server/views/app.ejs +55 -28
  33. package/server/views/bookmarklet.ejs +1 -1
  34. package/server/views/bootstrap.ejs +1 -0
  35. package/server/views/connect.ejs +1 -2
  36. package/server/views/create.ejs +63 -0
  37. package/server/views/editor.ejs +36 -4
  38. package/server/views/index.ejs +1 -2
  39. package/server/views/index2.ejs +1 -2
  40. package/server/views/init/index.ejs +36 -28
  41. package/server/views/install.ejs +20 -22
  42. package/server/views/layout.ejs +2 -8
  43. package/server/views/logs.ejs +155 -0
  44. package/server/views/mini.ejs +0 -18
  45. package/server/views/net.ejs +2 -2
  46. package/server/views/network.ejs +1 -2
  47. package/server/views/network2.ejs +1 -2
  48. package/server/views/old_network.ejs +1 -2
  49. package/server/views/pro.ejs +26 -23
  50. package/server/views/prototype/index.ejs +30 -34
  51. package/server/views/screenshots.ejs +1 -2
  52. package/server/views/settings.ejs +1 -20
  53. package/server/views/shell.ejs +59 -66
  54. package/server/views/terminal.ejs +118 -73
  55. package/server/views/tools.ejs +1 -2
package/server/index.js CHANGED
@@ -40,6 +40,8 @@ const ejs = require('ejs');
40
40
 
41
41
  const DEFAULT_PORT = 42000
42
42
  const NOTIFICATION_SOUND_EXTENSIONS = new Set(['.aac', '.flac', '.m4a', '.mp3', '.ogg', '.wav', '.webm'])
43
+ const LOG_STREAM_INITIAL_BYTES = 512 * 1024
44
+ const LOG_STREAM_KEEPALIVE_MS = 25000
43
45
 
44
46
  const ex = fn => (req, res, next) => {
45
47
  Promise.resolve(fn(req, res, next)).catch(next);
@@ -3124,6 +3126,116 @@ class Server {
3124
3126
  return { peer_access_points, peer_url, peer_qr }
3125
3127
  }
3126
3128
 
3129
+ async ensureLogsRootDirectory() {
3130
+ const logsRoot = path.resolve(this.kernel.path("logs"))
3131
+ await fs.promises.mkdir(logsRoot, { recursive: true })
3132
+ return logsRoot
3133
+ }
3134
+ async resolveLogsRoot(options = {}) {
3135
+ const workspace = typeof options.workspace === 'string' ? options.workspace.trim() : ''
3136
+ if (workspace) {
3137
+ const apiRoot = path.resolve(this.kernel.path("api"))
3138
+ const segments = workspace.replace(/\\+/g, '/').split('/').map((segment) => segment.trim()).filter((segment) => segment.length > 0 && segment !== '.')
3139
+ if (segments.length === 0) {
3140
+ throw new Error('Workspace not found')
3141
+ }
3142
+ const normalized = segments.join('/')
3143
+ const workspacePath = path.resolve(apiRoot, normalized)
3144
+ const relative = path.relative(apiRoot, workspacePath)
3145
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
3146
+ throw new Error('Invalid workspace path')
3147
+ }
3148
+ let workspaceStats
3149
+ try {
3150
+ workspaceStats = await fs.promises.stat(workspacePath)
3151
+ } catch (error) {
3152
+ if (error.code === 'ENOENT') {
3153
+ throw new Error('Workspace not found')
3154
+ }
3155
+ throw error
3156
+ }
3157
+ if (!workspaceStats.isDirectory()) {
3158
+ throw new Error('Workspace path is not a directory')
3159
+ }
3160
+ const candidate = path.resolve(workspacePath, 'logs')
3161
+ await fs.promises.mkdir(candidate, { recursive: true })
3162
+ return {
3163
+ logsRoot: candidate,
3164
+ displayPath: this.formatLogsDisplayPath(candidate),
3165
+ title: normalized
3166
+ }
3167
+ }
3168
+ const logsRoot = await this.ensureLogsRootDirectory()
3169
+ return {
3170
+ logsRoot,
3171
+ displayPath: this.formatLogsDisplayPath(logsRoot),
3172
+ title: null
3173
+ }
3174
+ }
3175
+ sanitizeWorkspaceForFilename(workspace) {
3176
+ if (!workspace || typeof workspace !== 'string') {
3177
+ return 'workspace'
3178
+ }
3179
+ const sanitized = workspace.replace(/[^a-zA-Z0-9._-]/g, '_')
3180
+ return sanitized.length > 0 ? sanitized : 'workspace'
3181
+ }
3182
+ async removeRouterSnapshots(targetDir) {
3183
+ try {
3184
+ const entries = await fs.promises.readdir(targetDir, { withFileTypes: true })
3185
+ for (const entry of entries) {
3186
+ if (entry.isFile() && /^router-default-\d+\.json$/.test(entry.name)) {
3187
+ await fs.promises.rm(path.join(targetDir, entry.name)).catch(() => {})
3188
+ }
3189
+ }
3190
+ } catch (_) {}
3191
+ }
3192
+ formatLogsDisplayPath(absolutePath) {
3193
+ if (!absolutePath) {
3194
+ return ''
3195
+ }
3196
+ const systemHome = os.homedir ? path.resolve(os.homedir()) : null
3197
+ if (systemHome) {
3198
+ const relativeToSystem = path.relative(systemHome, absolutePath)
3199
+ if (!relativeToSystem || (!relativeToSystem.startsWith('..') && !path.isAbsolute(relativeToSystem))) {
3200
+ if (!relativeToSystem) {
3201
+ return '~'
3202
+ }
3203
+ const normalized = relativeToSystem.split(path.sep).join('/')
3204
+ return `~/${normalized}`
3205
+ }
3206
+ }
3207
+ const configuredHome = this.kernel.homedir ? path.resolve(this.kernel.homedir) : null
3208
+ if (configuredHome) {
3209
+ const relativeToConfigured = path.relative(configuredHome, absolutePath)
3210
+ if (!relativeToConfigured || (!relativeToConfigured.startsWith('..') && !path.isAbsolute(relativeToConfigured))) {
3211
+ if (!relativeToConfigured) {
3212
+ return '~'
3213
+ }
3214
+ const normalized = relativeToConfigured.split(path.sep).join('/')
3215
+ return `~/${normalized}`
3216
+ }
3217
+ }
3218
+ return absolutePath
3219
+ }
3220
+ formatLogsRelativePath(relativePath = '') {
3221
+ if (!relativePath || relativePath === '.') {
3222
+ return ''
3223
+ }
3224
+ return relativePath.split(path.sep).join('/')
3225
+ }
3226
+ resolveLogsAbsolutePath(logsRoot, requestedPath = '') {
3227
+ const trimmed = typeof requestedPath === 'string' ? requestedPath.trim() : ''
3228
+ const normalizedRequest = trimmed ? path.normalize(trimmed) : '.'
3229
+ const absolutePath = path.resolve(logsRoot, normalizedRequest)
3230
+ const relativePath = path.relative(logsRoot, absolutePath)
3231
+ if (relativePath && (relativePath.startsWith('..') || path.isAbsolute(relativePath))) {
3232
+ throw new Error('INVALID_LOGS_PATH')
3233
+ }
3234
+ return {
3235
+ absolutePath,
3236
+ relativePath: relativePath === '.' ? '' : relativePath
3237
+ }
3238
+ }
3127
3239
 
3128
3240
  async syncConfig() {
3129
3241
 
@@ -4311,6 +4423,263 @@ class Server {
4311
4423
  list,
4312
4424
  })
4313
4425
  }))
4426
+ this.app.get("/logs", ex(async (req, res) => {
4427
+ const peerAccess = await this.composePeerAccessPayload()
4428
+ const list = this.getPeers()
4429
+ const workspace = typeof req.query.workspace === 'string' ? req.query.workspace.trim() : ''
4430
+ let context
4431
+ const downloadUrl = workspace ? `/pinokio/logs.zip?workspace=${encodeURIComponent(workspace)}` : '/pinokio/logs.zip'
4432
+ try {
4433
+ context = await this.resolveLogsRoot({ workspace })
4434
+ } catch (error) {
4435
+ res.status(404).render("logs", {
4436
+ current_host: this.kernel.peer.host,
4437
+ ...peerAccess,
4438
+ portal: this.portal,
4439
+ logo: this.logo,
4440
+ theme: this.theme,
4441
+ agent: req.agent,
4442
+ list,
4443
+ logsRootDisplay: '',
4444
+ logsWorkspace: workspace || null,
4445
+ logsTitle: workspace || null,
4446
+ logsError: error && error.message ? error.message : 'Workspace not found',
4447
+ logsDownloadUrl: downloadUrl,
4448
+ })
4449
+ return
4450
+ }
4451
+ res.render("logs", {
4452
+ current_host: this.kernel.peer.host,
4453
+ ...peerAccess,
4454
+ portal: this.portal,
4455
+ logo: this.logo,
4456
+ theme: this.theme,
4457
+ agent: req.agent,
4458
+ list,
4459
+ logsRootDisplay: context.displayPath,
4460
+ logsWorkspace: workspace || null,
4461
+ logsTitle: context.title,
4462
+ logsError: null,
4463
+ logsDownloadUrl: downloadUrl,
4464
+ })
4465
+ }))
4466
+ this.app.get("/api/logs/tree", ex(async (req, res) => {
4467
+ const workspace = typeof req.query.workspace === 'string' ? req.query.workspace.trim() : ''
4468
+ let context
4469
+ try {
4470
+ context = await this.resolveLogsRoot({ workspace })
4471
+ } catch (error) {
4472
+ res.status(404).json({ error: error && error.message ? error.message : 'Workspace not found' })
4473
+ return
4474
+ }
4475
+ const logsRoot = context.logsRoot
4476
+ let descriptor
4477
+ try {
4478
+ descriptor = this.resolveLogsAbsolutePath(logsRoot, req.query.path || '')
4479
+ } catch (_) {
4480
+ res.status(400).json({ error: "Invalid path" })
4481
+ return
4482
+ }
4483
+ let stats
4484
+ try {
4485
+ stats = await fs.promises.stat(descriptor.absolutePath)
4486
+ } catch (error) {
4487
+ res.status(404).json({ error: "Path not found" })
4488
+ return
4489
+ }
4490
+ if (!stats.isDirectory()) {
4491
+ res.status(400).json({ error: "Path is not a directory" })
4492
+ return
4493
+ }
4494
+ let dirents
4495
+ try {
4496
+ dirents = await fs.promises.readdir(descriptor.absolutePath, { withFileTypes: true })
4497
+ } catch (error) {
4498
+ res.status(500).json({ error: "Failed to read directory", detail: error.message })
4499
+ return
4500
+ }
4501
+ const entries = []
4502
+ for (const dirent of dirents) {
4503
+ if (dirent.name === '.' || dirent.name === '..') {
4504
+ continue
4505
+ }
4506
+ const entryPath = path.join(descriptor.absolutePath, dirent.name)
4507
+ let entryStats
4508
+ try {
4509
+ entryStats = await fs.promises.stat(entryPath)
4510
+ } catch (error) {
4511
+ continue
4512
+ }
4513
+ const relativePath = path.relative(logsRoot, entryPath)
4514
+ entries.push({
4515
+ name: dirent.name,
4516
+ path: this.formatLogsRelativePath(relativePath),
4517
+ type: entryStats.isDirectory() ? "directory" : "file",
4518
+ size: entryStats.isDirectory() ? null : entryStats.size,
4519
+ modified: entryStats.mtime
4520
+ })
4521
+ }
4522
+ entries.sort((a, b) => {
4523
+ if (a.type === b.type) {
4524
+ return a.name.localeCompare(b.name)
4525
+ }
4526
+ return a.type === "directory" ? -1 : 1
4527
+ })
4528
+ res.set("Cache-Control", "no-store")
4529
+ res.json({
4530
+ path: this.formatLogsRelativePath(descriptor.relativePath),
4531
+ entries
4532
+ })
4533
+ }))
4534
+ this.app.get("/api/logs/stream", ex(async (req, res) => {
4535
+ const workspace = typeof req.query.workspace === 'string' ? req.query.workspace.trim() : ''
4536
+ let context
4537
+ try {
4538
+ context = await this.resolveLogsRoot({ workspace })
4539
+ } catch (error) {
4540
+ res.status(404).json({ error: error && error.message ? error.message : 'Workspace not found' })
4541
+ return
4542
+ }
4543
+ const logsRoot = context.logsRoot
4544
+ let descriptor
4545
+ try {
4546
+ descriptor = this.resolveLogsAbsolutePath(logsRoot, req.query.path || '')
4547
+ } catch (_) {
4548
+ res.status(400).json({ error: "Invalid path" })
4549
+ return
4550
+ }
4551
+ let stats
4552
+ try {
4553
+ stats = await fs.promises.stat(descriptor.absolutePath)
4554
+ } catch (error) {
4555
+ res.status(404).json({ error: "File not found" })
4556
+ return
4557
+ }
4558
+ if (!stats.isFile()) {
4559
+ res.status(400).json({ error: "Path is not a file" })
4560
+ return
4561
+ }
4562
+ res.writeHead(200, {
4563
+ "Content-Type": "text/event-stream",
4564
+ "Cache-Control": "no-cache, no-transform",
4565
+ Connection: "keep-alive"
4566
+ })
4567
+ if (res.flushHeaders) {
4568
+ res.flushHeaders()
4569
+ }
4570
+ if (req.socket && req.socket.setKeepAlive) {
4571
+ req.socket.setKeepAlive(true)
4572
+ }
4573
+ if (req.socket && req.socket.setNoDelay) {
4574
+ req.socket.setNoDelay(true)
4575
+ }
4576
+
4577
+ const sendEvent = (eventName, payload) => {
4578
+ if (res.writableEnded) {
4579
+ return
4580
+ }
4581
+ res.write(`event: ${eventName}
4582
+ `)
4583
+ res.write(`data: ${JSON.stringify(payload)}
4584
+
4585
+ `)
4586
+ }
4587
+ res.write(`retry: 2000
4588
+
4589
+ `)
4590
+
4591
+ let watcher
4592
+ let keepAliveTimer
4593
+ let closed = false
4594
+ const cleanup = () => {
4595
+ if (closed) {
4596
+ return
4597
+ }
4598
+ closed = true
4599
+ if (keepAliveTimer) {
4600
+ clearInterval(keepAliveTimer)
4601
+ }
4602
+ if (watcher) {
4603
+ watcher.close()
4604
+ }
4605
+ if (!res.writableEnded) {
4606
+ res.end()
4607
+ }
4608
+ }
4609
+
4610
+ req.on("close", cleanup)
4611
+ req.on("error", cleanup)
4612
+
4613
+ keepAliveTimer = setInterval(() => {
4614
+ if (!res.writableEnded) {
4615
+ res.write(`: keep-alive ${Date.now()}
4616
+
4617
+ `)
4618
+ }
4619
+ }, LOG_STREAM_KEEPALIVE_MS)
4620
+
4621
+ const streamRange = (start, end) => {
4622
+ return new Promise((resolve, reject) => {
4623
+ if (end <= start) {
4624
+ resolve()
4625
+ return
4626
+ }
4627
+ const reader = fs.createReadStream(descriptor.absolutePath, {
4628
+ encoding: "utf8",
4629
+ start,
4630
+ end: end - 1
4631
+ })
4632
+ reader.on("data", (chunk) => {
4633
+ sendEvent("chunk", { data: chunk })
4634
+ })
4635
+ reader.on("error", reject)
4636
+ reader.on("end", resolve)
4637
+ })
4638
+ }
4639
+
4640
+ const initialStart = Math.max(0, stats.size - LOG_STREAM_INITIAL_BYTES)
4641
+ sendEvent("snapshot", {
4642
+ path: this.formatLogsRelativePath(descriptor.relativePath),
4643
+ size: stats.size,
4644
+ truncated: initialStart > 0
4645
+ })
4646
+ try {
4647
+ await streamRange(initialStart, stats.size)
4648
+ } catch (error) {
4649
+ sendEvent("server-error", { message: error.message || "Failed to read log file" })
4650
+ cleanup()
4651
+ return
4652
+ }
4653
+ let cursor = stats.size
4654
+ sendEvent("ready", { cursor })
4655
+
4656
+ try {
4657
+ watcher = fs.watch(descriptor.absolutePath, async (eventType) => {
4658
+ if (eventType === "rename") {
4659
+ sendEvent("rotate", { message: "File rotated or removed" })
4660
+ cleanup()
4661
+ return
4662
+ }
4663
+ try {
4664
+ const nextStats = await fs.promises.stat(descriptor.absolutePath)
4665
+ if (nextStats.size < cursor) {
4666
+ cursor = 0
4667
+ sendEvent("reset", { reason: "truncate" })
4668
+ }
4669
+ if (nextStats.size > cursor) {
4670
+ await streamRange(cursor, nextStats.size)
4671
+ cursor = nextStats.size
4672
+ }
4673
+ } catch (error) {
4674
+ sendEvent("server-error", { message: error.message || "Streaming stopped" })
4675
+ cleanup()
4676
+ }
4677
+ })
4678
+ } catch (error) {
4679
+ sendEvent("server-error", { message: error.message || "Unable to watch file" })
4680
+ cleanup()
4681
+ }
4682
+ }))
4314
4683
  this.app.get("/columns", ex(async (req, res) => {
4315
4684
  const originSrc = req.query.origin || req.get('Referrer') || '/';
4316
4685
  const targetSrc = req.query.target || originSrc;
@@ -4347,7 +4716,7 @@ class Server {
4347
4716
  const protocol = (req.$source && req.$source.protocol) || req.protocol || 'http';
4348
4717
  const host = req.get('host') || `localhost:${this.port}`;
4349
4718
  const baseUrl = `${protocol}://${host}`;
4350
- const targetBase = `${baseUrl}/?create=1&prompt=`;
4719
+ const targetBase = `${baseUrl}/create?prompt=`;
4351
4720
  const safeTargetBase = targetBase.replace(/'/g, "\\'");
4352
4721
  const bookmarkletHref = `javascript:(()=>{window.open('${safeTargetBase}'+encodeURIComponent(window.location.href),'_blank');})();`;
4353
4722
 
@@ -4476,6 +4845,24 @@ class Server {
4476
4845
  })
4477
4846
  }))
4478
4847
  const renderHomePage = ex(async (req, res) => {
4848
+ if (Object.prototype.hasOwnProperty.call(req.query, 'create')) {
4849
+ const protocol = (req.$source && req.$source.protocol) || req.protocol || 'http'
4850
+ const host = req.get('host') || `localhost:${this.port}`
4851
+ const baseUrl = `${protocol}://${host}`
4852
+ const target = new URL('/create', baseUrl)
4853
+ for (const [key, value] of Object.entries(req.query)) {
4854
+ if (key === 'create' || key === 'session') {
4855
+ continue
4856
+ }
4857
+ if (Array.isArray(value)) {
4858
+ value.forEach((val) => target.searchParams.append(key, val))
4859
+ } else if (value != null) {
4860
+ target.searchParams.set(key, value)
4861
+ }
4862
+ }
4863
+ res.redirect(target.pathname + target.search + target.hash)
4864
+ return
4865
+ }
4479
4866
  // check bin folder
4480
4867
  // let bin_path = this.kernel.path("bin/miniconda")
4481
4868
  // let bin_exists = await this.exists(bin_path)
@@ -4616,11 +5003,12 @@ class Server {
4616
5003
  const host = req.get('host') || `localhost:${this.port}`
4617
5004
  const baseUrl = `${protocol}://${host}`
4618
5005
 
4619
- const initialUrl = new URL('/home', baseUrl)
5006
+ const wantsCreatePage = Object.prototype.hasOwnProperty.call(req.query, 'create')
5007
+ const initialUrl = new URL(wantsCreatePage ? '/create/page' : '/home', baseUrl)
4620
5008
  const defaultUrl = new URL('/home', baseUrl)
4621
5009
 
4622
5010
  for (const [key, value] of Object.entries(req.query)) {
4623
- if (key === 'session') {
5011
+ if (key === 'session' || key === 'create') {
4624
5012
  continue
4625
5013
  }
4626
5014
  if (Array.isArray(value)) {
@@ -4649,6 +5037,76 @@ class Server {
4649
5037
  })
4650
5038
  }))
4651
5039
 
5040
+ this.app.get("/create", ex(async (req, res) => {
5041
+ const protocol = (req.$source && req.$source.protocol) || req.protocol || 'http'
5042
+ const host = req.get('host') || `localhost:${this.port}`
5043
+ const baseUrl = `${protocol}://${host}`
5044
+
5045
+ const initialUrl = new URL('/create/page', baseUrl)
5046
+ const defaultUrl = new URL('/home', baseUrl)
5047
+
5048
+ for (const [key, value] of Object.entries(req.query)) {
5049
+ if (key === 'session' || key === 'create') {
5050
+ continue
5051
+ }
5052
+ if (Array.isArray(value)) {
5053
+ value.forEach((val) => initialUrl.searchParams.append(key, val))
5054
+ } else if (value != null) {
5055
+ initialUrl.searchParams.set(key, value)
5056
+ }
5057
+ }
5058
+
5059
+ if (!home) {
5060
+ defaultUrl.searchParams.set('mode', 'settings')
5061
+ }
5062
+
5063
+ res.render('layout', {
5064
+ platform: this.kernel.platform,
5065
+ theme: this.theme,
5066
+ agent: req.agent,
5067
+ initialPath: initialUrl.pathname + initialUrl.search + initialUrl.hash,
5068
+ defaultPath: defaultUrl.pathname + defaultUrl.search + defaultUrl.hash,
5069
+ sessionId: typeof req.query.session === 'string' ? req.query.session : null
5070
+ })
5071
+ }))
5072
+
5073
+ this.app.get("/create/page", ex(async (req, res) => {
5074
+ const defaults = {}
5075
+ const templateDefaults = {}
5076
+
5077
+ if (typeof req.query.prompt === 'string' && req.query.prompt.trim()) {
5078
+ defaults.prompt = req.query.prompt.trim()
5079
+ }
5080
+ if (typeof req.query.folder === 'string' && req.query.folder.trim()) {
5081
+ defaults.folder = req.query.folder.trim()
5082
+ }
5083
+ if (typeof req.query.tool === 'string' && req.query.tool.trim()) {
5084
+ defaults.tool = req.query.tool.trim()
5085
+ }
5086
+
5087
+ for (const [key, value] of Object.entries(req.query)) {
5088
+ if ((key.startsWith('template.') || key.startsWith('template_')) && typeof value === 'string') {
5089
+ const name = key.replace(/^template[._]/, '')
5090
+ if (name) {
5091
+ templateDefaults[name] = value.trim()
5092
+ }
5093
+ }
5094
+ }
5095
+
5096
+ if (Object.keys(templateDefaults).length > 0) {
5097
+ defaults.templateValues = templateDefaults
5098
+ }
5099
+
5100
+ res.render('create', {
5101
+ theme: this.theme,
5102
+ agent: req.agent,
5103
+ logo: this.logo,
5104
+ portal: this.portal,
5105
+ paths: [],
5106
+ defaults,
5107
+ })
5108
+ }))
5109
+
4652
5110
  this.app.get("/home", renderHomePage)
4653
5111
 
4654
5112
 
@@ -7687,12 +8145,38 @@ class Server {
7687
8145
  res.json({ error: err.stack })
7688
8146
  }
7689
8147
  }))
7690
- this.app.get("/pinokio/logs.zip", ex((req, res) => {
8148
+ this.app.get("/pinokio/logs.zip", ex(async (req, res) => {
8149
+ const workspace = typeof req.query.workspace === 'string' ? req.query.workspace.trim() : ''
8150
+ if (workspace) {
8151
+ const safeName = this.sanitizeWorkspaceForFilename(workspace)
8152
+ const zipPath = this.kernel.path(`logs-${safeName}.zip`)
8153
+ try {
8154
+ await fs.promises.access(zipPath, fs.constants.F_OK)
8155
+ } catch (_) {
8156
+ res.status(404).send('Workspace archive not found. Generate a new archive and try again.')
8157
+ return
8158
+ }
8159
+ res.download(zipPath, `${safeName}-logs.zip`)
8160
+ return
8161
+ }
7691
8162
  let zipPath = this.kernel.path("logs.zip")
7692
8163
  res.download(zipPath)
7693
8164
  }))
7694
8165
  this.app.post("/pinokio/log", ex(async (req, res) => {
7695
-
8166
+ const workspace = typeof req.query.workspace === 'string' ? req.query.workspace.trim() : ''
8167
+ if (workspace) {
8168
+ try {
8169
+ const context = await this.resolveLogsRoot({ workspace })
8170
+ const safeName = this.sanitizeWorkspaceForFilename(workspace)
8171
+ const zipPath = this.kernel.path(`logs-${safeName}.zip`)
8172
+ await fs.promises.rm(zipPath, { force: true }).catch(() => {})
8173
+ await compressing.zip.compressDir(context.logsRoot, zipPath)
8174
+ res.json({ success: true, download: `/pinokio/logs.zip?workspace=${encodeURIComponent(workspace)}` })
8175
+ } catch (error) {
8176
+ res.status(404).json({ error: error && error.message ? error.message : 'Workspace not found' })
8177
+ }
8178
+ return
8179
+ }
7696
8180
 
7697
8181
  let states = this.kernel.shell.shells.map((s) => {
7698
8182
  return {
@@ -7728,13 +8212,14 @@ class Server {
7728
8212
  this.kernel.path("logs"),
7729
8213
  this.kernel.path("exported_logs")
7730
8214
  , { recursive: true })
8215
+ await this.removeRouterSnapshots(this.kernel.path("exported_logs"))
7731
8216
  await this.kernel.shell.logs()
7732
8217
 
7733
8218
 
7734
8219
  let folder = this.kernel.path("exported_logs")
7735
8220
  let zipPath = this.kernel.path("logs.zip")
7736
8221
  await compressing.zip.compressDir(folder, zipPath)
7737
- res.json({ success: true })
8222
+ res.json({ success: true, download: '/pinokio/logs.zip' })
7738
8223
  }))
7739
8224
  this.app.get("/pinokio/version", ex(async (req, res) => {
7740
8225
  let version = this.version