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.
Files changed (51) hide show
  1. package/kernel/api/index.js +111 -15
  2. package/kernel/api/script/index.js +10 -0
  3. package/kernel/autolaunch.js +111 -0
  4. package/kernel/environment.js +89 -1
  5. package/kernel/git.js +9 -19
  6. package/kernel/index.js +142 -43
  7. package/kernel/launch_requirements.js +1115 -0
  8. package/kernel/ready.js +231 -0
  9. package/kernel/script.js +16 -0
  10. package/kernel/shells.js +9 -1
  11. package/kernel/workspace_status.js +111 -45
  12. package/package.json +1 -1
  13. package/server/autolaunch.js +725 -0
  14. package/server/index.js +244 -411
  15. package/server/public/logs.js +15 -1
  16. package/server/public/style.css +99 -31
  17. package/server/views/app.ejs +263 -159
  18. package/server/views/autolaunch.ejs +363 -75
  19. package/server/views/index.ejs +550 -26
  20. package/server/views/logs.ejs +14 -12
  21. package/server/views/partials/app_autolaunch_dependency_events.ejs +99 -0
  22. package/server/views/partials/app_autolaunch_dependency_save.ejs +10 -0
  23. package/server/views/partials/app_autolaunch_dependency_styles.ejs +357 -0
  24. package/server/views/partials/app_autolaunch_modal_helpers.ejs +347 -0
  25. package/server/views/partials/autolaunch_dependency_helpers.ejs +204 -0
  26. package/server/views/partials/autolaunch_dependency_save.ejs +13 -0
  27. package/server/views/partials/autolaunch_dependency_styles.ejs +347 -0
  28. package/server/views/partials/home_action_modal.ejs +4 -1
  29. package/server/views/partials/launch_requirements_status_client.ejs +271 -0
  30. package/server/views/partials/launch_requirements_status_styles.ejs +171 -0
  31. package/server/views/partials/launch_settings_dependency_save_factory.ejs +45 -0
  32. package/server/views/partials/launch_settings_dependency_script_loader_factory.ejs +25 -0
  33. package/server/views/terminal.ejs +196 -2
  34. package/test/home-autolaunch-live-ui.test.js +455 -0
  35. package/test/launch-requirements-browser.test.js +579 -0
  36. package/test/launch-requirements-contract-coverage.test.js +627 -0
  37. package/test/launch-requirements-real-browser.js +625 -0
  38. package/test/launch-requirements-status-client.test.js +132 -0
  39. package/test/launch-requirements.test.js +1806 -0
  40. package/test/launch-settings-ui.test.js +370 -0
  41. package/test/ready-state.test.js +49 -0
  42. package/test/server-autolaunch.test.js +1052 -0
  43. package/test/startup-git-index-benchmark.js +409 -0
  44. package/test/startup-git-index-browser.js +320 -0
  45. package/test/startup-git-index-performance.test.js +380 -0
  46. package/test/startup-git-index-refactor.test.js +450 -0
  47. package/test/startup-git-index-route.test.js +588 -0
  48. package/test/universal-launcher.smoke.spec.js +10 -9
  49. package/test/workspace-gitignore-benchmark.js +815 -0
  50. package/test/workspace-gitignore-path-scoped.test.js +256 -0
  51. package/spec/INSTRUCTION_SYNC.md +0 -432
@@ -0,0 +1,347 @@
1
+ const dependencyOptions = () => {
2
+ return candidateState && Array.isArray(candidateState.dependency_apps)
3
+ ? candidateState.dependency_apps
4
+ : []
5
+ }
6
+ const dependencyById = (dependencyId) => {
7
+ return dependencyOptions().find((candidate) => candidate && candidate.id === dependencyId) || null
8
+ }
9
+ const dependencyFolderPath = (app) => {
10
+ const raw = String((app && (app.workspace_path || app.launcher_path || app.id)) || "").replace(/\\/g, "/")
11
+ if (!raw) return ""
12
+ const pinokioApiMatch = raw.match(/\/pinokio\/api\/(.+)$/i)
13
+ if (pinokioApiMatch && pinokioApiMatch[1]) {
14
+ return `~/pinokio/api/${pinokioApiMatch[1]}`
15
+ }
16
+ return raw.replace(/^\/Users\/[^/]+/, "~")
17
+ }
18
+ const dependencyFullPath = (app) => String((app && (app.workspace_path || app.launcher_path || app.id)) || "")
19
+ const renderDependencyOption = (candidate) => {
20
+ const title = candidate.title || candidate.name || candidate.id
21
+ const folderPath = dependencyFolderPath(candidate)
22
+ const fullFolderPath = dependencyFullPath(candidate)
23
+ const searchText = `${title} ${candidate.id || ""} ${folderPath} ${fullFolderPath}`.toLowerCase()
24
+ return `
25
+ <button type="button" class="app-autolaunch-dependency-option" data-app-autolaunch-add-dependency="${escapeHtml(candidate.id)}" data-search-text="${escapeHtml(searchText)}" title="${escapeHtml(fullFolderPath)}">
26
+ <img src="${escapeHtml(candidate.icon || "/pinokio-black.png")}" alt="" onerror="this.onerror=null; this.src='/pinokio-black.png'">
27
+ <span class="app-autolaunch-dependency-option-meta">
28
+ <span class="app-autolaunch-dependency-option-title">${escapeHtml(title)}</span>
29
+ <span class="app-autolaunch-dependency-option-path">${escapeHtml(folderPath)}</span>
30
+ </span>
31
+ </button>
32
+ `
33
+ }
34
+ const renderSelectedDependency = (dependencyId) => {
35
+ const candidate = dependencyById(dependencyId)
36
+ const title = candidate ? (candidate.title || candidate.name || candidate.id) : dependencyId
37
+ const folderPath = candidate ? dependencyFolderPath(candidate) : dependencyId
38
+ const fullFolderPath = candidate ? dependencyFullPath(candidate) : dependencyId
39
+ const script = candidate && candidate.autolaunch ? candidate.autolaunch : ""
40
+ const scriptLine = script
41
+ ? `<span class="app-autolaunch-dependency-option-path">${escapeHtml(script)}</span>`
42
+ : `<button type="button" class="app-autolaunch-dependency-configure" data-app-autolaunch-configure-dependency="${escapeHtml(dependencyId)}">
43
+ <i class="fa-solid fa-gear" aria-hidden="true"></i>
44
+ Missing launch script
45
+ </button>`
46
+ return `
47
+ <div class="app-autolaunch-dependency-row" title="${escapeHtml(fullFolderPath)}">
48
+ <img src="${escapeHtml((candidate && candidate.icon) || "/pinokio-black.png")}" alt="" onerror="this.onerror=null; this.src='/pinokio-black.png'">
49
+ <span class="app-autolaunch-dependency-option-meta">
50
+ <span class="app-autolaunch-dependency-option-title">${escapeHtml(title)}</span>
51
+ <span class="app-autolaunch-dependency-option-path">${escapeHtml(folderPath)}</span>
52
+ ${scriptLine}
53
+ </span>
54
+ <button type="button" class="app-autolaunch-dependency-remove" data-app-autolaunch-remove-dependency="${escapeHtml(dependencyId)}" aria-label="Remove ${escapeHtml(title)}">
55
+ <i class="fa-solid fa-xmark" aria-hidden="true"></i>
56
+ </button>
57
+ </div>
58
+ `
59
+ }
60
+ const renderDependencyScriptModal = () => {
61
+ const dependencyId = dependencyScriptPickerOpen
62
+ if (!dependencyId) return ""
63
+ const pickerState = dependencyScriptCandidates[dependencyId]
64
+ const dependency = dependencyById(dependencyId)
65
+ const title = dependency ? (dependency.title || dependency.name || dependency.id) : dependencyId
66
+ const folderPath = dependency ? dependencyFolderPath(dependency) : dependencyId
67
+ const fullFolderPath = dependency ? dependencyFullPath(dependency) : dependencyId
68
+ const icon = (dependency && dependency.icon) || "/pinokio-black.png"
69
+ const selectedDependencyScript = dependencyScriptSelections[dependencyId] || (dependency && dependency.autolaunch ? dependency.autolaunch : "")
70
+ let body = ""
71
+ if (!pickerState || pickerState.loading) {
72
+ body = '<div class="app-autolaunch-empty">Loading script choices...</div>'
73
+ } else if (pickerState.error) {
74
+ body = `<div class="app-autolaunch-empty">${escapeHtml(pickerState.error)}</div>`
75
+ } else {
76
+ const candidates = [].concat(pickerState.menu || [], pickerState.other || [])
77
+ if (candidates.length === 0) {
78
+ body = '<div class="app-autolaunch-empty">No local scripts were found for this app.</div>'
79
+ } else {
80
+ body = `
81
+ <div class="app-autolaunch-script-list">
82
+ ${candidates.map((candidate) => {
83
+ const script = candidate && candidate.script ? candidate.script : ""
84
+ if (!script) return ""
85
+ const checked = script === selectedDependencyScript ? "checked" : ""
86
+ const selected = script === selectedDependencyScript ? " selected" : ""
87
+ return `
88
+ <label class="app-autolaunch-script-option${selected}">
89
+ <input type="radio" name="app-autolaunch-dependency-script-${escapeHtml(dependencyId)}" data-app-autolaunch-dependency-script-choice data-app-autolaunch-dependency-script-app="${escapeHtml(dependencyId)}" value="${escapeHtml(script)}" ${checked}>
90
+ <div>
91
+ <div class="app-autolaunch-script-title"><span>${escapeHtml(candidate.label || script)}</span></div>
92
+ <div class="app-autolaunch-script-path">${escapeHtml(script)}</div>
93
+ </div>
94
+ </label>
95
+ `
96
+ }).join("")}
97
+ </div>
98
+ `
99
+ }
100
+ }
101
+ return `
102
+ <div class="app-autolaunch-dependency-script-modal" role="dialog" aria-modal="true" aria-label="Select launch script for ${escapeHtml(title)}">
103
+ <div class="app-autolaunch-dependency-script-panel">
104
+ <div class="app-autolaunch-dependency-script-head">
105
+ <div>
106
+ <div class="app-autolaunch-title">Select launch script</div>
107
+ <div class="app-autolaunch-note">This app must have a launch script before it can be required.</div>
108
+ </div>
109
+ <button type="button" class="app-autolaunch-close" data-app-autolaunch-close-dependency-script-modal aria-label="Close script selection">
110
+ <i class="fa-solid fa-xmark" aria-hidden="true"></i>
111
+ </button>
112
+ </div>
113
+ <div class="app-autolaunch-dependency-script-app" title="${escapeHtml(fullFolderPath)}">
114
+ <img src="${escapeHtml(icon)}" alt="" onerror="this.onerror=null; this.src='/pinokio-black.png'">
115
+ <div class="app-autolaunch-dependency-option-meta">
116
+ <div class="app-autolaunch-dependency-option-title">${escapeHtml(title)}</div>
117
+ <div class="app-autolaunch-dependency-option-path">${escapeHtml(folderPath)}</div>
118
+ </div>
119
+ </div>
120
+ <div class="app-autolaunch-dependency-script-body">
121
+ ${body}
122
+ </div>
123
+ <div class="app-autolaunch-dependency-script-actions">
124
+ <button type="button" class="app-autolaunch-primary" data-app-autolaunch-confirm-dependency-script data-app-autolaunch-dependency-script-app="${escapeHtml(dependencyId)}" ${selectedDependencyScript ? "" : "disabled"}>
125
+ <i class="fa-solid fa-plus" aria-hidden="true"></i>
126
+ <span class="app-autolaunch-primary-text">Add requirement</span>
127
+ ${selectedDependencyScript ? `<span class="app-autolaunch-primary-script">${escapeHtml(selectedDependencyScript)}</span>` : ""}
128
+ </button>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ `
133
+ }
134
+ const configuredScript = (data) => data && data.current ? data.current : ""
135
+ const setFeedback = (message, isError = false) => {
136
+ feedbackEl.textContent = message || ""
137
+ feedbackEl.classList.toggle("error", !!isError)
138
+ }
139
+ const setBusy = (busy) => {
140
+ saving = !!busy
141
+ const blocked = saving || savingDependencies || savingDependencyScript || loadingCandidates
142
+ const setControlBusy = (control, disabled) => {
143
+ if (!control) return
144
+ const attr = "data-app-autolaunch-disabled-before-busy"
145
+ if (disabled) {
146
+ if (!control.hasAttribute(attr)) {
147
+ control.setAttribute(attr, control.disabled ? "true" : "false")
148
+ }
149
+ control.disabled = true
150
+ return
151
+ }
152
+ const previous = control.getAttribute(attr)
153
+ if (previous !== null) {
154
+ control.disabled = previous === "true"
155
+ control.removeAttribute(attr)
156
+ }
157
+ }
158
+ setControlBusy(switchButton, blocked)
159
+ button.toggleAttribute("aria-busy", saving || savingDependencies || savingDependencyScript)
160
+ scriptsEl.querySelectorAll("input").forEach((input) => {
161
+ setControlBusy(input, saving || savingDependencyScript)
162
+ })
163
+ scriptsEl.querySelectorAll("button").forEach((control) => {
164
+ setControlBusy(control, saving || savingDependencyScript)
165
+ })
166
+ dependenciesEl.querySelectorAll("button, input").forEach((control) => {
167
+ setControlBusy(control, saving || savingDependencies || savingDependencyScript)
168
+ })
169
+ }
170
+ const renderStatus = () => {
171
+ const enabled = !!(state && state.autolaunch_enabled)
172
+ status.textContent = enabled ? "ON" : "OFF"
173
+ button.dataset.enabled = enabled ? "true" : "false"
174
+ button.setAttribute("aria-label", `Autolaunch. Start with Pinokio ${enabled ? "On" : "Off"}`)
175
+ switchButton.setAttribute("aria-checked", enabled ? "true" : "false")
176
+ switchButton.title = `Start with Pinokio is ${enabled ? "on" : "off"}`
177
+ switchLabel.textContent = enabled ? "ON" : "OFF"
178
+ root.classList.toggle("enabled", enabled)
179
+ }
180
+ const setOpen = (open) => {
181
+ root.classList.toggle("open", !!open)
182
+ button.setAttribute("aria-expanded", open ? "true" : "false")
183
+ modal.classList.toggle("hidden", !open)
184
+ if (open) {
185
+ if (!candidateState && !loadingCandidates) {
186
+ loadCandidates()
187
+ }
188
+ window.setTimeout(() => {
189
+ if (!modal.classList.contains("hidden")) {
190
+ switchButton.focus()
191
+ }
192
+ }, 0)
193
+ } else {
194
+ dependencyPickerOpen = false
195
+ pendingDependencyId = ""
196
+ dependencyScriptPickerOpen = ""
197
+ renderDependencies()
198
+ button.focus()
199
+ }
200
+ }
201
+ const renderScripts = () => {
202
+ if (loadingCandidates) {
203
+ scriptsEl.innerHTML = '<div class="app-autolaunch-empty">Loading scripts...</div>'
204
+ setBusy(false)
205
+ return
206
+ }
207
+ if (candidateError) {
208
+ scriptsEl.innerHTML = `<div class="app-autolaunch-empty">${escapeHtml(candidateError)}</div>`
209
+ setBusy(false)
210
+ return
211
+ }
212
+ if (!candidateState) {
213
+ scriptsEl.innerHTML = '<div class="app-autolaunch-empty">Open to load scripts.</div>'
214
+ setBusy(false)
215
+ return
216
+ }
217
+ const candidates = allCandidates().slice()
218
+ const current = candidateState.current || state.autolaunch || ""
219
+ if (current && !candidates.some((candidate) => candidate && candidate.script === current)) {
220
+ candidates.unshift({
221
+ script: current,
222
+ label: "Current",
223
+ source: "current",
224
+ current: true
225
+ })
226
+ }
227
+ if (candidates.length === 0) {
228
+ scriptsEl.innerHTML = '<div class="app-autolaunch-empty">No local scripts were found for this app.</div>'
229
+ setBusy(false)
230
+ return
231
+ }
232
+ const dependencies = Array.isArray(state.autolaunch_depends) ? state.autolaunch_depends : []
233
+ const hasRequirements = dependencies.length > 0
234
+ const requirementWarning = hasRequirements && !current
235
+ ? '<div class="app-autolaunch-feedback error">Choose this app\'s launch script before these requirements can run.</div>'
236
+ : ""
237
+ const clear = current
238
+ ? `<button type="button" class="app-autolaunch-clear-script" data-app-autolaunch-clear-script title="${hasRequirements ? "Remove requirements before clearing this app's launch script." : "Clear launch script"}" ${hasRequirements ? "disabled" : ""}>
239
+ <i class="fa-solid fa-xmark" aria-hidden="true"></i>
240
+ <span>Clear launch script</span>
241
+ </button>`
242
+ : ""
243
+ scriptsEl.innerHTML = requirementWarning + clear + candidates.map((candidate) => {
244
+ const script = candidate && candidate.script ? candidate.script : ""
245
+ if (!script) return ""
246
+ const label = candidate.label || script
247
+ const checked = script === selectedScript ? "checked" : ""
248
+ const selected = script === selectedScript ? " selected" : ""
249
+ const tags = [
250
+ candidate.current ? '<span class="app-autolaunch-tag">Current</span>' : "",
251
+ candidate.menu_default ? '<span class="app-autolaunch-tag">Default</span>' : "",
252
+ candidate.has_params ? '<span class="app-autolaunch-tag">Params ignored</span>' : ""
253
+ ].filter(Boolean).join("")
254
+ const pathLine = label === script
255
+ ? ""
256
+ : `<div class="app-autolaunch-script-path" title="${escapeHtml(script)}">${escapeHtml(script)}</div>`
257
+ return `
258
+ <label class="app-autolaunch-script-option${selected}">
259
+ <input type="radio" name="app-autolaunch-script" value="${escapeHtml(script)}" ${checked}>
260
+ <div>
261
+ <div class="app-autolaunch-script-title"><span>${escapeHtml(label)}</span>${tags}</div>
262
+ ${pathLine}
263
+ </div>
264
+ </label>
265
+ `
266
+ }).join("")
267
+ setBusy(false)
268
+ }
269
+ const syncState = (updated) => {
270
+ if (updated) {
271
+ state = Object.assign({}, state, updated)
272
+ if (candidateState) {
273
+ candidateState.app = Object.assign({}, candidateState.app || {}, updated)
274
+ candidateState.current = updated.autolaunch || ""
275
+ }
276
+ selectedScript = updated.autolaunch || ""
277
+ }
278
+ renderStatus()
279
+ }
280
+ const renderDependencies = () => {
281
+ if (loadingCandidates) {
282
+ dependenciesEl.innerHTML = '<div class="app-autolaunch-empty">Loading dependencies...</div>'
283
+ return
284
+ }
285
+ if (!candidateState) {
286
+ dependenciesEl.innerHTML = '<div class="app-autolaunch-empty">Open to load dependencies.</div>'
287
+ return
288
+ }
289
+ const dependencies = Array.isArray(state.autolaunch_depends) ? state.autolaunch_depends : []
290
+ const dependencySet = new Set(dependencies)
291
+ const displayDependencies = dependencies.slice()
292
+ const selectedRows = displayDependencies.length > 0
293
+ ? displayDependencies.map(renderSelectedDependency).join("")
294
+ : '<div class="app-autolaunch-dependency-row-empty">No dependencies.</div>'
295
+ const availableDependencies = dependencyOptions()
296
+ .filter((candidate) => candidate && candidate.id && !dependencySet.has(candidate.id))
297
+ const picker = availableDependencies.length > 0
298
+ ? `
299
+ <div class="app-autolaunch-dependency-picker" data-app-autolaunch-dependency-picker>
300
+ <button type="button" class="app-autolaunch-dependency-trigger" data-app-autolaunch-toggle-dependency-picker aria-expanded="${dependencyPickerOpen ? "true" : "false"}">
301
+ <i class="fa-solid fa-plus" aria-hidden="true"></i>
302
+ <span>Add dependency...</span>
303
+ <i class="fa-solid fa-chevron-${dependencyPickerOpen ? "up" : "down"}" aria-hidden="true"></i>
304
+ </button>
305
+ ${dependencyPickerOpen ? `
306
+ <div class="app-autolaunch-dependency-menu">
307
+ <label class="app-autolaunch-dependency-filter">
308
+ <i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
309
+ <input type="search" placeholder="Search apps" data-app-autolaunch-dependency-search autocomplete="off">
310
+ </label>
311
+ <div class="app-autolaunch-dependency-options">
312
+ ${availableDependencies.map(renderDependencyOption).join("")}
313
+ <div class="app-autolaunch-dependency-picker-empty" data-app-autolaunch-dependency-empty hidden>No apps match that search.</div>
314
+ </div>
315
+ </div>
316
+ ` : ""}
317
+ </div>
318
+ `
319
+ : `
320
+ <button type="button" class="app-autolaunch-dependency-trigger" disabled>
321
+ <i class="fa-solid fa-check" aria-hidden="true"></i>
322
+ <span>No more apps to add</span>
323
+ </button>
324
+ `
325
+ dependenciesEl.innerHTML = `
326
+ <div class="app-autolaunch-dependency-stack">
327
+ <div class="app-autolaunch-dependency-selected">${selectedRows}</div>
328
+ ${picker}
329
+ </div>
330
+ ${renderDependencyScriptModal()}
331
+ `
332
+ setBusy(false)
333
+ }
334
+ const filterDependencyPicker = (picker) => {
335
+ if (!picker) return
336
+ const input = picker.querySelector("[data-app-autolaunch-dependency-search]")
337
+ const query = input ? input.value.trim().toLowerCase() : ""
338
+ let visibleCount = 0
339
+ picker.querySelectorAll("[data-app-autolaunch-add-dependency]").forEach((option) => {
340
+ const haystack = option.getAttribute("data-search-text") || ""
341
+ const visible = !query || haystack.includes(query)
342
+ option.hidden = !visible
343
+ if (visible) visibleCount += 1
344
+ })
345
+ const empty = picker.querySelector("[data-app-autolaunch-dependency-empty]")
346
+ if (empty) empty.hidden = visibleCount !== 0
347
+ }
@@ -0,0 +1,204 @@
1
+ const dependencyAppsFor = (app) => {
2
+ if (!app) return [];
3
+ return autolaunchApps
4
+ .filter((candidate) => candidate && candidate.id && candidate.id !== app.id)
5
+ .slice()
6
+ .sort(compareApps);
7
+ };
8
+
9
+ const dependencyById = (appId) => autolaunchApps.find((candidate) => candidate && candidate.id === appId) || null;
10
+
11
+ const dependencyFolderPath = (app) => formatAppFolderPath(app);
12
+
13
+ const dependencyFullPath = (app) => String((app && (app.workspace_path || app.launcher_path || app.id)) || "");
14
+
15
+ const renderDependencyOption = (candidate) => {
16
+ const title = candidate.title || candidate.name || candidate.id;
17
+ const folderPath = dependencyFolderPath(candidate);
18
+ const fullFolderPath = dependencyFullPath(candidate);
19
+ const searchText = `${title} ${candidate.id || ""} ${folderPath} ${fullFolderPath}`.toLowerCase();
20
+ return `
21
+ <button type="button" class="autolaunch-dependency-option" data-add-dependency="${escapeHtml(candidate.id)}" data-search-text="${escapeHtml(searchText)}" title="${escapeHtml(fullFolderPath)}" ${savingDependencies ? "disabled" : ""}>
22
+ <img src="${escapeHtml(candidate.icon || "/pinokio-black.png")}" alt="" onerror="this.onerror=null; this.src='/pinokio-black.png'">
23
+ <span class="autolaunch-dependency-option-meta">
24
+ <span class="autolaunch-dependency-option-title">${escapeHtml(title)}</span>
25
+ <span class="autolaunch-dependency-option-path">${escapeHtml(folderPath)}</span>
26
+ </span>
27
+ </button>
28
+ `;
29
+ };
30
+
31
+ const renderSelectedDependency = (appId) => {
32
+ const candidate = dependencyById(appId);
33
+ const title = candidate ? (candidate.title || candidate.name || candidate.id) : appId;
34
+ const folderPath = candidate ? dependencyFolderPath(candidate) : appId;
35
+ const fullFolderPath = candidate ? dependencyFullPath(candidate) : appId;
36
+ const script = candidate && candidate.autolaunch ? candidate.autolaunch : "";
37
+ const scriptLine = script
38
+ ? `<span class="autolaunch-dependency-option-path">${escapeHtml(script)}</span>`
39
+ : `<button type="button" class="autolaunch-dependency-configure" data-configure-dependency="${escapeHtml(appId)}" ${savingDependencies ? "disabled" : ""}>
40
+ <i class="fa-solid fa-gear" aria-hidden="true"></i>
41
+ Missing launch script
42
+ </button>`;
43
+ return `
44
+ <div class="autolaunch-dependency-row" title="${escapeHtml(fullFolderPath)}">
45
+ <img src="${escapeHtml((candidate && candidate.icon) || "/pinokio-black.png")}" alt="" onerror="this.onerror=null; this.src='/pinokio-black.png'">
46
+ <span class="autolaunch-dependency-option-meta">
47
+ <span class="autolaunch-dependency-option-title">${escapeHtml(title)}</span>
48
+ <span class="autolaunch-dependency-option-path">${escapeHtml(folderPath)}</span>
49
+ ${scriptLine}
50
+ </span>
51
+ <button type="button" class="autolaunch-dependency-remove" data-remove-dependency="${escapeHtml(appId)}" aria-label="Remove ${escapeHtml(title)}" ${savingDependencies ? "disabled" : ""}>
52
+ <i class="fa-solid fa-xmark" aria-hidden="true"></i>
53
+ </button>
54
+ </div>
55
+ `;
56
+ };
57
+
58
+ const renderDependencyScriptModal = () => {
59
+ const appId = dependencyScriptPickerOpen;
60
+ if (!appId) {
61
+ return "";
62
+ }
63
+ const state = dependencyScriptCandidates[appId];
64
+ const dependency = dependencyById(appId);
65
+ const title = dependency ? (dependency.title || dependency.name || dependency.id) : appId;
66
+ const folderPath = dependency ? dependencyFolderPath(dependency) : appId;
67
+ const fullFolderPath = dependency ? dependencyFullPath(dependency) : appId;
68
+ const appIcon = (dependency && dependency.icon) || "/pinokio-black.png";
69
+ const selectedDependencyScript = dependencyScriptSelections[appId] || (dependency && dependency.autolaunch ? dependency.autolaunch : "");
70
+ let body = "";
71
+ if (!state || state.loading) {
72
+ body = '<div class="autolaunch-loading"><i class="fa-solid fa-circle-notch fa-spin"></i> Loading script choices...</div>';
73
+ } else if (state.error) {
74
+ body = `<div class="autolaunch-empty">${escapeHtml(state.error)}</div>`;
75
+ } else {
76
+ const candidates = [].concat(state.menu || [], state.other || []);
77
+ if (candidates.length === 0) {
78
+ body = '<div class="autolaunch-empty">No local scripts were found for this app.</div>';
79
+ } else {
80
+ body = `
81
+ <div class="autolaunch-script-list">
82
+ ${candidates.map((candidate) => {
83
+ const checked = candidate.script === selectedDependencyScript ? "checked" : "";
84
+ const selected = candidate.script === selectedDependencyScript ? " selected" : "";
85
+ const icon = candidate.icon || "fa-regular fa-file-code";
86
+ return `
87
+ <label class="autolaunch-script-option${selected}">
88
+ <input type="radio" name="dependency-script-${escapeHtml(appId)}" data-dependency-script-choice data-dependency-script-app="${escapeHtml(appId)}" value="${escapeHtml(candidate.script)}" ${checked} ${savingDependencyScript ? "disabled" : ""}>
89
+ <i class="${escapeHtml(icon)}"></i>
90
+ <div>
91
+ <div class="autolaunch-script-title"><span>${escapeHtml(candidate.label || candidate.script)}</span></div>
92
+ <div class="autolaunch-script-path">${escapeHtml(candidate.script)}</div>
93
+ </div>
94
+ </label>
95
+ `;
96
+ }).join("")}
97
+ </div>
98
+ `;
99
+ }
100
+ }
101
+ return `
102
+ <div class="autolaunch-dependency-script-modal" role="dialog" aria-modal="true" aria-label="Select launch script for ${escapeHtml(title)}">
103
+ <div class="autolaunch-dependency-script-panel">
104
+ <div class="autolaunch-dependency-script-head">
105
+ <div>
106
+ <h3 class="autolaunch-section-title">Select launch script</h3>
107
+ <div class="autolaunch-section-note">This app must have a launch script before it can be required.</div>
108
+ </div>
109
+ <button type="button" class="autolaunch-icon-button" data-close-dependency-script-modal aria-label="Close script selection" ${savingDependencyScript ? "disabled" : ""}>
110
+ <i class="fa-solid fa-xmark" aria-hidden="true"></i>
111
+ </button>
112
+ </div>
113
+ <div class="autolaunch-dependency-script-app" title="${escapeHtml(fullFolderPath)}">
114
+ <img src="${escapeHtml(appIcon)}" alt="" onerror="this.onerror=null; this.src='/pinokio-black.png'">
115
+ <span class="autolaunch-dependency-option-meta">
116
+ <span class="autolaunch-dependency-option-title">${escapeHtml(title)}</span>
117
+ <span class="autolaunch-dependency-option-path">${escapeHtml(folderPath)}</span>
118
+ </span>
119
+ </div>
120
+ <div class="autolaunch-dependency-script-body">
121
+ ${body}
122
+ </div>
123
+ <div class="autolaunch-dependency-script-actions">
124
+ <button type="button" class="autolaunch-primary" data-confirm-dependency-script data-dependency-script-app="${escapeHtml(appId)}" ${selectedDependencyScript ? "" : "disabled"}>
125
+ <i class="fa-solid fa-plus" aria-hidden="true"></i>
126
+ <span class="autolaunch-primary-text">Add requirement</span>
127
+ ${selectedDependencyScript ? `<span class="autolaunch-primary-script">${escapeHtml(selectedDependencyScript)}</span>` : ""}
128
+ </button>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ `;
133
+ };
134
+
135
+ const renderDependencySection = (app) => {
136
+ if (!app) {
137
+ return "";
138
+ }
139
+ const dependencies = Array.isArray(app.autolaunch_depends) ? app.autolaunch_depends : [];
140
+ const dependencySet = new Set(dependencies);
141
+ const displayDependencies = dependencies.slice();
142
+ const dependencyApps = dependencyAppsFor(app);
143
+ const selectedRows = displayDependencies.length > 0
144
+ ? displayDependencies.map(renderSelectedDependency).join("")
145
+ : '<div class="autolaunch-dependency-row-empty">No dependencies.</div>';
146
+ const availableDependencies = dependencyApps.filter((candidate) => !dependencySet.has(candidate.id));
147
+ const picker = availableDependencies.length > 0
148
+ ? `
149
+ <div class="autolaunch-dependency-picker" data-dependency-picker>
150
+ <button type="button" class="autolaunch-dependency-trigger" data-toggle-dependency-picker aria-expanded="${dependencyPickerOpen ? "true" : "false"}" ${savingDependencies ? "disabled" : ""}>
151
+ <i class="fa-solid fa-plus" aria-hidden="true"></i>
152
+ <span>Add dependency...</span>
153
+ <i class="fa-solid fa-chevron-${dependencyPickerOpen ? "up" : "down"}" aria-hidden="true"></i>
154
+ </button>
155
+ ${dependencyPickerOpen ? `
156
+ <div class="autolaunch-dependency-menu">
157
+ <label class="autolaunch-dependency-filter">
158
+ <i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
159
+ <input type="search" placeholder="Search apps" data-dependency-search autocomplete="off" ${savingDependencies ? "disabled" : ""}>
160
+ </label>
161
+ <div class="autolaunch-dependency-options">
162
+ ${availableDependencies.map(renderDependencyOption).join("")}
163
+ <div class="autolaunch-dependency-picker-empty" data-dependency-empty hidden>No apps match that search.</div>
164
+ </div>
165
+ </div>
166
+ ` : ""}
167
+ </div>
168
+ `
169
+ : `
170
+ <button type="button" class="autolaunch-dependency-trigger" disabled>
171
+ <i class="fa-solid fa-check" aria-hidden="true"></i>
172
+ <span>No more apps to add</span>
173
+ </button>
174
+ `;
175
+ return `
176
+ <div class="autolaunch-section">
177
+ <div class="autolaunch-section-header">
178
+ <h3 class="autolaunch-section-title">Requires</h3>
179
+ </div>
180
+ <div class="autolaunch-dependency-list">
181
+ <div class="autolaunch-dependency-stack">
182
+ <div class="autolaunch-dependency-selected">${selectedRows}</div>
183
+ ${picker}
184
+ </div>
185
+ </div>
186
+ </div>
187
+ ${renderDependencyScriptModal()}
188
+ `;
189
+ };
190
+
191
+ const filterDependencyPicker = (picker) => {
192
+ if (!picker) return;
193
+ const input = picker.querySelector("[data-dependency-search]");
194
+ const query = input ? input.value.trim().toLowerCase() : "";
195
+ let visibleCount = 0;
196
+ picker.querySelectorAll("[data-add-dependency]").forEach((option) => {
197
+ const haystack = option.getAttribute("data-search-text") || "";
198
+ const visible = !query || haystack.includes(query);
199
+ option.hidden = !visible;
200
+ if (visible) visibleCount += 1;
201
+ });
202
+ const empty = picker.querySelector("[data-dependency-empty]");
203
+ if (empty) empty.hidden = visibleCount !== 0;
204
+ };
@@ -0,0 +1,13 @@
1
+ <%- include('launch_settings_dependency_save_factory') %>
2
+ const saveDependencies = createLaunchSettingsDependencySaver({
3
+ getAppId: () => {
4
+ const app = currentApp();
5
+ return app ? app.id : "";
6
+ },
7
+ isSaving: () => savingDependencies,
8
+ setSaving: (value) => { savingDependencies = value; },
9
+ setBusy: setDetailBusy,
10
+ setFeedback,
11
+ onSaved: syncAppState,
12
+ render: renderCandidates
13
+ });