pinokiod 7.5.7 → 7.5.8
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/shell/index.js +7 -2
- package/kernel/bin/node.js +13 -8
- package/kernel/shell.js +20 -0
- package/kernel/shell_conda_runtime_guard.js +647 -0
- package/kernel/shells.js +7 -1
- package/package.json +1 -1
- package/server/index.js +3 -0
- package/server/public/common.js +11 -4
- package/server/public/logs.js +597 -4
- package/server/public/style.css +203 -42
- package/server/views/app.ejs +39 -31
- package/server/views/editor.ejs +1 -0
- package/server/views/install.ejs +1 -0
- package/server/views/logs.ejs +9 -5
- package/server/views/terminal.ejs +34 -5
- package/test/node-bin.test.js +39 -0
- package/test/shell-api.test.js +99 -9
- package/test/shell-conda-runtime-guard.test.js +558 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const unparse = require('yargs-unparser-custom-flag')
|
|
3
|
+
|
|
4
|
+
const SHELL_RUN_GUARD = Symbol('pinokio.shellRunCondaRuntimeGuard')
|
|
5
|
+
const noticeSessions = new Set()
|
|
6
|
+
|
|
7
|
+
const MUTATING_COMMANDS = new Set([
|
|
8
|
+
'create',
|
|
9
|
+
'install',
|
|
10
|
+
'remove',
|
|
11
|
+
'uninstall',
|
|
12
|
+
'update',
|
|
13
|
+
'upgrade',
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
const ENV_MUTATING_COMMANDS = new Set([
|
|
17
|
+
'create',
|
|
18
|
+
'remove',
|
|
19
|
+
'update',
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
const PROTECTED_PACKAGES = new Set([
|
|
23
|
+
'conda',
|
|
24
|
+
'python',
|
|
25
|
+
'conda-libmamba-solver',
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
const VALUE_FLAGS = new Set([
|
|
29
|
+
'-c',
|
|
30
|
+
'--channel',
|
|
31
|
+
'-f',
|
|
32
|
+
'--file',
|
|
33
|
+
'-n',
|
|
34
|
+
'--name',
|
|
35
|
+
'-p',
|
|
36
|
+
'--prefix',
|
|
37
|
+
'--solver',
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
function pathApi(platform) {
|
|
41
|
+
return platform === 'win32' ? path.win32 : path.posix
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizePathname(value, platform) {
|
|
45
|
+
if (!value || typeof value !== 'string') {
|
|
46
|
+
return ''
|
|
47
|
+
}
|
|
48
|
+
const api = pathApi(platform)
|
|
49
|
+
const normalized = api.normalize(value)
|
|
50
|
+
return platform === 'win32' ? normalized.toLowerCase() : normalized
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isSamePath(target, root, platform) {
|
|
54
|
+
const normalizedTarget = normalizePathname(target, platform)
|
|
55
|
+
const normalizedRoot = normalizePathname(root, platform)
|
|
56
|
+
if (!normalizedTarget || !normalizedRoot) {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
return pathApi(platform).relative(normalizedRoot, normalizedTarget) === ''
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolvePathFrom(basePath, value, platform) {
|
|
63
|
+
if (!value || typeof value !== 'string') {
|
|
64
|
+
return ''
|
|
65
|
+
}
|
|
66
|
+
const api = pathApi(platform)
|
|
67
|
+
if (api.isAbsolute(value)) {
|
|
68
|
+
return api.normalize(value)
|
|
69
|
+
}
|
|
70
|
+
return api.resolve(basePath || process.cwd(), value)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function usesBackslashEscapes(context = {}) {
|
|
74
|
+
const shellName = String(context.shellName || '').toLowerCase()
|
|
75
|
+
if (shellName.includes('cmd.exe') || shellName === 'cmd' || shellName.includes('powershell') || shellName.includes('pwsh')) {
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
return context.platform !== 'win32'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function shellTokenize(input, context = {}) {
|
|
82
|
+
const tokens = []
|
|
83
|
+
let token = ''
|
|
84
|
+
let quote = null
|
|
85
|
+
let escaped = false
|
|
86
|
+
const backslashEscapes = usesBackslashEscapes(context)
|
|
87
|
+
|
|
88
|
+
const push = () => {
|
|
89
|
+
if (token.length > 0) {
|
|
90
|
+
tokens.push(token)
|
|
91
|
+
token = ''
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const char of String(input || '')) {
|
|
96
|
+
if (escaped) {
|
|
97
|
+
token += char
|
|
98
|
+
escaped = false
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
if (quote === "'") {
|
|
102
|
+
if (char === "'") {
|
|
103
|
+
quote = null
|
|
104
|
+
} else {
|
|
105
|
+
token += char
|
|
106
|
+
}
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
if (quote === '"') {
|
|
110
|
+
if (char === '"') {
|
|
111
|
+
quote = null
|
|
112
|
+
} else if (char === '\\' && backslashEscapes) {
|
|
113
|
+
escaped = true
|
|
114
|
+
} else {
|
|
115
|
+
token += char
|
|
116
|
+
}
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
if (char === "'" || char === '"') {
|
|
120
|
+
quote = char
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
if (char === '\\' && backslashEscapes) {
|
|
124
|
+
escaped = true
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
if (/\s/.test(char)) {
|
|
128
|
+
push()
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
token += char
|
|
132
|
+
}
|
|
133
|
+
if (escaped) {
|
|
134
|
+
token += '\\'
|
|
135
|
+
}
|
|
136
|
+
push()
|
|
137
|
+
return { tokens, closed: quote === null }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function splitShellSegments(input, context = {}) {
|
|
141
|
+
const parts = []
|
|
142
|
+
let segment = ''
|
|
143
|
+
let quote = null
|
|
144
|
+
let escaped = false
|
|
145
|
+
const text = String(input || '')
|
|
146
|
+
const backslashEscapes = usesBackslashEscapes(context)
|
|
147
|
+
|
|
148
|
+
const pushSegment = () => {
|
|
149
|
+
parts.push({ type: 'segment', text: segment })
|
|
150
|
+
segment = ''
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (let i = 0; i < text.length; i++) {
|
|
154
|
+
const char = text[i]
|
|
155
|
+
const next = text[i + 1]
|
|
156
|
+
|
|
157
|
+
if (escaped) {
|
|
158
|
+
segment += char
|
|
159
|
+
escaped = false
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
if (quote === "'") {
|
|
163
|
+
segment += char
|
|
164
|
+
if (char === "'") {
|
|
165
|
+
quote = null
|
|
166
|
+
}
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
if (quote === '"') {
|
|
170
|
+
segment += char
|
|
171
|
+
if (char === '"') {
|
|
172
|
+
quote = null
|
|
173
|
+
} else if (char === '\\' && backslashEscapes) {
|
|
174
|
+
escaped = true
|
|
175
|
+
}
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
if (char === "'" || char === '"') {
|
|
179
|
+
quote = char
|
|
180
|
+
segment += char
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
if (char === '\\' && backslashEscapes) {
|
|
184
|
+
escaped = true
|
|
185
|
+
segment += char
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
if ((char === '&' && next === '&') || (char === '|' && next === '|')) {
|
|
189
|
+
pushSegment()
|
|
190
|
+
parts.push({ type: 'separator', text: char + next })
|
|
191
|
+
i++
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
if (char === '&') {
|
|
195
|
+
const previous = text[i - 1]
|
|
196
|
+
if (previous === '>' || previous === '<' || next === '>') {
|
|
197
|
+
segment += char
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
pushSegment()
|
|
201
|
+
parts.push({ type: 'unsafe-separator', text: char })
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
if (char === '|') {
|
|
205
|
+
pushSegment()
|
|
206
|
+
parts.push({ type: 'unsafe-separator', text: char })
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
if (char === ';' || char === '\n' || char === '\r') {
|
|
210
|
+
pushSegment()
|
|
211
|
+
parts.push({ type: 'separator', text: char })
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
segment += char
|
|
215
|
+
}
|
|
216
|
+
pushSegment()
|
|
217
|
+
return parts
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function getExecutableToken(tokens) {
|
|
221
|
+
let index = 0
|
|
222
|
+
while (index < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[index])) {
|
|
223
|
+
index++
|
|
224
|
+
}
|
|
225
|
+
if (tokens[index] === '&' || String(tokens[index]).toLowerCase() === 'call') {
|
|
226
|
+
index++
|
|
227
|
+
}
|
|
228
|
+
return { executable: tokens[index] || '', index }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function executableBasename(executable, platform) {
|
|
232
|
+
const api = pathApi(platform)
|
|
233
|
+
return api.basename(String(executable || '')).toLowerCase()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function isCondaExecutable(executable, context) {
|
|
237
|
+
const platform = context.platform || process.platform
|
|
238
|
+
if (!executable) {
|
|
239
|
+
return false
|
|
240
|
+
}
|
|
241
|
+
if (String(executable).toLowerCase() === 'conda') {
|
|
242
|
+
return true
|
|
243
|
+
}
|
|
244
|
+
const base = executableBasename(executable, platform)
|
|
245
|
+
return base === 'conda' || base === 'conda.exe' || base === 'conda.bat' || base === 'conda.cmd'
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function resolveWrapperTarget(params, context) {
|
|
249
|
+
const conda = params ? params.conda : undefined
|
|
250
|
+
const platform = context.platform || process.platform
|
|
251
|
+
const cwd = context.cwd || (params && params.path) || process.cwd()
|
|
252
|
+
if (!conda) {
|
|
253
|
+
return { kind: 'managed-base', source: 'wrapper', prefix: context.managedBasePrefix || '' }
|
|
254
|
+
}
|
|
255
|
+
if (typeof conda === 'string') {
|
|
256
|
+
return { kind: 'app-env', source: 'wrapper', prefix: resolvePathFrom(cwd, conda, platform) }
|
|
257
|
+
}
|
|
258
|
+
if (conda && conda.path) {
|
|
259
|
+
return { kind: 'app-env', source: 'wrapper', prefix: resolvePathFrom(cwd, conda.path, platform) }
|
|
260
|
+
}
|
|
261
|
+
if (conda && conda.name) {
|
|
262
|
+
if (conda.name === 'base') {
|
|
263
|
+
return { kind: 'managed-base', source: 'wrapper', prefix: context.managedBasePrefix || '' }
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
kind: 'app-env',
|
|
267
|
+
source: 'wrapper',
|
|
268
|
+
name: conda.name,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { kind: 'unknown', source: 'wrapper' }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function parseCommandTarget(tokens, executableIndex) {
|
|
275
|
+
for (let i = executableIndex + 1; i < tokens.length; i++) {
|
|
276
|
+
const token = tokens[i]
|
|
277
|
+
if (token === '-p' || token === '--prefix') {
|
|
278
|
+
return { type: 'prefix', value: tokens[i + 1] || '' }
|
|
279
|
+
}
|
|
280
|
+
if (token.startsWith('--prefix=')) {
|
|
281
|
+
return { type: 'prefix', value: token.slice('--prefix='.length) }
|
|
282
|
+
}
|
|
283
|
+
if (token === '-n' || token === '--name') {
|
|
284
|
+
return { type: 'name', value: tokens[i + 1] || '' }
|
|
285
|
+
}
|
|
286
|
+
if (token.startsWith('--name=')) {
|
|
287
|
+
return { type: 'name', value: token.slice('--name='.length) }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return null
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function refineTargetFromCommandArgs(target, commandTarget, params, context) {
|
|
294
|
+
if (!commandTarget || !commandTarget.value) {
|
|
295
|
+
return target
|
|
296
|
+
}
|
|
297
|
+
const platform = context.platform || process.platform
|
|
298
|
+
const managedBasePrefix = context.managedBasePrefix || ''
|
|
299
|
+
if (commandTarget.type === 'name') {
|
|
300
|
+
if (commandTarget.value === 'base') {
|
|
301
|
+
return { kind: 'managed-base', source: 'command-target', name: 'base', prefix: managedBasePrefix }
|
|
302
|
+
}
|
|
303
|
+
return { kind: 'app-env', source: 'command-target', name: commandTarget.value }
|
|
304
|
+
}
|
|
305
|
+
const resolved = resolvePathFrom(context.cwd || (params && params.path) || process.cwd(), commandTarget.value, platform)
|
|
306
|
+
if (managedBasePrefix && isSamePath(resolved, managedBasePrefix, platform)) {
|
|
307
|
+
return { kind: 'managed-base', source: 'command-target', prefix: managedBasePrefix }
|
|
308
|
+
}
|
|
309
|
+
return { kind: 'app-env', source: 'command-target', prefix: resolved }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function resolveTarget(tokens, executableIndex, params, context) {
|
|
313
|
+
const wrapperTarget = resolveWrapperTarget(params, context)
|
|
314
|
+
return refineTargetFromCommandArgs(wrapperTarget, parseCommandTarget(tokens, executableIndex), params, context)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function findSubcommand(tokens, executableIndex) {
|
|
318
|
+
for (let i = executableIndex + 1; i < tokens.length; i++) {
|
|
319
|
+
const token = String(tokens[i] || '').toLowerCase()
|
|
320
|
+
if (!token || token.startsWith('-')) {
|
|
321
|
+
if (VALUE_FLAGS.has(token)) {
|
|
322
|
+
i++
|
|
323
|
+
}
|
|
324
|
+
continue
|
|
325
|
+
}
|
|
326
|
+
if (token === 'env') {
|
|
327
|
+
const subcommand = String(tokens[i + 1] || '').toLowerCase()
|
|
328
|
+
if (ENV_MUTATING_COMMANDS.has(subcommand)) {
|
|
329
|
+
return { name: `env ${subcommand}`, index: i, endIndex: i + 1, mutating: true }
|
|
330
|
+
}
|
|
331
|
+
return { name: `env ${subcommand}`, index: i, endIndex: i + 1, mutating: false }
|
|
332
|
+
}
|
|
333
|
+
if (MUTATING_COMMANDS.has(token)) {
|
|
334
|
+
return { name: token, index: i, endIndex: i, mutating: true }
|
|
335
|
+
}
|
|
336
|
+
return { name: token, index: i, endIndex: i, mutating: false }
|
|
337
|
+
}
|
|
338
|
+
return null
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function isEnvironmentTargetingSubcommand(subcommand) {
|
|
342
|
+
return subcommand && (
|
|
343
|
+
subcommand.name === 'create'
|
|
344
|
+
|| subcommand.name === 'env create'
|
|
345
|
+
|| subcommand.name === 'env remove'
|
|
346
|
+
|| subcommand.name === 'env update'
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function normalizePackageSpec(token) {
|
|
351
|
+
if (!token || token.startsWith('-')) {
|
|
352
|
+
return ''
|
|
353
|
+
}
|
|
354
|
+
const scoped = String(token).split('::').pop()
|
|
355
|
+
return scoped.split(/[=<>!~]/)[0].trim().toLowerCase()
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function hasAllFlag(tokens, startIndex) {
|
|
359
|
+
return tokens.slice(startIndex).some((token) => token === '--all')
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function hasDryRunFlag(tokens, startIndex) {
|
|
363
|
+
return tokens.slice(startIndex).some((token) => token === '--dry-run')
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function explicitPackageNames(tokens, startIndex) {
|
|
367
|
+
const packages = []
|
|
368
|
+
for (let i = startIndex; i < tokens.length; i++) {
|
|
369
|
+
const token = tokens[i]
|
|
370
|
+
if (!token) {
|
|
371
|
+
continue
|
|
372
|
+
}
|
|
373
|
+
if (token.startsWith('--') && token.includes('=')) {
|
|
374
|
+
continue
|
|
375
|
+
}
|
|
376
|
+
if (VALUE_FLAGS.has(token)) {
|
|
377
|
+
i++
|
|
378
|
+
continue
|
|
379
|
+
}
|
|
380
|
+
if (token.startsWith('-')) {
|
|
381
|
+
continue
|
|
382
|
+
}
|
|
383
|
+
const pkg = normalizePackageSpec(token)
|
|
384
|
+
if (pkg) {
|
|
385
|
+
packages.push(pkg)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return packages
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function protectedPackageFromSubcommand(tokens, subcommand) {
|
|
392
|
+
const packages = explicitPackageNames(tokens, subcommand.endIndex + 1)
|
|
393
|
+
return packages.find((pkg) => PROTECTED_PACKAGES.has(pkg)) || ''
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function isListedBroadMutation(tokens, subcommand, target) {
|
|
397
|
+
if ((subcommand.name === 'update' || subcommand.name === 'upgrade') && hasAllFlag(tokens, subcommand.endIndex + 1)) {
|
|
398
|
+
return true
|
|
399
|
+
}
|
|
400
|
+
return subcommand.name === 'env remove' && target.source === 'command-target'
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function classifyCommandTokens(tokens, params = {}, context = {}) {
|
|
404
|
+
const { executable, index: executableIndex } = getExecutableToken(tokens)
|
|
405
|
+
if (!isCondaExecutable(executable, context)) {
|
|
406
|
+
return { shouldSkip: false }
|
|
407
|
+
}
|
|
408
|
+
const target = resolveTarget(tokens, executableIndex, params, context)
|
|
409
|
+
if (target.kind !== 'managed-base') {
|
|
410
|
+
return { shouldSkip: false }
|
|
411
|
+
}
|
|
412
|
+
const subcommand = findSubcommand(tokens, executableIndex)
|
|
413
|
+
if (!subcommand || !subcommand.mutating) {
|
|
414
|
+
return { shouldSkip: false }
|
|
415
|
+
}
|
|
416
|
+
if (hasDryRunFlag(tokens, subcommand.endIndex + 1)) {
|
|
417
|
+
return { shouldSkip: false }
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (isEnvironmentTargetingSubcommand(subcommand) && target.source !== 'command-target') {
|
|
421
|
+
return { shouldSkip: false }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const protectedPackage = protectedPackageFromSubcommand(tokens, subcommand)
|
|
425
|
+
if (!protectedPackage && !isListedBroadMutation(tokens, subcommand, target)) {
|
|
426
|
+
return { shouldSkip: false }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
shouldSkip: true,
|
|
431
|
+
target,
|
|
432
|
+
reason: protectedPackage
|
|
433
|
+
? `protected base Conda package: ${protectedPackage}`
|
|
434
|
+
: `listed broad managed-base mutation: ${subcommand.name}`,
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function noopCommand() {
|
|
439
|
+
return 'echo Pinokio skipped a Conda setup command. Pinokio is continuing.'
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function classifySegment(segment, params, context) {
|
|
443
|
+
const parsed = shellTokenize(segment, context)
|
|
444
|
+
if (!parsed.closed) {
|
|
445
|
+
return { shouldSkip: false }
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
...classifyCommandTokens(parsed.tokens, params, context),
|
|
449
|
+
tokens: parsed.tokens,
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function rewriteStringMessage(message, params, context) {
|
|
454
|
+
const parts = splitShellSegments(message, context)
|
|
455
|
+
const skipped = []
|
|
456
|
+
const hasUnsafeSeparator = parts.some((part) => part.type === 'unsafe-separator')
|
|
457
|
+
|
|
458
|
+
if (hasUnsafeSeparator) {
|
|
459
|
+
for (const part of parts) {
|
|
460
|
+
if (part.type !== 'segment') {
|
|
461
|
+
continue
|
|
462
|
+
}
|
|
463
|
+
const body = part.text.trim()
|
|
464
|
+
if (!body) {
|
|
465
|
+
continue
|
|
466
|
+
}
|
|
467
|
+
const classification = classifySegment(body, params, context)
|
|
468
|
+
if (classification.shouldSkip) {
|
|
469
|
+
skipped.push({
|
|
470
|
+
command: body,
|
|
471
|
+
reason: classification.reason,
|
|
472
|
+
target: classification.target,
|
|
473
|
+
})
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (skipped.length > 0) {
|
|
477
|
+
return { message: noopCommand(), skipped }
|
|
478
|
+
}
|
|
479
|
+
return { message, skipped }
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const rewritten = parts.map((part) => {
|
|
483
|
+
if (part.type !== 'segment') {
|
|
484
|
+
return part.text
|
|
485
|
+
}
|
|
486
|
+
const leading = (part.text.match(/^\s*/) || [''])[0]
|
|
487
|
+
const trailing = (part.text.match(/\s*$/) || [''])[0]
|
|
488
|
+
const body = part.text.trim()
|
|
489
|
+
if (!body) {
|
|
490
|
+
return part.text
|
|
491
|
+
}
|
|
492
|
+
const classification = classifySegment(body, params, context)
|
|
493
|
+
if (!classification.shouldSkip) {
|
|
494
|
+
return part.text
|
|
495
|
+
}
|
|
496
|
+
skipped.push({
|
|
497
|
+
command: body,
|
|
498
|
+
reason: classification.reason,
|
|
499
|
+
target: classification.target,
|
|
500
|
+
})
|
|
501
|
+
return leading + noopCommand() + trailing
|
|
502
|
+
}).join('')
|
|
503
|
+
return { message: rewritten, skipped }
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function structuredTokens(message) {
|
|
507
|
+
if (!message || message.constructor !== Object) {
|
|
508
|
+
return null
|
|
509
|
+
}
|
|
510
|
+
return unparse(message)
|
|
511
|
+
.filter((item) => item != null)
|
|
512
|
+
.map((item) => String(item))
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function rewriteSingleMessage(message, params, context) {
|
|
516
|
+
if (typeof message === 'string') {
|
|
517
|
+
return rewriteStringMessage(message, params, context)
|
|
518
|
+
}
|
|
519
|
+
const tokens = structuredTokens(message)
|
|
520
|
+
if (!tokens) {
|
|
521
|
+
return { message, skipped: [] }
|
|
522
|
+
}
|
|
523
|
+
const classification = classifyCommandTokens(tokens, params, context)
|
|
524
|
+
if (!classification.shouldSkip) {
|
|
525
|
+
return { message, skipped: [] }
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
message: noopCommand(),
|
|
529
|
+
skipped: [{
|
|
530
|
+
command: tokens.join(' '),
|
|
531
|
+
reason: classification.reason,
|
|
532
|
+
target: classification.target,
|
|
533
|
+
}],
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function sessionKey(params, context) {
|
|
538
|
+
if (context.sessionKey) {
|
|
539
|
+
return context.sessionKey
|
|
540
|
+
}
|
|
541
|
+
const parent = params && params.$parent
|
|
542
|
+
return (parent && (parent.id || parent.path)) || (params && (params.group || params.id || params.path)) || 'global'
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function escapeHtml(value) {
|
|
546
|
+
return String(value)
|
|
547
|
+
.replace(/&/g, '&')
|
|
548
|
+
.replace(/</g, '<')
|
|
549
|
+
.replace(/>/g, '>')
|
|
550
|
+
.replace(/"/g, '"')
|
|
551
|
+
.replace(/'/g, ''')
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function formatRawNotice(skip) {
|
|
555
|
+
const target = skip.target && skip.target.kind ? skip.target.kind : 'unknown'
|
|
556
|
+
return [
|
|
557
|
+
'',
|
|
558
|
+
'[Pinokio] Skipped Conda setup command.',
|
|
559
|
+
`[Pinokio] Command: ${JSON.stringify(skip.command)}`,
|
|
560
|
+
"[Pinokio] The command targets Pinokio's protected base Conda setup.",
|
|
561
|
+
`[Pinokio] Reason: ${skip.reason || 'protected base Conda setup'}`,
|
|
562
|
+
`[Pinokio] Target: ${target}`,
|
|
563
|
+
'',
|
|
564
|
+
].join('\r\n')
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function formatNotifyHtml(skipped) {
|
|
568
|
+
const count = skipped.length
|
|
569
|
+
const firstCommand = skipped[0] && skipped[0].command ? String(skipped[0].command) : ''
|
|
570
|
+
const truncatedCommand = firstCommand.length > 240 ? `${firstCommand.slice(0, 237)}...` : firstCommand
|
|
571
|
+
const title = count === 1
|
|
572
|
+
? 'Pinokio skipped a Conda setup command.'
|
|
573
|
+
: `Pinokio skipped ${count} Conda setup commands.`
|
|
574
|
+
const commandHtml = truncatedCommand
|
|
575
|
+
? [
|
|
576
|
+
'<pre style="white-space:pre-wrap;margin:8px 0 0;font-size:12px;line-height:1.35;">',
|
|
577
|
+
escapeHtml(truncatedCommand),
|
|
578
|
+
'</pre>',
|
|
579
|
+
].join('')
|
|
580
|
+
: ''
|
|
581
|
+
const moreHtml = count > 1
|
|
582
|
+
? `<br>${escapeHtml(`${count - 1} more skipped command${count === 2 ? ' is' : 's are'} listed in the terminal log.`)}`
|
|
583
|
+
: ''
|
|
584
|
+
return [
|
|
585
|
+
`<b>${escapeHtml(title)}</b>`,
|
|
586
|
+
escapeHtml('Pinokio already manages base Conda.'),
|
|
587
|
+
escapeHtml('Pinokio is continuing.'),
|
|
588
|
+
commandHtml,
|
|
589
|
+
`${escapeHtml('Details are in the terminal/log.')}${moreHtml}`,
|
|
590
|
+
].filter(Boolean).join('<br>')
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function emitSkipNotices(params, skipped, context) {
|
|
594
|
+
if (!skipped.length || typeof context.ondata !== 'function') {
|
|
595
|
+
return
|
|
596
|
+
}
|
|
597
|
+
for (const skip of skipped) {
|
|
598
|
+
context.ondata({ raw: formatRawNotice(skip) })
|
|
599
|
+
}
|
|
600
|
+
const key = sessionKey(params, context)
|
|
601
|
+
if (noticeSessions.has(key)) {
|
|
602
|
+
return
|
|
603
|
+
}
|
|
604
|
+
noticeSessions.add(key)
|
|
605
|
+
context.ondata({
|
|
606
|
+
silent: true,
|
|
607
|
+
type: 'warning',
|
|
608
|
+
html: formatNotifyHtml(skipped),
|
|
609
|
+
}, 'notify')
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function applyCondaRuntimeGuard(params, context = {}) {
|
|
613
|
+
if (!params || !params[SHELL_RUN_GUARD]) {
|
|
614
|
+
return { params, skipped: [] }
|
|
615
|
+
}
|
|
616
|
+
if (params.conda && params.conda.skip === true) {
|
|
617
|
+
return { params, skipped: [] }
|
|
618
|
+
}
|
|
619
|
+
const message = params.message
|
|
620
|
+
if (typeof message === 'undefined' || message === null) {
|
|
621
|
+
return { params, skipped: [] }
|
|
622
|
+
}
|
|
623
|
+
const allSkipped = []
|
|
624
|
+
if (Array.isArray(message)) {
|
|
625
|
+
params.message = message.map((item) => {
|
|
626
|
+
const result = rewriteSingleMessage(item, params, context)
|
|
627
|
+
allSkipped.push(...result.skipped)
|
|
628
|
+
return result.message
|
|
629
|
+
})
|
|
630
|
+
} else {
|
|
631
|
+
const result = rewriteSingleMessage(message, params, context)
|
|
632
|
+
params.message = result.message
|
|
633
|
+
allSkipped.push(...result.skipped)
|
|
634
|
+
}
|
|
635
|
+
emitSkipNotices(params, allSkipped, context)
|
|
636
|
+
return { params, skipped: allSkipped }
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function resetNoticeSessionsForTest() {
|
|
640
|
+
noticeSessions.clear()
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
module.exports = {
|
|
644
|
+
SHELL_RUN_GUARD,
|
|
645
|
+
applyCondaRuntimeGuard,
|
|
646
|
+
resetNoticeSessionsForTest,
|
|
647
|
+
}
|
package/kernel/shells.js
CHANGED
|
@@ -497,7 +497,7 @@ class Shells {
|
|
|
497
497
|
return ""
|
|
498
498
|
}
|
|
499
499
|
|
|
500
|
-
let response = await sh.start(params, async (stream) => {
|
|
500
|
+
let response = await sh.start(params, async (stream, type) => {
|
|
501
501
|
/*
|
|
502
502
|
{
|
|
503
503
|
method: "shell.run",
|
|
@@ -521,6 +521,12 @@ class Shells {
|
|
|
521
521
|
}
|
|
522
522
|
*/
|
|
523
523
|
try {
|
|
524
|
+
if (type) {
|
|
525
|
+
if (ondata) {
|
|
526
|
+
ondata(stream, type)
|
|
527
|
+
}
|
|
528
|
+
return
|
|
529
|
+
}
|
|
524
530
|
const normalizedChunk = normalizeLiveEventChunk(stream.raw)
|
|
525
531
|
if (normalizedChunk.length > 0) {
|
|
526
532
|
liveEventBuffer = (liveEventBuffer + normalizedChunk).slice(-300)
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -4776,6 +4776,7 @@ class Server {
|
|
|
4776
4776
|
await fs.promises.mkdir(candidate, { recursive: true })
|
|
4777
4777
|
return {
|
|
4778
4778
|
logsRoot: candidate,
|
|
4779
|
+
workspacePath,
|
|
4779
4780
|
displayPath: this.formatLogsDisplayPath(candidate),
|
|
4780
4781
|
title: normalized
|
|
4781
4782
|
}
|
|
@@ -8142,6 +8143,7 @@ class Server {
|
|
|
8142
8143
|
logsDraftUrl: draftUrl,
|
|
8143
8144
|
logsEmbedded: embedded,
|
|
8144
8145
|
logsInitialView: initialView,
|
|
8146
|
+
logsWorkspaceCwd: '',
|
|
8145
8147
|
})
|
|
8146
8148
|
return
|
|
8147
8149
|
}
|
|
@@ -8162,6 +8164,7 @@ class Server {
|
|
|
8162
8164
|
logsDraftUrl: draftUrl,
|
|
8163
8165
|
logsEmbedded: embedded,
|
|
8164
8166
|
logsInitialView: initialView,
|
|
8167
|
+
logsWorkspaceCwd: context.workspacePath || '',
|
|
8165
8168
|
})
|
|
8166
8169
|
}))
|
|
8167
8170
|
this.app.get("/api/logs/tree", ex(async (req, res) => {
|