@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.
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/assets/showcase.png +0 -0
- package/bin/pi.js +50 -0
- package/bin/piweb.js +12 -0
- package/next.config.ts +21 -0
- package/package.json +88 -0
- package/postcss.config.mjs +7 -0
- package/public/file-icons/3d.svg +1 -0
- package/public/file-icons/LICENSE +8 -0
- package/public/file-icons/adobe-illustrator.svg +1 -0
- package/public/file-icons/adobe-photoshop.svg +1 -0
- package/public/file-icons/agent.svg +1 -0
- package/public/file-icons/asciidoc.svg +1 -0
- package/public/file-icons/assembly.svg +1 -0
- package/public/file-icons/astro.svg +1 -0
- package/public/file-icons/audio.svg +1 -0
- package/public/file-icons/authors.svg +1 -0
- package/public/file-icons/babel.svg +1 -0
- package/public/file-icons/bibliography.svg +1 -0
- package/public/file-icons/bicep.svg +1 -0
- package/public/file-icons/blender.svg +1 -0
- package/public/file-icons/bun.svg +1 -0
- package/public/file-icons/c.svg +1 -0
- package/public/file-icons/certificate.svg +1 -0
- package/public/file-icons/changelog.svg +1 -0
- package/public/file-icons/clojure.svg +1 -0
- package/public/file-icons/cmake.svg +1 -0
- package/public/file-icons/codeowners.svg +1 -0
- package/public/file-icons/commitlint.svg +1 -0
- package/public/file-icons/conduct.svg +1 -0
- package/public/file-icons/console.svg +1 -0
- package/public/file-icons/contributing.svg +1 -0
- package/public/file-icons/cpp.svg +1 -0
- package/public/file-icons/csharp.svg +1 -0
- package/public/file-icons/css.svg +1 -0
- package/public/file-icons/cursor.svg +1 -0
- package/public/file-icons/dart.svg +1 -0
- package/public/file-icons/database.svg +1 -0
- package/public/file-icons/deno.svg +1 -0
- package/public/file-icons/diff.svg +1 -0
- package/public/file-icons/docker.svg +1 -0
- package/public/file-icons/document.svg +1 -0
- package/public/file-icons/editorconfig.svg +1 -0
- package/public/file-icons/ejs.svg +1 -0
- package/public/file-icons/elixir.svg +1 -0
- package/public/file-icons/elm.svg +1 -0
- package/public/file-icons/email.svg +1 -0
- package/public/file-icons/erlang.svg +1 -0
- package/public/file-icons/eslint.svg +1 -0
- package/public/file-icons/favicon.svg +1 -0
- package/public/file-icons/file.svg +1 -0
- package/public/file-icons/folder-admin-open.svg +1 -0
- package/public/file-icons/folder-admin.svg +1 -0
- package/public/file-icons/folder-android-open.svg +1 -0
- package/public/file-icons/folder-android.svg +1 -0
- package/public/file-icons/folder-api-open.svg +1 -0
- package/public/file-icons/folder-api.svg +1 -0
- package/public/file-icons/folder-app-open.svg +1 -0
- package/public/file-icons/folder-app.svg +1 -0
- package/public/file-icons/folder-archive-open.svg +1 -0
- package/public/file-icons/folder-archive.svg +1 -0
- package/public/file-icons/folder-atom-open.svg +1 -0
- package/public/file-icons/folder-atom.svg +1 -0
- package/public/file-icons/folder-audio-open.svg +1 -0
- package/public/file-icons/folder-audio.svg +1 -0
- package/public/file-icons/folder-aws-open.svg +1 -0
- package/public/file-icons/folder-aws.svg +1 -0
- package/public/file-icons/folder-backup-open.svg +1 -0
- package/public/file-icons/folder-backup.svg +1 -0
- package/public/file-icons/folder-benchmark-open.svg +1 -0
- package/public/file-icons/folder-benchmark.svg +1 -0
- package/public/file-icons/folder-cart-open.svg +1 -0
- package/public/file-icons/folder-cart.svg +1 -0
- package/public/file-icons/folder-ci-open.svg +1 -0
- package/public/file-icons/folder-ci.svg +1 -0
- package/public/file-icons/folder-circleci-open.svg +1 -0
- package/public/file-icons/folder-circleci.svg +1 -0
- package/public/file-icons/folder-class-open.svg +1 -0
- package/public/file-icons/folder-class.svg +1 -0
- package/public/file-icons/folder-client-open.svg +1 -0
- package/public/file-icons/folder-client.svg +1 -0
- package/public/file-icons/folder-command-open.svg +1 -0
- package/public/file-icons/folder-command.svg +1 -0
- package/public/file-icons/folder-components-open.svg +1 -0
- package/public/file-icons/folder-components.svg +1 -0
- package/public/file-icons/folder-config-open.svg +1 -0
- package/public/file-icons/folder-config.svg +1 -0
- package/public/file-icons/folder-console-open.svg +1 -0
- package/public/file-icons/folder-console.svg +1 -0
- package/public/file-icons/folder-content-open.svg +1 -0
- package/public/file-icons/folder-content.svg +1 -0
- package/public/file-icons/folder-context-open.svg +1 -0
- package/public/file-icons/folder-context.svg +1 -0
- package/public/file-icons/folder-contract-open.svg +1 -0
- package/public/file-icons/folder-contract.svg +1 -0
- package/public/file-icons/folder-controller-open.svg +1 -0
- package/public/file-icons/folder-controller.svg +1 -0
- package/public/file-icons/folder-core-open.svg +1 -0
- package/public/file-icons/folder-core.svg +1 -0
- package/public/file-icons/folder-coverage-open.svg +1 -0
- package/public/file-icons/folder-coverage.svg +1 -0
- package/public/file-icons/folder-css-open.svg +1 -0
- package/public/file-icons/folder-css.svg +1 -0
- package/public/file-icons/folder-database-open.svg +1 -0
- package/public/file-icons/folder-database.svg +1 -0
- package/public/file-icons/folder-debug-open.svg +1 -0
- package/public/file-icons/folder-debug.svg +1 -0
- package/public/file-icons/folder-deprecated-open.svg +1 -0
- package/public/file-icons/folder-deprecated.svg +1 -0
- package/public/file-icons/folder-desktop-open.svg +1 -0
- package/public/file-icons/folder-desktop.svg +1 -0
- package/public/file-icons/folder-dist-open.svg +1 -0
- package/public/file-icons/folder-dist.svg +1 -0
- package/public/file-icons/folder-docker-open.svg +1 -0
- package/public/file-icons/folder-docker.svg +1 -0
- package/public/file-icons/folder-docs-open.svg +1 -0
- package/public/file-icons/folder-docs.svg +1 -0
- package/public/file-icons/folder-element-open.svg +1 -0
- package/public/file-icons/folder-element.svg +1 -0
- package/public/file-icons/folder-environment-open.svg +1 -0
- package/public/file-icons/folder-environment.svg +1 -0
- package/public/file-icons/folder-event-open.svg +1 -0
- package/public/file-icons/folder-event.svg +1 -0
- package/public/file-icons/folder-examples-open.svg +1 -0
- package/public/file-icons/folder-examples.svg +1 -0
- package/public/file-icons/folder-features-open.svg +1 -0
- package/public/file-icons/folder-features.svg +1 -0
- package/public/file-icons/folder-font-open.svg +1 -0
- package/public/file-icons/folder-font.svg +1 -0
- package/public/file-icons/folder-functions-open.svg +1 -0
- package/public/file-icons/folder-functions.svg +1 -0
- package/public/file-icons/folder-generator-open.svg +1 -0
- package/public/file-icons/folder-generator.svg +1 -0
- package/public/file-icons/folder-git-open.svg +1 -0
- package/public/file-icons/folder-git.svg +1 -0
- package/public/file-icons/folder-github-open.svg +1 -0
- package/public/file-icons/folder-github.svg +1 -0
- package/public/file-icons/folder-gitlab-open.svg +1 -0
- package/public/file-icons/folder-gitlab.svg +1 -0
- package/public/file-icons/folder-graphql-open.svg +1 -0
- package/public/file-icons/folder-graphql.svg +1 -0
- package/public/file-icons/folder-helper-open.svg +1 -0
- package/public/file-icons/folder-helper.svg +1 -0
- package/public/file-icons/folder-hook-open.svg +1 -0
- package/public/file-icons/folder-hook.svg +1 -0
- package/public/file-icons/folder-husky-open.svg +1 -0
- package/public/file-icons/folder-husky.svg +1 -0
- package/public/file-icons/folder-i18n-open.svg +1 -0
- package/public/file-icons/folder-i18n.svg +1 -0
- package/public/file-icons/folder-images-open.svg +1 -0
- package/public/file-icons/folder-images.svg +1 -0
- package/public/file-icons/folder-include-open.svg +1 -0
- package/public/file-icons/folder-include.svg +1 -0
- package/public/file-icons/folder-intellij-open.svg +1 -0
- package/public/file-icons/folder-intellij.svg +1 -0
- package/public/file-icons/folder-interface-open.svg +1 -0
- package/public/file-icons/folder-interface.svg +1 -0
- package/public/file-icons/folder-ios-open.svg +1 -0
- package/public/file-icons/folder-ios.svg +1 -0
- package/public/file-icons/folder-java-open.svg +1 -0
- package/public/file-icons/folder-java.svg +1 -0
- package/public/file-icons/folder-job-open.svg +1 -0
- package/public/file-icons/folder-job.svg +1 -0
- package/public/file-icons/folder-jupyter-open.svg +1 -0
- package/public/file-icons/folder-jupyter.svg +1 -0
- package/public/file-icons/folder-keys-open.svg +1 -0
- package/public/file-icons/folder-keys.svg +1 -0
- package/public/file-icons/folder-kubernetes-open.svg +1 -0
- package/public/file-icons/folder-kubernetes.svg +1 -0
- package/public/file-icons/folder-layout-open.svg +1 -0
- package/public/file-icons/folder-layout.svg +1 -0
- package/public/file-icons/folder-less-open.svg +1 -0
- package/public/file-icons/folder-less.svg +1 -0
- package/public/file-icons/folder-lib-open.svg +1 -0
- package/public/file-icons/folder-lib.svg +1 -0
- package/public/file-icons/folder-log-open.svg +1 -0
- package/public/file-icons/folder-log.svg +1 -0
- package/public/file-icons/folder-messages-open.svg +1 -0
- package/public/file-icons/folder-messages.svg +1 -0
- package/public/file-icons/folder-meta-open.svg +1 -0
- package/public/file-icons/folder-meta.svg +1 -0
- package/public/file-icons/folder-middleware-open.svg +1 -0
- package/public/file-icons/folder-middleware.svg +1 -0
- package/public/file-icons/folder-migrations-open.svg +1 -0
- package/public/file-icons/folder-migrations.svg +1 -0
- package/public/file-icons/folder-mobile-open.svg +1 -0
- package/public/file-icons/folder-mobile.svg +1 -0
- package/public/file-icons/folder-mock-open.svg +1 -0
- package/public/file-icons/folder-mock.svg +1 -0
- package/public/file-icons/folder-molecule-open.svg +1 -0
- package/public/file-icons/folder-molecule.svg +1 -0
- package/public/file-icons/folder-next-open.svg +1 -0
- package/public/file-icons/folder-next.svg +1 -0
- package/public/file-icons/folder-node-open.svg +1 -0
- package/public/file-icons/folder-node.svg +1 -0
- package/public/file-icons/folder-nuxt-open.svg +1 -0
- package/public/file-icons/folder-nuxt.svg +1 -0
- package/public/file-icons/folder-open.svg +1 -0
- package/public/file-icons/folder-organism-open.svg +1 -0
- package/public/file-icons/folder-organism.svg +1 -0
- package/public/file-icons/folder-packages-open.svg +1 -0
- package/public/file-icons/folder-packages.svg +1 -0
- package/public/file-icons/folder-plugin-open.svg +1 -0
- package/public/file-icons/folder-plugin.svg +1 -0
- package/public/file-icons/folder-prisma-open.svg +1 -0
- package/public/file-icons/folder-prisma.svg +1 -0
- package/public/file-icons/folder-prompts-open.svg +1 -0
- package/public/file-icons/folder-prompts.svg +1 -0
- package/public/file-icons/folder-proto-open.svg +1 -0
- package/public/file-icons/folder-proto.svg +1 -0
- package/public/file-icons/folder-public-open.svg +1 -0
- package/public/file-icons/folder-public.svg +1 -0
- package/public/file-icons/folder-python-open.svg +1 -0
- package/public/file-icons/folder-python.svg +1 -0
- package/public/file-icons/folder-queue-open.svg +1 -0
- package/public/file-icons/folder-queue.svg +1 -0
- package/public/file-icons/folder-resource-open.svg +1 -0
- package/public/file-icons/folder-resource.svg +1 -0
- package/public/file-icons/folder-review-open.svg +1 -0
- package/public/file-icons/folder-review.svg +1 -0
- package/public/file-icons/folder-robot-open.svg +1 -0
- package/public/file-icons/folder-robot.svg +1 -0
- package/public/file-icons/folder-routes-open.svg +1 -0
- package/public/file-icons/folder-routes.svg +1 -0
- package/public/file-icons/folder-rust-open.svg +1 -0
- package/public/file-icons/folder-rust.svg +1 -0
- package/public/file-icons/folder-sass-open.svg +1 -0
- package/public/file-icons/folder-sass.svg +1 -0
- package/public/file-icons/folder-scripts-open.svg +1 -0
- package/public/file-icons/folder-scripts.svg +1 -0
- package/public/file-icons/folder-secure-open.svg +1 -0
- package/public/file-icons/folder-secure.svg +1 -0
- package/public/file-icons/folder-seeders-open.svg +1 -0
- package/public/file-icons/folder-seeders.svg +1 -0
- package/public/file-icons/folder-server-open.svg +1 -0
- package/public/file-icons/folder-server.svg +1 -0
- package/public/file-icons/folder-shared-open.svg +1 -0
- package/public/file-icons/folder-shared.svg +1 -0
- package/public/file-icons/folder-skills-open.svg +1 -0
- package/public/file-icons/folder-skills.svg +1 -0
- package/public/file-icons/folder-src-open.svg +1 -0
- package/public/file-icons/folder-src.svg +1 -0
- package/public/file-icons/folder-store-open.svg +1 -0
- package/public/file-icons/folder-store.svg +1 -0
- package/public/file-icons/folder-storybook-open.svg +1 -0
- package/public/file-icons/folder-storybook.svg +1 -0
- package/public/file-icons/folder-supabase-open.svg +1 -0
- package/public/file-icons/folder-supabase.svg +1 -0
- package/public/file-icons/folder-svelte-open.svg +1 -0
- package/public/file-icons/folder-svelte.svg +1 -0
- package/public/file-icons/folder-target-open.svg +1 -0
- package/public/file-icons/folder-target.svg +1 -0
- package/public/file-icons/folder-tasks-open.svg +1 -0
- package/public/file-icons/folder-tasks.svg +1 -0
- package/public/file-icons/folder-temp-open.svg +1 -0
- package/public/file-icons/folder-temp.svg +1 -0
- package/public/file-icons/folder-template-open.svg +1 -0
- package/public/file-icons/folder-template.svg +1 -0
- package/public/file-icons/folder-terraform-open.svg +1 -0
- package/public/file-icons/folder-terraform.svg +1 -0
- package/public/file-icons/folder-test-open.svg +1 -0
- package/public/file-icons/folder-test.svg +1 -0
- package/public/file-icons/folder-theme-open.svg +1 -0
- package/public/file-icons/folder-theme.svg +1 -0
- package/public/file-icons/folder-tools-open.svg +1 -0
- package/public/file-icons/folder-tools.svg +1 -0
- package/public/file-icons/folder-turborepo-open.svg +1 -0
- package/public/file-icons/folder-turborepo.svg +1 -0
- package/public/file-icons/folder-typescript-open.svg +1 -0
- package/public/file-icons/folder-typescript.svg +1 -0
- package/public/file-icons/folder-ui-open.svg +1 -0
- package/public/file-icons/folder-ui.svg +1 -0
- package/public/file-icons/folder-utils-open.svg +1 -0
- package/public/file-icons/folder-utils.svg +1 -0
- package/public/file-icons/folder-video-open.svg +1 -0
- package/public/file-icons/folder-video.svg +1 -0
- package/public/file-icons/folder-views-open.svg +1 -0
- package/public/file-icons/folder-views.svg +1 -0
- package/public/file-icons/folder-vscode-open.svg +1 -0
- package/public/file-icons/folder-vscode.svg +1 -0
- package/public/file-icons/folder.svg +1 -0
- package/public/file-icons/font.svg +1 -0
- package/public/file-icons/fsharp.svg +1 -0
- package/public/file-icons/gemfile.svg +1 -0
- package/public/file-icons/git.svg +1 -0
- package/public/file-icons/go-mod.svg +1 -0
- package/public/file-icons/go.svg +1 -0
- package/public/file-icons/gradle.svg +1 -0
- package/public/file-icons/graphql.svg +1 -0
- package/public/file-icons/groovy.svg +1 -0
- package/public/file-icons/h.svg +1 -0
- package/public/file-icons/handlebars.svg +1 -0
- package/public/file-icons/haskell.svg +1 -0
- package/public/file-icons/hcl.svg +1 -0
- package/public/file-icons/heroku.svg +1 -0
- package/public/file-icons/hpp.svg +1 -0
- package/public/file-icons/html.svg +1 -0
- package/public/file-icons/husky.svg +1 -0
- package/public/file-icons/i18n.svg +1 -0
- package/public/file-icons/image.svg +1 -0
- package/public/file-icons/java.svg +1 -0
- package/public/file-icons/javascript.svg +1 -0
- package/public/file-icons/jenkins.svg +1 -0
- package/public/file-icons/jest.svg +1 -0
- package/public/file-icons/jsconfig.svg +1 -0
- package/public/file-icons/json.svg +1 -0
- package/public/file-icons/julia.svg +1 -0
- package/public/file-icons/jupyter.svg +1 -0
- package/public/file-icons/just.svg +1 -0
- package/public/file-icons/key.svg +1 -0
- package/public/file-icons/kotlin.svg +1 -0
- package/public/file-icons/lerna.svg +1 -0
- package/public/file-icons/less.svg +1 -0
- package/public/file-icons/license.svg +1 -0
- package/public/file-icons/lintstaged.svg +1 -0
- package/public/file-icons/liquid.svg +1 -0
- package/public/file-icons/lock.svg +1 -0
- package/public/file-icons/log.svg +1 -0
- package/public/file-icons/lua.svg +1 -0
- package/public/file-icons/makefile.svg +1 -0
- package/public/file-icons/markdown.svg +1 -0
- package/public/file-icons/maven.svg +1 -0
- package/public/file-icons/mdx.svg +1 -0
- package/public/file-icons/netlify.svg +1 -0
- package/public/file-icons/next.svg +1 -0
- package/public/file-icons/nim.svg +1 -0
- package/public/file-icons/nix.svg +1 -0
- package/public/file-icons/nodejs.svg +1 -0
- package/public/file-icons/nodemon.svg +1 -0
- package/public/file-icons/npm.svg +1 -0
- package/public/file-icons/nx.svg +1 -0
- package/public/file-icons/objective-c.svg +1 -0
- package/public/file-icons/objective-cpp.svg +1 -0
- package/public/file-icons/ocaml.svg +1 -0
- package/public/file-icons/pdf.svg +1 -0
- package/public/file-icons/php.svg +1 -0
- package/public/file-icons/pm2-ecosystem.svg +1 -0
- package/public/file-icons/pnpm.svg +1 -0
- package/public/file-icons/poetry.svg +1 -0
- package/public/file-icons/postcss.svg +1 -0
- package/public/file-icons/powerpoint.svg +1 -0
- package/public/file-icons/powershell.svg +1 -0
- package/public/file-icons/prettier.svg +1 -0
- package/public/file-icons/prisma.svg +1 -0
- package/public/file-icons/prolog.svg +1 -0
- package/public/file-icons/proto.svg +1 -0
- package/public/file-icons/pug.svg +1 -0
- package/public/file-icons/python-misc.svg +1 -0
- package/public/file-icons/python.svg +1 -0
- package/public/file-icons/r.svg +1 -0
- package/public/file-icons/razor.svg +1 -0
- package/public/file-icons/react.svg +1 -0
- package/public/file-icons/react_ts.svg +1 -0
- package/public/file-icons/readme.svg +1 -0
- package/public/file-icons/renovate.svg +1 -0
- package/public/file-icons/robots.svg +1 -0
- package/public/file-icons/rollup.svg +1 -0
- package/public/file-icons/ruby.svg +1 -0
- package/public/file-icons/rust.svg +1 -0
- package/public/file-icons/sass.svg +1 -0
- package/public/file-icons/scala.svg +1 -0
- package/public/file-icons/semantic-release.svg +1 -0
- package/public/file-icons/settings.svg +1 -0
- package/public/file-icons/shader.svg +1 -0
- package/public/file-icons/sml.svg +1 -0
- package/public/file-icons/solidity.svg +1 -0
- package/public/file-icons/stylus.svg +1 -0
- package/public/file-icons/svelte.svg +1 -0
- package/public/file-icons/svg.svg +1 -0
- package/public/file-icons/swift.svg +1 -0
- package/public/file-icons/table.svg +1 -0
- package/public/file-icons/tailwindcss.svg +1 -0
- package/public/file-icons/taskfile.svg +1 -0
- package/public/file-icons/terraform.svg +1 -0
- package/public/file-icons/test-js.svg +1 -0
- package/public/file-icons/test-jsx.svg +1 -0
- package/public/file-icons/test-ts.svg +1 -0
- package/public/file-icons/tex.svg +1 -0
- package/public/file-icons/toml.svg +1 -0
- package/public/file-icons/travis.svg +1 -0
- package/public/file-icons/tsconfig.svg +1 -0
- package/public/file-icons/tune.svg +1 -0
- package/public/file-icons/turborepo.svg +1 -0
- package/public/file-icons/typescript-def.svg +1 -0
- package/public/file-icons/typescript.svg +1 -0
- package/public/file-icons/unity.svg +1 -0
- package/public/file-icons/uv.svg +1 -0
- package/public/file-icons/vala.svg +1 -0
- package/public/file-icons/vercel.svg +1 -0
- package/public/file-icons/video.svg +1 -0
- package/public/file-icons/vim.svg +1 -0
- package/public/file-icons/vite.svg +1 -0
- package/public/file-icons/vitest.svg +1 -0
- package/public/file-icons/vlang.svg +1 -0
- package/public/file-icons/vue.svg +1 -0
- package/public/file-icons/webassembly.svg +1 -0
- package/public/file-icons/webpack.svg +1 -0
- package/public/file-icons/word.svg +1 -0
- package/public/file-icons/xml.svg +1 -0
- package/public/file-icons/yaml.svg +1 -0
- package/public/file-icons/yarn.svg +1 -0
- package/public/file-icons/zig.svg +1 -0
- package/public/file-icons/zip.svg +1 -0
- package/public/provider-icons/antgroup-color.svg +1 -0
- package/public/provider-icons/anthropic.svg +1 -0
- package/public/provider-icons/azure-color.svg +1 -0
- package/public/provider-icons/baseten.svg +1 -0
- package/public/provider-icons/bedrock-color.svg +1 -0
- package/public/provider-icons/cerebras-color.svg +1 -0
- package/public/provider-icons/cloudflare-color.svg +1 -0
- package/public/provider-icons/deepseek-color.svg +1 -0
- package/public/provider-icons/fireworks-color.svg +1 -0
- package/public/provider-icons/githubcopilot.svg +1 -0
- package/public/provider-icons/google-color.svg +1 -0
- package/public/provider-icons/googlecloud-color.svg +1 -0
- package/public/provider-icons/groq.svg +1 -0
- package/public/provider-icons/huggingface-color.svg +1 -0
- package/public/provider-icons/kimi-color.svg +1 -0
- package/public/provider-icons/minimax-color.svg +1 -0
- package/public/provider-icons/mistral-color.svg +1 -0
- package/public/provider-icons/moonshot.svg +1 -0
- package/public/provider-icons/nvidia-color.svg +1 -0
- package/public/provider-icons/openai.svg +1 -0
- package/public/provider-icons/opencode.svg +1 -0
- package/public/provider-icons/openrouter-color.svg +1 -0
- package/public/provider-icons/qwen-color.svg +1 -0
- package/public/provider-icons/together-color.svg +1 -0
- package/public/provider-icons/vercel.svg +1 -0
- package/public/provider-icons/xai.svg +1 -0
- package/public/provider-icons/xiaomimimo.svg +1 -0
- package/public/provider-icons/zai.svg +1 -0
- package/scripts/build-output.mjs +131 -0
- package/scripts/dev.mjs +18 -0
- package/scripts/entrypoint.mjs +8 -0
- package/scripts/install-build.mjs +134 -0
- package/scripts/launcher.mjs +249 -0
- package/scripts/pick-folder.ps1 +110 -0
- package/scripts/postinstall.mjs +12 -0
- package/scripts/process-runner.mjs +60 -0
- package/scripts/release.mjs +172 -0
- package/scripts/sync-file-icons.mjs +118 -0
- package/scripts/sync-provider-icons.mjs +41 -0
- package/src/app/api/agent/[id]/events/route.ts +81 -0
- package/src/app/api/agent/[id]/route.ts +24 -0
- package/src/app/api/agent/[id]/tree/route.ts +49 -0
- package/src/app/api/agent/new/route.ts +19 -0
- package/src/app/api/config/route.ts +22 -0
- package/src/app/api/files/route.ts +63 -0
- package/src/app/api/git/route.ts +32 -0
- package/src/app/api/growth/route.ts +50 -0
- package/src/app/api/health/route.ts +7 -0
- package/src/app/api/models/route.ts +129 -0
- package/src/app/api/pi-settings/route.ts +22 -0
- package/src/app/api/plugins/route.ts +79 -0
- package/src/app/api/security/route.ts +41 -0
- package/src/app/api/sessions/[id]/export/route.ts +25 -0
- package/src/app/api/sessions/[id]/route.ts +37 -0
- package/src/app/api/sessions/route.ts +23 -0
- package/src/app/api/skills/route.ts +39 -0
- package/src/app/api/update/route.ts +33 -0
- package/src/app/api/version/route.ts +8 -0
- package/src/app/api/web-auth/route.ts +80 -0
- package/src/app/api/workspaces/route.ts +78 -0
- package/src/app/globals.css +2039 -0
- package/src/app/icon.svg +21 -0
- package/src/app/layout.tsx +29 -0
- package/src/app/login/page.tsx +111 -0
- package/src/app/page.tsx +10 -0
- package/src/components/AppShell.tsx +1171 -0
- package/src/components/ChatInput.tsx +729 -0
- package/src/components/ChatWindow.tsx +1320 -0
- package/src/components/ContextMeter.tsx +131 -0
- package/src/components/DiffView.tsx +139 -0
- package/src/components/ExtensionUI.tsx +181 -0
- package/src/components/FileViewer.tsx +555 -0
- package/src/components/FullDiff.tsx +220 -0
- package/src/components/GitPanel.tsx +357 -0
- package/src/components/GrowthTree.tsx +317 -0
- package/src/components/ModelSelector.tsx +286 -0
- package/src/components/OutlineRail.tsx +166 -0
- package/src/components/PiMark.tsx +79 -0
- package/src/components/ProjectPanel.tsx +309 -0
- package/src/components/ProviderBrand.tsx +28 -0
- package/src/components/ProviderSetupModal.module.css +560 -0
- package/src/components/ProviderSetupModal.tsx +559 -0
- package/src/components/SessionSidebar.tsx +1066 -0
- package/src/components/SettingsPanel.tsx +2216 -0
- package/src/components/TrajectoryAnalyzer.module.css +225 -0
- package/src/components/TrajectoryView.tsx +289 -0
- package/src/components/icons.tsx +443 -0
- package/src/hooks/useFileViewer.ts +58 -0
- package/src/hooks/useGrowth.ts +635 -0
- package/src/hooks/usePiWeb.ts +900 -0
- package/src/i18n.tsx +1043 -0
- package/src/lib/agent-manager.ts +1348 -0
- package/src/lib/app-root.ts +4 -0
- package/src/lib/auth.ts +51 -0
- package/src/lib/command-validation.ts +116 -0
- package/src/lib/enter-behavior.ts +40 -0
- package/src/lib/extension-ui.ts +134 -0
- package/src/lib/file-icons.json +557 -0
- package/src/lib/file-icons.ts +39 -0
- package/src/lib/files-service.ts +280 -0
- package/src/lib/git-service.ts +255 -0
- package/src/lib/growth-rounds.ts +84 -0
- package/src/lib/growth-service.ts +586 -0
- package/src/lib/growth-tracker.ts +328 -0
- package/src/lib/growth-tree.ts +245 -0
- package/src/lib/growth-turns.ts +23 -0
- package/src/lib/highlight.ts +59 -0
- package/src/lib/model-draft.ts +103 -0
- package/src/lib/models-service.ts +852 -0
- package/src/lib/path-security.ts +101 -0
- package/src/lib/pi-settings.ts +115 -0
- package/src/lib/pi.ts +367 -0
- package/src/lib/plugins-service.ts +312 -0
- package/src/lib/process-format.ts +261 -0
- package/src/lib/provider-display.ts +57 -0
- package/src/lib/security-service.ts +52 -0
- package/src/lib/semver.ts +39 -0
- package/src/lib/session-reader.ts +204 -0
- package/src/lib/settings-write-lock.ts +45 -0
- package/src/lib/skills-service.ts +97 -0
- package/src/lib/text-sanitize.ts +20 -0
- package/src/lib/theme.ts +137 -0
- package/src/lib/trajectory.ts +294 -0
- package/src/lib/types.ts +300 -0
- package/src/lib/update-service.ts +129 -0
- package/src/lib/version.ts +6 -0
- package/src/lib/web-auth-shared.ts +35 -0
- package/src/lib/web-auth.ts +53 -0
- package/src/lib/workspace-store.ts +261 -0
- package/src/proxy.ts +53 -0
- package/tsconfig.json +49 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* growth-service:项目生长快照引擎(影子仓库)。
|
|
3
|
+
*
|
|
4
|
+
* 每个工作区在 ~/.pi/agent/web-growth/<key>/ 下有一个裸 git 仓库(repo/)和一份账本(ledger.jsonl)。
|
|
5
|
+
* 快照 = 以工作区为 work-tree 往影子索引里 update-index,再 write-tree 得到 tree 哈希;
|
|
6
|
+
* 两步之间用 diff-tree 得增删改清单,任意两步任意文件用 git diff 得行级 patch,cat-file 取任意步全文。
|
|
7
|
+
* 每步顺手 commit-tree 到 refs/piweb/growth,让所有快照可达、不会被 gc 清掉。
|
|
8
|
+
* 用户自己的 .git 零改动;非 git 目录同样可用;工作区的 .gitignore 与这里的 info/exclude 都生效。
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
13
|
+
import fs from "node:fs/promises";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { GROWTH_EXCLUDE_DIRS } from "./growth-tree";
|
|
16
|
+
import { getAgentDir } from "./pi";
|
|
17
|
+
import type { GrowthChange, GrowthStep, GrowthStepKind } from "./types";
|
|
18
|
+
|
|
19
|
+
/** 单文件快照上限:超过的不进影子仓库(树里也就看不到它) */
|
|
20
|
+
export const MAX_FILE_BYTES = 1024 * 1024;
|
|
21
|
+
/** 单次 patch 输出上限 */
|
|
22
|
+
const MAX_PATCH_BYTES = 4 * 1024 * 1024;
|
|
23
|
+
/** 单文件全文预览上限 */
|
|
24
|
+
const MAX_CONTENT_BYTES = 256 * 1024;
|
|
25
|
+
/** 防止意外把依赖/缓存仓库完整哈希入库,导致工作区长时间卡住。 */
|
|
26
|
+
const MAX_CANDIDATES = 50_000;
|
|
27
|
+
/** 每步账本里最多记多少条变更;更多的由前端按 tree 对再取 */
|
|
28
|
+
const MAX_CHANGES_PER_STEP = 2000;
|
|
29
|
+
const GROWTH_REF = "refs/piweb/growth";
|
|
30
|
+
|
|
31
|
+
/** 不进快照的目录 / 文件(写进影子仓库的 info/exclude);工作区自己的 .gitignore 同样生效 */
|
|
32
|
+
export const EXCLUDE_DIRS = GROWTH_EXCLUDE_DIRS;
|
|
33
|
+
const EXCLUDE_FILES = [".DS_Store", "Thumbs.db", "*.log", ".env", ".env.*", "!.env.example", "*.pem", "*.key", "*.p12", "*.pfx", "id_rsa*", "id_ed25519*"];
|
|
34
|
+
const EXCLUDED_DIR_SET = new Set(EXCLUDE_DIRS.map((d) => d.toLowerCase()));
|
|
35
|
+
const EXCLUDE_CONTENT = `# PiWeb growth snapshot exclusions (generated, do not edit)\n${[...EXCLUDE_DIRS.map((d) => `${d}/`), ...EXCLUDE_FILES].join("\n")}\n`;
|
|
36
|
+
|
|
37
|
+
export class GrowthError extends Error {
|
|
38
|
+
constructor(
|
|
39
|
+
message: string,
|
|
40
|
+
public code: "unavailable" | "too-large" | "not-found" | "failed" = "failed",
|
|
41
|
+
) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "GrowthError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface WorkspaceState {
|
|
48
|
+
key: string;
|
|
49
|
+
cwd: string;
|
|
50
|
+
dir: string;
|
|
51
|
+
repo: string;
|
|
52
|
+
ledger: string;
|
|
53
|
+
steps: GrowthStep[];
|
|
54
|
+
lastTree?: string;
|
|
55
|
+
lastCommit?: string;
|
|
56
|
+
emptyTree: string;
|
|
57
|
+
excludeRules?: string;
|
|
58
|
+
/** 同一工作区的快照串行执行(共用一个影子索引) */
|
|
59
|
+
lock: Promise<unknown>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface Registry {
|
|
63
|
+
workspaces: Map<string, Promise<WorkspaceState>>;
|
|
64
|
+
gitOk?: Promise<boolean>;
|
|
65
|
+
}
|
|
66
|
+
const globalForGrowth = globalThis as typeof globalThis & { __piWebGrowth?: Registry };
|
|
67
|
+
const registry: Registry = (globalForGrowth.__piWebGrowth ??= { workspaces: new Map() });
|
|
68
|
+
|
|
69
|
+
// ---------- 基础工具 ----------
|
|
70
|
+
|
|
71
|
+
function comparablePath(p: string): string {
|
|
72
|
+
const norm = path.resolve(p).replace(/\\/g, "/");
|
|
73
|
+
return process.platform === "win32" ? norm.toLowerCase() : norm;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 工作区目录名:路径 slug + 8 位哈希(Windows 大小写不敏感) */
|
|
77
|
+
export function workspaceKey(cwd: string): string {
|
|
78
|
+
// Canonicalize first: Windows may report an 8.3 short path (RUNNER~1) in one call and
|
|
79
|
+
// the long path in another, and two spellings must not split one workspace's ledger.
|
|
80
|
+
let resolved = path.resolve(cwd);
|
|
81
|
+
try {
|
|
82
|
+
resolved = realpathSync.native(resolved);
|
|
83
|
+
} catch {
|
|
84
|
+
/* the directory may not exist yet; the resolved path is stable enough */
|
|
85
|
+
}
|
|
86
|
+
const cmp = comparablePath(resolved);
|
|
87
|
+
const slug = cmp.replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").slice(-48).toLowerCase();
|
|
88
|
+
return `${slug || "root"}-${createHash("sha1").update(cmp).digest("hex").slice(0, 8)}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 目录监听 / 树渲染共用:这条相对路径是否落在排除目录里 */
|
|
92
|
+
export function isExcludedRelPath(rel: string): boolean {
|
|
93
|
+
const parts = rel.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
94
|
+
return parts.some((seg) => EXCLUDED_DIR_SET.has(seg.toLowerCase()));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sameSession(a: string, b: string): boolean {
|
|
98
|
+
return comparablePath(a) === comparablePath(b);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface RunResult {
|
|
102
|
+
stdout: Buffer;
|
|
103
|
+
stderr: string;
|
|
104
|
+
code: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type Env = Record<string, string | undefined>;
|
|
108
|
+
|
|
109
|
+
function runGit(args: string[], opts: { cwd?: string; input?: Buffer | string; maxBuffer?: number; env?: Env } = {}): Promise<RunResult> {
|
|
110
|
+
const maxBuffer = opts.maxBuffer ?? 64 * 1024 * 1024;
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const child = spawn("git", args, {
|
|
113
|
+
cwd: opts.cwd,
|
|
114
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_OPTIONAL_LOCKS: "0", ...opts.env },
|
|
115
|
+
windowsHide: true,
|
|
116
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
117
|
+
});
|
|
118
|
+
const out: Buffer[] = [];
|
|
119
|
+
const err: Buffer[] = [];
|
|
120
|
+
let size = 0;
|
|
121
|
+
let settled = false;
|
|
122
|
+
const timer = setTimeout(() => fail(new GrowthError("git timed out", "failed")), 30_000);
|
|
123
|
+
const fail = (error: Error) => {
|
|
124
|
+
if (settled) return;
|
|
125
|
+
settled = true;
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
child.kill();
|
|
128
|
+
reject(error);
|
|
129
|
+
};
|
|
130
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
131
|
+
size += chunk.length;
|
|
132
|
+
if (size > maxBuffer) {
|
|
133
|
+
child.kill();
|
|
134
|
+
fail(new GrowthError("git output exceeded the buffer limit", "too-large"));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
out.push(chunk);
|
|
138
|
+
});
|
|
139
|
+
child.stderr.on("data", (chunk: Buffer) => err.push(chunk));
|
|
140
|
+
child.on("error", (error: NodeJS.ErrnoException) => {
|
|
141
|
+
if (error.code === "ENOENT" && opts.cwd && !existsSync(opts.cwd)) fail(new GrowthError("workspace directory not found", "failed"));
|
|
142
|
+
else fail(error.code === "ENOENT" ? new GrowthError("git is not installed", "unavailable") : error);
|
|
143
|
+
});
|
|
144
|
+
child.on("close", (code) => {
|
|
145
|
+
if (settled) return;
|
|
146
|
+
settled = true;
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
resolve({ stdout: Buffer.concat(out), stderr: Buffer.concat(err).toString("utf8"), code: code ?? -1 });
|
|
149
|
+
});
|
|
150
|
+
if (opts.input !== undefined) child.stdin.end(opts.input);
|
|
151
|
+
else child.stdin.end();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function gitOk(): Promise<boolean> {
|
|
156
|
+
registry.gitOk ??= runGit(["--version"]).then((r) => {
|
|
157
|
+
const ok = r.code === 0;
|
|
158
|
+
if (!ok) registry.gitOk = undefined;
|
|
159
|
+
return ok;
|
|
160
|
+
}, () => {
|
|
161
|
+
registry.gitOk = undefined;
|
|
162
|
+
return false;
|
|
163
|
+
});
|
|
164
|
+
return registry.gitOk;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 影子仓库命令:固定 git-dir / work-tree,关掉换行转换与路径转义,输出按字节原样 */
|
|
168
|
+
async function shadow(ws: WorkspaceState, args: string[], opts: { input?: Buffer | string; maxBuffer?: number; allowFail?: boolean; env?: Env } = {}): Promise<RunResult> {
|
|
169
|
+
const base = [
|
|
170
|
+
"--git-dir", ws.repo,
|
|
171
|
+
"--work-tree", ws.cwd,
|
|
172
|
+
"-c", "core.autocrlf=false",
|
|
173
|
+
"-c", "core.safecrlf=false",
|
|
174
|
+
"-c", "core.quotepath=false",
|
|
175
|
+
"-c", "core.fsmonitor=false",
|
|
176
|
+
"-c", "core.untrackedCache=false",
|
|
177
|
+
];
|
|
178
|
+
const r = await runGit([...base, ...args], { cwd: ws.cwd, input: opts.input, maxBuffer: opts.maxBuffer, env: opts.env });
|
|
179
|
+
if (r.code !== 0 && !opts.allowFail) {
|
|
180
|
+
throw new GrowthError(`git ${args[0]} failed: ${r.stderr.trim().split("\n")[0] || `exit ${r.code}`}`);
|
|
181
|
+
}
|
|
182
|
+
return r;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function growthRoot(): string {
|
|
186
|
+
return path.join(getAgentDir(), "web-growth");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function parseStep(line: string): GrowthStep | null {
|
|
190
|
+
try {
|
|
191
|
+
const s = JSON.parse(line) as GrowthStep;
|
|
192
|
+
if (typeof s?.tree !== "string" || typeof s.seq !== "number") return null;
|
|
193
|
+
return s;
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function createWorkspace(cwd: string): Promise<WorkspaceState> {
|
|
200
|
+
if (!(await gitOk())) throw new GrowthError("git is not installed", "unavailable");
|
|
201
|
+
const key = workspaceKey(cwd);
|
|
202
|
+
const dir = path.join(growthRoot(), key);
|
|
203
|
+
const repo = path.join(dir, "repo");
|
|
204
|
+
const ledger = path.join(dir, "ledger.jsonl");
|
|
205
|
+
await fs.mkdir(dir, { recursive: true });
|
|
206
|
+
const initialized = await fs.stat(path.join(repo, "HEAD")).then(() => true, () => false);
|
|
207
|
+
if (!initialized) {
|
|
208
|
+
const r = await runGit(["init", "-q", "--bare", repo]);
|
|
209
|
+
if (r.code !== 0) throw new GrowthError(`git init failed: ${r.stderr.trim()}`);
|
|
210
|
+
}
|
|
211
|
+
await fs.mkdir(path.join(repo, "info"), { recursive: true });
|
|
212
|
+
await fs.writeFile(path.join(repo, "info", "exclude"), EXCLUDE_CONTENT, "utf8");
|
|
213
|
+
await fs.writeFile(path.join(dir, "meta.json"), JSON.stringify({ cwd, key, createdAt: new Date().toISOString() }, null, 2), "utf8");
|
|
214
|
+
|
|
215
|
+
const steps: GrowthStep[] = [];
|
|
216
|
+
try {
|
|
217
|
+
for (const line of (await fs.readFile(ledger, "utf8")).split("\n")) {
|
|
218
|
+
if (!line.trim()) continue;
|
|
219
|
+
const s = parseStep(line);
|
|
220
|
+
if (s) steps.push(s);
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
224
|
+
}
|
|
225
|
+
const ws: WorkspaceState = { key, cwd, dir, repo, ledger, steps, emptyTree: "", excludeRules: EXCLUDE_CONTENT, lock: Promise.resolve() };
|
|
226
|
+
const last = steps[steps.length - 1];
|
|
227
|
+
ws.lastTree = last?.tree;
|
|
228
|
+
ws.lastCommit = last?.commit;
|
|
229
|
+
const empty = await shadow(ws, ["hash-object", "-t", "tree", "--stdin"], { input: "" });
|
|
230
|
+
ws.emptyTree = empty.stdout.toString("utf8").trim();
|
|
231
|
+
return ws;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function getWorkspace(cwdValue: string): Promise<WorkspaceState> {
|
|
235
|
+
const cwd = await fs.realpath(path.resolve(cwdValue)).catch(() => path.resolve(cwdValue));
|
|
236
|
+
const key = workspaceKey(cwd);
|
|
237
|
+
let pending = registry.workspaces.get(key);
|
|
238
|
+
if (pending) {
|
|
239
|
+
// 影子仓库可能在运行期间被用户清理掉(~/.pi/agent/web-growth/<key>/):缓存里的状态就指向不存在的 git-dir,
|
|
240
|
+
// 之后每次快照都报 not a git repository。发现 HEAD 不在就丢掉缓存重建(账本也一并从头开始)
|
|
241
|
+
const ws = await pending.catch(() => null);
|
|
242
|
+
const alive = ws ? await fs.stat(path.join(ws.repo, "HEAD")).then(() => true, () => false) : false;
|
|
243
|
+
if (!alive) {
|
|
244
|
+
registry.workspaces.delete(key);
|
|
245
|
+
pending = undefined;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (!pending) {
|
|
249
|
+
pending = createWorkspace(cwd);
|
|
250
|
+
registry.workspaces.set(key, pending);
|
|
251
|
+
pending.catch(() => registry.workspaces.delete(key));
|
|
252
|
+
}
|
|
253
|
+
return pending;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function withLock<T>(ws: WorkspaceState, fn: () => Promise<T>): Promise<T> {
|
|
257
|
+
const run = ws.lock.then(fn, fn);
|
|
258
|
+
ws.lock = run.then(() => undefined, () => undefined);
|
|
259
|
+
return run;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const HASH_RE = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
263
|
+
function assertHash(value: unknown, what = "hash"): string {
|
|
264
|
+
if (typeof value !== "string" || !HASH_RE.test(value)) throw new GrowthError(`invalid ${what}`, "not-found");
|
|
265
|
+
return value;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function assertRelPath(value: unknown): string {
|
|
269
|
+
if (typeof value !== "string" || !value.trim()) throw new GrowthError("missing path", "not-found");
|
|
270
|
+
const rel = value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
271
|
+
if (rel.startsWith("/") || /^[A-Za-z]:\//.test(rel) || rel.split("/").some((seg) => seg === "..")) {
|
|
272
|
+
throw new GrowthError("invalid path", "not-found");
|
|
273
|
+
}
|
|
274
|
+
return rel;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ---------- 输出解析(导出供测试) ----------
|
|
278
|
+
|
|
279
|
+
/** `diff-tree -r -M --name-status -z` 输出 → 变更列表(R 带 from) */
|
|
280
|
+
export function parseNameStatus(raw: string): GrowthChange[] {
|
|
281
|
+
const tokens = raw.split("\0");
|
|
282
|
+
const out: GrowthChange[] = [];
|
|
283
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
284
|
+
const status = tokens[i];
|
|
285
|
+
if (!status) continue;
|
|
286
|
+
const letter = status[0];
|
|
287
|
+
if (letter === "R" || letter === "C") {
|
|
288
|
+
const from = tokens[i + 1];
|
|
289
|
+
const to = tokens[i + 2];
|
|
290
|
+
i += 2;
|
|
291
|
+
if (to === undefined) break;
|
|
292
|
+
out.push(letter === "R" ? { status: "R", path: to, from } : { status: "A", path: to });
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const p = tokens[i + 1];
|
|
296
|
+
i += 1;
|
|
297
|
+
if (p === undefined) break;
|
|
298
|
+
if (letter === "A" || letter === "M" || letter === "D") out.push({ status: letter, path: p });
|
|
299
|
+
else if (letter === "T") out.push({ status: "M", path: p });
|
|
300
|
+
}
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** `diff-tree -r -M --numstat -z` 输出 → path → {add, del, binary}(重命名按新路径记) */
|
|
305
|
+
export function parseNumstat(raw: string): Map<string, { add: number; del: number; binary: boolean }> {
|
|
306
|
+
const tokens = raw.split("\0");
|
|
307
|
+
const out = new Map<string, { add: number; del: number; binary: boolean }>();
|
|
308
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
309
|
+
const t = tokens[i];
|
|
310
|
+
if (!t) continue;
|
|
311
|
+
const [a, d, rest] = t.split("\t");
|
|
312
|
+
if (a === undefined || d === undefined) continue;
|
|
313
|
+
const binary = a === "-" || d === "-";
|
|
314
|
+
const entry = { add: binary ? 0 : Number(a) || 0, del: binary ? 0 : Number(d) || 0, binary };
|
|
315
|
+
if (rest === undefined || rest === "") {
|
|
316
|
+
// 重命名:路径在后两个 token(old, new)
|
|
317
|
+
const to = tokens[i + 2];
|
|
318
|
+
i += 2;
|
|
319
|
+
if (to !== undefined) out.set(to, entry);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
out.set(rest, entry);
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export interface TreeFile {
|
|
328
|
+
path: string;
|
|
329
|
+
size: number;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** `ls-tree -r -l -z` 输出 → 文件清单(只取 blob) */
|
|
333
|
+
export function parseLsTree(raw: string): TreeFile[] {
|
|
334
|
+
const out: TreeFile[] = [];
|
|
335
|
+
for (const entry of raw.split("\0")) {
|
|
336
|
+
if (!entry) continue;
|
|
337
|
+
const m = entry.match(/^(\d+) (\w+) ([0-9a-f]+) +(-|\d+)\t([\s\S]+)$/);
|
|
338
|
+
if (!m || m[2] !== "blob") continue;
|
|
339
|
+
out.push({ path: m[5], size: m[4] === "-" ? 0 : Number(m[4]) });
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function summarize(changes: GrowthChange[]): GrowthStep["stats"] {
|
|
345
|
+
const stats = { added: 0, modified: 0, deleted: 0, renamed: 0, add: 0, del: 0 };
|
|
346
|
+
for (const c of changes) {
|
|
347
|
+
if (c.status === "A") stats.added += 1;
|
|
348
|
+
else if (c.status === "M") stats.modified += 1;
|
|
349
|
+
else if (c.status === "D") stats.deleted += 1;
|
|
350
|
+
else stats.renamed += 1;
|
|
351
|
+
stats.add += c.add ?? 0;
|
|
352
|
+
stats.del += c.del ?? 0;
|
|
353
|
+
}
|
|
354
|
+
return stats;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ---------- 快照 ----------
|
|
358
|
+
|
|
359
|
+
async function diffTreesRaw(ws: WorkspaceState, from: string, to: string): Promise<GrowthChange[]> {
|
|
360
|
+
const [names, nums] = await Promise.all([
|
|
361
|
+
shadow(ws, ["diff-tree", "-r", "-M", "--name-status", "-z", from, to]),
|
|
362
|
+
shadow(ws, ["diff-tree", "-r", "-M", "--numstat", "-z", from, to]),
|
|
363
|
+
]);
|
|
364
|
+
const changes = parseNameStatus(names.stdout.toString("utf8"));
|
|
365
|
+
const stats = parseNumstat(nums.stdout.toString("utf8"));
|
|
366
|
+
for (const c of changes) {
|
|
367
|
+
const s = stats.get(c.path);
|
|
368
|
+
if (!s) continue;
|
|
369
|
+
c.add = s.add;
|
|
370
|
+
c.del = s.del;
|
|
371
|
+
if (s.binary) c.binary = true;
|
|
372
|
+
}
|
|
373
|
+
changes.sort((a, b) => a.path.localeCompare(b.path));
|
|
374
|
+
return changes;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** 候选路径 → 更新影子索引;返回本次处理的文件数 */
|
|
378
|
+
async function refreshIndex(ws: WorkspaceState): Promise<number> {
|
|
379
|
+
const listed = await shadow(ws, ["ls-files", "-z", "--others", "--modified", "--deleted", "--exclude-standard"]);
|
|
380
|
+
const candidates = Array.from(new Set(listed.stdout.toString("utf8").split("\0").filter((p) => p && !p.endsWith("/"))));
|
|
381
|
+
if (candidates.length > MAX_CANDIDATES) {
|
|
382
|
+
const counts = new Map<string, number>();
|
|
383
|
+
for (const candidate of candidates) {
|
|
384
|
+
const parts = candidate.split("/");
|
|
385
|
+
const group = parts.length > 1 ? parts.slice(0, 2).join("/") : "(root)";
|
|
386
|
+
counts.set(group, (counts.get(group) ?? 0) + 1);
|
|
387
|
+
}
|
|
388
|
+
const largest = [...counts].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([dir, count]) => `${dir}: ${count}`).join(", ");
|
|
389
|
+
throw new GrowthError(`snapshot has ${candidates.length} candidate files (limit ${MAX_CANDIDATES}); largest directories: ${largest}. Exclude generated directories with .gitignore`, "too-large");
|
|
390
|
+
}
|
|
391
|
+
const adds: string[] = [];
|
|
392
|
+
const removals: string[] = [];
|
|
393
|
+
const BATCH = 256;
|
|
394
|
+
const INDEX_BATCH = 256;
|
|
395
|
+
const directories = new Map<string, Promise<boolean>>();
|
|
396
|
+
const regularDirectory = (rel: string): Promise<boolean> => {
|
|
397
|
+
if (!rel) return Promise.resolve(true);
|
|
398
|
+
const cached = directories.get(rel);
|
|
399
|
+
if (cached) return cached;
|
|
400
|
+
const check = (async () => {
|
|
401
|
+
const parent = rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : "";
|
|
402
|
+
if (!(await regularDirectory(parent))) return false;
|
|
403
|
+
const stat = await fs.lstat(path.join(ws.cwd, rel)).catch((error: NodeJS.ErrnoException) => {
|
|
404
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
|
|
405
|
+
throw error;
|
|
406
|
+
});
|
|
407
|
+
return Boolean(stat?.isDirectory() && !stat.isSymbolicLink());
|
|
408
|
+
})();
|
|
409
|
+
directories.set(rel, check);
|
|
410
|
+
return check;
|
|
411
|
+
};
|
|
412
|
+
// 先分类全部候选,不能在 lstat 批次内穿插 add:冲突路径可能落在后续批次。
|
|
413
|
+
for (let i = 0; i < candidates.length; i += BATCH) {
|
|
414
|
+
const classified = await Promise.all(
|
|
415
|
+
candidates.slice(i, i + BATCH).map(async (rel) => {
|
|
416
|
+
// Git for Windows 会枚举 junction 内的文件;只查叶子 lstat 无法发现它。
|
|
417
|
+
const parent = rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : "";
|
|
418
|
+
if (!(await regularDirectory(parent))) return { rel, include: false };
|
|
419
|
+
const stat = await fs.lstat(path.join(ws.cwd, rel)).catch((error: NodeJS.ErrnoException) => {
|
|
420
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
|
|
421
|
+
throw error;
|
|
422
|
+
});
|
|
423
|
+
return { rel, include: Boolean(stat?.isFile() && stat.size <= MAX_FILE_BYTES) };
|
|
424
|
+
}),
|
|
425
|
+
);
|
|
426
|
+
for (const { rel, include } of classified) (include ? adds : removals).push(rel);
|
|
427
|
+
}
|
|
428
|
+
// --remove 只移除磁盘上已消失的路径;符号链接/目录/超大文件仍在,必须 force-remove。
|
|
429
|
+
// 全体移除先完成,保证 p → p/child 和 p/child → p 都不受候选顺序、批次边界影响。
|
|
430
|
+
for (let i = 0; i < removals.length; i += INDEX_BATCH) {
|
|
431
|
+
await shadow(ws, ["update-index", "--force-remove", "-z", "--stdin"], { input: `${removals.slice(i, i + INDEX_BATCH).join("\0")}\0` });
|
|
432
|
+
}
|
|
433
|
+
for (let i = 0; i < adds.length; i += INDEX_BATCH) {
|
|
434
|
+
// Git 的 --replace 只移除与待添加项冲突的索引项,兜底扫描期间的类型转换;
|
|
435
|
+
// --remove 同时容忍 lstat 后刚被删掉的文件。这些命令始终只操作影子 git-dir。
|
|
436
|
+
await shadow(ws, ["update-index", "--add", "--remove", "--replace", "-z", "--stdin"], { input: `${adds.slice(i, i + INDEX_BATCH).join("\0")}\0` });
|
|
437
|
+
}
|
|
438
|
+
return candidates.length;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export interface SnapshotMeta {
|
|
442
|
+
kind: GrowthStepKind;
|
|
443
|
+
label: string;
|
|
444
|
+
session: string;
|
|
445
|
+
toolCallId?: string;
|
|
446
|
+
toolName?: string;
|
|
447
|
+
/** tree 没变也记一步(会话基线用) */
|
|
448
|
+
force?: boolean;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* 给工作区拍一张快照。tree 与上一步相同且未 force 时返回 null(不记步)。
|
|
453
|
+
*/
|
|
454
|
+
export async function snapshot(cwdValue: string, meta: SnapshotMeta): Promise<GrowthStep | null> {
|
|
455
|
+
const ws = await getWorkspace(cwdValue);
|
|
456
|
+
return withLock(ws, async () => {
|
|
457
|
+
if (ws.excludeRules !== EXCLUDE_CONTENT) {
|
|
458
|
+
await fs.writeFile(path.join(ws.repo, "info", "exclude"), EXCLUDE_CONTENT, "utf8");
|
|
459
|
+
ws.excludeRules = EXCLUDE_CONTENT;
|
|
460
|
+
}
|
|
461
|
+
await refreshIndex(ws);
|
|
462
|
+
const tree = (await shadow(ws, ["write-tree"])).stdout.toString("utf8").trim();
|
|
463
|
+
assertHash(tree, "tree");
|
|
464
|
+
const parent = ws.lastTree ?? ws.emptyTree;
|
|
465
|
+
if (tree === parent && !meta.force) return null;
|
|
466
|
+
|
|
467
|
+
const initial = ws.lastTree === undefined;
|
|
468
|
+
// 工作区第一张快照把所有文件都算“新增”,账本里不存这份清单(前端把基线当中性起点)
|
|
469
|
+
const all = initial ? [] : await diffTreesRaw(ws, parent, tree);
|
|
470
|
+
const changes = all.slice(0, MAX_CHANGES_PER_STEP);
|
|
471
|
+
|
|
472
|
+
const env = {
|
|
473
|
+
GIT_AUTHOR_NAME: "PiWeb", GIT_AUTHOR_EMAIL: "piweb@localhost",
|
|
474
|
+
GIT_COMMITTER_NAME: "PiWeb", GIT_COMMITTER_EMAIL: "piweb@localhost",
|
|
475
|
+
};
|
|
476
|
+
const commitArgs = ["commit-tree", tree, "-m", `${meta.kind}: ${meta.label || "snapshot"}`];
|
|
477
|
+
if (ws.lastCommit) commitArgs.push("-p", ws.lastCommit);
|
|
478
|
+
const commit = (await shadow(ws, commitArgs, { env })).stdout.toString("utf8").trim();
|
|
479
|
+
await shadow(ws, ["update-ref", GROWTH_REF, commit]);
|
|
480
|
+
|
|
481
|
+
const step: GrowthStep = {
|
|
482
|
+
seq: (ws.steps[ws.steps.length - 1]?.seq ?? 0) + 1,
|
|
483
|
+
ts: Date.now(),
|
|
484
|
+
session: meta.session,
|
|
485
|
+
kind: meta.kind,
|
|
486
|
+
label: meta.label.slice(0, 200),
|
|
487
|
+
tree,
|
|
488
|
+
parent,
|
|
489
|
+
commit,
|
|
490
|
+
toolCallId: meta.toolCallId,
|
|
491
|
+
toolName: meta.toolName,
|
|
492
|
+
changes,
|
|
493
|
+
truncated: all.length > changes.length || undefined,
|
|
494
|
+
initial: initial || undefined,
|
|
495
|
+
stats: summarize(all),
|
|
496
|
+
};
|
|
497
|
+
await fs.appendFile(ws.ledger, `${JSON.stringify(step)}\n`, "utf8");
|
|
498
|
+
ws.steps.push(step);
|
|
499
|
+
ws.lastTree = tree;
|
|
500
|
+
ws.lastCommit = commit;
|
|
501
|
+
if (step.seq % 100 === 0) {
|
|
502
|
+
// 维护命令同样串行化,避免与下一张快照同时操作影子仓库。
|
|
503
|
+
void withLock(ws, async () => {
|
|
504
|
+
await shadow(ws, ["gc", "--auto", "-q"], { allowFail: true });
|
|
505
|
+
}).catch(() => undefined);
|
|
506
|
+
}
|
|
507
|
+
return step;
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ---------- 读取 ----------
|
|
512
|
+
|
|
513
|
+
export async function isAvailable(): Promise<boolean> {
|
|
514
|
+
return gitOk();
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** 账本里的步:默认只取某个会话的;省略 session 取工作区全部。只读:还没建过影子仓库的工作区直接返回空 */
|
|
518
|
+
export async function readSteps(cwdValue: string, session?: string): Promise<GrowthStep[]> {
|
|
519
|
+
const cwd = await fs.realpath(path.resolve(cwdValue)).catch(() => path.resolve(cwdValue));
|
|
520
|
+
const key = workspaceKey(cwd);
|
|
521
|
+
if (!registry.workspaces.has(key)) {
|
|
522
|
+
const exists = await fs.stat(path.join(growthRoot(), key, "ledger.jsonl")).then(() => true, () => false);
|
|
523
|
+
if (!exists) return [];
|
|
524
|
+
}
|
|
525
|
+
const ws = await getWorkspace(cwd);
|
|
526
|
+
if (!session) return [...ws.steps];
|
|
527
|
+
return ws.steps.filter((s) => sameSession(s.session, session));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export async function hasSessionSteps(cwdValue: string, session: string): Promise<boolean> {
|
|
531
|
+
const ws = await getWorkspace(cwdValue);
|
|
532
|
+
return ws.steps.some((s) => sameSession(s.session, session));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** 某个 tree 的文件清单 */
|
|
536
|
+
export async function listTree(cwdValue: string, tree: unknown): Promise<TreeFile[]> {
|
|
537
|
+
const ws = await getWorkspace(cwdValue);
|
|
538
|
+
const hash = assertHash(tree, "tree");
|
|
539
|
+
if (hash === ws.emptyTree) return [];
|
|
540
|
+
const r = await shadow(ws, ["ls-tree", "-r", "-l", "-z", hash]);
|
|
541
|
+
return parseLsTree(r.stdout.toString("utf8"));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** 两个 tree 之间的全部变更 */
|
|
545
|
+
export async function changesBetween(cwdValue: string, from: unknown, to: unknown): Promise<GrowthChange[]> {
|
|
546
|
+
const ws = await getWorkspace(cwdValue);
|
|
547
|
+
return diffTreesRaw(ws, assertHash(from, "from"), assertHash(to, "to"));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export interface GrowthPatch {
|
|
551
|
+
patch: string;
|
|
552
|
+
binary: boolean;
|
|
553
|
+
truncated: boolean;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** 单文件在两个 tree 之间的 unified diff(整文件上下文,前端自己折叠未改动段) */
|
|
557
|
+
export async function filePatch(cwdValue: string, from: unknown, to: unknown, relPath: unknown, renamedFrom?: unknown): Promise<GrowthPatch> {
|
|
558
|
+
const ws = await getWorkspace(cwdValue);
|
|
559
|
+
const rel = assertRelPath(relPath);
|
|
560
|
+
const paths = [rel];
|
|
561
|
+
if (typeof renamedFrom === "string" && renamedFrom) paths.push(assertRelPath(renamedFrom));
|
|
562
|
+
const r = await shadow(ws, ["diff", "-M", "--no-color", "--no-ext-diff", "-U1000000", assertHash(from, "from"), assertHash(to, "to"), "--", ...paths]);
|
|
563
|
+
if (/^Binary files .* differ$/m.test(r.stdout.toString("utf8", 0, Math.min(r.stdout.length, 64 * 1024)))) return { patch: "", binary: true, truncated: false };
|
|
564
|
+
if (r.stdout.length <= MAX_PATCH_BYTES) return { patch: r.stdout.toString("utf8"), binary: false, truncated: false };
|
|
565
|
+
return { patch: r.stdout.subarray(0, MAX_PATCH_BYTES).toString("utf8"), binary: false, truncated: true };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export interface GrowthContent {
|
|
569
|
+
content: string;
|
|
570
|
+
binary: boolean;
|
|
571
|
+
truncated: boolean;
|
|
572
|
+
size: number;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** 某个 tree 里单文件的全文 */
|
|
576
|
+
export async function fileContent(cwdValue: string, tree: unknown, relPath: unknown): Promise<GrowthContent> {
|
|
577
|
+
const ws = await getWorkspace(cwdValue);
|
|
578
|
+
const rel = assertRelPath(relPath);
|
|
579
|
+
const r = await shadow(ws, ["cat-file", "-p", `${assertHash(tree, "tree")}:${rel}`], { allowFail: true });
|
|
580
|
+
if (r.code !== 0) throw new GrowthError("file not found in this snapshot", "not-found");
|
|
581
|
+
const buf = r.stdout;
|
|
582
|
+
const head = buf.subarray(0, Math.min(buf.length, 8000));
|
|
583
|
+
if (head.includes(0)) return { content: "", binary: true, truncated: false, size: buf.length };
|
|
584
|
+
if (buf.length <= MAX_CONTENT_BYTES) return { content: buf.toString("utf8"), binary: false, truncated: false, size: buf.length };
|
|
585
|
+
return { content: buf.subarray(0, MAX_CONTENT_BYTES).toString("utf8"), binary: false, truncated: true, size: buf.length };
|
|
586
|
+
}
|