pinokiod 3.271.0 → 3.273.0
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/ansi_stream_tracker.js +115 -0
- package/kernel/api/app/index.js +422 -0
- package/kernel/api/htmlmodal/index.js +94 -0
- package/kernel/app_launcher/index.js +115 -0
- package/kernel/app_launcher/platform/base.js +276 -0
- package/kernel/app_launcher/platform/linux.js +229 -0
- package/kernel/app_launcher/platform/macos.js +163 -0
- package/kernel/app_launcher/platform/unsupported.js +34 -0
- package/kernel/app_launcher/platform/windows.js +247 -0
- package/kernel/bin/conda-meta.js +93 -0
- package/kernel/bin/conda.js +2 -4
- package/kernel/bin/index.js +2 -4
- package/kernel/index.js +7 -0
- package/kernel/shell.js +212 -1
- package/package.json +1 -1
- package/server/index.js +491 -6
- package/server/public/common.js +224 -741
- package/server/public/create-launcher.js +754 -0
- package/server/public/htmlmodal.js +292 -0
- package/server/public/logs.js +715 -0
- package/server/public/resizeSync.js +117 -0
- package/server/public/style.css +653 -8
- package/server/public/tab-idle-notifier.js +34 -59
- package/server/public/tab-link-popover.js +7 -10
- package/server/public/terminal-settings.js +723 -9
- package/server/public/terminal_input_utils.js +72 -0
- package/server/public/terminal_key_caption.js +187 -0
- package/server/public/urldropdown.css +120 -3
- package/server/public/xterm-inline-bridge.js +116 -0
- package/server/socket.js +29 -0
- package/server/views/agents.ejs +1 -2
- package/server/views/app.ejs +55 -28
- package/server/views/bookmarklet.ejs +1 -1
- package/server/views/bootstrap.ejs +1 -0
- package/server/views/connect.ejs +1 -2
- package/server/views/create.ejs +63 -0
- package/server/views/editor.ejs +36 -4
- package/server/views/index.ejs +1 -2
- package/server/views/index2.ejs +1 -2
- package/server/views/init/index.ejs +36 -28
- package/server/views/install.ejs +20 -22
- package/server/views/layout.ejs +2 -8
- package/server/views/logs.ejs +155 -0
- package/server/views/mini.ejs +0 -18
- package/server/views/net.ejs +2 -2
- package/server/views/network.ejs +1 -2
- package/server/views/network2.ejs +1 -2
- package/server/views/old_network.ejs +1 -2
- package/server/views/pro.ejs +26 -23
- package/server/views/prototype/index.ejs +30 -34
- package/server/views/screenshots.ejs +1 -2
- package/server/views/settings.ejs +1 -20
- package/server/views/shell.ejs +59 -66
- package/server/views/terminal.ejs +118 -73
- package/server/views/tools.ejs +1 -2
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const VOLATILE_CSI = new Set(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'P', 'S', 'T', 'X', '@', '`', 'd', 'e', 'f']);
|
|
2
|
+
|
|
3
|
+
class AnsiStreamTracker {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.state = 'text';
|
|
6
|
+
this.params = '';
|
|
7
|
+
this.awaitingPrintable = false;
|
|
8
|
+
this.stringAllowsBell = false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
markVolatile() {
|
|
12
|
+
this.awaitingPrintable = true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
push(chunk = '') {
|
|
16
|
+
if (!chunk || typeof chunk !== 'string') {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
let triggered = false;
|
|
20
|
+
let reason;
|
|
21
|
+
const setTriggered = (why) => {
|
|
22
|
+
if (!reason) {
|
|
23
|
+
reason = why;
|
|
24
|
+
}
|
|
25
|
+
triggered = true;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
29
|
+
const ch = chunk[i];
|
|
30
|
+
if (this.state === 'text') {
|
|
31
|
+
if (ch === '\u001b') {
|
|
32
|
+
this.state = 'esc';
|
|
33
|
+
} else if (ch === '\r' || ch === '\b') {
|
|
34
|
+
this.markVolatile();
|
|
35
|
+
} else if (ch === '\n') {
|
|
36
|
+
this.awaitingPrintable = false;
|
|
37
|
+
} else if (ch > ' ' && ch !== '\u007f') {
|
|
38
|
+
if (this.awaitingPrintable) {
|
|
39
|
+
this.awaitingPrintable = false;
|
|
40
|
+
setTriggered('line-rewrite');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (this.state === 'esc') {
|
|
47
|
+
if (ch === '[') {
|
|
48
|
+
this.state = 'csi';
|
|
49
|
+
this.params = '';
|
|
50
|
+
} else if (ch === ']') {
|
|
51
|
+
this.state = 'osc';
|
|
52
|
+
this.stringAllowsBell = true;
|
|
53
|
+
} else if ('PX^_'.includes(ch)) {
|
|
54
|
+
this.state = 'osc';
|
|
55
|
+
this.stringAllowsBell = false;
|
|
56
|
+
} else {
|
|
57
|
+
if ('78MDE'.includes(ch)) {
|
|
58
|
+
this.markVolatile();
|
|
59
|
+
}
|
|
60
|
+
this.state = 'text';
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (this.state === 'osc') {
|
|
66
|
+
if (this.stringAllowsBell && ch === '\u0007') {
|
|
67
|
+
this.state = 'text';
|
|
68
|
+
this.stringAllowsBell = false;
|
|
69
|
+
} else if (ch === '\u001b') {
|
|
70
|
+
this.state = 'osc_esc';
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (this.state === 'osc_esc') {
|
|
76
|
+
if (ch === '\\') {
|
|
77
|
+
this.state = 'text';
|
|
78
|
+
this.stringAllowsBell = false;
|
|
79
|
+
} else {
|
|
80
|
+
this.state = 'osc';
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (this.state === 'csi') {
|
|
86
|
+
if (ch === '\u001b') {
|
|
87
|
+
this.state = 'esc';
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (ch >= '@' && ch <= '~') {
|
|
91
|
+
const finalChar = ch;
|
|
92
|
+
const params = this.params;
|
|
93
|
+
if ((finalChar === 'h' || finalChar === 'l') && params.includes('2026')) {
|
|
94
|
+
setTriggered('decsync');
|
|
95
|
+
this.awaitingPrintable = false;
|
|
96
|
+
} else if (VOLATILE_CSI.has(finalChar)) {
|
|
97
|
+
this.markVolatile();
|
|
98
|
+
}
|
|
99
|
+
this.state = 'text';
|
|
100
|
+
this.params = '';
|
|
101
|
+
} else {
|
|
102
|
+
this.params += ch;
|
|
103
|
+
}
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (triggered) {
|
|
109
|
+
return { reason: reason || 'line-rewrite' };
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = AnsiStreamTracker;
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const { promisify } = require('util')
|
|
3
|
+
const { execFile } = require('child_process')
|
|
4
|
+
const HtmlModal = require('../htmlmodal')
|
|
5
|
+
const Util = require('../../util')
|
|
6
|
+
|
|
7
|
+
const DEFAULT_INSTALL_TIMEOUT = 5 * 60 * 1000
|
|
8
|
+
const DEFAULT_INSTALL_INTERVAL = 5000
|
|
9
|
+
const execFileAsync = promisify(execFile)
|
|
10
|
+
|
|
11
|
+
const escapeHtml = (value = '') => {
|
|
12
|
+
return String(value).replace(/[&<>"']/g, (char) => {
|
|
13
|
+
switch (char) {
|
|
14
|
+
case '&':
|
|
15
|
+
return '&'
|
|
16
|
+
case '<':
|
|
17
|
+
return '<'
|
|
18
|
+
case '>':
|
|
19
|
+
return '>'
|
|
20
|
+
case '"':
|
|
21
|
+
return '"'
|
|
22
|
+
case '\'':
|
|
23
|
+
return '''
|
|
24
|
+
default:
|
|
25
|
+
return char
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class AppAPI {
|
|
31
|
+
constructor() {
|
|
32
|
+
this.htmlModal = new HtmlModal()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
ensureService(kernel) {
|
|
36
|
+
if (!kernel.appLauncher) {
|
|
37
|
+
throw new Error('App launcher service is unavailable')
|
|
38
|
+
}
|
|
39
|
+
return kernel.appLauncher
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async launch(req, ondata, kernel) {
|
|
43
|
+
/*
|
|
44
|
+
{
|
|
45
|
+
"method": "app.launch",
|
|
46
|
+
"params": {
|
|
47
|
+
"id": <optional app id>,
|
|
48
|
+
"app": <name when id missing>,
|
|
49
|
+
"args": [<arg>, ...],
|
|
50
|
+
"refresh": <force reindex boolean>,
|
|
51
|
+
"install": <install url>,
|
|
52
|
+
"installTimeout": <ms to wait>,
|
|
53
|
+
"installPollInterval": <poll frequency ms>
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
*/
|
|
57
|
+
const params = req.params || {}
|
|
58
|
+
const launcher = this.ensureService(kernel)
|
|
59
|
+
try {
|
|
60
|
+
const result = await launcher.launch({
|
|
61
|
+
id: params.id,
|
|
62
|
+
app: params.app || params.name,
|
|
63
|
+
args: params.args,
|
|
64
|
+
refresh: params.refresh,
|
|
65
|
+
install: params.install
|
|
66
|
+
})
|
|
67
|
+
return result
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (params.install && error && error.code === 'APP_NOT_FOUND') {
|
|
70
|
+
return await this.handleInstallFlow({ req, ondata, kernel, launcher, params })
|
|
71
|
+
}
|
|
72
|
+
throw error
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async search(req, ondata, kernel) {
|
|
77
|
+
/*
|
|
78
|
+
{
|
|
79
|
+
"method": "app.search",
|
|
80
|
+
"params": {
|
|
81
|
+
"query": <text>,
|
|
82
|
+
"limit": <max results>,
|
|
83
|
+
"refresh": <force reindex boolean>
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
*/
|
|
87
|
+
const params = req.params || {}
|
|
88
|
+
const launcher = this.ensureService(kernel)
|
|
89
|
+
return launcher.search({
|
|
90
|
+
query: params.query || params.app || params.name || '',
|
|
91
|
+
limit: params.limit || 25,
|
|
92
|
+
refresh: params.refresh
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async info(req, ondata, kernel) {
|
|
97
|
+
/*
|
|
98
|
+
{
|
|
99
|
+
"method": "app.info",
|
|
100
|
+
"params": {
|
|
101
|
+
"id": <required app id>,
|
|
102
|
+
"refresh": <force reindex boolean>
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
*/
|
|
106
|
+
const params = req.params || {}
|
|
107
|
+
if (!params.id) {
|
|
108
|
+
throw new Error('app.info requires params.id')
|
|
109
|
+
}
|
|
110
|
+
const launcher = this.ensureService(kernel)
|
|
111
|
+
return launcher.info({ id: params.id, refresh: params.refresh })
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async refresh(req, ondata, kernel) {
|
|
115
|
+
/*
|
|
116
|
+
{
|
|
117
|
+
"method": "app.refresh",
|
|
118
|
+
"params": {
|
|
119
|
+
"force": <optional flag propagated to adapter>
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
*/
|
|
123
|
+
const launcher = this.ensureService(kernel)
|
|
124
|
+
return launcher.refresh(req && req.params ? req.params : {})
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
AppAPI.prototype.sleep = function sleep(ms) {
|
|
129
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
AppAPI.prototype.modalRequest = function modalRequest(req, modalId, params = {}) {
|
|
133
|
+
return {
|
|
134
|
+
params: Object.assign({ id: modalId }, params),
|
|
135
|
+
parent: req.parent,
|
|
136
|
+
cwd: req.cwd
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
AppAPI.prototype.buildInstallActions = function buildInstallActions(appName, installUrl, extraActions = []) {
|
|
141
|
+
const actions = []
|
|
142
|
+
if (installUrl) {
|
|
143
|
+
actions.push({
|
|
144
|
+
id: 'install-link',
|
|
145
|
+
label: `Install ${appName}`,
|
|
146
|
+
type: 'link',
|
|
147
|
+
href: installUrl,
|
|
148
|
+
variant: 'link',
|
|
149
|
+
icon: 'fa-solid fa-arrow-up-right-from-square'
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
return actions.concat(extraActions || [])
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
AppAPI.prototype.buildReadyActions = function buildReadyActions({ appName, installUrl, entry, needsManualLaunch }) {
|
|
156
|
+
const actions = this.buildInstallActions(appName, installUrl)
|
|
157
|
+
if (needsManualLaunch) {
|
|
158
|
+
actions.push({
|
|
159
|
+
id: 'reveal',
|
|
160
|
+
label: 'Open in Finder',
|
|
161
|
+
type: 'submit',
|
|
162
|
+
variant: 'secondary',
|
|
163
|
+
close: false
|
|
164
|
+
})
|
|
165
|
+
actions.push({
|
|
166
|
+
id: 'confirm-launch',
|
|
167
|
+
label: `I've opened ${entry.name || appName}`,
|
|
168
|
+
type: 'submit',
|
|
169
|
+
primary: true,
|
|
170
|
+
close: false
|
|
171
|
+
})
|
|
172
|
+
actions.push({
|
|
173
|
+
id: 'cancel',
|
|
174
|
+
label: 'Cancel',
|
|
175
|
+
type: 'submit',
|
|
176
|
+
variant: 'secondary'
|
|
177
|
+
})
|
|
178
|
+
} else {
|
|
179
|
+
actions.push({
|
|
180
|
+
id: 'launch',
|
|
181
|
+
label: `Open ${entry.name || appName}`,
|
|
182
|
+
type: 'submit',
|
|
183
|
+
primary: true
|
|
184
|
+
})
|
|
185
|
+
actions.push({
|
|
186
|
+
id: 'cancel',
|
|
187
|
+
label: 'Cancel',
|
|
188
|
+
type: 'submit',
|
|
189
|
+
variant: 'secondary'
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
return actions
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
AppAPI.prototype.installIntroHtml = function installIntroHtml(appName, installUrl) {
|
|
196
|
+
const safeName = escapeHtml(appName)
|
|
197
|
+
const safeUrl = escapeHtml(installUrl)
|
|
198
|
+
return `
|
|
199
|
+
<p>Pinokio could not find <strong>${safeName}</strong> on this system.</p>
|
|
200
|
+
<p>Click <em>Install ${safeName}</em> to open the official download page (${safeUrl}). Keep this window open; Pinokio will monitor for the installation automatically.</p>
|
|
201
|
+
`
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
AppAPI.prototype.installReadyHtml = function installReadyHtml(appName) {
|
|
205
|
+
const safeName = escapeHtml(appName)
|
|
206
|
+
return `
|
|
207
|
+
<p><strong>${safeName}</strong> was detected successfully.</p>
|
|
208
|
+
<p>Click <em>Open ${safeName}</em> to launch the application now.</p>
|
|
209
|
+
`
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
AppAPI.prototype.manualLaunchHtml = function manualLaunchHtml(appName) {
|
|
213
|
+
const safeName = escapeHtml(appName)
|
|
214
|
+
return `
|
|
215
|
+
<p>macOS needs you to open <strong>${safeName}</strong> manually the first time.</p>
|
|
216
|
+
<ol>
|
|
217
|
+
<li>Click <em>Open in Finder</em> to reveal the app.</li>
|
|
218
|
+
<li>Double-click ${safeName} in Finder and choose <em>Open</em> when macOS asks for confirmation.</li>
|
|
219
|
+
<li>Return here and click <em>I've opened ${safeName}</em>.</li>
|
|
220
|
+
</ol>
|
|
221
|
+
`
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
AppAPI.prototype.handleInstallFlow = async function handleInstallFlow({ req, ondata, kernel, launcher, params }) {
|
|
225
|
+
const installUrl = params.install
|
|
226
|
+
if (!installUrl) {
|
|
227
|
+
throw new Error('Install URL is required when install flow is requested')
|
|
228
|
+
}
|
|
229
|
+
const appName = params.app || params.name || params.id || 'the application'
|
|
230
|
+
const modalId = `app-install:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`
|
|
231
|
+
|
|
232
|
+
const pollInterval = Number(params.installPollInterval) > 0 ? Number(params.installPollInterval) : DEFAULT_INSTALL_INTERVAL
|
|
233
|
+
const timeout = Number(params.installTimeout) > 0 ? Number(params.installTimeout) : DEFAULT_INSTALL_TIMEOUT
|
|
234
|
+
|
|
235
|
+
const actions = this.buildInstallActions(appName, installUrl)
|
|
236
|
+
|
|
237
|
+
await this.htmlModal.open(
|
|
238
|
+
this.modalRequest(req, modalId, {
|
|
239
|
+
title: `Install ${appName}`,
|
|
240
|
+
html: this.installIntroHtml(appName, installUrl),
|
|
241
|
+
status: { text: `Waiting for ${escapeHtml(appName)} to be installed...`, waiting: true },
|
|
242
|
+
actions,
|
|
243
|
+
dismissible: true
|
|
244
|
+
}),
|
|
245
|
+
ondata,
|
|
246
|
+
kernel
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
const entry = await this.waitForInstall({
|
|
250
|
+
req,
|
|
251
|
+
ondata,
|
|
252
|
+
kernel,
|
|
253
|
+
launcher,
|
|
254
|
+
appName,
|
|
255
|
+
modalId,
|
|
256
|
+
pollInterval,
|
|
257
|
+
timeout,
|
|
258
|
+
actions
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
if (!entry) {
|
|
262
|
+
await this.htmlModal.update(
|
|
263
|
+
this.modalRequest(req, modalId, {
|
|
264
|
+
status: { text: `Still cannot find ${escapeHtml(appName)}. Please complete the installation and try again.`, variant: 'error' },
|
|
265
|
+
actions: this.buildInstallActions(appName, installUrl, [{
|
|
266
|
+
id: 'close',
|
|
267
|
+
label: 'Close',
|
|
268
|
+
type: 'submit',
|
|
269
|
+
variant: 'secondary',
|
|
270
|
+
close: true
|
|
271
|
+
}]),
|
|
272
|
+
await: true
|
|
273
|
+
}),
|
|
274
|
+
ondata,
|
|
275
|
+
kernel
|
|
276
|
+
)
|
|
277
|
+
await this.htmlModal.close(this.modalRequest(req, modalId, {}), ondata, kernel)
|
|
278
|
+
throw new Error(`Timed out waiting for ${appName} to be installed`)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const needsManualLaunch = await this.requiresManualLaunch(entry, kernel)
|
|
282
|
+
const readyPayload = {
|
|
283
|
+
title: entry.name || appName,
|
|
284
|
+
html: needsManualLaunch ? this.manualLaunchHtml(entry.name || appName) : this.installReadyHtml(entry.name || appName),
|
|
285
|
+
status: needsManualLaunch
|
|
286
|
+
? { text: `Open ${escapeHtml(entry.name || appName)} once from Finder, then confirm below.` }
|
|
287
|
+
: { text: `${escapeHtml(entry.name || appName)} detected.`, variant: 'success' },
|
|
288
|
+
actions: this.buildReadyActions({ appName, installUrl, entry, needsManualLaunch }),
|
|
289
|
+
await: true,
|
|
290
|
+
dismissible: true
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let readyResponse
|
|
294
|
+
while (true) {
|
|
295
|
+
readyResponse = await this.htmlModal.update(
|
|
296
|
+
this.modalRequest(req, modalId, readyPayload),
|
|
297
|
+
ondata,
|
|
298
|
+
kernel
|
|
299
|
+
)
|
|
300
|
+
if (!readyResponse) {
|
|
301
|
+
continue
|
|
302
|
+
}
|
|
303
|
+
if (readyResponse.action === 'reveal') {
|
|
304
|
+
await this.openInExplorer(entry.path, kernel)
|
|
305
|
+
readyPayload.status = {
|
|
306
|
+
text: `Opened Finder at ${escapeHtml(entry.name || appName)}. After approving it, click "I've opened it".`,
|
|
307
|
+
}
|
|
308
|
+
continue
|
|
309
|
+
}
|
|
310
|
+
if (['cancel', 'close', 'dismissed'].includes(readyResponse.action)) {
|
|
311
|
+
await this.htmlModal.close(this.modalRequest(req, modalId, {}), ondata, kernel)
|
|
312
|
+
throw new Error('Launch cancelled by user')
|
|
313
|
+
}
|
|
314
|
+
if (readyResponse.action === 'launch' || readyResponse.action === 'confirm-launch') {
|
|
315
|
+
break
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const launchResult = await launcher.launch({
|
|
321
|
+
id: entry.id,
|
|
322
|
+
app: entry.name,
|
|
323
|
+
args: params.args
|
|
324
|
+
})
|
|
325
|
+
await this.htmlModal.update(
|
|
326
|
+
this.modalRequest(req, modalId, {
|
|
327
|
+
status: { text: `Opening ${escapeHtml(entry.name || appName)}...`, variant: 'success' },
|
|
328
|
+
actions: [],
|
|
329
|
+
dismissible: true
|
|
330
|
+
}),
|
|
331
|
+
ondata,
|
|
332
|
+
kernel
|
|
333
|
+
)
|
|
334
|
+
await this.htmlModal.close(this.modalRequest(req, modalId, {}), ondata, kernel)
|
|
335
|
+
return launchResult
|
|
336
|
+
} catch (err) {
|
|
337
|
+
await this.htmlModal.update(
|
|
338
|
+
this.modalRequest(req, modalId, {
|
|
339
|
+
status: { text: `Failed to launch ${escapeHtml(entry.name || appName)}: ${escapeHtml(err.message || 'Unknown error')}`, variant: 'error' },
|
|
340
|
+
actions: this.buildInstallActions(appName, installUrl, [{
|
|
341
|
+
id: 'close',
|
|
342
|
+
label: 'Close',
|
|
343
|
+
type: 'submit',
|
|
344
|
+
variant: 'secondary',
|
|
345
|
+
close: true
|
|
346
|
+
}]),
|
|
347
|
+
await: true
|
|
348
|
+
}),
|
|
349
|
+
ondata,
|
|
350
|
+
kernel
|
|
351
|
+
)
|
|
352
|
+
await this.htmlModal.close(this.modalRequest(req, modalId, {}), ondata, kernel)
|
|
353
|
+
throw err
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
AppAPI.prototype.waitForInstall = async function waitForInstall({ req, ondata, kernel, launcher, appName, modalId, pollInterval, timeout, actions }) {
|
|
358
|
+
const start = Date.now()
|
|
359
|
+
let attempt = 0
|
|
360
|
+
while ((Date.now() - start) < timeout) {
|
|
361
|
+
await this.sleep(pollInterval)
|
|
362
|
+
attempt += 1
|
|
363
|
+
try {
|
|
364
|
+
await launcher.refresh({ force: true })
|
|
365
|
+
} catch (_) {
|
|
366
|
+
}
|
|
367
|
+
const match = await launcher.findMatch(appName, { force: false })
|
|
368
|
+
if (match && match.entry) {
|
|
369
|
+
return match.entry
|
|
370
|
+
}
|
|
371
|
+
await this.htmlModal.update(
|
|
372
|
+
this.modalRequest(req, modalId, {
|
|
373
|
+
status: { text: `Waiting for ${escapeHtml(appName)} (check #${attempt})...`, waiting: true },
|
|
374
|
+
actions
|
|
375
|
+
}),
|
|
376
|
+
ondata,
|
|
377
|
+
kernel
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
return null
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
AppAPI.prototype.requiresManualLaunch = async function requiresManualLaunch(entry, kernel) {
|
|
384
|
+
if (!entry || kernel.platform !== 'darwin') {
|
|
385
|
+
return false
|
|
386
|
+
}
|
|
387
|
+
if (!entry.path) {
|
|
388
|
+
return false
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
await fs.promises.access(entry.path)
|
|
392
|
+
} catch (_) {
|
|
393
|
+
return false
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
await execFileAsync('xattr', ['-p', 'com.apple.quarantine', entry.path])
|
|
397
|
+
return true
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (error && typeof error.code !== 'undefined') {
|
|
400
|
+
if (error.code === 1) {
|
|
401
|
+
return false
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (error && error.stderr && /No such xattr/i.test(error.stderr)) {
|
|
405
|
+
return false
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return false
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
AppAPI.prototype.openInExplorer = async function openInExplorer(targetPath, kernel) {
|
|
412
|
+
if (!targetPath) {
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
Util.openfs(targetPath, { action: 'view' }, kernel)
|
|
417
|
+
} catch (error) {
|
|
418
|
+
console.warn('[app.launch] Failed to open file explorer:', error && error.message ? error.message : error)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
module.exports = AppAPI
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
class HtmlModalAPI {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.defaultIdPrefix = 'htmlmodal'
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
resolveParentPath(req) {
|
|
7
|
+
if (req && req.parent && req.parent.path) {
|
|
8
|
+
return req.parent.path
|
|
9
|
+
}
|
|
10
|
+
if (req && req.cwd) {
|
|
11
|
+
return req.cwd
|
|
12
|
+
}
|
|
13
|
+
if (req && req.params && req.params.id) {
|
|
14
|
+
return req.params.id
|
|
15
|
+
}
|
|
16
|
+
return this.defaultIdPrefix
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
buildPacket(req, action) {
|
|
20
|
+
const params = Object.assign({}, req.params || {})
|
|
21
|
+
if (!params.id) {
|
|
22
|
+
params.id = `${this.defaultIdPrefix}:${this.resolveParentPath(req)}`
|
|
23
|
+
}
|
|
24
|
+
return Object.assign({ action }, params)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async dispatch(req, ondata, kernel, action, options = {}) {
|
|
28
|
+
if (!req || typeof req !== 'object') {
|
|
29
|
+
req = { params: {} }
|
|
30
|
+
}
|
|
31
|
+
if (!req.params) {
|
|
32
|
+
req.params = {}
|
|
33
|
+
}
|
|
34
|
+
const packet = this.buildPacket(req, action)
|
|
35
|
+
if (options.forceAwait === false) {
|
|
36
|
+
packet.await = false
|
|
37
|
+
}
|
|
38
|
+
ondata(packet, 'htmlmodal')
|
|
39
|
+
if (packet.await) {
|
|
40
|
+
const waitKey = this.resolveParentPath(req)
|
|
41
|
+
const response = await kernel.api.wait(waitKey)
|
|
42
|
+
return response
|
|
43
|
+
}
|
|
44
|
+
return packet
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async open(req, ondata, kernel) {
|
|
48
|
+
/*
|
|
49
|
+
{
|
|
50
|
+
"method": "htmlmodal.open",
|
|
51
|
+
"params": {
|
|
52
|
+
"id": <optional modal id>,
|
|
53
|
+
"title": <string>,
|
|
54
|
+
"html": <html string>,
|
|
55
|
+
"statusText": <string>,
|
|
56
|
+
"actions": [ { ... } ],
|
|
57
|
+
"await": <wait for response>
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
*/
|
|
61
|
+
return this.dispatch(req, ondata, kernel, 'open')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async update(req, ondata, kernel) {
|
|
65
|
+
/*
|
|
66
|
+
{
|
|
67
|
+
"method": "htmlmodal.update",
|
|
68
|
+
"params": {
|
|
69
|
+
"id": <modal id>,
|
|
70
|
+
"html": <html string>,
|
|
71
|
+
"statusText": <string>,
|
|
72
|
+
"waiting": <bool>,
|
|
73
|
+
"actions": [ { ... } ],
|
|
74
|
+
"await": <wait for response>
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
*/
|
|
78
|
+
return this.dispatch(req, ondata, kernel, 'update')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async close(req, ondata, kernel) {
|
|
82
|
+
/*
|
|
83
|
+
{
|
|
84
|
+
"method": "htmlmodal.close",
|
|
85
|
+
"params": {
|
|
86
|
+
"id": <modal id>
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
*/
|
|
90
|
+
return this.dispatch(req, ondata, kernel, 'close', { forceAwait: false })
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = HtmlModalAPI
|