pinokiod 7.5.7 → 7.5.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.
- 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/autolaunch.ejs +1 -1
- 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/launch-settings-ui.test.js +20 -0
- 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
|
@@ -1639,6 +1639,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1639
1639
|
this.currentId = baseScriptId
|
|
1640
1640
|
this.latestLogFallbackActive = false
|
|
1641
1641
|
this.pendingInput = []
|
|
1642
|
+
this.pendingTerminalWrite = ""
|
|
1643
|
+
this.pendingTerminalWriteFrame = null
|
|
1642
1644
|
this.inputBuffer = ""
|
|
1643
1645
|
this.inputTracker = window.TerminalInputTracker ? new window.TerminalInputTracker({
|
|
1644
1646
|
getFrameName: () => window.name || null,
|
|
@@ -1723,6 +1725,32 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1723
1725
|
}
|
|
1724
1726
|
this.dirty = true
|
|
1725
1727
|
}
|
|
1728
|
+
flushPendingTerminalWrite() {
|
|
1729
|
+
if (!this.pendingTerminalWrite) {
|
|
1730
|
+
this.pendingTerminalWriteFrame = null
|
|
1731
|
+
return
|
|
1732
|
+
}
|
|
1733
|
+
const text = this.pendingTerminalWrite
|
|
1734
|
+
this.pendingTerminalWrite = ""
|
|
1735
|
+
this.pendingTerminalWriteFrame = null
|
|
1736
|
+
this.write(text)
|
|
1737
|
+
}
|
|
1738
|
+
writeBatched(text) {
|
|
1739
|
+
if (text === "\u0007" || typeof text !== "string" || text.length === 0) {
|
|
1740
|
+
return
|
|
1741
|
+
}
|
|
1742
|
+
this.pendingTerminalWrite += text
|
|
1743
|
+
this.dirty = true
|
|
1744
|
+
if (this.pendingTerminalWriteFrame !== null) {
|
|
1745
|
+
return
|
|
1746
|
+
}
|
|
1747
|
+
const flush = () => this.flushPendingTerminalWrite()
|
|
1748
|
+
if (typeof window.requestAnimationFrame === "function") {
|
|
1749
|
+
this.pendingTerminalWriteFrame = window.requestAnimationFrame(flush)
|
|
1750
|
+
} else {
|
|
1751
|
+
this.pendingTerminalWriteFrame = window.setTimeout(flush, 16)
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1726
1754
|
finished() {
|
|
1727
1755
|
/*
|
|
1728
1756
|
n.Noty({
|
|
@@ -1921,13 +1949,13 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1921
1949
|
this.resizeSync.sendInitial()
|
|
1922
1950
|
}
|
|
1923
1951
|
if (packet.data.raw) {
|
|
1924
|
-
this.
|
|
1952
|
+
this.writeBatched(packet.data.raw)
|
|
1925
1953
|
} else if (packet.data.json) {
|
|
1926
|
-
this.
|
|
1927
|
-
this.
|
|
1954
|
+
this.writeBatched(JSON.stringify(packet.data.json).replace(/\n/g, "\r\n"))
|
|
1955
|
+
this.writeBatched("\r\n")
|
|
1928
1956
|
} else if (packet.data.json2) {
|
|
1929
|
-
this.
|
|
1930
|
-
this.
|
|
1957
|
+
this.writeBatched(JSON.stringify(packet.data.json2, null, 2).replace(/\n/g, "\r\n"))
|
|
1958
|
+
this.writeBatched("\r\n")
|
|
1931
1959
|
}
|
|
1932
1960
|
if (packet.data.type === "emit2") {
|
|
1933
1961
|
// long text => still posting to the shell => show progress
|
|
@@ -1945,6 +1973,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1945
1973
|
refreshParent(packet)
|
|
1946
1974
|
pluginTerminalDiscoveryRefresher.clear()
|
|
1947
1975
|
reloadMemory()
|
|
1976
|
+
this.flushPendingTerminalWrite()
|
|
1948
1977
|
this.term.write("\r\nDisconnected...\r\n")
|
|
1949
1978
|
document.querySelector("#status-window").innerHTML = "<b>Ready</b>"
|
|
1950
1979
|
this.socket.close()
|
|
@@ -74,6 +74,26 @@ test("launch settings visible labels use autolaunch wording", async () => {
|
|
|
74
74
|
assert.doesNotMatch(sidebar, />Launch settings</)
|
|
75
75
|
})
|
|
76
76
|
|
|
77
|
+
test("autolaunch page presents launch script choices before requirements", async () => {
|
|
78
|
+
const globalView = await fs.readFile(path.resolve(root, "server/views/autolaunch.ejs"), "utf8")
|
|
79
|
+
|
|
80
|
+
const currentIndex = globalView.indexOf('<h3 class="autolaunch-section-title">Current</h3>')
|
|
81
|
+
const menuIndex = globalView.indexOf('${renderScriptSection("Menu scripts"')
|
|
82
|
+
const otherIndex = globalView.indexOf('${renderScriptSection("Other local scripts"')
|
|
83
|
+
const manualIndex = globalView.indexOf('<div class="autolaunch-manual">')
|
|
84
|
+
const requirementsIndex = globalView.indexOf('${renderDependencySection(app)}')
|
|
85
|
+
|
|
86
|
+
assert.notEqual(currentIndex, -1)
|
|
87
|
+
assert.notEqual(menuIndex, -1)
|
|
88
|
+
assert.notEqual(otherIndex, -1)
|
|
89
|
+
assert.notEqual(manualIndex, -1)
|
|
90
|
+
assert.notEqual(requirementsIndex, -1)
|
|
91
|
+
assert.ok(currentIndex < menuIndex)
|
|
92
|
+
assert.ok(menuIndex < otherIndex)
|
|
93
|
+
assert.ok(otherIndex < manualIndex)
|
|
94
|
+
assert.ok(manualIndex < requirementsIndex)
|
|
95
|
+
})
|
|
96
|
+
|
|
77
97
|
test("startup progress uses canonical ready-state progress instead of autolaunch mirrors", async () => {
|
|
78
98
|
const kernelIndex = await fs.readFile(path.resolve(root, "kernel/index.js"), "utf8")
|
|
79
99
|
const autolaunch = await fs.readFile(path.resolve(root, "kernel/autolaunch.js"), "utf8")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const test = require('node:test')
|
|
3
|
+
|
|
4
|
+
const Node = require('../kernel/bin/node')
|
|
5
|
+
|
|
6
|
+
function createNode({ nodeVersion = '24.18.0', pnpmVersion = '11.9.0', hasNode = true, hasPnpm = true } = {}) {
|
|
7
|
+
const node = new Node()
|
|
8
|
+
const conda = new Set()
|
|
9
|
+
if (hasNode) conda.add('nodejs')
|
|
10
|
+
if (hasPnpm) conda.add('pnpm')
|
|
11
|
+
node.kernel = {
|
|
12
|
+
bin: {
|
|
13
|
+
installed: {
|
|
14
|
+
conda,
|
|
15
|
+
conda_versions: {
|
|
16
|
+
nodejs: nodeVersion,
|
|
17
|
+
pnpm: pnpmVersion,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
return node
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('Node bin pins Node.js 24 LTS and matching pnpm', () => {
|
|
26
|
+
assert.equal(createNode().cmd(), 'nodejs=24.18.0 pnpm=11.9.0')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('Node.installed accepts exactly the pinned Node.js and pnpm versions', async () => {
|
|
30
|
+
assert.equal(await createNode().installed(), true)
|
|
31
|
+
assert.equal(await createNode({ nodeVersion: '24.18.0-h654b19f_0' }).installed(), true)
|
|
32
|
+
assert.equal(await createNode({ pnpmVersion: '11.9.0-h7c87c79_0' }).installed(), true)
|
|
33
|
+
assert.equal(await createNode({ nodeVersion: '22.21.1' }).installed(), false)
|
|
34
|
+
assert.equal(await createNode({ nodeVersion: '26.4.0' }).installed(), false)
|
|
35
|
+
assert.equal(await createNode({ pnpmVersion: '10.29.3' }).installed(), false)
|
|
36
|
+
assert.equal(await createNode({ pnpmVersion: '11.8.0' }).installed(), false)
|
|
37
|
+
assert.equal(await createNode({ hasNode: false }).installed(), false)
|
|
38
|
+
assert.equal(await createNode({ hasPnpm: false }).installed(), false)
|
|
39
|
+
})
|
package/test/shell-api.test.js
CHANGED
|
@@ -2,6 +2,9 @@ const test = require('node:test')
|
|
|
2
2
|
const assert = require('node:assert/strict')
|
|
3
3
|
const path = require('node:path')
|
|
4
4
|
const ShellAPI = require('../kernel/api/shell')
|
|
5
|
+
const Shell = require('../kernel/shell')
|
|
6
|
+
const Shells = require('../kernel/shells')
|
|
7
|
+
const CondaRuntimeGuard = require('../kernel/shell_conda_runtime_guard')
|
|
5
8
|
|
|
6
9
|
function createKernel () {
|
|
7
10
|
const calls = []
|
|
@@ -10,23 +13,23 @@ function createKernel () {
|
|
|
10
13
|
homedir: '/tmp/pinokio-home',
|
|
11
14
|
shell: {
|
|
12
15
|
start: async (params, options) => {
|
|
13
|
-
calls.push({ method: 'start', params: structuredClone(params), options: structuredClone(options) })
|
|
16
|
+
calls.push({ method: 'start', guard: params[CondaRuntimeGuard.SHELL_RUN_GUARD] === true, params: structuredClone(params), options: structuredClone(options) })
|
|
14
17
|
return 'shell-id'
|
|
15
18
|
},
|
|
16
19
|
enter: async (params) => {
|
|
17
|
-
calls.push({ method: 'enter', params: structuredClone(params) })
|
|
20
|
+
calls.push({ method: 'enter', guard: params[CondaRuntimeGuard.SHELL_RUN_GUARD] === true, params: structuredClone(params) })
|
|
18
21
|
return 'entered'
|
|
19
22
|
},
|
|
20
23
|
write: async (params) => {
|
|
21
|
-
calls.push({ method: 'write', params: structuredClone(params) })
|
|
24
|
+
calls.push({ method: 'write', guard: params[CondaRuntimeGuard.SHELL_RUN_GUARD] === true, params: structuredClone(params) })
|
|
22
25
|
return 'written'
|
|
23
26
|
},
|
|
24
27
|
run: async (params, options) => {
|
|
25
|
-
calls.push({ method: 'run', params: structuredClone(params), options: structuredClone(options) })
|
|
28
|
+
calls.push({ method: 'run', guard: params[CondaRuntimeGuard.SHELL_RUN_GUARD] === true, params: structuredClone(params), options: structuredClone(options) })
|
|
26
29
|
return { stdout: 'ok' }
|
|
27
30
|
},
|
|
28
31
|
kill: async (params) => {
|
|
29
|
-
calls.push({ method: 'kill', params: structuredClone(params) })
|
|
32
|
+
calls.push({ method: 'kill', guard: params[CondaRuntimeGuard.SHELL_RUN_GUARD] === true, params: structuredClone(params) })
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
}
|
|
@@ -51,6 +54,7 @@ test('shell.start forwards defaults, client size, parent, and shell options', as
|
|
|
51
54
|
assert.equal(result, 'shell-id')
|
|
52
55
|
assert.deepEqual(kernel.calls, [{
|
|
53
56
|
method: 'start',
|
|
57
|
+
guard: false,
|
|
54
58
|
params: {
|
|
55
59
|
id: cwd,
|
|
56
60
|
path: cwd,
|
|
@@ -76,25 +80,28 @@ test('shell.enter, shell.write, and shell.stop target current cwd by default', a
|
|
|
76
80
|
const cwd = path.join('/tmp', 'pinokio-shell-api')
|
|
77
81
|
const parent = { path: path.join(cwd, 'script.js') }
|
|
78
82
|
|
|
79
|
-
assert.equal(await api.enter({ cwd, parent, params: { message: '
|
|
80
|
-
assert.equal(await api.write({ cwd, params: { message: '
|
|
83
|
+
assert.equal(await api.enter({ cwd, parent, params: { message: 'conda install python=3.12 -y' } }, () => {}, kernel), 'entered')
|
|
84
|
+
assert.equal(await api.write({ cwd, params: { message: 'conda update --all' } }, () => {}, kernel), 'written')
|
|
81
85
|
await api.stop({ cwd, params: {} }, () => {}, kernel)
|
|
82
86
|
|
|
83
87
|
assert.deepEqual(kernel.calls, [{
|
|
84
88
|
method: 'enter',
|
|
89
|
+
guard: false,
|
|
85
90
|
params: {
|
|
86
|
-
message: '
|
|
91
|
+
message: 'conda install python=3.12 -y',
|
|
87
92
|
id: cwd,
|
|
88
93
|
$parent: parent
|
|
89
94
|
}
|
|
90
95
|
}, {
|
|
91
96
|
method: 'write',
|
|
97
|
+
guard: false,
|
|
92
98
|
params: {
|
|
93
|
-
message: '
|
|
99
|
+
message: 'conda update --all',
|
|
94
100
|
id: cwd
|
|
95
101
|
}
|
|
96
102
|
}, {
|
|
97
103
|
method: 'kill',
|
|
104
|
+
guard: false,
|
|
98
105
|
params: {
|
|
99
106
|
id: cwd
|
|
100
107
|
}
|
|
@@ -118,6 +125,7 @@ test('shell.run applies deterministic defaults and forwards execution options',
|
|
|
118
125
|
assert.deepEqual(result, { stdout: 'ok' })
|
|
119
126
|
assert.deepEqual(kernel.calls, [{
|
|
120
127
|
method: 'run',
|
|
128
|
+
guard: true,
|
|
121
129
|
params: {
|
|
122
130
|
message: 'echo ok',
|
|
123
131
|
path: cwd,
|
|
@@ -132,3 +140,85 @@ test('shell.run applies deterministic defaults and forwards execution options',
|
|
|
132
140
|
}
|
|
133
141
|
}])
|
|
134
142
|
})
|
|
143
|
+
|
|
144
|
+
test('shell.run forwards typed stream events', async () => {
|
|
145
|
+
const api = new ShellAPI()
|
|
146
|
+
const events = []
|
|
147
|
+
const kernel = createKernel()
|
|
148
|
+
kernel.shell.run = async (params, options, ondata) => {
|
|
149
|
+
ondata({ html: 'notice', type: 'warning' }, 'notify')
|
|
150
|
+
return { stdout: 'ok' }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const result = await api.run({
|
|
154
|
+
cwd: path.join('/tmp', 'pinokio-shell-api'),
|
|
155
|
+
params: {
|
|
156
|
+
message: 'echo ok'
|
|
157
|
+
}
|
|
158
|
+
}, (stream, type) => {
|
|
159
|
+
events.push({ stream, type })
|
|
160
|
+
}, kernel)
|
|
161
|
+
|
|
162
|
+
assert.deepEqual(result, { stdout: 'ok' })
|
|
163
|
+
assert.deepEqual(events, [{
|
|
164
|
+
stream: {
|
|
165
|
+
html: 'notice',
|
|
166
|
+
type: 'warning'
|
|
167
|
+
},
|
|
168
|
+
type: 'notify'
|
|
169
|
+
}])
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('Shells.launch forwards typed stream events without live-output parsing', async () => {
|
|
173
|
+
const events = []
|
|
174
|
+
const originalStart = Shell.prototype.start
|
|
175
|
+
Shell.prototype.start = async function (_params, ondata) {
|
|
176
|
+
this.id = 'stub-shell'
|
|
177
|
+
await ondata({ html: 'notice', type: 'warning' }, 'notify')
|
|
178
|
+
return ''
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const root = path.join('/tmp', 'pinokio-shells-api')
|
|
183
|
+
const shells = new Shells({
|
|
184
|
+
homedir: root,
|
|
185
|
+
platform: 'darwin',
|
|
186
|
+
bracketedPasteSupport: { bash: true },
|
|
187
|
+
which: () => null,
|
|
188
|
+
path: (...parts) => path.join(root, ...parts),
|
|
189
|
+
bin: {
|
|
190
|
+
envs: (env = {}) => env
|
|
191
|
+
},
|
|
192
|
+
api: {
|
|
193
|
+
resolvePath: (cwd, target) => path.resolve(cwd, target),
|
|
194
|
+
running: {}
|
|
195
|
+
},
|
|
196
|
+
git: {
|
|
197
|
+
repos: async () => [],
|
|
198
|
+
restoreNewReposForActiveSnapshot: async () => {}
|
|
199
|
+
},
|
|
200
|
+
template: {
|
|
201
|
+
render: (value) => value
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
await shells.launch({
|
|
206
|
+
path: path.join(root, 'api', 'demo'),
|
|
207
|
+
message: 'echo ok'
|
|
208
|
+
}, {
|
|
209
|
+
cwd: root
|
|
210
|
+
}, (stream, type) => {
|
|
211
|
+
events.push({ stream, type })
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
assert.deepEqual(events, [{
|
|
215
|
+
stream: {
|
|
216
|
+
html: 'notice',
|
|
217
|
+
type: 'warning'
|
|
218
|
+
},
|
|
219
|
+
type: 'notify'
|
|
220
|
+
}])
|
|
221
|
+
} finally {
|
|
222
|
+
Shell.prototype.start = originalStart
|
|
223
|
+
}
|
|
224
|
+
})
|