@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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixed-command update preparation followed by a checked, isolated Next release.
|
|
3
|
+
* Source/node_modules changes are not snapshots: run only in a maintenance window.
|
|
4
|
+
* No serving process is stopped, and a build is selected only on a manual restart.
|
|
5
|
+
*/
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { installPiUpdate, PI_PACKAGE, prepareRelease } from "../../scripts/release.mjs";
|
|
9
|
+
import { assertInstallation } from "../../scripts/build-output.mjs";
|
|
10
|
+
import { runCommand } from "../../scripts/process-runner.mjs";
|
|
11
|
+
import { isNewer } from "./semver";
|
|
12
|
+
import { samePath } from "./path-security";
|
|
13
|
+
import { getRuntimeVersions } from "./version";
|
|
14
|
+
import { APP_ROOT } from "./app-root";
|
|
15
|
+
|
|
16
|
+
export type UpdateTarget = "piweb" | "pi";
|
|
17
|
+
|
|
18
|
+
export interface UpdateCheck {
|
|
19
|
+
current: string;
|
|
20
|
+
latest: string;
|
|
21
|
+
canUpdate: boolean;
|
|
22
|
+
/** Only piweb: commits behind the branch which the update command pulls. */
|
|
23
|
+
behind?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface UpdateResult {
|
|
27
|
+
version: string;
|
|
28
|
+
needsRestart: boolean;
|
|
29
|
+
buildDir: string;
|
|
30
|
+
buildId: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class UpdateBusyError extends Error {}
|
|
34
|
+
|
|
35
|
+
type CommandRunner = typeof runCommand;
|
|
36
|
+
|
|
37
|
+
interface UpdateServiceOptions {
|
|
38
|
+
root?: string;
|
|
39
|
+
run?: CommandRunner;
|
|
40
|
+
versions?: typeof getRuntimeVersions;
|
|
41
|
+
env?: Record<string, string | undefined>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function assertTarget(target: UpdateTarget): void {
|
|
45
|
+
if (target !== "piweb" && target !== "pi") throw new Error("unknown target");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function readPackageVersion(filePath: string): Promise<string> {
|
|
49
|
+
const raw = JSON.parse(await fs.readFile(filePath, "utf8")) as { version?: unknown };
|
|
50
|
+
if (typeof raw.version !== "string" || !raw.version) throw new Error(`no version field in ${filePath}`);
|
|
51
|
+
return raw.version;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Injectable process runner/root: tests never contact registries or update this checkout. */
|
|
55
|
+
export function createUpdateService({ root = APP_ROOT, run = runCommand, versions = getRuntimeVersions, env = process.env }: UpdateServiceOptions = {}) {
|
|
56
|
+
let updateRunning = false;
|
|
57
|
+
const commandEnv = { ...env };
|
|
58
|
+
delete commandEnv.PI_WEB_PASSWORD;
|
|
59
|
+
|
|
60
|
+
async function checkedRoot(): Promise<string> {
|
|
61
|
+
return assertInstallation(root);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function checkForUpdate(target: UpdateTarget): Promise<UpdateCheck> {
|
|
65
|
+
assertTarget(target);
|
|
66
|
+
if (updateRunning) throw new UpdateBusyError("an update is already running");
|
|
67
|
+
const installation = await checkedRoot();
|
|
68
|
+
const command = (cmd: string, args: string[], timeoutMs: number) => run(cmd, args, { cwd: installation, env: commandEnv, timeoutMs });
|
|
69
|
+
const { piWeb, piEngine } = await versions();
|
|
70
|
+
if (target === "pi") {
|
|
71
|
+
const latest = (await command("npm", ["view", PI_PACKAGE, "version"], 30_000)).trim().split(/\r?\n/)[0];
|
|
72
|
+
return { current: piEngine, latest, canUpdate: isNewer(latest, piEngine) };
|
|
73
|
+
}
|
|
74
|
+
await command("git", ["fetch", "origin", "main", "--tags"], 30_000);
|
|
75
|
+
const behind = Number((await command("git", ["rev-list", "--count", "HEAD..origin/main"], 15_000)).trim());
|
|
76
|
+
if (!Number.isSafeInteger(behind) || behind < 0) throw new Error("invalid upstream commit count");
|
|
77
|
+
const remotePkg = JSON.parse(await command("git", ["show", "origin/main:package.json"], 15_000)) as { name?: string; version?: string };
|
|
78
|
+
if ((remotePkg.name !== "piweb" && remotePkg.name !== "pi-web") || typeof remotePkg.version !== "string" || !remotePkg.version) throw new Error("upstream is not a PiWeb package");
|
|
79
|
+
// Do not advertise a tag version that `git pull origin main` will never install.
|
|
80
|
+
return { current: piWeb, latest: remotePkg.version, behind, canUpdate: isNewer(remotePkg.version, piWeb) || behind > 0 };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function runUpdate(target: UpdateTarget, { assertIdle = async () => {} }: { assertIdle?: () => Promise<void> } = {}): Promise<UpdateResult> {
|
|
84
|
+
assertTarget(target);
|
|
85
|
+
if (updateRunning) throw new UpdateBusyError("an update is already running");
|
|
86
|
+
updateRunning = true;
|
|
87
|
+
try {
|
|
88
|
+
const installation = await checkedRoot();
|
|
89
|
+
let version = "";
|
|
90
|
+
const release = await prepareRelease({
|
|
91
|
+
root: installation, env, run,
|
|
92
|
+
beforePrepare: async () => {
|
|
93
|
+
await assertIdle();
|
|
94
|
+
if (target === "piweb") {
|
|
95
|
+
const command = (args: string[]) => run("git", args, { cwd: installation, env: commandEnv, timeoutMs: 15_000 });
|
|
96
|
+
const top = (await command(["rev-parse", "--show-toplevel"])).trim();
|
|
97
|
+
// Git may report a differently cased or 8.3-shortened path; compare canonically.
|
|
98
|
+
const realTop = await fs.realpath(top).catch(() => path.resolve(top));
|
|
99
|
+
if (!samePath(realTop, installation)) throw new Error("refusing to update: PiWeb is not the Git installation root");
|
|
100
|
+
if ((await command(["status", "--porcelain", "--untracked-files=no"])).trim()) {
|
|
101
|
+
throw new Error("PiWeb has local tracked changes; preserve them before updating (no files were reset)");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
await assertIdle();
|
|
105
|
+
},
|
|
106
|
+
prepare: async (context) => {
|
|
107
|
+
const { env: releaseEnv, signal } = context;
|
|
108
|
+
const command = (cmd: string, args: string[], timeoutMs: number) => run(cmd, args, { cwd: installation, env: releaseEnv, timeoutMs, signal });
|
|
109
|
+
if (target === "pi") {
|
|
110
|
+
version = await installPiUpdate(context, assertIdle);
|
|
111
|
+
} else {
|
|
112
|
+
await assertIdle();
|
|
113
|
+
await command("git", ["pull", "--ff-only", "origin", "main"], 120_000);
|
|
114
|
+
assertInstallation(installation);
|
|
115
|
+
await assertIdle();
|
|
116
|
+
await command("npm", ["install", "--no-audit", "--no-fund"], 600_000);
|
|
117
|
+
version = await readPackageVersion(path.join(installation, "package.json"));
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
return { ...release, version, needsRestart: true };
|
|
122
|
+
} finally { updateRunning = false; }
|
|
123
|
+
}
|
|
124
|
+
return { checkForUpdate, runUpdate };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const service = createUpdateService();
|
|
128
|
+
export const checkForUpdate = service.checkForUpdate;
|
|
129
|
+
export const runUpdate = service.runUpdate;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { VERSION as PI_ENGINE_VERSION } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import appManifest from "../../package.json";
|
|
3
|
+
|
|
4
|
+
export function getRuntimeVersions(): Promise<{ piWeb: string; piEngine: string }> {
|
|
5
|
+
return Promise.resolve({ piWeb: appManifest.version, piEngine: PI_ENGINE_VERSION });
|
|
6
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话认证的共享部分(客户端安全,无 node 内置模块依赖):
|
|
3
|
+
* 登录页与 proxy / API 路由都用这些常量与纯函数。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const SESSION_COOKIE = "piweb_session";
|
|
7
|
+
export const SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
|
|
8
|
+
|
|
9
|
+
/** 登录后跳转目标白名单:仅同源相对路径,防开放重定向 */
|
|
10
|
+
export function safeNextPath(value: string | null | undefined): string {
|
|
11
|
+
if (!value || !value.startsWith("/") || value.startsWith("//") || value.includes("\\")) return "/";
|
|
12
|
+
try {
|
|
13
|
+
const parsed = new URL(value, "http://localhost");
|
|
14
|
+
return parsed.origin === "http://localhost" ? `${parsed.pathname}${parsed.search}` : "/";
|
|
15
|
+
} catch {
|
|
16
|
+
return "/";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 从 Cookie 请求头里取指定 cookie */
|
|
21
|
+
export function readCookieHeader(header: string | null, name: string): string | undefined {
|
|
22
|
+
if (!header) return undefined;
|
|
23
|
+
for (const part of header.split(";")) {
|
|
24
|
+
const eq = part.indexOf("=");
|
|
25
|
+
if (eq < 0) continue;
|
|
26
|
+
if (part.slice(0, eq).trim() === name) {
|
|
27
|
+
try {
|
|
28
|
+
return decodeURIComponent(part.slice(eq + 1).trim());
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话认证(服务端):登录页用密码换一个无状态令牌(HMAC 派生),存 httpOnly Cookie。
|
|
3
|
+
* Basic 认证保留为 API / 脚本兜底(bin/piweb.js 探活与 curl 依赖它)。
|
|
4
|
+
* 纯常量与浏览器安全函数在 web-auth-shared(登录页引用,不能带上 node:crypto)。
|
|
5
|
+
*/
|
|
6
|
+
import { createHmac } from "node:crypto";
|
|
7
|
+
import { equalText } from "./auth";
|
|
8
|
+
import { SESSION_COOKIE, SESSION_MAX_AGE_SECONDS, readCookieHeader, safeNextPath } from "./web-auth-shared";
|
|
9
|
+
|
|
10
|
+
export { SESSION_COOKIE, SESSION_MAX_AGE_SECONDS, readCookieHeader, safeNextPath };
|
|
11
|
+
|
|
12
|
+
/** 校验登录密码(恒定时间比较) */
|
|
13
|
+
export function validPassword(candidate: string, password: string): boolean {
|
|
14
|
+
return equalText(candidate, password);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 令牌 = 过期时间戳 + 以密码为密钥的 HMAC。密码变更即全部令牌失效。 */
|
|
18
|
+
export function createSessionToken(password: string, now = Date.now()): string {
|
|
19
|
+
const expires = now + SESSION_MAX_AGE_SECONDS * 1000;
|
|
20
|
+
return `${expires}.${sign(password, String(expires))}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function validSessionToken(token: string | undefined | null, password: string, now = Date.now()): boolean {
|
|
24
|
+
if (!token) return false;
|
|
25
|
+
const separator = token.indexOf(".");
|
|
26
|
+
if (separator <= 0) return false;
|
|
27
|
+
const expires = Number(token.slice(0, separator));
|
|
28
|
+
if (!Number.isSafeInteger(expires) || expires <= now) return false;
|
|
29
|
+
return equalText(token.slice(separator + 1), sign(password, String(expires)));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sign(password: string, payload: string): string {
|
|
33
|
+
return createHmac("sha256", `piweb-session:${password}`).update(payload).digest("base64url");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isSecureRequest(request: Request): boolean {
|
|
37
|
+
if (new URL(request.url).protocol === "https:") return true;
|
|
38
|
+
return (request.headers.get("x-forwarded-proto") ?? "").split(",")[0].trim() === "https";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function sessionCookieOptions(request: Request) {
|
|
42
|
+
return {
|
|
43
|
+
httpOnly: true,
|
|
44
|
+
sameSite: "strict" as const,
|
|
45
|
+
secure: isSecureRequest(request),
|
|
46
|
+
path: "/",
|
|
47
|
+
maxAge: SESSION_MAX_AGE_SECONDS,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function clearedCookieOptions(request: Request) {
|
|
52
|
+
return { ...sessionCookieOptions(request), maxAge: 0 };
|
|
53
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workspace-store:手动添加的工作区注册表(~/.pi/agent/web-workspaces.json)。
|
|
3
|
+
* 会话 cwd + 手动添加的目录共同构成侧栏「工作区」列表;pi 不受影响(独立文件)。
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { APP_ROOT } from "./app-root";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { StringDecoder } from "node:string_decoder";
|
|
10
|
+
import { getAgentDir } from "./pi";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { withExternalSettingsLock, withSettingsWriteLock } from "./settings-write-lock";
|
|
13
|
+
|
|
14
|
+
interface WorkspaceFile {
|
|
15
|
+
workspaces: string[];
|
|
16
|
+
aliases?: Record<string, string>;
|
|
17
|
+
archivedSessions?: string[];
|
|
18
|
+
removedWorkspaces?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WorkspaceRegistrySnapshot {
|
|
22
|
+
workspaces: string[];
|
|
23
|
+
aliases: Record<string, string>;
|
|
24
|
+
archivedSessions: string[];
|
|
25
|
+
removedWorkspaces: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function file(): string {
|
|
29
|
+
return path.join(getAgentDir(), "web-workspaces.json");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function readFile(target = file()): Promise<WorkspaceFile> {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(await fs.readFile(target, "utf8")) as WorkspaceFile;
|
|
35
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid workspace registry");
|
|
36
|
+
const aliases = Object.fromEntries(
|
|
37
|
+
Object.entries(typeof parsed.aliases === "object" && parsed.aliases !== null ? parsed.aliases : {})
|
|
38
|
+
.filter((entry): entry is [string, string] => typeof entry[1] === "string"),
|
|
39
|
+
);
|
|
40
|
+
return {
|
|
41
|
+
workspaces: Array.isArray(parsed.workspaces) ? parsed.workspaces.filter((w) => typeof w === "string") : [],
|
|
42
|
+
aliases,
|
|
43
|
+
archivedSessions: Array.isArray(parsed.archivedSessions) ? parsed.archivedSessions.filter((s) => typeof s === "string") : [],
|
|
44
|
+
removedWorkspaces: Array.isArray(parsed.removedWorkspaces) ? parsed.removedWorkspaces.filter((s) => typeof s === "string") : [],
|
|
45
|
+
};
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
48
|
+
}
|
|
49
|
+
return { workspaces: [], aliases: {}, archivedSessions: [], removedWorkspaces: [] };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function pathKey(value: string): string {
|
|
53
|
+
const resolved = path.resolve(value);
|
|
54
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function write(data: WorkspaceFile, target: string): Promise<void> {
|
|
58
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
59
|
+
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
60
|
+
try {
|
|
61
|
+
await fs.writeFile(temporary, JSON.stringify(data, null, 2), { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
62
|
+
await fs.rename(temporary, target);
|
|
63
|
+
} finally {
|
|
64
|
+
await fs.unlink(temporary).catch(() => undefined);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function read(): Promise<WorkspaceFile> {
|
|
69
|
+
const target = file();
|
|
70
|
+
return withSettingsWriteLock(target, () => readFile(target));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function mutate<T>(change: (data: WorkspaceFile, target: string) => Promise<T> | T): Promise<T> {
|
|
74
|
+
const target = file();
|
|
75
|
+
return withSettingsWriteLock(target, () => withExternalSettingsLock(target, async () => change(await readFile(target), target)));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function listAdded(): Promise<string[]> {
|
|
79
|
+
return (await read()).workspaces;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function getRemovedWorkspaces(): Promise<string[]> {
|
|
83
|
+
return (await read()).removedWorkspaces ?? [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function getAliases(): Promise<Record<string, string>> {
|
|
87
|
+
return (await read()).aliases ?? {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function setAlias(dir: string, name: string): Promise<Record<string, string>> {
|
|
91
|
+
const key = pathKey(path.resolve(dir));
|
|
92
|
+
return mutate(async (data, target) => {
|
|
93
|
+
if (!data.aliases) data.aliases = {};
|
|
94
|
+
// 键统一走 pathKey(win32 小写),并清掉历史遗留的大小写变体,避免别名时有时无
|
|
95
|
+
for (const existing of Object.keys(data.aliases)) {
|
|
96
|
+
if (existing !== key && pathKey(existing) === key) delete data.aliases[existing];
|
|
97
|
+
}
|
|
98
|
+
if (name.trim()) {
|
|
99
|
+
data.aliases[key] = name.trim();
|
|
100
|
+
} else {
|
|
101
|
+
delete data.aliases[key];
|
|
102
|
+
}
|
|
103
|
+
await write(data, target);
|
|
104
|
+
return data.aliases;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function getArchivedSessions(): Promise<string[]> {
|
|
109
|
+
return (await read()).archivedSessions ?? [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function getWorkspaceRegistry(): Promise<WorkspaceRegistrySnapshot> {
|
|
113
|
+
const data = await read();
|
|
114
|
+
return {
|
|
115
|
+
workspaces: data.workspaces,
|
|
116
|
+
aliases: data.aliases ?? {},
|
|
117
|
+
archivedSessions: data.archivedSessions ?? [],
|
|
118
|
+
removedWorkspaces: data.removedWorkspaces ?? [],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function archiveSession(sessionPath: string): Promise<string[]> {
|
|
123
|
+
const norm = path.resolve(sessionPath);
|
|
124
|
+
const key = pathKey(norm);
|
|
125
|
+
return mutate(async (data, target) => {
|
|
126
|
+
if (!data.archivedSessions) data.archivedSessions = [];
|
|
127
|
+
if (!data.archivedSessions.some((p) => pathKey(p) === key)) {
|
|
128
|
+
data.archivedSessions.push(norm);
|
|
129
|
+
await write(data, target);
|
|
130
|
+
}
|
|
131
|
+
return data.archivedSessions;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function forgetSession(sessionPath: string): Promise<string[]> {
|
|
136
|
+
const norm = path.resolve(sessionPath);
|
|
137
|
+
const key = pathKey(norm);
|
|
138
|
+
return mutate(async (data, target) => {
|
|
139
|
+
const previous = data.archivedSessions ?? [];
|
|
140
|
+
const next = previous.filter((p) => pathKey(p) !== key);
|
|
141
|
+
if (next.length !== previous.length) {
|
|
142
|
+
data.archivedSessions = next;
|
|
143
|
+
await write(data, target);
|
|
144
|
+
}
|
|
145
|
+
return next;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function addWorkspace(dir: string): Promise<{ workspaces: string[]; removedWorkspaces: string[] }> {
|
|
150
|
+
const norm = path.resolve(dir);
|
|
151
|
+
const key = pathKey(norm);
|
|
152
|
+
return mutate(async (data, target) => {
|
|
153
|
+
let changed = false;
|
|
154
|
+
if (!data.workspaces.some((w) => pathKey(w) === key)) {
|
|
155
|
+
data.workspaces.push(norm);
|
|
156
|
+
changed = true;
|
|
157
|
+
}
|
|
158
|
+
if (data.removedWorkspaces) {
|
|
159
|
+
const next = data.removedWorkspaces.filter((w) => pathKey(w) !== key);
|
|
160
|
+
changed ||= next.length !== data.removedWorkspaces.length;
|
|
161
|
+
data.removedWorkspaces = next;
|
|
162
|
+
}
|
|
163
|
+
if (changed) await write(data, target);
|
|
164
|
+
return { workspaces: data.workspaces, removedWorkspaces: data.removedWorkspaces ?? [] };
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function registerCwds(cwds: string[]): Promise<string[]> {
|
|
169
|
+
return mutate(async (data, target) => {
|
|
170
|
+
let changed = false;
|
|
171
|
+
const removedKeys = new Set((data.removedWorkspaces ?? []).map(pathKey));
|
|
172
|
+
const existingKeys = new Set(data.workspaces.map(pathKey));
|
|
173
|
+
|
|
174
|
+
for (const cwd of cwds) {
|
|
175
|
+
if (!cwd || typeof cwd !== "string") continue;
|
|
176
|
+
const norm = path.resolve(cwd);
|
|
177
|
+
const key = pathKey(norm);
|
|
178
|
+
if (!removedKeys.has(key) && !existingKeys.has(key)) {
|
|
179
|
+
data.workspaces.push(norm);
|
|
180
|
+
existingKeys.add(key);
|
|
181
|
+
changed = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (changed) await write(data, target);
|
|
185
|
+
return data.workspaces;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function removeWorkspace(dir: string): Promise<{ workspaces: string[]; removedWorkspaces: string[]; aliases: Record<string, string> }> {
|
|
190
|
+
const norm = path.resolve(dir);
|
|
191
|
+
const key = pathKey(norm);
|
|
192
|
+
return mutate(async (data, target) => {
|
|
193
|
+
data.workspaces = data.workspaces.filter((w) => pathKey(w) !== key);
|
|
194
|
+
if (!data.removedWorkspaces) data.removedWorkspaces = [];
|
|
195
|
+
if (!data.removedWorkspaces.some((w) => pathKey(w) === key)) {
|
|
196
|
+
data.removedWorkspaces.push(norm);
|
|
197
|
+
}
|
|
198
|
+
if (data.aliases) {
|
|
199
|
+
for (const k of Object.keys(data.aliases)) {
|
|
200
|
+
if (pathKey(k) === key) delete data.aliases[k];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
await write(data, target);
|
|
204
|
+
return { workspaces: data.workspaces, removedWorkspaces: data.removedWorkspaces, aliases: data.aliases ?? {} };
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let picking = false;
|
|
209
|
+
|
|
210
|
+
/** 弹出系统原生文件夹选择对话框(Windows 现代资源管理器风格,屏幕居中) */
|
|
211
|
+
export async function pickFolderNative(): Promise<{ path: string | null; canceled: boolean }> {
|
|
212
|
+
if (process.platform !== "win32") throw new Error("native folder picker is currently available only on Windows");
|
|
213
|
+
if (picking) return { path: null, canceled: true };
|
|
214
|
+
picking = true;
|
|
215
|
+
try {
|
|
216
|
+
const script = path.join(APP_ROOT, "scripts", "pick-folder.ps1");
|
|
217
|
+
const out = await new Promise<string>((resolve, reject) => {
|
|
218
|
+
const child = spawn(
|
|
219
|
+
"powershell.exe",
|
|
220
|
+
["-NoProfile", "-STA", "-ExecutionPolicy", "Bypass", "-File", script, "选择工作区文件夹"],
|
|
221
|
+
// windowsHide:不再先闪出一个黑色 PowerShell 控制台窗口,只出现资源管理器风格的选择对话框
|
|
222
|
+
{ stdio: ["ignore", "pipe", "pipe"], windowsHide: true },
|
|
223
|
+
);
|
|
224
|
+
const decoder = new StringDecoder("utf8");
|
|
225
|
+
let buf = "";
|
|
226
|
+
let stderr = "";
|
|
227
|
+
let decoderEnded = false;
|
|
228
|
+
child.stdout.on("data", (data: Buffer) => {
|
|
229
|
+
buf += decoder.write(data);
|
|
230
|
+
});
|
|
231
|
+
child.stderr.on("data", (data: Buffer) => { stderr = (stderr + data.toString("utf8")).slice(-1000); });
|
|
232
|
+
let settled = false;
|
|
233
|
+
let timeout: ReturnType<typeof setTimeout>;
|
|
234
|
+
const finish = (error?: Error) => {
|
|
235
|
+
if (settled) return;
|
|
236
|
+
settled = true;
|
|
237
|
+
clearTimeout(timeout);
|
|
238
|
+
if (!decoderEnded) {
|
|
239
|
+
decoderEnded = true;
|
|
240
|
+
buf += decoder.end();
|
|
241
|
+
}
|
|
242
|
+
if (error) reject(error);
|
|
243
|
+
else resolve(buf.trim());
|
|
244
|
+
};
|
|
245
|
+
child.on("error", (error) => finish(error));
|
|
246
|
+
child.on("close", (code) => finish(code === 0 ? undefined : new Error(stderr || `folder picker exited with code ${code}`)));
|
|
247
|
+
// 5 分钟超时保护
|
|
248
|
+
timeout = setTimeout(() => {
|
|
249
|
+
try {
|
|
250
|
+
child.kill();
|
|
251
|
+
} catch {
|
|
252
|
+
/* ignore */
|
|
253
|
+
}
|
|
254
|
+
finish(new Error("folder picker timed out"));
|
|
255
|
+
}, 5 * 60 * 1000);
|
|
256
|
+
});
|
|
257
|
+
return out ? { path: out, canceled: false } : { path: null, canceled: true };
|
|
258
|
+
} finally {
|
|
259
|
+
picking = false;
|
|
260
|
+
}
|
|
261
|
+
}
|
package/src/proxy.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from "next/server";
|
|
2
|
+
import { isSafeHost, isSafeOrigin, validBasicAuthorization } from "@/lib/auth";
|
|
3
|
+
import { SESSION_COOKIE, readCookieHeader, validSessionToken } from "@/lib/web-auth";
|
|
4
|
+
|
|
5
|
+
const READ_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
6
|
+
/** 未认证也可达:登录页自身与认证接口 */
|
|
7
|
+
const PUBLIC_PATHS = new Set(["/login", "/api/web-auth"]);
|
|
8
|
+
/** 静态资源不参与认证(dev 的 assetPrefix 是 /_piweb-dev/<id>,生产是 /_next) */
|
|
9
|
+
const ASSET_PREFIXES = ["/_next/", "/_piweb-dev/"];
|
|
10
|
+
const ASSET_PATHS = new Set(["/icon.svg", "/favicon.ico"]);
|
|
11
|
+
|
|
12
|
+
export function proxy(request: NextRequest) {
|
|
13
|
+
const password = process.env.PI_WEB_PASSWORD;
|
|
14
|
+
if (!isSafeHost(request, Boolean(password))) {
|
|
15
|
+
return NextResponse.json({ success: false, error: "untrusted host" }, { status: 403 });
|
|
16
|
+
}
|
|
17
|
+
if (!READ_METHODS.has(request.method) && !isSafeOrigin(request)) {
|
|
18
|
+
return NextResponse.json({ success: false, error: "cross-origin request rejected" }, { status: 403 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!password) return NextResponse.next();
|
|
22
|
+
|
|
23
|
+
const pathname = request.nextUrl.pathname;
|
|
24
|
+
if (pathname === "/api/health" && (request.method === "GET" || request.method === "HEAD")) return NextResponse.next();
|
|
25
|
+
if (ASSET_PREFIXES.some((prefix) => pathname.startsWith(prefix)) || ASSET_PATHS.has(pathname)) return NextResponse.next();
|
|
26
|
+
|
|
27
|
+
const authenticated =
|
|
28
|
+
validBasicAuthorization(request.headers.get("authorization"), password) ||
|
|
29
|
+
validSessionToken(readCookieHeader(request.headers.get("cookie"), SESSION_COOKIE), password);
|
|
30
|
+
if (authenticated) return NextResponse.next();
|
|
31
|
+
|
|
32
|
+
if (PUBLIC_PATHS.has(pathname)) return NextResponse.next();
|
|
33
|
+
|
|
34
|
+
// 页面请求 → 登录页(带返回地址);API → 401,保留脚本客户端的 Basic 挑战。
|
|
35
|
+
if (pathname.startsWith("/api/")) {
|
|
36
|
+
return new NextResponse(JSON.stringify({ success: false, error: "authentication required" }), {
|
|
37
|
+
status: 401,
|
|
38
|
+
headers: {
|
|
39
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
40
|
+
"WWW-Authenticate": 'Basic realm="PiWeb", charset="UTF-8"',
|
|
41
|
+
"Cache-Control": "no-store",
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const login = request.nextUrl.clone();
|
|
46
|
+
login.pathname = "/login";
|
|
47
|
+
login.search = `?next=${encodeURIComponent(`${pathname}${request.nextUrl.search}`)}`;
|
|
48
|
+
return NextResponse.redirect(login, 307);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const config = {
|
|
52
|
+
matcher: ["/((?!_next/|_piweb-dev/|icon\\.svg|favicon\\.ico).*)"],
|
|
53
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": [
|
|
5
|
+
"dom",
|
|
6
|
+
"dom.iterable",
|
|
7
|
+
"esnext"
|
|
8
|
+
],
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"module": "esnext",
|
|
15
|
+
"moduleResolution": "bundler",
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"isolatedModules": true,
|
|
18
|
+
"jsx": "react-jsx",
|
|
19
|
+
"incremental": true,
|
|
20
|
+
"plugins": [
|
|
21
|
+
{
|
|
22
|
+
"name": "next"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"paths": {
|
|
26
|
+
"@/*": [
|
|
27
|
+
"./src/*"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"include": [
|
|
32
|
+
"next-env.d.ts",
|
|
33
|
+
"**/*.ts",
|
|
34
|
+
"**/*.tsx",
|
|
35
|
+
".next/types/**/*.ts",
|
|
36
|
+
".next/dev/types/**/*.ts",
|
|
37
|
+
".next-dev/types/**/*.ts",
|
|
38
|
+
".next-dev/dev/types/**/*.ts",
|
|
39
|
+
".next-dev-webpack/types/**/*.ts",
|
|
40
|
+
".next-dev-webpack/dev/types/**/*.ts"
|
|
41
|
+
],
|
|
42
|
+
"exclude": [
|
|
43
|
+
"node_modules",
|
|
44
|
+
".next-releases",
|
|
45
|
+
"pi",
|
|
46
|
+
"dsh",
|
|
47
|
+
"bin"
|
|
48
|
+
]
|
|
49
|
+
}
|