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/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(express.json());
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 states = this.kernel.shell.shells.map((s) => {
15811
- return {
15812
- state: s.state,
15813
- id: s.id,
15814
- group: s.group,
15815
- env: s.env,
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
- await fs.promises.writeFile(this.kernel.path("logs/system.json"), JSON.stringify(info, null, 2))
15837
- await fs.promises.writeFile(this.kernel.path("logs/state.json"), JSON.stringify(states, null, 2))
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 appRoot = status && status.path ? status.path : null
22
- if (!appRoot) {
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 rawSections = await this.collectSections(appRoot, tailLines)
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(appRoot)),
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
- const apiLogsRoot = path.resolve(appRoot, 'logs', 'api')
73
-
74
- if (await this.registry.pathIsDirectory(apiLogsRoot)) {
75
- const latestFiles = await this.findNamedFiles(apiLogsRoot, 'latest')
76
- for (const file of latestFiles) {
77
- const relativeDir = path.relative(apiLogsRoot, path.dirname(file))
78
- const script = this.toPosix(relativeDir)
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: apiLogsRoot,
98
+ root: logsRoot,
82
99
  file,
83
- source: 'api',
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
- .filter(Boolean)
92
- .sort((a, b) => this.compareSections(a, b))
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 findNamedFiles(root, filename) {
96
- const out = []
97
- const walk = async (dir) => {
98
- let entries = []
99
- try {
100
- entries = await fs.promises.readdir(dir, { withFileTypes: true })
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
- for (const entry of entries) {
105
- const entryPath = path.resolve(dir, entry.name)
106
- if (!this.registry.isPathWithin(root, entryPath)) {
107
- continue
108
- }
109
- if (entry.isDirectory()) {
110
- await walk(entryPath)
111
- } else if (entry.name === filename) {
112
- out.push(entryPath)
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
- await walk(root)
117
- return out
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 appRoot = context.appRoot ? String(context.appRoot) : ''
215
- if (appRoot) {
216
- patterns.push(new RegExp(`(^|[\\s"'(=])${escapeRegExp(appRoot)}`, 'g'))
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
  }