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
@@ -140,6 +140,12 @@ body.autolaunch-page .autolaunch-app-row {
140
140
  cursor: pointer;
141
141
  transition: background 140ms ease, box-shadow 140ms ease;
142
142
  }
143
+ body.autolaunch-page .autolaunch-app-row:focus {
144
+ outline: none;
145
+ }
146
+ body.autolaunch-page .autolaunch-app-row:focus-visible {
147
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--task-accent) 48%, transparent);
148
+ }
143
149
  body.autolaunch-page .autolaunch-app-row:last-child {
144
150
  border-bottom: 0;
145
151
  }
@@ -282,6 +288,18 @@ body.autolaunch-page .autolaunch-switch-label {
282
288
  min-width: 18px;
283
289
  text-align: left;
284
290
  }
291
+ body.autolaunch-page .autolaunch-startup-control {
292
+ display: inline-flex;
293
+ align-items: center;
294
+ gap: 8px;
295
+ justify-self: end;
296
+ }
297
+ body.autolaunch-page .autolaunch-startup-label {
298
+ color: var(--task-muted);
299
+ font-size: 11px;
300
+ font-weight: 700;
301
+ white-space: nowrap;
302
+ }
285
303
  body.autolaunch-page .autolaunch-detail {
286
304
  min-height: 480px;
287
305
  }
@@ -382,6 +400,39 @@ body.autolaunch-page .autolaunch-script-title span:first-child {
382
400
  text-overflow: ellipsis;
383
401
  white-space: nowrap;
384
402
  }
403
+ body.autolaunch-page .autolaunch-current-row {
404
+ display: flex;
405
+ align-items: center;
406
+ gap: 10px;
407
+ }
408
+ body.autolaunch-page .autolaunch-current-row .autolaunch-current {
409
+ flex: 1;
410
+ min-width: 0;
411
+ }
412
+ body.autolaunch-page .autolaunch-clear-script {
413
+ flex: 0 0 auto;
414
+ min-height: 28px;
415
+ border: 1px solid var(--task-border);
416
+ border-radius: 7px;
417
+ background: transparent;
418
+ color: var(--task-muted);
419
+ display: inline-flex;
420
+ align-items: center;
421
+ gap: 6px;
422
+ padding: 0 9px;
423
+ font: inherit;
424
+ font-size: 12px;
425
+ font-weight: 650;
426
+ cursor: pointer;
427
+ }
428
+ body.autolaunch-page .autolaunch-clear-script:hover {
429
+ background: color-mix(in srgb, var(--task-border) 55%, transparent);
430
+ color: var(--task-text);
431
+ }
432
+ body.autolaunch-page .autolaunch-clear-script:disabled {
433
+ cursor: not-allowed;
434
+ opacity: 0.55;
435
+ }
385
436
  body.autolaunch-page .autolaunch-tag {
386
437
  border-radius: 999px;
387
438
  padding: 2px 6px 3px;
@@ -391,26 +442,7 @@ body.autolaunch-page .autolaunch-tag {
391
442
  font-weight: 650;
392
443
  line-height: 1;
393
444
  }
394
- body.autolaunch-page .autolaunch-manual {
395
- padding: 16px;
396
- border-top: 1px solid var(--task-border);
397
- }
398
- body.autolaunch-page .autolaunch-manual-row {
399
- display: flex;
400
- align-items: center;
401
- gap: 8px;
402
- }
403
- body.autolaunch-page .autolaunch-manual input {
404
- flex: 1 1 auto;
405
- min-height: 32px;
406
- padding: 0 10px;
407
- border: 1px solid var(--task-border-strong);
408
- border-radius: 7px;
409
- background: color-mix(in srgb, var(--task-panel) 98%, white);
410
- }
411
- body.dark.autolaunch-page .autolaunch-manual input {
412
- background: color-mix(in srgb, var(--task-panel) 95%, black);
413
- }
445
+ <%- include('partials/autolaunch_dependency_styles') %>
414
446
  body.autolaunch-page .autolaunch-feedback {
415
447
  min-height: 16px;
416
448
  margin-top: 7px;
@@ -479,15 +511,15 @@ body.autolaunch-page .autolaunch-loading i {
479
511
  <section class="task-shell autolaunch-shell">
480
512
  <header class="task-shell-header">
481
513
  <div class="task-shell-header-main">
482
- <h1 class="task-title">Startup scripts</h1>
483
- <p class="task-description">Choose which installed app script runs when Pinokio starts.</p>
514
+ <h1 class="task-title">Autolaunch</h1>
515
+ <p class="task-description">Choose app launch scripts, startup behavior, and requirements.</p>
484
516
  </div>
485
517
  <div class="task-header-actions autolaunch-summary">
486
- <span class="autolaunch-summary-item"><strong data-enabled-count><%= enabledCount %></strong> enabled</span>
518
+ <span class="autolaunch-summary-item"><strong data-enabled-count><%= enabledCount %></strong> startup enabled</span>
487
519
  <span class="autolaunch-summary-item"><strong data-app-count><%= apps.length %></strong> apps</span>
488
520
  <button type="button" class="task-button danger" data-disable-all <%= enabledCount ? '' : 'disabled' %>>
489
521
  <i class="fa-solid fa-power-off"></i>
490
- Disable all
522
+ Disable startup
491
523
  </button>
492
524
  </div>
493
525
  </header>
@@ -497,7 +529,7 @@ body.autolaunch-page .autolaunch-loading i {
497
529
  <div class="task-section-head">
498
530
  <div>
499
531
  <h2 class="task-section-title">Installed apps</h2>
500
- <p class="task-section-subcopy">Apps with autolaunch enabled appear first. Changes take effect the next time Pinokio starts.</p>
532
+ <p class="task-section-subcopy">Apps with startup enabled appear first. Requirements are used for startup and manual launches.</p>
501
533
  </div>
502
534
  </div>
503
535
  <div class="autolaunch-toolbar">
@@ -512,7 +544,7 @@ body.autolaunch-page .autolaunch-loading i {
512
544
  </aside>
513
545
 
514
546
  <section class="autolaunch-detail" data-detail>
515
- <div class="autolaunch-empty">Select an app to configure its startup script.</div>
547
+ <div class="autolaunch-empty">Select an app to configure Autolaunch.</div>
516
548
  </section>
517
549
  </div>
518
550
  </section>
@@ -528,6 +560,13 @@ let selectedAppId = "";
528
560
  let selectedScript = "";
529
561
  let candidateState = null;
530
562
  let savingAutolaunch = false;
563
+ let savingDependencies = false;
564
+ let savingDependencyScript = false;
565
+ let dependencyPickerOpen = false;
566
+ let dependencyScriptPickerOpen = "";
567
+ let pendingDependencyId = "";
568
+ let dependencyScriptCandidates = {};
569
+ let dependencyScriptSelections = {};
531
570
 
532
571
  const appListEl = document.querySelector("[data-app-list]");
533
572
  const searchEl = document.querySelector("[data-app-search]");
@@ -586,11 +625,24 @@ const syncAppState = (updatedApp) => {
586
625
  };
587
626
 
588
627
  const setDetailBusy = (busy) => {
589
- detailEl.querySelectorAll("[data-autolaunch-switch], [data-apply-manual]").forEach((control) => {
590
- control.disabled = !!busy;
628
+ const blocked = !!busy || savingAutolaunch || savingDependencies || savingDependencyScript;
629
+ detailEl.querySelectorAll("[data-autolaunch-switch], [data-apply-manual], [data-clear-script], [data-toggle-dependency-picker], [data-add-dependency], [data-remove-dependency], [data-configure-dependency], [data-dependency-search], [data-dependency-script-choice], [data-close-dependency-script-modal]").forEach((control) => {
630
+ control.disabled = blocked;
591
631
  });
592
632
  };
593
633
 
634
+ const updateSelectedAppUrl = (appId) => {
635
+ try {
636
+ const url = new URL(window.location.href);
637
+ if (appId) {
638
+ url.searchParams.set("app", appId);
639
+ } else {
640
+ url.searchParams.delete("app");
641
+ }
642
+ window.history.replaceState({}, "", `${url.pathname}${url.search}${url.hash}`);
643
+ } catch (_) {}
644
+ };
645
+
594
646
  const renderAppList = () => {
595
647
  const query = (searchEl.value || "").trim().toLowerCase();
596
648
  const visible = orderedApps().filter((app) => {
@@ -608,24 +660,24 @@ const renderAppList = () => {
608
660
  visible.forEach((app) => {
609
661
  const folderPath = formatAppFolderPath(app);
610
662
  const fullFolderPath = app.workspace_path || app.launcher_path || folderPath;
611
- const scriptLine = app.autolaunch_enabled && app.autolaunch
612
- ? `<div class="autolaunch-app-script">Runs ${escapeHtml(app.autolaunch)}</div>`
663
+ const scriptLine = app.autolaunch
664
+ ? `<div class="autolaunch-app-script">Launches ${escapeHtml(app.autolaunch)}</div>`
613
665
  : "";
614
- const button = document.createElement("button");
615
- button.type = "button";
616
- button.className = `autolaunch-app-row${app.id === selectedAppId ? " selected" : ""}`;
617
- button.dataset.appId = app.id;
618
- button.innerHTML = `
666
+ const row = document.createElement("button");
667
+ row.type = "button";
668
+ row.className = `autolaunch-app-row${app.id === selectedAppId ? " selected" : ""}`;
669
+ row.dataset.appId = app.id;
670
+ row.innerHTML = `
619
671
  <img src="${escapeHtml(app.icon || "/pinokio-black.png")}" alt="" onerror="this.onerror=null; this.src='/pinokio-black.png'">
620
672
  <div class="autolaunch-app-meta">
621
673
  <div class="autolaunch-app-name">${escapeHtml(app.title || app.id)}</div>
622
674
  <div class="autolaunch-app-folder" title="${escapeHtml(fullFolderPath)}">${escapeHtml(folderPath)}</div>
623
675
  ${scriptLine}
624
676
  </div>
625
- <span class="autolaunch-pill ${app.autolaunch_enabled ? "on" : ""}">${app.autolaunch_enabled ? "ON" : "OFF"}</span>
677
+ <span class="autolaunch-pill ${app.autolaunch_enabled ? "on" : ""}">${app.autolaunch_enabled ? "Startup ON" : "Startup OFF"}</span>
626
678
  `;
627
- button.addEventListener("click", () => selectApp(app.id));
628
- appListEl.appendChild(button);
679
+ row.addEventListener("click", () => selectApp(app.id));
680
+ appListEl.appendChild(row);
629
681
  });
630
682
  };
631
683
 
@@ -639,10 +691,13 @@ const renderDetailShell = (app, bodyHtml) => {
639
691
  <h2 class="autolaunch-detail-title">${escapeHtml(app.title || app.id)}</h2>
640
692
  <div class="autolaunch-current">${escapeHtml(app.workspace_path || app.id)}</div>
641
693
  </div>
642
- <button type="button" class="autolaunch-switch" role="switch" aria-checked="${checked}" data-autolaunch-switch ${savingAutolaunch ? "disabled" : ""}>
643
- <span class="autolaunch-switch-track" aria-hidden="true"><span class="autolaunch-switch-thumb"></span></span>
644
- <span class="autolaunch-switch-label">${label}</span>
645
- </button>
694
+ <div class="autolaunch-startup-control">
695
+ <span class="autolaunch-startup-label">Start with Pinokio</span>
696
+ <button type="button" class="autolaunch-switch" role="switch" aria-checked="${checked}" aria-label="Start with Pinokio" data-autolaunch-switch ${savingAutolaunch ? "disabled" : ""}>
697
+ <span class="autolaunch-switch-track" aria-hidden="true"><span class="autolaunch-switch-thumb"></span></span>
698
+ <span class="autolaunch-switch-label">${label}</span>
699
+ </button>
700
+ </div>
646
701
  </div>
647
702
  ${bodyHtml}
648
703
  `;
@@ -692,18 +747,9 @@ const renderScriptSection = (title, note, candidates) => {
692
747
  `;
693
748
  };
694
749
 
695
- const chooseInitialScript = (data) => {
696
- const all = [].concat(data.menu || [], data.other || []);
697
- if (data.current) {
698
- return data.current;
699
- }
700
- const defaultCandidate = all.find((candidate) => candidate.menu_default);
701
- if (defaultCandidate) {
702
- return defaultCandidate.script;
703
- }
704
- return all.length > 0 ? all[0].script : (data.current || "");
705
- };
750
+ const configuredScript = (data) => data && data.current ? data.current : "";
706
751
 
752
+ <%- include('partials/autolaunch_dependency_helpers') %>
707
753
  const bindDetailControls = () => {
708
754
  detailEl.querySelectorAll("input[name='autolaunch-script']").forEach((input) => {
709
755
  input.addEventListener("change", () => {
@@ -712,11 +758,13 @@ const bindDetailControls = () => {
712
758
  if (manual) manual.value = selectedScript;
713
759
  renderCandidates();
714
760
  const app = currentApp();
715
- if (app && app.autolaunch_enabled) {
716
- saveScript(selectedScript);
761
+ if (app) {
762
+ saveScript(selectedScript, !!app.autolaunch_enabled);
717
763
  }
718
764
  });
719
765
  });
766
+ const clear = detailEl.querySelector("[data-clear-script]");
767
+ if (clear) clear.addEventListener("click", clearScript);
720
768
  const toggle = detailEl.querySelector("[data-autolaunch-switch]");
721
769
  if (toggle) toggle.addEventListener("click", toggleAutolaunch);
722
770
  const manual = detailEl.querySelector("[data-manual-script]");
@@ -743,6 +791,103 @@ const bindDetailControls = () => {
743
791
  }
744
792
  const applyManual = detailEl.querySelector("[data-apply-manual]");
745
793
  if (applyManual) applyManual.addEventListener("click", saveSelectedScript);
794
+ const toggleDependencyPicker = detailEl.querySelector("[data-toggle-dependency-picker]");
795
+ if (toggleDependencyPicker) {
796
+ toggleDependencyPicker.addEventListener("click", () => {
797
+ dependencyPickerOpen = !dependencyPickerOpen;
798
+ renderCandidates();
799
+ if (dependencyPickerOpen) {
800
+ window.setTimeout(() => {
801
+ const search = detailEl.querySelector("[data-dependency-search]");
802
+ if (search) search.focus();
803
+ }, 0);
804
+ }
805
+ });
806
+ }
807
+ const dependencySearch = detailEl.querySelector("[data-dependency-search]");
808
+ if (dependencySearch) {
809
+ dependencySearch.addEventListener("input", () => {
810
+ filterDependencyPicker(detailEl.querySelector("[data-dependency-picker]"));
811
+ });
812
+ }
813
+ detailEl.querySelectorAll("[data-add-dependency]").forEach((control) => {
814
+ control.addEventListener("click", () => {
815
+ const app = currentApp();
816
+ const dependency = control.getAttribute("data-add-dependency");
817
+ if (!app || !dependency) return;
818
+ if (!(selectedScript || app.autolaunch)) {
819
+ dependencyPickerOpen = false;
820
+ renderCandidates();
821
+ setFeedback("Choose this app's launch script before adding requirements.", true);
822
+ return;
823
+ }
824
+ const dependencies = Array.isArray(app.autolaunch_depends) ? app.autolaunch_depends.slice() : [];
825
+ if (!dependencies.includes(dependency)) {
826
+ dependencyPickerOpen = false;
827
+ pendingDependencyId = dependency;
828
+ dependencyScriptPickerOpen = dependency;
829
+ renderCandidates();
830
+ loadDependencyScriptCandidates(dependency);
831
+ setFeedback("");
832
+ }
833
+ });
834
+ });
835
+ detailEl.querySelectorAll("[data-close-dependency-script-modal]").forEach((control) => {
836
+ control.addEventListener("click", () => {
837
+ if (pendingDependencyId === dependencyScriptPickerOpen) {
838
+ pendingDependencyId = "";
839
+ }
840
+ if (dependencyScriptPickerOpen) {
841
+ delete dependencyScriptSelections[dependencyScriptPickerOpen];
842
+ }
843
+ dependencyScriptPickerOpen = "";
844
+ renderCandidates();
845
+ setFeedback("");
846
+ });
847
+ });
848
+ detailEl.querySelectorAll("[data-remove-dependency]").forEach((control) => {
849
+ control.addEventListener("click", () => {
850
+ const app = currentApp();
851
+ if (!app) return;
852
+ const dependency = control.getAttribute("data-remove-dependency");
853
+ const dependencies = Array.isArray(app.autolaunch_depends) ? app.autolaunch_depends : [];
854
+ if (dependency) {
855
+ saveDependencies(dependencies.filter((id) => id !== dependency));
856
+ }
857
+ });
858
+ });
859
+ detailEl.querySelectorAll("[data-configure-dependency]").forEach((control) => {
860
+ control.addEventListener("click", () => {
861
+ const dependency = control.getAttribute("data-configure-dependency");
862
+ if (dependency) {
863
+ pendingDependencyId = "";
864
+ dependencyScriptPickerOpen = dependency;
865
+ renderCandidates();
866
+ if (dependencyScriptPickerOpen) {
867
+ loadDependencyScriptCandidates(dependency);
868
+ }
869
+ }
870
+ });
871
+ });
872
+ detailEl.querySelectorAll("[data-dependency-script-choice]").forEach((input) => {
873
+ input.addEventListener("change", () => {
874
+ const dependency = input.getAttribute("data-dependency-script-app");
875
+ if (dependency && input.value) {
876
+ dependencyScriptSelections[dependency] = input.value;
877
+ renderCandidates();
878
+ }
879
+ });
880
+ });
881
+ detailEl.querySelectorAll("[data-confirm-dependency-script]").forEach((control) => {
882
+ control.addEventListener("click", () => {
883
+ const dependency = control.getAttribute("data-dependency-script-app");
884
+ const dependencyApp = dependency ? dependencyById(dependency) : null;
885
+ const script = dependency ? (dependencyScriptSelections[dependency] || (dependencyApp && dependencyApp.autolaunch) || "") : "";
886
+ if (dependency && script) {
887
+ saveDependencyScript(dependency, script);
888
+ }
889
+ });
890
+ });
746
891
  };
747
892
 
748
893
  const renderCandidates = () => {
@@ -750,15 +895,31 @@ const renderCandidates = () => {
750
895
  if (!app || !candidateState) return;
751
896
  const hasCandidates = (candidateState.menu && candidateState.menu.length) || (candidateState.other && candidateState.other.length);
752
897
  const currentValue = candidateState.current || "";
898
+ const dependencies = Array.isArray(app.autolaunch_depends) ? app.autolaunch_depends : [];
899
+ const hasRequirements = dependencies.length > 0;
900
+ const requirementWarning = hasRequirements && !currentValue
901
+ ? '<div class="autolaunch-feedback error">Choose this app\'s launch script before these requirements can run.</div>'
902
+ : "";
903
+ const clearButton = currentValue
904
+ ? `<button type="button" class="autolaunch-clear-script" data-clear-script title="${hasRequirements ? "Remove requirements before clearing this app's launch script." : "Clear launch script"}" ${hasRequirements ? "disabled" : ""}>
905
+ <i class="fa-solid fa-xmark" aria-hidden="true"></i>
906
+ <span>Clear</span>
907
+ </button>`
908
+ : "";
753
909
  renderDetailShell(app, `
754
910
  <div class="autolaunch-section">
755
911
  <div class="autolaunch-section-header">
756
912
  <h3 class="autolaunch-section-title">Current</h3>
757
- <span class="autolaunch-section-note">Takes effect next time Pinokio starts</span>
913
+ <span class="autolaunch-section-note">Launch script</span>
758
914
  </div>
759
- <div class="autolaunch-current">${escapeHtml(currentValue || "OFF")}</div>
915
+ <div class="autolaunch-current-row">
916
+ <div class="autolaunch-current">${escapeHtml(currentValue || "OFF")}</div>
917
+ ${clearButton}
918
+ </div>
919
+ ${requirementWarning}
760
920
  <div class="autolaunch-feedback" data-feedback></div>
761
921
  </div>
922
+ ${renderDependencySection(app)}
762
923
  ${hasCandidates ? "" : '<div class="autolaunch-section"><div class="autolaunch-empty">No local scripts were found for this app.</div></div>'}
763
924
  ${renderScriptSection("Menu scripts", "From pinokio.js", candidateState.menu || [])}
764
925
  ${renderScriptSection("Other local scripts", "Advanced", candidateState.other || [])}
@@ -783,17 +944,19 @@ const toggleAutolaunch = () => {
783
944
  const app = currentApp();
784
945
  if (!app || savingAutolaunch) return;
785
946
  if (app.autolaunch_enabled) {
786
- saveScript("");
947
+ const manual = detailEl.querySelector("[data-manual-script]");
948
+ const script = manual && manual.value.trim() ? manual.value.trim() : (selectedScript || app.autolaunch || "");
949
+ saveScript(script, false);
787
950
  return;
788
951
  }
789
952
  const manual = detailEl.querySelector("[data-manual-script]");
790
- const script = manual && manual.value.trim() ? manual.value.trim() : selectedScript;
953
+ const script = manual && manual.value.trim() ? manual.value.trim() : (selectedScript || "");
791
954
  if (!script) {
792
- setFeedback("Choose a script first.", true);
955
+ setFeedback("Choose a launch script first.", true);
793
956
  return;
794
957
  }
795
958
  selectedScript = script;
796
- saveScript(script);
959
+ saveScript(script, true);
797
960
  };
798
961
 
799
962
  const loadCandidates = async (appId) => {
@@ -812,7 +975,7 @@ const loadCandidates = async (appId) => {
812
975
  }
813
976
  candidateState = data;
814
977
  syncAppState(data.app);
815
- selectedScript = chooseInitialScript(data);
978
+ selectedScript = configuredScript(data);
816
979
  renderCandidates();
817
980
  } catch (error) {
818
981
  renderDetailShell(app, `
@@ -823,13 +986,88 @@ const loadCandidates = async (appId) => {
823
986
  }
824
987
  };
825
988
 
826
- const selectApp = (appId) => {
989
+ const selectApp = (appId, updateUrl = true) => {
827
990
  selectedAppId = appId;
991
+ dependencyPickerOpen = false;
992
+ dependencyScriptPickerOpen = "";
993
+ pendingDependencyId = "";
994
+ if (updateUrl) {
995
+ updateSelectedAppUrl(appId);
996
+ }
828
997
  renderAppList();
829
998
  loadCandidates(appId);
830
999
  };
831
1000
 
832
- const saveScript = async (script) => {
1001
+ <%- include('partials/autolaunch_dependency_save') %>
1002
+ const updateDependencyCandidateState = (updatedApp) => {
1003
+ if (!updatedApp || !updatedApp.id || !candidateState || !Array.isArray(candidateState.dependency_apps)) return;
1004
+ candidateState.dependency_apps = candidateState.dependency_apps.map((app) => app.id === updatedApp.id ? updatedApp : app);
1005
+ };
1006
+
1007
+ <%- include('partials/launch_settings_dependency_script_loader_factory') %>
1008
+ const loadDependencyScriptCandidates = createLaunchSettingsDependencyScriptLoader({
1009
+ getCache: () => dependencyScriptCandidates,
1010
+ render: renderCandidates
1011
+ });
1012
+
1013
+ const saveDependencyScript = async (appId, script) => {
1014
+ const dependency = autolaunchApps.find((app) => app.id === appId);
1015
+ if (!dependency || !script || savingDependencyScript) return;
1016
+ savingDependencyScript = true;
1017
+ setDetailBusy(true);
1018
+ setFeedback("Saving requirement script...");
1019
+ try {
1020
+ const response = await fetch("/autolaunch", {
1021
+ method: "POST",
1022
+ headers: {
1023
+ "Content-Type": "application/json",
1024
+ "Accept": "application/json"
1025
+ },
1026
+ body: JSON.stringify({
1027
+ app: appId,
1028
+ script,
1029
+ enabled: !!dependency.autolaunch_enabled
1030
+ })
1031
+ });
1032
+ const data = await response.json();
1033
+ if (!response.ok || !data.ok) {
1034
+ throw new Error(data.error || "Failed to save launch script.");
1035
+ }
1036
+ syncAppState(data.app);
1037
+ updateDependencyCandidateState(data.app);
1038
+ if (pendingDependencyId === appId) {
1039
+ const app = currentApp();
1040
+ const dependencies = app && Array.isArray(app.autolaunch_depends) ? app.autolaunch_depends.slice() : [];
1041
+ if (!dependencies.includes(appId)) {
1042
+ const savedDependency = await saveDependencies(dependencies.concat(appId), "Requirement saved.");
1043
+ if (savedDependency) {
1044
+ pendingDependencyId = "";
1045
+ delete dependencyScriptSelections[appId];
1046
+ dependencyScriptPickerOpen = "";
1047
+ renderCandidates();
1048
+ }
1049
+ } else {
1050
+ pendingDependencyId = "";
1051
+ delete dependencyScriptSelections[appId];
1052
+ dependencyScriptPickerOpen = "";
1053
+ renderCandidates();
1054
+ setFeedback("Requirement saved.");
1055
+ }
1056
+ } else {
1057
+ dependencyScriptPickerOpen = "";
1058
+ delete dependencyScriptSelections[appId];
1059
+ renderCandidates();
1060
+ setFeedback("Requirement script saved.");
1061
+ }
1062
+ } catch (error) {
1063
+ setFeedback(error && error.message ? error.message : "Failed to save launch script.", true);
1064
+ } finally {
1065
+ savingDependencyScript = false;
1066
+ setDetailBusy(false);
1067
+ }
1068
+ };
1069
+
1070
+ const saveScript = async (script, enabled = !!(currentApp() && currentApp().autolaunch_enabled)) => {
833
1071
  const app = currentApp();
834
1072
  if (!app || savingAutolaunch) return;
835
1073
  savingAutolaunch = true;
@@ -844,12 +1082,13 @@ const saveScript = async (script) => {
844
1082
  },
845
1083
  body: JSON.stringify({
846
1084
  app: app.id,
847
- script
1085
+ script,
1086
+ enabled: !!enabled
848
1087
  })
849
1088
  });
850
1089
  const data = await response.json();
851
1090
  if (!response.ok || !data.ok) {
852
- throw new Error(data.error || "Failed to save autolaunch.");
1091
+ throw new Error(data.error || "Failed to save Autolaunch settings.");
853
1092
  }
854
1093
  syncAppState(data.app);
855
1094
  if (candidateState) {
@@ -857,9 +1096,50 @@ const saveScript = async (script) => {
857
1096
  }
858
1097
  selectedScript = data.app.autolaunch || selectedScript;
859
1098
  renderCandidates();
860
- setFeedback(data.app.autolaunch_enabled ? "Saved." : "Disabled.");
1099
+ setFeedback(data.app.autolaunch_enabled ? "Saved." : (script ? "Launch script saved." : "Disabled."));
1100
+ } catch (error) {
1101
+ setFeedback(error && error.message ? error.message : "Failed to save Autolaunch settings.", true);
1102
+ } finally {
1103
+ savingAutolaunch = false;
1104
+ setDetailBusy(false);
1105
+ }
1106
+ };
1107
+
1108
+ const clearScript = async () => {
1109
+ const app = currentApp();
1110
+ if (!app || savingAutolaunch) return;
1111
+ if (Array.isArray(app.autolaunch_depends) && app.autolaunch_depends.length > 0) {
1112
+ setFeedback("Remove requirements before clearing this app's launch script.", true);
1113
+ return;
1114
+ }
1115
+ savingAutolaunch = true;
1116
+ setDetailBusy(true);
1117
+ setFeedback("Clearing launch script...");
1118
+ try {
1119
+ const response = await fetch("/autolaunch", {
1120
+ method: "POST",
1121
+ headers: {
1122
+ "Content-Type": "application/json",
1123
+ "Accept": "application/json"
1124
+ },
1125
+ body: JSON.stringify({
1126
+ app: app.id,
1127
+ clear_script: true
1128
+ })
1129
+ });
1130
+ const data = await response.json();
1131
+ if (!response.ok || !data.ok) {
1132
+ throw new Error(data.error || "Failed to clear launch script.");
1133
+ }
1134
+ syncAppState(data.app);
1135
+ if (candidateState) {
1136
+ candidateState.current = "";
1137
+ }
1138
+ selectedScript = "";
1139
+ renderCandidates();
1140
+ setFeedback("Launch script cleared.");
861
1141
  } catch (error) {
862
- setFeedback(error && error.message ? error.message : "Failed to save autolaunch.", true);
1142
+ setFeedback(error && error.message ? error.message : "Failed to clear launch script.", true);
863
1143
  } finally {
864
1144
  savingAutolaunch = false;
865
1145
  setDetailBusy(false);
@@ -873,11 +1153,12 @@ const saveSelectedScript = () => {
873
1153
  setFeedback("Choose a script first.", true);
874
1154
  return;
875
1155
  }
876
- saveScript(script);
1156
+ const app = currentApp();
1157
+ saveScript(script, !!(app && app.autolaunch_enabled));
877
1158
  };
878
1159
 
879
1160
  const disableAll = async () => {
880
- if (!window.confirm("Disable autolaunch for every installed app?")) {
1161
+ if (!window.confirm("Turn off startup launch for every installed app?")) {
881
1162
  return;
882
1163
  }
883
1164
  disableAllButton.disabled = true;
@@ -888,16 +1169,16 @@ const disableAll = async () => {
888
1169
  });
889
1170
  const data = await response.json();
890
1171
  if (!response.ok || !data.ok) {
891
- throw new Error(data.error || "Failed to disable autolaunch.");
1172
+ throw new Error(data.error || "Failed to disable startup launch.");
892
1173
  }
893
- autolaunchApps = data.apps || autolaunchApps.map((app) => ({ ...app, autolaunch: "", autolaunch_enabled: false }));
1174
+ autolaunchApps = data.apps || autolaunchApps.map((app) => ({ ...app, autolaunch_enabled: false }));
894
1175
  updateStats();
895
1176
  renderAppList();
896
1177
  if (selectedAppId) {
897
1178
  loadCandidates(selectedAppId);
898
1179
  }
899
1180
  } catch (error) {
900
- window.alert(error && error.message ? error.message : "Failed to disable autolaunch.");
1181
+ window.alert(error && error.message ? error.message : "Failed to disable startup launch.");
901
1182
  updateStats();
902
1183
  }
903
1184
  };
@@ -907,9 +1188,16 @@ disableAllButton.addEventListener("click", disableAll);
907
1188
 
908
1189
  updateStats();
909
1190
  renderAppList();
910
- const firstApp = orderedApps()[0];
1191
+ let initialApp = null;
1192
+ try {
1193
+ const requestedAppId = new URLSearchParams(window.location.search).get("app");
1194
+ if (requestedAppId) {
1195
+ initialApp = autolaunchApps.find((app) => app && app.id === requestedAppId) || null;
1196
+ }
1197
+ } catch (_) {}
1198
+ const firstApp = initialApp || orderedApps()[0];
911
1199
  if (firstApp) {
912
- selectApp(firstApp.id);
1200
+ selectApp(firstApp.id, false);
913
1201
  }
914
1202
  </script>
915
1203
  </body>