pinokiod 7.3.14 → 7.4.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/api/index.js +111 -15
- package/kernel/api/script/index.js +10 -0
- package/kernel/autolaunch.js +111 -0
- package/kernel/environment.js +89 -1
- package/kernel/git.js +9 -19
- package/kernel/index.js +142 -43
- package/kernel/launch_requirements.js +1115 -0
- package/kernel/ready.js +231 -0
- package/kernel/script.js +16 -0
- package/kernel/shells.js +9 -1
- package/kernel/workspace_status.js +111 -45
- package/package.json +1 -1
- package/server/autolaunch.js +725 -0
- package/server/index.js +244 -411
- package/server/public/logs.js +15 -1
- package/server/public/style.css +99 -31
- package/server/views/app.ejs +263 -159
- package/server/views/autolaunch.ejs +363 -75
- package/server/views/index.ejs +550 -26
- package/server/views/logs.ejs +14 -12
- package/server/views/partials/app_autolaunch_dependency_events.ejs +99 -0
- package/server/views/partials/app_autolaunch_dependency_save.ejs +10 -0
- package/server/views/partials/app_autolaunch_dependency_styles.ejs +357 -0
- package/server/views/partials/app_autolaunch_modal_helpers.ejs +347 -0
- package/server/views/partials/autolaunch_dependency_helpers.ejs +204 -0
- package/server/views/partials/autolaunch_dependency_save.ejs +13 -0
- package/server/views/partials/autolaunch_dependency_styles.ejs +347 -0
- package/server/views/partials/home_action_modal.ejs +4 -1
- package/server/views/partials/launch_requirements_status_client.ejs +271 -0
- package/server/views/partials/launch_requirements_status_styles.ejs +171 -0
- package/server/views/partials/launch_settings_dependency_save_factory.ejs +45 -0
- package/server/views/partials/launch_settings_dependency_script_loader_factory.ejs +25 -0
- package/server/views/terminal.ejs +196 -2
- package/test/home-autolaunch-live-ui.test.js +455 -0
- package/test/launch-requirements-browser.test.js +579 -0
- package/test/launch-requirements-contract-coverage.test.js +627 -0
- package/test/launch-requirements-real-browser.js +625 -0
- package/test/launch-requirements-status-client.test.js +132 -0
- package/test/launch-requirements.test.js +1806 -0
- package/test/launch-settings-ui.test.js +370 -0
- package/test/ready-state.test.js +49 -0
- package/test/server-autolaunch.test.js +1052 -0
- package/test/startup-git-index-benchmark.js +409 -0
- package/test/startup-git-index-browser.js +320 -0
- package/test/startup-git-index-performance.test.js +380 -0
- package/test/startup-git-index-refactor.test.js +450 -0
- package/test/startup-git-index-route.test.js +588 -0
- package/test/universal-launcher.smoke.spec.js +10 -9
- package/test/workspace-gitignore-benchmark.js +815 -0
- package/test/workspace-gitignore-path-scoped.test.js +256 -0
- package/spec/INSTRUCTION_SYNC.md +0 -432
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const Environment = require('../kernel/environment')
|
|
4
|
+
const Util = require('../kernel/util')
|
|
5
|
+
|
|
6
|
+
const ex = fn => (req, res, next) => {
|
|
7
|
+
Promise.resolve(fn(req, res, next)).catch(next)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const cloneWithFunctionRefs = (value, seen = new WeakMap()) => {
|
|
11
|
+
if (value === null || typeof value !== "object") {
|
|
12
|
+
return value
|
|
13
|
+
}
|
|
14
|
+
if (seen.has(value)) {
|
|
15
|
+
return seen.get(value)
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
const clone = []
|
|
19
|
+
seen.set(value, clone)
|
|
20
|
+
for (const entry of value) {
|
|
21
|
+
clone.push(typeof entry === "function" ? entry : cloneWithFunctionRefs(entry, seen))
|
|
22
|
+
}
|
|
23
|
+
return clone
|
|
24
|
+
}
|
|
25
|
+
const clone = {}
|
|
26
|
+
seen.set(value, clone)
|
|
27
|
+
for (const key of Object.keys(value)) {
|
|
28
|
+
const entry = value[key]
|
|
29
|
+
clone[key] = typeof entry === "function" ? entry : cloneWithFunctionRefs(entry, seen)
|
|
30
|
+
}
|
|
31
|
+
return clone
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const safeStructuredClone = (value) => {
|
|
35
|
+
try {
|
|
36
|
+
return structuredClone(value)
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return cloneWithFunctionRefs(value)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class ServerAutolaunch {
|
|
43
|
+
constructor(server) {
|
|
44
|
+
this.server = server
|
|
45
|
+
this.kernel = server.kernel
|
|
46
|
+
}
|
|
47
|
+
normalizeAppId(value) {
|
|
48
|
+
if (typeof value !== "string") {
|
|
49
|
+
return ""
|
|
50
|
+
}
|
|
51
|
+
const id = value.trim()
|
|
52
|
+
if (!id || id === "." || id === ".." || id.includes("\0") || /[\\/]/.test(id)) {
|
|
53
|
+
return ""
|
|
54
|
+
}
|
|
55
|
+
return id
|
|
56
|
+
}
|
|
57
|
+
compareApps(a, b) {
|
|
58
|
+
const aTitle = a && (a.title || a.name || a.id) ? (a.title || a.name || a.id) : ""
|
|
59
|
+
const bTitle = b && (b.title || b.name || b.id) ? (b.title || b.name || b.id) : ""
|
|
60
|
+
return String(aTitle).localeCompare(String(bTitle), undefined, { sensitivity: "base", numeric: true }) ||
|
|
61
|
+
String((a && a.id) || "").localeCompare(String((b && b.id) || ""), undefined, { sensitivity: "base", numeric: true })
|
|
62
|
+
}
|
|
63
|
+
parseDependencyValue(value) {
|
|
64
|
+
return Environment.parseAutolaunchList(value)
|
|
65
|
+
.map((id) => this.normalizeAppId(id))
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
}
|
|
68
|
+
normalizeDependencies(ids, apps, currentAppId) {
|
|
69
|
+
const appList = Array.isArray(apps) ? apps : []
|
|
70
|
+
const installedIds = new Set()
|
|
71
|
+
for (const app of appList) {
|
|
72
|
+
if (app && app.id) {
|
|
73
|
+
installedIds.add(app.id)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const seen = new Set()
|
|
77
|
+
const normalized = []
|
|
78
|
+
for (const id of Array.isArray(ids) ? ids : []) {
|
|
79
|
+
const appId = this.normalizeAppId(id)
|
|
80
|
+
if (!appId || appId === currentAppId || seen.has(appId)) {
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
if (!installedIds.has(appId)) {
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
seen.add(appId)
|
|
87
|
+
normalized.push(appId)
|
|
88
|
+
}
|
|
89
|
+
return normalized
|
|
90
|
+
}
|
|
91
|
+
sortAppStates(states) {
|
|
92
|
+
return states.slice().sort((a, b) => {
|
|
93
|
+
const enabledDelta = Number(!!b.autolaunch_enabled) - Number(!!a.autolaunch_enabled)
|
|
94
|
+
if (enabledDelta !== 0) {
|
|
95
|
+
return enabledDelta
|
|
96
|
+
}
|
|
97
|
+
return this.compareApps(a, b)
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
normalizeScriptPath(value) {
|
|
101
|
+
if (typeof value !== "string") {
|
|
102
|
+
return ""
|
|
103
|
+
}
|
|
104
|
+
let script = value.trim().replace(/\\/g, "/")
|
|
105
|
+
if (!script || script.includes("\0")) {
|
|
106
|
+
return ""
|
|
107
|
+
}
|
|
108
|
+
script = script.split("#")[0].split("?")[0].trim()
|
|
109
|
+
if (!script || /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(script) || script.startsWith("//")) {
|
|
110
|
+
return ""
|
|
111
|
+
}
|
|
112
|
+
if (script.startsWith("/")) {
|
|
113
|
+
return ""
|
|
114
|
+
}
|
|
115
|
+
const normalized = path.posix.normalize(script).replace(/^\.\/+/, "")
|
|
116
|
+
if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../") || path.posix.isAbsolute(normalized)) {
|
|
117
|
+
return ""
|
|
118
|
+
}
|
|
119
|
+
return normalized
|
|
120
|
+
}
|
|
121
|
+
isScriptFilename(filename) {
|
|
122
|
+
const ext = path.extname(filename || "").toLowerCase()
|
|
123
|
+
if (![".js", ".json", ".mjs", ".cjs"].includes(ext)) {
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
const base = path.basename(filename || "").toLowerCase()
|
|
127
|
+
return !["package.json", "pinokio.js", "pinokio.json", "pinokio_meta.json"].includes(base)
|
|
128
|
+
}
|
|
129
|
+
stripLabel(value) {
|
|
130
|
+
if (typeof value !== "string") {
|
|
131
|
+
return ""
|
|
132
|
+
}
|
|
133
|
+
return value
|
|
134
|
+
.replace(/<[^>]*>/g, " ")
|
|
135
|
+
.replace(/\s+/g, " ")
|
|
136
|
+
.trim()
|
|
137
|
+
}
|
|
138
|
+
async getAppById(appId) {
|
|
139
|
+
const id = this.normalizeAppId(appId)
|
|
140
|
+
if (!id) {
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
const apps = await this.kernel.api.listApps()
|
|
144
|
+
return apps.find((app) => app && app.id === id) || null
|
|
145
|
+
}
|
|
146
|
+
async getEnvInfo(app) {
|
|
147
|
+
const appRoot = path.resolve(this.kernel.api.userdir, app.id)
|
|
148
|
+
const gotRoot = await Environment.get_root({ path: appRoot }, this.kernel)
|
|
149
|
+
const envRoot = gotRoot && gotRoot.root ? gotRoot.root : appRoot
|
|
150
|
+
const envPath = path.resolve(envRoot, "ENVIRONMENT")
|
|
151
|
+
const env = await Util.parse_env(envPath)
|
|
152
|
+
const value = Environment.getScriptAutolaunch(env)
|
|
153
|
+
const launch = value
|
|
154
|
+
const dependencies = Environment.getScriptRequirements(env)
|
|
155
|
+
const enabled = Environment.getScriptAutolaunchEnabled(env)
|
|
156
|
+
const exists = await this.server.exists(envPath)
|
|
157
|
+
return {
|
|
158
|
+
appRoot,
|
|
159
|
+
envRoot,
|
|
160
|
+
envPath,
|
|
161
|
+
envRelpath: gotRoot && gotRoot.relpath ? gotRoot.relpath : "",
|
|
162
|
+
exists,
|
|
163
|
+
value,
|
|
164
|
+
launch,
|
|
165
|
+
dependencies,
|
|
166
|
+
enabled
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async buildAppState(app) {
|
|
170
|
+
const envInfo = await this.getEnvInfo(app)
|
|
171
|
+
return {
|
|
172
|
+
id: app.id,
|
|
173
|
+
name: app.name,
|
|
174
|
+
title: app.title || app.name || app.id,
|
|
175
|
+
description: app.description || "",
|
|
176
|
+
icon: app.icon || "/pinokio-black.png",
|
|
177
|
+
workspace_path: app.workspace_path,
|
|
178
|
+
launcher_path: app.launcher_path,
|
|
179
|
+
launcher_root: app.launcher_root || "",
|
|
180
|
+
env_path: envInfo.envPath,
|
|
181
|
+
autolaunch: envInfo.launch,
|
|
182
|
+
autolaunch_startup: envInfo.enabled ? envInfo.launch : "",
|
|
183
|
+
autolaunch_depends: envInfo.dependencies,
|
|
184
|
+
autolaunch_enabled: envInfo.enabled
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async buildAppsStateRaw() {
|
|
188
|
+
const apps = await this.kernel.api.listApps()
|
|
189
|
+
const states = []
|
|
190
|
+
for (const app of apps) {
|
|
191
|
+
states.push(await this.buildAppState(app))
|
|
192
|
+
}
|
|
193
|
+
return states
|
|
194
|
+
}
|
|
195
|
+
async buildAppsState() {
|
|
196
|
+
const states = await this.buildAppsStateRaw()
|
|
197
|
+
return this.sortAppStates(states)
|
|
198
|
+
}
|
|
199
|
+
async buildHomeStartupDisplayGraph() {
|
|
200
|
+
if (this.kernel.launch_complete) {
|
|
201
|
+
return new Map()
|
|
202
|
+
}
|
|
203
|
+
const states = await this.buildAppsStateRaw()
|
|
204
|
+
const byId = new Map()
|
|
205
|
+
for (const state of states) {
|
|
206
|
+
if (state && state.id) {
|
|
207
|
+
byId.set(state.id, state)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const graph = new Map()
|
|
211
|
+
const visited = new Set()
|
|
212
|
+
const isCancelled = (appId) => {
|
|
213
|
+
return !!(this.kernel.launchRequirements &&
|
|
214
|
+
typeof this.kernel.launchRequirements.isCancelled === "function" &&
|
|
215
|
+
this.kernel.launchRequirements.isCancelled(appId))
|
|
216
|
+
}
|
|
217
|
+
const addApp = (appId, startupRoot = false) => {
|
|
218
|
+
const id = this.normalizeAppId(appId)
|
|
219
|
+
if (!id || visited.has(id) || isCancelled(id)) {
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
const state = byId.get(id)
|
|
223
|
+
if (!state || !state.autolaunch) {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
visited.add(id)
|
|
227
|
+
const dependencies = Array.isArray(state.autolaunch_depends)
|
|
228
|
+
? state.autolaunch_depends.map((dependencyId) => this.normalizeAppId(dependencyId)).filter(Boolean)
|
|
229
|
+
: []
|
|
230
|
+
graph.set(id, {
|
|
231
|
+
id,
|
|
232
|
+
title: state.title || state.name || id,
|
|
233
|
+
script: state.autolaunch,
|
|
234
|
+
dependencies,
|
|
235
|
+
waiting_for: dependencies,
|
|
236
|
+
startup_root: !!startupRoot
|
|
237
|
+
})
|
|
238
|
+
for (const dependencyId of dependencies) {
|
|
239
|
+
addApp(dependencyId, false)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
for (const state of states) {
|
|
243
|
+
if (state && state.autolaunch_enabled && state.autolaunch) {
|
|
244
|
+
addApp(state.id, true)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return graph
|
|
248
|
+
}
|
|
249
|
+
async buildDependencyOptions(appId, states = null) {
|
|
250
|
+
const appStates = Array.isArray(states) ? states : await this.buildAppsStateRaw()
|
|
251
|
+
return this.sortAppStates(appStates)
|
|
252
|
+
.filter((app) => app && app.id !== appId)
|
|
253
|
+
.map((app) => ({
|
|
254
|
+
id: app.id,
|
|
255
|
+
title: app.title || app.name || app.id,
|
|
256
|
+
icon: app.icon || "/pinokio-black.png",
|
|
257
|
+
workspace_path: app.workspace_path,
|
|
258
|
+
launcher_path: app.launcher_path,
|
|
259
|
+
autolaunch: app.autolaunch,
|
|
260
|
+
autolaunch_enabled: !!app.autolaunch_enabled
|
|
261
|
+
}))
|
|
262
|
+
}
|
|
263
|
+
async resolveScript(appRoot, script) {
|
|
264
|
+
const normalized = this.normalizeScriptPath(script)
|
|
265
|
+
if (!normalized || !this.isScriptFilename(normalized)) {
|
|
266
|
+
return null
|
|
267
|
+
}
|
|
268
|
+
const scriptPath = path.resolve(appRoot, normalized)
|
|
269
|
+
if (!this.server.is_subpath(appRoot, scriptPath)) {
|
|
270
|
+
return null
|
|
271
|
+
}
|
|
272
|
+
let stat
|
|
273
|
+
try {
|
|
274
|
+
stat = await fs.promises.stat(scriptPath)
|
|
275
|
+
} catch (_) {
|
|
276
|
+
return null
|
|
277
|
+
}
|
|
278
|
+
if (!stat || !stat.isFile()) {
|
|
279
|
+
return null
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
script: normalized,
|
|
283
|
+
path: scriptPath
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
flattenMenu(menu, trail = []) {
|
|
287
|
+
const items = []
|
|
288
|
+
if (!Array.isArray(menu)) {
|
|
289
|
+
return items
|
|
290
|
+
}
|
|
291
|
+
for (const menuitem of menu) {
|
|
292
|
+
if (!menuitem || typeof menuitem !== "object") {
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
295
|
+
const label = this.stripLabel(menuitem.text || menuitem.name || menuitem.html || "")
|
|
296
|
+
const nextTrail = label ? trail.concat(label) : trail
|
|
297
|
+
if (Array.isArray(menuitem.menu)) {
|
|
298
|
+
items.push(...this.flattenMenu(menuitem.menu, nextTrail))
|
|
299
|
+
} else {
|
|
300
|
+
items.push({ item: menuitem, group: trail.join(" / ") })
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return items
|
|
304
|
+
}
|
|
305
|
+
async addCandidate(candidates, seen, candidate, appRoot) {
|
|
306
|
+
const resolved = await this.resolveScript(appRoot, candidate.script)
|
|
307
|
+
if (!resolved || seen.has(resolved.script)) {
|
|
308
|
+
return null
|
|
309
|
+
}
|
|
310
|
+
seen.add(resolved.script)
|
|
311
|
+
const label = this.stripLabel(candidate.label || "") || resolved.script
|
|
312
|
+
const menuDefault = !!candidate.menu_default
|
|
313
|
+
const item = {
|
|
314
|
+
script: resolved.script,
|
|
315
|
+
label,
|
|
316
|
+
group: candidate.group || "",
|
|
317
|
+
icon: candidate.icon || "",
|
|
318
|
+
source: candidate.source || "local",
|
|
319
|
+
menu_default: menuDefault,
|
|
320
|
+
has_params: !!candidate.has_params
|
|
321
|
+
}
|
|
322
|
+
candidates.push(item)
|
|
323
|
+
return item
|
|
324
|
+
}
|
|
325
|
+
async collectScriptFiles(root, appRoot) {
|
|
326
|
+
const results = []
|
|
327
|
+
const ignoredDirs = new Set([
|
|
328
|
+
".git",
|
|
329
|
+
".venv",
|
|
330
|
+
"__pycache__",
|
|
331
|
+
"app",
|
|
332
|
+
"cache",
|
|
333
|
+
"data",
|
|
334
|
+
"env",
|
|
335
|
+
"logs",
|
|
336
|
+
"models",
|
|
337
|
+
"node_modules",
|
|
338
|
+
"output",
|
|
339
|
+
"outputs",
|
|
340
|
+
"venv"
|
|
341
|
+
])
|
|
342
|
+
const maxResults = 500
|
|
343
|
+
const walk = async (dir, depth) => {
|
|
344
|
+
if (depth > 4 || results.length >= maxResults) {
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
let entries
|
|
348
|
+
try {
|
|
349
|
+
entries = await fs.promises.readdir(dir, { withFileTypes: true })
|
|
350
|
+
} catch (_) {
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
for (const entry of entries) {
|
|
354
|
+
if (!entry || !entry.name || entry.name.includes("\0")) {
|
|
355
|
+
continue
|
|
356
|
+
}
|
|
357
|
+
const fullPath = path.resolve(dir, entry.name)
|
|
358
|
+
if (entry.isDirectory()) {
|
|
359
|
+
if (!ignoredDirs.has(entry.name)) {
|
|
360
|
+
await walk(fullPath, depth + 1)
|
|
361
|
+
}
|
|
362
|
+
continue
|
|
363
|
+
}
|
|
364
|
+
if (!entry.isFile() || !this.isScriptFilename(entry.name)) {
|
|
365
|
+
continue
|
|
366
|
+
}
|
|
367
|
+
if (!this.server.is_subpath(appRoot, fullPath)) {
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
const rel = path.relative(appRoot, fullPath).split(path.sep).join("/")
|
|
371
|
+
results.push(rel)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
await walk(root, 0)
|
|
375
|
+
results.sort((a, b) => a.localeCompare(b))
|
|
376
|
+
return results
|
|
377
|
+
}
|
|
378
|
+
async buildCandidates(app) {
|
|
379
|
+
const envInfo = await this.getEnvInfo(app)
|
|
380
|
+
const appRoot = envInfo.appRoot
|
|
381
|
+
const launcher = await this.kernel.api.launcher(app.id)
|
|
382
|
+
const launcherRoot = launcher && launcher.launcher_root
|
|
383
|
+
? path.resolve(appRoot, launcher.launcher_root)
|
|
384
|
+
: appRoot
|
|
385
|
+
const menuCandidates = []
|
|
386
|
+
const otherCandidates = []
|
|
387
|
+
const seen = new Set()
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
let config = await this.kernel.api.meta(app.id)
|
|
391
|
+
config = await this.server.processMenu(app.id, safeStructuredClone(config || {}))
|
|
392
|
+
const flat = this.flattenMenu(config && config.menu ? config.menu : [])
|
|
393
|
+
for (const entry of flat) {
|
|
394
|
+
const menuitem = entry.item
|
|
395
|
+
if (!menuitem || typeof menuitem.href !== "string") {
|
|
396
|
+
continue
|
|
397
|
+
}
|
|
398
|
+
let href = menuitem.href.trim()
|
|
399
|
+
const apiPrefix = `/api/${app.id}/`
|
|
400
|
+
if (href.startsWith(apiPrefix)) {
|
|
401
|
+
href = href.slice(apiPrefix.length)
|
|
402
|
+
} else if (href.startsWith("/")) {
|
|
403
|
+
continue
|
|
404
|
+
}
|
|
405
|
+
const localScript = this.normalizeScriptPath(href)
|
|
406
|
+
if (!localScript) {
|
|
407
|
+
continue
|
|
408
|
+
}
|
|
409
|
+
const scriptPath = path.resolve(launcherRoot, localScript)
|
|
410
|
+
if (!this.server.is_subpath(appRoot, scriptPath)) {
|
|
411
|
+
continue
|
|
412
|
+
}
|
|
413
|
+
const script = path.relative(appRoot, scriptPath).split(path.sep).join("/")
|
|
414
|
+
await this.addCandidate(menuCandidates, seen, {
|
|
415
|
+
script,
|
|
416
|
+
label: menuitem.text || menuitem.name || script,
|
|
417
|
+
group: entry.group,
|
|
418
|
+
icon: typeof menuitem.icon === "string" ? menuitem.icon : "",
|
|
419
|
+
source: "menu",
|
|
420
|
+
menu_default: !!menuitem.default,
|
|
421
|
+
has_params: !!(menuitem.params && typeof menuitem.params === "object")
|
|
422
|
+
}, appRoot)
|
|
423
|
+
}
|
|
424
|
+
} catch (error) {
|
|
425
|
+
console.warn("[autolaunch] failed to resolve menu candidates", app.id, error && error.message ? error.message : error)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (envInfo.launch) {
|
|
429
|
+
await this.addCandidate(menuCandidates, seen, {
|
|
430
|
+
script: envInfo.launch,
|
|
431
|
+
label: envInfo.launch,
|
|
432
|
+
source: "current"
|
|
433
|
+
}, appRoot)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const localScripts = await this.collectScriptFiles(launcherRoot, appRoot)
|
|
437
|
+
for (const script of localScripts) {
|
|
438
|
+
await this.addCandidate(otherCandidates, seen, {
|
|
439
|
+
script,
|
|
440
|
+
label: script,
|
|
441
|
+
source: "local"
|
|
442
|
+
}, appRoot)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
app: await this.buildAppState(app),
|
|
447
|
+
dependency_apps: await this.buildDependencyOptions(app.id),
|
|
448
|
+
launcher_root: path.relative(appRoot, launcherRoot).split(path.sep).join("/"),
|
|
449
|
+
menu: menuCandidates,
|
|
450
|
+
other: otherCandidates,
|
|
451
|
+
current: envInfo.launch
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async applyHomeStartingState(item, index, homeStartupDisplayGraph = null) {
|
|
455
|
+
let autolaunchInfo = null
|
|
456
|
+
let autolaunchStatus = null
|
|
457
|
+
let startupDisplayInfo = null
|
|
458
|
+
const appId = this.normalizeAppId(item && (item.uri || item.name))
|
|
459
|
+
if (!appId) {
|
|
460
|
+
return false
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
autolaunchStatus = this.kernel.autolaunch_status && this.kernel.autolaunch_status.apps
|
|
464
|
+
? this.kernel.autolaunch_status.apps[appId]
|
|
465
|
+
: null
|
|
466
|
+
if (!this.kernel.launch_complete || autolaunchStatus) {
|
|
467
|
+
autolaunchInfo = await this.getEnvInfo({ id: appId })
|
|
468
|
+
}
|
|
469
|
+
} catch (error) {
|
|
470
|
+
console.warn("[home] failed to read autolaunch state", appId, error && error.message ? error.message : error)
|
|
471
|
+
}
|
|
472
|
+
if (!autolaunchStatus && !this.kernel.launch_complete) {
|
|
473
|
+
const displayGraph = homeStartupDisplayGraph || await this.buildHomeStartupDisplayGraph()
|
|
474
|
+
startupDisplayInfo = displayGraph && typeof displayGraph.get === "function"
|
|
475
|
+
? displayGraph.get(appId)
|
|
476
|
+
: null
|
|
477
|
+
}
|
|
478
|
+
const ready = autolaunchStatus && autolaunchStatus.state === "ready"
|
|
479
|
+
const hasActiveStatus = autolaunchStatus && !ready
|
|
480
|
+
const hasConfiguredStartup = !this.kernel.launch_complete && autolaunchInfo && autolaunchInfo.enabled
|
|
481
|
+
const hasStartupDisplayInfo = !this.kernel.launch_complete && !!startupDisplayInfo
|
|
482
|
+
if (ready || (!hasActiveStatus && !hasConfiguredStartup && !hasStartupDisplayInfo)) {
|
|
483
|
+
return false
|
|
484
|
+
}
|
|
485
|
+
const launchScript = (autolaunchStatus && autolaunchStatus.script) ||
|
|
486
|
+
(startupDisplayInfo && startupDisplayInfo.script) ||
|
|
487
|
+
(autolaunchInfo ? autolaunchInfo.value || autolaunchInfo.launch : "")
|
|
488
|
+
const launchPath = autolaunchStatus && autolaunchStatus.launch_path
|
|
489
|
+
? autolaunchStatus.launch_path
|
|
490
|
+
: (launchScript && autolaunchInfo ? path.resolve(autolaunchInfo.appRoot, launchScript) : "")
|
|
491
|
+
const progress = autolaunchStatus && Number.isInteger(Number(autolaunchStatus.step_current)) && Number.isInteger(Number(autolaunchStatus.step_total))
|
|
492
|
+
? {
|
|
493
|
+
step_current: Number(autolaunchStatus.step_current),
|
|
494
|
+
step_total: Number(autolaunchStatus.step_total)
|
|
495
|
+
}
|
|
496
|
+
: (this.kernel && typeof this.kernel.getScriptProgress === "function" ? this.kernel.getScriptProgress(launchPath) : null)
|
|
497
|
+
const progressLabel = progress && progress.step_total > 1
|
|
498
|
+
? ` (${progress.step_current}/${progress.step_total})`
|
|
499
|
+
: ""
|
|
500
|
+
const statusWaitingFor = autolaunchStatus && Array.isArray(autolaunchStatus.waiting_for) ? autolaunchStatus.waiting_for : []
|
|
501
|
+
const configuredDependencies = startupDisplayInfo && Array.isArray(startupDisplayInfo.waiting_for)
|
|
502
|
+
? startupDisplayInfo.waiting_for
|
|
503
|
+
: (autolaunchInfo && Array.isArray(autolaunchInfo.dependencies) ? autolaunchInfo.dependencies : [])
|
|
504
|
+
const waitingFor = autolaunchStatus ? statusWaitingFor : configuredDependencies
|
|
505
|
+
const waitingLabels = []
|
|
506
|
+
for (const id of waitingFor) {
|
|
507
|
+
const status = this.kernel.autolaunch_status && this.kernel.autolaunch_status.apps
|
|
508
|
+
? this.kernel.autolaunch_status.apps[id]
|
|
509
|
+
: null
|
|
510
|
+
if (status && status.title) {
|
|
511
|
+
waitingLabels.push(status.title)
|
|
512
|
+
continue
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
const dependencyLauncher = await this.kernel.api.launcher(id)
|
|
516
|
+
waitingLabels.push(dependencyLauncher && dependencyLauncher.script && dependencyLauncher.script.title
|
|
517
|
+
? dependencyLauncher.script.title
|
|
518
|
+
: id)
|
|
519
|
+
} catch (_) {
|
|
520
|
+
waitingLabels.push(id)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const waitingLabel = waitingLabels.length > 0 ? `Waiting for ${waitingLabels.join(", ")}` : ""
|
|
524
|
+
item.running = true
|
|
525
|
+
item.autolaunch_starting = true
|
|
526
|
+
item.autolaunch_blocked = autolaunchStatus && autolaunchStatus.state === "blocked"
|
|
527
|
+
item.autolaunch_waiting = !item.autolaunch_blocked && waitingFor.length > 0
|
|
528
|
+
item.autolaunch_status_label = item.autolaunch_blocked
|
|
529
|
+
? (autolaunchStatus.blocked_reason || "Startup blocked")
|
|
530
|
+
: (item.autolaunch_waiting && waitingLabel ? waitingLabel : `Starting ${launchScript || "automatically"}${progressLabel}`)
|
|
531
|
+
item.autolaunch_script = launchScript
|
|
532
|
+
if (progress) {
|
|
533
|
+
item.autolaunch_step_current = progress.step_current
|
|
534
|
+
item.autolaunch_step_total = progress.step_total
|
|
535
|
+
}
|
|
536
|
+
item.index = index
|
|
537
|
+
return true
|
|
538
|
+
}
|
|
539
|
+
registerRoutes() {
|
|
540
|
+
this.server.app.get("/autolaunch/candidates", ex(async (req, res) => {
|
|
541
|
+
const app = await this.getAppById(req.query.app)
|
|
542
|
+
if (!app) {
|
|
543
|
+
res.status(404).json({ ok: false, error: "App not found." })
|
|
544
|
+
return
|
|
545
|
+
}
|
|
546
|
+
const state = await this.buildCandidates(app)
|
|
547
|
+
res.json({ ok: true, ...state })
|
|
548
|
+
}))
|
|
549
|
+
this.server.app.post("/autolaunch/dependencies", ex(async (req, res) => {
|
|
550
|
+
const app = await this.getAppById(req.body && req.body.app)
|
|
551
|
+
if (!app) {
|
|
552
|
+
res.status(404).json({ ok: false, error: "App not found." })
|
|
553
|
+
return
|
|
554
|
+
}
|
|
555
|
+
const requestedDependencies = req.body && Array.isArray(req.body.dependencies) ? req.body.dependencies : null
|
|
556
|
+
if (!requestedDependencies) {
|
|
557
|
+
res.status(400).json({ ok: false, error: "Dependencies must be an array of app IDs." })
|
|
558
|
+
return
|
|
559
|
+
}
|
|
560
|
+
const states = await this.buildAppsStateRaw()
|
|
561
|
+
const installedIds = new Set(states.map((state) => state && state.id).filter(Boolean))
|
|
562
|
+
const seen = new Set()
|
|
563
|
+
const normalized = []
|
|
564
|
+
for (const rawId of requestedDependencies) {
|
|
565
|
+
const dependencyId = this.normalizeAppId(rawId)
|
|
566
|
+
if (!dependencyId) {
|
|
567
|
+
res.status(400).json({ ok: false, error: "Requirement app id is invalid." })
|
|
568
|
+
return
|
|
569
|
+
}
|
|
570
|
+
if (dependencyId === app.id) {
|
|
571
|
+
res.status(400).json({ ok: false, error: "An app cannot require itself." })
|
|
572
|
+
return
|
|
573
|
+
}
|
|
574
|
+
if (seen.has(dependencyId)) {
|
|
575
|
+
res.status(400).json({ ok: false, error: "Requirement app ids must be unique." })
|
|
576
|
+
return
|
|
577
|
+
}
|
|
578
|
+
if (!installedIds.has(dependencyId)) {
|
|
579
|
+
res.status(400).json({ ok: false, error: `Required app is not installed: ${dependencyId}` })
|
|
580
|
+
return
|
|
581
|
+
}
|
|
582
|
+
seen.add(dependencyId)
|
|
583
|
+
normalized.push(dependencyId)
|
|
584
|
+
}
|
|
585
|
+
if (normalized.length > 0) {
|
|
586
|
+
const envInfo = await this.getEnvInfo(app)
|
|
587
|
+
if (!envInfo.launch) {
|
|
588
|
+
res.status(400).json({ ok: false, error: "Choose this app's launch script before adding requirements." })
|
|
589
|
+
return
|
|
590
|
+
}
|
|
591
|
+
const stateById = new Map(states.map((state) => [state.id, state]))
|
|
592
|
+
const missing = normalized
|
|
593
|
+
.map((id) => stateById.get(id))
|
|
594
|
+
.find((state) => !state || !state.autolaunch)
|
|
595
|
+
if (missing) {
|
|
596
|
+
res.status(400).json({
|
|
597
|
+
ok: false,
|
|
598
|
+
error: `Choose ${missing.title || missing.name || missing.id}'s launch script before adding it as a requirement.`
|
|
599
|
+
})
|
|
600
|
+
return
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const initialized = await Environment.init({ name: app.id }, this.kernel)
|
|
604
|
+
await Util.update_env(initialized.env_path, {
|
|
605
|
+
[Environment.SCRIPT_REQUIREMENTS_KEY]: Environment.formatAutolaunchList(normalized)
|
|
606
|
+
})
|
|
607
|
+
if (this.kernel && typeof this.kernel.clearLaunchRequirementsStatus === "function") {
|
|
608
|
+
this.kernel.clearLaunchRequirementsStatus(app.id)
|
|
609
|
+
}
|
|
610
|
+
res.json({
|
|
611
|
+
ok: true,
|
|
612
|
+
app: await this.buildAppState(app)
|
|
613
|
+
})
|
|
614
|
+
}))
|
|
615
|
+
this.server.app.post("/autolaunch", ex(async (req, res) => {
|
|
616
|
+
const app = await this.getAppById(req.body && req.body.app)
|
|
617
|
+
if (!app) {
|
|
618
|
+
res.status(404).json({ ok: false, error: "App not found." })
|
|
619
|
+
return
|
|
620
|
+
}
|
|
621
|
+
const requestedScript = typeof req.body.script === "string" ? req.body.script.trim() : ""
|
|
622
|
+
const clearScript = !!(req.body && req.body.clear_script === true)
|
|
623
|
+
const hasEnabled = req.body && typeof req.body.enabled === "boolean"
|
|
624
|
+
if (!clearScript && requestedScript && !hasEnabled) {
|
|
625
|
+
res.status(400).json({
|
|
626
|
+
ok: false,
|
|
627
|
+
error: "Startup enabled must be explicit when selecting a launch script."
|
|
628
|
+
})
|
|
629
|
+
return
|
|
630
|
+
}
|
|
631
|
+
const enabled = hasEnabled ? req.body.enabled : false
|
|
632
|
+
if (clearScript) {
|
|
633
|
+
const envInfo = await this.getEnvInfo(app)
|
|
634
|
+
if (envInfo.dependencies && envInfo.dependencies.length > 0) {
|
|
635
|
+
res.status(400).json({
|
|
636
|
+
ok: false,
|
|
637
|
+
error: "Remove requirements before clearing this app's launch script."
|
|
638
|
+
})
|
|
639
|
+
return
|
|
640
|
+
}
|
|
641
|
+
if (envInfo.exists) {
|
|
642
|
+
await Util.update_env(envInfo.envPath, {
|
|
643
|
+
[Environment.SCRIPT_AUTOLAUNCH_KEY]: "",
|
|
644
|
+
[Environment.SCRIPT_AUTOLAUNCH_ENABLED_KEY]: ""
|
|
645
|
+
})
|
|
646
|
+
}
|
|
647
|
+
if (this.kernel && typeof this.kernel.clearLaunchRequirementsStatus === "function") {
|
|
648
|
+
this.kernel.clearLaunchRequirementsStatus(app.id)
|
|
649
|
+
}
|
|
650
|
+
res.json({
|
|
651
|
+
ok: true,
|
|
652
|
+
app: await this.buildAppState(app)
|
|
653
|
+
})
|
|
654
|
+
return
|
|
655
|
+
}
|
|
656
|
+
if (!requestedScript) {
|
|
657
|
+
res.status(400).json({
|
|
658
|
+
ok: false,
|
|
659
|
+
error: "Use clear_script: true to clear this app's launch script."
|
|
660
|
+
})
|
|
661
|
+
return
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const envInfo = await this.getEnvInfo(app)
|
|
665
|
+
const resolved = await this.resolveScript(envInfo.appRoot, requestedScript)
|
|
666
|
+
if (!resolved) {
|
|
667
|
+
res.status(400).json({
|
|
668
|
+
ok: false,
|
|
669
|
+
error: "Select an existing local script inside the app."
|
|
670
|
+
})
|
|
671
|
+
return
|
|
672
|
+
}
|
|
673
|
+
const initialized = await Environment.init({ name: app.id }, this.kernel)
|
|
674
|
+
await Util.update_env(initialized.env_path, {
|
|
675
|
+
[Environment.SCRIPT_AUTOLAUNCH_KEY]: resolved.script,
|
|
676
|
+
[Environment.SCRIPT_AUTOLAUNCH_ENABLED_KEY]: enabled ? "true" : "false"
|
|
677
|
+
})
|
|
678
|
+
if (this.kernel && typeof this.kernel.clearLaunchRequirementsStatus === "function") {
|
|
679
|
+
this.kernel.clearLaunchRequirementsStatus(app.id)
|
|
680
|
+
}
|
|
681
|
+
res.json({
|
|
682
|
+
ok: true,
|
|
683
|
+
app: await this.buildAppState(app)
|
|
684
|
+
})
|
|
685
|
+
}))
|
|
686
|
+
this.server.app.post("/autolaunch/disable-all", ex(async (req, res) => {
|
|
687
|
+
const apps = await this.kernel.api.listApps()
|
|
688
|
+
let disabled = 0
|
|
689
|
+
for (const app of apps) {
|
|
690
|
+
const envInfo = await this.getEnvInfo(app)
|
|
691
|
+
if (envInfo.exists && envInfo.enabled) {
|
|
692
|
+
await Util.update_env(envInfo.envPath, {
|
|
693
|
+
[Environment.SCRIPT_AUTOLAUNCH_ENABLED_KEY]: "false"
|
|
694
|
+
})
|
|
695
|
+
disabled++
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
res.json({
|
|
699
|
+
ok: true,
|
|
700
|
+
disabled,
|
|
701
|
+
apps: await this.buildAppsState()
|
|
702
|
+
})
|
|
703
|
+
}))
|
|
704
|
+
this.server.app.get("/autolaunch", ex(async (req, res) => {
|
|
705
|
+
const peerAccess = await this.server.composePeerAccessPayload()
|
|
706
|
+
const list = this.server.getPeers()
|
|
707
|
+
const apps = await this.buildAppsState()
|
|
708
|
+
const appsJson = JSON.stringify(apps).replace(/</g, "\\u003c")
|
|
709
|
+
res.render("autolaunch", {
|
|
710
|
+
current_host: this.kernel.peer.host,
|
|
711
|
+
...peerAccess,
|
|
712
|
+
apps,
|
|
713
|
+
appsJson,
|
|
714
|
+
enabledCount: apps.filter((app) => app.autolaunch_enabled).length,
|
|
715
|
+
portal: this.server.portal,
|
|
716
|
+
logo: this.server.logo,
|
|
717
|
+
theme: this.server.theme,
|
|
718
|
+
agent: req.agent,
|
|
719
|
+
list,
|
|
720
|
+
})
|
|
721
|
+
}))
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
module.exports = ServerAutolaunch
|