@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,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plugins-service:pi 包与扩展的清单、安装/卸载/更新/启用/停用(与 pi 运行时完全同步与持久化)。
|
|
3
|
+
*/
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { getAgentDir, getPackageManager, getResourceLoader, getSettingsManager, reloadAllLoaders, resourceLoaderReady } from "./pi";
|
|
8
|
+
import { reloadSessionsForCwd } from "./agent-manager";
|
|
9
|
+
import { flushSettingsOrThrow, withExternalSettingsLock, withSettingsWriteLock } from "./settings-write-lock";
|
|
10
|
+
|
|
11
|
+
export interface PackageView {
|
|
12
|
+
source: string;
|
|
13
|
+
scope: "user" | "project";
|
|
14
|
+
installedPath?: string;
|
|
15
|
+
resources: { extensions: number; skills: number; prompts: number; themes: number };
|
|
16
|
+
disabled: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ExtensionView {
|
|
20
|
+
name: string;
|
|
21
|
+
path: string;
|
|
22
|
+
disabled: boolean;
|
|
23
|
+
/** 加载失败的原因(语法错误、依赖缺失等),来自 pi 资源加载器 */
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* pi 的启用/停用语义(core/package-manager.js isEnabledByOverrides):
|
|
29
|
+
* settings.extensions 里 `-<path>` 强制排除、`+<path>` 强制包含,路径相对于该 settings 所在的基目录。
|
|
30
|
+
* 之前 PiWeb 写的 disabledExtensions 键 pi 根本不读,开关是假的。
|
|
31
|
+
*/
|
|
32
|
+
function extensionPattern(extPath: string, baseDir: string): string {
|
|
33
|
+
const rel = path.relative(baseDir, extPath).replace(/\\/g, "/");
|
|
34
|
+
return rel && !rel.startsWith("..") ? rel : extPath.replace(/\\/g, "/");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isForceExcluded(extPath: string, patterns: string[], baseDir: string): boolean {
|
|
38
|
+
const rel = path.relative(baseDir, extPath).replace(/\\/g, "/");
|
|
39
|
+
const abs = extPath.replace(/\\/g, "/");
|
|
40
|
+
const norm = (p: string) => (p.startsWith("./") ? p.slice(2) : p).replace(/\\/g, "/");
|
|
41
|
+
return patterns.some((p) => p.startsWith("-") && [rel, abs].includes(norm(p.slice(1))));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 全部变更后的统一收尾:加载器就地重载 + 活跃会话 session.reload() + 广播新清单;全局变更影响所有目录 */
|
|
45
|
+
async function applyResourceChange(cwd: string, scope: "user" | "project"): Promise<void> {
|
|
46
|
+
await reloadAllLoaders();
|
|
47
|
+
await reloadSessionsForCwd(scope === "project" ? cwd : undefined);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function readSettingsFile(filePath: string): Promise<Record<string, unknown>> {
|
|
51
|
+
try {
|
|
52
|
+
return await readSettingsFileStrict(filePath);
|
|
53
|
+
} catch {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function readSettingsFileStrict(filePath: string): Promise<Record<string, unknown>> {
|
|
59
|
+
let text: string;
|
|
60
|
+
try {
|
|
61
|
+
text = await fs.readFile(filePath, "utf8");
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
const parsed: unknown = JSON.parse(text.replace(/^\uFEFF/, ""));
|
|
67
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${filePath} must contain a JSON object`);
|
|
68
|
+
return parsed as Record<string, unknown>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function prepareSettingsWrite(cwd: string, file: string): Promise<ReturnType<typeof getSettingsManager>> {
|
|
72
|
+
await readSettingsFileStrict(file);
|
|
73
|
+
const manager = getSettingsManager(cwd);
|
|
74
|
+
await flushSettingsOrThrow(manager);
|
|
75
|
+
// Another cwd's manager may have changed the same global array since our last read.
|
|
76
|
+
await manager.reload();
|
|
77
|
+
await flushSettingsOrThrow(manager);
|
|
78
|
+
return manager;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function verifySettingsField(file: string, field: "extensions" | "packages", expected: unknown): Promise<void> {
|
|
82
|
+
const persisted = await readSettingsFileStrict(file);
|
|
83
|
+
if (JSON.stringify(persisted[field] ?? []) !== JSON.stringify(expected ?? [])) {
|
|
84
|
+
throw new Error(`Pi settings ${field} was not persisted`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function writeSettingsFile(filePath: string, data: Record<string, unknown>): Promise<void> {
|
|
89
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
90
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
91
|
+
try {
|
|
92
|
+
await fs.writeFile(temporary, JSON.stringify(data, null, "\t"), "utf8");
|
|
93
|
+
await fs.rename(temporary, filePath);
|
|
94
|
+
} finally {
|
|
95
|
+
await fs.rm(temporary, { force: true }).catch(() => undefined);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function countResources(installedPath: string): Promise<PackageView["resources"]> {
|
|
100
|
+
const out = { extensions: 0, skills: 0, prompts: 0, themes: 0 };
|
|
101
|
+
const dirs: (keyof PackageView["resources"] | "prompts")[] = ["extensions", "skills", "prompts", "themes"];
|
|
102
|
+
// 包可能直接就是资源目录,也可能带 src/dist;按约定目录统计
|
|
103
|
+
const candidates = [installedPath, path.join(installedPath, "dist")];
|
|
104
|
+
for (const base of candidates) {
|
|
105
|
+
for (const d of dirs) {
|
|
106
|
+
try {
|
|
107
|
+
const entries = await fs.readdir(path.join(base, d), { withFileTypes: true });
|
|
108
|
+
if (d === "extensions") {
|
|
109
|
+
out.extensions += entries.filter((e) => e.isFile() && /\.(ts|js)$/.test(e.name)).length;
|
|
110
|
+
} else if (d === "skills") {
|
|
111
|
+
out.skills += entries.filter((e) => e.isDirectory()).length;
|
|
112
|
+
} else if (d === "prompts") {
|
|
113
|
+
out.prompts += entries.filter((e) => e.isFile() && /\.md$/.test(e.name)).length;
|
|
114
|
+
} else if (d === "themes") {
|
|
115
|
+
out.themes += entries.filter((e) => e.isFile() && /\.json$/.test(e.name)).length;
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
/* 目录不存在 */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function listPackages(cwd: string): Promise<PackageView[]> {
|
|
126
|
+
const pm = getPackageManager(cwd);
|
|
127
|
+
const configured = pm.listConfiguredPackages();
|
|
128
|
+
|
|
129
|
+
const globalSettings = await readSettingsFile(path.join(getAgentDir(), "settings.json"));
|
|
130
|
+
const projectSettings = await readSettingsFile(path.join(cwd, ".pi", "settings.json"));
|
|
131
|
+
|
|
132
|
+
const sourceOf = (p: unknown): string => (typeof p === "string" ? p : String((p as { source?: string } | null)?.source ?? ""));
|
|
133
|
+
const userDisabled = (Array.isArray(globalSettings.disabledPackages) ? globalSettings.disabledPackages : []).map(sourceOf).filter(Boolean);
|
|
134
|
+
const projectDisabled = (Array.isArray(projectSettings.disabledPackages) ? projectSettings.disabledPackages : []).map(sourceOf).filter(Boolean);
|
|
135
|
+
|
|
136
|
+
const out: PackageView[] = [];
|
|
137
|
+
const seen = new Set<string>();
|
|
138
|
+
|
|
139
|
+
// 1. 活跃配置包(已启用)
|
|
140
|
+
for (const p of configured) {
|
|
141
|
+
const key = `${p.scope}:${p.source}`;
|
|
142
|
+
seen.add(key);
|
|
143
|
+
const installedPath = p.installedPath ?? pm.getInstalledPath(p.source, p.scope);
|
|
144
|
+
const resources = installedPath ? await countResources(installedPath) : { extensions: 0, skills: 0, prompts: 0, themes: 0 };
|
|
145
|
+
out.push({ source: p.source, scope: p.scope, installedPath, resources, disabled: false });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 2. 停用包(持久化在 disabledPackages)
|
|
149
|
+
for (const source of projectDisabled) {
|
|
150
|
+
const key = `project:${source}`;
|
|
151
|
+
if (seen.has(key)) continue;
|
|
152
|
+
seen.add(key);
|
|
153
|
+
const installedPath = pm.getInstalledPath(source, "project");
|
|
154
|
+
const resources = installedPath ? await countResources(installedPath) : { extensions: 0, skills: 0, prompts: 0, themes: 0 };
|
|
155
|
+
out.push({ source, scope: "project", installedPath, resources, disabled: true });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (const source of userDisabled) {
|
|
159
|
+
const key = `user:${source}`;
|
|
160
|
+
if (seen.has(key)) continue;
|
|
161
|
+
seen.add(key);
|
|
162
|
+
const installedPath = pm.getInstalledPath(source, "user");
|
|
163
|
+
const resources = installedPath ? await countResources(installedPath) : { extensions: 0, skills: 0, prompts: 0, themes: 0 };
|
|
164
|
+
out.push({ source, scope: "user", installedPath, resources, disabled: true });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function listLoadedExtensions(cwd: string): Promise<ExtensionView[]> {
|
|
171
|
+
await resourceLoaderReady(cwd);
|
|
172
|
+
const loader = getResourceLoader(cwd);
|
|
173
|
+
const result = loader.getExtensions() as unknown as {
|
|
174
|
+
extensions?: Array<{ path?: string; name?: string }>;
|
|
175
|
+
errors?: Array<{ path: string; error: string }>;
|
|
176
|
+
};
|
|
177
|
+
const sm = getSettingsManager(cwd);
|
|
178
|
+
const globalPatterns = sm.getGlobalSettings().extensions ?? [];
|
|
179
|
+
const projectPatterns = sm.getProjectSettings().extensions ?? [];
|
|
180
|
+
const agentDir = getAgentDir();
|
|
181
|
+
const projectBase = path.join(cwd, ".pi");
|
|
182
|
+
|
|
183
|
+
const out: ExtensionView[] = [];
|
|
184
|
+
const seenPaths = new Set<string>();
|
|
185
|
+
for (const e of result?.extensions ?? []) {
|
|
186
|
+
const p = String(e.path ?? "");
|
|
187
|
+
if (!p) continue;
|
|
188
|
+
const name = String(e.name ?? path.basename(p).replace(/\.(ts|js)$/, ""));
|
|
189
|
+
seenPaths.add(p);
|
|
190
|
+
out.push({ name, path: p, disabled: false });
|
|
191
|
+
}
|
|
192
|
+
for (const err of result?.errors ?? []) {
|
|
193
|
+
if (seenPaths.has(err.path)) continue;
|
|
194
|
+
seenPaths.add(err.path);
|
|
195
|
+
out.push({ name: path.basename(err.path).replace(/\.(ts|js)$/, ""), path: err.path, disabled: false, error: err.error });
|
|
196
|
+
}
|
|
197
|
+
// 被 `-path` 排除的扩展不会出现在 loader 结果里:从两份 settings 的模式里补出来,让用户能重新启用
|
|
198
|
+
const excluded: Array<{ pattern: string; base: string }> = [
|
|
199
|
+
...globalPatterns.filter((p) => p.startsWith("-")).map((p) => ({ pattern: p.slice(1), base: agentDir })),
|
|
200
|
+
...projectPatterns.filter((p) => p.startsWith("-")).map((p) => ({ pattern: p.slice(1), base: projectBase })),
|
|
201
|
+
];
|
|
202
|
+
for (const { pattern, base } of excluded) {
|
|
203
|
+
const abs = path.isAbsolute(pattern) ? pattern : path.resolve(base, pattern);
|
|
204
|
+
if (seenPaths.has(abs)) continue;
|
|
205
|
+
seenPaths.add(abs);
|
|
206
|
+
out.push({ name: path.basename(abs).replace(/\.(ts|js)$/, ""), path: abs, disabled: true });
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function togglePackage(
|
|
212
|
+
source: string,
|
|
213
|
+
disable: boolean,
|
|
214
|
+
scope: "user" | "project",
|
|
215
|
+
cwd: string
|
|
216
|
+
): Promise<void> {
|
|
217
|
+
const settingsPath = scope === "project" ? path.join(cwd, ".pi", "settings.json") : path.join(getAgentDir(), "settings.json");
|
|
218
|
+
await withSettingsWriteLock(settingsPath, () => withExternalSettingsLock(settingsPath, async () => {
|
|
219
|
+
const settings = await readSettingsFileStrict(settingsPath);
|
|
220
|
+
const sourceOf = (p: unknown) => (typeof p === "string" ? p : (p as { source?: string } | null)?.source);
|
|
221
|
+
const packages: unknown[] = Array.isArray(settings.packages) ? settings.packages : [];
|
|
222
|
+
// 停用的包原样存到 disabledPackages(PiWeb 私有键,pi 不读)
|
|
223
|
+
const disabledPackages: unknown[] = Array.isArray(settings.disabledPackages) ? settings.disabledPackages : [];
|
|
224
|
+
if (disable) {
|
|
225
|
+
const moved = packages.filter((p) => sourceOf(p) === source);
|
|
226
|
+
settings.packages = packages.filter((p) => sourceOf(p) !== source);
|
|
227
|
+
settings.disabledPackages = [...disabledPackages.filter((p) => sourceOf(p) !== source), ...(moved.length ? moved : [source])];
|
|
228
|
+
} else {
|
|
229
|
+
const restored = disabledPackages.filter((p) => sourceOf(p) === source);
|
|
230
|
+
settings.disabledPackages = disabledPackages.filter((p) => sourceOf(p) !== source);
|
|
231
|
+
settings.packages = packages.some((p) => sourceOf(p) === source) ? packages : [...packages, ...(restored.length ? restored : [source])];
|
|
232
|
+
}
|
|
233
|
+
await writeSettingsFile(settingsPath, settings);
|
|
234
|
+
}));
|
|
235
|
+
await applyResourceChange(cwd, scope);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function toggleExtension(
|
|
239
|
+
extPath: string,
|
|
240
|
+
disable: boolean,
|
|
241
|
+
cwd: string
|
|
242
|
+
): Promise<void> {
|
|
243
|
+
const norm = extPath.replace(/\\/g, "/").toLowerCase();
|
|
244
|
+
const isProject = norm.startsWith(`${path.resolve(cwd).replace(/\\/g, "/").toLowerCase()}/`);
|
|
245
|
+
const baseDir = isProject ? path.join(cwd, ".pi") : getAgentDir();
|
|
246
|
+
const settingsPath = isProject ? path.join(cwd, ".pi", "settings.json") : path.join(getAgentDir(), "settings.json");
|
|
247
|
+
await withSettingsWriteLock(settingsPath, async () => {
|
|
248
|
+
const sm = await prepareSettingsWrite(cwd, settingsPath);
|
|
249
|
+
const current = (isProject ? sm.getProjectSettings().extensions : sm.getGlobalSettings().extensions) ?? [];
|
|
250
|
+
const pattern = extensionPattern(extPath, baseDir);
|
|
251
|
+
const withoutOurs = current.filter((p) => !(p.startsWith("-") && [pattern, extPath.replace(/\\/g, "/")].includes((p.slice(1).startsWith("./") ? p.slice(3) : p.slice(1)).replace(/\\/g, "/"))));
|
|
252
|
+
const next = disable ? [...withoutOurs, `-${pattern}`] : withoutOurs;
|
|
253
|
+
if (!disable && isForceExcluded(extPath, next, baseDir)) {
|
|
254
|
+
for (let i = next.length - 1; i >= 0; i -= 1) if (isForceExcluded(extPath, [next[i]], baseDir)) next.splice(i, 1);
|
|
255
|
+
}
|
|
256
|
+
if (isProject) sm.setProjectExtensionPaths(next);
|
|
257
|
+
else sm.setExtensionPaths(next);
|
|
258
|
+
await flushSettingsOrThrow(sm);
|
|
259
|
+
await verifySettingsField(settingsPath, "extensions", next);
|
|
260
|
+
});
|
|
261
|
+
await applyResourceChange(cwd, isProject ? "project" : "user");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function installPackage(source: string, local: boolean, cwd: string): Promise<void> {
|
|
265
|
+
const pm = getPackageManager(cwd);
|
|
266
|
+
const settingsPath = local ? path.join(cwd, ".pi", "settings.json") : path.join(getAgentDir(), "settings.json");
|
|
267
|
+
await withSettingsWriteLock(settingsPath, async () => {
|
|
268
|
+
const sm = await prepareSettingsWrite(cwd, settingsPath);
|
|
269
|
+
await pm.installAndPersist(source, { local });
|
|
270
|
+
await flushSettingsOrThrow(sm);
|
|
271
|
+
await verifySettingsField(settingsPath, "packages", (local ? sm.getProjectSettings() : sm.getGlobalSettings()).packages);
|
|
272
|
+
});
|
|
273
|
+
await applyResourceChange(cwd, local ? "project" : "user");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function removePackage(source: string, local: boolean, cwd: string): Promise<boolean> {
|
|
277
|
+
const pm = getPackageManager(cwd);
|
|
278
|
+
const settingsPath = local ? path.join(cwd, ".pi", "settings.json") : path.join(getAgentDir(), "settings.json");
|
|
279
|
+
const removed = await withSettingsWriteLock(settingsPath, async () => {
|
|
280
|
+
const sm = await prepareSettingsWrite(cwd, settingsPath);
|
|
281
|
+
// SDK failures must surface before touching the disabled-package bookkeeping.
|
|
282
|
+
const removedActive = await pm.removeAndPersist(source, { local });
|
|
283
|
+
await flushSettingsOrThrow(sm);
|
|
284
|
+
await verifySettingsField(settingsPath, "packages", (local ? sm.getProjectSettings() : sm.getGlobalSettings()).packages);
|
|
285
|
+
const removedDisabled = await withExternalSettingsLock(settingsPath, async () => {
|
|
286
|
+
const settings = await readSettingsFileStrict(settingsPath);
|
|
287
|
+
const sourceOf = (p: unknown) => (typeof p === "string" ? p : (p as { source?: string } | null)?.source);
|
|
288
|
+
if (Array.isArray(settings.disabledPackages) && settings.disabledPackages.some((p) => sourceOf(p) === source)) {
|
|
289
|
+
settings.disabledPackages = settings.disabledPackages.filter((p) => sourceOf(p) !== source);
|
|
290
|
+
await writeSettingsFile(settingsPath, settings);
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
});
|
|
295
|
+
return removedActive || removedDisabled;
|
|
296
|
+
});
|
|
297
|
+
await applyResourceChange(cwd, local ? "project" : "user");
|
|
298
|
+
return removed;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function updatePackages(source: string | undefined, cwd: string): Promise<void> {
|
|
302
|
+
const pm = getPackageManager(cwd);
|
|
303
|
+
await withSettingsWriteLock(path.join(getAgentDir(), "settings.json"), () => pm.update(source));
|
|
304
|
+
await applyResourceChange(cwd, "user");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** 热重载全部扩展(pi /reload 等价物):加载器 + 所有活跃会话 */
|
|
308
|
+
export async function reloadExtensions(cwd?: string): Promise<{ loaders: number; sessions: number }> {
|
|
309
|
+
const loaders = (await reloadAllLoaders()).length;
|
|
310
|
+
const sessions = await reloadSessionsForCwd(cwd);
|
|
311
|
+
return { loaders, sessions };
|
|
312
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 对话过程行的纯文本整理:工具种类归类、命令去噪、路径相对化、思考首句、模型错误归类。
|
|
3
|
+
* 不依赖 React,便于单测(tests/process-format.test.ts)。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type ToolKind = "cmd" | "read" | "search" | "write" | "other";
|
|
7
|
+
|
|
8
|
+
/** 工具名 → 种类(决定颜色与结果预览方式) */
|
|
9
|
+
export function toolKind(name: string): ToolKind {
|
|
10
|
+
const n = name.toLowerCase();
|
|
11
|
+
if (n === "bash" || n === "powershell" || n === "pwsh" || n === "shell" || n === "sh" || n === "cmd") return "cmd";
|
|
12
|
+
if (n === "read") return "read";
|
|
13
|
+
if (n === "grep" || n === "glob" || n === "find" || n === "ls" || n === "search") return "search";
|
|
14
|
+
if (n === "edit" || n === "write" || n === "multiedit" || n === "multi_edit") return "write";
|
|
15
|
+
return "other";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 统一为正斜杠;去掉包裹引号;MSYS 形式 /d/AIApp → D:/AIApp */
|
|
19
|
+
export function canonPath(p: string): string {
|
|
20
|
+
let s = p.trim().replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1").replace(/\\/g, "/");
|
|
21
|
+
const msys = s.match(/^\/([a-zA-Z])\/(.*)$/);
|
|
22
|
+
if (msys) s = `${msys[1].toUpperCase()}:/${msys[2]}`;
|
|
23
|
+
return s;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stripTrailingSlash(p: string): string {
|
|
27
|
+
return p.replace(/\/+$/, "");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 工作区内的绝对路径显示为相对路径;工作区外原样(仅统一斜杠) */
|
|
31
|
+
export function relativizePath(path: string, cwd?: string): string {
|
|
32
|
+
if (!path) return path;
|
|
33
|
+
const p = canonPath(path);
|
|
34
|
+
if (!cwd) return p;
|
|
35
|
+
const root = stripTrailingSlash(canonPath(cwd));
|
|
36
|
+
if (!root) return p;
|
|
37
|
+
const pl = p.toLowerCase();
|
|
38
|
+
const rl = root.toLowerCase();
|
|
39
|
+
if (pl === rl) return ".";
|
|
40
|
+
if (pl.startsWith(`${rl}/`)) return p.slice(root.length + 1);
|
|
41
|
+
return p;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function basename(p: string): string {
|
|
45
|
+
const parts = canonPath(p).split("/").filter(Boolean);
|
|
46
|
+
return parts[parts.length - 1] ?? p;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 命令行摘要:去掉开头“cd <工作区> &&”这类噪音(目标恰为当前工作区时才去),
|
|
51
|
+
* 折叠空白,只取首行;返回总行数供“…N 行”提示。
|
|
52
|
+
*/
|
|
53
|
+
export function cleanCommand(command: string, cwd?: string): { text: string; lines: number } {
|
|
54
|
+
const raw = command.replace(/\r/g, "");
|
|
55
|
+
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
56
|
+
let first = lines[0] ?? "";
|
|
57
|
+
if (cwd) {
|
|
58
|
+
const root = stripTrailingSlash(canonPath(cwd)).toLowerCase();
|
|
59
|
+
// 允许连着多个 cd(极少见),逐个剥
|
|
60
|
+
for (;;) {
|
|
61
|
+
const m = first.match(/^\s*(?:cd|pushd)\s+(?:\/d\s+)?("[^"]*"|'[^']*'|\S+)\s*(?:&&|;)\s*/);
|
|
62
|
+
if (!m) break;
|
|
63
|
+
const target = stripTrailingSlash(canonPath(m[1])).toLowerCase();
|
|
64
|
+
if (target !== root) break;
|
|
65
|
+
first = first.slice(m[0].length);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
let text = first.replace(/\s+/g, " ").trim();
|
|
69
|
+
text = dropEchoSeparators(text);
|
|
70
|
+
if (cwd) text = relativizeInText(text, cwd);
|
|
71
|
+
return { text, lines: lines.length };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 按顶层的 ; && || 切分(引号内不切) */
|
|
75
|
+
function splitTopLevel(cmd: string): string[] {
|
|
76
|
+
const parts: string[] = [];
|
|
77
|
+
let cur = "";
|
|
78
|
+
let quote: string | null = null;
|
|
79
|
+
for (let i = 0; i < cmd.length; i += 1) {
|
|
80
|
+
const ch = cmd[i];
|
|
81
|
+
if (quote) {
|
|
82
|
+
cur += ch;
|
|
83
|
+
if (ch === quote && cmd[i - 1] !== "\\") quote = null;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (ch === '"' || ch === "'") {
|
|
87
|
+
quote = ch;
|
|
88
|
+
cur += ch;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (ch === ";") {
|
|
92
|
+
parts.push(cur);
|
|
93
|
+
cur = "";
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if ((ch === "&" && cmd[i + 1] === "&") || (ch === "|" && cmd[i + 1] === "|")) {
|
|
97
|
+
parts.push(cur);
|
|
98
|
+
cur = "";
|
|
99
|
+
i += 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
cur += ch;
|
|
103
|
+
}
|
|
104
|
+
parts.push(cur);
|
|
105
|
+
return parts;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 去掉只起分隔/标题作用的 echo 段(echo "═══ rag ═══"; echo ---; echo ""),只留真正的命令。
|
|
110
|
+
* 带变量的 echo(echo "EXIT=$?")是结果输出,保留。全是分隔段就原样返回。
|
|
111
|
+
*/
|
|
112
|
+
export function dropEchoSeparators(cmd: string): string {
|
|
113
|
+
const parts = splitTopLevel(cmd).map((p) => p.trim());
|
|
114
|
+
if (parts.length < 2) return cmd;
|
|
115
|
+
const isSeparator = (p: string): boolean => {
|
|
116
|
+
const m = p.match(/^echo(?:\s+-[ne]+)?\s*(?:"([^"]*)"|'([^']*)'|(\S*))?$/);
|
|
117
|
+
if (!m) return false;
|
|
118
|
+
const payload = m[1] ?? m[2] ?? m[3] ?? "";
|
|
119
|
+
if (payload.includes("$")) return false;
|
|
120
|
+
if (/^[\s\-=─═—#*~_.>]*$/.test(payload)) return true;
|
|
121
|
+
return /[─═=\-#*~]{3,}/.test(payload);
|
|
122
|
+
};
|
|
123
|
+
const kept = parts.filter((p) => p && !isSeparator(p));
|
|
124
|
+
if (!kept.length || kept.length === parts.filter(Boolean).length) return cmd;
|
|
125
|
+
return kept.join("; ");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const TOOL_DISPLAY: Record<string, string> = {
|
|
129
|
+
bash: "Bash",
|
|
130
|
+
powershell: "PowerShell",
|
|
131
|
+
pwsh: "PowerShell",
|
|
132
|
+
shell: "Shell",
|
|
133
|
+
read: "Read",
|
|
134
|
+
edit: "Edit",
|
|
135
|
+
multiedit: "MultiEdit",
|
|
136
|
+
multi_edit: "MultiEdit",
|
|
137
|
+
write: "Write",
|
|
138
|
+
grep: "Grep",
|
|
139
|
+
glob: "Glob",
|
|
140
|
+
find: "Find",
|
|
141
|
+
ls: "List",
|
|
142
|
+
search: "Search",
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** 工具名的展示写法(Claude Code 式的 Bash / Read / Edit…),未知工具首字母大写 */
|
|
146
|
+
export function displayToolName(name: string): string {
|
|
147
|
+
const n = name.toLowerCase();
|
|
148
|
+
return TOOL_DISPLAY[n] ?? (name.charAt(0).toUpperCase() + name.slice(1));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const LIST_VERBS = new Set(["ls", "dir", "tree", "find", "fd"]);
|
|
152
|
+
|
|
153
|
+
/** 命令的动词(去掉环境变量前缀与路径) */
|
|
154
|
+
export function commandVerb(command: string): string {
|
|
155
|
+
const first = command.replace(/\r/g, "").split("\n").find((l) => l.trim().length > 0) ?? "";
|
|
156
|
+
const tokens = first.trim().replace(/^(?:\w+=\S*\s+)+/, "").split(/\s+/);
|
|
157
|
+
return basename(tokens[0] ?? "").replace(/\.exe$/i, "").toLowerCase();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 列目录类命令(Codex 的「Listed」) */
|
|
161
|
+
export function isListCommand(command: string): boolean {
|
|
162
|
+
return LIST_VERBS.has(commandVerb(command));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function escapeRe(s: string): string {
|
|
166
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 把文本里出现的工作区绝对路径改成相对路径(正反斜杠、MSYS 形式都认);单独出现的工作区本身变成 "." */
|
|
170
|
+
export function relativizeInText(text: string, cwd: string): string {
|
|
171
|
+
const root = stripTrailingSlash(canonPath(cwd));
|
|
172
|
+
if (!root) return text;
|
|
173
|
+
const drive = root.match(/^([A-Za-z]):\/(.*)$/);
|
|
174
|
+
const rest = drive ? drive[2] : root.replace(/^\//, "");
|
|
175
|
+
// 路径分隔:正斜杠或(可能成对的)反斜杠
|
|
176
|
+
const sep = "(?:\\/|\\\\+)";
|
|
177
|
+
const restRe = escapeRe(rest).replace(/\//g, sep);
|
|
178
|
+
const prefixRe = drive ? `(?:${drive[1]}:${sep}|/${drive[1]}/)` : "/";
|
|
179
|
+
// 之后必须是分隔符、引号、空白或结尾,避免误伤 PiWebX 这类同前缀目录
|
|
180
|
+
const re = new RegExp(`${prefixRe}${restRe}(?=\\/|\\\\|["'\\s]|$)(${sep})?`, "gi");
|
|
181
|
+
return text.replace(re, (_m, tail?: string) => (tail ? "" : "."));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 思考摘要:取第一句(句末标点后接空白或结尾);过短的首句退回整段截断 */
|
|
185
|
+
export function firstSentence(text: string, max = 140): string {
|
|
186
|
+
const one = text.replace(/\s+/g, " ").trim();
|
|
187
|
+
if (!one) return "";
|
|
188
|
+
// 英文句号要求后接空白(避免 3.5 这种小数),中文句号不需要
|
|
189
|
+
const m = one.match(/^.*?(?:[.!?](?=\s|$)|[。!?])/);
|
|
190
|
+
let s = m ? m[0].trim() : one;
|
|
191
|
+
// 首句过短("OK.")时退回整段;中文按字算,阈值更低
|
|
192
|
+
const min = /[㐀-鿿]/.test(s) ? 4 : 12;
|
|
193
|
+
if (s.length < min && one.length > s.length) s = one;
|
|
194
|
+
return s.length > max ? `${s.slice(0, max - 1).trimEnd()}…` : s;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export type ModelErrorKind = "rateLimit" | "billing" | "auth" | "notFound" | "context" | "timeout" | "server" | "network";
|
|
198
|
+
|
|
199
|
+
/** 把供应商原始报错归到少数几类,界面给一句人话;认不出返回 null(只显示原文) */
|
|
200
|
+
export function classifyModelError(message: string): ModelErrorKind | null {
|
|
201
|
+
const m = message.toLowerCase();
|
|
202
|
+
if (/\b429\b|rate.?limit|too many requests|requests per (minute|second)/.test(m)) return "rateLimit";
|
|
203
|
+
if (/\b402\b|insufficient (balance|credits|quota|funds)|billing|quota exceeded|余额/.test(m)) return "billing";
|
|
204
|
+
if (/\b401\b|\b403\b|unauthori[sz]ed|forbidden|invalid.*(api.?key|token)|authentication|permission denied/.test(m)) return "auth";
|
|
205
|
+
if (/\b404\b|model.*not (found|exist)|unknown model|no such model|not_found_error/.test(m)) return "notFound";
|
|
206
|
+
if (/context.*(length|window|limit)|too long|maximum.*tokens|prompt is too long|input length|exceeds the (limit|maximum)/.test(m)) return "context";
|
|
207
|
+
if (/timeout|timed out|etimedout|\b408\b|\b504\b/.test(m)) return "timeout";
|
|
208
|
+
if (/\b5\d\d\b|overloaded|server error|bad gateway|unavailable|internal error/.test(m)) return "server";
|
|
209
|
+
if (/econnrefused|econnreset|enotfound|eai_again|fetch failed|network|socket hang up/.test(m)) return "network";
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** 非空行(去 \r) */
|
|
214
|
+
export function splitLines(raw: string): string[] {
|
|
215
|
+
return raw.replace(/\r/g, "").split("\n").filter((l) => l.trim().length > 0);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const EXT_LANG: Record<string, string> = {
|
|
219
|
+
ts: "TypeScript", tsx: "TSX", js: "JavaScript", jsx: "JSX", mjs: "JavaScript", cjs: "JavaScript",
|
|
220
|
+
py: "Python", rs: "Rust", go: "Go", java: "Java", kt: "Kotlin", rb: "Ruby", php: "PHP", cs: "C#",
|
|
221
|
+
c: "C", h: "C", cpp: "C++", hpp: "C++", swift: "Swift", vue: "Vue", svelte: "Svelte",
|
|
222
|
+
css: "CSS", scss: "SCSS", html: "HTML", json: "JSON", md: "Markdown", mdx: "MDX",
|
|
223
|
+
yml: "YAML", yaml: "YAML", toml: "TOML", sh: "Shell", bash: "Shell", ps1: "PowerShell", sql: "SQL",
|
|
224
|
+
xml: "XML", txt: "Text", lock: "Lock", env: "Env",
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/** 由扩展名给一个语言名(read 行的元信息);认不出返回空串 */
|
|
228
|
+
export function langOfPath(p: string): string {
|
|
229
|
+
const name = basename(p).toLowerCase();
|
|
230
|
+
if (name === "dockerfile") return "Dockerfile";
|
|
231
|
+
if (name === "makefile") return "Makefile";
|
|
232
|
+
const ext = name.includes(".") ? name.slice(name.lastIndexOf(".") + 1) : "";
|
|
233
|
+
return EXT_LANG[ext] ?? "";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const HEAD_VERBS = new Set([
|
|
237
|
+
"cat", "ls", "dir", "tree", "find", "fd", "head", "tail", "type", "grep", "rg", "egrep", "fgrep", "ag", "sed", "awk",
|
|
238
|
+
"echo", "printf", "less", "more", "which", "where", "env", "printenv", "set", "stat", "file", "ps", "curl", "wget", "jq",
|
|
239
|
+
]);
|
|
240
|
+
const GIT_HEAD_SUBS = new Set(["log", "status", "diff", "show", "branch", "ls-files", "blame", "remote", "stash", "tag", "config", "grep"]);
|
|
241
|
+
|
|
242
|
+
/** 命令输出预览取开头还是结尾:列表 / 查看类命令看开头,构建 / 测试 / 脚本类看结尾(结论在最后);开头的 cd 一律跳过 */
|
|
243
|
+
export function previewSide(command: string): "head" | "tail" {
|
|
244
|
+
let first = command.replace(/\r/g, "").split("\n").find((l) => l.trim().length > 0) ?? "";
|
|
245
|
+
for (;;) {
|
|
246
|
+
const m = first.match(/^\s*(?:cd|pushd)\s+(?:\/d\s+)?(?:"[^"]*"|'[^']*'|\S+)\s*(?:&&|;)\s*/);
|
|
247
|
+
if (!m) break;
|
|
248
|
+
first = first.slice(m[0].length);
|
|
249
|
+
}
|
|
250
|
+
const tokens = first.trim().replace(/^(?:\w+=\S*\s+)+/, "").split(/\s+/);
|
|
251
|
+
const verb = basename(tokens[0] ?? "").replace(/\.exe$/i, "").toLowerCase();
|
|
252
|
+
if (verb === "git") return GIT_HEAD_SUBS.has((tokens[1] ?? "").toLowerCase()) ? "head" : "tail";
|
|
253
|
+
return HEAD_VERBS.has(verb) ? "head" : "tail";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** 去掉结尾只含括号 / 符号的行("}"、");"、"$"),这些行做预览等于没看 */
|
|
257
|
+
export function trimNoiseTail(lines: string[]): string[] {
|
|
258
|
+
let end = lines.length;
|
|
259
|
+
while (end > 0 && !/[\p{L}\p{N}]/u.test(lines[end - 1])) end -= 1;
|
|
260
|
+
return end === lines.length ? lines : lines.slice(0, end);
|
|
261
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const ICON_RULES: Array<[RegExp, string]> = [
|
|
2
|
+
[/^amazon-bedrock$/, "bedrock-color"],
|
|
3
|
+
[/^ant-ling$/, "antgroup-color"],
|
|
4
|
+
[/^anthropic$/, "anthropic"],
|
|
5
|
+
[/^azure-/, "azure-color"],
|
|
6
|
+
[/^baseten$/, "baseten"],
|
|
7
|
+
[/^cerebras$/, "cerebras-color"],
|
|
8
|
+
[/^cloudflare-/, "cloudflare-color"],
|
|
9
|
+
[/^deepseek$/, "deepseek-color"],
|
|
10
|
+
[/^fireworks$/, "fireworks-color"],
|
|
11
|
+
[/^github-copilot$/, "githubcopilot"],
|
|
12
|
+
[/^google-vertex(?:-|$)/, "googlecloud-color"],
|
|
13
|
+
[/^google$/, "google-color"],
|
|
14
|
+
[/^groq$/, "groq"],
|
|
15
|
+
[/^huggingface$/, "huggingface-color"],
|
|
16
|
+
[/^kimi(?:-|$)/, "kimi-color"],
|
|
17
|
+
[/^minimax/, "minimax-color"],
|
|
18
|
+
[/^mistral$/, "mistral-color"],
|
|
19
|
+
[/^moonshot(?:ai)?(?:-|$)/, "moonshot"],
|
|
20
|
+
[/^nvidia$/, "nvidia-color"],
|
|
21
|
+
[/^openai/, "openai"],
|
|
22
|
+
[/^opencode(?:-|$)/, "opencode"],
|
|
23
|
+
[/^openrouter$/, "openrouter-color"],
|
|
24
|
+
[/^qwen(?:-|$)/, "qwen-color"],
|
|
25
|
+
[/^together$/, "together-color"],
|
|
26
|
+
[/^vercel(?:-|$)/, "vercel"],
|
|
27
|
+
[/^xai$/, "xai"],
|
|
28
|
+
[/^xiaomi(?:-|$)/, "xiaomimimo"],
|
|
29
|
+
[/^zai(?:-|$)/, "zai"],
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export const PROVIDER_ICON_SLUGS = [...new Set(ICON_RULES.map(([, slug]) => slug))];
|
|
33
|
+
|
|
34
|
+
export function providerIconSlug(providerId: string): string | null {
|
|
35
|
+
return ICON_RULES.find(([pattern]) => pattern.test(providerId))?.[1] ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function providerInitials(name: string, providerId: string): string {
|
|
39
|
+
const words = name.trim().split(/[\s_-]+/).filter(Boolean);
|
|
40
|
+
if (words.length >= 2) return (words[0][0] + words[1][0]).toUpperCase();
|
|
41
|
+
return (words[0] ?? providerId).slice(0, 2).toUpperCase();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const CUSTOM_PROVIDER_ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
45
|
+
|
|
46
|
+
export const FALLBACK_CUSTOM_APIS = [
|
|
47
|
+
"openai-completions",
|
|
48
|
+
"openai-responses",
|
|
49
|
+
"anthropic-messages",
|
|
50
|
+
"google-generative-ai",
|
|
51
|
+
] as const;
|
|
52
|
+
|
|
53
|
+
export function customApiOptions(providerApis: string[]): string[] {
|
|
54
|
+
const fallback = new Set<string>(FALLBACK_CUSTOM_APIS);
|
|
55
|
+
const discovered = [...new Set(providerApis.filter((api) => api && !fallback.has(api)))].sort();
|
|
56
|
+
return [...FALLBACK_CUSTOM_APIS, ...discovered];
|
|
57
|
+
}
|