pinokiod 3.270.0 → 3.272.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.
- package/kernel/ansi_stream_tracker.js +115 -0
- package/kernel/api/app/index.js +422 -0
- package/kernel/api/htmlmodal/index.js +94 -0
- package/kernel/app_launcher/index.js +115 -0
- package/kernel/app_launcher/platform/base.js +276 -0
- package/kernel/app_launcher/platform/linux.js +229 -0
- package/kernel/app_launcher/platform/macos.js +163 -0
- package/kernel/app_launcher/platform/unsupported.js +34 -0
- package/kernel/app_launcher/platform/windows.js +247 -0
- package/kernel/bin/conda-meta.js +93 -0
- package/kernel/bin/conda.js +2 -4
- package/kernel/bin/index.js +2 -4
- package/kernel/index.js +9 -2
- package/kernel/peer.js +186 -19
- package/kernel/shell.js +212 -1
- package/package.json +1 -1
- package/server/index.js +491 -6
- package/server/public/common.js +224 -741
- package/server/public/create-launcher.js +754 -0
- package/server/public/htmlmodal.js +292 -0
- package/server/public/logs.js +715 -0
- package/server/public/resizeSync.js +117 -0
- package/server/public/style.css +651 -6
- package/server/public/tab-idle-notifier.js +34 -59
- package/server/public/tab-link-popover.js +7 -10
- package/server/public/terminal-settings.js +723 -9
- package/server/public/terminal_input_utils.js +72 -0
- package/server/public/terminal_key_caption.js +187 -0
- package/server/public/urldropdown.css +120 -3
- package/server/public/xterm-inline-bridge.js +116 -0
- package/server/socket.js +29 -0
- package/server/views/agents.ejs +1 -2
- package/server/views/app.ejs +55 -28
- package/server/views/bookmarklet.ejs +1 -1
- package/server/views/bootstrap.ejs +1 -0
- package/server/views/connect.ejs +1 -2
- package/server/views/create.ejs +63 -0
- package/server/views/editor.ejs +36 -4
- package/server/views/index.ejs +1 -2
- package/server/views/index2.ejs +1 -2
- package/server/views/init/index.ejs +36 -28
- package/server/views/install.ejs +20 -22
- package/server/views/layout.ejs +2 -8
- package/server/views/logs.ejs +155 -0
- package/server/views/mini.ejs +0 -18
- package/server/views/net.ejs +2 -2
- package/server/views/network.ejs +1 -2
- package/server/views/network2.ejs +1 -2
- package/server/views/old_network.ejs +1 -2
- package/server/views/pro.ejs +26 -23
- package/server/views/prototype/index.ejs +30 -34
- package/server/views/screenshots.ejs +1 -2
- package/server/views/settings.ejs +1 -20
- package/server/views/shell.ejs +59 -66
- package/server/views/terminal.ejs +118 -73
- 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}
|
|
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
|
|
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
|