@rexvane/piweb 0.3.1

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 (535) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +128 -0
  3. package/assets/showcase.png +0 -0
  4. package/bin/pi.js +50 -0
  5. package/bin/piweb.js +12 -0
  6. package/next.config.ts +21 -0
  7. package/package.json +88 -0
  8. package/postcss.config.mjs +7 -0
  9. package/public/file-icons/3d.svg +1 -0
  10. package/public/file-icons/LICENSE +8 -0
  11. package/public/file-icons/adobe-illustrator.svg +1 -0
  12. package/public/file-icons/adobe-photoshop.svg +1 -0
  13. package/public/file-icons/agent.svg +1 -0
  14. package/public/file-icons/asciidoc.svg +1 -0
  15. package/public/file-icons/assembly.svg +1 -0
  16. package/public/file-icons/astro.svg +1 -0
  17. package/public/file-icons/audio.svg +1 -0
  18. package/public/file-icons/authors.svg +1 -0
  19. package/public/file-icons/babel.svg +1 -0
  20. package/public/file-icons/bibliography.svg +1 -0
  21. package/public/file-icons/bicep.svg +1 -0
  22. package/public/file-icons/blender.svg +1 -0
  23. package/public/file-icons/bun.svg +1 -0
  24. package/public/file-icons/c.svg +1 -0
  25. package/public/file-icons/certificate.svg +1 -0
  26. package/public/file-icons/changelog.svg +1 -0
  27. package/public/file-icons/clojure.svg +1 -0
  28. package/public/file-icons/cmake.svg +1 -0
  29. package/public/file-icons/codeowners.svg +1 -0
  30. package/public/file-icons/commitlint.svg +1 -0
  31. package/public/file-icons/conduct.svg +1 -0
  32. package/public/file-icons/console.svg +1 -0
  33. package/public/file-icons/contributing.svg +1 -0
  34. package/public/file-icons/cpp.svg +1 -0
  35. package/public/file-icons/csharp.svg +1 -0
  36. package/public/file-icons/css.svg +1 -0
  37. package/public/file-icons/cursor.svg +1 -0
  38. package/public/file-icons/dart.svg +1 -0
  39. package/public/file-icons/database.svg +1 -0
  40. package/public/file-icons/deno.svg +1 -0
  41. package/public/file-icons/diff.svg +1 -0
  42. package/public/file-icons/docker.svg +1 -0
  43. package/public/file-icons/document.svg +1 -0
  44. package/public/file-icons/editorconfig.svg +1 -0
  45. package/public/file-icons/ejs.svg +1 -0
  46. package/public/file-icons/elixir.svg +1 -0
  47. package/public/file-icons/elm.svg +1 -0
  48. package/public/file-icons/email.svg +1 -0
  49. package/public/file-icons/erlang.svg +1 -0
  50. package/public/file-icons/eslint.svg +1 -0
  51. package/public/file-icons/favicon.svg +1 -0
  52. package/public/file-icons/file.svg +1 -0
  53. package/public/file-icons/folder-admin-open.svg +1 -0
  54. package/public/file-icons/folder-admin.svg +1 -0
  55. package/public/file-icons/folder-android-open.svg +1 -0
  56. package/public/file-icons/folder-android.svg +1 -0
  57. package/public/file-icons/folder-api-open.svg +1 -0
  58. package/public/file-icons/folder-api.svg +1 -0
  59. package/public/file-icons/folder-app-open.svg +1 -0
  60. package/public/file-icons/folder-app.svg +1 -0
  61. package/public/file-icons/folder-archive-open.svg +1 -0
  62. package/public/file-icons/folder-archive.svg +1 -0
  63. package/public/file-icons/folder-atom-open.svg +1 -0
  64. package/public/file-icons/folder-atom.svg +1 -0
  65. package/public/file-icons/folder-audio-open.svg +1 -0
  66. package/public/file-icons/folder-audio.svg +1 -0
  67. package/public/file-icons/folder-aws-open.svg +1 -0
  68. package/public/file-icons/folder-aws.svg +1 -0
  69. package/public/file-icons/folder-backup-open.svg +1 -0
  70. package/public/file-icons/folder-backup.svg +1 -0
  71. package/public/file-icons/folder-benchmark-open.svg +1 -0
  72. package/public/file-icons/folder-benchmark.svg +1 -0
  73. package/public/file-icons/folder-cart-open.svg +1 -0
  74. package/public/file-icons/folder-cart.svg +1 -0
  75. package/public/file-icons/folder-ci-open.svg +1 -0
  76. package/public/file-icons/folder-ci.svg +1 -0
  77. package/public/file-icons/folder-circleci-open.svg +1 -0
  78. package/public/file-icons/folder-circleci.svg +1 -0
  79. package/public/file-icons/folder-class-open.svg +1 -0
  80. package/public/file-icons/folder-class.svg +1 -0
  81. package/public/file-icons/folder-client-open.svg +1 -0
  82. package/public/file-icons/folder-client.svg +1 -0
  83. package/public/file-icons/folder-command-open.svg +1 -0
  84. package/public/file-icons/folder-command.svg +1 -0
  85. package/public/file-icons/folder-components-open.svg +1 -0
  86. package/public/file-icons/folder-components.svg +1 -0
  87. package/public/file-icons/folder-config-open.svg +1 -0
  88. package/public/file-icons/folder-config.svg +1 -0
  89. package/public/file-icons/folder-console-open.svg +1 -0
  90. package/public/file-icons/folder-console.svg +1 -0
  91. package/public/file-icons/folder-content-open.svg +1 -0
  92. package/public/file-icons/folder-content.svg +1 -0
  93. package/public/file-icons/folder-context-open.svg +1 -0
  94. package/public/file-icons/folder-context.svg +1 -0
  95. package/public/file-icons/folder-contract-open.svg +1 -0
  96. package/public/file-icons/folder-contract.svg +1 -0
  97. package/public/file-icons/folder-controller-open.svg +1 -0
  98. package/public/file-icons/folder-controller.svg +1 -0
  99. package/public/file-icons/folder-core-open.svg +1 -0
  100. package/public/file-icons/folder-core.svg +1 -0
  101. package/public/file-icons/folder-coverage-open.svg +1 -0
  102. package/public/file-icons/folder-coverage.svg +1 -0
  103. package/public/file-icons/folder-css-open.svg +1 -0
  104. package/public/file-icons/folder-css.svg +1 -0
  105. package/public/file-icons/folder-database-open.svg +1 -0
  106. package/public/file-icons/folder-database.svg +1 -0
  107. package/public/file-icons/folder-debug-open.svg +1 -0
  108. package/public/file-icons/folder-debug.svg +1 -0
  109. package/public/file-icons/folder-deprecated-open.svg +1 -0
  110. package/public/file-icons/folder-deprecated.svg +1 -0
  111. package/public/file-icons/folder-desktop-open.svg +1 -0
  112. package/public/file-icons/folder-desktop.svg +1 -0
  113. package/public/file-icons/folder-dist-open.svg +1 -0
  114. package/public/file-icons/folder-dist.svg +1 -0
  115. package/public/file-icons/folder-docker-open.svg +1 -0
  116. package/public/file-icons/folder-docker.svg +1 -0
  117. package/public/file-icons/folder-docs-open.svg +1 -0
  118. package/public/file-icons/folder-docs.svg +1 -0
  119. package/public/file-icons/folder-element-open.svg +1 -0
  120. package/public/file-icons/folder-element.svg +1 -0
  121. package/public/file-icons/folder-environment-open.svg +1 -0
  122. package/public/file-icons/folder-environment.svg +1 -0
  123. package/public/file-icons/folder-event-open.svg +1 -0
  124. package/public/file-icons/folder-event.svg +1 -0
  125. package/public/file-icons/folder-examples-open.svg +1 -0
  126. package/public/file-icons/folder-examples.svg +1 -0
  127. package/public/file-icons/folder-features-open.svg +1 -0
  128. package/public/file-icons/folder-features.svg +1 -0
  129. package/public/file-icons/folder-font-open.svg +1 -0
  130. package/public/file-icons/folder-font.svg +1 -0
  131. package/public/file-icons/folder-functions-open.svg +1 -0
  132. package/public/file-icons/folder-functions.svg +1 -0
  133. package/public/file-icons/folder-generator-open.svg +1 -0
  134. package/public/file-icons/folder-generator.svg +1 -0
  135. package/public/file-icons/folder-git-open.svg +1 -0
  136. package/public/file-icons/folder-git.svg +1 -0
  137. package/public/file-icons/folder-github-open.svg +1 -0
  138. package/public/file-icons/folder-github.svg +1 -0
  139. package/public/file-icons/folder-gitlab-open.svg +1 -0
  140. package/public/file-icons/folder-gitlab.svg +1 -0
  141. package/public/file-icons/folder-graphql-open.svg +1 -0
  142. package/public/file-icons/folder-graphql.svg +1 -0
  143. package/public/file-icons/folder-helper-open.svg +1 -0
  144. package/public/file-icons/folder-helper.svg +1 -0
  145. package/public/file-icons/folder-hook-open.svg +1 -0
  146. package/public/file-icons/folder-hook.svg +1 -0
  147. package/public/file-icons/folder-husky-open.svg +1 -0
  148. package/public/file-icons/folder-husky.svg +1 -0
  149. package/public/file-icons/folder-i18n-open.svg +1 -0
  150. package/public/file-icons/folder-i18n.svg +1 -0
  151. package/public/file-icons/folder-images-open.svg +1 -0
  152. package/public/file-icons/folder-images.svg +1 -0
  153. package/public/file-icons/folder-include-open.svg +1 -0
  154. package/public/file-icons/folder-include.svg +1 -0
  155. package/public/file-icons/folder-intellij-open.svg +1 -0
  156. package/public/file-icons/folder-intellij.svg +1 -0
  157. package/public/file-icons/folder-interface-open.svg +1 -0
  158. package/public/file-icons/folder-interface.svg +1 -0
  159. package/public/file-icons/folder-ios-open.svg +1 -0
  160. package/public/file-icons/folder-ios.svg +1 -0
  161. package/public/file-icons/folder-java-open.svg +1 -0
  162. package/public/file-icons/folder-java.svg +1 -0
  163. package/public/file-icons/folder-job-open.svg +1 -0
  164. package/public/file-icons/folder-job.svg +1 -0
  165. package/public/file-icons/folder-jupyter-open.svg +1 -0
  166. package/public/file-icons/folder-jupyter.svg +1 -0
  167. package/public/file-icons/folder-keys-open.svg +1 -0
  168. package/public/file-icons/folder-keys.svg +1 -0
  169. package/public/file-icons/folder-kubernetes-open.svg +1 -0
  170. package/public/file-icons/folder-kubernetes.svg +1 -0
  171. package/public/file-icons/folder-layout-open.svg +1 -0
  172. package/public/file-icons/folder-layout.svg +1 -0
  173. package/public/file-icons/folder-less-open.svg +1 -0
  174. package/public/file-icons/folder-less.svg +1 -0
  175. package/public/file-icons/folder-lib-open.svg +1 -0
  176. package/public/file-icons/folder-lib.svg +1 -0
  177. package/public/file-icons/folder-log-open.svg +1 -0
  178. package/public/file-icons/folder-log.svg +1 -0
  179. package/public/file-icons/folder-messages-open.svg +1 -0
  180. package/public/file-icons/folder-messages.svg +1 -0
  181. package/public/file-icons/folder-meta-open.svg +1 -0
  182. package/public/file-icons/folder-meta.svg +1 -0
  183. package/public/file-icons/folder-middleware-open.svg +1 -0
  184. package/public/file-icons/folder-middleware.svg +1 -0
  185. package/public/file-icons/folder-migrations-open.svg +1 -0
  186. package/public/file-icons/folder-migrations.svg +1 -0
  187. package/public/file-icons/folder-mobile-open.svg +1 -0
  188. package/public/file-icons/folder-mobile.svg +1 -0
  189. package/public/file-icons/folder-mock-open.svg +1 -0
  190. package/public/file-icons/folder-mock.svg +1 -0
  191. package/public/file-icons/folder-molecule-open.svg +1 -0
  192. package/public/file-icons/folder-molecule.svg +1 -0
  193. package/public/file-icons/folder-next-open.svg +1 -0
  194. package/public/file-icons/folder-next.svg +1 -0
  195. package/public/file-icons/folder-node-open.svg +1 -0
  196. package/public/file-icons/folder-node.svg +1 -0
  197. package/public/file-icons/folder-nuxt-open.svg +1 -0
  198. package/public/file-icons/folder-nuxt.svg +1 -0
  199. package/public/file-icons/folder-open.svg +1 -0
  200. package/public/file-icons/folder-organism-open.svg +1 -0
  201. package/public/file-icons/folder-organism.svg +1 -0
  202. package/public/file-icons/folder-packages-open.svg +1 -0
  203. package/public/file-icons/folder-packages.svg +1 -0
  204. package/public/file-icons/folder-plugin-open.svg +1 -0
  205. package/public/file-icons/folder-plugin.svg +1 -0
  206. package/public/file-icons/folder-prisma-open.svg +1 -0
  207. package/public/file-icons/folder-prisma.svg +1 -0
  208. package/public/file-icons/folder-prompts-open.svg +1 -0
  209. package/public/file-icons/folder-prompts.svg +1 -0
  210. package/public/file-icons/folder-proto-open.svg +1 -0
  211. package/public/file-icons/folder-proto.svg +1 -0
  212. package/public/file-icons/folder-public-open.svg +1 -0
  213. package/public/file-icons/folder-public.svg +1 -0
  214. package/public/file-icons/folder-python-open.svg +1 -0
  215. package/public/file-icons/folder-python.svg +1 -0
  216. package/public/file-icons/folder-queue-open.svg +1 -0
  217. package/public/file-icons/folder-queue.svg +1 -0
  218. package/public/file-icons/folder-resource-open.svg +1 -0
  219. package/public/file-icons/folder-resource.svg +1 -0
  220. package/public/file-icons/folder-review-open.svg +1 -0
  221. package/public/file-icons/folder-review.svg +1 -0
  222. package/public/file-icons/folder-robot-open.svg +1 -0
  223. package/public/file-icons/folder-robot.svg +1 -0
  224. package/public/file-icons/folder-routes-open.svg +1 -0
  225. package/public/file-icons/folder-routes.svg +1 -0
  226. package/public/file-icons/folder-rust-open.svg +1 -0
  227. package/public/file-icons/folder-rust.svg +1 -0
  228. package/public/file-icons/folder-sass-open.svg +1 -0
  229. package/public/file-icons/folder-sass.svg +1 -0
  230. package/public/file-icons/folder-scripts-open.svg +1 -0
  231. package/public/file-icons/folder-scripts.svg +1 -0
  232. package/public/file-icons/folder-secure-open.svg +1 -0
  233. package/public/file-icons/folder-secure.svg +1 -0
  234. package/public/file-icons/folder-seeders-open.svg +1 -0
  235. package/public/file-icons/folder-seeders.svg +1 -0
  236. package/public/file-icons/folder-server-open.svg +1 -0
  237. package/public/file-icons/folder-server.svg +1 -0
  238. package/public/file-icons/folder-shared-open.svg +1 -0
  239. package/public/file-icons/folder-shared.svg +1 -0
  240. package/public/file-icons/folder-skills-open.svg +1 -0
  241. package/public/file-icons/folder-skills.svg +1 -0
  242. package/public/file-icons/folder-src-open.svg +1 -0
  243. package/public/file-icons/folder-src.svg +1 -0
  244. package/public/file-icons/folder-store-open.svg +1 -0
  245. package/public/file-icons/folder-store.svg +1 -0
  246. package/public/file-icons/folder-storybook-open.svg +1 -0
  247. package/public/file-icons/folder-storybook.svg +1 -0
  248. package/public/file-icons/folder-supabase-open.svg +1 -0
  249. package/public/file-icons/folder-supabase.svg +1 -0
  250. package/public/file-icons/folder-svelte-open.svg +1 -0
  251. package/public/file-icons/folder-svelte.svg +1 -0
  252. package/public/file-icons/folder-target-open.svg +1 -0
  253. package/public/file-icons/folder-target.svg +1 -0
  254. package/public/file-icons/folder-tasks-open.svg +1 -0
  255. package/public/file-icons/folder-tasks.svg +1 -0
  256. package/public/file-icons/folder-temp-open.svg +1 -0
  257. package/public/file-icons/folder-temp.svg +1 -0
  258. package/public/file-icons/folder-template-open.svg +1 -0
  259. package/public/file-icons/folder-template.svg +1 -0
  260. package/public/file-icons/folder-terraform-open.svg +1 -0
  261. package/public/file-icons/folder-terraform.svg +1 -0
  262. package/public/file-icons/folder-test-open.svg +1 -0
  263. package/public/file-icons/folder-test.svg +1 -0
  264. package/public/file-icons/folder-theme-open.svg +1 -0
  265. package/public/file-icons/folder-theme.svg +1 -0
  266. package/public/file-icons/folder-tools-open.svg +1 -0
  267. package/public/file-icons/folder-tools.svg +1 -0
  268. package/public/file-icons/folder-turborepo-open.svg +1 -0
  269. package/public/file-icons/folder-turborepo.svg +1 -0
  270. package/public/file-icons/folder-typescript-open.svg +1 -0
  271. package/public/file-icons/folder-typescript.svg +1 -0
  272. package/public/file-icons/folder-ui-open.svg +1 -0
  273. package/public/file-icons/folder-ui.svg +1 -0
  274. package/public/file-icons/folder-utils-open.svg +1 -0
  275. package/public/file-icons/folder-utils.svg +1 -0
  276. package/public/file-icons/folder-video-open.svg +1 -0
  277. package/public/file-icons/folder-video.svg +1 -0
  278. package/public/file-icons/folder-views-open.svg +1 -0
  279. package/public/file-icons/folder-views.svg +1 -0
  280. package/public/file-icons/folder-vscode-open.svg +1 -0
  281. package/public/file-icons/folder-vscode.svg +1 -0
  282. package/public/file-icons/folder.svg +1 -0
  283. package/public/file-icons/font.svg +1 -0
  284. package/public/file-icons/fsharp.svg +1 -0
  285. package/public/file-icons/gemfile.svg +1 -0
  286. package/public/file-icons/git.svg +1 -0
  287. package/public/file-icons/go-mod.svg +1 -0
  288. package/public/file-icons/go.svg +1 -0
  289. package/public/file-icons/gradle.svg +1 -0
  290. package/public/file-icons/graphql.svg +1 -0
  291. package/public/file-icons/groovy.svg +1 -0
  292. package/public/file-icons/h.svg +1 -0
  293. package/public/file-icons/handlebars.svg +1 -0
  294. package/public/file-icons/haskell.svg +1 -0
  295. package/public/file-icons/hcl.svg +1 -0
  296. package/public/file-icons/heroku.svg +1 -0
  297. package/public/file-icons/hpp.svg +1 -0
  298. package/public/file-icons/html.svg +1 -0
  299. package/public/file-icons/husky.svg +1 -0
  300. package/public/file-icons/i18n.svg +1 -0
  301. package/public/file-icons/image.svg +1 -0
  302. package/public/file-icons/java.svg +1 -0
  303. package/public/file-icons/javascript.svg +1 -0
  304. package/public/file-icons/jenkins.svg +1 -0
  305. package/public/file-icons/jest.svg +1 -0
  306. package/public/file-icons/jsconfig.svg +1 -0
  307. package/public/file-icons/json.svg +1 -0
  308. package/public/file-icons/julia.svg +1 -0
  309. package/public/file-icons/jupyter.svg +1 -0
  310. package/public/file-icons/just.svg +1 -0
  311. package/public/file-icons/key.svg +1 -0
  312. package/public/file-icons/kotlin.svg +1 -0
  313. package/public/file-icons/lerna.svg +1 -0
  314. package/public/file-icons/less.svg +1 -0
  315. package/public/file-icons/license.svg +1 -0
  316. package/public/file-icons/lintstaged.svg +1 -0
  317. package/public/file-icons/liquid.svg +1 -0
  318. package/public/file-icons/lock.svg +1 -0
  319. package/public/file-icons/log.svg +1 -0
  320. package/public/file-icons/lua.svg +1 -0
  321. package/public/file-icons/makefile.svg +1 -0
  322. package/public/file-icons/markdown.svg +1 -0
  323. package/public/file-icons/maven.svg +1 -0
  324. package/public/file-icons/mdx.svg +1 -0
  325. package/public/file-icons/netlify.svg +1 -0
  326. package/public/file-icons/next.svg +1 -0
  327. package/public/file-icons/nim.svg +1 -0
  328. package/public/file-icons/nix.svg +1 -0
  329. package/public/file-icons/nodejs.svg +1 -0
  330. package/public/file-icons/nodemon.svg +1 -0
  331. package/public/file-icons/npm.svg +1 -0
  332. package/public/file-icons/nx.svg +1 -0
  333. package/public/file-icons/objective-c.svg +1 -0
  334. package/public/file-icons/objective-cpp.svg +1 -0
  335. package/public/file-icons/ocaml.svg +1 -0
  336. package/public/file-icons/pdf.svg +1 -0
  337. package/public/file-icons/php.svg +1 -0
  338. package/public/file-icons/pm2-ecosystem.svg +1 -0
  339. package/public/file-icons/pnpm.svg +1 -0
  340. package/public/file-icons/poetry.svg +1 -0
  341. package/public/file-icons/postcss.svg +1 -0
  342. package/public/file-icons/powerpoint.svg +1 -0
  343. package/public/file-icons/powershell.svg +1 -0
  344. package/public/file-icons/prettier.svg +1 -0
  345. package/public/file-icons/prisma.svg +1 -0
  346. package/public/file-icons/prolog.svg +1 -0
  347. package/public/file-icons/proto.svg +1 -0
  348. package/public/file-icons/pug.svg +1 -0
  349. package/public/file-icons/python-misc.svg +1 -0
  350. package/public/file-icons/python.svg +1 -0
  351. package/public/file-icons/r.svg +1 -0
  352. package/public/file-icons/razor.svg +1 -0
  353. package/public/file-icons/react.svg +1 -0
  354. package/public/file-icons/react_ts.svg +1 -0
  355. package/public/file-icons/readme.svg +1 -0
  356. package/public/file-icons/renovate.svg +1 -0
  357. package/public/file-icons/robots.svg +1 -0
  358. package/public/file-icons/rollup.svg +1 -0
  359. package/public/file-icons/ruby.svg +1 -0
  360. package/public/file-icons/rust.svg +1 -0
  361. package/public/file-icons/sass.svg +1 -0
  362. package/public/file-icons/scala.svg +1 -0
  363. package/public/file-icons/semantic-release.svg +1 -0
  364. package/public/file-icons/settings.svg +1 -0
  365. package/public/file-icons/shader.svg +1 -0
  366. package/public/file-icons/sml.svg +1 -0
  367. package/public/file-icons/solidity.svg +1 -0
  368. package/public/file-icons/stylus.svg +1 -0
  369. package/public/file-icons/svelte.svg +1 -0
  370. package/public/file-icons/svg.svg +1 -0
  371. package/public/file-icons/swift.svg +1 -0
  372. package/public/file-icons/table.svg +1 -0
  373. package/public/file-icons/tailwindcss.svg +1 -0
  374. package/public/file-icons/taskfile.svg +1 -0
  375. package/public/file-icons/terraform.svg +1 -0
  376. package/public/file-icons/test-js.svg +1 -0
  377. package/public/file-icons/test-jsx.svg +1 -0
  378. package/public/file-icons/test-ts.svg +1 -0
  379. package/public/file-icons/tex.svg +1 -0
  380. package/public/file-icons/toml.svg +1 -0
  381. package/public/file-icons/travis.svg +1 -0
  382. package/public/file-icons/tsconfig.svg +1 -0
  383. package/public/file-icons/tune.svg +1 -0
  384. package/public/file-icons/turborepo.svg +1 -0
  385. package/public/file-icons/typescript-def.svg +1 -0
  386. package/public/file-icons/typescript.svg +1 -0
  387. package/public/file-icons/unity.svg +1 -0
  388. package/public/file-icons/uv.svg +1 -0
  389. package/public/file-icons/vala.svg +1 -0
  390. package/public/file-icons/vercel.svg +1 -0
  391. package/public/file-icons/video.svg +1 -0
  392. package/public/file-icons/vim.svg +1 -0
  393. package/public/file-icons/vite.svg +1 -0
  394. package/public/file-icons/vitest.svg +1 -0
  395. package/public/file-icons/vlang.svg +1 -0
  396. package/public/file-icons/vue.svg +1 -0
  397. package/public/file-icons/webassembly.svg +1 -0
  398. package/public/file-icons/webpack.svg +1 -0
  399. package/public/file-icons/word.svg +1 -0
  400. package/public/file-icons/xml.svg +1 -0
  401. package/public/file-icons/yaml.svg +1 -0
  402. package/public/file-icons/yarn.svg +1 -0
  403. package/public/file-icons/zig.svg +1 -0
  404. package/public/file-icons/zip.svg +1 -0
  405. package/public/provider-icons/antgroup-color.svg +1 -0
  406. package/public/provider-icons/anthropic.svg +1 -0
  407. package/public/provider-icons/azure-color.svg +1 -0
  408. package/public/provider-icons/baseten.svg +1 -0
  409. package/public/provider-icons/bedrock-color.svg +1 -0
  410. package/public/provider-icons/cerebras-color.svg +1 -0
  411. package/public/provider-icons/cloudflare-color.svg +1 -0
  412. package/public/provider-icons/deepseek-color.svg +1 -0
  413. package/public/provider-icons/fireworks-color.svg +1 -0
  414. package/public/provider-icons/githubcopilot.svg +1 -0
  415. package/public/provider-icons/google-color.svg +1 -0
  416. package/public/provider-icons/googlecloud-color.svg +1 -0
  417. package/public/provider-icons/groq.svg +1 -0
  418. package/public/provider-icons/huggingface-color.svg +1 -0
  419. package/public/provider-icons/kimi-color.svg +1 -0
  420. package/public/provider-icons/minimax-color.svg +1 -0
  421. package/public/provider-icons/mistral-color.svg +1 -0
  422. package/public/provider-icons/moonshot.svg +1 -0
  423. package/public/provider-icons/nvidia-color.svg +1 -0
  424. package/public/provider-icons/openai.svg +1 -0
  425. package/public/provider-icons/opencode.svg +1 -0
  426. package/public/provider-icons/openrouter-color.svg +1 -0
  427. package/public/provider-icons/qwen-color.svg +1 -0
  428. package/public/provider-icons/together-color.svg +1 -0
  429. package/public/provider-icons/vercel.svg +1 -0
  430. package/public/provider-icons/xai.svg +1 -0
  431. package/public/provider-icons/xiaomimimo.svg +1 -0
  432. package/public/provider-icons/zai.svg +1 -0
  433. package/scripts/build-output.mjs +131 -0
  434. package/scripts/dev.mjs +18 -0
  435. package/scripts/entrypoint.mjs +8 -0
  436. package/scripts/install-build.mjs +134 -0
  437. package/scripts/launcher.mjs +249 -0
  438. package/scripts/pick-folder.ps1 +110 -0
  439. package/scripts/postinstall.mjs +12 -0
  440. package/scripts/process-runner.mjs +60 -0
  441. package/scripts/release.mjs +172 -0
  442. package/scripts/sync-file-icons.mjs +118 -0
  443. package/scripts/sync-provider-icons.mjs +41 -0
  444. package/src/app/api/agent/[id]/events/route.ts +81 -0
  445. package/src/app/api/agent/[id]/route.ts +24 -0
  446. package/src/app/api/agent/[id]/tree/route.ts +49 -0
  447. package/src/app/api/agent/new/route.ts +19 -0
  448. package/src/app/api/config/route.ts +22 -0
  449. package/src/app/api/files/route.ts +63 -0
  450. package/src/app/api/git/route.ts +32 -0
  451. package/src/app/api/growth/route.ts +50 -0
  452. package/src/app/api/health/route.ts +7 -0
  453. package/src/app/api/models/route.ts +129 -0
  454. package/src/app/api/pi-settings/route.ts +22 -0
  455. package/src/app/api/plugins/route.ts +79 -0
  456. package/src/app/api/security/route.ts +41 -0
  457. package/src/app/api/sessions/[id]/export/route.ts +25 -0
  458. package/src/app/api/sessions/[id]/route.ts +37 -0
  459. package/src/app/api/sessions/route.ts +23 -0
  460. package/src/app/api/skills/route.ts +39 -0
  461. package/src/app/api/update/route.ts +33 -0
  462. package/src/app/api/version/route.ts +8 -0
  463. package/src/app/api/web-auth/route.ts +80 -0
  464. package/src/app/api/workspaces/route.ts +78 -0
  465. package/src/app/globals.css +2039 -0
  466. package/src/app/icon.svg +21 -0
  467. package/src/app/layout.tsx +29 -0
  468. package/src/app/login/page.tsx +111 -0
  469. package/src/app/page.tsx +10 -0
  470. package/src/components/AppShell.tsx +1171 -0
  471. package/src/components/ChatInput.tsx +729 -0
  472. package/src/components/ChatWindow.tsx +1320 -0
  473. package/src/components/ContextMeter.tsx +131 -0
  474. package/src/components/DiffView.tsx +139 -0
  475. package/src/components/ExtensionUI.tsx +181 -0
  476. package/src/components/FileViewer.tsx +555 -0
  477. package/src/components/FullDiff.tsx +220 -0
  478. package/src/components/GitPanel.tsx +357 -0
  479. package/src/components/GrowthTree.tsx +317 -0
  480. package/src/components/ModelSelector.tsx +286 -0
  481. package/src/components/OutlineRail.tsx +166 -0
  482. package/src/components/PiMark.tsx +79 -0
  483. package/src/components/ProjectPanel.tsx +309 -0
  484. package/src/components/ProviderBrand.tsx +28 -0
  485. package/src/components/ProviderSetupModal.module.css +560 -0
  486. package/src/components/ProviderSetupModal.tsx +559 -0
  487. package/src/components/SessionSidebar.tsx +1066 -0
  488. package/src/components/SettingsPanel.tsx +2216 -0
  489. package/src/components/TrajectoryAnalyzer.module.css +225 -0
  490. package/src/components/TrajectoryView.tsx +289 -0
  491. package/src/components/icons.tsx +443 -0
  492. package/src/hooks/useFileViewer.ts +58 -0
  493. package/src/hooks/useGrowth.ts +635 -0
  494. package/src/hooks/usePiWeb.ts +900 -0
  495. package/src/i18n.tsx +1043 -0
  496. package/src/lib/agent-manager.ts +1348 -0
  497. package/src/lib/app-root.ts +4 -0
  498. package/src/lib/auth.ts +51 -0
  499. package/src/lib/command-validation.ts +116 -0
  500. package/src/lib/enter-behavior.ts +40 -0
  501. package/src/lib/extension-ui.ts +134 -0
  502. package/src/lib/file-icons.json +557 -0
  503. package/src/lib/file-icons.ts +39 -0
  504. package/src/lib/files-service.ts +280 -0
  505. package/src/lib/git-service.ts +255 -0
  506. package/src/lib/growth-rounds.ts +84 -0
  507. package/src/lib/growth-service.ts +586 -0
  508. package/src/lib/growth-tracker.ts +328 -0
  509. package/src/lib/growth-tree.ts +245 -0
  510. package/src/lib/growth-turns.ts +23 -0
  511. package/src/lib/highlight.ts +59 -0
  512. package/src/lib/model-draft.ts +103 -0
  513. package/src/lib/models-service.ts +852 -0
  514. package/src/lib/path-security.ts +101 -0
  515. package/src/lib/pi-settings.ts +115 -0
  516. package/src/lib/pi.ts +367 -0
  517. package/src/lib/plugins-service.ts +312 -0
  518. package/src/lib/process-format.ts +261 -0
  519. package/src/lib/provider-display.ts +57 -0
  520. package/src/lib/security-service.ts +52 -0
  521. package/src/lib/semver.ts +39 -0
  522. package/src/lib/session-reader.ts +204 -0
  523. package/src/lib/settings-write-lock.ts +45 -0
  524. package/src/lib/skills-service.ts +97 -0
  525. package/src/lib/text-sanitize.ts +20 -0
  526. package/src/lib/theme.ts +137 -0
  527. package/src/lib/trajectory.ts +294 -0
  528. package/src/lib/types.ts +300 -0
  529. package/src/lib/update-service.ts +129 -0
  530. package/src/lib/version.ts +6 -0
  531. package/src/lib/web-auth-shared.ts +35 -0
  532. package/src/lib/web-auth.ts +53 -0
  533. package/src/lib/workspace-store.ts +261 -0
  534. package/src/proxy.ts +53 -0
  535. package/tsconfig.json +49 -0
@@ -0,0 +1,280 @@
1
+ /**
2
+ * files-service:会话工作区的文件浏览(详情栏「文件」面板)。
3
+ * 安全边界:目标路径必须落在工作区根内(realpath 双重校验,防符号链接逃逸)。
4
+ */
5
+ import { spawn } from "node:child_process";
6
+ import fs from "node:fs/promises";
7
+ import path from "node:path";
8
+ import { BoundaryError, isPathInside, resolveWorkspacePath } from "./path-security";
9
+ import { getAgentDir } from "./pi";
10
+
11
+ export interface FileEntry {
12
+ name: string;
13
+ kind: "dir" | "file";
14
+ size?: number;
15
+ }
16
+
17
+ export interface DirListing {
18
+ /** 工作区根下的相对路径("/" 分隔;根目录为 ".") */
19
+ dir: string;
20
+ entries: FileEntry[];
21
+ /** 单目录分页;客户端可继续拉取,直到 nextOffset 为 null。 */
22
+ nextOffset: number | null;
23
+ truncated: boolean;
24
+ }
25
+
26
+ /** 文本预览上限:256 KB——详情栏预览用,不是文件编辑器 */
27
+ const MAX_READ_BYTES = 256 * 1024;
28
+ /** 单次目录响应上限;后续条目通过 offset 分页获取。 */
29
+ const MAX_ENTRIES = 2000;
30
+
31
+ function toRel(root: string, target: string): string {
32
+ const rel = path.relative(root, target).replace(/\\/g, "/");
33
+ return rel === "" ? "." : rel;
34
+ }
35
+
36
+ export async function listWorkspaceDir(cwdValue: unknown, relPath: unknown, offsetValue: unknown = 0): Promise<DirListing> {
37
+ const root = await resolveWorkspacePath(cwdValue);
38
+ const offset = offsetValue === null || offsetValue === "" ? 0 : Number(offsetValue);
39
+ if (!Number.isSafeInteger(offset) || offset < 0) throw new BoundaryError("invalid directory offset");
40
+ const rel = typeof relPath === "string" && relPath.trim() ? relPath.trim().replace(/^[/\\]+/, "") : "";
41
+ const target = path.resolve(root, rel);
42
+ if (!isPathInside(root, target)) throw new BoundaryError("path escapes the workspace");
43
+ const real = await fs.realpath(target).catch(() => {
44
+ throw new BoundaryError("directory not found");
45
+ });
46
+ if (!isPathInside(root, real)) throw new BoundaryError("path escapes the workspace");
47
+ const stat = await fs.stat(real);
48
+ if (!stat.isDirectory()) throw new BoundaryError("not a directory");
49
+
50
+ const raw = await fs.readdir(real, { withFileTypes: true });
51
+ const entries: FileEntry[] = [];
52
+ const special: typeof raw = [];
53
+ for (const item of raw) {
54
+ if (item.isDirectory()) {
55
+ entries.push({ name: item.name, kind: "dir" });
56
+ continue;
57
+ }
58
+ if (item.isFile()) {
59
+ entries.push({ name: item.name, kind: "file" });
60
+ continue;
61
+ }
62
+ special.push(item);
63
+ }
64
+ // Symlinks/junctions need stat before sorting because they may be directories.
65
+ for (let i = 0; i < special.length; i += 64) {
66
+ const inspected = await Promise.all(special.slice(i, i + 64).map(async (item) => {
67
+ const info = await fs.stat(path.join(real, item.name)).catch(() => null);
68
+ if (!info) return null;
69
+ return { name: item.name, kind: info.isDirectory() ? "dir" : "file", size: info.isDirectory() ? undefined : info.size } as FileEntry;
70
+ }));
71
+ for (const entry of inspected) if (entry) entries.push(entry);
72
+ }
73
+ entries.sort((a, b) => {
74
+ if (a.kind !== b.kind) return a.kind === "dir" ? -1 : 1;
75
+ return a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: "base" });
76
+ });
77
+ const nextOffset = offset + MAX_ENTRIES < entries.length ? offset + MAX_ENTRIES : null;
78
+ const page = entries.slice(offset, offset + MAX_ENTRIES);
79
+ for (let i = 0; i < page.length; i += 64) {
80
+ await Promise.all(page.slice(i, i + 64).map(async (entry) => {
81
+ if (entry.kind !== "file" || entry.size !== undefined) return;
82
+ const info = await fs.stat(path.join(real, entry.name)).catch(() => null);
83
+ if (info?.isFile()) entry.size = info.size;
84
+ }));
85
+ }
86
+ return {
87
+ dir: toRel(root, real),
88
+ entries: page,
89
+ nextOffset,
90
+ truncated: nextOffset !== null,
91
+ };
92
+ }
93
+
94
+ export interface FilePreview {
95
+ path: string;
96
+ binary: boolean;
97
+ truncated: boolean;
98
+ content: string;
99
+ }
100
+
101
+ export async function readWorkspaceFile(cwdValue: unknown, relPath: unknown): Promise<FilePreview> {
102
+ if (typeof relPath !== "string" || !relPath.trim()) throw new BoundaryError("missing file path");
103
+ const root = await resolveWorkspacePath(cwdValue);
104
+ const target = path.resolve(root, relPath.trim().replace(/^[/\\]+/, ""));
105
+ if (!isPathInside(root, target)) throw new BoundaryError("path escapes the workspace");
106
+ const real = await fs.realpath(target).catch(() => {
107
+ throw new BoundaryError("file not found");
108
+ });
109
+ if (!isPathInside(root, real)) throw new BoundaryError("path escapes the workspace");
110
+ const stat = await fs.stat(real);
111
+ if (!stat.isFile()) throw new BoundaryError("not a file");
112
+
113
+ const handle = await fs.open(real, "r");
114
+ const buffer = Buffer.allocUnsafe(MAX_READ_BYTES + 1);
115
+ let bytesRead = 0;
116
+ try {
117
+ while (bytesRead < buffer.length) {
118
+ const result = await handle.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
119
+ if (result.bytesRead === 0) break;
120
+ bytesRead += result.bytesRead;
121
+ }
122
+ } finally {
123
+ await handle.close();
124
+ }
125
+ const preview = buffer.subarray(0, Math.min(bytesRead, MAX_READ_BYTES));
126
+ // 只检查预览范围,绝不为识别二进制而加载整个大文件
127
+ if (preview.includes(0)) {
128
+ return { path: toRel(root, real), binary: true, truncated: false, content: "" };
129
+ }
130
+ const truncated = stat.size > MAX_READ_BYTES || bytesRead > MAX_READ_BYTES;
131
+ return {
132
+ path: toRel(root, real),
133
+ binary: false,
134
+ truncated,
135
+ content: preview.toString("utf8"),
136
+ }
137
+ }
138
+
139
+ // ---------- 拖拽上传(dsh attachment-local 语义:原样字节保存,模型经路径引用) ----------
140
+
141
+ /** 单文件上传上限:100 MB(与 ChatInput 本地校验一致) */
142
+ const MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
143
+ const MAX_UPLOAD_BASE64_CHARS = Math.ceil(MAX_UPLOAD_BYTES / 3) * 4;
144
+
145
+ async function openUniqueUpload(name: string) {
146
+ const safeName = sanitizeUploadName(name);
147
+ const dir = path.join(getAgentDir(), "web-uploads");
148
+ await fs.mkdir(dir, { recursive: true });
149
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
150
+ for (let i = 0; i < 32; i += 1) {
151
+ const target = path.join(dir, `${stamp}_${i ? `${i}_` : ""}${safeName}`);
152
+ try {
153
+ return { handle: await fs.open(target, "wx"), target, safeName };
154
+ } catch (error) {
155
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
156
+ }
157
+ }
158
+ throw new BoundaryError("upload filename collision limit exceeded");
159
+ }
160
+
161
+ /** 文件名净化:剥目录成分与控制字符,限长;空名给占位 */
162
+ function sanitizeUploadName(name: string): string {
163
+ const base = name.replace(/[\\/\u0000-\u001f\u007f]/g, "_").trim();
164
+ const trimmed = base.slice(0, 120);
165
+ return trimmed || "upload.bin";
166
+ }
167
+
168
+ /**
169
+ * 保存拖入的普通文件(非图片):原样字节落盘到 ~/.pi/agent/web-uploads/,
170
+ * 返回绝对路径供消息引用(agent 用 read/bash 等工具访问)。
171
+ */
172
+ export async function saveUpload(name: unknown, dataBase64: unknown): Promise<{ path: string; name: string; size: number }> {
173
+ if (typeof name !== "string" || typeof dataBase64 !== "string" || !dataBase64) throw new BoundaryError("missing upload payload");
174
+ if (dataBase64.length > MAX_UPLOAD_BASE64_CHARS) throw new BoundaryError("file too large (max 100 MB)");
175
+ const buffer = Buffer.from(dataBase64, "base64");
176
+ if (buffer.length === 0) throw new BoundaryError("empty upload");
177
+ if (buffer.length > MAX_UPLOAD_BYTES) throw new BoundaryError("file too large (max 100 MB)");
178
+
179
+ const { handle, target, safeName } = await openUniqueUpload(name);
180
+ try {
181
+ await handle.writeFile(buffer);
182
+ } catch (error) {
183
+ await handle.close();
184
+ await fs.unlink(target).catch(() => undefined);
185
+ throw error;
186
+ }
187
+ await handle.close();
188
+ return { path: target, name: safeName, size: buffer.length };
189
+ }
190
+
191
+ export function parseUploadLength(value: string | null): number {
192
+ if (value === null || !/^\d+$/.test(value)) throw new BoundaryError("missing or invalid upload size");
193
+ const size = Number(value);
194
+ if (!Number.isSafeInteger(size) || size <= 0) throw new BoundaryError("empty or invalid upload size");
195
+ if (size > MAX_UPLOAD_BYTES) throw new BoundaryError("file too large (max 100 MB)");
196
+ return size;
197
+ }
198
+
199
+ export async function saveUploadStream(name: unknown, stream: ReadableStream<Uint8Array> | null, expectedSize?: number): Promise<{ path: string; name: string; size: number }> {
200
+ if (typeof name !== "string" || !name.trim() || !stream) throw new BoundaryError("missing upload payload");
201
+ if (expectedSize !== undefined) parseUploadLength(String(expectedSize));
202
+ const { handle, target, safeName } = await openUniqueUpload(name);
203
+ const reader = stream.getReader();
204
+ let size = 0;
205
+ let complete = false;
206
+ try {
207
+ for (;;) {
208
+ const { done, value } = await reader.read();
209
+ if (done) break;
210
+ size += value.byteLength;
211
+ if (size > MAX_UPLOAD_BYTES) throw new BoundaryError("file too large (max 100 MB)");
212
+ if (expectedSize !== undefined && size > expectedSize) throw new BoundaryError("upload size does not match the declared length");
213
+ let offset = 0;
214
+ while (offset < value.byteLength) {
215
+ const { bytesWritten } = await handle.write(value, offset, value.byteLength - offset, null);
216
+ if (bytesWritten === 0) throw new Error("upload write made no progress");
217
+ offset += bytesWritten;
218
+ }
219
+ }
220
+ if (size === 0) throw new BoundaryError("empty upload");
221
+ if (expectedSize !== undefined && size !== expectedSize) throw new BoundaryError("incomplete upload: size does not match the declared length");
222
+ complete = true;
223
+ return { path: target, name: safeName, size };
224
+ } finally {
225
+ await reader.cancel().catch(() => undefined);
226
+ reader.releaseLock();
227
+ await handle.close();
228
+ if (!complete) await fs.unlink(target).catch(() => undefined);
229
+ }
230
+ }
231
+
232
+ /**
233
+ * 在本机编辑器里打开工作区文件(服务本来就跑在本机)。
234
+ * 默认 `code`(VS Code),可用 PI_WEB_EDITOR 覆盖(cursor / windsurf / subl / notepad …)。
235
+ * VS Code 系列支持 `-g 路径:行号`,其他编辑器只传路径。
236
+ */
237
+ export async function openInEditor(cwdValue: unknown, relOrAbs: unknown, line?: unknown): Promise<{ editor: string; target: string }> {
238
+ if (typeof relOrAbs !== "string" || !relOrAbs.trim()) throw new BoundaryError("missing file path");
239
+ const root = await resolveWorkspacePath(cwdValue);
240
+ const target = path.isAbsolute(relOrAbs) ? path.resolve(relOrAbs) : path.resolve(root, relOrAbs.trim().replace(/^[/\\]+/, ""));
241
+ if (!isPathInside(root, target)) throw new BoundaryError("path escapes the workspace");
242
+ const real = await fs.realpath(target).catch(() => {
243
+ throw new BoundaryError("file not found");
244
+ });
245
+ if (!isPathInside(root, real)) throw new BoundaryError("path escapes the workspace");
246
+
247
+ const editor = (process.env.PI_WEB_EDITOR || "code").trim();
248
+ const gotoCapable = /\b(code|codium|cursor|windsurf|trae)\b/i.test(editor);
249
+ const lineNo = typeof line === "number" && Number.isFinite(line) && line > 0 ? Math.floor(line) : undefined;
250
+ const args = gotoCapable ? ["-g", lineNo ? `${real}:${lineNo}` : real] : [real];
251
+ if (process.platform === "win32" && !isSafeWindowsEditorArgument(`${editor}${real}`)) {
252
+ throw new BoundaryError("file or editor path contains characters unsafe for cmd.exe");
253
+ }
254
+
255
+ await new Promise<void>((resolve, reject) => {
256
+ // Windows 下 code 是 code.cmd,必须经 cmd.exe 启动;Node 会给带空格的参数自动加引号
257
+ const child = process.platform === "win32"
258
+ ? spawn(process.env.ComSpec || "cmd.exe", ["/d", "/c", editor, ...args], { stdio: "ignore", windowsHide: true })
259
+ : spawn(editor, args, { stdio: "ignore", detached: true });
260
+ const timer = setTimeout(() => {
261
+ // 编辑器进程长期不退出(首次启动 GUI)也视为成功
262
+ resolve();
263
+ }, 4000);
264
+ child.on("error", (error) => {
265
+ clearTimeout(timer);
266
+ reject(new Error(`无法启动编辑器 "${editor}":${error.message}`));
267
+ });
268
+ child.on("exit", (code) => {
269
+ clearTimeout(timer);
270
+ if (code === 0 || code === null) resolve();
271
+ else reject(new Error(`编辑器 "${editor}" 退出码 ${code};请确认它在 PATH 中,或用 PI_WEB_EDITOR 指定`));
272
+ });
273
+ if (process.platform !== "win32") child.unref();
274
+ });
275
+ return { editor, target: real };
276
+ }
277
+
278
+ export function isSafeWindowsEditorArgument(value: string): boolean {
279
+ return !/[&|^<>()%!"\x00-\x1f]/.test(value);
280
+ }
@@ -0,0 +1,255 @@
1
+ /**
2
+ * git-service:会话工作区的 Git 状态与差异(详情栏「Git」面板)。
3
+ * 只读:porcelain v2 状态、最近提交、单文件差异、单次提交内容;不做任何写操作。
4
+ * 所有 git 命令以仓库根为 cwd 运行,因为 porcelain 输出的路径都相对仓库根。
5
+ */
6
+ import { execFile } from "node:child_process";
7
+ import fs from "node:fs/promises";
8
+ import path from "node:path";
9
+ import { promisify } from "node:util";
10
+ import { BoundaryError, isPathInside, resolveWorkspacePath } from "./path-security";
11
+
12
+ const exec = promisify(execFile);
13
+
14
+ export interface GitFileEntry {
15
+ path: string;
16
+ /** 暂存区状态字母(" " 表示无) */
17
+ indexStatus: string;
18
+ /** 工作区状态字母(" " 表示无) */
19
+ workStatus: string;
20
+ kind: "modified" | "added" | "deleted" | "renamed" | "untracked" | "conflict";
21
+ }
22
+
23
+ export interface GitCommit {
24
+ hash: string;
25
+ subject: string;
26
+ author: string;
27
+ /** git 的相对时间("2 hours ago") */
28
+ relative: string;
29
+ }
30
+
31
+ export interface GitInfo {
32
+ /** 机器上找不到 git 命令 */
33
+ available: boolean;
34
+ isRepo: boolean;
35
+ /** 仓库根(用于把面板里的相对路径映射回文件面板) */
36
+ root: string | null;
37
+ branch: string | null;
38
+ upstream: string | null;
39
+ ahead: number;
40
+ behind: number;
41
+ detached: boolean;
42
+ files: GitFileEntry[];
43
+ /** 状态读取失败时文件变更未知;不能把空 files 解读为工作区干净。 */
44
+ statusError?: string;
45
+ commits: GitCommit[];
46
+ }
47
+
48
+ const EMPTY: GitInfo = {
49
+ available: true,
50
+ isRepo: false,
51
+ root: null,
52
+ branch: null,
53
+ upstream: null,
54
+ ahead: 0,
55
+ behind: 0,
56
+ detached: false,
57
+ files: [],
58
+ commits: [],
59
+ };
60
+
61
+ /** 单次差异输出上限:超过则截断并标记(详情栏不是完整的 diff 工具) */
62
+ const MAX_DIFF_BYTES = 400 * 1024;
63
+
64
+ function runGit(cwd: string, args: string[], maxBuffer = 4 * 1024 * 1024): Promise<string> {
65
+ return exec("git", args, { cwd, timeout: 15_000, maxBuffer, windowsHide: true, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" } }).then((r) => r.stdout);
66
+ }
67
+
68
+ function kindOf(x: string, y: string): GitFileEntry["kind"] {
69
+ if (x === "U" || y === "U" || (x === "A" && y === "A") || (x === "D" && y === "D")) return "conflict";
70
+ const letter = x !== "." ? x : y;
71
+ if (letter === "A") return "added";
72
+ if (letter === "D") return "deleted";
73
+ if (letter === "R" || letter === "C") return "renamed";
74
+ return "modified";
75
+ }
76
+
77
+ async function repoRoot(cwd: string): Promise<{ available: boolean; root: string | null }> {
78
+ try {
79
+ const out = (await runGit(cwd, ["rev-parse", "--show-toplevel"])).trim();
80
+ return { available: true, root: out ? path.resolve(out) : null };
81
+ } catch (error) {
82
+ const code = (error as NodeJS.ErrnoException).code;
83
+ if (code === "ENOENT") return { available: false, root: null };
84
+ // exit 128:不是仓库
85
+ return { available: true, root: null };
86
+ }
87
+ }
88
+
89
+ export async function gitInfo(cwdValue: unknown): Promise<GitInfo> {
90
+ const cwd = await resolveWorkspacePath(cwdValue);
91
+ const { available, root } = await repoRoot(cwd);
92
+ if (!available) return { ...EMPTY, available: false };
93
+ if (!root) return EMPTY;
94
+
95
+ let branch: string | null = null;
96
+ let upstream: string | null = null;
97
+ let ahead = 0;
98
+ let behind = 0;
99
+ let detached = false;
100
+ let statusError: string | undefined;
101
+ const files: GitFileEntry[] = [];
102
+ try {
103
+ // porcelain v2 是结构化输出,不依赖 "No branch" 这类措辞;-z 让含空格/非 ASCII 的路径也稳定
104
+ const status = await runGit(root, ["status", "--porcelain=v2", "--branch", "-z"]);
105
+ const records = status.split("\0");
106
+ for (let i = 0; i < records.length; i += 1) {
107
+ const line = records[i];
108
+ if (!line) continue;
109
+ if (line.startsWith("# branch.head ")) {
110
+ const head = line.slice("# branch.head ".length);
111
+ if (head === "(detached)") detached = true;
112
+ else branch = head;
113
+ continue;
114
+ }
115
+ if (line.startsWith("# branch.upstream ")) {
116
+ upstream = line.slice("# branch.upstream ".length);
117
+ continue;
118
+ }
119
+ if (line.startsWith("# branch.ab ")) {
120
+ const m = line.match(/\+(\d+) -(\d+)/);
121
+ if (m) {
122
+ ahead = Number(m[1]);
123
+ behind = Number(m[2]);
124
+ }
125
+ continue;
126
+ }
127
+ if (line.startsWith("# ")) continue;
128
+ const type = line[0];
129
+ if (type === "?") {
130
+ files.push({ path: line.slice(2), indexStatus: " ", workStatus: "?", kind: "untracked" });
131
+ continue;
132
+ }
133
+ if (type === "!") continue;
134
+ if (type === "1" || type === "2" || type === "u") {
135
+ const parts = line.split(" ");
136
+ const xy = parts[1] ?? "..";
137
+ const x = xy[0] ?? ".";
138
+ const y = xy[1] ?? ".";
139
+ // 1: <XY> <sub> <mH> <mI> <mW> <hH> <hI> <path>
140
+ // 2: <XY> <sub> <mH> <mI> <mW> <hH> <hI> <X><score> <path> (NUL 后跟原路径,占下一条记录)
141
+ // u: <XY> <sub> <m1> <m2> <m3> <mW> <h1> <h2> <h3> <path>
142
+ const pathIndex = type === "1" ? 8 : type === "2" ? 9 : 10;
143
+ const filePath = parts.slice(pathIndex).join(" ");
144
+ if (type === "2") i += 1; // 跳过 rename 的原路径记录
145
+ if (!filePath) continue;
146
+ files.push({
147
+ path: filePath,
148
+ indexStatus: x === "." ? " " : x,
149
+ workStatus: y === "." ? " " : y,
150
+ kind: kindOf(x, y),
151
+ });
152
+ }
153
+ }
154
+ } catch (error) {
155
+ // 分支与提交可继续展示,但文件状态未知,绝不能回退为「工作区干净」。
156
+ const stderr = (error as { stderr?: unknown })?.stderr;
157
+ statusError = (typeof stderr === "string" ? stderr.trim() : error instanceof Error ? error.message : String(error)) || "git status failed";
158
+ }
159
+
160
+ let commits: GitCommit[] = [];
161
+ try {
162
+ const log = await runGit(root, ["log", "-10", "--pretty=%h%x1f%s%x1f%an%x1f%cr"]);
163
+ commits = log
164
+ .split("\n")
165
+ .filter(Boolean)
166
+ .map((line) => {
167
+ const [hash, subject, author, relative] = line.split("");
168
+ return { hash, subject: subject ?? "", author: author ?? "", relative: relative ?? "" };
169
+ });
170
+ } catch {
171
+ // 空仓库没有提交
172
+ }
173
+
174
+ if (branch === null && !detached) {
175
+ try {
176
+ branch = (await runGit(root, ["rev-parse", "--abbrev-ref", "HEAD"])).trim() || null;
177
+ } catch {
178
+ /* keep null */
179
+ }
180
+ }
181
+ return { available: true, isRepo: true, root, branch, upstream, ahead, behind, detached, files, commits, ...(statusError ? { statusError } : {}) };
182
+ }
183
+
184
+ export interface GitDiffResult {
185
+ patch: string;
186
+ truncated: boolean;
187
+ binary: boolean;
188
+ }
189
+
190
+ function truncate(patch: string): GitDiffResult {
191
+ if (Buffer.byteLength(patch) <= MAX_DIFF_BYTES) return { patch, truncated: false, binary: false };
192
+ return { patch: Buffer.from(patch).subarray(0, MAX_DIFF_BYTES).toString("utf8"), truncated: true, binary: false };
193
+ }
194
+
195
+ function assertRepoRelative(root: string, relPath: unknown): string {
196
+ if (typeof relPath !== "string" || !relPath.trim()) throw new BoundaryError("missing path");
197
+ const abs = path.resolve(root, relPath);
198
+ if (!isPathInside(root, abs)) throw new BoundaryError("path escapes the repository");
199
+ return abs;
200
+ }
201
+
202
+ /** 单文件差异:staged(索引 vs HEAD)、worktree(工作区 vs 索引)、untracked(整文件视为新增) */
203
+ export async function gitDiff(cwdValue: unknown, relPath: unknown, mode: unknown): Promise<GitDiffResult> {
204
+ const cwd = await resolveWorkspacePath(cwdValue);
205
+ const { available, root } = await repoRoot(cwd);
206
+ if (!available || !root) throw new BoundaryError("not a git repository");
207
+ const abs = assertRepoRelative(root, relPath);
208
+ const rel = path.relative(root, abs).replace(/\\/g, "/");
209
+ if (mode === "untracked") {
210
+ // 未跟踪文件:合成一个全新增的 patch,让前端用同一套 diff 视图展示
211
+ const realRoot = await fs.realpath(root);
212
+ const realFile = await fs.realpath(abs).catch(() => { throw new BoundaryError("file not found"); });
213
+ if (!isPathInside(realRoot, realFile)) throw new BoundaryError("path escapes the repository");
214
+ const stat = await fs.stat(realFile).catch(() => null);
215
+ if (!stat?.isFile()) throw new BoundaryError("file not found");
216
+ const buffer = Buffer.alloc(Math.min(stat.size, MAX_DIFF_BYTES + 1));
217
+ const handle = await fs.open(realFile, "r");
218
+ let bytesRead = 0;
219
+ try {
220
+ while (bytesRead < buffer.length) {
221
+ const read = await handle.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
222
+ if (read.bytesRead === 0) break;
223
+ bytesRead += read.bytesRead;
224
+ }
225
+ } finally {
226
+ await handle.close();
227
+ }
228
+ const head = buffer.subarray(0, bytesRead);
229
+ if (head.includes(0)) return { patch: "", truncated: false, binary: true };
230
+ const text = head.subarray(0, MAX_DIFF_BYTES).toString("utf8").replace(/\r/g, "");
231
+ const lines = text.split("\n");
232
+ if (lines[lines.length - 1] === "") lines.pop();
233
+ const body = lines.map((l) => `+${l}`).join("\n");
234
+ return {
235
+ patch: `--- /dev/null\n+++ b/${rel}\n@@ -0,0 +1,${lines.length} @@\n${body}\n`,
236
+ truncated: stat.size > MAX_DIFF_BYTES,
237
+ binary: false,
238
+ };
239
+ }
240
+ const args = mode === "staged" ? ["diff", "--cached", "--no-color", "--", rel] : ["diff", "--no-color", "--", rel];
241
+ const out = await runGit(root, args, 8 * 1024 * 1024);
242
+ if (/^Binary files .* differ$/m.test(out)) return { patch: "", truncated: false, binary: true };
243
+ return truncate(out);
244
+ }
245
+
246
+ /** 单次提交的内容(含各文件 patch) */
247
+ export async function gitShow(cwdValue: unknown, hash: unknown): Promise<GitDiffResult & { title: string }> {
248
+ const cwd = await resolveWorkspacePath(cwdValue);
249
+ const { available, root } = await repoRoot(cwd);
250
+ if (!available || !root) throw new BoundaryError("not a git repository");
251
+ if (typeof hash !== "string" || !/^[0-9a-f]{4,40}$/i.test(hash)) throw new BoundaryError("invalid commit hash");
252
+ const title = (await runGit(root, ["log", "-1", "--pretty=%h %s%n%an · %cr", hash])).trim();
253
+ const out = await runGit(root, ["show", "--no-color", "--format=", "--patch", hash], 8 * 1024 * 1024);
254
+ return { title, ...truncate(out) };
255
+ }
@@ -0,0 +1,84 @@
1
+ import type { GrowthStep } from "./types";
2
+
3
+ export interface GrowthRound {
4
+ /** 真实用户轮次;不会因没有文件改动而跳号。 */
5
+ id: number;
6
+ startedAt: number;
7
+ first: GrowthStep | null;
8
+ /** 此轮结束时可用的最后一张快照;无改动轮沿用前一张。 */
9
+ last: GrowthStep | null;
10
+ steps: GrowthStep[];
11
+ /** 相邻两轮终点的 tree;第一轮从 prompt 前的基线开始。 */
12
+ fromTree: string | null;
13
+ toTree: string | null;
14
+ /** 旧会话可能在启用生长账本前已发生,不能假装它是零改动。 */
15
+ recorded: boolean;
16
+ }
17
+
18
+ /** 兼容没有消息时间戳的调用者:按账本里的 turn 标记分组。 */
19
+ function ledgerRounds(steps: GrowthStep[]): GrowthRound[] {
20
+ const rounds: GrowthRound[] = [];
21
+ let current: GrowthStep[] = [];
22
+ const flush = () => {
23
+ if (!current.length) return;
24
+ const first = current[0];
25
+ const last = current[current.length - 1];
26
+ rounds.push({ id: rounds.length + 1, startedAt: first.ts, first, last, steps: current, fromTree: first.parent, toTree: last.tree, recorded: true });
27
+ current = [];
28
+ };
29
+ for (const [index, step] of steps.entries()) {
30
+ if (index === 0 && step.initial) continue;
31
+ if (step.kind === "baseline") {
32
+ flush();
33
+ continue;
34
+ }
35
+ if (step.kind === "external" || step.kind === "manual") flush();
36
+ current.push(step);
37
+ if (step.kind === "turn" || step.kind === "external" || step.kind === "manual") flush();
38
+ }
39
+ flush();
40
+ return rounds;
41
+ }
42
+
43
+ /** 轮数以持久会话的用户消息为准;快照只负责证明这一轮的前后树与差异。 */
44
+ export function buildGrowthRounds(steps: GrowthStep[], turnStarts?: number[]): GrowthRound[] {
45
+ if (turnStarts === undefined) return ledgerRounds(steps);
46
+ if (!turnStarts.length) return [];
47
+ const starts: number[] = [];
48
+ for (const value of turnStarts) {
49
+ const previous = starts[starts.length - 1];
50
+ starts.push(Number.isFinite(value) ? Math.max(value, previous === undefined ? value : previous + 0.001) : (previous ?? 0) + 0.001);
51
+ }
52
+ const ordered = [...steps].sort((a, b) => a.ts - b.ts || a.seq - b.seq);
53
+ const rounds: GrowthRound[] = [];
54
+ let cursor = 0;
55
+ let before: GrowthStep | null = null;
56
+ let previousEnd: GrowthStep | null = null;
57
+ for (let index = 0; index < starts.length; index += 1) {
58
+ const start = starts[index];
59
+ const end = starts[index + 1] ?? Infinity;
60
+ while (cursor < ordered.length && ordered[cursor].ts < start) before = ordered[cursor++];
61
+ const atStart = before;
62
+ const current: GrowthStep[] = [];
63
+ while (cursor < ordered.length && ordered[cursor].ts < end) {
64
+ const step = ordered[cursor++];
65
+ if (step.kind !== "baseline") current.push(step);
66
+ before = step;
67
+ }
68
+ const first = current[0] ?? null;
69
+ const last: GrowthStep | null = current[current.length - 1] ?? (atStart && (!previousEnd || atStart.ts > previousEnd.ts) ? atStart : previousEnd);
70
+ const fromTree = previousEnd?.tree ?? atStart?.tree ?? first?.parent ?? null;
71
+ rounds.push({
72
+ id: index + 1,
73
+ startedAt: start,
74
+ first,
75
+ last,
76
+ steps: current,
77
+ fromTree,
78
+ toTree: last?.tree ?? null,
79
+ recorded: current.length > 0 && fromTree !== null && !first?.initial,
80
+ });
81
+ previousEnd = last;
82
+ }
83
+ return rounds;
84
+ }