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,171 @@
1
+ .launch-requirements-status {
2
+ position: absolute;
3
+ inset: 0;
4
+ z-index: 10001;
5
+ display: grid;
6
+ place-items: center;
7
+ box-sizing: border-box;
8
+ padding: 24px;
9
+ pointer-events: none;
10
+ color: rgba(17, 24, 39, 0.92);
11
+ }
12
+ .launch-requirements-status[hidden] {
13
+ display: none !important;
14
+ }
15
+ .launch-requirements-card {
16
+ width: min(520px, calc(100vw - 64px));
17
+ box-sizing: border-box;
18
+ padding: 0;
19
+ text-align: center;
20
+ pointer-events: auto;
21
+ }
22
+ body.dark .launch-requirements-card {
23
+ color: rgba(248, 250, 252, 0.92);
24
+ }
25
+ .launch-requirements-title {
26
+ display: flex;
27
+ justify-content: center;
28
+ align-items: center;
29
+ gap: 8px;
30
+ margin-bottom: 6px;
31
+ font-size: 14px;
32
+ font-weight: 750;
33
+ }
34
+ .launch-requirements-title i {
35
+ color: #2563eb;
36
+ }
37
+ .launch-requirements-detail {
38
+ color: rgba(100, 116, 139, 0.96);
39
+ font-size: 12px;
40
+ line-height: 1.35;
41
+ margin-bottom: 12px;
42
+ }
43
+ body.dark .launch-requirements-detail {
44
+ color: rgba(203, 213, 225, 0.86);
45
+ }
46
+ .launch-requirements-list {
47
+ display: grid;
48
+ gap: 6px;
49
+ margin-top: 14px;
50
+ text-align: left;
51
+ }
52
+ .launch-requirements-row {
53
+ display: grid;
54
+ grid-template-columns: 26px minmax(0, 1fr) auto;
55
+ align-items: center;
56
+ gap: 8px;
57
+ min-height: 38px;
58
+ padding: 6px 8px;
59
+ border: 1px solid rgba(148, 163, 184, 0.18);
60
+ border-radius: 8px;
61
+ background: rgba(148, 163, 184, 0.08);
62
+ }
63
+ body.dark .launch-requirements-row {
64
+ border-color: rgba(148, 163, 184, 0.12);
65
+ background: rgba(148, 163, 184, 0.07);
66
+ }
67
+ .launch-requirements-row img {
68
+ width: 26px;
69
+ height: 26px;
70
+ border-radius: 6px;
71
+ object-fit: cover;
72
+ }
73
+ .launch-requirements-name {
74
+ min-width: 0;
75
+ overflow: hidden;
76
+ text-overflow: ellipsis;
77
+ white-space: nowrap;
78
+ font-size: 12px;
79
+ font-weight: 700;
80
+ }
81
+ .launch-requirements-state {
82
+ display: inline-flex;
83
+ align-items: center;
84
+ justify-content: flex-end;
85
+ gap: 6px;
86
+ color: rgba(100, 116, 139, 0.96);
87
+ font-size: 11px;
88
+ white-space: nowrap;
89
+ }
90
+ .launch-requirements-choose-script {
91
+ appearance: none;
92
+ border: 1px solid rgba(148, 163, 184, 0.34);
93
+ border-radius: 6px;
94
+ background: rgba(255, 255, 255, 0.72);
95
+ color: rgba(30, 41, 59, 0.92);
96
+ display: inline-flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ gap: 5px;
100
+ min-height: 24px;
101
+ margin-left: 2px;
102
+ padding: 0 8px;
103
+ font: inherit;
104
+ font-size: 11px;
105
+ font-weight: 750;
106
+ line-height: 1;
107
+ cursor: pointer;
108
+ white-space: nowrap;
109
+ }
110
+ .launch-requirements-choose-script i {
111
+ color: rgba(71, 85, 105, 0.9);
112
+ font-size: 10px;
113
+ }
114
+ .launch-requirements-choose-script:hover {
115
+ border-color: rgba(100, 116, 139, 0.5);
116
+ background: rgba(248, 250, 252, 0.96);
117
+ color: rgba(15, 23, 42, 0.96);
118
+ }
119
+ .launch-requirements-choose-script:focus-visible {
120
+ outline: 2px solid rgba(37, 99, 235, 0.34);
121
+ outline-offset: 2px;
122
+ }
123
+ .launch-requirements-state.is-active i {
124
+ color: #2563eb;
125
+ }
126
+ .launch-requirements-state.is-ready i {
127
+ color: #16a34a;
128
+ }
129
+ body.dark .launch-requirements-state {
130
+ color: rgba(203, 213, 225, 0.82);
131
+ }
132
+ body.dark .launch-requirements-choose-script {
133
+ border-color: rgba(148, 163, 184, 0.22);
134
+ background: rgba(15, 23, 42, 0.7);
135
+ color: rgba(226, 232, 240, 0.92);
136
+ }
137
+ body.dark .launch-requirements-choose-script i {
138
+ color: rgba(203, 213, 225, 0.82);
139
+ }
140
+ body.dark .launch-requirements-choose-script:hover {
141
+ border-color: rgba(148, 163, 184, 0.36);
142
+ background: rgba(30, 41, 59, 0.78);
143
+ color: rgba(248, 250, 252, 0.96);
144
+ }
145
+ .launch-requirements-actions {
146
+ display: flex;
147
+ justify-content: center;
148
+ margin-top: 12px;
149
+ }
150
+ .launch-requirements-stop {
151
+ display: inline-flex;
152
+ align-items: center;
153
+ justify-content: center;
154
+ gap: 7px;
155
+ min-height: 30px;
156
+ padding: 0 11px;
157
+ border: 1px solid rgba(148, 163, 184, 0.28);
158
+ border-radius: 8px;
159
+ color: inherit;
160
+ background: rgba(148, 163, 184, 0.1);
161
+ font-size: 12px;
162
+ font-weight: 700;
163
+ cursor: pointer;
164
+ }
165
+ .launch-requirements-stop:hover {
166
+ background: rgba(148, 163, 184, 0.16);
167
+ }
168
+ .launch-requirements-stop:disabled {
169
+ cursor: default;
170
+ opacity: 0.65;
171
+ }
@@ -0,0 +1,45 @@
1
+ const createLaunchSettingsDependencySaver = ({
2
+ getAppId,
3
+ isSaving,
4
+ setSaving,
5
+ setBusy,
6
+ setFeedback,
7
+ onSaved,
8
+ render
9
+ }) => async (dependencies, successMessage = "Dependencies saved.") => {
10
+ const appId = typeof getAppId === "function" ? getAppId() : ""
11
+ if (!appId || (typeof isSaving === "function" && isSaving())) return
12
+ if (typeof setSaving === "function") setSaving(true)
13
+ if (typeof setBusy === "function") setBusy(true)
14
+ if (typeof setFeedback === "function") setFeedback("Saving dependencies...")
15
+ let feedbackMessage = successMessage
16
+ let feedbackError = false
17
+ let saved = false
18
+ try {
19
+ const response = await fetch("/autolaunch/dependencies", {
20
+ method: "POST",
21
+ headers: {
22
+ "Content-Type": "application/json",
23
+ "Accept": "application/json"
24
+ },
25
+ body: JSON.stringify({
26
+ app: appId,
27
+ dependencies: Array.isArray(dependencies) ? dependencies : []
28
+ })
29
+ })
30
+ const data = await response.json()
31
+ if (!response.ok || !data.ok) {
32
+ throw new Error(data.error || "Failed to save dependencies.")
33
+ }
34
+ if (typeof onSaved === "function") onSaved(data.app)
35
+ saved = true
36
+ } catch (error) {
37
+ feedbackMessage = error && error.message ? error.message : "Failed to save dependencies."
38
+ feedbackError = true
39
+ } finally {
40
+ if (typeof setSaving === "function") setSaving(false)
41
+ if (typeof render === "function") render()
42
+ if (typeof setFeedback === "function") setFeedback(feedbackMessage, feedbackError)
43
+ }
44
+ return saved
45
+ }
@@ -0,0 +1,25 @@
1
+ const createLaunchSettingsDependencyScriptLoader = ({
2
+ getCache,
3
+ render
4
+ }) => async (appId) => {
5
+ const cache = typeof getCache === "function" ? getCache() : null
6
+ if (!appId || !cache || cache[appId]) return
7
+ cache[appId] = { loading: true }
8
+ if (typeof render === "function") render()
9
+ try {
10
+ const response = await fetch(`/autolaunch/candidates?app=${encodeURIComponent(appId)}`, {
11
+ headers: { "Accept": "application/json" }
12
+ })
13
+ const data = await response.json()
14
+ if (!response.ok || !data.ok) {
15
+ throw new Error(data.error || "Failed to load script choices.")
16
+ }
17
+ cache[appId] = data
18
+ } catch (error) {
19
+ cache[appId] = {
20
+ error: error && error.message ? error.message : "Failed to load script choices."
21
+ }
22
+ } finally {
23
+ if (typeof render === "function") render()
24
+ }
25
+ }
@@ -62,6 +62,7 @@
62
62
  <link href="/electron.css" rel="stylesheet"/>
63
63
  <% } %>
64
64
  <style>
65
+ <%- include('./partials/launch_requirements_status_styles') %>
65
66
  html {
66
67
  scroll-behavior: smooth;
67
68
  height: 100%;
@@ -1491,6 +1492,83 @@ document.addEventListener("DOMContentLoaded", async () => {
1491
1492
  if (activeProcessWait && (activeProcessWait.title || activeProcessWait.description || activeProcessWait.message) && window.PinokioWaitFooterStatus) {
1492
1493
  window.PinokioWaitFooterStatus.show(activeProcessWait)
1493
1494
  }
1495
+ const resolveCurrentApiScript = () => {
1496
+ let parsed
1497
+ try {
1498
+ parsed = new URL(window.location.href)
1499
+ } catch (_) {
1500
+ return null
1501
+ }
1502
+ const prefix = "/api/"
1503
+ if (!parsed.pathname.startsWith(prefix)) {
1504
+ return null
1505
+ }
1506
+ const parts = parsed.pathname.slice(prefix.length).split("/").filter(Boolean).map((part) => {
1507
+ try {
1508
+ return decodeURIComponent(part)
1509
+ } catch (_) {
1510
+ return part
1511
+ }
1512
+ })
1513
+ if (parts.length < 2) {
1514
+ return null
1515
+ }
1516
+ const appId = parts[0]
1517
+ const script = parts.slice(1).join("/")
1518
+ if (!appId || !script) {
1519
+ return null
1520
+ }
1521
+ return {
1522
+ appId,
1523
+ script,
1524
+ homePath: `api/${appId}/${script}`
1525
+ }
1526
+ }
1527
+ const fetchJsonNoStore = async (url) => {
1528
+ try {
1529
+ const response = await fetch(url, {
1530
+ cache: "no-store",
1531
+ headers: {
1532
+ "Accept": "application/json"
1533
+ }
1534
+ })
1535
+ if (!response.ok) {
1536
+ return null
1537
+ }
1538
+ return response.json().catch(() => null)
1539
+ } catch (_) {
1540
+ return null
1541
+ }
1542
+ }
1543
+ const findRunningScriptEntry = (status, scriptInfo) => {
1544
+ const runningScripts = status && Array.isArray(status.running_scripts) ? status.running_scripts : []
1545
+ return runningScripts.find((entry) => {
1546
+ if (!entry || typeof entry !== "object") {
1547
+ return false
1548
+ }
1549
+ if (entry.home_path === scriptInfo.homePath) {
1550
+ return true
1551
+ }
1552
+ return entry.app === scriptInfo.appId && entry.script_path === scriptInfo.script
1553
+ }) || null
1554
+ }
1555
+ const hasLiveShellForScript = (shells, runningEntry) => {
1556
+ if (!runningEntry || !runningEntry.id || !Array.isArray(shells)) {
1557
+ return false
1558
+ }
1559
+ const scriptId = String(runningEntry.id)
1560
+ return shells.some((shell) => {
1561
+ if (!shell || shell.done === true) {
1562
+ return false
1563
+ }
1564
+ const group = typeof shell.group === "string" ? shell.group : ""
1565
+ const id = typeof shell.id === "string" ? shell.id : ""
1566
+ return group === scriptId || id === scriptId || id.startsWith(`${scriptId}?`)
1567
+ })
1568
+ }
1569
+ const normalizeLogText = (value) => {
1570
+ return String(value || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r\n")
1571
+ }
1494
1572
  const shouldBypassAiConsent = () => {
1495
1573
  const normalize = (value) => (typeof value === "string" ? value.replace(/\\/g, "/") : "")
1496
1574
  const uri = normalize(scriptUri || "")
@@ -1518,6 +1596,33 @@ document.addEventListener("DOMContentLoaded", async () => {
1518
1596
  initialEnabled: initialProtectionEnabled,
1519
1597
  runControls
1520
1598
  })
1599
+ const settleLaunchRequirementsControl = (packet) => {
1600
+ refreshParent(packet || {})
1601
+ pluginTerminalDiscoveryRefresher.clear()
1602
+ document.querySelector("#status-window").innerHTML = "<b>Ready</b>"
1603
+ document.querySelector("#progress-window").classList.add("hidden")
1604
+ document.querySelector("#progress-bar").style.width = "0%"
1605
+ runControls.set("idle")
1606
+ if (rpc && rpc.resizeSync) {
1607
+ rpc.resizeSync.reset()
1608
+ }
1609
+ }
1610
+ window.addEventListener("pinokio:launch-requirements-control", (event) => {
1611
+ settleLaunchRequirementsControl({
1612
+ type: "launch.requirements.control",
1613
+ data: event && event.detail ? event.detail : {}
1614
+ })
1615
+ })
1616
+ window.addEventListener("message", (event) => {
1617
+ const data = event && event.data ? event.data : null
1618
+ if (!data || data.type !== "pinokio:launch-requirements-control") {
1619
+ return
1620
+ }
1621
+ settleLaunchRequirementsControl({
1622
+ type: "launch.requirements.control",
1623
+ data: data.detail || {}
1624
+ })
1625
+ })
1521
1626
  consentManager.showChipIfRemembered()
1522
1627
  class RPC {
1523
1628
  constructor() {
@@ -1532,6 +1637,7 @@ document.addEventListener("DOMContentLoaded", async () => {
1532
1637
  }
1533
1638
  this.baseScriptId = baseScriptId
1534
1639
  this.currentId = baseScriptId
1640
+ this.latestLogFallbackActive = false
1535
1641
  this.pendingInput = []
1536
1642
  this.inputBuffer = ""
1537
1643
  this.inputTracker = window.TerminalInputTracker ? new window.TerminalInputTracker({
@@ -1636,6 +1742,39 @@ document.addEventListener("DOMContentLoaded", async () => {
1636
1742
  if (runId) {
1637
1743
  params.id = runId
1638
1744
  }
1745
+ if (this.latestLogFallbackActive) {
1746
+ let settled = false
1747
+ const settleFallbackStop = (packet) => {
1748
+ if (settled) {
1749
+ return
1750
+ }
1751
+ settled = true
1752
+ this.latestLogFallbackActive = false
1753
+ if (packet) {
1754
+ refreshParent(packet)
1755
+ }
1756
+ pluginTerminalDiscoveryRefresher.clear()
1757
+ document.querySelector("#status-window").innerHTML = "<b>Ready</b>"
1758
+ document.querySelector("#progress-window").classList.add("hidden")
1759
+ document.querySelector("#progress-bar").style.width = "0%"
1760
+ runControls.set("idle")
1761
+ this.resizeSync.reset()
1762
+ this.socket.close()
1763
+ }
1764
+ this.socket.run({
1765
+ method: "kernel.api.stop",
1766
+ params
1767
+ }, (packet) => {
1768
+ if (packet && (packet.type === "disconnect" || packet.type === "event" || packet.type === "result" || packet.type === "error")) {
1769
+ settleFallbackStop(packet)
1770
+ }
1771
+ }).then(() => {
1772
+ settleFallbackStop()
1773
+ }).catch(() => {
1774
+ settleFallbackStop()
1775
+ })
1776
+ return
1777
+ }
1639
1778
  this.socket.run({
1640
1779
  method: "kernel.api.stop",
1641
1780
  params
@@ -1903,6 +2042,8 @@ document.addEventListener("DOMContentLoaded", async () => {
1903
2042
  }
1904
2043
  */
1905
2044
  runControls.set("running")
2045
+ } else if (packet.type === "launch.requirements.control") {
2046
+ settleLaunchRequirementsControl(packet)
1906
2047
  } else if (packet.type === 'resize') {
1907
2048
  this.resizeSync.handleResizePacket(packet)
1908
2049
  // } else if (packet.type === "key.set") {
@@ -2242,8 +2383,57 @@ document.addEventListener("DOMContentLoaded", async () => {
2242
2383
  // pageBottom.scrollIntoView()
2243
2384
  })
2244
2385
  }
2245
- async run (mode) {
2386
+ async showLatestLogFallback() {
2387
+ try {
2388
+ const scriptInfo = resolveCurrentApiScript()
2389
+ if (!scriptInfo) {
2390
+ return false
2391
+ }
2392
+ const status = await fetchJsonNoStore("/pinokio/home_status")
2393
+ const runningEntry = findRunningScriptEntry(status, scriptInfo)
2394
+ if (!runningEntry) {
2395
+ return false
2396
+ }
2397
+ const shells = await fetchJsonNoStore("/info/shells")
2398
+ if (hasLiveShellForScript(shells, runningEntry)) {
2399
+ return false
2400
+ }
2401
+ const logUrl = `/apps/logs/${encodeURIComponent(scriptInfo.appId)}?script=${encodeURIComponent(scriptInfo.script)}&tail=400`
2402
+ const logData = await fetchJsonNoStore(logUrl)
2403
+ const text = logData && typeof logData.text === "string" ? logData.text : ""
2404
+ if (!text.trim()) {
2405
+ return false
2406
+ }
2407
+ this.currentId = runningEntry.id || this.baseScriptId || null
2408
+ <% if (theme === "dark") { %>
2409
+ await this.createTerm(xtermTheme.FrontEndDelight)
2410
+ <% } else { %>
2411
+ await this.createTerm(xtermTheme.Tomorrow)
2412
+ <% } %>
2413
+ const fileLabel = logData.file || `logs/api/${scriptInfo.script}/latest`
2414
+ this.write(`Latest log: ${fileLabel}\r\nNo live terminal session is available for this running script.\r\n\r\n`)
2415
+ this.write(normalizeLogText(text))
2416
+ this.write("\r\n")
2417
+ document.querySelector("#status-window").innerHTML = "<b>Latest log</b><div class='flexible content'>No live terminal session</div>"
2418
+ document.querySelector("#progress-window").classList.add("hidden")
2419
+ document.querySelector("#progress-bar").style.width = "0%"
2420
+ this.latestLogFallbackActive = true
2421
+ runControls.set("running")
2422
+ return true
2423
+ } catch (_) {
2424
+ this.latestLogFallbackActive = false
2425
+ return false
2426
+ }
2427
+ }
2428
+ async run (mode, options = {}) {
2246
2429
  this.mode = (mode ? mode : "run")
2430
+ if (options && options.allowLatestLogFallback) {
2431
+ const showedFallback = await this.showLatestLogFallback()
2432
+ if (showedFallback) {
2433
+ return true
2434
+ }
2435
+ }
2436
+ this.latestLogFallbackActive = false
2247
2437
  const allowed = aiConsentRequired ? await consentManager.ensureAllowed() : true
2248
2438
  if (!allowed) {
2249
2439
  pluginTerminalDiscoveryRefresher.clear()
@@ -3086,7 +3276,7 @@ document.addEventListener("DOMContentLoaded", async () => {
3086
3276
  <% } else { %>
3087
3277
  <% if (run) { %>
3088
3278
  // run (query params run=true)
3089
- await rpc.run()
3279
+ await rpc.run(undefined, { allowLatestLogFallback: true })
3090
3280
  <% } else { %>
3091
3281
  await rpc.run("listen")
3092
3282
  <% } %>
@@ -3280,6 +3470,7 @@ const reloadMemory = async () => {
3280
3470
  </div>
3281
3471
  <% } else { %>
3282
3472
  <div class='terminal-container'>
3473
+ <div id="launch-requirements-status" class="launch-requirements-status" data-launch-requirements-status hidden aria-live="polite"></div>
3283
3474
  <% if (memory.length > 0) { %>
3284
3475
  <div class='memory'>
3285
3476
  <div class='memory-header'>
@@ -3334,5 +3525,8 @@ const reloadMemory = async () => {
3334
3525
  </a>
3335
3526
  </div>
3336
3527
  </div>
3528
+ <script>
3529
+ <%- include('partials/launch_requirements_status_client') %>
3530
+ </script>
3337
3531
  </body>
3338
3532
  </html>