@baique/pi-web-sky 0.1.35 → 0.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.
Files changed (294) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +16 -16
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/react-loadable-manifest.json +1 -1
  5. package/.next/required-server-files.js +1 -1
  6. package/.next/required-server-files.json +1 -1
  7. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_not-found.html +1 -1
  15. package/.next/server/app/_not-found.rsc +3 -3
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  17. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
  18. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  19. package/.next/server/app/api/agent/[id]/bash-output/route.js +1 -1
  20. package/.next/server/app/api/agent/[id]/events/route.js +1 -1
  21. package/.next/server/app/api/agent/[id]/route.js +1 -1
  22. package/.next/server/app/api/app-update/route.js +1 -1
  23. package/.next/server/app/api/auth/login/[provider]/route.js +1 -1
  24. package/.next/server/app/api/cwd/validate/route.js +1 -1
  25. package/.next/server/app/api/default-cwd/route.js +1 -1
  26. package/.next/server/app/api/file-index/route.js +1 -1
  27. package/.next/server/app/api/files/[...path]/route.js +1 -1
  28. package/.next/server/app/api/mcp/route.js +1 -1
  29. package/.next/server/app/api/models/route.js +1 -1
  30. package/.next/server/app/api/plugins/route.js +1 -1
  31. package/.next/server/app/api/project-trust/route.js +1 -1
  32. package/.next/server/app/api/search/route.js +1 -1
  33. package/.next/server/app/api/sessions/[id]/auto-name/route.js +1 -1
  34. package/.next/server/app/api/sessions/[id]/context/route.js +1 -1
  35. package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +1 -1
  36. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  37. package/.next/server/app/api/sessions/[id]/state/route.js +1 -1
  38. package/.next/server/app/api/sessions/[id]/todos/route.js +1 -1
  39. package/.next/server/app/api/sessions/route.js +1 -1
  40. package/.next/server/app/api/skills/check/route.js +1 -1
  41. package/.next/server/app/api/skills/check/route.js.nft.json +1 -1
  42. package/.next/server/app/api/skills/install/route.js +1 -1
  43. package/.next/server/app/api/skills/route.js +2 -31
  44. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  45. package/.next/server/app/api/skills/update/route.js +1 -1
  46. package/.next/server/app/api/skills/update/route.js.nft.json +1 -1
  47. package/.next/server/app/api/worktrees/route.js +1 -1
  48. package/.next/server/app/index.html +1 -1
  49. package/.next/server/app/index.rsc +5 -5
  50. package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
  51. package/.next/server/app/index.segments/_full.segment.rsc +5 -5
  52. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  53. package/.next/server/app/page.js +2 -2
  54. package/.next/server/app/page_client-reference-manifest.js +1 -1
  55. package/.next/server/app-paths-manifest.json +16 -16
  56. package/.next/server/chunks/2404.js +1 -1
  57. package/.next/server/chunks/3500.js +30 -0
  58. package/.next/server/chunks/4226.js +2 -2
  59. package/.next/server/chunks/542.js +10 -5
  60. package/.next/server/chunks/5794.js +1 -1
  61. package/.next/server/chunks/6429.js +1 -1
  62. package/.next/server/chunks/898.js +1 -1
  63. package/.next/server/middleware-build-manifest.js +1 -1
  64. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  65. package/.next/server/pages/404.html +1 -1
  66. package/.next/server/pages/500.html +1 -1
  67. package/.next/static/chunks/5315.7e72d26a3ad3bd9a.js +4 -0
  68. package/.next/static/chunks/app/{layout-1d9a7a44fbc92d2e.js → layout-ebbc0d287267b07b.js} +1 -1
  69. package/.next/static/chunks/app/{page-6d6779a81a0a7639.js → page-e568623a44c67606.js} +2 -2
  70. package/.next/static/chunks/{webpack-fa4f40085b1ec1ac.js → webpack-2e4e348867a555cd.js} +1 -1
  71. package/.next/static/css/422c7dc0faa04d89.css +1 -0
  72. package/.next/trace +8 -9
  73. package/.next/trace-build +1 -1
  74. package/bin/pi-web-sky.js +15 -20
  75. package/lib/agent-client.test.mjs +64 -0
  76. package/lib/agent-client.ts +53 -0
  77. package/lib/agent-event-connection.test.mjs +193 -0
  78. package/lib/agent-event-connection.ts +202 -0
  79. package/lib/agent-event-stream.test.mjs +155 -0
  80. package/lib/agent-event-stream.ts +132 -0
  81. package/lib/agent-event-wire.test.mjs +278 -0
  82. package/lib/agent-event-wire.ts +107 -0
  83. package/lib/agent-phase.ts +24 -0
  84. package/lib/allowed-roots.ts +31 -0
  85. package/lib/ansi.test.mjs +52 -0
  86. package/lib/ansi.ts +211 -0
  87. package/lib/api-types.ts +139 -0
  88. package/lib/app-update.test.mjs +27 -0
  89. package/lib/app-update.ts +28 -0
  90. package/lib/atomic-file.test.mjs +37 -0
  91. package/lib/atomic-file.ts +34 -0
  92. package/lib/audit-session.ts +235 -0
  93. package/lib/bash-output.test.mjs +63 -0
  94. package/lib/bash-output.ts +53 -0
  95. package/lib/bg-image.ts +129 -0
  96. package/lib/board-events.ts +83 -0
  97. package/lib/board-purge.test.mjs +66 -0
  98. package/lib/board-purge.ts +65 -0
  99. package/lib/board-search.test.mjs +110 -0
  100. package/lib/board-search.ts +63 -0
  101. package/lib/board-store.test.mjs +542 -0
  102. package/lib/board-store.ts +794 -0
  103. package/lib/board-types.ts +96 -0
  104. package/lib/board-utils.ts +17 -0
  105. package/lib/bounded-form-data.test.mjs +54 -0
  106. package/lib/bounded-form-data.ts +49 -0
  107. package/lib/browser-notifications.test.mjs +167 -0
  108. package/lib/browser-notifications.ts +104 -0
  109. package/lib/chat-lazy-load.test.mjs +140 -0
  110. package/lib/chat-lazy-load.ts +64 -0
  111. package/lib/clipboard-paths.test.mjs +140 -0
  112. package/lib/clipboard-paths.ts +192 -0
  113. package/lib/clipboard.ts +18 -0
  114. package/lib/compaction-summary.test.mjs +44 -0
  115. package/lib/compaction-summary.ts +34 -0
  116. package/lib/custom-ui-terminal.test.mjs +32 -0
  117. package/lib/custom-ui-terminal.ts +27 -0
  118. package/lib/deepseek-pricing.test.mjs +120 -0
  119. package/lib/deepseek-pricing.ts +71 -0
  120. package/lib/directory-browser.test.mjs +60 -0
  121. package/lib/directory-browser.ts +83 -0
  122. package/lib/draft-stash.test.mjs +91 -0
  123. package/lib/draft-stash.ts +102 -0
  124. package/lib/draft-store.ts +105 -0
  125. package/lib/dropdown-direction.ts +27 -0
  126. package/lib/extension-command.test.mjs +63 -0
  127. package/lib/extension-command.ts +81 -0
  128. package/lib/file-access.test.mjs +31 -0
  129. package/lib/file-access.ts +59 -0
  130. package/lib/file-dirent.test.mjs +56 -0
  131. package/lib/file-dirent.ts +17 -0
  132. package/lib/file-explorer-state.test.mjs +63 -0
  133. package/lib/file-explorer-state.ts +36 -0
  134. package/lib/file-fuzzy.test.mjs +29 -0
  135. package/lib/file-fuzzy.ts +189 -0
  136. package/lib/file-links.test.mjs +92 -0
  137. package/lib/file-links.ts +148 -0
  138. package/lib/file-paths.ts +43 -0
  139. package/lib/file-types.test.mjs +34 -0
  140. package/lib/file-types.ts +73 -0
  141. package/lib/file-upload.test.mjs +70 -0
  142. package/lib/file-upload.ts +59 -0
  143. package/lib/file-viewer-state.test.mjs +20 -0
  144. package/lib/file-viewer-state.ts +15 -0
  145. package/lib/frontmatter.test.mjs +55 -0
  146. package/lib/frontmatter.ts +87 -0
  147. package/lib/git-changes.test.mjs +52 -0
  148. package/lib/git-changes.ts +232 -0
  149. package/lib/git-status.ts +47 -0
  150. package/lib/git-types.ts +29 -0
  151. package/lib/http-dispatcher.test.mjs +83 -0
  152. package/lib/http-dispatcher.ts +86 -0
  153. package/lib/i18n/format.test.mjs +19 -0
  154. package/lib/i18n/format.ts +60 -0
  155. package/lib/i18n/messages/en.ts +662 -0
  156. package/lib/i18n/messages/zh-CN.ts +661 -0
  157. package/lib/i18n/registry.test.mjs +32 -0
  158. package/lib/i18n/registry.ts +43 -0
  159. package/lib/i18n/types.ts +15 -0
  160. package/lib/image-attachments.test.mjs +27 -0
  161. package/lib/image-attachments.ts +56 -0
  162. package/lib/initial-navigation.test.mjs +65 -0
  163. package/lib/initial-navigation.ts +15 -0
  164. package/lib/markdown.test.mjs +104 -0
  165. package/lib/markdown.ts +347 -0
  166. package/lib/message-display.test.mjs +130 -0
  167. package/lib/message-display.ts +47 -0
  168. package/lib/model-catalog.test.mjs +327 -0
  169. package/lib/model-catalog.ts +414 -0
  170. package/lib/model-discovery-auth.ts +58 -0
  171. package/lib/model-discovery.test.mjs +50 -0
  172. package/lib/model-discovery.ts +75 -0
  173. package/lib/model-id-preference.ts +46 -0
  174. package/lib/model-scope.test.mjs +193 -0
  175. package/lib/model-scope.ts +198 -0
  176. package/lib/models-cache.test.mjs +122 -0
  177. package/lib/models-cache.ts +90 -0
  178. package/lib/models-config-store.test.mjs +200 -0
  179. package/lib/models-config-store.ts +84 -0
  180. package/lib/next-config-esm.test.mjs +14 -0
  181. package/lib/next-config.test.mjs +13 -0
  182. package/lib/node-version.test.mjs +33 -0
  183. package/lib/normalize.test.mjs +54 -0
  184. package/lib/normalize.ts +63 -0
  185. package/lib/npx.ts +62 -0
  186. package/lib/panel-layout.test.mjs +56 -0
  187. package/lib/panel-layout.ts +52 -0
  188. package/lib/patch.test.mjs +110 -0
  189. package/lib/patch.ts +134 -0
  190. package/lib/path-security.ts +42 -0
  191. package/lib/paths.test.mjs +58 -0
  192. package/lib/paths.ts +71 -0
  193. package/lib/pi-types.ts +183 -0
  194. package/lib/pi-web-options.test.mjs +63 -0
  195. package/lib/process-lifecycle.test.mjs +83 -0
  196. package/lib/project-command-env.test.mjs +298 -0
  197. package/lib/project-command-env.ts +129 -0
  198. package/lib/project-groups.test.mjs +60 -0
  199. package/lib/project-groups.ts +53 -0
  200. package/lib/project-identity.test.mjs +37 -0
  201. package/lib/project-identity.ts +26 -0
  202. package/lib/project-tree.test.mjs +115 -0
  203. package/lib/project-tree.ts +176 -0
  204. package/lib/project-trust.test.mjs +117 -0
  205. package/lib/project-trust.ts +48 -0
  206. package/lib/prompt-recovery.test.mjs +33 -0
  207. package/lib/prompt-recovery.ts +47 -0
  208. package/lib/provider-api-key-route.test.mjs +12 -0
  209. package/lib/provider-credential-store.test.mjs +126 -0
  210. package/lib/provider-credential-store.ts +114 -0
  211. package/lib/provider-listing-runtime.ts +37 -0
  212. package/lib/provider-listing.test.mjs +114 -0
  213. package/lib/provider-listing.ts +118 -0
  214. package/lib/provider-services.ts +64 -0
  215. package/lib/request-security.test.mjs +256 -0
  216. package/lib/request-security.ts +136 -0
  217. package/lib/rpc-manager-shutdown.test.mjs +372 -0
  218. package/lib/rpc-manager-widgets.test.mjs +342 -0
  219. package/lib/rpc-manager.test.mjs +139 -0
  220. package/lib/rpc-manager.ts +1703 -0
  221. package/lib/rpc-session-info.test.mjs +93 -0
  222. package/lib/session-delete.test.mjs +53 -0
  223. package/lib/session-delete.ts +86 -0
  224. package/lib/session-file-references-core.ts +82 -0
  225. package/lib/session-file-references.test.mjs +91 -0
  226. package/lib/session-file-references.ts +29 -0
  227. package/lib/session-path.test.mjs +24 -0
  228. package/lib/session-path.ts +11 -0
  229. package/lib/session-reader.pagination.test.mjs +115 -0
  230. package/lib/session-reader.test.mjs +446 -0
  231. package/lib/session-reader.ts +548 -0
  232. package/lib/session-resolve.test.mjs +65 -0
  233. package/lib/session-row-context-menu.test.mjs +46 -0
  234. package/lib/session-row-context-menu.ts +29 -0
  235. package/lib/session-scanner.test.mjs +151 -0
  236. package/lib/session-scanner.ts +238 -0
  237. package/lib/session-search.test.mjs +173 -0
  238. package/lib/session-search.ts +284 -0
  239. package/lib/session-stats.ts +26 -0
  240. package/lib/session-timing.test.mjs +107 -0
  241. package/lib/session-timing.ts +45 -0
  242. package/lib/session-title.test.mjs +284 -0
  243. package/lib/session-title.ts +255 -0
  244. package/lib/skill-frontmatter.test.mjs +75 -0
  245. package/lib/skill-frontmatter.ts +40 -0
  246. package/lib/skill-lock.test.mjs +182 -0
  247. package/lib/skill-lock.ts +146 -0
  248. package/lib/skill-updates.test.mjs +182 -0
  249. package/lib/skill-updates.ts +263 -0
  250. package/lib/skills-service.ts +16 -0
  251. package/lib/slash-display.test.mjs +122 -0
  252. package/lib/slash-display.ts +30 -0
  253. package/lib/sqlite-db.test.mjs +140 -0
  254. package/lib/sqlite-db.ts +205 -0
  255. package/lib/startup-preferences.test.mjs +130 -0
  256. package/lib/startup-preferences.ts +65 -0
  257. package/lib/streaming-message.test.mjs +212 -0
  258. package/lib/streaming-message.ts +151 -0
  259. package/lib/subagent-widget.test.mjs +87 -0
  260. package/lib/subagent-widget.ts +109 -0
  261. package/lib/sync-room-server.mjs +137 -0
  262. package/lib/task-card-store.test.mjs +309 -0
  263. package/lib/task-card-store.ts +559 -0
  264. package/lib/task-scheduler.ts +498 -0
  265. package/lib/task-store.test.mjs +216 -0
  266. package/lib/task-store.ts +271 -0
  267. package/lib/terminal-input.test.mjs +46 -0
  268. package/lib/terminal-input.ts +63 -0
  269. package/lib/terminal-manager.test.mjs +71 -0
  270. package/lib/terminal-manager.ts +245 -0
  271. package/lib/tool-execution-progress.test.mjs +32 -0
  272. package/lib/tool-execution-progress.ts +29 -0
  273. package/lib/tool-names.ts +25 -0
  274. package/lib/tool-preset-preference.test.mjs +72 -0
  275. package/lib/tool-preset-preference.ts +41 -0
  276. package/lib/tool-presets.test.mjs +56 -0
  277. package/lib/tool-presets.ts +42 -0
  278. package/lib/turn-written-files.test.mjs +137 -0
  279. package/lib/turn-written-files.ts +60 -0
  280. package/lib/types.ts +349 -0
  281. package/lib/wallpaper-settings.ts +191 -0
  282. package/lib/web-auth.test.mjs +50 -0
  283. package/lib/web-auth.ts +45 -0
  284. package/lib/workspace-memory.test.mjs +129 -0
  285. package/lib/workspace-memory.ts +98 -0
  286. package/lib/worktree.test.mjs +50 -0
  287. package/lib/worktree.ts +250 -0
  288. package/package.json +3 -1
  289. package/server.mjs +34 -0
  290. package/.next/server/chunks/7513.js +0 -30
  291. package/.next/static/chunks/5315.03de3c6abd8b283e.js +0 -4
  292. package/.next/static/css/cf25164cb7c04d95.css +0 -1
  293. /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_buildManifest.js +0 -0
  294. /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_ssgManifest.js +0 -0
@@ -0,0 +1,148 @@
1
+ function safeDecode(value: string): string {
2
+ try {
3
+ return decodeURIComponent(value);
4
+ } catch {
5
+ return value;
6
+ }
7
+ }
8
+
9
+ function normalizeFilePathSlashes(filePath: string): string {
10
+ if (/^[a-zA-Z]:[\\/]/.test(filePath) || filePath.startsWith("\\\\")) {
11
+ return filePath.replace(/\\/g, "/");
12
+ }
13
+ return filePath;
14
+ }
15
+
16
+ function stripLineSuffix(filePath: string): string {
17
+ return filePath.replace(/:\d+(?::\d+)?$/, "");
18
+ }
19
+
20
+ function normalizeLocalPath(filePath: string): string {
21
+ const normalized = normalizeFilePathSlashes(filePath);
22
+ const isWindowsDrive = /^[a-zA-Z]:\//.test(normalized);
23
+ const isUnc = normalized.startsWith("//");
24
+ const leadingSlash = normalized.startsWith("/") && !isWindowsDrive && !isUnc;
25
+ const parts: string[] = [];
26
+
27
+ for (const part of normalized.split("/")) {
28
+ if (!part || part === ".") continue;
29
+ if (part === "..") {
30
+ if (parts.length > 0 && parts[parts.length - 1] !== "..") {
31
+ parts.pop();
32
+ } else if (!leadingSlash && !isWindowsDrive && !isUnc) {
33
+ parts.push(part);
34
+ }
35
+ continue;
36
+ }
37
+ parts.push(part);
38
+ }
39
+
40
+ const joined = parts.join("/");
41
+ if (isWindowsDrive) return joined;
42
+ if (isUnc) return `//${joined}`;
43
+ return leadingSlash ? `/${joined}` : joined;
44
+ }
45
+
46
+ function isPathInside(candidate: string, root: string): boolean {
47
+ const normalizedCandidate = normalizeLocalPath(candidate).replace(/\/+$/, "");
48
+ const normalizedRoot = normalizeLocalPath(root).replace(/\/+$/, "");
49
+ const useCaseInsensitive = /^[a-zA-Z]:\//.test(normalizedCandidate) || /^[a-zA-Z]:\//.test(normalizedRoot);
50
+ const filePath = useCaseInsensitive ? normalizedCandidate.toLowerCase() : normalizedCandidate;
51
+ const rootPath = useCaseInsensitive ? normalizedRoot.toLowerCase() : normalizedRoot;
52
+ return filePath === rootPath || filePath.startsWith(`${rootPath}/`);
53
+ }
54
+
55
+ function looksLikeRelativeFileHref(href: string): boolean {
56
+ if (href.startsWith("#") || href.startsWith("?")) return false;
57
+ if (href.startsWith("./") || href.startsWith("../")) return true;
58
+ if (href.includes("/")) return true;
59
+ return /(^|\/)\.?[^/]+\.[^/.]+$/.test(href);
60
+ }
61
+
62
+ function fileUrlToPath(href: string): string | null {
63
+ try {
64
+ const url = new URL(href);
65
+ if (url.protocol !== "file:") return null;
66
+ const pathname = safeDecode(url.pathname);
67
+ if (url.hostname) {
68
+ return `//${url.hostname}${pathname.startsWith("/") ? pathname : `/${pathname}`}`;
69
+ }
70
+ if (/^\/[a-zA-Z]:\//.test(pathname)) return pathname.slice(1);
71
+ return pathname;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ export function resolveLocalFileHref(
78
+ href: string | undefined,
79
+ baseDir?: string,
80
+ relativeRoot = baseDir,
81
+ ): string | null {
82
+ if (!href) return null;
83
+
84
+ const cleanHref = href.split("#", 1)[0].split("?", 1)[0].trim();
85
+ if (!cleanHref) return null;
86
+
87
+ let candidate: string | null = null;
88
+ let candidateKind: "absolute" | "relative" | null = null;
89
+ const decodedHref = safeDecode(cleanHref);
90
+ const isBackslashUncPath = decodedHref.startsWith("\\\\");
91
+ const normalizedHref = normalizeFilePathSlashes(decodedHref);
92
+ const lowerHref = normalizedHref.toLowerCase();
93
+
94
+ if (lowerHref.startsWith("/api/") || lowerHref.startsWith("/_next/")) return null;
95
+ if (!isBackslashUncPath && normalizedHref.startsWith("//")) return null;
96
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/i.test(normalizedHref) && !lowerHref.startsWith("file:") && !/^[a-zA-Z]:\//.test(normalizedHref)) {
97
+ return null;
98
+ }
99
+
100
+ if (lowerHref.startsWith("file:")) {
101
+ candidate = fileUrlToPath(normalizedHref);
102
+ candidateKind = candidate ? "absolute" : null;
103
+ } else if (/^[a-zA-Z]:\//.test(normalizedHref)) {
104
+ candidate = normalizedHref;
105
+ candidateKind = "absolute";
106
+ } else if (normalizedHref.startsWith("/")) {
107
+ candidate = normalizedHref;
108
+ candidateKind = "absolute";
109
+ } else if (baseDir && looksLikeRelativeFileHref(normalizedHref)) {
110
+ candidate = `${normalizeFilePathSlashes(baseDir).replace(/\/+$/, "")}/${normalizedHref}`;
111
+ candidateKind = "relative";
112
+ }
113
+
114
+ if (!candidate) return null;
115
+
116
+ const filePath = stripLineSuffix(normalizeLocalPath(candidate));
117
+ if (candidateKind === "relative" && relativeRoot && !isPathInside(filePath, relativeRoot)) return null;
118
+ return filePath;
119
+ }
120
+
121
+ /** Resolve a filesystem path without applying URL or source-location syntax. */
122
+ export function resolveLocalFilePath(filePath: string | undefined, baseDir?: string): string | null {
123
+ if (!filePath) return null;
124
+
125
+ const windowsStyle = /^[a-zA-Z]:[\\/]/.test(filePath) ||
126
+ filePath.startsWith("\\\\") ||
127
+ (baseDir !== undefined && (/^[a-zA-Z]:[\\/]/.test(baseDir) || baseDir.startsWith("\\\\")));
128
+ const normalizeSlashes = (value: string) => windowsStyle ? value.replace(/\\/g, "/") : value;
129
+ const normalizedPath = normalizeSlashes(filePath);
130
+ const normalizedBase = baseDir ? normalizeSlashes(baseDir).replace(/\/+$/, "") : undefined;
131
+
132
+ const isDriveAbsolute = /^[a-zA-Z]:\//.test(normalizedPath);
133
+ const isUncAbsolute = normalizedPath.startsWith("//");
134
+ let candidate: string;
135
+
136
+ if (isDriveAbsolute || isUncAbsolute) {
137
+ candidate = normalizedPath;
138
+ } else if (normalizedPath.startsWith("/")) {
139
+ const windowsRoot = normalizedBase?.match(/^([a-zA-Z]:)(?:\/|$)/)?.[1]
140
+ ?? normalizedBase?.match(/^(\/\/[^/]+\/[^/]+)(?:\/|$)/)?.[1];
141
+ candidate = windowsRoot ? `${windowsRoot}${normalizedPath}` : normalizedPath;
142
+ } else {
143
+ if (!normalizedBase) return null;
144
+ candidate = `${normalizedBase}/${normalizedPath}`;
145
+ }
146
+
147
+ return normalizeLocalPath(candidate);
148
+ }
@@ -0,0 +1,43 @@
1
+ export function normalizeFilePathSlashes(filePath: string): string {
2
+ if (/^[a-zA-Z]:[\\/]/.test(filePath) || filePath.startsWith("\\\\")) {
3
+ return filePath.replace(/\\/g, "/");
4
+ }
5
+ return filePath;
6
+ }
7
+
8
+ export function encodeFilePathForApi(filePath: string): string {
9
+ return normalizeFilePathSlashes(filePath)
10
+ .split("/")
11
+ .filter(Boolean)
12
+ .map(encodeURIComponent)
13
+ .join("/");
14
+ }
15
+
16
+ export function getFileName(filePath: string): string {
17
+ const normalized = normalizeFilePathSlashes(filePath).replace(/\/+$/, "");
18
+ return normalized.split("/").pop() ?? normalized;
19
+ }
20
+
21
+ export function getFileDirectory(filePath: string): string {
22
+ const normalized = normalizeFilePathSlashes(filePath).replace(/\/+$/, "");
23
+ const lastSlash = normalized.lastIndexOf("/");
24
+ if (lastSlash < 0) return "";
25
+ if (lastSlash === 0) return "/";
26
+ if (lastSlash === 2 && /^[a-zA-Z]:\//.test(normalized)) return normalized.slice(0, 3);
27
+ return normalized.slice(0, lastSlash);
28
+ }
29
+
30
+ export function getRelativeFilePath(filePath: string, cwd?: string): string {
31
+ if (!cwd) return filePath;
32
+
33
+ const normalizedFile = normalizeFilePathSlashes(filePath);
34
+ const normalizedCwd = normalizeFilePathSlashes(cwd).replace(/\/$/, "");
35
+ if (normalizedFile.startsWith(normalizedCwd + "/")) {
36
+ return normalizedFile.slice(normalizedCwd.length + 1);
37
+ }
38
+ return filePath;
39
+ }
40
+
41
+ export function joinFilePath(parent: string, child: string): string {
42
+ return `${normalizeFilePathSlashes(parent).replace(/\/$/, "")}/${child}`;
43
+ }
@@ -0,0 +1,34 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ async function loadSubject() {
5
+ return import("./file-types.ts");
6
+ }
7
+
8
+ test("detects image, audio, and document preview paths", async () => {
9
+ const {
10
+ getAudioMime,
11
+ getDocumentMime,
12
+ getImageMime,
13
+ isAudioPath,
14
+ isDocumentPreviewPath,
15
+ isImagePath,
16
+ } = await loadSubject();
17
+
18
+ assert.equal(getImageMime("/tmp/screenshot.PNG"), "image/png");
19
+ assert.equal(getAudioMime("C:\\Users\\me\\voice.OPUS"), "audio/ogg");
20
+ assert.equal(getDocumentMime("/tmp/report.docx"), "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
21
+ assert.equal(isImagePath("/tmp/screenshot.PNG"), true);
22
+ assert.equal(isAudioPath("C:\\Users\\me\\voice.OPUS"), true);
23
+ assert.equal(isDocumentPreviewPath("/tmp/report.pdf"), true);
24
+ assert.equal(isDocumentPreviewPath("/tmp/report.txt"), false);
25
+ });
26
+
27
+ test("extracts extensions from mixed path styles", async () => {
28
+ const { documentPreviewKind, getFileExt } = await loadSubject();
29
+
30
+ assert.equal(getFileExt("/tmp/archive.tar.gz"), "gz");
31
+ assert.equal(getFileExt("C:\\Users\\me\\photo.AVIF"), "avif");
32
+ assert.equal(documentPreviewKind("/tmp/manual.PDF"), "pdf");
33
+ assert.equal(documentPreviewKind("/tmp/manual.md"), null);
34
+ });
@@ -0,0 +1,73 @@
1
+ export const TEXT_PREVIEW_MAX_BYTES = 256 * 1024;
2
+ export const IMAGE_PREVIEW_MAX_BYTES = 10 * 1024 * 1024;
3
+ export const DOCX_PREVIEW_MAX_BYTES = 10 * 1024 * 1024;
4
+
5
+ export type DocumentPreviewKind = "pdf" | "docx";
6
+
7
+ export const IMAGE_EXT_TO_MIME: Record<string, string> = {
8
+ png: "image/png",
9
+ jpg: "image/jpeg",
10
+ jpeg: "image/jpeg",
11
+ gif: "image/gif",
12
+ webp: "image/webp",
13
+ svg: "image/svg+xml",
14
+ bmp: "image/bmp",
15
+ ico: "image/x-icon",
16
+ avif: "image/avif",
17
+ };
18
+
19
+ export const AUDIO_EXT_TO_MIME: Record<string, string> = {
20
+ mp3: "audio/mpeg",
21
+ wav: "audio/wav",
22
+ ogg: "audio/ogg",
23
+ oga: "audio/ogg",
24
+ opus: "audio/ogg",
25
+ m4a: "audio/mp4",
26
+ aac: "audio/aac",
27
+ flac: "audio/flac",
28
+ weba: "audio/webm",
29
+ webm: "audio/webm",
30
+ };
31
+
32
+ export const DOCUMENT_EXT_TO_MIME: Record<DocumentPreviewKind, string> = {
33
+ pdf: "application/pdf",
34
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
35
+ };
36
+
37
+ function getBaseName(filePath: string): string {
38
+ return filePath.replace(/\\/g, "/").split("/").pop() ?? "";
39
+ }
40
+
41
+ export function getFileExt(filePath: string): string {
42
+ return getBaseName(filePath).toLowerCase().split(".").pop() ?? "";
43
+ }
44
+
45
+ export function getImageMime(filePath: string): string | null {
46
+ return IMAGE_EXT_TO_MIME[getFileExt(filePath)] ?? null;
47
+ }
48
+
49
+ export function getAudioMime(filePath: string): string | null {
50
+ return AUDIO_EXT_TO_MIME[getFileExt(filePath)] ?? null;
51
+ }
52
+
53
+ export function getDocumentMime(filePath: string): string | null {
54
+ return DOCUMENT_EXT_TO_MIME[getFileExt(filePath) as DocumentPreviewKind] ?? null;
55
+ }
56
+
57
+ export function documentPreviewKind(filePath: string): DocumentPreviewKind | null {
58
+ const ext = getFileExt(filePath);
59
+ if (ext === "pdf" || ext === "docx") return ext;
60
+ return null;
61
+ }
62
+
63
+ export function isImagePath(filePath: string): boolean {
64
+ return getImageMime(filePath) !== null;
65
+ }
66
+
67
+ export function isAudioPath(filePath: string): boolean {
68
+ return getAudioMime(filePath) !== null;
69
+ }
70
+
71
+ export function isDocumentPreviewPath(filePath: string): boolean {
72
+ return documentPreviewKind(filePath) !== null;
73
+ }
@@ -0,0 +1,70 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+
7
+ async function loadSubject() {
8
+ return import("./file-upload.ts");
9
+ }
10
+
11
+ test("validates upload names without accepting paths or duplicates", async () => {
12
+ const { validateUploadFileNames } = await loadSubject();
13
+
14
+ assert.equal(validateUploadFileNames(["one.txt", "two file.md"]), null);
15
+ assert.match(validateUploadFileNames(["../secret.txt"]), /must not contain a path/);
16
+ assert.match(validateUploadFileNames(["folder\\secret.txt"]), /must not contain a path/);
17
+ assert.match(validateUploadFileNames(["same.txt", "same.txt"]), /Duplicate/);
18
+ assert.match(validateUploadFileNames([]), /No files/);
19
+ });
20
+
21
+ test("finds conflicts and prevents replacing directories", async (t) => {
22
+ const { inspectUploadTargets } = await loadSubject();
23
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-web-upload-"));
24
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
25
+
26
+ fs.writeFileSync(path.join(root, "file.txt"), "old");
27
+ fs.mkdirSync(path.join(root, "directory"));
28
+
29
+ assert.deepEqual(
30
+ inspectUploadTargets(root, ["new.txt", "file.txt", "directory"]),
31
+ {
32
+ conflicts: ["file.txt", "directory"],
33
+ nonReplaceable: ["directory"],
34
+ },
35
+ );
36
+ });
37
+
38
+ test("prevents replacing symbolic links", async (t) => {
39
+ const { inspectUploadTargets } = await loadSubject();
40
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-web-upload-link-"));
41
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
42
+
43
+ fs.writeFileSync(path.join(root, "file.txt"), "old");
44
+ try {
45
+ fs.symlinkSync("file.txt", path.join(root, "link.txt"));
46
+ } catch (error) {
47
+ if (error?.code === "EPERM") {
48
+ t.skip("Creating symbolic links requires additional privileges on this platform");
49
+ return;
50
+ }
51
+ throw error;
52
+ }
53
+
54
+ assert.deepEqual(
55
+ inspectUploadTargets(root, ["link.txt"]),
56
+ {
57
+ conflicts: ["link.txt"],
58
+ nonReplaceable: ["link.txt"],
59
+ },
60
+ );
61
+ });
62
+
63
+ test("parses only supported conflict strategies", async () => {
64
+ const { parseUploadConflictStrategy } = await loadSubject();
65
+
66
+ assert.equal(parseUploadConflictStrategy(null), "error");
67
+ assert.equal(parseUploadConflictStrategy("overwrite"), "overwrite");
68
+ assert.equal(parseUploadConflictStrategy("skip"), "skip");
69
+ assert.equal(parseUploadConflictStrategy("rename"), null);
70
+ });
@@ -0,0 +1,59 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export const UPLOAD_CONFLICT_STRATEGIES = ["error", "overwrite", "skip"] as const;
5
+ export type UploadConflictStrategy = typeof UPLOAD_CONFLICT_STRATEGIES[number];
6
+
7
+ const UPLOAD_CONFLICT_STRATEGY_SET = new Set<string>(UPLOAD_CONFLICT_STRATEGIES);
8
+
9
+ export interface UploadTargetInspection {
10
+ conflicts: string[];
11
+ nonReplaceable: string[];
12
+ }
13
+
14
+ export function parseUploadConflictStrategy(value: string | null): UploadConflictStrategy | null {
15
+ const candidate = value ?? "error";
16
+ return UPLOAD_CONFLICT_STRATEGY_SET.has(candidate)
17
+ ? candidate as UploadConflictStrategy
18
+ : null;
19
+ }
20
+
21
+ export function validateUploadFileNames(fileNames: string[]): string | null {
22
+ if (fileNames.length === 0) return "No files selected";
23
+
24
+ const seen = new Set<string>();
25
+ for (const fileName of fileNames) {
26
+ if (!fileName || fileName === "." || fileName === ".." || fileName.includes("\0")) {
27
+ return `Invalid file name: ${fileName || "(empty)"}`;
28
+ }
29
+ if (fileName.includes("/") || fileName.includes("\\") || path.basename(fileName) !== fileName) {
30
+ return `File names must not contain a path: ${fileName}`;
31
+ }
32
+ if (seen.has(fileName)) return `Duplicate file name in upload: ${fileName}`;
33
+ seen.add(fileName);
34
+ }
35
+
36
+ return null;
37
+ }
38
+
39
+ export function inspectUploadTargets(directory: string, fileNames: string[]): UploadTargetInspection {
40
+ const conflicts: string[] = [];
41
+ const nonReplaceable: string[] = [];
42
+
43
+ for (const fileName of fileNames) {
44
+ const destination = path.join(directory, fileName);
45
+ let stat: fs.Stats;
46
+ try {
47
+ stat = fs.lstatSync(destination);
48
+ } catch (error) {
49
+ const code = (error as NodeJS.ErrnoException).code;
50
+ if (code === "ENOENT") continue;
51
+ throw error;
52
+ }
53
+
54
+ conflicts.push(fileName);
55
+ if (!stat.isFile() || stat.isSymbolicLink()) nonReplaceable.push(fileName);
56
+ }
57
+
58
+ return { conflicts, nonReplaceable };
59
+ }
@@ -0,0 +1,20 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { resolveInitialFileDisplayMode } from "./file-viewer-state.ts";
5
+
6
+ test("a restored display mode wins over a stale open hint", () => {
7
+ const state = {
8
+ displayMode: "source",
9
+ wrapLines: true,
10
+ scrollTop: 80,
11
+ scrollLeft: 0,
12
+ };
13
+
14
+ assert.equal(resolveInitialFileDisplayMode(state, "diff"), "source");
15
+ });
16
+
17
+ test("the open hint is used only before viewer state has been saved", () => {
18
+ assert.equal(resolveInitialFileDisplayMode(undefined, "diff"), "diff");
19
+ assert.equal(resolveInitialFileDisplayMode(), "source");
20
+ });
@@ -0,0 +1,15 @@
1
+ export type FileViewerDisplayMode = "source" | "preview" | "diff";
2
+
3
+ export interface FileViewerState {
4
+ displayMode: FileViewerDisplayMode;
5
+ wrapLines: boolean;
6
+ scrollTop: number;
7
+ scrollLeft: number;
8
+ }
9
+
10
+ export function resolveInitialFileDisplayMode(
11
+ initialState?: FileViewerState,
12
+ initialDisplayMode?: FileViewerDisplayMode,
13
+ ): FileViewerDisplayMode {
14
+ return initialState?.displayMode ?? initialDisplayMode ?? "source";
15
+ }
@@ -0,0 +1,55 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+
4
+ import {
5
+ formatFrontmatterValue,
6
+ getFrontmatterTitle,
7
+ parseFrontmatter,
8
+ } from "./frontmatter.ts";
9
+
10
+ describe("parseFrontmatter", () => {
11
+ it("accepts the same common fence variants as remark-frontmatter", () => {
12
+ const cases = [
13
+ "---\ntitle: Demo\n---\nbody",
14
+ "\uFEFF---\ntitle: Demo\n---\nbody",
15
+ "--- \ntitle: Demo\n---\t\nbody",
16
+ "---\r\ntitle: Demo\r\n---\r\nbody",
17
+ "---\rtitle: Demo\r---\rbody",
18
+ ];
19
+
20
+ for (const markdown of cases) {
21
+ assert.deepEqual(parseFrontmatter(markdown), {
22
+ data: { title: "Demo" },
23
+ rest: "body",
24
+ });
25
+ }
26
+ });
27
+
28
+ it("removes an empty or malformed fenced block from the returned body", () => {
29
+ assert.deepEqual(parseFrontmatter("---\n---\nbody"), { data: null, rest: "body" });
30
+ assert.deepEqual(parseFrontmatter("---\n[invalid\n---\nbody"), {
31
+ data: null,
32
+ rest: "body",
33
+ });
34
+ });
35
+
36
+ it("leaves documents without a leading frontmatter block untouched", () => {
37
+ const markdown = "body\n---\ntitle: Not frontmatter\n---";
38
+ assert.deepEqual(parseFrontmatter(markdown), { data: null, rest: markdown });
39
+ });
40
+ });
41
+
42
+ describe("frontmatter value formatting", () => {
43
+ it("does not recurse forever through YAML aliases", () => {
44
+ const { data } = parseFrontmatter("---\nloop: &loop\n - *loop\n---\n");
45
+ assert.ok(data);
46
+ assert.equal(formatFrontmatterValue(data.loop), "[Circular]");
47
+ });
48
+
49
+ it("uses scalar titles and leaves structured titles for the metadata rows", () => {
50
+ assert.equal(getFrontmatterTitle(" Demo "), "Demo");
51
+ assert.equal(getFrontmatterTitle(1984), "1984");
52
+ assert.equal(getFrontmatterTitle(false), "false");
53
+ assert.equal(getFrontmatterTitle(["Demo"]), null);
54
+ });
55
+ });
@@ -0,0 +1,87 @@
1
+ import { load as parseYaml } from "js-yaml";
2
+
3
+ const FRONTMATTER_OPEN_RE = /^(?:\uFEFF)?---[ \t]*(?:\r\n|\n|\r)/;
4
+
5
+ export interface FrontmatterResult {
6
+ data: Record<string, unknown> | null;
7
+ rest: string;
8
+ }
9
+
10
+ interface FrontmatterBlock {
11
+ yaml: string;
12
+ rest: string;
13
+ }
14
+
15
+ function extractFrontmatter(markdown: string): FrontmatterBlock | null {
16
+ const opening = FRONTMATTER_OPEN_RE.exec(markdown);
17
+ if (!opening) return null;
18
+
19
+ const closingPattern = /^---[ \t]*(?:(?:\r\n|\n|\r)|$)/gm;
20
+ closingPattern.lastIndex = opening[0].length;
21
+ const closing = closingPattern.exec(markdown);
22
+ if (!closing) return null;
23
+
24
+ const yaml = markdown
25
+ .slice(opening[0].length, closing.index)
26
+ .replace(/(?:\r\n|\n|\r)$/, "");
27
+
28
+ return {
29
+ yaml,
30
+ rest: markdown.slice(closing.index + closing[0].length),
31
+ };
32
+ }
33
+
34
+ export function parseFrontmatter(markdown: string): FrontmatterResult {
35
+ const block = extractFrontmatter(markdown);
36
+ if (!block) return { data: null, rest: markdown };
37
+
38
+ try {
39
+ const parsed = parseYaml(block.yaml);
40
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
41
+ return { data: parsed as Record<string, unknown>, rest: block.rest };
42
+ }
43
+ } catch {
44
+ // The remark plugin still hides a syntactically fenced malformed block.
45
+ }
46
+
47
+ return { data: null, rest: block.rest };
48
+ }
49
+
50
+ export function formatFrontmatterValue(value: unknown): string {
51
+ return formatValue(value, new WeakSet<object>());
52
+ }
53
+
54
+ function formatValue(value: unknown, ancestors: WeakSet<object>): string {
55
+ if (value === null || value === undefined) return "";
56
+ if (typeof value === "string") return value;
57
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
58
+
59
+ if (Array.isArray(value)) {
60
+ if (ancestors.has(value)) return "[Circular]";
61
+ ancestors.add(value);
62
+ try {
63
+ return value
64
+ .map((item) => formatValue(item, ancestors))
65
+ .filter(Boolean)
66
+ .join(", ");
67
+ } finally {
68
+ ancestors.delete(value);
69
+ }
70
+ }
71
+
72
+ if (typeof value === "object") {
73
+ try {
74
+ return JSON.stringify(value) ?? Object.prototype.toString.call(value);
75
+ } catch {
76
+ return Object.prototype.toString.call(value);
77
+ }
78
+ }
79
+
80
+ return String(value);
81
+ }
82
+
83
+ export function getFrontmatterTitle(value: unknown): string | null {
84
+ if (typeof value === "string") return value.trim() || null;
85
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
86
+ return null;
87
+ }
@@ -0,0 +1,52 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ async function loadSubject() {
5
+ return import("./git-status.ts");
6
+ }
7
+
8
+ test("parses null-delimited Git status entries including renames", async () => {
9
+ const { parseGitPorcelainV1 } = await loadSubject();
10
+ const entries = parseGitPorcelainV1([
11
+ " M components/App.tsx",
12
+ "?? notes.txt",
13
+ "R src/new-name.ts",
14
+ "src/old-name.ts",
15
+ "",
16
+ ].join("\0"));
17
+
18
+ assert.deepEqual(entries, [
19
+ {
20
+ path: "components/App.tsx",
21
+ indexStatus: " ",
22
+ worktreeStatus: "M",
23
+ },
24
+ {
25
+ path: "notes.txt",
26
+ indexStatus: "?",
27
+ worktreeStatus: "?",
28
+ },
29
+ {
30
+ path: "src/new-name.ts",
31
+ originalPath: "src/old-name.ts",
32
+ indexStatus: "R",
33
+ worktreeStatus: " ",
34
+ },
35
+ ]);
36
+ });
37
+
38
+ test("classifies Git status for explorer badges", async () => {
39
+ const { classifyGitStatus } = await loadSubject();
40
+ const classify = (pair) => classifyGitStatus({
41
+ path: "file.ts",
42
+ indexStatus: pair[0],
43
+ worktreeStatus: pair[1],
44
+ });
45
+
46
+ assert.deepEqual(classify(" M"), { status: "modified", code: "M" });
47
+ assert.deepEqual(classify("??"), { status: "untracked", code: "U" });
48
+ assert.deepEqual(classify("A "), { status: "added", code: "A" });
49
+ assert.deepEqual(classify("R "), { status: "renamed", code: "R" });
50
+ assert.deepEqual(classify("UU"), { status: "conflict", code: "C" });
51
+ assert.deepEqual(classify(" D"), { status: "deleted", code: "D" });
52
+ });