@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,249 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { assertInstallation, selectProductionBuild } from "./build-output.mjs";
|
|
7
|
+
import { ensureInstallBuild, isProductionOnlyInstall } from "./install-build.mjs";
|
|
8
|
+
export { isMainModule } from "./entrypoint.mjs";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
|
|
12
|
+
/** @typedef {Record<string, string | undefined>} Environment */
|
|
13
|
+
/** @typedef {{port: number, hostname: string, noOpen: boolean, isDev: boolean, help: boolean}} ParsedOptions */
|
|
14
|
+
/** @typedef {import('node:events').EventEmitter & {kill(signal?: NodeJS.Signals | number): boolean}} LauncherChild */
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} LauncherOptions
|
|
17
|
+
* @property {string[]} [args]
|
|
18
|
+
* @property {Environment} [env]
|
|
19
|
+
* @property {string} [root]
|
|
20
|
+
* @property {{log?: (message: string) => void, probePort?: typeof probePort,
|
|
21
|
+
* isPiWebRunning?: typeof isPiWebRunning, openBrowser?: (url: string) => Promise<void>,
|
|
22
|
+
* spawn?: (command: string, args: string[], options: import('node:child_process').SpawnOptions) => LauncherChild}} [dependencies]
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export function isLoopback(hostname) {
|
|
26
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
27
|
+
if (host === "localhost") return true;
|
|
28
|
+
if (net.isIP(host) === 4) return host.startsWith("127.");
|
|
29
|
+
return net.isIP(host) === 6 && new URL(`http://[${host}]`).hostname === "[::1]";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeHostname(value) {
|
|
33
|
+
if (typeof value !== "string" || !value) throw new Error("无效监听地址");
|
|
34
|
+
const host = value.toLowerCase().replace(/^\[([^\]]+)\]$/, "$1");
|
|
35
|
+
if (net.isIP(host)) return host;
|
|
36
|
+
if (host.length > 253 || !host.split(".").every((part) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(part))) {
|
|
37
|
+
throw new Error(`无效监听地址:${value}`);
|
|
38
|
+
}
|
|
39
|
+
return host;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @param {string[]} [args] @param {Environment} [env] @returns {ParsedOptions} */
|
|
43
|
+
export function parseLauncherArgs(args = [], env = process.env) {
|
|
44
|
+
let port = Number(env.PORT || 30141);
|
|
45
|
+
let hostname = env.PI_WEB_HOSTNAME || "127.0.0.1";
|
|
46
|
+
let noOpen = env.PI_WEB_NO_OPEN === "1";
|
|
47
|
+
let isDev = env.PI_WEB_DEV === "1";
|
|
48
|
+
let help = false;
|
|
49
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
50
|
+
const arg = args[i];
|
|
51
|
+
if (arg === "-p" || arg === "--port") port = Number(args[++i]);
|
|
52
|
+
else if (arg.startsWith("--port=")) port = Number(arg.slice(7));
|
|
53
|
+
else if (arg === "-H" || arg === "--hostname") hostname = args[++i];
|
|
54
|
+
else if (arg.startsWith("--hostname=")) hostname = arg.slice(11);
|
|
55
|
+
else if (arg === "--no-open") noOpen = true;
|
|
56
|
+
else if (arg === "--dev") isDev = true;
|
|
57
|
+
else if (arg === "-h" || arg === "--help") help = true;
|
|
58
|
+
else throw new Error(`未知参数:${arg}`);
|
|
59
|
+
}
|
|
60
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`无效端口:${port}`);
|
|
61
|
+
return { port, hostname: normalizeHostname(hostname), noOpen, isDev, help };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} root
|
|
66
|
+
* @param {ParsedOptions} options
|
|
67
|
+
* @param {Environment} [env]
|
|
68
|
+
* @returns {{root: string, command: 'dev' | 'start', env: Environment}}
|
|
69
|
+
*/
|
|
70
|
+
export function createLaunchPlan(root, options, env = process.env) {
|
|
71
|
+
const loopback = isLoopback(options.hostname);
|
|
72
|
+
if (!loopback && options.isDev) {
|
|
73
|
+
throw new Error("拒绝非本机开发模式:PI_WEB_PASSWORD 不能保护 Next 开发工具端点。请绑定 127.0.0.1,或先运行 npm run build:release 后用生产模式启动。");
|
|
74
|
+
}
|
|
75
|
+
if (!loopback && !env.PI_WEB_PASSWORD) throw new Error("拒绝无密码的非本机监听。请设置 PI_WEB_PASSWORD,或绑定 127.0.0.1。");
|
|
76
|
+
const installation = assertInstallation(root);
|
|
77
|
+
const build = options.isDev ? null : selectProductionBuild(installation, env.PIWEB_BUILD_DIR);
|
|
78
|
+
if (!build && !loopback) {
|
|
79
|
+
throw new Error("找不到可用生产构建,拒绝对外自动回退到开发模式。请先运行 npm run build:release,再以相同启动参数重试。");
|
|
80
|
+
}
|
|
81
|
+
const command = options.isDev || !build ? "dev" : "start";
|
|
82
|
+
const childEnv = {
|
|
83
|
+
...env,
|
|
84
|
+
PI_WEB_ROOT: installation,
|
|
85
|
+
PYTHONUTF8: env.PYTHONUTF8 ?? "1",
|
|
86
|
+
PYTHONIOENCODING: env.PYTHONIOENCODING ?? "utf-8",
|
|
87
|
+
};
|
|
88
|
+
if (command === "start") childEnv.PIWEB_BUILD_DIR = build.buildDir;
|
|
89
|
+
else {
|
|
90
|
+
delete childEnv.PIWEB_BUILD_DIR;
|
|
91
|
+
childEnv.PIWEB_DEV_ASSET_ID = `${Date.now().toString(36)}-${process.pid.toString(36)}`;
|
|
92
|
+
childEnv.NODE_OPTIONS = [env.NODE_OPTIONS, "--max-old-space-size=3072"].filter(Boolean).join(" ");
|
|
93
|
+
}
|
|
94
|
+
return { root: installation, command, env: childEnv };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function localUrl(hostname, port) {
|
|
98
|
+
const host = hostname === "0.0.0.0" ? "127.0.0.1" : hostname === "::" ? "::1" : hostname;
|
|
99
|
+
return `http://${net.isIP(host) === 6 ? `[${host}]` : host}:${port}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Never send credentials to a port occupant, even if it forges PiWeb JSON/401.
|
|
104
|
+
* @param {string} base
|
|
105
|
+
* @param {{fetchImpl?: typeof fetch, timeoutMs?: number, signal?: AbortSignal}} [options]
|
|
106
|
+
* @returns {Promise<boolean>}
|
|
107
|
+
*/
|
|
108
|
+
export async function isPiWebRunning(base, { fetchImpl = fetch, timeoutMs = 1500, signal } = {}) {
|
|
109
|
+
try {
|
|
110
|
+
const url = new URL("/api/health", base);
|
|
111
|
+
if (url.username || url.password || url.protocol !== "http:") return false;
|
|
112
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
113
|
+
const response = await fetchImpl(url, {
|
|
114
|
+
credentials: "omit",
|
|
115
|
+
redirect: "error",
|
|
116
|
+
headers: { accept: "application/json" },
|
|
117
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
118
|
+
});
|
|
119
|
+
if (response.status !== 200 || !response.body) return false;
|
|
120
|
+
const reader = response.body.getReader();
|
|
121
|
+
let text = "";
|
|
122
|
+
let bytes = 0;
|
|
123
|
+
const decoder = new TextDecoder();
|
|
124
|
+
try {
|
|
125
|
+
for (;;) {
|
|
126
|
+
const chunk = await reader.read();
|
|
127
|
+
if (chunk.done) break;
|
|
128
|
+
bytes += chunk.value.byteLength;
|
|
129
|
+
if (bytes > 4096) return false;
|
|
130
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
131
|
+
}
|
|
132
|
+
text += decoder.decode();
|
|
133
|
+
} finally {
|
|
134
|
+
await reader.cancel().catch(() => {});
|
|
135
|
+
reader.releaseLock();
|
|
136
|
+
}
|
|
137
|
+
const body = JSON.parse(text);
|
|
138
|
+
return body?.success === true && body?.data?.ok === true && body?.data?.service === "piweb";
|
|
139
|
+
} catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** @param {number} port @param {string} hostname @returns {Promise<'busy' | 'free' | NodeJS.ErrnoException>} */
|
|
145
|
+
export function probePort(port, hostname) {
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
const server = net.createServer();
|
|
148
|
+
server.once("error", (error) => resolve(error.code === "EADDRINUSE" ? "busy" : error));
|
|
149
|
+
server.once("listening", () => server.close(() => resolve("free")));
|
|
150
|
+
server.listen({ port, host: hostname, exclusive: true });
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function openBrowser(url) {
|
|
155
|
+
const command = process.platform === "win32" ? "cmd.exe" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
156
|
+
const args = process.platform === "win32" ? ["/d", "/c", "start", "", url] : [url];
|
|
157
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
158
|
+
child.once("error", () => {});
|
|
159
|
+
child.unref();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Import-safe entrypoint; injectable I/O keeps launcher tests off real services.
|
|
164
|
+
* @param {LauncherOptions} [options]
|
|
165
|
+
* @returns {Promise<number>}
|
|
166
|
+
*/
|
|
167
|
+
export async function runLauncher({ args = process.argv.slice(2), env = process.env, root = DEFAULT_ROOT, dependencies = {} } = {}) {
|
|
168
|
+
const options = parseLauncherArgs(args, env);
|
|
169
|
+
const log = dependencies.log ?? console.log;
|
|
170
|
+
if (options.help) {
|
|
171
|
+
log("Usage: pi web [-p, --port <port>] [-H, --hostname <host>] [--dev] [--no-open]");
|
|
172
|
+
log("默认绑定 127.0.0.1:30141;端口被占自动 +1。开发模式仅允许本机监听。");
|
|
173
|
+
log("对外监听必须使用生产构建与 PI_WEB_PASSWORD。构建/恢复发布:npm run build:release。");
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
|
176
|
+
// Validate before probing/reusing: an existing server must not hide an unsafe request.
|
|
177
|
+
// Production-only installs (`npm i -g piweb`) have no dev dependencies, so the
|
|
178
|
+
// development fallback is unusable there: prepare the one-time build first.
|
|
179
|
+
if (!options.isDev) {
|
|
180
|
+
const prepared = ensureInstallBuild(root, { log, warn: dependencies.warn ?? console.warn });
|
|
181
|
+
if (prepared === "failed" && isProductionOnlyInstall(root)) {
|
|
182
|
+
throw new Error("无法准备生产构建,且该安装没有开发依赖。请用 `npm rebuild -g piweb` 重试,或改用仓库中的 npm run dev。");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const plan = createLaunchPlan(root, options, env);
|
|
186
|
+
const checkPort = dependencies.probePort ?? probePort;
|
|
187
|
+
const checkHealth = dependencies.isPiWebRunning ?? isPiWebRunning;
|
|
188
|
+
const browse = dependencies.openBrowser ?? openBrowser;
|
|
189
|
+
const start = dependencies.spawn ?? spawn;
|
|
190
|
+
let port = options.port;
|
|
191
|
+
for (let attempts = 0; ; attempts += 1) {
|
|
192
|
+
const state = await checkPort(port, options.hostname);
|
|
193
|
+
if (state === "free") break;
|
|
194
|
+
if (state !== "busy") throw new Error(`无法监听 ${options.hostname}:${port}:${state.code ?? state.message ?? state}`);
|
|
195
|
+
const url = localUrl(options.hostname, port);
|
|
196
|
+
if (await checkHealth(url)) {
|
|
197
|
+
log(`[piweb] 检测到已有 PiWeb:${url}(未发送凭据)`);
|
|
198
|
+
if (!options.noOpen) await browse(url);
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
if (attempts >= 50 || port >= 65535) throw new Error(`连续 ${attempts + 1} 个端口都被占用,请用 -p 指定端口`);
|
|
202
|
+
log(`[piweb] 端口 ${port} 被占用,尝试 ${port + 1}`);
|
|
203
|
+
port += 1;
|
|
204
|
+
}
|
|
205
|
+
const url = localUrl(options.hostname, port);
|
|
206
|
+
log(`[piweb] 启动 ${url} (${plan.command} 模式)`);
|
|
207
|
+
if (env.PI_WEB_PASSWORD) log(plan.command === "dev" ? "[piweb] 开发模式仅限本机;密码不保护 Next 开发工具端点。" : "[piweb] 已启用密码认证(浏览器登录页,API 兼容 Basic 用户名 pi)");
|
|
208
|
+
if (!isLoopback(options.hostname)) {
|
|
209
|
+
for (const addresses of Object.values(os.networkInterfaces())) {
|
|
210
|
+
for (const address of addresses ?? []) {
|
|
211
|
+
if (address.family === "IPv4" && !address.internal && !address.address.startsWith("169.254.")) {
|
|
212
|
+
log(`[piweb] 局域网/手机访问地址(需同一网络;已启用密码认证):http://${address.address}:${port}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const next = path.join(plan.root, "node_modules", "next", "dist", "bin", "next");
|
|
218
|
+
const child = start(process.execPath, [next, plan.command, ...(plan.command === "dev" ? ["--webpack"] : []), "-p", String(port), "-H", options.hostname], {
|
|
219
|
+
cwd: plan.root, stdio: "inherit", env: plan.env,
|
|
220
|
+
});
|
|
221
|
+
const controller = new AbortController();
|
|
222
|
+
let timer;
|
|
223
|
+
const ready = async (attempt = 0) => {
|
|
224
|
+
if (controller.signal.aborted || attempt > 120) return;
|
|
225
|
+
if (await checkHealth(url, { signal: controller.signal })) {
|
|
226
|
+
if (controller.signal.aborted) return;
|
|
227
|
+
log(`[piweb] 就绪:${url}`);
|
|
228
|
+
if (!options.noOpen) await browse(url);
|
|
229
|
+
} else if (!controller.signal.aborted) {
|
|
230
|
+
timer = setTimeout(() => void ready(attempt + 1), 500);
|
|
231
|
+
timer.unref();
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
const interrupt = () => { child.kill("SIGINT"); };
|
|
236
|
+
const terminate = () => { child.kill("SIGTERM"); };
|
|
237
|
+
const cleanup = () => {
|
|
238
|
+
controller.abort();
|
|
239
|
+
clearTimeout(timer);
|
|
240
|
+
process.off("SIGINT", interrupt);
|
|
241
|
+
process.off("SIGTERM", terminate);
|
|
242
|
+
};
|
|
243
|
+
process.once("SIGINT", interrupt);
|
|
244
|
+
process.once("SIGTERM", terminate);
|
|
245
|
+
child.once("error", (error) => { cleanup(); reject(error); });
|
|
246
|
+
child.once("exit", (code) => { cleanup(); resolve(code ?? 1); });
|
|
247
|
+
void ready().catch(() => {});
|
|
248
|
+
});
|
|
249
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Pop the Windows modern folder picker (IFileOpenDialog, Explorer style, centered).
|
|
2
|
+
# Usage: powershell -NoProfile -STA -ExecutionPolicy Bypass -File pick-folder.ps1 [title]
|
|
3
|
+
# Output: the selected path on one line; empty when canceled.
|
|
4
|
+
#
|
|
5
|
+
# Encoding contract: this file must stay pure ASCII. Windows PowerShell 5.1 reads
|
|
6
|
+
# BOM-less .ps1 files with the active ANSI code page (GBK on zh-CN), so any literal
|
|
7
|
+
# CJK character here would reach the dialog mojibake'd. Non-ASCII UI strings are
|
|
8
|
+
# embedded as C# \uXXXX escapes instead.
|
|
9
|
+
param([string]$Title = 'Select workspace folder')
|
|
10
|
+
|
|
11
|
+
# PowerShell 5.1 otherwise emits text using the active Windows code page.
|
|
12
|
+
try {
|
|
13
|
+
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
14
|
+
$OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
15
|
+
} catch {}
|
|
16
|
+
|
|
17
|
+
$src = @'
|
|
18
|
+
using System;
|
|
19
|
+
using System.Runtime.InteropServices;
|
|
20
|
+
|
|
21
|
+
public class PiFolderPicker {
|
|
22
|
+
[ComImport, Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")]
|
|
23
|
+
private class FileOpenDialogRCW {}
|
|
24
|
+
|
|
25
|
+
[ComImport, Guid("D57C7288-D4AD-4768-BE02-9D969532D960"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
26
|
+
private interface IFileDialog {
|
|
27
|
+
[PreserveSig] int Show(IntPtr hwndOwner);
|
|
28
|
+
[PreserveSig] int SetFileTypes(uint cFileTypes, IntPtr rgFilterSpec);
|
|
29
|
+
[PreserveSig] int SetFileTypeIndex(uint iFileType);
|
|
30
|
+
[PreserveSig] int GetFileTypeIndex(out uint piFileType);
|
|
31
|
+
[PreserveSig] int Advise(IntPtr pfde, out uint pdwCookie);
|
|
32
|
+
[PreserveSig] int Unadvise(uint pdwCookie);
|
|
33
|
+
[PreserveSig] int SetOptions(uint fos);
|
|
34
|
+
[PreserveSig] int GetOptions(out uint pfos);
|
|
35
|
+
[PreserveSig] int SetDefaultFolder(IShellItem psi);
|
|
36
|
+
[PreserveSig] int SetFolder(IShellItem psi);
|
|
37
|
+
[PreserveSig] int GetFolder(out IShellItem ppsi);
|
|
38
|
+
[PreserveSig] int GetCurrentSelection(out IShellItem ppsi);
|
|
39
|
+
[PreserveSig] int SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName);
|
|
40
|
+
[PreserveSig] int GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
|
|
41
|
+
[PreserveSig] int SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
|
|
42
|
+
[PreserveSig] int SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText);
|
|
43
|
+
[PreserveSig] int SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
|
|
44
|
+
[PreserveSig] int GetResult(out IShellItem ppsi);
|
|
45
|
+
[PreserveSig] int AddPlace(IShellItem psi, int fdap);
|
|
46
|
+
[PreserveSig] int SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
|
|
47
|
+
[PreserveSig] int Close(int hr);
|
|
48
|
+
[PreserveSig] int SetClientGuid(ref Guid guid);
|
|
49
|
+
[PreserveSig] int ClearClientData();
|
|
50
|
+
[PreserveSig] int SetFilter(IntPtr pFilter);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
[ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
54
|
+
private interface IShellItem {
|
|
55
|
+
[PreserveSig] int BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv);
|
|
56
|
+
[PreserveSig] int GetParent(out IShellItem ppsi);
|
|
57
|
+
[PreserveSig] int GetDisplayName(uint sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);
|
|
58
|
+
[PreserveSig] int GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
|
|
59
|
+
[PreserveSig] int Compare(IShellItem psi, uint hint, out int piOrder);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private const uint FOS_PICKFOLDERS = 0x20;
|
|
63
|
+
private const uint FOS_FORCEFILESYSTEM = 0x40;
|
|
64
|
+
private const uint SIGDN_FILESYSPATH = 0x80058000;
|
|
65
|
+
|
|
66
|
+
// Ok-button label ("select this folder", zh) as escapes: keep this .ps1 ASCII.
|
|
67
|
+
private const string OkLabel = "\u9009\u62E9\u6B64\u6587\u4EF6\u5939";
|
|
68
|
+
|
|
69
|
+
[DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow();
|
|
70
|
+
[DllImport("user32.dll")] private static extern bool SetProcessDpiAwarenessContext(IntPtr value);
|
|
71
|
+
[DllImport("user32.dll")] private static extern bool SetProcessDPIAware();
|
|
72
|
+
|
|
73
|
+
// A non-DPI-aware host process gets the dialog bitmap-stretched on scaled
|
|
74
|
+
// displays (blurry). Per-Monitor V2 (-4) keeps it crisp; fall back to the
|
|
75
|
+
// legacy declaration on pre-1703 Windows. Best effort: both may no-op when
|
|
76
|
+
// awareness is already set by a manifest.
|
|
77
|
+
private static void EnableDpiAwareness() {
|
|
78
|
+
try {
|
|
79
|
+
if (!SetProcessDpiAwarenessContext(new IntPtr(-4))) SetProcessDPIAware();
|
|
80
|
+
} catch {
|
|
81
|
+
try { SetProcessDPIAware(); } catch {}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public static string Pick(string title) {
|
|
86
|
+
try {
|
|
87
|
+
EnableDpiAwareness();
|
|
88
|
+
var dlg = (IFileDialog)(new FileOpenDialogRCW());
|
|
89
|
+
uint options;
|
|
90
|
+
dlg.GetOptions(out options);
|
|
91
|
+
dlg.SetOptions(options | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM);
|
|
92
|
+
if (!string.IsNullOrEmpty(title)) dlg.SetTitle(title);
|
|
93
|
+
dlg.SetOkButtonLabel(OkLabel);
|
|
94
|
+
int hr = dlg.Show(GetForegroundWindow());
|
|
95
|
+
if (hr != 0) return "";
|
|
96
|
+
IShellItem item;
|
|
97
|
+
dlg.GetResult(out item);
|
|
98
|
+
string path;
|
|
99
|
+
item.GetDisplayName(SIGDN_FILESYSPATH, out path);
|
|
100
|
+
return path ?? "";
|
|
101
|
+
} catch {
|
|
102
|
+
return "";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
'@
|
|
107
|
+
|
|
108
|
+
Add-Type -TypeDefinition $src -Language CSharp
|
|
109
|
+
$result = [PiFolderPicker]::Pick($Title)
|
|
110
|
+
Write-Output $result
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* npm postinstall hook: prepare the production build for production-only installs.
|
|
4
|
+
* The same helper also runs from the first `piweb` start, so installs that skip
|
|
5
|
+
* lifecycle scripts heal themselves on first use.
|
|
6
|
+
*/
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { ensureInstallBuild } from "./install-build.mjs";
|
|
10
|
+
|
|
11
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
|
+
ensureInstallBuild(root);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
/** Kill only the command tree that this helper started, never the serving app. */
|
|
4
|
+
async function terminateProcessTree(child) {
|
|
5
|
+
if (!child.pid) { child.kill(); return; }
|
|
6
|
+
if (process.platform !== "win32") {
|
|
7
|
+
try { process.kill(-child.pid, "SIGKILL"); } catch { child.kill("SIGKILL"); }
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
await new Promise((resolve) => {
|
|
11
|
+
const killer = spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
12
|
+
const timer = setTimeout(() => { child.kill(); killer.kill(); resolve(); }, 10_000);
|
|
13
|
+
const done = () => { clearTimeout(timer); resolve(); };
|
|
14
|
+
killer.once("error", () => { child.kill(); done(); });
|
|
15
|
+
killer.once("close", done);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Fixed argument arrays, bounded output and whole-tree timeouts for update/build commands.
|
|
21
|
+
* @param {string} command
|
|
22
|
+
* @param {string[]} args
|
|
23
|
+
* @param {{cwd: string, env?: Record<string, string | undefined>, timeoutMs?: number, signal?: AbortSignal}} options
|
|
24
|
+
* @returns {Promise<string>}
|
|
25
|
+
*/
|
|
26
|
+
export function runCommand(command, args, { cwd, env = process.env, timeoutMs = 600_000, signal }) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
if (signal?.aborted) { reject(new Error("release cancelled")); return; }
|
|
29
|
+
// Only npm needs cmd.exe to resolve npm.cmd. Never enable a general shell.
|
|
30
|
+
const npmShell = process.platform === "win32" && command === "npm";
|
|
31
|
+
const child = spawn(npmShell ? env.ComSpec || "cmd.exe" : command, npmShell ? ["/d", "/c", "npm", ...args] : args, {
|
|
32
|
+
cwd, env, windowsHide: true, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"],
|
|
33
|
+
});
|
|
34
|
+
let output = "";
|
|
35
|
+
let errorOutput = "";
|
|
36
|
+
let stopping = false;
|
|
37
|
+
let settled = false;
|
|
38
|
+
const cleanup = () => { clearTimeout(timer); signal?.removeEventListener("abort", abort); };
|
|
39
|
+
const fail = (error) => { if (!settled) { settled = true; cleanup(); reject(error); } };
|
|
40
|
+
const stop = (message) => {
|
|
41
|
+
if (settled || stopping) return;
|
|
42
|
+
stopping = true;
|
|
43
|
+
void terminateProcessTree(child).then(() => fail(new Error(message)), fail);
|
|
44
|
+
};
|
|
45
|
+
const abort = () => stop("release cancelled; command process tree terminated");
|
|
46
|
+
const timer = setTimeout(() => stop(`timeout after ${Math.round(timeoutMs / 1000)}s: ${command} ${args.join(" ")}`), timeoutMs);
|
|
47
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
48
|
+
if (signal?.aborted) abort();
|
|
49
|
+
child.stdout.on("data", (chunk) => { output = (output + chunk).slice(-256 * 1024); });
|
|
50
|
+
child.stderr.on("data", (chunk) => { errorOutput = (errorOutput + chunk).slice(-256 * 1024); });
|
|
51
|
+
child.once("error", (error) => { if (!stopping) fail(error); });
|
|
52
|
+
child.once("close", (code) => {
|
|
53
|
+
if (stopping || settled) return;
|
|
54
|
+
if (code !== 0) { fail(new Error((errorOutput || output || `exit code ${code}`).trim().slice(-1000))); return; }
|
|
55
|
+
settled = true;
|
|
56
|
+
cleanup();
|
|
57
|
+
resolve(output);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
6
|
+
import { assertInstallation, assertLocalPath, readProductionBuild, resolveBuildOutput, RELEASES_DIR, RELEASE_MANIFEST } from "./build-output.mjs";
|
|
7
|
+
import { runCommand } from "./process-runner.mjs";
|
|
8
|
+
import { isMainModule } from "./entrypoint.mjs";
|
|
9
|
+
|
|
10
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
const LOCK_PATH = `${RELEASES_DIR}/update.lock`;
|
|
12
|
+
const CONFIG_PATH = `${RELEASES_DIR}/tsconfig.json`;
|
|
13
|
+
|
|
14
|
+
/** Rename within one directory: a launcher sees the complete old or new state. */
|
|
15
|
+
async function atomicJson(root, relative, value) {
|
|
16
|
+
const target = assertLocalPath(root, relative);
|
|
17
|
+
const temporary = assertLocalPath(root, `${relative}.${randomUUID()}.tmp`);
|
|
18
|
+
const handle = await fs.open(temporary, "wx", 0o600);
|
|
19
|
+
try {
|
|
20
|
+
try {
|
|
21
|
+
await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
22
|
+
await handle.sync();
|
|
23
|
+
} finally { await handle.close(); }
|
|
24
|
+
await fs.rename(temporary, target);
|
|
25
|
+
} finally { await fs.rm(temporary, { force: true }); }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function acquireLock(root) {
|
|
29
|
+
const lockPath = assertLocalPath(root, LOCK_PATH);
|
|
30
|
+
let lock;
|
|
31
|
+
try { lock = await fs.open(lockPath, "wx", 0o600); }
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code === "EEXIST") throw new Error(`an update/build is already running (${lockPath}); after an interrupted build, confirm its process tree has stopped before removing this lock and retrying npm run build:release`);
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
try { await lock.writeFile(JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() })); }
|
|
37
|
+
catch (error) { await lock.close(); await fs.unlink(lockPath); throw error; }
|
|
38
|
+
return async () => { await lock.close(); await fs.unlink(lockPath); };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function writeReleaseTypeConfig(root, buildDir) {
|
|
42
|
+
const id = buildDir.slice(`${RELEASES_DIR}/`.length);
|
|
43
|
+
await atomicJson(root, CONFIG_PATH, {
|
|
44
|
+
extends: "../tsconfig.json",
|
|
45
|
+
compilerOptions: { tsBuildInfoFile: `./${id}.tsbuildinfo` },
|
|
46
|
+
include: [
|
|
47
|
+
"../next-env.d.ts", "../*.ts", "../*.mts", "../src/**/*.ts", "../src/**/*.tsx",
|
|
48
|
+
"../tests/**/*.ts", "../tests/**/*.tsx", `./${id}/types/**/*.ts`, `./${id}/dev/types/**/*.ts`,
|
|
49
|
+
],
|
|
50
|
+
exclude: ["../node_modules", "../pi", "../dsh", "../bin"],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Run in a fresh process, after install/pull, so cached config/Next modules cannot lie. */
|
|
55
|
+
export async function verifyReleaseConfig(root = process.cwd(), env = process.env) {
|
|
56
|
+
const installation = assertInstallation(root);
|
|
57
|
+
const buildDir = resolveBuildOutput(installation, env.PIWEB_BUILD_DIR);
|
|
58
|
+
if (!buildDir.startsWith(`${RELEASES_DIR}/`)) throw new Error("release requires an isolated build output");
|
|
59
|
+
const require = createRequire(path.join(installation, "package.json"));
|
|
60
|
+
const module = await import(pathToFileURL(require.resolve("next/dist/server/config.js")).href);
|
|
61
|
+
const loadConfig = typeof module.default === "function" ? module.default : module.default.default;
|
|
62
|
+
const { PHASE_PRODUCTION_BUILD } = require("next/constants");
|
|
63
|
+
const config = await loadConfig(PHASE_PRODUCTION_BUILD, installation, { silent: true });
|
|
64
|
+
if (config.distDir !== buildDir || config.typescript?.tsconfigPath !== CONFIG_PATH || config.typescript?.ignoreBuildErrors) {
|
|
65
|
+
throw new Error("Next config does not honor the isolated release directory/typecheck; refusing to build over the serving output");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const PI_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
70
|
+
const PI_AI_PACKAGE = "@earendil-works/pi-ai";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Shared by the UI and update:pi; called only after the building state is durable.
|
|
74
|
+
* @param {{root: string, env: Record<string, string | undefined>, run: typeof runCommand, signal?: AbortSignal}} context
|
|
75
|
+
* @param {() => Promise<void>} [assertIdle]
|
|
76
|
+
* @returns {Promise<string>}
|
|
77
|
+
*/
|
|
78
|
+
export async function installPiUpdate({ root, env, run, signal }, assertIdle = async () => {}) {
|
|
79
|
+
await assertIdle();
|
|
80
|
+
await run("npm", ["install", "--save-exact", `${PI_PACKAGE}@latest`, `${PI_AI_PACKAGE}@latest`, "--no-audit", "--no-fund"], {
|
|
81
|
+
cwd: root, env, timeoutMs: 600_000, signal,
|
|
82
|
+
});
|
|
83
|
+
await run("npm", ["run", "sync:provider-icons"], { cwd: root, env, timeoutMs: 60_000, signal });
|
|
84
|
+
const manifest = JSON.parse(await fs.readFile(path.join(root, "node_modules", PI_PACKAGE, "package.json"), "utf8"));
|
|
85
|
+
if (typeof manifest.version !== "string" || !manifest.version) throw new Error("Pi engine package version is missing");
|
|
86
|
+
return manifest.version;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Prepare one production output without touching the serving .next/release.
|
|
91
|
+
* Source/dependency preparation is deliberately a caller callback: this command
|
|
92
|
+
* alone never fetches Git or installs packages, and works in a no-Git source copy.
|
|
93
|
+
* @param {{root?: string, env?: Record<string, string | undefined>, run?: typeof runCommand,
|
|
94
|
+
* beforePrepare?: () => Promise<void>, prepare?: (context: {root: string, buildDir: string, env: Record<string, string | undefined>, run: typeof runCommand, signal?: AbortSignal}) => Promise<void>,
|
|
95
|
+
* signal?: AbortSignal}} options
|
|
96
|
+
*/
|
|
97
|
+
export async function prepareRelease({ root = ROOT, env = process.env, run = runCommand, beforePrepare, prepare, signal } = {}) {
|
|
98
|
+
const installation = assertInstallation(root);
|
|
99
|
+
const releases = assertLocalPath(installation, RELEASES_DIR);
|
|
100
|
+
await fs.mkdir(releases, { recursive: true });
|
|
101
|
+
assertLocalPath(installation, RELEASES_DIR);
|
|
102
|
+
const releaseLock = await acquireLock(installation);
|
|
103
|
+
let started = false;
|
|
104
|
+
let state;
|
|
105
|
+
try {
|
|
106
|
+
await beforePrepare?.();
|
|
107
|
+
signal?.throwIfAborted();
|
|
108
|
+
const buildDir = resolveBuildOutput(installation, `${RELEASES_DIR}/${Date.now().toString(36)}-${randomUUID()}`);
|
|
109
|
+
// Never reuse a failed or previous directory, even if it has a BUILD_ID.
|
|
110
|
+
await fs.mkdir(assertLocalPath(installation, buildDir));
|
|
111
|
+
state = { schema: 1, state: "building", buildDir, startedAt: new Date().toISOString() };
|
|
112
|
+
await atomicJson(installation, RELEASE_MANIFEST, state);
|
|
113
|
+
started = true;
|
|
114
|
+
const childEnv = {
|
|
115
|
+
...env, PI_WEB_ROOT: installation, PIWEB_BUILD_DIR: buildDir,
|
|
116
|
+
PYTHONUTF8: env.PYTHONUTF8 ?? "1", PYTHONIOENCODING: env.PYTHONIOENCODING ?? "utf-8",
|
|
117
|
+
};
|
|
118
|
+
// Do not pass the live web password to Git/npm/package build scripts.
|
|
119
|
+
delete childEnv.PI_WEB_PASSWORD;
|
|
120
|
+
delete childEnv.PI_WEB_DEV;
|
|
121
|
+
delete childEnv.NODE_ENV;
|
|
122
|
+
const context = { root: installation, buildDir, env: childEnv, run, signal };
|
|
123
|
+
await prepare?.(context);
|
|
124
|
+
signal?.throwIfAborted();
|
|
125
|
+
assertInstallation(installation);
|
|
126
|
+
await writeReleaseTypeConfig(installation, buildDir);
|
|
127
|
+
await run(process.execPath, [path.join(installation, "scripts", "release.mjs"), "--verify-config"], {
|
|
128
|
+
cwd: installation, env: childEnv, timeoutMs: 60_000, signal,
|
|
129
|
+
});
|
|
130
|
+
await run("npm", ["run", "check"], { cwd: installation, env: childEnv, timeoutMs: 600_000, signal });
|
|
131
|
+
signal?.throwIfAborted();
|
|
132
|
+
const build = readProductionBuild(installation, buildDir);
|
|
133
|
+
const version = JSON.parse(await fs.readFile(path.join(installation, "package.json"), "utf8")).version;
|
|
134
|
+
if (typeof version !== "string" || !version) throw new Error("PiWeb package version is missing");
|
|
135
|
+
await atomicJson(installation, RELEASE_MANIFEST, { ...state, ...build, state: "ready", version, finishedAt: new Date().toISOString() });
|
|
136
|
+
return { ...build, version, needsRestart: true };
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (started) {
|
|
139
|
+
try { await atomicJson(installation, RELEASE_MANIFEST, { ...state, state: "failed", finishedAt: new Date().toISOString() }); }
|
|
140
|
+
catch { /* The previously persisted building state still blocks stale fallback. */ }
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
} finally { await releaseLock(); }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (isMainModule(import.meta.url)) {
|
|
147
|
+
const controller = new AbortController();
|
|
148
|
+
const cancel = () => controller.abort();
|
|
149
|
+
process.once("SIGINT", cancel);
|
|
150
|
+
process.once("SIGTERM", cancel);
|
|
151
|
+
try {
|
|
152
|
+
if (process.argv.length === 3 && process.argv[2] === "--verify-config") {
|
|
153
|
+
await verifyReleaseConfig();
|
|
154
|
+
} else {
|
|
155
|
+
const updatePi = process.argv.length === 3 && process.argv[2] === "--update-pi";
|
|
156
|
+
if (process.argv.length > 2 && !updatePi) throw new Error("Usage: node scripts/release.mjs [--update-pi]");
|
|
157
|
+
console.log("[piweb] Checking and building an isolated release; the existing server is not restarted.");
|
|
158
|
+
if (updatePi) console.log("[piweb] Pi dependency update: use a maintenance window with no active sessions; node_modules is shared.");
|
|
159
|
+
const release = await prepareRelease({
|
|
160
|
+
signal: controller.signal,
|
|
161
|
+
prepare: updatePi ? async (context) => { await installPiUpdate(context); } : undefined,
|
|
162
|
+
});
|
|
163
|
+
console.log(`[piweb] Release ready: ${release.buildDir} (${release.buildId}). Restart PiWeb manually when idle to use it.`);
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error(`[piweb] Release failed: ${error instanceof Error ? error.message : error}`);
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
} finally {
|
|
169
|
+
process.off("SIGINT", cancel);
|
|
170
|
+
process.off("SIGTERM", cancel);
|
|
171
|
+
}
|
|
172
|
+
}
|