pinokiod 7.2.7 → 7.2.9

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.
@@ -11,6 +11,7 @@ const fastq = require('fastq')
11
11
  const Loader = require("../loader")
12
12
  const Environment = require("../environment")
13
13
  const Util = require('../util')
14
+ const ShellRunTemplate = require('./shell_run_template')
14
15
 
15
16
  class Api {
16
17
  constructor(kernel) {
@@ -416,6 +417,12 @@ class Api {
416
417
  await this.process(script.on.stop)
417
418
  }
418
419
  }
420
+ if (this.kernel.watch && typeof this.kernel.watch.stop === "function") {
421
+ if (req.params.id) {
422
+ await this.kernel.watch.stop(req.params.id)
423
+ }
424
+ await this.kernel.watch.stop(requestPath)
425
+ }
419
426
  // reset modules
420
427
  let modpath = this.resolvePath(cwd, req.params.uri)
421
428
  if (this.child_procs[modpath]) {
@@ -1010,6 +1017,7 @@ class Api {
1010
1017
  }
1011
1018
  // replace {{{ }}} with {{ }}
1012
1019
  rpc = this.kernel.template.flatten(rpc)
1020
+ rpc = ShellRunTemplate.renderEnvArgs(this.kernel, rpc, memory)
1013
1021
 
1014
1022
  // 6. rpc must have method names
1015
1023
  if (rpc.method) {
@@ -1454,6 +1462,25 @@ class Api {
1454
1462
  }
1455
1463
  return false
1456
1464
  }
1465
+ async startWatchersForRequest(request, script, scriptDir, input) {
1466
+ if (!this.kernel.watch || typeof this.kernel.watch.startForScript !== "function") {
1467
+ return
1468
+ }
1469
+ const id = request.id || request.path
1470
+ if (!id) {
1471
+ return
1472
+ }
1473
+ const cwd = request.cwd || scriptDir
1474
+ await this.kernel.watch.startForScript({
1475
+ id,
1476
+ request,
1477
+ script,
1478
+ cwd,
1479
+ dirname: scriptDir,
1480
+ input,
1481
+ args: input
1482
+ })
1483
+ }
1457
1484
  createQueue(queue_id, concurrency) {
1458
1485
  this.queues[queue_id] = fastq.promise(async ({ request, rawrpc, input, step, total, cwd, args }) => {
1459
1486
  try {
@@ -1695,6 +1722,7 @@ class Api {
1695
1722
  }
1696
1723
 
1697
1724
  const initialPayload = typeof request.input === "undefined" ? {} : request.input
1725
+ await this.startWatchersForRequest(request, script, cwd, initialPayload)
1698
1726
  this.queue(request, steps[0], initialPayload, 0, steps.length, cwd, initialPayload)
1699
1727
 
1700
1728
  } else {
@@ -0,0 +1,273 @@
1
+ function isCmdShellName(shellName) {
2
+ const name = (shellName || '').toLowerCase()
3
+ return name.includes('cmd.exe') || name === 'cmd'
4
+ }
5
+
6
+ function isPowerShellName(shellName) {
7
+ const name = (shellName || '').toLowerCase()
8
+ return name.includes('powershell') || name.includes('pwsh')
9
+ }
10
+
11
+ const ENV_ARG_MARKER_RE = /__PINOKIO_ENVARG_(\d+)__/g
12
+
13
+ function envArgMarker(index) {
14
+ return `__PINOKIO_ENVARG_${index}__`
15
+ }
16
+
17
+ function isPinokioEnvArgKey(key) {
18
+ return /^PINOKIO_ARG_\d+$/.test(key || "")
19
+ }
20
+
21
+ function hasEnvArgMarker(value) {
22
+ ENV_ARG_MARKER_RE.lastIndex = 0
23
+ return ENV_ARG_MARKER_RE.test(String(value))
24
+ }
25
+
26
+ function quotePosixLiteral(value) {
27
+ const input = value == null ? "" : String(value)
28
+ return `'${input.split("'").join("'\"'\"'")}'`
29
+ }
30
+
31
+ function quotePowerShellComposite(value) {
32
+ const input = value == null ? "" : String(value)
33
+ ENV_ARG_MARKER_RE.lastIndex = 0
34
+ let output = '"'
35
+ let lastIndex = 0
36
+ for (const match of input.matchAll(ENV_ARG_MARKER_RE)) {
37
+ const literal = input.slice(lastIndex, match.index)
38
+ output += literal.replace(/[`"$]/g, (char) => "`" + char)
39
+ output += "${env:PINOKIO_ARG_" + match[1] + "}"
40
+ lastIndex = match.index + match[0].length
41
+ }
42
+ output += input.slice(lastIndex).replace(/[`"$]/g, (char) => "`" + char)
43
+ output += '"'
44
+ return output
45
+ }
46
+
47
+ function quoteCmdComposite(value) {
48
+ const input = value == null ? "" : String(value)
49
+ ENV_ARG_MARKER_RE.lastIndex = 0
50
+ let output = '"'
51
+ let lastIndex = 0
52
+ for (const match of input.matchAll(ENV_ARG_MARKER_RE)) {
53
+ const literal = input.slice(lastIndex, match.index)
54
+ output += literal.replace(/([()%!^"<>&|])/g, '^$1')
55
+ output += "!PINOKIO_ARG_" + match[1] + "!"
56
+ lastIndex = match.index + match[0].length
57
+ }
58
+ output += input.slice(lastIndex).replace(/([()%!^"<>&|])/g, '^$1')
59
+ output += '"'
60
+ return output
61
+ }
62
+
63
+ function quoteEnvArgComposite(value, shellName) {
64
+ const input = value == null ? "" : String(value)
65
+ if (isCmdShellName(shellName)) {
66
+ return quoteCmdComposite(input)
67
+ }
68
+ if (isPowerShellName(shellName)) {
69
+ return quotePowerShellComposite(input)
70
+ }
71
+
72
+ ENV_ARG_MARKER_RE.lastIndex = 0
73
+ const parts = []
74
+ let lastIndex = 0
75
+ for (const match of input.matchAll(ENV_ARG_MARKER_RE)) {
76
+ const literal = input.slice(lastIndex, match.index)
77
+ if (literal) {
78
+ parts.push(quotePosixLiteral(literal))
79
+ }
80
+ parts.push('"$PINOKIO_ARG_' + match[1] + '"')
81
+ lastIndex = match.index + match[0].length
82
+ }
83
+ const tail = input.slice(lastIndex)
84
+ if (tail) {
85
+ parts.push(quotePosixLiteral(tail))
86
+ }
87
+ return parts.length > 0 ? parts.join("") : "''"
88
+ }
89
+
90
+ function shellNameFor(kernel, params) {
91
+ let shellName = kernel && kernel.platform === "win32" ? "cmd.exe" : "bash"
92
+ if (params && typeof params.shell === "string" && params.shell.trim()) {
93
+ shellName = params.shell
94
+ }
95
+ return shellName
96
+ }
97
+
98
+ function isPlainObject(value) {
99
+ return value && value.constructor === Object
100
+ }
101
+
102
+ function hasMultiline(value) {
103
+ return typeof value === "string" && /[\r\n]/.test(value)
104
+ }
105
+
106
+ function isStructuredArgvMessage(value) {
107
+ return isPlainObject(value) && Array.isArray(value._)
108
+ }
109
+
110
+ function hasStructuredArgvMessage(value) {
111
+ if (isStructuredArgvMessage(value)) {
112
+ return true
113
+ }
114
+ if (Array.isArray(value)) {
115
+ return value.some((item) => isStructuredArgvMessage(item))
116
+ }
117
+ return false
118
+ }
119
+
120
+ function protectStructuredString(value, state) {
121
+ if (!hasMultiline(value)) {
122
+ return value
123
+ }
124
+ const name = `PINOKIO_ARG_${state.args.length}`
125
+ state.args.push({
126
+ name,
127
+ value: value == null ? "" : String(value)
128
+ })
129
+ return envArgMarker(state.args.length - 1)
130
+ }
131
+
132
+ function protectStructuredValue(value, state) {
133
+ if (typeof value === "string") {
134
+ return protectStructuredString(value, state)
135
+ }
136
+ if (Array.isArray(value)) {
137
+ return value.map((item) => protectStructuredValue(item, state))
138
+ }
139
+ if (isPlainObject(value)) {
140
+ const rendered = {}
141
+ for (const [key, item] of Object.entries(value)) {
142
+ rendered[key] = protectStructuredValue(item, state)
143
+ }
144
+ return rendered
145
+ }
146
+ return value
147
+ }
148
+
149
+ function protectStructuredMessage(value, state) {
150
+ if (isStructuredArgvMessage(value)) {
151
+ return protectStructuredValue(value, state)
152
+ }
153
+ if (Array.isArray(value)) {
154
+ return value.map((item) => isStructuredArgvMessage(item) ? protectStructuredValue(item, state) : item)
155
+ }
156
+ return value
157
+ }
158
+
159
+ function renderEnvArgs(kernel, rpc, memory) {
160
+ if (!rpc || rpc.method !== "shell.run" || !rpc.params || !hasStructuredArgvMessage(rpc.params.message)) {
161
+ return rpc
162
+ }
163
+
164
+ const shellName = shellNameFor(kernel, rpc.params)
165
+ const state = { args: [] }
166
+ const message = protectStructuredMessage(rpc.params.message, state)
167
+
168
+ if (state.args.length === 0) {
169
+ return rpc
170
+ }
171
+
172
+ const env = Object.assign({}, rpc.params.env || {})
173
+ for (const arg of state.args) {
174
+ env[arg.name] = arg.value
175
+ }
176
+
177
+ return {
178
+ ...rpc,
179
+ params: {
180
+ ...rpc.params,
181
+ message,
182
+ env,
183
+ _pinokio_env_args: state.args,
184
+ _pinokio_cmd_delayed_expansion: isCmdShellName(shellName)
185
+ }
186
+ }
187
+ }
188
+
189
+ function envArgDetails(value) {
190
+ const normalized = String(value == null ? "" : value)
191
+ .replace(/\r\n/g, "\n")
192
+ .replace(/\r/g, "\n")
193
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "?")
194
+ const lines = normalized.split("\n")
195
+ const previewLines = []
196
+ const maxLines = 8
197
+ const maxChars = 800
198
+ let used = 0
199
+
200
+ for (const line of lines.slice(0, maxLines)) {
201
+ const remaining = maxChars - used
202
+ if (remaining <= 0) {
203
+ break
204
+ }
205
+ if (line.length > remaining) {
206
+ previewLines.push(line.slice(0, remaining) + "...")
207
+ used = maxChars
208
+ break
209
+ }
210
+ previewLines.push(line)
211
+ used += line.length + 1
212
+ }
213
+
214
+ const preview = previewLines.join("\n")
215
+ const truncated = lines.length > previewLines.length || normalized.length > preview.length
216
+ if (truncated && previewLines[previewLines.length - 1] !== "...") {
217
+ previewLines.push("...")
218
+ }
219
+
220
+ return {
221
+ lineCount: normalized.length === 0 ? 0 : lines.length,
222
+ previewLines,
223
+ truncated
224
+ }
225
+ }
226
+
227
+ function formatEnvArgsPreview(args) {
228
+ if (!Array.isArray(args) || args.length === 0) {
229
+ return ""
230
+ }
231
+ const lines = ["\r\nPinokio shell args", ""]
232
+ for (const arg of args) {
233
+ const details = envArgDetails(arg.value)
234
+ lines.push(`${arg.name} ${details.lineCount} lines`)
235
+ for (const previewLine of details.previewLines) {
236
+ lines.push(` ${previewLine}`)
237
+ }
238
+ lines.push("")
239
+ }
240
+ return lines.join("\r\n") + "\r\n"
241
+ }
242
+
243
+ function envArgSummary(value) {
244
+ const details = envArgDetails(value)
245
+ return {
246
+ type: "pinokio env arg",
247
+ lines: details.lineCount,
248
+ preview: details.previewLines.join("\n"),
249
+ truncated: details.truncated
250
+ }
251
+ }
252
+
253
+ function redactEnvArgs(env) {
254
+ if (!env || typeof env !== "object") {
255
+ return env
256
+ }
257
+ const redacted = { ...env }
258
+ for (const key of Object.keys(redacted)) {
259
+ if (isPinokioEnvArgKey(key)) {
260
+ redacted[key] = envArgSummary(redacted[key])
261
+ }
262
+ }
263
+ return redacted
264
+ }
265
+
266
+ module.exports = {
267
+ renderEnvArgs,
268
+ quoteEnvArgComposite,
269
+ hasEnvArgMarker,
270
+ isPinokioEnvArgKey,
271
+ formatEnvArgsPreview,
272
+ redactEnvArgs
273
+ }
package/kernel/index.js CHANGED
@@ -40,6 +40,7 @@ const Git = require('./git')
40
40
  const Connect = require('./connect')
41
41
  const Favicon = require('./favicon')
42
42
  const AppLauncher = require('./app_launcher')
43
+ const WatchManager = require('./watch')
43
44
  const { DownloaderHelper } = require('node-downloader-helper');
44
45
  const { ProxyAgent } = require('proxy-agent');
45
46
  const fakeUa = require('fake-useragent');
@@ -975,6 +976,7 @@ class Kernel {
975
976
  this.loader = new Loader()
976
977
  this.bin = new Bin(this)
977
978
  this.api = new Api(this)
979
+ this.watch = new WatchManager(this)
978
980
  this.python = new Python(this)
979
981
  this.shell = new Shells(this)
980
982
  this.appLauncher = new AppLauncher(this)
package/kernel/shell.js CHANGED
@@ -20,6 +20,7 @@ const { applyWindowsNodePackageManagerEnv } = require('./windows_node_package_ma
20
20
  const ShellParser = require('./shell_parser')
21
21
  const AnsiStreamTracker = require('./ansi_stream_tracker')
22
22
  const ShellStateSync = require('./shell_state_sync')
23
+ const ShellRunTemplate = require('./api/shell_run_template')
23
24
  const home = os.homedir()
24
25
 
25
26
  function normalizeComparablePath(filePath, platform) {
@@ -308,7 +309,7 @@ class Shell {
308
309
  delete this.env[key]
309
310
  }
310
311
  let val = this.env[key]
311
- if (/[\r\n]/.test(val)) {
312
+ if (!ShellRunTemplate.isPinokioEnvArgKey(key) && /[\r\n]/.test(val)) {
312
313
  const replaced = val.replaceAll(/[\r\n]+/g, ' ');
313
314
  this.env[key] = replaced
314
315
  // delete this.env[key]
@@ -362,6 +363,9 @@ class Shell {
362
363
  }
363
364
  quoteArgForShell(value, shellName=this.shell) {
364
365
  const input = value == null ? "" : String(value)
366
+ if (ShellRunTemplate.hasEnvArgMarker(input)) {
367
+ return ShellRunTemplate.quoteEnvArgComposite(input, shellName)
368
+ }
365
369
  if (this.isCmdShell(shellName)) {
366
370
  return `"${input.replace(/([()%!^"<>&|])/g, '^$1')}"`
367
371
  }
@@ -409,6 +413,7 @@ class Shell {
409
413
  this.userActive = false
410
414
  this.decsyncBuffer = ''
411
415
  this.stateSync.reset()
416
+ this.envArgsPreviewed = false
412
417
 
413
418
  /*
414
419
  params := {
@@ -540,6 +545,9 @@ class Shell {
540
545
  //}
541
546
  }
542
547
  }
548
+ if (params._pinokio_cmd_delayed_expansion && this.isCmdShell(this.shell) && !this.args.includes("/V:ON")) {
549
+ this.args.push("/V:ON")
550
+ }
543
551
 
544
552
  // 3. path => path can be http, relative, absolute
545
553
  this.path = params.path
@@ -757,6 +765,16 @@ class Shell {
757
765
  }
758
766
  this.stateSync.invalidate({ clearTail: true })
759
767
  }
768
+ emitEnvArgsPreview(params) {
769
+ if (!params || !Array.isArray(params._pinokio_env_args) || params._pinokio_env_args.length === 0 || this.envArgsPreviewed) {
770
+ return
771
+ }
772
+ this.envArgsPreviewed = true
773
+ const raw = ShellRunTemplate.formatEnvArgsPreview(params._pinokio_env_args)
774
+ if (raw && this.ondata) {
775
+ this.ondata({ raw })
776
+ }
777
+ }
760
778
  async run(params, cb) {
761
779
  let r = await this.request(params, cb)
762
780
  return r
@@ -1362,6 +1380,7 @@ class Shell {
1362
1380
  }
1363
1381
  async exec(params) {
1364
1382
  this.parser = new ShellParser()
1383
+ this.emitEnvArgsPreview(params)
1365
1384
  params = await this.activate(params)
1366
1385
  this.cmd = this.build(params)
1367
1386
  let res = await new Promise((resolve, reject) => {
@@ -1729,7 +1748,7 @@ class Shell {
1729
1748
  cmd: this.cmd,
1730
1749
  index: this.index,
1731
1750
  group: this.group,
1732
- env: this.env,
1751
+ env: ShellRunTemplate.redactEnvArgs(this.env),
1733
1752
  done: this.done,
1734
1753
  ready: this.ready,
1735
1754
  id: this.id,
@@ -0,0 +1,42 @@
1
+ const path = require("path")
2
+ const { watchFs } = require("./drivers/fs")
3
+ const { poll } = require("./drivers/poll")
4
+
5
+ class WatchContext {
6
+ constructor(options = {}) {
7
+ this.kernel = options.kernel
8
+ this.manager = options.manager
9
+ this.id = options.id
10
+ this.cwd = path.resolve(options.cwd)
11
+ this.dirname = path.resolve(options.dirname || options.cwd)
12
+ this.request = options.request
13
+ this.script = options.script
14
+ this.declaration = options.declaration
15
+ this.input = options.input || {}
16
+ this.args = options.args || this.input
17
+ this.watch = {
18
+ fs: (targetPath, callback, watchOptions = {}) => {
19
+ return watchFs(this.resolve(targetPath), callback, {
20
+ ...watchOptions,
21
+ onError: watchOptions.onError || ((error) => {
22
+ console.warn("[watch.fs]", error && error.message ? error.message : error)
23
+ })
24
+ })
25
+ }
26
+ }
27
+ }
28
+
29
+ resolve(targetPath) {
30
+ return this.kernel.api.resolvePath(this.cwd, String(targetPath || "."))
31
+ }
32
+
33
+ resolveModule(targetPath) {
34
+ return this.kernel.api.resolvePath(this.dirname, String(targetPath || "."))
35
+ }
36
+
37
+ poll(interval, callback, options = {}) {
38
+ return poll(interval, callback, options)
39
+ }
40
+ }
41
+
42
+ module.exports = WatchContext
@@ -0,0 +1,71 @@
1
+ const fs = require("fs")
2
+ const path = require("path")
3
+ const ParcelWatcher = require("@parcel/watcher")
4
+
5
+ const DEFAULT_IGNORE = [
6
+ "**/.git/**",
7
+ "**/node_modules/**",
8
+ "**/__pycache__/**",
9
+ "**/.venv/**",
10
+ "**/venv/**",
11
+ "**/env/**"
12
+ ]
13
+
14
+ function isInside(candidate, parent) {
15
+ const relative = path.relative(parent, candidate)
16
+ return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative))
17
+ }
18
+
19
+ async function nearestExistingDirectory(targetPath) {
20
+ let current = path.resolve(targetPath)
21
+ while (current && current !== path.dirname(current)) {
22
+ const stats = await fs.promises.stat(current).catch(() => null)
23
+ if (stats && stats.isDirectory()) {
24
+ return current
25
+ }
26
+ current = path.dirname(current)
27
+ }
28
+ return current || path.parse(path.resolve(targetPath)).root
29
+ }
30
+
31
+ async function watchFs(targetPath, callback, options = {}) {
32
+ const resolvedTarget = path.resolve(targetPath)
33
+ const targetStats = await fs.promises.stat(resolvedTarget).catch(() => null)
34
+ const watchRoot = targetStats && targetStats.isDirectory()
35
+ ? resolvedTarget
36
+ : await nearestExistingDirectory(resolvedTarget)
37
+ const filterToTarget = watchRoot !== resolvedTarget
38
+
39
+ const subscription = await ParcelWatcher.subscribe(
40
+ watchRoot,
41
+ (error, events) => {
42
+ if (error) {
43
+ if (typeof options.onError === "function") {
44
+ options.onError(error)
45
+ }
46
+ return
47
+ }
48
+ const normalizedEvents = Array.isArray(events) ? events : []
49
+ const filteredEvents = filterToTarget
50
+ ? normalizedEvents.filter((event) => event && event.path && isInside(path.resolve(event.path), resolvedTarget))
51
+ : normalizedEvents
52
+ if (filteredEvents.length === 0) {
53
+ return
54
+ }
55
+ callback(filteredEvents)
56
+ },
57
+ {
58
+ ignore: Array.isArray(options.ignore) ? options.ignore : DEFAULT_IGNORE
59
+ }
60
+ )
61
+
62
+ return async () => {
63
+ if (subscription && typeof subscription.unsubscribe === "function") {
64
+ await subscription.unsubscribe().catch(() => {})
65
+ }
66
+ }
67
+ }
68
+
69
+ module.exports = {
70
+ watchFs
71
+ }
@@ -0,0 +1,33 @@
1
+ function poll(interval, callback, options = {}) {
2
+ const delay = Math.max(100, Number(interval || options.interval || 1000))
3
+ let stopped = false
4
+ let running = false
5
+
6
+ const tick = async () => {
7
+ if (stopped || running) return
8
+ running = true
9
+ try {
10
+ await callback()
11
+ } catch (error) {
12
+ if (typeof options.onError === "function") {
13
+ options.onError(error)
14
+ }
15
+ } finally {
16
+ running = false
17
+ }
18
+ }
19
+
20
+ if (options.immediate !== false) {
21
+ setTimeout(tick, 0)
22
+ }
23
+ const timer = setInterval(tick, delay)
24
+
25
+ return async () => {
26
+ stopped = true
27
+ clearInterval(timer)
28
+ }
29
+ }
30
+
31
+ module.exports = {
32
+ poll
33
+ }