@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,232 @@
1
+ import { execFile } from "child_process";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { promisify } from "util";
5
+ import { TEXT_PREVIEW_MAX_BYTES } from "./file-types";
6
+ import type {
7
+ GitFileDiffResponse,
8
+ GitFileStatus,
9
+ GitStatusResponse,
10
+ } from "./git-types";
11
+ import {
12
+ classifyGitStatus,
13
+ parseGitPorcelainV1,
14
+ type GitPorcelainEntry,
15
+ } from "./git-status";
16
+
17
+ const execFileAsync = promisify(execFile);
18
+ const GIT_TIMEOUT_MS = 10_000;
19
+ const GIT_STATUS_MAX_BUFFER = 8 * 1024 * 1024;
20
+
21
+ async function git(cwd: string, args: string[], maxBuffer = GIT_STATUS_MAX_BUFFER): Promise<string> {
22
+ const { stdout } = await execFileAsync("git", ["-C", cwd, ...args], {
23
+ timeout: GIT_TIMEOUT_MS,
24
+ maxBuffer,
25
+ env: { ...process.env, LC_ALL: "C" },
26
+ });
27
+ return stdout;
28
+ }
29
+
30
+ async function findRepositoryRoot(cwd: string): Promise<string | null> {
31
+ try {
32
+ return (await git(cwd, ["rev-parse", "--show-toplevel"])).trim() || null;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function isWithinPath(parent: string, target: string): boolean {
39
+ const relative = path.relative(path.resolve(parent), path.resolve(target));
40
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
41
+ }
42
+
43
+ function toGitPath(filePath: string): string {
44
+ return filePath.split(path.sep).join("/");
45
+ }
46
+
47
+ async function readStatusEntries(repositoryRoot: string): Promise<GitPorcelainEntry[]> {
48
+ const output = await git(repositoryRoot, [
49
+ "status",
50
+ "--porcelain=v1",
51
+ "-z",
52
+ "--untracked-files=all",
53
+ ]);
54
+ return parseGitPorcelainV1(output);
55
+ }
56
+
57
+ async function readTrackedLineStats(
58
+ repositoryRoot: string,
59
+ cwd: string,
60
+ ): Promise<{ additions: number; deletions: number }> {
61
+ const relativeCwd = toGitPath(path.relative(repositoryRoot, cwd));
62
+ const pathspec = relativeCwd || ".";
63
+ try {
64
+ const output = await git(repositoryRoot, [
65
+ "diff",
66
+ "--no-color",
67
+ "--no-ext-diff",
68
+ "--numstat",
69
+ "HEAD",
70
+ "--",
71
+ pathspec,
72
+ ]);
73
+ let additions = 0;
74
+ let deletions = 0;
75
+ for (const line of output.split(/\r?\n/)) {
76
+ if (!line) continue;
77
+ const [added, deleted] = line.split("\t", 2);
78
+ const addedCount = Number(added);
79
+ const deletedCount = Number(deleted);
80
+ if (Number.isInteger(addedCount)) additions += addedCount;
81
+ if (Number.isInteger(deletedCount)) deletions += deletedCount;
82
+ }
83
+ return { additions, deletions };
84
+ } catch {
85
+ return { additions: 0, deletions: 0 };
86
+ }
87
+ }
88
+
89
+ function countUntrackedTextLines(filePath: string): number {
90
+ try {
91
+ const stat = fs.lstatSync(filePath);
92
+ if (!stat.isFile() || stat.size > TEXT_PREVIEW_MAX_BYTES) return 0;
93
+ const content = fs.readFileSync(filePath);
94
+ if (hasNullByte(content) || content.length === 0) return 0;
95
+ const text = content.toString("utf8");
96
+ return text.endsWith("\n") ? text.split("\n").length - 1 : text.split("\n").length;
97
+ } catch {
98
+ return 0;
99
+ }
100
+ }
101
+
102
+ export async function getGitStatus(cwd: string): Promise<GitStatusResponse> {
103
+ const repositoryRoot = await findRepositoryRoot(cwd);
104
+ if (!repositoryRoot) {
105
+ return {
106
+ isGitRepository: false,
107
+ repositoryRoot: null,
108
+ files: [],
109
+ additions: 0,
110
+ deletions: 0,
111
+ };
112
+ }
113
+
114
+ const [entries, trackedLineStats] = await Promise.all([
115
+ readStatusEntries(repositoryRoot),
116
+ readTrackedLineStats(repositoryRoot, cwd),
117
+ ]);
118
+ const files = entries.flatMap((entry): GitFileStatus[] => {
119
+ const filePath = path.resolve(repositoryRoot, entry.path);
120
+ if (!isWithinPath(cwd, filePath)) return [];
121
+ const classified = classifyGitStatus(entry);
122
+ return [{
123
+ filePath,
124
+ ...classified,
125
+ indexStatus: entry.indexStatus,
126
+ worktreeStatus: entry.worktreeStatus,
127
+ }];
128
+ });
129
+ const untrackedAdditions = files.reduce(
130
+ (total, file) => total + (file.status === "untracked" ? countUntrackedTextLines(file.filePath) : 0),
131
+ 0,
132
+ );
133
+
134
+ return {
135
+ isGitRepository: true,
136
+ repositoryRoot,
137
+ files,
138
+ additions: trackedLineStats.additions + untrackedAdditions,
139
+ deletions: trackedLineStats.deletions,
140
+ };
141
+ }
142
+
143
+ function hasNullByte(content: Buffer): boolean {
144
+ return content.includes(0);
145
+ }
146
+
147
+ function createAddedFilePatch(gitPath: string, content: string): string {
148
+ const hasTrailingNewline = content.endsWith("\n");
149
+ const lines = content.split("\n");
150
+ if (hasTrailingNewline) lines.pop();
151
+ const body = lines.map((line) => `+${line}`).join("\n");
152
+ const noNewlineMarker = !hasTrailingNewline && lines.length > 0
153
+ ? "\n\"
154
+ : "";
155
+ return [
156
+ `diff --git a/${gitPath} b/${gitPath}`,
157
+ "new file mode 100644",
158
+ "--- /dev/null",
159
+ `+++ b/${gitPath}`,
160
+ `@@ -0,0 +1,${lines.length} @@`,
161
+ `${body}${noNewlineMarker}`,
162
+ ].join("\n");
163
+ }
164
+
165
+ async function createTrackedFilePatch(
166
+ repositoryRoot: string,
167
+ relativePath: string,
168
+ originalPath?: string,
169
+ ): Promise<string | null> {
170
+ const paths = originalPath && originalPath !== relativePath
171
+ ? [originalPath, relativePath]
172
+ : [relativePath];
173
+ try {
174
+ return await git(repositoryRoot, [
175
+ "diff",
176
+ "--no-color",
177
+ "--no-ext-diff",
178
+ "--unified=3",
179
+ "HEAD",
180
+ "--",
181
+ ...paths,
182
+ ], TEXT_PREVIEW_MAX_BYTES * 4);
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
187
+
188
+ export async function getGitFileDiff(cwd: string, filePath: string): Promise<GitFileDiffResponse> {
189
+ const repositoryRoot = await findRepositoryRoot(cwd);
190
+ if (!repositoryRoot || !isWithinPath(repositoryRoot, filePath)) return { supported: false };
191
+
192
+ const resolvedFilePath = path.resolve(filePath);
193
+ const relativePath = toGitPath(path.relative(repositoryRoot, resolvedFilePath));
194
+ const entries = await readStatusEntries(repositoryRoot);
195
+ const entry = entries.find((candidate) => candidate.path === relativePath);
196
+ if (!entry) return { supported: false };
197
+
198
+ const { status } = classifyGitStatus(entry);
199
+ if (status === "deleted") {
200
+ const patch = await createTrackedFilePatch(repositoryRoot, relativePath, entry.originalPath);
201
+ if (!patch?.includes("\n@@ ")) return { supported: false };
202
+ return { supported: true, status, patch };
203
+ }
204
+
205
+ let stat: fs.Stats;
206
+ try {
207
+ stat = fs.lstatSync(resolvedFilePath);
208
+ } catch {
209
+ return { supported: false };
210
+ }
211
+ if (!stat.isFile() || stat.size > TEXT_PREVIEW_MAX_BYTES) return { supported: false };
212
+
213
+ const currentBuffer = fs.readFileSync(resolvedFilePath);
214
+ if (hasNullByte(currentBuffer)) return { supported: false };
215
+ const newContent = currentBuffer.toString("utf8");
216
+
217
+ let patch: string;
218
+ if (status === "untracked") {
219
+ patch = createAddedFilePatch(relativePath, newContent);
220
+ } else {
221
+ const trackedPatch = await createTrackedFilePatch(repositoryRoot, relativePath, entry.originalPath);
222
+ if (trackedPatch === null) {
223
+ if (status !== "added") return { supported: false };
224
+ patch = createAddedFilePatch(relativePath, newContent);
225
+ } else {
226
+ patch = trackedPatch;
227
+ }
228
+ }
229
+
230
+ if (!patch.includes("\n@@ ")) return { supported: false };
231
+ return { supported: true, status, patch };
232
+ }
@@ -0,0 +1,47 @@
1
+ import type { GitFileStatus } from "./git-types";
2
+
3
+ export interface GitPorcelainEntry {
4
+ path: string;
5
+ originalPath?: string;
6
+ indexStatus: string;
7
+ worktreeStatus: string;
8
+ }
9
+
10
+ function usesRenamePath(indexStatus: string, worktreeStatus: string): boolean {
11
+ return indexStatus === "R" || indexStatus === "C" || worktreeStatus === "R" || worktreeStatus === "C";
12
+ }
13
+
14
+ export function parseGitPorcelainV1(output: string): GitPorcelainEntry[] {
15
+ const records = output.split("\0");
16
+ const entries: GitPorcelainEntry[] = [];
17
+
18
+ for (let i = 0; i < records.length; i++) {
19
+ const record = records[i];
20
+ if (!record || record.length < 4 || record[2] !== " ") continue;
21
+ const indexStatus = record[0];
22
+ const worktreeStatus = record[1];
23
+ const entry: GitPorcelainEntry = {
24
+ path: record.slice(3),
25
+ indexStatus,
26
+ worktreeStatus,
27
+ };
28
+ if (usesRenamePath(indexStatus, worktreeStatus)) {
29
+ entry.originalPath = records[++i] || undefined;
30
+ }
31
+ entries.push(entry);
32
+ }
33
+
34
+ return entries;
35
+ }
36
+
37
+ const CONFLICT_STATUSES = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
38
+
39
+ export function classifyGitStatus(entry: GitPorcelainEntry): Pick<GitFileStatus, "status" | "code"> {
40
+ const pair = `${entry.indexStatus}${entry.worktreeStatus}`;
41
+ if (pair === "??") return { status: "untracked", code: "U" };
42
+ if (CONFLICT_STATUSES.has(pair) || pair.includes("U")) return { status: "conflict", code: "C" };
43
+ if (pair.includes("D")) return { status: "deleted", code: "D" };
44
+ if (pair.includes("R") || pair.includes("C")) return { status: "renamed", code: "R" };
45
+ if (pair.includes("A")) return { status: "added", code: "A" };
46
+ return { status: "modified", code: "M" };
47
+ }
@@ -0,0 +1,29 @@
1
+ export type GitFileStatusKind =
2
+ | "modified"
3
+ | "added"
4
+ | "deleted"
5
+ | "renamed"
6
+ | "untracked"
7
+ | "conflict";
8
+
9
+ export interface GitFileStatus {
10
+ filePath: string;
11
+ status: GitFileStatusKind;
12
+ code: "M" | "A" | "D" | "R" | "U" | "C";
13
+ indexStatus: string;
14
+ worktreeStatus: string;
15
+ }
16
+
17
+ export interface GitStatusResponse {
18
+ isGitRepository: boolean;
19
+ repositoryRoot: string | null;
20
+ files: GitFileStatus[];
21
+ additions: number;
22
+ deletions: number;
23
+ }
24
+
25
+ export interface GitFileDiffResponse {
26
+ supported: boolean;
27
+ status?: GitFileStatusKind;
28
+ patch?: string;
29
+ }
@@ -0,0 +1,83 @@
1
+ import assert from "node:assert/strict";
2
+ import { createServer } from "node:http";
3
+ import { once } from "node:events";
4
+ import test from "node:test";
5
+ import { createJiti } from "jiti";
6
+
7
+ const PROXY_ENV_KEYS = [
8
+ "HTTP_PROXY",
9
+ "HTTPS_PROXY",
10
+ "NO_PROXY",
11
+ "http_proxy",
12
+ "https_proxy",
13
+ "no_proxy",
14
+ "ALL_PROXY",
15
+ "all_proxy",
16
+ ];
17
+
18
+ test("configures HTTP_PROXY, HTTPS_PROXY, and NO_PROXY for global fetch", async (t) => {
19
+ const originalEnv = new Map(PROXY_ENV_KEYS.map((key) => [key, process.env[key]]));
20
+ for (const key of PROXY_ENV_KEYS) delete process.env[key];
21
+
22
+ const connectTargets = [];
23
+ const forwardedRequests = [];
24
+ const proxy = createServer((req, res) => {
25
+ forwardedRequests.push(`${req.method} ${req.url}`);
26
+ res.writeHead(204, { Connection: "close" });
27
+ res.end();
28
+ });
29
+ proxy.on("connect", (req, socket) => {
30
+ connectTargets.push(req.url);
31
+ socket.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
32
+ });
33
+ proxy.listen(0, "127.0.0.1");
34
+ await once(proxy, "listening");
35
+
36
+ t.after(async () => {
37
+ for (const [key, value] of originalEnv) {
38
+ if (value === undefined) delete process.env[key];
39
+ else process.env[key] = value;
40
+ }
41
+ await new Promise((resolve, reject) => {
42
+ proxy.close((error) => error ? reject(error) : resolve());
43
+ });
44
+ });
45
+
46
+ const address = proxy.address();
47
+ assert.ok(address && typeof address === "object");
48
+ const proxyUrl = `http://127.0.0.1:${address.port}`;
49
+ process.env.HTTP_PROXY = proxyUrl;
50
+ process.env.HTTPS_PROXY = proxyUrl;
51
+ process.env.NO_PROXY = "bypass.invalid";
52
+
53
+ const jiti = createJiti(import.meta.url);
54
+ const { configureHttpDispatcher } = await jiti.import("./http-dispatcher.ts");
55
+ const { getGlobalDispatcher } = await import("undici");
56
+
57
+ assert.throws(() => configureHttpDispatcher(-1), /Invalid HTTP idle timeout/);
58
+ configureHttpDispatcher(2_000);
59
+
60
+ const dispatcher = getGlobalDispatcher();
61
+ configureHttpDispatcher(5_000);
62
+ assert.equal(getGlobalDispatcher(), dispatcher, "configuration should be idempotent");
63
+
64
+ const httpResponse = await fetch("http://target.invalid/through-http-proxy", {
65
+ signal: AbortSignal.timeout(2_000),
66
+ });
67
+ assert.equal(httpResponse.status, 204);
68
+ assert.deepEqual(forwardedRequests, ["GET http://target.invalid/through-http-proxy"]);
69
+ assert.deepEqual(connectTargets, []);
70
+
71
+ await assert.rejects(fetch("https://target.invalid/through-https-proxy", {
72
+ signal: AbortSignal.timeout(2_000),
73
+ }));
74
+ assert.deepEqual(connectTargets, ["target.invalid:443"]);
75
+
76
+ const forwardedRequestCount = forwardedRequests.length;
77
+ const connectTargetCount = connectTargets.length;
78
+ await assert.rejects(fetch("http://bypass.invalid:9/no-proxy", {
79
+ signal: AbortSignal.timeout(2_000),
80
+ }));
81
+ assert.equal(forwardedRequests.length, forwardedRequestCount);
82
+ assert.equal(connectTargets.length, connectTargetCount);
83
+ });
@@ -0,0 +1,86 @@
1
+ import { EventEmitter } from "node:events";
2
+ import * as undici from "undici";
3
+
4
+ export const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000;
5
+
6
+ type DispatcherGlobal = typeof globalThis & {
7
+ __piWebHttpDispatcherConfigured?: boolean;
8
+ };
9
+
10
+ const dispatcherGlobal = globalThis as DispatcherGlobal;
11
+ const originalGlobalFetch = globalThis.fetch;
12
+ const ignoreUndiciDispatcherError = (): void => {};
13
+
14
+ function parseHttpIdleTimeoutMs(value: unknown): number | undefined {
15
+ if (typeof value === "string") {
16
+ const trimmed = value.trim();
17
+ if (trimmed.toLowerCase() === "disabled") return 0;
18
+ if (trimmed.length === 0) return undefined;
19
+ return parseHttpIdleTimeoutMs(Number(trimmed));
20
+ }
21
+
22
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
23
+ return undefined;
24
+ }
25
+ return Math.floor(value);
26
+ }
27
+
28
+ // Undici can emit an internal Client error while terminating a response body.
29
+ // The body stream still rejects; this prevents the EventEmitter error from
30
+ // terminating the Next.js process first.
31
+ function withUndiciErrorListener<T extends undici.Dispatcher>(dispatcher: T): T {
32
+ if (dispatcher instanceof EventEmitter) {
33
+ EventEmitter.prototype.on.call(dispatcher, "error", ignoreUndiciDispatcherError);
34
+ }
35
+ return dispatcher;
36
+ }
37
+
38
+ function createUndiciClient(origin: string | URL, options: object): undici.Dispatcher {
39
+ return withUndiciErrorListener(
40
+ new undici.Client(origin, options as undici.Client.Options),
41
+ );
42
+ }
43
+
44
+ function createUndiciOriginDispatcher(origin: string | URL, options: object): undici.Dispatcher {
45
+ const dispatcherOptions = options as undici.Pool.Options;
46
+ if (dispatcherOptions.connections === 1) {
47
+ return createUndiciClient(origin, dispatcherOptions);
48
+ }
49
+
50
+ return withUndiciErrorListener(
51
+ new undici.Pool(origin, {
52
+ ...dispatcherOptions,
53
+ factory: createUndiciClient,
54
+ }),
55
+ );
56
+ }
57
+
58
+ export function configureHttpDispatcher(
59
+ timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS,
60
+ ): void {
61
+ if (dispatcherGlobal.__piWebHttpDispatcherConfigured) return;
62
+
63
+ const normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs);
64
+ if (normalizedTimeoutMs === undefined) {
65
+ throw new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);
66
+ }
67
+
68
+ const dispatcher = withUndiciErrorListener(
69
+ new undici.EnvHttpProxyAgent({
70
+ allowH2: false,
71
+ bodyTimeout: normalizedTimeoutMs,
72
+ headersTimeout: normalizedTimeoutMs,
73
+ clientFactory: createUndiciClient,
74
+ factory: createUndiciOriginDispatcher,
75
+ }),
76
+ );
77
+ undici.setGlobalDispatcher(dispatcher);
78
+
79
+ // Keep fetch and the dispatcher on the same undici implementation. Preserve
80
+ // an intentional fetch override installed after this module was loaded.
81
+ if (globalThis.fetch === originalGlobalFetch) {
82
+ undici.install?.();
83
+ }
84
+
85
+ dispatcherGlobal.__piWebHttpDispatcherConfigured = true;
86
+ }
@@ -0,0 +1,19 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { formatRelativeTime, interpolateMessage, translateMessage } from "./format.ts";
5
+
6
+ test("interpolates string and numeric parameters", () => {
7
+ assert.equal(interpolateMessage("Hello, {name} ({count})", { name: "Pi", count: 2 }), "Hello, Pi (2)");
8
+ });
9
+
10
+ test("falls back to English and returns the key when both are missing", () => {
11
+ assert.equal(translateMessage("zh-CN", "common.ok", { en: { "common.ok": "OK" }, "zh-CN": {} }), "OK");
12
+ assert.equal(translateMessage("zh-CN", "missing.key", { en: {}, "zh-CN": {} }), "missing.key");
13
+ });
14
+
15
+ test("formats relative time using the selected locale", () => {
16
+ const now = new Date("2026-01-01T00:00:00.000Z");
17
+ assert.equal(formatRelativeTime(new Date("2026-01-01T00:05:00.000Z"), "en", now), "in 5 minutes");
18
+ assert.equal(formatRelativeTime(new Date("2025-12-31T23:00:00.000Z"), "zh-CN", now), "1小时前");
19
+ });
@@ -0,0 +1,60 @@
1
+ import type { Locale, TranslationParams } from "./types";
2
+
3
+ type MessagesByLocale = Record<string, Record<string, string>>;
4
+
5
+ /**
6
+ * 替换翻译消息中的简单插值占位符。
7
+ * @param message 原始翻译消息
8
+ * @param params 插值参数
9
+ * @returns 完成参数替换后的消息
10
+ */
11
+ export function interpolateMessage(message: string, params: TranslationParams = {}): string {
12
+ return message.replace(/\{([\w.-]+)\}/g, (token, name: string) => {
13
+ const value = params[name];
14
+ return value === undefined ? token : String(value);
15
+ });
16
+ }
17
+
18
+ /**
19
+ * 从当前语言和英语语言包中解析消息。
20
+ * @param locale 当前语言
21
+ * @param key 翻译 key
22
+ * @param messages 各语言的消息字典
23
+ * @param params 可选的插值参数
24
+ * @returns 翻译结果,缺失时返回 key
25
+ */
26
+ export function translateMessage(
27
+ locale: Locale,
28
+ key: string,
29
+ messages: MessagesByLocale,
30
+ params: TranslationParams = {},
31
+ ): string {
32
+ const message = messages[locale]?.[key] ?? messages.en?.[key];
33
+ if (message === undefined) {
34
+ if (process.env.NODE_ENV !== "production") console.warn(`[i18n] Missing translation: ${key}`);
35
+ return key;
36
+ }
37
+ return interpolateMessage(message, params);
38
+ }
39
+
40
+ /**
41
+ * 按当前语言格式化相对时间。
42
+ * @param date 要格式化的时间
43
+ * @param locale 当前语言
44
+ * @param now 用于测试或特殊场景的当前时间
45
+ * @returns locale-aware 的相对时间文本
46
+ */
47
+ export function formatRelativeTime(date: Date | string, locale: Locale, now = new Date()): string {
48
+ const target = date instanceof Date ? date : new Date(date);
49
+ const diffMs = target.getTime() - now.getTime();
50
+ const absMs = Math.abs(diffMs);
51
+ const [unit, divisor] = absMs < 60_000
52
+ ? ["second", 1_000]
53
+ : absMs < 3_600_000
54
+ ? ["minute", 60_000]
55
+ : absMs < 86_400_000
56
+ ? ["hour", 3_600_000]
57
+ : ["day", 86_400_000];
58
+ const value = Math.round(diffMs / divisor);
59
+ return new Intl.RelativeTimeFormat(locale, { numeric: "always" }).format(value, unit as Intl.RelativeTimeFormatUnit);
60
+ }