pinokiod 8.2.13 → 8.2.15

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.
@@ -30,7 +30,6 @@ module.exports = {
30
30
  requirements = requirements.concat([
31
31
  { name: "git", },
32
32
  { name: "node", },
33
- { name: "bun", },
34
33
  { name: "cli", },
35
34
  { name: "ffmpeg", },
36
35
  // { name: "caddy", }
@@ -165,7 +164,6 @@ module.exports = {
165
164
  requirements = requirements.concat([
166
165
  { name: "git", },
167
166
  { name: "node", },
168
- { name: "bun", },
169
167
  { name: "cli", },
170
168
  { name: "uv", },
171
169
  { name: "py", },
@@ -0,0 +1,88 @@
1
+ // Local copy of clear-module 4.1.2 with one logic correction.
2
+ // Fix deep recursive clearing: reuse cached filenames instead of resolving them
3
+ // through parent-module, whose caller lookup fails when V8 truncates the stack.
4
+ // https://github.com/sindresorhus/clear-module/issues/21
5
+
6
+ 'use strict';
7
+ const path = require('path');
8
+ const resolveFrom = require('resolve-from');
9
+ const parentModule = require('parent-module');
10
+
11
+ const resolve = moduleId => {
12
+ try {
13
+ return resolveFrom(path.dirname(parentModule(__filename)), moduleId);
14
+ } catch (_) {}
15
+ };
16
+
17
+ const clear = moduleId => {
18
+ if (typeof moduleId !== 'string') {
19
+ throw new TypeError(`Expected a \`string\`, got \`${typeof moduleId}\``);
20
+ }
21
+
22
+ const filePath = require.cache[moduleId] ? moduleId : resolve(moduleId);
23
+
24
+ if (!filePath) {
25
+ return;
26
+ }
27
+
28
+ // Delete itself from module parent
29
+ if (require.cache[filePath] && require.cache[filePath].parent) {
30
+ let i = require.cache[filePath].parent.children.length;
31
+
32
+ while (i--) {
33
+ if (require.cache[filePath].parent.children[i].id === filePath) {
34
+ require.cache[filePath].parent.children.splice(i, 1);
35
+ }
36
+ }
37
+ }
38
+
39
+ // Remove all descendants from cache as well
40
+ if (require.cache[filePath]) {
41
+ const children = require.cache[filePath].children.map(child => child.id);
42
+
43
+ // Delete module from cache
44
+ delete require.cache[filePath];
45
+
46
+ for (const id of children) {
47
+ clear(id);
48
+ }
49
+ }
50
+ };
51
+
52
+ clear.all = () => {
53
+ const directory = path.dirname(parentModule(__filename));
54
+
55
+ for (const moduleId of Object.keys(require.cache)) {
56
+ delete require.cache[resolveFrom(directory, moduleId)];
57
+ }
58
+ };
59
+
60
+ clear.match = regex => {
61
+ for (const moduleId of Object.keys(require.cache)) {
62
+ if (regex.test(moduleId)) {
63
+ clear(moduleId);
64
+ }
65
+ }
66
+ };
67
+
68
+ clear.single = moduleId => {
69
+ if (typeof moduleId !== 'string') {
70
+ throw new TypeError(`Expected a \`string\`, got \`${typeof moduleId}\``);
71
+ }
72
+
73
+ delete require.cache[resolve(moduleId)];
74
+ };
75
+
76
+ module.exports = clear;
77
+
78
+ /*
79
+ MIT License
80
+
81
+ Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
82
+
83
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
84
+
85
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
86
+
87
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
88
+ */
package/kernel/loader.js CHANGED
@@ -2,7 +2,7 @@ const fs = require("fs")
2
2
  const { parse } = require("csv-parse")
3
3
  const YAML = require("yaml")
4
4
  const path = require("path")
5
- const clearModule = require('clear-module');
5
+ const clearModule = require('./clear_module');
6
6
 
7
7
  class Loader {
8
8
  async load(_path) {
package/kernel/shell.js CHANGED
@@ -627,23 +627,25 @@ class Shell {
627
627
  // Modify process.env (and need to immediately revert it back to original process.env so as to not affect other logic)
628
628
  let old_env = process.env
629
629
  process.env = this.env
630
- sudo.exec(params.message, options, (err, stdout, stderr) => {
631
- if (err) {
632
- console.log("SUDOPROMPT ERR", err)
633
- // even when there's an error, just log the error, and don't throw the error. Instead, print the stdout so it displays what happened
634
- resolve(stdout)
630
+ try {
631
+ sudo.exec(params.message, options, (err, stdout, stderr) => {
632
+ if (err) {
633
+ console.log("SUDOPROMPT ERR", err)
634
+ // even when there's an error, just log the error, and don't throw the error. Instead, print the stdout so it displays what happened
635
+ resolve(stdout)
635
636
  // reject(err)
636
- } else if (stderr) {
637
- console.log("SUDOPROMPT STDERR", stderr)
638
- resolve(stdout)
637
+ } else if (stderr) {
638
+ console.log("SUDOPROMPT STDERR", stderr)
639
+ resolve(stdout)
639
640
  // reject(stderr)
640
- } else {
641
- resolve(stdout)
642
- }
643
- });
644
-
645
- // Immediately revert env back to original
646
- process.env = old_env
641
+ } else {
642
+ resolve(stdout)
643
+ }
644
+ });
645
+ } finally {
646
+ // Restore the environment even if the elevation helper throws.
647
+ process.env = old_env
648
+ }
647
649
  })
648
650
  if (ondata) ondata({
649
651
  id: this.id,
package/kernel/util.js CHANGED
@@ -30,7 +30,8 @@ const {
30
30
  const platform = os.platform()
31
31
  const WINDOWS_TOAST_APP_ID = process.env.PINOKIO_WINDOWS_APP_ID || 'computer.pinokio'
32
32
  const DEFAULT_CHIME_URL_PATH = '/chime.mp3'
33
- const pushListeners = new Set()
33
+ // Process listeners survive reloads of this module.
34
+ const PUSH_EVENT = 'pinokio:push'
34
35
  let cachedMacNotifierPath
35
36
 
36
37
  function resolveAsarPath(p) {
@@ -67,15 +68,15 @@ function registerPushListener(listener) {
67
68
  if (typeof listener !== 'function') {
68
69
  throw new TypeError('push listener must be a function')
69
70
  }
70
- pushListeners.add(listener)
71
- return () => pushListeners.delete(listener)
71
+ if (!process.listeners(PUSH_EVENT).includes(listener)) process.on(PUSH_EVENT, listener)
72
+ return () => process.removeListener(PUSH_EVENT, listener)
72
73
  }
73
74
 
74
75
  function emitPushEvent(event) {
75
76
  if (!event) {
76
77
  return
77
78
  }
78
- pushListeners.forEach((listener) => {
79
+ process.listeners(PUSH_EVENT).forEach((listener) => {
79
80
  try {
80
81
  listener(event)
81
82
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.2.13",
3
+ "version": "8.2.15",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -21,7 +21,6 @@
21
21
  "axios": "^1.4.0",
22
22
  "batch": "^0.6.1",
23
23
  "better-sqlite3": "12.11.1",
24
- "clear-module": "^4.1.2",
25
24
  "compressing": "^1.10.0",
26
25
  "cookie-parser": "^1.4.6",
27
26
  "copy-paste": "^2.2.0",
@@ -68,6 +67,7 @@
68
67
  "node-gradio-client": "^0.14.6",
69
68
  "normalize-path": "^3.0.0",
70
69
  "p-limit": "^3.1.0",
70
+ "parent-module": "^2.0.0",
71
71
  "parseurl": "^1.3.3",
72
72
  "pdrive": "^0.0.27",
73
73
  "portfinder-cp": "^1.0.34",
@@ -75,11 +75,12 @@
75
75
  "qrcode": "^1.5.3",
76
76
  "querystring": "^0.2.1",
77
77
  "random-useragent": "^0.5.0",
78
+ "resolve-from": "^5.0.0",
78
79
  "rimraf": "^5.0.0",
79
80
  "sanitize-filename": "^1.6.3",
80
81
  "semver": "^7.5.4",
81
82
  "shell-path": "^2.0.0",
82
- "sudo-prompt-programfiles-x86": "^9.2.10",
83
+ "sudo-prompt-programfiles-x86": "^9.2.11",
83
84
  "symlink-dir": "^5.2.1",
84
85
  "systeminformation": "^5.31.1",
85
86
  "toasted-notifier": "^10.1.0",
@@ -1,6 +1,6 @@
1
1
  const fs = require("fs")
2
2
  const path = require("path")
3
- const clearModule = require("clear-module")
3
+ const clearModule = require("../../kernel/clear_module")
4
4
  const PluginSources = require("../../kernel/plugin_sources")
5
5
 
6
6
  const {
@@ -195,7 +195,7 @@ test('Conda uses Miniforge assets and writes conda-forge-only config', async ()
195
195
 
196
196
  assert.equal(
197
197
  conda.urls.win32.x64,
198
- 'https://github.com/conda-forge/miniforge/releases/download/26.9.4-0/Miniforge3-Windows-x86_64.exe'
198
+ 'https://github.com/conda-forge/miniforge/releases/download/26.3.2-3/Miniforge3-Windows-x86_64.exe'
199
199
  )
200
200
 
201
201
  await conda.init()
@@ -0,0 +1,131 @@
1
+ const assert = require('node:assert/strict')
2
+ const { execFileSync } = require('node:child_process')
3
+ const path = require('node:path')
4
+ const test = require('node:test')
5
+
6
+ function isolated(source) {
7
+ execFileSync(process.execPath, ['-e', `
8
+ const assert = require('node:assert/strict')
9
+ const fs = require('node:fs')
10
+ const os = require('node:os')
11
+ const path = require('node:path')
12
+ const root = process.argv[1]
13
+ const from = require('node:module').createRequire(root + '/kernel/bin/index.js')
14
+ const Loader = from(root + '/kernel/loader.js')
15
+ const loader = new Loader()
16
+ const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'pinokio-reload-'))
17
+ ;(async () => {
18
+ try { ${source} }
19
+ finally { fs.rmSync(fixture, { recursive: true, force: true }) }
20
+ })().catch(error => { console.error(error); process.exitCode = 1 })
21
+ `, path.resolve(__dirname, '..')], { timeout: 30000, stdio: 'pipe' })
22
+ }
23
+
24
+ test('managed module reloads preserve compiler, Python and venv discovery', () => {
25
+ isolated(`
26
+ from(root + '/kernel/bin/index.js')
27
+ const compiler = '2019/BuildTools/VC/Tools/MSVC'
28
+ fs.mkdirSync(path.join(fixture, compiler), { recursive: true })
29
+ const bin = process.platform === 'win32' ? 'Scripts' : 'bin'
30
+ const python = process.platform === 'win32' ? 'python.exe' : 'python'
31
+ fs.mkdirSync(path.join(fixture, 'env', bin), { recursive: true })
32
+ fs.writeFileSync(path.join(fixture, 'env', bin, python), '')
33
+ const files = fs.readdirSync(root + '/kernel/bin').filter(file =>
34
+ file.endsWith('.js') && file !== 'index.js' && file !== 'cmake.js')
35
+ for (let round = 0; round < 2; round++) {
36
+ for (const file of files) {
37
+ await loader.load(root + '/kernel/bin/' + file)
38
+ assert.equal((await from('glob').glob('**/VC/Tools/MSVC', { cwd: fixture })).length, 1, file)
39
+ const util = from(root + '/kernel/util.js')
40
+ assert.equal((await util.find_python(fixture)).length, 1, file)
41
+ assert.deepEqual(await util.find_venv(fixture), [path.join(fixture, 'env')], file)
42
+ }
43
+ // Exercise the delayed path resolver before the next reload cycle.
44
+ await from(root + '/kernel/bin/conda-meta.js')
45
+ .runPipSeedRepair(path.join(fixture, 'missing-python'), 'unused').catch(() => {})
46
+ }
47
+ for (const api of ['push', 'fs', 'shell', 'load', 'json', 'set', 'rm']) {
48
+ for (let round = 0; round < 2; round++) {
49
+ await loader.load(root + '/kernel/api/' + api)
50
+ assert.equal((await from('glob').glob('**/VC/Tools/MSVC', { cwd: fixture })).length, 1, api)
51
+ assert.equal((await from(root + '/kernel/util.js').find_python(fixture)).length, 1, api)
52
+ }
53
+ }
54
+ `)
55
+ })
56
+
57
+ test('browser push delivery and listener disposal survive API reloads', () => {
58
+ isolated(`
59
+ const Socket = from(root + '/server/socket.js')
60
+ const socket = Object.create(Socket.prototype)
61
+ const messages = []
62
+ const client = { readyState: 1, send: frame => messages.push(JSON.parse(frame)) }
63
+ socket.notificationChannel = 'kernel.notifications'
64
+ socket.subscriptions = new Map([[socket.notificationChannel, new Set([client])]])
65
+ socket.ensureNotificationBridge()
66
+ try {
67
+ for (let round = 0; round < 3; round++) {
68
+ const push = (await loader.load(root + '/kernel/api/push')).resolved
69
+ await push({ cwd: fixture, params: { title: 'Fixture', message: String(round), host: false, sound: false } })
70
+ }
71
+ assert.deepEqual(messages.map(message => message.data.message), ['0', '1', '2'])
72
+ socket.subscriptions.clear()
73
+ socket.checkNotificationBridge()
74
+ const util = from(root + '/kernel/util.js')
75
+ util.push({ host: false, sound: false, message: 'disposed' })
76
+ assert.equal(messages.length, 3)
77
+
78
+ let delivered = 0
79
+ const listener = () => delivered++
80
+ const dispose = util.registerPushListener(listener)
81
+ const reloaded = (await loader.load(root + '/kernel/util.js')).resolved
82
+ const disposeAgain = reloaded.registerPushListener(listener)
83
+ reloaded.emitPushEvent({ fixture: true })
84
+ assert.equal(delivered, 1, 'duplicate registration remains deduplicated')
85
+ dispose()
86
+ reloaded.emitPushEvent({ fixture: true })
87
+ assert.equal(delivered, 1, 'old disposer removes a listener after reload')
88
+ disposeAgain()
89
+ } finally {
90
+ if (socket.notificationBridgeDispose) socket.notificationBridgeDispose()
91
+ }
92
+ `)
93
+ })
94
+
95
+ test('launcher loading and validation reload sibling helpers and app-owned packages', () => {
96
+ isolated(`
97
+ const app = path.join(fixture, 'app')
98
+ const vendor = path.join(app, 'node_modules/fixture-lib/index.js')
99
+ fs.mkdirSync(path.dirname(vendor), { recursive: true })
100
+ const helper = path.join(fixture, 'helper.js')
101
+ const launcher = path.join(app, 'pinokio.js')
102
+ const glob = from('glob')
103
+ fs.writeFileSync(launcher, "require(" + JSON.stringify(from.resolve('glob')) + "); module.exports = { title: require('../helper').title + require('./node_modules/fixture-lib').version }")
104
+ fs.writeFileSync(helper, "module.exports = { title: 'before' }")
105
+ fs.writeFileSync(vendor, "module.exports = { version: '1' }")
106
+ assert.equal((await loader.load(launcher)).resolved.title, 'before1')
107
+ fs.writeFileSync(helper, "module.exports = { title: 'after' }")
108
+ fs.writeFileSync(vendor, "module.exports = { version: '2' }")
109
+ assert.equal((await loader.load(launcher)).resolved.title, 'after2')
110
+ const kernel = { path: () => app, api: { launcher_path: async () => app } }
111
+ const validation = from(root + '/server/lib/content_validation.js').createContentValidationService({ kernel })
112
+ fs.writeFileSync(helper, "module.exports = { title: 'validated' }")
113
+ assert.equal((await validation.validateAppByName('fixture')).subjectTitle, 'validated2')
114
+ assert.notEqual(from('glob'), glob, 'shared dependencies retain recursive reload behavior')
115
+ `)
116
+ })
117
+
118
+ test('recursive clearing reloads dependencies beyond the caller stack limit', () => {
119
+ isolated(`
120
+ Error.stackTraceLimit = 10
121
+ for (let index = 0; index < 25; index++) {
122
+ fs.writeFileSync(path.join(fixture, index + '.js'), index === 24
123
+ ? 'module.exports = 1'
124
+ : "module.exports = require('./" + (index + 1) + ".js')")
125
+ }
126
+ const entry = path.join(fixture, '0.js')
127
+ assert.equal((await loader.load(entry)).resolved, 1)
128
+ fs.writeFileSync(path.join(fixture, '24.js'), 'module.exports = 2')
129
+ assert.equal((await loader.load(entry)).resolved, 2)
130
+ `)
131
+ })
@@ -0,0 +1,105 @@
1
+ const assert = require('node:assert/strict')
2
+ const fs = require('node:fs')
3
+ const os = require('node:os')
4
+ const path = require('node:path')
5
+ const test = require('node:test')
6
+ const vm = require('node:vm')
7
+
8
+ function windowsPrompt(root, exitCode = 0) {
9
+ const scripts = []
10
+ const module = { exports: {} }
11
+ const processStub = { platform: 'win32', env: process.env, cwd: () => root, title: 'Pinokio' }
12
+ const child = {
13
+ exec(command, options, callback) {
14
+ const directory = path.join(root, fs.readdirSync(root)[0])
15
+ if (command.startsWith('rmdir ')) {
16
+ assert.equal(command, `rmdir /s /q "${directory}"`)
17
+ fs.rmSync(directory, { recursive: true })
18
+ queueMicrotask(() => callback(null, '', ''))
19
+ return
20
+ }
21
+ scripts.push(fs.readFileSync(path.join(directory, 'command.bat'), 'utf8'))
22
+ fs.writeFileSync(path.join(directory, 'stdout'), 'fixture output')
23
+ fs.writeFileSync(path.join(directory, 'stderr'), exitCode ? 'fixture failure' : '')
24
+ fs.writeFileSync(path.join(directory, 'status'), exitCode + '\r\n')
25
+ queueMicrotask(() => callback(null, '', ''))
26
+ return { stdin: { end() {} } }
27
+ }
28
+ }
29
+ vm.runInNewContext(fs.readFileSync(require.resolve('sudo-prompt-programfiles-x86'), 'utf8'), {
30
+ module, process: processStub, setTimeout, console: { log() {} },
31
+ require(name) {
32
+ if (name === 'util') return {} // Node 24 no longer provides util.isObject/isFunction.
33
+ if (name === 'os') return { tmpdir: () => root }
34
+ if (name === 'child_process') return child
35
+ return require(name)
36
+ }
37
+ })
38
+ return { exec: module.exports.exec, scripts }
39
+ }
40
+
41
+ test('elevation accepts both callback signatures without removed Node utilities', () => {
42
+ const sudo = require('sudo-prompt-programfiles-x86')
43
+ for (const options of [null, { name: 'Pinokio' }]) {
44
+ let called = false
45
+ const callback = error => {
46
+ called = true
47
+ assert.match(error.message, /should not be prefixed/)
48
+ }
49
+ // Validation must finish before any platform elevation or prompt is attempted.
50
+ if (options) sudo.exec('sudo echo fixture', options, callback)
51
+ else sudo.exec('sudo echo fixture', callback)
52
+ assert.equal(called, true)
53
+ }
54
+ })
55
+
56
+ test('Windows elevation backend writes requested environment and reports success or failure', async t => {
57
+ for (const code of [0, 7]) {
58
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pinokio-sudo-'))
59
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }))
60
+ const prompt = windowsPrompt(root, code)
61
+ const result = await new Promise(resolve => prompt.exec('echo fixture', {
62
+ name: 'Pinokio', env: { PATH: 'fixture-path', 'ProgramFiles(x86)': 'C:\\Program Files (x86)' }
63
+ }, (error, stdout, stderr) => resolve({ error, stdout, stderr })))
64
+ assert.equal(prompt.scripts.length, 1)
65
+ assert.match(prompt.scripts[0], /set PATH=fixture-path/)
66
+ assert.ok(prompt.scripts[0].includes('set ProgramFiles(x86)=C:^\\Program Files (x86)'))
67
+ assert.match(prompt.scripts[0], /echo fixture/)
68
+ assert.equal(result.stdout, 'fixture output')
69
+ assert.equal(result.stderr, code ? 'fixture failure' : '')
70
+ if (code) {
71
+ assert.equal(result.error.code, code)
72
+ assert.match(result.error.message, /fixture failure/)
73
+ } else {
74
+ assert.equal(result.error, undefined)
75
+ assert.equal(result.stdout, 'fixture output')
76
+ }
77
+ }
78
+ })
79
+
80
+ test('Shell restores its process environment and preserves elevation error output', async t => {
81
+ const Shell = require('../kernel/shell')
82
+ const sudo = require('sudo-prompt-programfiles-x86')
83
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pinokio-sudo-shell-'))
84
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }))
85
+ for (const synchronous of [true, false]) {
86
+ const originalEnv = process.env
87
+ const error = new Error('fixture elevation failure')
88
+ const shell = new Shell({ shell: { add() {} } })
89
+ shell.init_env = async () => { shell.env = { ...process.env, TEMP: root, 'ProgramFiles(x86)': 'fixture programs' } }
90
+ const mock = t.mock.method(sudo, 'exec', (command, options, callback) => {
91
+ assert.equal(process.env['ProgramFiles(x86)'], 'fixture programs')
92
+ if (synchronous) throw error
93
+ callback(error, 'fixture output', 'fixture stderr')
94
+ })
95
+ try {
96
+ const run = () => shell.start({ path: root, message: 'echo fixture', sudo: true }, () => {})
97
+ if (synchronous) await assert.rejects(run(), failure => failure === error)
98
+ else assert.equal(await run(), 'fixture output')
99
+ assert.equal(process.env, originalEnv)
100
+ } finally {
101
+ mock.mock.restore()
102
+ if (shell.vt) shell.vt.dispose()
103
+ }
104
+ }
105
+ })