pinokiod 7.5.24 → 7.5.26
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/api/index.js +29 -1
- package/kernel/app_log_sessions.js +424 -0
- package/kernel/index.js +0 -3
- package/kernel/procs.js +4 -2
- package/kernel/router/localhost_static_router.js +5 -3
- package/package.json +1 -1
- package/server/index.js +183 -32
- package/server/lib/app_log_report.js +126 -61
- package/server/lib/log_redaction.js +273 -0
- package/server/public/logs-top-redaction.css +154 -0
- package/server/public/logs-top-redaction.js +711 -0
- package/server/public/logs.js +169 -42
- package/server/public/style.css +62 -23
- package/server/routes/apps.js +2 -1
- package/server/socket.js +18 -1
- package/server/views/logs.ejs +17 -7
- package/server/views/net.ejs +85 -6
- package/server/views/partials/logs_top_redaction_actions.ejs +2 -0
- package/server/views/partials/logs_top_redaction_pane.ejs +25 -0
- package/server/views/partials/net_route_list.ejs +278 -0
- package/server/views/partials/net_summary.ejs +10 -0
- package/test/app-log-report.test.js +224 -17
- package/test/app-log-sessions.test.js +518 -0
- package/test/dns-static-routes.test.js +146 -0
- package/test/log-redaction-overrides.test.js +157 -0
- package/test/logs-ask-ai.test.js +194 -3
- package/test/procs.test.js +3 -3
- package/test/terminal-log-paths.test.js +47 -0
package/server/index.js
CHANGED
|
@@ -84,6 +84,7 @@ const { createTaskPackageService } = require("./lib/task_packages")
|
|
|
84
84
|
const { createTaskWorkspaceLinkService } = require("./lib/task_workspace_links")
|
|
85
85
|
const { createContentValidationService } = require("./lib/content_validation")
|
|
86
86
|
const { buildSecureRouterDebugSnapshot, createSecureRouterDebugStore } = require("./lib/secure_router_debug")
|
|
87
|
+
const logRedaction = require("./lib/log_redaction")
|
|
87
88
|
const AppRegistryService = require("./lib/app_registry")
|
|
88
89
|
const AppLogService = require("./lib/app_logs")
|
|
89
90
|
const AppLogReportService = require("./lib/app_log_report")
|
|
@@ -858,6 +859,50 @@ class Server {
|
|
|
858
859
|
}
|
|
859
860
|
return cfg
|
|
860
861
|
}
|
|
862
|
+
defaultAppLogLaunchPath(menu) {
|
|
863
|
+
const stack = Array.isArray(menu) ? [...menu] : []
|
|
864
|
+
while (stack.length > 0) {
|
|
865
|
+
const item = stack.shift()
|
|
866
|
+
if (!item || typeof item !== "object") {
|
|
867
|
+
continue
|
|
868
|
+
}
|
|
869
|
+
if (Array.isArray(item.menu)) {
|
|
870
|
+
stack.unshift(...item.menu)
|
|
871
|
+
}
|
|
872
|
+
if (!item.default || typeof item.href !== "string") {
|
|
873
|
+
continue
|
|
874
|
+
}
|
|
875
|
+
let pathname
|
|
876
|
+
try {
|
|
877
|
+
pathname = new URL(item.href, "http://localhost").pathname
|
|
878
|
+
} catch (_) {
|
|
879
|
+
continue
|
|
880
|
+
}
|
|
881
|
+
if (pathname.startsWith("/api/")) {
|
|
882
|
+
try {
|
|
883
|
+
const parts = pathname.slice("/api/".length).split("/").filter(Boolean).map(decodeURIComponent)
|
|
884
|
+
return this.kernel.path("api", ...parts)
|
|
885
|
+
} catch (_) {
|
|
886
|
+
return ""
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
if (PluginSources.isRunPath(pathname)) {
|
|
890
|
+
return PluginSources.resolveRunPath(this.kernel, pathname)
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return ""
|
|
894
|
+
}
|
|
895
|
+
async reserveDefaultAppLogSession(menu, enabled, options = {}) {
|
|
896
|
+
if (!enabled || !this.kernel || !this.kernel.api || !this.kernel.api.logSessions) {
|
|
897
|
+
return
|
|
898
|
+
}
|
|
899
|
+
const launchPath = this.defaultAppLogLaunchPath(menu)
|
|
900
|
+
if (launchPath) {
|
|
901
|
+
await this.kernel.api.logSessions.reserveLaunch(launchPath, {
|
|
902
|
+
existingRoutineOnly: !!options.existingRoutineOnly
|
|
903
|
+
})
|
|
904
|
+
}
|
|
905
|
+
}
|
|
861
906
|
async renderIndex(name, cfg) {
|
|
862
907
|
let p = this.kernel.path("api", name)
|
|
863
908
|
let index_path = path.resolve(p, "index.html")
|
|
@@ -1337,7 +1382,6 @@ class Server {
|
|
|
1337
1382
|
} catch(e) {
|
|
1338
1383
|
config.menu = []
|
|
1339
1384
|
}
|
|
1340
|
-
|
|
1341
1385
|
await this.renderMenu(req, uri, name, config, [])
|
|
1342
1386
|
|
|
1343
1387
|
let platform = os.platform()
|
|
@@ -1425,6 +1469,10 @@ class Server {
|
|
|
1425
1469
|
run_tab = "/p/" + name
|
|
1426
1470
|
autoselect = false
|
|
1427
1471
|
}
|
|
1472
|
+
await this.reserveDefaultAppLogSession(
|
|
1473
|
+
config && config.menu,
|
|
1474
|
+
autoselect && env.PINOKIO_SCRIPT_DEFAULT && env.PINOKIO_SCRIPT_DEFAULT.toString().toLowerCase() === "true"
|
|
1475
|
+
)
|
|
1428
1476
|
let dev_tab = "/p/" + name + "/dev"
|
|
1429
1477
|
let review_tab = "/p/" + name + "/review"
|
|
1430
1478
|
let files_tab = "/p/" + name + "/files"
|
|
@@ -6328,8 +6376,7 @@ class Server {
|
|
|
6328
6376
|
path.resolve(__dirname, "views")
|
|
6329
6377
|
])
|
|
6330
6378
|
}
|
|
6331
|
-
this.app.use(
|
|
6332
|
-
this.app.use(express.urlencoded({ extended: true }));
|
|
6379
|
+
logRedaction.createLogRedactionBodyParsers(express).forEach((parser) => this.app.use(parser))
|
|
6333
6380
|
this.app.use(cookieParser());
|
|
6334
6381
|
this.app.use(session({
|
|
6335
6382
|
secret: "secret",
|
|
@@ -8247,6 +8294,9 @@ class Server {
|
|
|
8247
8294
|
})
|
|
8248
8295
|
return
|
|
8249
8296
|
}
|
|
8297
|
+
if (!workspace) {
|
|
8298
|
+
await logRedaction.writeCurrentLogSnapshot(this.kernel, this.version)
|
|
8299
|
+
}
|
|
8250
8300
|
res.render("logs", {
|
|
8251
8301
|
current_host: this.kernel.peer.host,
|
|
8252
8302
|
...peerAccess,
|
|
@@ -8335,6 +8385,7 @@ class Server {
|
|
|
8335
8385
|
entries
|
|
8336
8386
|
})
|
|
8337
8387
|
}))
|
|
8388
|
+
this.app.get("/pinokio/logs/file", ex(logRedaction.createTopLevelLogFileHandler(this)))
|
|
8338
8389
|
this.app.get("/api/logs/stream", ex(async (req, res) => {
|
|
8339
8390
|
const workspace = typeof req.query.workspace === 'string' ? req.query.workspace.trim() : ''
|
|
8340
8391
|
let context
|
|
@@ -13519,6 +13570,104 @@ class Server {
|
|
|
13519
13570
|
requirements_pending,
|
|
13520
13571
|
})
|
|
13521
13572
|
}))
|
|
13573
|
+
this.app.get("/net/:name/routes", ex(async (req, res) => {
|
|
13574
|
+
const protocol = req.get('X-Forwarded-Proto') || "http"
|
|
13575
|
+
await this.kernel.peer.check_peers()
|
|
13576
|
+
|
|
13577
|
+
const list = this.kernel.peer.info ? this.getPeers() : []
|
|
13578
|
+
let processes = []
|
|
13579
|
+
let host
|
|
13580
|
+
let peer
|
|
13581
|
+
for (let item of list) {
|
|
13582
|
+
if (item.name === req.params.name) {
|
|
13583
|
+
processes = item.processes
|
|
13584
|
+
host = item.host
|
|
13585
|
+
peer = item
|
|
13586
|
+
}
|
|
13587
|
+
}
|
|
13588
|
+
|
|
13589
|
+
let peer_info = host && this.kernel.peer.info ? this.kernel.peer.info[host] : null
|
|
13590
|
+
if (!peer_info && req.params.name === this.kernel.peer.name) {
|
|
13591
|
+
try {
|
|
13592
|
+
await this.kernel.peer.refresh_host(this.kernel.peer.host)
|
|
13593
|
+
} catch (e) {
|
|
13594
|
+
}
|
|
13595
|
+
host = this.kernel.peer.host
|
|
13596
|
+
peer_info = this.kernel.peer.info && this.kernel.peer.info[host] ? this.kernel.peer.info[host] : null
|
|
13597
|
+
if (peer_info) {
|
|
13598
|
+
peer = peer_info
|
|
13599
|
+
}
|
|
13600
|
+
}
|
|
13601
|
+
|
|
13602
|
+
if (peer_info) {
|
|
13603
|
+
processes = peer_info.router_info || []
|
|
13604
|
+
}
|
|
13605
|
+
for (let i = 0; i < processes.length; i++) {
|
|
13606
|
+
if (!processes[i].icon) {
|
|
13607
|
+
if (protocol === "https") {
|
|
13608
|
+
processes[i].icon = processes[i].https_icon
|
|
13609
|
+
} else {
|
|
13610
|
+
processes[i].icon = processes[i].http_icon
|
|
13611
|
+
}
|
|
13612
|
+
}
|
|
13613
|
+
}
|
|
13614
|
+
|
|
13615
|
+
const installed = peer_info ? peer_info.installed : []
|
|
13616
|
+
const serverless_mapping = peer_info ? peer_info.rewrite_mapping : {}
|
|
13617
|
+
const serverless = Object.keys(serverless_mapping).map((name) => {
|
|
13618
|
+
return serverless_mapping[name]
|
|
13619
|
+
})
|
|
13620
|
+
const static_routes = this.kernel.router && this.kernel.router.rewrite_mapping
|
|
13621
|
+
? Object.keys(this.kernel.router.rewrite_mapping).map((key) => this.kernel.router.rewrite_mapping[key])
|
|
13622
|
+
: []
|
|
13623
|
+
const current_host = this.kernel.peer.host
|
|
13624
|
+
const showStaticRoutes = false
|
|
13625
|
+
const visibleStaticRoutes = showStaticRoutes ? static_routes : []
|
|
13626
|
+
const routeCount = current_host === host
|
|
13627
|
+
? processes.length + visibleStaticRoutes.length
|
|
13628
|
+
: processes.length + installed.length + serverless.length
|
|
13629
|
+
const allow_dns_creation = !!peer_info && req.params.name === this.kernel.peer.name
|
|
13630
|
+
const locals = {
|
|
13631
|
+
static_routes,
|
|
13632
|
+
visibleStaticRoutes,
|
|
13633
|
+
showStaticRoutes,
|
|
13634
|
+
processes,
|
|
13635
|
+
installed,
|
|
13636
|
+
serverless,
|
|
13637
|
+
host,
|
|
13638
|
+
peer,
|
|
13639
|
+
protocol,
|
|
13640
|
+
current_host,
|
|
13641
|
+
allow_dns_creation,
|
|
13642
|
+
routeCount,
|
|
13643
|
+
}
|
|
13644
|
+
const fingerprintPayload = current_host === host
|
|
13645
|
+
? { current_host, host, processes, visibleStaticRoutes }
|
|
13646
|
+
: { current_host, host, processes, installed, serverless }
|
|
13647
|
+
const fingerprint = crypto
|
|
13648
|
+
.createHash('sha256')
|
|
13649
|
+
.update(JSON.stringify(fingerprintPayload))
|
|
13650
|
+
.digest('hex')
|
|
13651
|
+
const renderPartial = (name) => {
|
|
13652
|
+
return new Promise((resolve, reject) => {
|
|
13653
|
+
this.app.render(name, locals, (err, html) => {
|
|
13654
|
+
if (err) {
|
|
13655
|
+
reject(err)
|
|
13656
|
+
} else {
|
|
13657
|
+
resolve(html)
|
|
13658
|
+
}
|
|
13659
|
+
})
|
|
13660
|
+
})
|
|
13661
|
+
}
|
|
13662
|
+
const routes_html = await renderPartial("partials/net_route_list")
|
|
13663
|
+
const summary_html = await renderPartial("partials/net_summary")
|
|
13664
|
+
res.json({
|
|
13665
|
+
success: true,
|
|
13666
|
+
fingerprint,
|
|
13667
|
+
routes_html,
|
|
13668
|
+
summary_html,
|
|
13669
|
+
})
|
|
13670
|
+
}))
|
|
13522
13671
|
this.app.get("/net/:name/diff", ex(async (req, res) => {
|
|
13523
13672
|
try {
|
|
13524
13673
|
let processes = this.kernel.peer.info[this.kernel.peer.host].router_info
|
|
@@ -15142,6 +15291,12 @@ class Server {
|
|
|
15142
15291
|
}
|
|
15143
15292
|
}
|
|
15144
15293
|
await this.renderMenu(req, uri, name, config, [])
|
|
15294
|
+
const env = await this.kernel.env("api/" + name)
|
|
15295
|
+
await this.reserveDefaultAppLogSession(
|
|
15296
|
+
config && config.menu,
|
|
15297
|
+
env.PINOKIO_SCRIPT_DEFAULT && env.PINOKIO_SCRIPT_DEFAULT.toString().toLowerCase() === "true",
|
|
15298
|
+
{ existingRoutineOnly: true }
|
|
15299
|
+
)
|
|
15145
15300
|
|
|
15146
15301
|
ejs.renderFile(path.resolve(__dirname, "views/partials/menu.ejs"), { menu: config.menu }, (err, html) => {
|
|
15147
15302
|
res.send(html)
|
|
@@ -15806,37 +15961,19 @@ class Server {
|
|
|
15806
15961
|
}
|
|
15807
15962
|
return
|
|
15808
15963
|
}
|
|
15809
|
-
|
|
15810
|
-
let
|
|
15811
|
-
|
|
15812
|
-
|
|
15813
|
-
|
|
15814
|
-
|
|
15815
|
-
|
|
15816
|
-
path: s.path,
|
|
15817
|
-
cmd: s.cmd,
|
|
15818
|
-
done: s.done,
|
|
15819
|
-
ready: s.ready,
|
|
15820
|
-
}
|
|
15821
|
-
})
|
|
15822
|
-
|
|
15823
|
-
let info = {
|
|
15824
|
-
platform: this.kernel.platform,
|
|
15825
|
-
arch: this.kernel.arch,
|
|
15826
|
-
running: this.kernel.api.running,
|
|
15827
|
-
home: this.kernel.homedir,
|
|
15828
|
-
vars: this.kernel.vars,
|
|
15829
|
-
memory: this.kernel.memory,
|
|
15830
|
-
procs: this.kernel.procs,
|
|
15831
|
-
gpu: this.kernel.gpu,
|
|
15832
|
-
gpus: this.kernel.gpus,
|
|
15833
|
-
version: this.version,
|
|
15834
|
-
...this.kernel.sysinfo
|
|
15964
|
+
const redactionOverridesRequested = Boolean(req.body && Object.prototype.hasOwnProperty.call(req.body, 'redacted_overrides'))
|
|
15965
|
+
let redactionOverrides = []
|
|
15966
|
+
try {
|
|
15967
|
+
redactionOverrides = logRedaction.normalizeLogRedactionOverrides(req.body || {})
|
|
15968
|
+
} catch (error) {
|
|
15969
|
+
res.status(400).json({ error: error && error.message ? error.message : 'Invalid redaction overrides' })
|
|
15970
|
+
return
|
|
15835
15971
|
}
|
|
15836
|
-
|
|
15837
|
-
await
|
|
15972
|
+
|
|
15973
|
+
await logRedaction.writeCurrentLogSnapshot(this.kernel, this.version)
|
|
15838
15974
|
|
|
15839
15975
|
|
|
15976
|
+
await fs.promises.rm(this.kernel.path("exported_logs"), { recursive: true, force: true }).catch(() => {})
|
|
15840
15977
|
await fs.promises.cp(
|
|
15841
15978
|
this.kernel.path("logs"),
|
|
15842
15979
|
this.kernel.path("exported_logs")
|
|
@@ -15847,8 +15984,22 @@ class Server {
|
|
|
15847
15984
|
|
|
15848
15985
|
let folder = this.kernel.path("exported_logs")
|
|
15849
15986
|
let zipPath = this.kernel.path("logs.zip")
|
|
15987
|
+
if (redactionOverridesRequested) {
|
|
15988
|
+
await fs.promises.rm(zipPath, { force: true }).catch(() => {})
|
|
15989
|
+
}
|
|
15990
|
+
let appliedRedactionOverrides = 0
|
|
15991
|
+
try {
|
|
15992
|
+
if (redactionOverridesRequested) {
|
|
15993
|
+
await logRedaction.assertCompleteLogRedactionOverrides(folder, redactionOverrides)
|
|
15994
|
+
}
|
|
15995
|
+
appliedRedactionOverrides = await logRedaction.applyLogRedactionOverrides(folder, redactionOverrides)
|
|
15996
|
+
} catch (error) {
|
|
15997
|
+
res.status(400).json({ error: error && error.message ? error.message : 'Failed to apply redaction overrides' })
|
|
15998
|
+
return
|
|
15999
|
+
}
|
|
16000
|
+
await fs.promises.rm(zipPath, { force: true }).catch(() => {})
|
|
15850
16001
|
await compressing.zip.compressDir(folder, zipPath)
|
|
15851
|
-
res.json({ success: true, download: '/pinokio/logs.zip' })
|
|
16002
|
+
res.json({ success: true, download: '/pinokio/logs.zip', redacted_overrides: appliedRedactionOverrides })
|
|
15852
16003
|
}))
|
|
15853
16004
|
this.app.get("/pinokio/version", ex(async (req, res) => {
|
|
15854
16005
|
let version = this.version
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
2
|
const os = require('os')
|
|
3
3
|
const path = require('path')
|
|
4
|
+
const Environment = require('../../kernel/environment')
|
|
4
5
|
|
|
5
6
|
const DEFAULT_TAIL_LINES = 800
|
|
6
7
|
const MAX_SECTION_CHARS = 120000
|
|
@@ -17,19 +18,26 @@ class AppLogReportService {
|
|
|
17
18
|
this.kernel = kernel
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
async buildReport({ appId, status, tail = DEFAULT_TAIL_LINES, redact = true }) {
|
|
21
|
-
const
|
|
22
|
-
if (!
|
|
21
|
+
async buildReport({ appId, status, tail = DEFAULT_TAIL_LINES, redact = true, session = '' }) {
|
|
22
|
+
const workspaceRoot = status && status.path ? status.path : null
|
|
23
|
+
if (!workspaceRoot) {
|
|
23
24
|
return null
|
|
24
25
|
}
|
|
26
|
+
const appRoot = await this.resolveAppRoot(workspaceRoot)
|
|
25
27
|
const tailLines = this.registry.parseTailCount(tail, DEFAULT_TAIL_LINES)
|
|
26
|
-
const
|
|
28
|
+
const sessionIndex = await this.readSessionIndex(appRoot)
|
|
29
|
+
const requestedSession = typeof session === 'string' && session.trim()
|
|
30
|
+
? session.trim()
|
|
31
|
+
: sessionIndex.latest_session
|
|
32
|
+
const manifest = requestedSession ? await this.readSessionManifest(appRoot, requestedSession) : null
|
|
33
|
+
const selectedSession = manifest ? manifest.id : null
|
|
34
|
+
const rawSections = await this.collectSections(appRoot, tailLines, manifest)
|
|
27
35
|
const sections = []
|
|
28
36
|
const totals = {}
|
|
29
37
|
|
|
30
38
|
for (const section of rawSections) {
|
|
31
39
|
if (redact) {
|
|
32
|
-
const redacted = this.redactText(section.text, { appRoot })
|
|
40
|
+
const redacted = this.redactText(section.text, { appRoot, workspaceRoot })
|
|
33
41
|
this.mergeCounts(totals, redacted.counts)
|
|
34
42
|
sections.push({
|
|
35
43
|
...section,
|
|
@@ -47,7 +55,7 @@ class AppLogReportService {
|
|
|
47
55
|
const metadata = {
|
|
48
56
|
app_id: appId,
|
|
49
57
|
title: status.title || appId,
|
|
50
|
-
repo_url: this.sanitizeRemoteUrl(status.repo_url || this.readGitRemote(
|
|
58
|
+
repo_url: this.sanitizeRemoteUrl(status.repo_url || this.readGitRemote(workspaceRoot)),
|
|
51
59
|
generated_at: new Date().toISOString(),
|
|
52
60
|
pinokiod: this.readPinokioVersion(),
|
|
53
61
|
platform: os.platform(),
|
|
@@ -55,7 +63,11 @@ class AppLogReportService {
|
|
|
55
63
|
node: process.version,
|
|
56
64
|
tail_count: tailLines,
|
|
57
65
|
system_spec: this.buildSystemSpec(),
|
|
58
|
-
redaction_mode: redact ? 'server_deterministic' : 'none'
|
|
66
|
+
redaction_mode: redact ? 'server_deterministic' : 'none',
|
|
67
|
+
latest_session: sessionIndex.latest_session,
|
|
68
|
+
session: selectedSession,
|
|
69
|
+
sessions: sessionIndex.sessions,
|
|
70
|
+
no_session: !manifest
|
|
59
71
|
}
|
|
60
72
|
|
|
61
73
|
return {
|
|
@@ -67,54 +79,121 @@ class AppLogReportService {
|
|
|
67
79
|
}
|
|
68
80
|
}
|
|
69
81
|
|
|
70
|
-
async collectSections(appRoot, tailLines) {
|
|
82
|
+
async collectSections(appRoot, tailLines, manifest = null) {
|
|
71
83
|
const sections = []
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
if (!manifest || !Array.isArray(manifest.runs)) {
|
|
85
|
+
return sections
|
|
86
|
+
}
|
|
87
|
+
const logsRoot = path.resolve(appRoot, 'logs')
|
|
88
|
+
for (const run of manifest.runs) {
|
|
89
|
+
const logs = Array.isArray(run && run.logs) ? run.logs : []
|
|
90
|
+
for (const log of logs) {
|
|
91
|
+
const file = this.resolveManifestLog(appRoot, log)
|
|
92
|
+
if (!file) {
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
const relativeLog = this.toPosix(path.relative(appRoot, file))
|
|
79
96
|
sections.push(await this.buildSection({
|
|
80
97
|
appRoot,
|
|
81
|
-
root:
|
|
98
|
+
root: logsRoot,
|
|
82
99
|
file,
|
|
83
|
-
source:
|
|
84
|
-
script,
|
|
100
|
+
source: this.sourceFromRelativeFile(relativeLog),
|
|
101
|
+
script: run.script || this.scriptFromRelativeFile(relativeLog),
|
|
85
102
|
tailLines
|
|
86
103
|
}))
|
|
87
104
|
}
|
|
88
105
|
}
|
|
89
106
|
|
|
90
|
-
return sections
|
|
91
|
-
|
|
92
|
-
|
|
107
|
+
return sections.filter(Boolean)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async resolveAppRoot(workspaceRoot) {
|
|
111
|
+
const fallback = path.resolve(workspaceRoot)
|
|
112
|
+
if (!this.kernel || typeof this.kernel.exists !== 'function') {
|
|
113
|
+
return fallback
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const root = await Environment.get_root({ path: fallback }, this.kernel)
|
|
117
|
+
return root && root.root ? path.resolve(root.root) : fallback
|
|
118
|
+
} catch (_) {
|
|
119
|
+
return fallback
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
safeSessionId(value) {
|
|
124
|
+
return typeof value === 'string' && /^[A-Za-z0-9._-]+$/.test(value) ? value : ''
|
|
93
125
|
}
|
|
94
126
|
|
|
95
|
-
async
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
} catch (_) {
|
|
102
|
-
return
|
|
127
|
+
async readSessionIndex(appRoot) {
|
|
128
|
+
try {
|
|
129
|
+
const indexPath = path.resolve(appRoot, 'logs', 'sessions', 'index.json')
|
|
130
|
+
const parsed = JSON.parse(await fs.promises.readFile(indexPath, 'utf8'))
|
|
131
|
+
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.sessions)) {
|
|
132
|
+
return { latest_session: null, sessions: [] }
|
|
103
133
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
134
|
+
return {
|
|
135
|
+
latest_session: this.safeSessionId(parsed.latest_session) || null,
|
|
136
|
+
sessions: parsed.sessions
|
|
137
|
+
.filter((entry) => entry && typeof entry === 'object' && this.safeSessionId(entry.id))
|
|
138
|
+
.map((entry) => ({
|
|
139
|
+
id: entry.id,
|
|
140
|
+
created_at: typeof entry.created_at === 'string' ? entry.created_at : null,
|
|
141
|
+
updated_at: typeof entry.updated_at === 'string' ? entry.updated_at : null,
|
|
142
|
+
runs: Array.isArray(entry.runs) ? entry.runs.filter((run) => typeof run === 'string') : []
|
|
143
|
+
}))
|
|
114
144
|
}
|
|
145
|
+
} catch (_) {
|
|
146
|
+
return { latest_session: null, sessions: [] }
|
|
115
147
|
}
|
|
116
|
-
|
|
117
|
-
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async readSessionManifest(appRoot, sessionId) {
|
|
151
|
+
const safeId = this.safeSessionId(sessionId)
|
|
152
|
+
if (!safeId) {
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const manifestPath = path.resolve(appRoot, 'logs', 'sessions', `${safeId}.json`)
|
|
157
|
+
const parsed = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8'))
|
|
158
|
+
if (!parsed || typeof parsed !== 'object' || parsed.id !== safeId || !Array.isArray(parsed.runs)) {
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
return parsed
|
|
162
|
+
} catch (_) {
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
resolveManifestLog(appRoot, log) {
|
|
168
|
+
if (!log || typeof log.path !== 'string' || !log.path.trim()) {
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
const logsRoot = path.resolve(appRoot, 'logs')
|
|
172
|
+
const sessionsRoot = path.resolve(logsRoot, 'sessions')
|
|
173
|
+
const file = path.resolve(appRoot, log.path)
|
|
174
|
+
if (!this.registry.isPathWithin(logsRoot, file)) {
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
if (this.registry.isPathWithin(sessionsRoot, file)) {
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
if (path.basename(file) === 'latest') {
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
return file
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
sourceFromRelativeFile(relativeFile) {
|
|
187
|
+
const parts = String(relativeFile || '').split('/').filter(Boolean)
|
|
188
|
+
return parts[0] === 'logs' && parts[1] ? parts[1] : 'api'
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
scriptFromRelativeFile(relativeFile) {
|
|
192
|
+
const parts = String(relativeFile || '').split('/').filter(Boolean)
|
|
193
|
+
if (parts[0] === 'logs' && parts[1] === 'api' && parts.length > 3) {
|
|
194
|
+
return parts.slice(2, -1).join('/')
|
|
195
|
+
}
|
|
196
|
+
return ''
|
|
118
197
|
}
|
|
119
198
|
|
|
120
199
|
async buildSection({ appRoot, root, file, source, script, tailLines }) {
|
|
@@ -211,9 +290,11 @@ class AppLogReportService {
|
|
|
211
290
|
/(^|[\s"'(=])\/home\/[^/\s"')]+/g,
|
|
212
291
|
/(^|[\s"'(=])[A-Za-z]:\\Users\\[^\\\s"')]+/g
|
|
213
292
|
]
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
293
|
+
const roots = [context.workspaceRoot, context.appRoot]
|
|
294
|
+
.map((root) => root ? String(root) : '')
|
|
295
|
+
.filter((root, index, values) => root && values.indexOf(root) === index)
|
|
296
|
+
for (const root of roots) {
|
|
297
|
+
patterns.push(new RegExp(`(^|[\\s"'(=])${escapeRegExp(root)}`, 'g'))
|
|
217
298
|
}
|
|
218
299
|
return patterns
|
|
219
300
|
}
|
|
@@ -257,7 +338,7 @@ class AppLogReportService {
|
|
|
257
338
|
lines.push('', '## Logs')
|
|
258
339
|
|
|
259
340
|
if (sections.length === 0) {
|
|
260
|
-
lines.push('', 'No app log files were found.')
|
|
341
|
+
lines.push('', metadata.no_session ? 'No session log bundle found.' : 'No app log files were found.')
|
|
261
342
|
}
|
|
262
343
|
|
|
263
344
|
for (const section of sections) {
|
|
@@ -330,22 +411,6 @@ class AppLogReportService {
|
|
|
330
411
|
return text
|
|
331
412
|
}
|
|
332
413
|
|
|
333
|
-
compareSections(a, b) {
|
|
334
|
-
const sourceOrder = { api: 0, shell: 1 }
|
|
335
|
-
const bySource = (sourceOrder[a.source] ?? 99) - (sourceOrder[b.source] ?? 99)
|
|
336
|
-
if (bySource !== 0) return bySource
|
|
337
|
-
return this.scriptRank(a.script) - this.scriptRank(b.script)
|
|
338
|
-
|| String(a.script || '').localeCompare(String(b.script || ''))
|
|
339
|
-
|| String(a.file || '').localeCompare(String(b.file || ''))
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
scriptRank(script) {
|
|
343
|
-
const value = String(script || '').toLowerCase()
|
|
344
|
-
const ordered = ['install', 'update', 'start', 'run', 'launch', 'serve', 'shell']
|
|
345
|
-
const idx = ordered.findIndex((name) => value === name || value.startsWith(`${name}.`) || value.includes(`/${name}.`))
|
|
346
|
-
return idx >= 0 ? idx : ordered.length
|
|
347
|
-
}
|
|
348
|
-
|
|
349
414
|
toPosix(value) {
|
|
350
415
|
return String(value || '').split(path.sep).filter(Boolean).join('/')
|
|
351
416
|
}
|