pinokiod 5.1.34 → 5.1.36

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/git.js CHANGED
@@ -84,6 +84,17 @@ class Git {
84
84
  const offM = pad2(abs % 60)
85
85
  return `${y}-${m}-${day}T${hh}:${mm}:${ss}${sign}${offH}:${offM}`
86
86
  }
87
+ normalizeRepoPath(rawPath) {
88
+ if (typeof rawPath !== "string") return "."
89
+ let value = rawPath.trim()
90
+ if (!value) return "."
91
+ value = value.replace(/\\/g, "/").replace(/\/{2,}/g, "/")
92
+ if (value === "." || value === "./") return "."
93
+ if (value.startsWith("./")) {
94
+ value = value.slice(2)
95
+ }
96
+ return value || "."
97
+ }
87
98
  upsertCommitMeta(repoUrlNorm, sha, meta) {
88
99
  if (!repoUrlNorm || typeof repoUrlNorm !== "string") return false
89
100
  if (!this.isCommitSha(sha)) return false
@@ -231,7 +242,7 @@ class Git {
231
242
  const repos = []
232
243
  for (const repo of rawRepos) {
233
244
  if (!repo) continue
234
- const pathVal = typeof repo.path === "string" && repo.path.length > 0 ? repo.path : "."
245
+ const pathVal = this.normalizeRepoPath(repo.path)
235
246
  let remote = typeof repo.remote === "string" && repo.remote.length > 0 ? repo.remote : null
236
247
  if (!remote) remote = typeof repo.repo === "string" && repo.repo.length > 0 ? repo.repo : null
237
248
  const commit = typeof repo.commit === "string" && repo.commit.length > 0 ? repo.commit : null
@@ -559,6 +570,40 @@ class Git {
559
570
  await this.saveManifest()
560
571
  return true
561
572
  }
573
+ async deleteCheckpoint(remoteKey, checkpointId) {
574
+ if (!remoteKey || checkpointId == null) return { ok: false, error: "invalid" }
575
+ const apps = this.apps()
576
+ const entry = apps[remoteKey]
577
+ if (!entry || !Array.isArray(entry.checkpoints)) return { ok: false, error: "not_found" }
578
+ const idStr = String(checkpointId)
579
+ const idx = entry.checkpoints.findIndex((c) => c && String(c.id) === idStr)
580
+ if (idx < 0) return { ok: false, error: "not_found" }
581
+ const record = entry.checkpoints[idx]
582
+ entry.checkpoints.splice(idx, 1)
583
+ await this.saveManifest()
584
+ const hash = record && record.hash ? String(record.hash) : null
585
+ let fileDeleted = false
586
+ if (hash) {
587
+ let stillUsed = false
588
+ for (const entry of Object.values(apps)) {
589
+ if (!entry || !Array.isArray(entry.checkpoints)) continue
590
+ if (entry.checkpoints.some((c) => c && String(c.hash) === hash)) {
591
+ stillUsed = true
592
+ break
593
+ }
594
+ }
595
+ if (!stillUsed) {
596
+ const filePath = this.checkpointFilePath(hash)
597
+ if (filePath) {
598
+ try {
599
+ await fs.promises.rm(filePath, { force: true })
600
+ fileDeleted = true
601
+ } catch (_) {}
602
+ }
603
+ }
604
+ }
605
+ return { ok: true, hash, fileDeleted }
606
+ }
562
607
  async logCheckpointRestore(event) {
563
608
  const logEntry = {
564
609
  ts: Date.now(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "5.1.34",
3
+ "version": "5.1.36",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -5205,6 +5205,38 @@ class Server {
5205
5205
  }
5206
5206
  res.json({ ok: true, redirect: `/p/${encodeURIComponent(folder)}` })
5207
5207
  }))
5208
+ this.app.post("/checkpoints/delete", ex(async (req, res) => {
5209
+ const remoteRaw = typeof req.body.remote === 'string'
5210
+ ? req.body.remote.trim()
5211
+ : (typeof req.query.remote === 'string' ? req.query.remote.trim() : '')
5212
+ const remoteKeyRaw = typeof req.body.remoteKey === 'string'
5213
+ ? req.body.remoteKey.trim()
5214
+ : (typeof req.query.remoteKey === 'string' ? req.query.remoteKey.trim() : '')
5215
+ const snapshotRaw = Object.prototype.hasOwnProperty.call(req.body || {}, "snapshotId")
5216
+ ? req.body.snapshotId
5217
+ : (Object.prototype.hasOwnProperty.call(req.query || {}, "snapshotId") ? req.query.snapshotId : "")
5218
+ const snapshotId = snapshotRaw === 'latest'
5219
+ ? ''
5220
+ : (snapshotRaw == null ? '' : String(snapshotRaw))
5221
+ const remoteKey = remoteKeyRaw || (remoteRaw ? this.kernel.git.normalizeRemote(remoteRaw) : '')
5222
+ if (!snapshotId || !remoteKey) {
5223
+ res.status(400).json({ ok: false, error: "Missing parameters" })
5224
+ return
5225
+ }
5226
+ const result = await this.kernel.git.deleteCheckpoint(remoteKey, snapshotId)
5227
+ if (!result || !result.ok) {
5228
+ res.status(404).json({ ok: false, error: "Snapshot not found" })
5229
+ return
5230
+ }
5231
+ res.json({
5232
+ ok: true,
5233
+ deleted: {
5234
+ id: snapshotId,
5235
+ hash: result.hash || null,
5236
+ fileDeleted: !!result.fileDeleted
5237
+ }
5238
+ })
5239
+ }))
5208
5240
  this.app.get("/checkpoints/restore/:workspace/:snapshotId", ex(async (req, res) => {
5209
5241
  const workspace = typeof req.params.workspace === 'string' ? req.params.workspace : ''
5210
5242
  const snapshotId = req.params.snapshotId
@@ -8652,6 +8684,56 @@ class Server {
8652
8684
  }))
8653
8685
 
8654
8686
 
8687
+ this.app.get("/info/apps", ex(async (req, res) => {
8688
+ const apps = []
8689
+ try {
8690
+ const apipath = this.kernel.path("api")
8691
+ const entries = await fs.promises.readdir(apipath, { withFileTypes: true })
8692
+ for (const entry of entries) {
8693
+ let type
8694
+ try {
8695
+ type = await Util.file_type(apipath, entry)
8696
+ } catch (typeErr) {
8697
+ console.warn('Failed to inspect api entry', entry.name, typeErr)
8698
+ continue
8699
+ }
8700
+ if (!type || !type.directory) {
8701
+ continue
8702
+ }
8703
+ try {
8704
+ const meta = await this.kernel.api.meta(entry.name)
8705
+ apps.push({
8706
+ name: entry.name,
8707
+ title: meta && meta.title ? meta.title : entry.name,
8708
+ description: meta && meta.description ? meta.description : '',
8709
+ icon: meta && meta.icon ? meta.icon : "/pinokio-black.png"
8710
+ })
8711
+ } catch (metaError) {
8712
+ console.warn('Failed to load app metadata', entry.name, metaError)
8713
+ apps.push({
8714
+ name: entry.name,
8715
+ title: entry.name,
8716
+ description: '',
8717
+ icon: "/pinokio-black.png"
8718
+ })
8719
+ }
8720
+ }
8721
+ } catch (enumerationError) {
8722
+ console.warn('Failed to enumerate api apps for url dropdown', enumerationError)
8723
+ }
8724
+
8725
+ apps.sort((a, b) => {
8726
+ const at = (a.title || a.name || '').toLowerCase()
8727
+ const bt = (b.title || b.name || '').toLowerCase()
8728
+ if (at < bt) return -1
8729
+ if (at > bt) return 1
8730
+ return (a.name || '').localeCompare(b.name || '')
8731
+ })
8732
+
8733
+ res.json({ apps })
8734
+ }))
8735
+
8736
+
8655
8737
  this.app.get("/info/procs", ex(async (req, res) => {
8656
8738
  await this.kernel.processes.refresh()
8657
8739
 
@@ -25,8 +25,25 @@ document.addEventListener("DOMContentLoaded", () => {
25
25
  return;
26
26
  }
27
27
 
28
+ const homeIcon = homeLink ? homeLink.querySelector("img.icon") : null;
29
+ const ensureHomeExpandIcon = () => {
30
+ if (!homeLink || !homeIcon) {
31
+ return null;
32
+ }
33
+ let icon = homeLink.querySelector(".home-expand-icon");
34
+ if (!icon) {
35
+ icon = document.createElement("i");
36
+ icon.className = "fa-solid fa-expand home-expand-icon";
37
+ icon.setAttribute("aria-hidden", "true");
38
+ homeLink.appendChild(icon);
39
+ }
40
+ return icon;
41
+ };
42
+ ensureHomeExpandIcon();
43
+
28
44
  // Helper functions used during initial restore must be defined before use
29
- const MIN_MARGIN = 8;
45
+ const MIN_MARGIN = 0;
46
+ const LEGACY_MARGIN = 8;
30
47
 
31
48
  function clampPosition(left, top, sizeOverride) {
32
49
  const rect = header.getBoundingClientRect();
@@ -168,19 +185,24 @@ document.addEventListener("DOMContentLoaded", () => {
168
185
  // Restore persisted or respect DOM state on load (per path, per session)
169
186
  const persisted = readPersisted();
170
187
  const restoreFromStorage = !!(persisted && persisted.minimized);
188
+ const hasStoredPosition = !!(persisted && Number.isFinite(persisted.left) && Number.isFinite(persisted.top));
189
+ const isLegacyDefault = hasStoredPosition
190
+ && Math.abs(persisted.left - LEGACY_MARGIN) < 0.5
191
+ && Math.abs(persisted.top - LEGACY_MARGIN) < 0.5;
192
+ const useStoredPosition = restoreFromStorage && hasStoredPosition && !isLegacyDefault;
171
193
  const domIsMinimized = header.classList.contains("minimized");
172
194
  if (restoreFromStorage || domIsMinimized) {
173
195
  header.classList.add("minimized");
174
196
  // Use minimized size for clamping/positioning
175
197
  const size = measureRect((clone) => { clone.classList.add("minimized"); });
176
- const fallbackLeft = Math.max(MIN_MARGIN, window.innerWidth - size.width - MIN_MARGIN);
177
- const fallbackTop = Math.max(MIN_MARGIN, window.innerHeight - size.height - MIN_MARGIN);
178
- const left = restoreFromStorage && Number.isFinite(persisted.left) ? persisted.left : fallbackLeft;
179
- const top = restoreFromStorage && Number.isFinite(persisted.top) ? persisted.top : fallbackTop;
198
+ const fallbackLeft = MIN_MARGIN;
199
+ const fallbackTop = MIN_MARGIN;
200
+ const left = useStoredPosition ? persisted.left : fallbackLeft;
201
+ const top = useStoredPosition ? persisted.top : fallbackTop;
180
202
  const clamped = clampPosition(left, top, size);
181
203
  state.lastLeft = clamped.left;
182
204
  state.lastTop = clamped.top;
183
- state.hasCustomPosition = restoreFromStorage;
205
+ state.hasCustomPosition = useStoredPosition;
184
206
  state.minimized = true;
185
207
  // Apply immediately and once after layout settles
186
208
  applyPosition(clamped.left, clamped.top);
@@ -234,8 +256,8 @@ document.addEventListener("DOMContentLoaded", () => {
234
256
  clone.classList.add("minimized");
235
257
  });
236
258
 
237
- const defaultLeft = Math.max(MIN_MARGIN, window.innerWidth - minimizedSize.width - MIN_MARGIN);
238
- const defaultTop = Math.max(MIN_MARGIN, window.innerHeight - minimizedSize.height - MIN_MARGIN);
259
+ const defaultLeft = MIN_MARGIN;
260
+ const defaultTop = MIN_MARGIN;
239
261
  const targetLeft = state.hasCustomPosition ? state.lastLeft : defaultLeft;
240
262
  const targetTop = state.hasCustomPosition ? state.lastTop : defaultTop;
241
263
 
@@ -2978,7 +2978,6 @@ header.navheader.minimized {
2978
2978
  height: auto;
2979
2979
  max-height: none;
2980
2980
  padding: 4px 8px;
2981
- border-radius: 12px;
2982
2981
  background: var(--light-nav-bg);
2983
2982
  box-shadow: 0 8px 18px rgba(0, 0, 0, 0.14), 0 2px 6px rgba(0, 0, 0, 0.08);
2984
2983
  display: inline-flex;
@@ -3052,9 +3051,23 @@ header.navheader.minimized .home {
3052
3051
  padding: 0;
3053
3052
  }
3054
3053
  header.navheader.minimized .home .icon {
3054
+ display: none;
3055
3055
  width: 24px;
3056
3056
  height: 24px;
3057
3057
  }
3058
+ header.navheader .home .home-expand-icon {
3059
+ display: none;
3060
+ }
3061
+ header.navheader.minimized .home .home-expand-icon {
3062
+ display: inline-flex;
3063
+ width: 24px;
3064
+ height: 24px;
3065
+ align-items: center;
3066
+ justify-content: center;
3067
+ font-size: 16px;
3068
+ line-height: 1;
3069
+ color: inherit;
3070
+ }
3058
3071
 
3059
3072
 
3060
3073
  header.navheader.transitioning {
@@ -132,3 +132,37 @@ body.dark .tab-link-popover .tab-link-popover-footer:hover,
132
132
  body.dark .tab-link-popover .tab-link-popover-footer:focus-visible {
133
133
  background: rgba(147, 197, 253, 0.35);
134
134
  }
135
+ .appcanvas > aside .menu-container .tab-link-popover-host {
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 8px;
139
+ }
140
+ .appcanvas > aside .menu-container .tab-link-popover-trigger {
141
+ display: inline-flex;
142
+ align-items: center;
143
+ justify-content: center;
144
+ width: 18px;
145
+ height: 18px;
146
+ border-radius: 6px;
147
+ flex: 0 0 auto;
148
+ color: inherit;
149
+ opacity: 0.55;
150
+ cursor: pointer;
151
+ transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease;
152
+ }
153
+ .appcanvas > aside .menu-container .tab-link-popover-trigger i {
154
+ font-size: 14px;
155
+ }
156
+ .appcanvas > aside .menu-container .frame-link:hover .tab-link-popover-trigger,
157
+ .appcanvas > aside .menu-container .frame-link:focus-within .tab-link-popover-trigger {
158
+ opacity: 0.85;
159
+ }
160
+ .appcanvas > aside .menu-container .tab-link-popover-trigger:hover,
161
+ .appcanvas > aside .menu-container .tab-link-popover-trigger:focus-visible {
162
+ background: rgba(15, 23, 42, 0.08);
163
+ opacity: 1;
164
+ }
165
+ body.dark .appcanvas > aside .menu-container .tab-link-popover-trigger:hover,
166
+ body.dark .appcanvas > aside .menu-container .tab-link-popover-trigger:focus-visible {
167
+ background: rgba(148, 163, 184, 0.2);
168
+ }
@@ -10,6 +10,40 @@
10
10
  let tabLinkRouterHttpsActive = null
11
11
  let tabLinkPeerInfoPromise = null
12
12
  let tabLinkPeerInfoExpiry = 0
13
+ const TAB_LINK_TRIGGER_CLASS = "tab-link-popover-trigger"
14
+ const TAB_LINK_TRIGGER_HOST_CLASS = "tab-link-popover-host"
15
+
16
+ const shouldAttachTabLinkTrigger = (link) => {
17
+ if (!link || !link.classList || !link.classList.contains("frame-link")) {
18
+ return false
19
+ }
20
+ if (!link.hasAttribute("href")) {
21
+ return false
22
+ }
23
+ const href = link.getAttribute("href")
24
+ return typeof href === "string" && href.trim().length > 0
25
+ }
26
+
27
+ const createTabLinkTrigger = () => {
28
+ const trigger = document.createElement("span")
29
+ trigger.className = TAB_LINK_TRIGGER_CLASS
30
+ trigger.setAttribute("role", "button")
31
+ trigger.setAttribute("tabindex", "0")
32
+ trigger.setAttribute("aria-label", "Open in browser")
33
+ trigger.innerHTML = '<i class="fa-solid fa-arrow-up-right-from-square"></i>'
34
+ return trigger
35
+ }
36
+
37
+ const ensureTabLinkTrigger = (link) => {
38
+ if (!shouldAttachTabLinkTrigger(link)) {
39
+ return
40
+ }
41
+ if (link.querySelector(`.${TAB_LINK_TRIGGER_CLASS}`)) {
42
+ return
43
+ }
44
+ link.classList.add(TAB_LINK_TRIGGER_HOST_CLASS)
45
+ link.appendChild(createTabLinkTrigger())
46
+ }
13
47
 
14
48
  const ensureTabLinkPopoverEl = () => {
15
49
  if (!tabLinkPopoverEl) {
@@ -38,7 +72,12 @@
38
72
  if (targetMode === "_self") {
39
73
  window.location.assign(url)
40
74
  } else {
41
- window.open(url, "_blank", "browser")
75
+ const agent = document.body ? document.body.getAttribute("data-agent") : null
76
+ if (agent === "electron") {
77
+ window.open(url, "_blank", "browser")
78
+ } else {
79
+ window.open(url, "_blank")
80
+ }
42
81
  // fetch("/go", {
43
82
  // method: "POST",
44
83
  // headers: {
@@ -1611,27 +1650,80 @@
1611
1650
  if (!container) {
1612
1651
  return
1613
1652
  }
1653
+ if (container.dataset.tabLinkPopoverReady === "1") {
1654
+ return
1655
+ }
1656
+ container.dataset.tabLinkPopoverReady = "1"
1614
1657
 
1615
- container.addEventListener("mouseover", (event) => {
1616
- const link = event.target.closest(".frame-link")
1617
- if (!link || !container.contains(link)) {
1658
+ const ensureTriggers = (root) => {
1659
+ if (!root || !root.querySelectorAll) {
1618
1660
  return
1619
1661
  }
1620
- renderTabLinkPopover(link, { requireAlternate: false })
1662
+ root.querySelectorAll(".frame-link").forEach((link) => {
1663
+ ensureTabLinkTrigger(link)
1664
+ })
1665
+ }
1666
+
1667
+ ensureTriggers(container)
1668
+
1669
+ const observer = new MutationObserver((mutations) => {
1670
+ mutations.forEach((mutation) => {
1671
+ mutation.addedNodes.forEach((node) => {
1672
+ if (!node || node.nodeType !== 1) {
1673
+ return
1674
+ }
1675
+ if (node.classList && node.classList.contains("frame-link")) {
1676
+ ensureTabLinkTrigger(node)
1677
+ }
1678
+ if (node.querySelectorAll) {
1679
+ node.querySelectorAll(".frame-link").forEach((link) => {
1680
+ ensureTabLinkTrigger(link)
1681
+ })
1682
+ }
1683
+ })
1684
+ })
1621
1685
  })
1686
+ observer.observe(container, { childList: true, subtree: true })
1622
1687
 
1623
- container.addEventListener("mouseout", (event) => {
1624
- const origin = event.target.closest(".frame-link")
1625
- if (!origin || !container.contains(origin)) {
1688
+ const togglePopoverForLink = (link) => {
1689
+ if (!link) {
1626
1690
  return
1627
1691
  }
1628
- const related = event.relatedTarget
1629
1692
  const popover = tabLinkPopoverEl || document.getElementById(TAB_LINK_POPOVER_ID)
1630
- if (related && (origin.contains(related) || (popover && popover.contains(related)))) {
1693
+ if (tabLinkActiveLink === link && popover && popover.classList.contains("visible")) {
1694
+ hideTabLinkPopover({ immediate: true })
1631
1695
  return
1632
1696
  }
1633
- hideTabLinkPopover()
1634
- })
1697
+ renderTabLinkPopover(link, { requireAlternate: false })
1698
+ }
1699
+
1700
+ const handleTriggerClick = (event) => {
1701
+ const trigger = event.target.closest(`.${TAB_LINK_TRIGGER_CLASS}`)
1702
+ if (!trigger || !container.contains(trigger)) {
1703
+ return
1704
+ }
1705
+ event.preventDefault()
1706
+ event.stopPropagation()
1707
+ const link = trigger.closest(".frame-link")
1708
+ togglePopoverForLink(link)
1709
+ }
1710
+
1711
+ const handleTriggerKeydown = (event) => {
1712
+ if (event.key !== "Enter" && event.key !== " ") {
1713
+ return
1714
+ }
1715
+ const trigger = event.target.closest(`.${TAB_LINK_TRIGGER_CLASS}`)
1716
+ if (!trigger || !container.contains(trigger)) {
1717
+ return
1718
+ }
1719
+ event.preventDefault()
1720
+ event.stopPropagation()
1721
+ const link = trigger.closest(".frame-link")
1722
+ togglePopoverForLink(link)
1723
+ }
1724
+
1725
+ container.addEventListener("click", handleTriggerClick, true)
1726
+ container.addEventListener("keydown", handleTriggerKeydown, true)
1635
1727
  }
1636
1728
 
1637
1729
  const handleGlobalPointer = (event) => {