diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 64babb4d..92dec62a 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -5,7 +5,7 @@ HOOK_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)" cd "$REPO_ROOT" -node scripts/split-knowledge-large-files.js +bun scripts/split-knowledge-large-files.js while IFS= read -r -d '' knowledge_dir; do git add -A -- "$knowledge_dir" done < <( diff --git a/.githooks/pre-push b/.githooks/pre-push index f1dbb702..93f23e5d 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -9,7 +9,7 @@ if [ "${DOCKER_GIT_SKIP_KNOWLEDGE_GUARD:-}" = "1" ]; then exit 0 fi -node scripts/pre-push-knowledge-guard.js "$@" +bun scripts/pre-push-knowledge-guard.js "$@" # CHANGE: backup AI session to a private session repository on push (supports Claude, Codex, Gemini) # WHY: allows returning to old AI sessions and provides PR context without gist limits @@ -18,6 +18,6 @@ node scripts/pre-push-knowledge-guard.js "$@" # PURITY: SHELL if [ "${DOCKER_GIT_SKIP_SESSION_BACKUP:-}" != "1" ]; then if command -v gh >/dev/null 2>&1; then - node scripts/session-backup-gist.js --verbose || echo "[session-backup] Warning: session backup failed (non-fatal)" + bun scripts/session-backup-gist.js --verbose || echo "[session-backup] Warning: session backup failed (non-fatal)" fi fi diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 34acd633..3dcf1303 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -1,21 +1,34 @@ name: Setup -description: Perform standard setup and install dependencies using pnpm. +description: Perform standard setup and install dependencies using Bun. inputs: + bun-version: + description: The version of Bun to install + required: true + default: 1.3.11 node-version: - description: The version of Node.js to install + description: The version of Node.js to install for compatibility/native builds required: true default: 24 runs: using: composite steps: - - name: Install pnpm - uses: pnpm/action-setup@v5 - - name: Install node + - name: Install Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: ${{ inputs.bun-version }} + - name: Install Node uses: actions/setup-node@v6 with: - cache: pnpm node-version: ${{ inputs.node-version }} + - name: Install OpenSSH client + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y openssh-client + - name: Install node-gyp + shell: bash + run: npm install -g node-gyp - name: Install dependencies shell: bash - run: pnpm install --frozen-lockfile + run: bun install --frozen-lockfile diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 4afb36a1..b95e316a 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -24,7 +24,7 @@ jobs: - name: Install dependencies uses: ./.github/actions/setup - name: Build (docker-git package) - run: pnpm --filter ./packages/app build + run: bun run --cwd packages/app build types: name: Types @@ -35,9 +35,9 @@ jobs: - name: Install dependencies uses: ./.github/actions/setup - name: Typecheck (app) - run: pnpm --filter ./packages/app check + run: bun run --cwd packages/app check - name: Typecheck (lib) - run: pnpm --filter ./packages/lib typecheck + run: bun run --cwd packages/lib typecheck lint: name: Lint @@ -47,16 +47,10 @@ jobs: - uses: actions/checkout@v6 - name: Install dependencies uses: ./.github/actions/setup - # vibecode-linter uses npx internally for dependency checks - # In pnpm workspaces, npx doesn't find local packages correctly - # Install TypeScript and Biome globally as a workaround - # See: https://github.com/ton-ai-core/vibecode-linter/issues (pending issue) - - name: Install global linter dependencies - run: npm install -g typescript @biomejs/biome - name: Lint (app) - run: pnpm --filter ./packages/app lint + run: bun run --cwd packages/app lint - name: Lint (lib) - run: pnpm --filter ./packages/lib lint + run: bun run --cwd packages/lib lint test: name: Test @@ -66,13 +60,10 @@ jobs: - uses: actions/checkout@v6 - name: Install dependencies uses: ./.github/actions/setup - # vibecode-linter uses npx internally for dependency checks (lint:tests runs first) - - name: Install global linter dependencies - run: npm install -g typescript @biomejs/biome - name: Test (app) - run: pnpm --filter ./packages/app test + run: bun run --cwd packages/app test - name: Test (lib) - run: pnpm --filter ./packages/lib test + run: bun run --cwd packages/lib test lint-effect: name: Lint Effect-TS @@ -83,9 +74,9 @@ jobs: - name: Install dependencies uses: ./.github/actions/setup - name: Lint Effect-TS (app) - run: pnpm --filter ./packages/app lint:effect + run: bun run --cwd packages/app lint:effect - name: Lint Effect-TS (lib) - run: pnpm --filter ./packages/lib lint:effect + run: bun run --cwd packages/lib lint:effect e2e-local-package: name: E2E (Local package CLI) @@ -95,7 +86,7 @@ jobs: - uses: actions/checkout@v6 - name: Install dependencies uses: ./.github/actions/setup - - name: Pack and run local package via pnpm + - name: Pack and run local package via Bun run: bash scripts/e2e/local-package-cli.sh e2e-opencode: diff --git a/.github/workflows/checking-dependencies.yml b/.github/workflows/checking-dependencies.yml index 57ab1ba1..6f3ba1e0 100644 --- a/.github/workflows/checking-dependencies.yml +++ b/.github/workflows/checking-dependencies.yml @@ -12,18 +12,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v6 + - uses: ./.github/actions/setup with: node-version: 24.14.0 - cache: pnpm - - - run: pnpm install --frozen-lockfile - - run: pnpm -C packages/app build + - run: bun run --cwd packages/app build - name: Dist deps prune (lint) - run: | - pnpm dlx @prover-coder-ai/dist-deps-prune scan \ - --package ./packages/app/package.json \ - --prune-dev true \ - --silent + run: bun run check:dist-deps-prune diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ec33349e..774c7345 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,16 +17,265 @@ jobs: if: github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest steps: - - uses: ProverCoderAI/action-release@v1.0.17 + - uses: actions/checkout@v6 with: - github_token: ${{ secrets.GITHUB_TOKEN }} - npm_token: ${{ secrets.NPM_TOKEN }} + fetch-depth: 0 ref: ${{ github.event.workflow_run.head_sha }} - branch: ${{ github.event.workflow_run.head_branch }} - package_json_path: packages/app/package.json - pnpm_filter: ./packages/app - bump_type: patch - publish_npm: true - publish_github_packages: true - skip_if_unchanged: true - cancel_on_no_changes: true + token: ${{ secrets.GITHUB_TOKEN }} + - name: Install dependencies + uses: ./.github/actions/setup + - name: Compare with npm package + id: compare_npm + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + shell: bash + run: | + set -euo pipefail + PKG_PATH="packages/app/package.json" + PKG_DIR="$(dirname "$PKG_PATH")" + PKG_NAME="$(bun -e "console.log(JSON.parse(await Bun.file('./${PKG_PATH}').text()).name)")" + TMP_DIR="$(mktemp -d)" + LOCAL_PACK_DIR="${TMP_DIR}/local-pack" + REMOTE_PACK_DIR="${TMP_DIR}/remote-pack" + README_DEST="packages/app/README.md" + README_BACKUP="" + README_CREATED="false" + BACKUP_PKG="${PKG_DIR}/.package.json.release.bak" + + cleanup() { + if [ -f "$BACKUP_PKG" ]; then + cp "$BACKUP_PKG" "$PKG_PATH" || true + rm -f "$BACKUP_PKG" || true + fi + if [ -f "$README_BACKUP" ]; then + cp "$README_BACKUP" "$README_DEST" || true + rm -f "$README_BACKUP" || true + elif [ "$README_CREATED" = "true" ] && [ -f "$README_DEST" ]; then + rm -f "$README_DEST" || true + fi + rm -rf "$TMP_DIR" + } + trap cleanup EXIT + + mkdir -p "$LOCAL_PACK_DIR" "$REMOTE_PACK_DIR" + + if [ -n "${NPM_TOKEN:-}" ]; then + printf '%s\n' "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > "$HOME/.npmrc" + fi + + if ! LATEST_VERSION="$(npm view "${PKG_NAME}" version 2>/dev/null)"; then + echo "Package ${PKG_NAME} not found on npm; proceeding with release." + echo "should_release=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + REMOTE_TARBALL="$(npm pack "${PKG_NAME}@${LATEST_VERSION}" --silent --pack-destination "$REMOTE_PACK_DIR" | tail -n 1)" + REMOTE_TAR_PATH="$REMOTE_PACK_DIR/$REMOTE_TARBALL" + if [ ! -f "$REMOTE_TAR_PATH" ]; then + echo "Unable to download ${PKG_NAME}@${LATEST_VERSION}; proceeding with release." + echo "should_release=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + bun run --cwd packages/app build + + if [ -f "$README_DEST" ]; then + README_BACKUP="${TMP_DIR}/README.backup" + cp "$README_DEST" "$README_BACKUP" + else + README_CREATED="true" + fi + mkdir -p "$(dirname "$README_DEST")" + cp README.md "$README_DEST" + + cp "$PKG_PATH" "$BACKUP_PKG" + bun x @prover-coder-ai/dist-deps-prune apply \ + --package "$PKG_PATH" \ + --prune-dev true \ + --write \ + --silent + + LOCAL_TAR_PATH="$(cd "$PKG_DIR" && bun pm pack --quiet --ignore-scripts --destination "$LOCAL_PACK_DIR" | tail -n 1 | tr -d '\r')" + if [ ! -f "$LOCAL_TAR_PATH" ]; then + echo "Unable to pack local ${PKG_NAME}; proceeding with release." + echo "should_release=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + LOCAL_DIR="${TMP_DIR}/local" + REMOTE_DIR="${TMP_DIR}/remote" + mkdir -p "$LOCAL_DIR" "$REMOTE_DIR" + + tar -xzf "$LOCAL_TAR_PATH" -C "$LOCAL_DIR" + tar -xzf "$REMOTE_TAR_PATH" -C "$REMOTE_DIR" + + LOCAL_PKG="${LOCAL_DIR}/package/package.json" + REMOTE_PKG="${REMOTE_DIR}/package/package.json" + + if [ ! -f "$LOCAL_PKG" ] || [ ! -f "$REMOTE_PKG" ]; then + echo "package.json missing in tarball; proceeding with release." + echo "should_release=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + bun -e "const p=process.argv[1];const sort=(v)=>Array.isArray(v)?v.map(sort):v&&typeof v==='object'?Object.keys(v).sort().reduce((acc,k)=>{acc[k]=sort(v[k]);return acc;},{}):v;const pkg=JSON.parse(await Bun.file(p).text());delete pkg.gitHead;pkg.version='0.0.0';const norm=sort(pkg);await Bun.write(p, JSON.stringify(norm, null, 2)+'\n');" "$LOCAL_PKG" + bun -e "const p=process.argv[1];const sort=(v)=>Array.isArray(v)?v.map(sort):v&&typeof v==='object'?Object.keys(v).sort().reduce((acc,k)=>{acc[k]=sort(v[k]);return acc;},{}):v;const pkg=JSON.parse(await Bun.file(p).text());delete pkg.gitHead;pkg.version='0.0.0';const norm=sort(pkg);await Bun.write(p, JSON.stringify(norm, null, 2)+'\n');" "$REMOTE_PKG" + + if diff -qr "$LOCAL_DIR/package" "$REMOTE_DIR/package" >/dev/null 2>&1; then + echo "::notice::No changes compared to ${PKG_NAME}@${LATEST_VERSION}. Skipping release." + echo "should_release=false" >> "$GITHUB_OUTPUT" + else + echo "Package differs from ${PKG_NAME}@${LATEST_VERSION}; proceeding with release." + echo "should_release=true" >> "$GITHUB_OUTPUT" + fi + - name: Auto changeset (patch if no changeset exists) + if: steps.compare_npm.outputs.should_release != 'false' && github.actor != 'github-actions[bot]' + shell: bash + run: | + set -euo pipefail + mkdir -p .changeset + if ! ls .changeset/*.md >/dev/null 2>&1; then + printf '%s\n' \ + '---' \ + '"@prover-coder-ai/docker-git": patch' \ + '---' \ + '' \ + 'chore: automated version bump' \ + > ".changeset/auto-${GITHUB_SHA}.md" + fi + - name: Version packages + if: steps.compare_npm.outputs.should_release != 'false' && github.actor != 'github-actions[bot]' + shell: bash + run: | + set -euo pipefail + bun run changeset-version + - name: Read version + id: release_version + if: steps.compare_npm.outputs.should_release != 'false' && github.actor != 'github-actions[bot]' + shell: bash + run: | + set -euo pipefail + VERSION="$(bun -e "console.log(JSON.parse(await Bun.file('./packages/app/package.json').text()).version)")" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT" + - name: Commit version changes + if: steps.compare_npm.outputs.should_release != 'false' && github.actor != 'github-actions[bot]' + shell: bash + run: | + set -euo pipefail + if ! git status --porcelain | grep -q .; then + exit 0 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + git add -A + git commit -m "chore(release): version packages" + BRANCH="${{ github.event.workflow_run.head_branch }}" + git fetch origin "${BRANCH}" + git rebase "origin/${BRANCH}" + if ! git push origin "HEAD:${BRANCH}"; then + git fetch origin "${BRANCH}" + git rebase "origin/${BRANCH}" + git push origin "HEAD:${BRANCH}" + fi + - name: Tag release + if: steps.compare_npm.outputs.should_release != 'false' && github.actor != 'github-actions[bot]' + shell: bash + run: | + set -euo pipefail + TAG="${{ steps.release_version.outputs.tag }}" + if git rev-parse "$TAG" >/dev/null 2>&1; then + echo "Tag $TAG already exists" + exit 0 + fi + git tag -a "$TAG" -m "$TAG" + git push origin "$TAG" + - name: Prepare package README + if: steps.compare_npm.outputs.should_release != 'false' + shell: bash + run: | + set -euo pipefail + mkdir -p packages/app + cp README.md packages/app/README.md + - name: Build dist + if: steps.compare_npm.outputs.should_release != 'false' + shell: bash + run: | + set -euo pipefail + bun run --cwd packages/app build + - name: Configure npm auth + if: steps.compare_npm.outputs.should_release != 'false' && github.actor != 'github-actions[bot]' + shell: bash + run: | + set -euo pipefail + if [ -z "${{ secrets.NPM_TOKEN }}" ]; then + echo "NPM_TOKEN is not set" + exit 1 + fi + printf '%s\n' "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > "$HOME/.npmrc" + - name: Publish to npm + if: steps.compare_npm.outputs.should_release != 'false' && github.actor != 'github-actions[bot]' + shell: bash + run: | + set -euo pipefail + PKG_PATH="packages/app/package.json" + PKG_NAME="$(bun -e "console.log(JSON.parse(await Bun.file('./${PKG_PATH}').text()).name)")" + VERSION="$(bun -e "console.log(JSON.parse(await Bun.file('./${PKG_PATH}').text()).version)")" + + if npm view "${PKG_NAME}@${VERSION}" version >/dev/null 2>&1; then + echo "Version ${VERSION} already published; skipping npm publish." + exit 0 + fi + + bun x @prover-coder-ai/dist-deps-prune release \ + --package "${PKG_PATH}" \ + --command "bash -lc 'cd packages/app && bun publish --ignore-scripts --access public'" \ + --silent + - name: Publish to GitHub Packages + if: steps.compare_npm.outputs.should_release != 'false' && github.actor != 'github-actions[bot]' + shell: bash + run: | + set -euo pipefail + OWNER="${{ github.repository_owner }}" + OWNER_LOWER="$(printf '%s' "$OWNER" | tr '[:upper:]' '[:lower:]')" + + PKG_NAME="$(bun -e "console.log(JSON.parse(await Bun.file('./packages/app/package.json').text()).name)")" + ORIGINAL_PKG_NAME="$PKG_NAME" + PKG_SUFFIX="${PKG_NAME#*/}" + if [ "$PKG_SUFFIX" = "$PKG_NAME" ]; then + GH_PKG="@${OWNER_LOWER}/${PKG_NAME}" + else + GH_PKG="@${OWNER_LOWER}/${PKG_SUFFIX}" + fi + + printf '%s\n' "@${OWNER_LOWER}:registry=https://npm.pkg.github.com" >> "$HOME/.npmrc" + printf '%s\n' "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" >> "$HOME/.npmrc" + + bun -e "const p='packages/app/package.json';const pkg=JSON.parse(await Bun.file(p).text());pkg.name='${GH_PKG}';await Bun.write(p, JSON.stringify(pkg, null, 2)+'\n');" + bun x @prover-coder-ai/dist-deps-prune release \ + --package "packages/app/package.json" \ + --command "bash -lc 'cd packages/app && bun publish --ignore-scripts --registry https://npm.pkg.github.com'" \ + --silent + bun -e "const p='packages/app/package.json';const pkg=JSON.parse(await Bun.file(p).text());pkg.name='${ORIGINAL_PKG_NAME}';await Bun.write(p, JSON.stringify(pkg, null, 2)+'\n');" + - name: Create GitHub Release + if: steps.compare_npm.outputs.should_release != 'false' && github.actor != 'github-actions[bot]' + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.release_version.outputs.tag }} + generate_release_notes: true + token: ${{ secrets.GITHUB_TOKEN }} + - name: Print npm package link + shell: bash + run: | + set -euo pipefail + PKG_NAME="$(bun -e "console.log(JSON.parse(await Bun.file('./packages/app/package.json').text()).name)")" + if [ -n "${{ secrets.NPM_TOKEN }}" ]; then + printf '%s\n' "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > "$HOME/.npmrc" + fi + if LATEST_VERSION="$(npm view "${PKG_NAME}" version 2>/dev/null)"; then + echo "::notice::npm package: https://www.npmjs.com/package/${PKG_NAME}/v/${LATEST_VERSION}" + else + echo "::notice::npm package: https://www.npmjs.com/package/${PKG_NAME}" + fi diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 037fa467..35ced16b 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -19,36 +19,17 @@ jobs: - name: Install dependencies uses: ./.github/actions/setup with: - node-version: 22.12.0 + bun-version: 1.3.11 - name: Build package - run: pnpm build - - name: Create snapshot - id: snapshot - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - shell: bash - working-directory: packages/app - run: | - set +e - pnpx pkg-pr-new@0.0.24 publish --pnpm --comment=off - STATUS=$? - if [ $STATUS -eq 0 ]; then - echo "success=true" >> "$GITHUB_OUTPUT" - else - echo "success=false" >> "$GITHUB_OUTPUT" - echo "pkg-pr-new failed (likely app not installed); falling back to artifacts." - fi - exit 0 - - name: Fallback snapshot artifacts - if: steps.snapshot.outputs.success != 'true' + run: bun run --cwd packages/app build + - name: Create snapshot artifacts shell: bash run: | set -euo pipefail mkdir -p artifacts cd packages/app - npm pack --silent --pack-destination ../../artifacts + bun pm pack --quiet --ignore-scripts --destination ../../artifacts - name: Upload snapshot artifacts - if: steps.snapshot.outputs.success != 'true' uses: actions/upload-artifact@v7 with: name: context-doc-snapshot diff --git a/.gitignore b/.gitignore index ebc26f4a..e27fe0ce 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ dist/ build/ coverage/ packages/*/dist/ +packages/*/dist-web/ npm-debug.log* yarn-debug.log* yarn-error.log* diff --git a/AGENTS.md b/AGENTS.md index 7c87c6cd..3e64fd7e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -340,7 +340,7 @@ describe("Message invariants", () => { - **Линт**: `npm run lint` (с функциональными правилами) - **Тесты**: `npm test` (unit + property-based + integration) -- **ts-morph скрипты**: `npx ts-node scripts/.ts` +- **ts-morph скрипты**: `bun scripts/.ts` ПРОВЕРКИ КАЧЕСТВА: ═══════════════════ diff --git a/CLAUDE.md b/CLAUDE.md index a8473ac5..02b9ade9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -234,7 +234,7 @@ describe("Message invariants", () => { - **Линт**: `npm run lint` (с функциональными правилами) - **Тесты**: `npm test` (unit + property-based + integration) -- **ts-morph скрипты**: `npx ts-node scripts/.ts` +- **ts-morph скрипты**: `bun scripts/.ts` ПРОВЕРКИ КАЧЕСТВА: ═══════════════════ diff --git a/Dockerfile b/Dockerfile index c2227fab..2e6a4a2f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,18 @@ FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive +ENV BUN_INSTALL=/opt/bun +ENV PATH=/opt/bun/bin:$PATH RUN apt-get update && apt-get install -y --no-install-recommends \ - openssh-server git ca-certificates nodejs npm sshpass \ + openssh-server git ca-certificates curl unzip sshpass gnupg \ && rm -rf /var/lib/apt/lists/* -# Tooling: pnpm + Codex CLI -RUN npm i -g pnpm@10.27.0 @openai/codex +# Tooling: Bun + Codex CLI +RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && curl -fsSL https://bun.sh/install | bash \ + && npm i -g node-gyp @openai/codex # Create non-root user for SSH RUN useradd -m -s /bin/bash dev diff --git a/bun.lock b/bun.lock new file mode 100644 index 00000000..6741a267 --- /dev/null +++ b/bun.lock @@ -0,0 +1,1937 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "effect-template-workspace", + "devDependencies": { + "@changesets/changelog-github": "^0.6.0", + "@changesets/cli": "^2.30.0", + "@prover-coder-ai/dist-deps-prune": "^1.0.17", + }, + }, + "packages/api": { + "name": "@effect-template/api", + "version": "0.1.0", + "dependencies": { + "@effect-template/lib": "workspace:*", + "@effect/platform": "^0.96.0", + "@effect/platform-node": "^0.106.0", + "@effect/schema": "^0.75.5", + "effect": "^3.21.0", + "node-pty": "^1.0.0", + "ws": "^8.18.3", + }, + "devDependencies": { + "@effect/vitest": "^0.29.0", + "@eslint/js": "10.0.1", + "@types/node": "^24.12.0", + "@types/ws": "^8.18.1", + "@typescript-eslint/eslint-plugin": "^8.57.1", + "@typescript-eslint/parser": "^8.57.1", + "eslint": "^10.1.0", + "globals": "^17.4.0", + "typescript": "^5.9.3", + "vitest": "^4.1.0", + }, + }, + "packages/app": { + "name": "@prover-coder-ai/docker-git", + "version": "1.0.77", + "bin": { + "docker-git": "dist/src/docker-git/main.js", + }, + "dependencies": { + "@effect/cli": "^0.75.0", + "@effect/cluster": "^0.58.0", + "@effect/experimental": "^0.60.0", + "@effect/platform": "^0.96.0", + "@effect/platform-node": "^0.106.0", + "@effect/printer": "^0.49.0", + "@effect/printer-ansi": "^0.49.0", + "@effect/rpc": "^0.75.0", + "@effect/schema": "^0.75.5", + "@effect/sql": "^0.51.0", + "@effect/typeclass": "^0.40.0", + "@effect/workflow": "^0.18.0", + "@gridland/bun": "0.2.53", + "@gridland/web": "0.2.53", + "effect": "^3.21.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-reconciler": "^0.33.0", + "ts-morph": "^27.0.2", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0", + }, + "devDependencies": { + "@biomejs/biome": "^2.4.8", + "@effect-template/lib": "workspace:*", + "@effect/eslint-plugin": "^0.3.2", + "@effect/language-service": "latest", + "@effect/vitest": "^0.29.0", + "@eslint-community/eslint-plugin-eslint-comments": "^4.7.1", + "@eslint/compat": "2.0.3", + "@eslint/eslintrc": "3.3.5", + "@eslint/js": "10.0.1", + "@prover-coder-ai/eslint-plugin-suggest-members": "^0.0.25", + "@ton-ai-core/vibecode-linter": "^1.0.11", + "@types/node": "^24.12.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@typescript-eslint/eslint-plugin": "^8.57.1", + "@typescript-eslint/parser": "^8.57.1", + "@vitejs/plugin-react": "^6.0.1", + "@vitest/coverage-v8": "^4.1.0", + "@vitest/eslint-plugin": "^1.6.13", + "biome": "npm:@biomejs/biome@^2.4.8", + "eslint": "^10.1.0", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-codegen": "0.34.1", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-simple-import-sort": "^12.1.1", + "eslint-plugin-sonarjs": "^4.0.2", + "eslint-plugin-sort-destructure-keys": "^3.0.0", + "eslint-plugin-unicorn": "^63.0.0", + "globals": "^17.4.0", + "jscpd": "^4.0.8", + "typescript": "^5.9.3", + "typescript-eslint": "^8.57.1", + "vite": "^8.0.1", + "vitest": "^4.1.0", + }, + }, + "packages/lib": { + "name": "@effect-template/lib", + "version": "1.0.0", + "dependencies": { + "@effect/cli": "^0.75.0", + "@effect/cluster": "^0.58.0", + "@effect/experimental": "^0.60.0", + "@effect/platform": "^0.96.0", + "@effect/platform-node": "^0.106.0", + "@effect/printer": "^0.49.0", + "@effect/printer-ansi": "^0.49.0", + "@effect/rpc": "^0.75.0", + "@effect/schema": "^0.75.5", + "@effect/sql": "^0.51.0", + "@effect/typeclass": "^0.40.0", + "@effect/workflow": "^0.18.0", + "effect": "^3.21.0", + "ts-morph": "^27.0.2", + }, + "devDependencies": { + "@biomejs/biome": "^2.4.8", + "@effect/eslint-plugin": "^0.3.2", + "@effect/language-service": "latest", + "@effect/vitest": "^0.29.0", + "@eslint-community/eslint-plugin-eslint-comments": "^4.7.1", + "@eslint/compat": "2.0.3", + "@eslint/eslintrc": "3.3.5", + "@eslint/js": "10.0.1", + "@prover-coder-ai/eslint-plugin-suggest-members": "^0.0.25", + "@ton-ai-core/vibecode-linter": "^1.0.11", + "@types/node": "^24.12.0", + "@typescript-eslint/eslint-plugin": "^8.57.1", + "@typescript-eslint/parser": "^8.57.1", + "@vitest/coverage-v8": "^4.1.0", + "@vitest/eslint-plugin": "^1.6.13", + "eslint": "^10.1.0", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-codegen": "0.34.1", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-simple-import-sort": "^12.1.1", + "eslint-plugin-sonarjs": "^4.0.2", + "eslint-plugin-sort-destructure-keys": "^3.0.0", + "eslint-plugin-unicorn": "^63.0.0", + "globals": "^17.4.0", + "jscpd": "^4.0.8", + "typescript": "^5.9.3", + "typescript-eslint": "^8.57.1", + "vite": "^8.0.1", + "vitest": "^4.1.0", + }, + }, + }, + "trustedDependencies": [ + "node-pty", + "msgpackr-extract", + "unrs-resolver", + "@parcel/watcher", + ], + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "7.28.5", "js-tokens": "4.0.0", "picocolors": "1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], + + "@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/generator": "7.28.5", "@babel/helper-compilation-targets": "7.27.2", "@babel/helper-module-transforms": "7.28.3", "@babel/helpers": "7.28.4", "@babel/parser": "7.28.5", "@babel/template": "7.27.2", "@babel/traverse": "7.28.5", "@babel/types": "7.28.5", "@jridgewell/remapping": "2.3.5", "convert-source-map": "2.0.0", "debug": "4.4.3", "gensync": "1.0.0-beta.2", "json5": "2.2.3", "semver": "6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], + + "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "7.28.5", "@babel/types": "7.28.5", "@jridgewell/gen-mapping": "0.3.13", "@jridgewell/trace-mapping": "0.3.31", "jsesc": "3.1.0" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "7.28.5", "@babel/helper-validator-option": "7.27.1", "browserslist": "4.28.0", "lru-cache": "5.1.1", "semver": "6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "7.28.5", "@babel/types": "7.28.5" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "7.27.1", "@babel/helper-validator-identifier": "7.28.5", "@babel/traverse": "7.28.5" }, "peerDependencies": { "@babel/core": "7.28.5" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "7.27.2", "@babel/types": "7.28.5" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + + "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/parser": "7.28.5", "@babel/types": "7.28.5" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/generator": "7.28.5", "@babel/helper-globals": "7.28.0", "@babel/parser": "7.28.5", "@babel/template": "7.27.2", "@babel/types": "7.28.5", "debug": "4.4.3" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "7.27.1", "@babel/helper-validator-identifier": "7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], + + "@biomejs/biome": ["@biomejs/biome@2.4.8", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.8", "@biomejs/cli-darwin-x64": "2.4.8", "@biomejs/cli-linux-arm64": "2.4.8", "@biomejs/cli-linux-arm64-musl": "2.4.8", "@biomejs/cli-linux-x64": "2.4.8", "@biomejs/cli-linux-x64-musl": "2.4.8", "@biomejs/cli-win32-arm64": "2.4.8", "@biomejs/cli-win32-x64": "2.4.8" }, "bin": { "biome": "bin/biome" } }, "sha512-ponn0oKOky1oRXBV+rlSaUlixUxf1aZvWC19Z41zBfUOUesthrQqL3OtiAlSB1EjFjyWpn98Q64DHelhA6jNlA=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ARx0tECE8I7S2C2yjnWYLNbBdDoPdq3oyNLhMglmuctThwUsuzFWRKrHmIGwIRWKz0Mat9DuzLEDp52hGnrxGQ=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Jg9/PsB9vDCJlANE8uhG7qDhb5w0Ix69D7XIIc8IfZPUoiPrbLm33k2Ig3NOJ/7nb3UbesFz3D1aDKm9DvzjhQ=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-5CdrsJct76XG2hpKFwXnEtlT1p+4g4yV+XvvwBpzKsTNLO9c6iLlAxwcae2BJ7ekPGWjNGw9j09T5KGPKKxQig=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-Zo9OhBQDJ3IBGPlqHiTISloo5H0+FBIpemqIJdW/0edJ+gEcLR+MZeZozcUyz3o1nXkVA7++DdRKQT0599j9jA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.8", "", { "os": "linux", "cpu": "x64" }, "sha512-PdKXspVEaMCQLjtZCn6vfSck/li4KX9KGwSDbZdgIqlrizJ2MnMcE3TvHa2tVfXNmbjMikzcfJpuPWH695yJrw=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.8", "", { "os": "linux", "cpu": "x64" }, "sha512-Gi8quv8MEuDdKaPFtS2XjEnMqODPsRg6POT6KhoP+VrkNb+T2ywunVB+TvOU0LX1jAZzfBr+3V1mIbBhzAMKvw=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-LoFatS0tnHv6KkCVpIy3qZCih+MxUMvdYiPWLHRri7mhi2vyOOs8OrbZBcLTUEWCS+ktO72nZMy4F96oMhkOHQ=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.8", "", { "os": "win32", "cpu": "x64" }, "sha512-vAn7iXDoUbqFXqVocuq1sMYAd33p8+mmurqJkWl6CtIhobd/O6moe4rY5AJvzbunn/qZCdiDVcveqtkFh1e7Hg=="], + + "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.1.0", "", { "dependencies": { "@changesets/config": "3.1.3", "@changesets/get-version-range-type": "0.4.0", "@changesets/git": "3.0.4", "@changesets/should-skip-package": "0.1.2", "@changesets/types": "6.1.0", "@manypkg/get-packages": "1.1.3", "detect-indent": "6.1.0", "fs-extra": "7.0.1", "lodash.startcase": "4.4.0", "outdent": "0.5.0", "prettier": "2.8.8", "resolve-from": "5.0.0", "semver": "7.7.4" } }, "sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ=="], + + "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.9", "", { "dependencies": { "@changesets/errors": "0.2.0", "@changesets/get-dependents-graph": "2.1.3", "@changesets/should-skip-package": "0.1.2", "@changesets/types": "6.1.0", "@manypkg/get-packages": "1.1.3", "semver": "7.7.4" } }, "sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ=="], + + "@changesets/changelog-git": ["@changesets/changelog-git@0.2.1", "", { "dependencies": { "@changesets/types": "6.1.0" } }, "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q=="], + + "@changesets/changelog-github": ["@changesets/changelog-github@0.6.0", "", { "dependencies": { "@changesets/get-github-info": "0.8.0", "@changesets/types": "6.1.0", "dotenv": "8.6.0" } }, "sha512-wA2/y4hR/A1K411cCT75rz0d46Iezxp1WYRFoFJDIUpkQ6oDBAIUiU7BZkDCmYgz0NBl94X1lgcZO+mHoiHnFg=="], + + "@changesets/cli": ["@changesets/cli@2.30.0", "", { "dependencies": { "@changesets/apply-release-plan": "7.1.0", "@changesets/assemble-release-plan": "6.0.9", "@changesets/changelog-git": "0.2.1", "@changesets/config": "3.1.3", "@changesets/errors": "0.2.0", "@changesets/get-dependents-graph": "2.1.3", "@changesets/get-release-plan": "4.0.15", "@changesets/git": "3.0.4", "@changesets/logger": "0.1.1", "@changesets/pre": "2.0.2", "@changesets/read": "0.6.7", "@changesets/should-skip-package": "0.1.2", "@changesets/types": "6.1.0", "@changesets/write": "0.4.0", "@inquirer/external-editor": "1.0.3", "@manypkg/get-packages": "1.1.3", "ansi-colors": "4.1.3", "enquirer": "2.4.1", "fs-extra": "7.0.1", "mri": "1.2.0", "package-manager-detector": "0.2.11", "picocolors": "1.1.1", "resolve-from": "5.0.0", "semver": "7.7.3", "spawndamnit": "3.0.1", "term-size": "2.2.1" }, "bin": { "changeset": "bin.js" } }, "sha512-5D3Nk2JPqMI1wK25pEymeWRSlSMdo5QOGlyfrKg0AOufrUcjEE3RQgaCpHoBiM31CSNrtSgdJ0U6zL1rLDDfBA=="], + + "@changesets/config": ["@changesets/config@3.1.3", "", { "dependencies": { "@changesets/errors": "0.2.0", "@changesets/get-dependents-graph": "2.1.3", "@changesets/logger": "0.1.1", "@changesets/should-skip-package": "0.1.2", "@changesets/types": "6.1.0", "@manypkg/get-packages": "1.1.3", "fs-extra": "7.0.1", "micromatch": "4.0.8" } }, "sha512-vnXjcey8YgBn2L1OPWd3ORs0bGC4LoYcK/ubpgvzNVr53JXV5GiTVj7fWdMRsoKUH7hhhMAQnsJUqLr21EncNw=="], + + "@changesets/errors": ["@changesets/errors@0.2.0", "", { "dependencies": { "extendable-error": "0.1.7" } }, "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow=="], + + "@changesets/get-dependents-graph": ["@changesets/get-dependents-graph@2.1.3", "", { "dependencies": { "@changesets/types": "6.1.0", "@manypkg/get-packages": "1.1.3", "picocolors": "1.1.1", "semver": "7.7.4" } }, "sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ=="], + + "@changesets/get-github-info": ["@changesets/get-github-info@0.8.0", "", { "dependencies": { "dataloader": "1.4.0", "node-fetch": "2.7.0" } }, "sha512-cRnC+xdF0JIik7coko3iUP9qbnfi1iJQ3sAa6dE+Tx3+ET8bjFEm63PA4WEohgjYcmsOikPHWzPsMWWiZmntOQ=="], + + "@changesets/get-release-plan": ["@changesets/get-release-plan@4.0.15", "", { "dependencies": { "@changesets/assemble-release-plan": "6.0.9", "@changesets/config": "3.1.3", "@changesets/pre": "2.0.2", "@changesets/read": "0.6.7", "@changesets/types": "6.1.0", "@manypkg/get-packages": "1.1.3" } }, "sha512-Q04ZaRPuEVZtA+auOYgFaVQQSA98dXiVe/yFaZfY7hoSmQICHGvP0TF4u3EDNHWmmCS4ekA/XSpKlSM2PyTS2g=="], + + "@changesets/get-version-range-type": ["@changesets/get-version-range-type@0.4.0", "", {}, "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ=="], + + "@changesets/git": ["@changesets/git@3.0.4", "", { "dependencies": { "@changesets/errors": "0.2.0", "@manypkg/get-packages": "1.1.3", "is-subdir": "1.2.0", "micromatch": "4.0.8", "spawndamnit": "3.0.1" } }, "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw=="], + + "@changesets/logger": ["@changesets/logger@0.1.1", "", { "dependencies": { "picocolors": "1.1.1" } }, "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg=="], + + "@changesets/parse": ["@changesets/parse@0.4.3", "", { "dependencies": { "@changesets/types": "6.1.0", "js-yaml": "4.1.1" } }, "sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A=="], + + "@changesets/pre": ["@changesets/pre@2.0.2", "", { "dependencies": { "@changesets/errors": "0.2.0", "@changesets/types": "6.1.0", "@manypkg/get-packages": "1.1.3", "fs-extra": "7.0.1" } }, "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug=="], + + "@changesets/read": ["@changesets/read@0.6.7", "", { "dependencies": { "@changesets/git": "3.0.4", "@changesets/logger": "0.1.1", "@changesets/parse": "0.4.3", "@changesets/types": "6.1.0", "fs-extra": "7.0.1", "p-filter": "2.1.0", "picocolors": "1.1.1" } }, "sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA=="], + + "@changesets/should-skip-package": ["@changesets/should-skip-package@0.1.2", "", { "dependencies": { "@changesets/types": "6.1.0", "@manypkg/get-packages": "1.1.3" } }, "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw=="], + + "@changesets/types": ["@changesets/types@6.1.0", "", {}, "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA=="], + + "@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "6.1.0", "fs-extra": "7.0.1", "human-id": "4.1.3", "prettier": "2.8.8" } }, "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q=="], + + "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], + + "@dprint/formatter": ["@dprint/formatter@0.4.1", "", {}, "sha512-IB/GXdlMOvi0UhQQ9mcY15Fxcrc2JPadmo6tqefCNV0bptFq7YBpggzpqYXldBXDa04CbKJ+rDwO2eNRPE2+/g=="], + + "@dprint/typescript": ["@dprint/typescript@0.91.8", "", {}, "sha512-tuKn4leCPItox1O4uunHcQF0QllDCvPWklnNQIh2PiWWVtRAGltJJnM4Cwj5AciplosD1Hiz7vAY3ew3crLb3A=="], + + "@effect-template/api": ["@effect-template/api@workspace:packages/api"], + + "@effect-template/lib": ["@effect-template/lib@workspace:packages/lib"], + + "@effect/cli": ["@effect/cli@0.75.0", "", { "dependencies": { "ini": "4.1.3", "toml": "3.0.0", "yaml": "2.8.2" }, "peerDependencies": { "@effect/platform": "0.96.0", "@effect/printer": "0.49.0", "@effect/printer-ansi": "0.49.0", "effect": "3.21.0" } }, "sha512-SAJj1a1kb5yoSUz4yORmwjyOBv89y2wf2Q08KC/RwskUCZunj29eNZgl8Pkbv6nDFTGlre6EW/Kl2S/aOtQWwQ=="], + + "@effect/cluster": ["@effect/cluster@0.58.0", "", { "dependencies": { "kubernetes-types": "1.30.0" }, "peerDependencies": { "@effect/platform": "0.96.0", "@effect/rpc": "0.75.0", "@effect/sql": "0.51.0", "@effect/workflow": "0.18.0", "effect": "3.21.0" } }, "sha512-0Zog7s7XdntWcTqdqWPoj6nc7hPaWIzp0k0DsFUWyCynXNPK9dAtgFrSce04NhddNqqbhtZck/lhuqJwNBrprQ=="], + + "@effect/eslint-plugin": ["@effect/eslint-plugin@0.3.2", "", { "dependencies": { "@dprint/formatter": "0.4.1", "@dprint/typescript": "0.91.8", "prettier-linter-helpers": "1.0.0" } }, "sha512-c4Vs9t3r54A4Zpl+wo8+PGzZz3JWYsip41H+UrebRLjQ2Hk/ap63IeCgN/HWcYtxtyhRopjp7gW9nOQ2Snbl+g=="], + + "@effect/experimental": ["@effect/experimental@0.60.0", "", { "dependencies": { "uuid": "11.1.0" }, "peerDependencies": { "@effect/platform": "0.96.0", "effect": "3.21.0" } }, "sha512-i5zIg7Xup2KgHyqHlYtkgqSE1bNzCL0GbbTQxrpIzKF0q/ebknOk/ox8B/gIq2vImjoEE81h/oxU+6i1NH210g=="], + + "@effect/language-service": ["@effect/language-service@0.85.1", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-EXnJjIy6zQ3nUO/MZ+ynWUb8B895KZPotd1++oTs9JjDkplwM7cb6zo8Zq2zU6piwq+KflO7amXbEfj1UMpHkw=="], + + "@effect/platform": ["@effect/platform@0.96.0", "", { "dependencies": { "find-my-way-ts": "0.1.6", "msgpackr": "1.11.5", "multipasta": "0.2.7" }, "peerDependencies": { "effect": "3.21.0" } }, "sha512-U7PLhkVzg7zzrgFvyWATOzD6reL87KG/fcdOxgLWBQ/J5CCU6qdPAVG+0o6o+IxcsLoqGwxs+rFxaFzrdtDV1A=="], + + "@effect/platform-node": ["@effect/platform-node@0.106.0", "", { "dependencies": { "@effect/platform-node-shared": "0.59.0", "mime": "3.0.0", "undici": "7.16.0", "ws": "8.18.3" }, "peerDependencies": { "@effect/cluster": "0.58.0", "@effect/platform": "0.96.0", "@effect/rpc": "0.75.0", "@effect/sql": "0.51.0", "effect": "3.21.0" } }, "sha512-mpsJK2jNLVd0jQAjHKBo8j3wdKWznSGvfnKBcAuG/9Rr4mb8bMRZFLXHHT9wUP7EvnZ0tDZJgEDxkC+j+ByRag=="], + + "@effect/platform-node-shared": ["@effect/platform-node-shared@0.59.0", "", { "dependencies": { "@parcel/watcher": "2.5.1", "multipasta": "0.2.7", "ws": "8.18.3" }, "peerDependencies": { "@effect/cluster": "0.58.0", "@effect/platform": "0.96.0", "@effect/rpc": "0.75.0", "@effect/sql": "0.51.0", "effect": "3.21.0" } }, "sha512-3bq2YKKfLY7UFauZSxqZUneCXoA3SMSls82V+0RKunvRlfPuPQW0hVn6t1RkvEdh0PDoygWG2mZXYQa6Iqgp9A=="], + + "@effect/printer": ["@effect/printer@0.49.0", "", { "peerDependencies": { "@effect/typeclass": "0.40.0", "effect": "3.21.0" } }, "sha512-hrjTuExF87wuWjOnnND1c2fKcCWhleQBVaoA7JlrU3rC7s+RYPETDOXtpgAK3/uuMCRnDhfVFQMevtKT8MBdKg=="], + + "@effect/printer-ansi": ["@effect/printer-ansi@0.49.0", "", { "dependencies": { "@effect/printer": "0.49.0" }, "peerDependencies": { "@effect/typeclass": "0.40.0", "effect": "3.21.0" } }, "sha512-N2OyqDTqcGLKeUy2URowThoU5issZQwG/Ihv5qOYWJD0neq9qBIgC57/9BkFpTRPNSMtPHyCOk1TFj297HGLLQ=="], + + "@effect/rpc": ["@effect/rpc@0.75.0", "", { "dependencies": { "msgpackr": "1.11.5" }, "peerDependencies": { "@effect/platform": "0.96.0", "effect": "3.21.0" } }, "sha512-VFeJ16cZUXqiIzG9UHOVKGuiBPJ7fV+0lEbJU6xi12JnnxXe/19BQPpOwiRawCUbPOR3/xIURDUgGxU+Ft0pvQ=="], + + "@effect/schema": ["@effect/schema@0.75.5", "", { "dependencies": { "fast-check": "3.23.2" }, "peerDependencies": { "effect": "3.21.0" } }, "sha512-TQInulTVCuF+9EIbJpyLP6dvxbQJMphrnRqgexm/Ze39rSjfhJuufF7XvU3SxTgg3HnL7B/kpORTJbHhlE6thw=="], + + "@effect/sql": ["@effect/sql@0.51.0", "", { "dependencies": { "uuid": "11.1.0" }, "peerDependencies": { "@effect/experimental": "0.60.0", "@effect/platform": "0.96.0", "effect": "3.21.0" } }, "sha512-e7hWe46QD15eMCr4kNBMVdItIVK/WLHJG+d8DLL1FjVf5Ra82k2mwUYIXplJewVbHjt3my6GSKPPd1ZrQjVd5A=="], + + "@effect/typeclass": ["@effect/typeclass@0.40.0", "", { "peerDependencies": { "effect": "3.21.0" } }, "sha512-L/2o2ImeqbemFlqH0b3y2PqQTFc+E0/DUnffCU8bkJUGh0yUZmh2RXuXhR8QOpfNCe718JQjI+mLnpVF2MMmaQ=="], + + "@effect/vitest": ["@effect/vitest@0.29.0", "", { "peerDependencies": { "effect": "3.21.0", "vitest": "4.1.0" } }, "sha512-DvWr1aeEcaZ8mtu8hNVb4e3rEYvGEwQSr7wsNrW53t6nKYjkmjRICcvVEsXUhjoCblRHSxRsRV0TOt0+UmcvaQ=="], + + "@effect/workflow": ["@effect/workflow@0.18.0", "", { "peerDependencies": { "@effect/experimental": "0.60.0", "@effect/platform": "0.96.0", "@effect/rpc": "0.75.0", "effect": "3.21.0" } }, "sha512-9Zp+x9ADtR0H6CRhU6wLyPcIRjO1PXjvSpUlFlBQ8piw7ldjPmnUWEY8YQuH6eExV2dalQ4z2LMiZ5Bd7XAJbA=="], + + "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "2.8.1" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + + "@eslint-community/eslint-plugin-eslint-comments": ["@eslint-community/eslint-plugin-eslint-comments@4.7.1", "", { "dependencies": { "escape-string-regexp": "4.0.0", "ignore": "7.0.5" }, "peerDependencies": { "eslint": "10.1.0" } }, "sha512-Ql2nJFwA8wUGpILYGOQaT1glPsmvEwE0d+a+l7AALLzQvInqdbXJdx7aSu0DpUX9dB1wMVBMhm99/++S3MdEtQ=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "3.4.3" }, "peerDependencies": { "eslint": "10.1.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/compat": ["@eslint/compat@2.0.3", "", { "dependencies": { "@eslint/core": "1.1.1" }, "optionalDependencies": { "eslint": "10.1.0" } }, "sha512-SjIJhGigp8hmd1YGIBwh7Ovri7Kisl42GYFjrOyHhtfYGGoLW6teYi/5p8W50KSsawUPpuLOSmsq1bD0NGQLBw=="], + + "@eslint/config-array": ["@eslint/config-array@0.23.3", "", { "dependencies": { "@eslint/object-schema": "3.0.3", "debug": "4.4.3", "minimatch": "10.2.4" } }, "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.5.3", "", { "dependencies": { "@eslint/core": "1.1.1" } }, "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw=="], + + "@eslint/core": ["@eslint/core@1.1.1", "", { "dependencies": { "@types/json-schema": "7.0.15" } }, "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "6.14.0", "debug": "4.4.3", "espree": "10.4.0", "globals": "14.0.0", "ignore": "5.3.2", "import-fresh": "3.3.1", "js-yaml": "4.1.1", "minimatch": "3.1.5", "strip-json-comments": "3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], + + "@eslint/js": ["@eslint/js@10.0.1", "", { "optionalDependencies": { "eslint": "10.1.0" } }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="], + + "@eslint/object-schema": ["@eslint/object-schema@3.0.3", "", {}, "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.1", "", { "dependencies": { "@eslint/core": "1.1.1", "levn": "0.4.1" } }, "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ=="], + + "@gridland/bun": ["@gridland/bun@0.2.53", "", { "dependencies": { "@gridland/utils": "0.2.53", "react": "^19.0.0", "react-reconciler": "0.33.0", "yoga-layout": "^3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.1.86", "@opentui/core-darwin-x64": "0.1.86", "@opentui/core-linux-arm64": "0.1.86", "@opentui/core-linux-x64": "0.1.86" } }, "sha512-D5NVGgF4lwXKZ/Uw3EVru3oDWArMbYR5SFUcfwmtxrRdq8Mn24H7Uzwe097ieMJ89DTjWmWHV0xbpFirvV+xhQ=="], + + "@gridland/utils": ["@gridland/utils@0.2.53", "", { "peerDependencies": { "react": "19.2.4" } }, "sha512-O8Nv2OZreiBJHFCh9L40uiKe8oNvT2DWkbxWxwhtS64covyss10qz9GuJ90K0OSw8Uf6aMOvavcc3ITKcJzgLg=="], + + "@gridland/web": ["@gridland/web@0.2.53", "", { "dependencies": { "@gridland/utils": "0.2.53", "diff": "8.0.4", "events": "3.3.0", "marked": "17.0.6", "react-reconciler": "0.33.0", "yoga-layout": "3.2.1" }, "peerDependencies": { "react": "19.2.4" } }, "sha512-P8ceTS/QL8ATSJG2aWNQ1dydhujiEy3+lPECelcrlp6vCA5bbPRBu0Mt22PKOaYE4ZyuLmzg7M+dnIxwAlP1Yw=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "0.19.1", "@humanwhocodes/retry": "0.4.3" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "2.1.1", "iconv-lite": "0.7.0" }, "optionalDependencies": { "@types/node": "24.12.0" } }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "5.1.2", "string-width-cjs": "npm:string-width@4.2.3", "strip-ansi": "7.1.2", "strip-ansi-cjs": "npm:strip-ansi@6.0.1", "wrap-ansi": "8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="], + + "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "29.6.3", "@types/istanbul-lib-coverage": "2.0.6", "@types/istanbul-reports": "3.0.4", "@types/node": "24.12.0", "@types/yargs": "17.0.35", "chalk": "4.1.2" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "1.5.5", "@jridgewell/trace-mapping": "0.3.31" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "0.3.13", "@jridgewell/trace-mapping": "0.3.31" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "3.1.2", "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@jscpd/badge-reporter": ["@jscpd/badge-reporter@4.0.4", "", { "dependencies": { "badgen": "3.2.3", "colors": "1.4.0", "fs-extra": "11.3.2" } }, "sha512-I9b4MmLXPM2vo0SxSUWnNGKcA4PjQlD3GzXvFK60z43cN/EIdLbOq3FVwCL+dg2obUqGXKIzAm7EsDFTg0D+mQ=="], + + "@jscpd/core": ["@jscpd/core@4.0.4", "", { "dependencies": { "eventemitter3": "5.0.1" } }, "sha512-QGMT3iXEX1fI6lgjPH+x8eyJwhwr2KkpSF5uBpjC0Z5Xloj0yFTFLtwJT+RhxP/Ob4WYrtx2jvpKB269oIwgMQ=="], + + "@jscpd/finder": ["@jscpd/finder@4.0.4", "", { "dependencies": { "@jscpd/core": "4.0.4", "@jscpd/tokenizer": "4.0.4", "blamer": "1.0.7", "bytes": "3.1.2", "cli-table3": "0.6.5", "colors": "1.4.0", "fast-glob": "3.3.3", "fs-extra": "11.3.2", "markdown-table": "2.0.0", "pug": "3.0.3" } }, "sha512-qVUWY7Nzuvfd5OIk+n7/5CM98LmFroLqblRXAI2gDABwZrc7qS+WH2SNr0qoUq0f4OqwM+piiwKvwL/VDNn/Cg=="], + + "@jscpd/html-reporter": ["@jscpd/html-reporter@4.0.4", "", { "dependencies": { "colors": "1.4.0", "fs-extra": "11.3.2", "pug": "3.0.3" } }, "sha512-YiepyeYkeH74Kx59PJRdUdonznct0wHPFkf6FLQN+mCBoy6leAWCcOfHtcexnp+UsBFDlItG5nRdKrDSxSH+Kg=="], + + "@jscpd/tokenizer": ["@jscpd/tokenizer@4.0.4", "", { "dependencies": { "@jscpd/core": "4.0.4", "reprism": "0.0.11", "spark-md5": "3.0.2" } }, "sha512-xxYYY/qaLah/FlwogEbGIxx9CjDO+G9E6qawcy26WwrflzJb6wsnhjwdneN6Wb0RNCDsqvzY+bzG453jsin4UQ=="], + + "@manypkg/find-root": ["@manypkg/find-root@1.1.0", "", { "dependencies": { "@babel/runtime": "7.28.4", "@types/node": "12.20.55", "find-up": "4.1.0", "fs-extra": "8.1.0" } }, "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA=="], + + "@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "7.28.4", "@changesets/types": "4.1.0", "@manypkg/find-root": "1.1.0", "fs-extra": "8.1.0", "globby": "11.1.0", "read-yaml-file": "1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="], + + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], + + "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="], + + "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="], + + "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "1.7.1", "@emnapi/runtime": "1.7.1", "@tybys/wasm-util": "0.10.1" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "1.2.0" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "1.19.1" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.86", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Zp7q64+d+Dcx6YrH3mRcnHq8EOBnrfc1RvjgSWLhpXr49hY6LzuhqpfZM57aGErPYlR+ff8QM6e5FUkFnDfyjw=="], + + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.86", "", { "os": "darwin", "cpu": "x64" }, "sha512-NcxfjCJm1kLnTMVOpAPdRYNi8W8XdAXNa6N7i9khiVFrl2v5KRQfUjbrSOUYVxFJNc3jKFG6rsn3jEApvn92qA=="], + + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.86", "", { "os": "linux", "cpu": "arm64" }, "sha512-EDHAvqSOr8CXzbDvo1aE5blJ6wu1aSbR2LqoXtoeXHemr2T2W42D2TdIWewG6K+/BuRbzZnqt9wnYFBksLW6lw=="], + + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.86", "", { "os": "linux", "cpu": "x64" }, "sha512-VBaBkVdQDxYV4WcKjb+jgyMS5PiVHepvfaoKWpz1Bq+J01xXW4XPcXyPGkgR1+2R93KzaugEnLscTW4mWtLHlQ=="], + + "@oxc-project/types": ["@oxc-project/types@0.120.0", "", {}, "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg=="], + + "@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "1.0.3", "is-glob": "4.0.3", "micromatch": "4.0.8", "node-addon-api": "7.1.1" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="], + + "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="], + + "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw=="], + + "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg=="], + + "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ=="], + + "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA=="], + + "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="], + + "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="], + + "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="], + + "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="], + + "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="], + + "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw=="], + + "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ=="], + + "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@pnpm/deps.graph-sequencer": ["@pnpm/deps.graph-sequencer@1.0.0", "", {}, "sha512-vWWVbYYBBN/kweokmURicokyg7crzcDZo9/naziv8B8RSWrLWFpq5Xl0ro6QCQKgRmb6O78Qy9uQT+Fp79RxsA=="], + + "@prover-coder-ai/dist-deps-prune": ["@prover-coder-ai/dist-deps-prune@1.0.17", "", { "dependencies": { "@effect/platform": "^0.94.5", "@effect/platform-node": "^0.104.1", "@effect/schema": "^0.75.5", "effect": "^3.19.17", "typescript": "^5.9.3" }, "bin": { "dist-deps-prune": "dist/main.js" } }, "sha512-bLSCtn7txAMDiFijYOY+xiXd0CEqLmM6Ti29auqxVaOKGG0+xHAu17MqF8lOr5Xgz4/My9ZrEp9OlyDdQtHS6g=="], + + "@prover-coder-ai/docker-git": ["@prover-coder-ai/docker-git@workspace:packages/app"], + + "@prover-coder-ai/eslint-plugin-suggest-members": ["@prover-coder-ai/eslint-plugin-suggest-members@0.0.25", "", { "dependencies": { "@effect/platform": "0.94.5", "@effect/platform-node": "0.104.1", "@effect/schema": "0.75.5", "@typescript-eslint/utils": "8.55.0", "effect": "3.21.0" }, "peerDependencies": { "eslint": "10.1.0", "typescript": "5.9.3" } }, "sha512-J0oZtIz6IYeXWBgNLXaX2HyzSOcqTsjE+vzs/MQr7SKASvBYsyA7F34dQsh/8GM/kWBuSltkUsfv2RIcM6+t5Q=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.10", "", { "os": "android", "cpu": "arm64" }, "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm" }, "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "x64" }, "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.10", "", { "os": "linux", "cpu": "x64" }, "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.10", "", { "os": "none", "cpu": "arm64" }, "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.10", "", { "dependencies": { "@napi-rs/wasm-runtime": "1.1.1" }, "cpu": "none" }, "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.10", "", { "os": "win32", "cpu": "x64" }, "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="], + + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ton-ai-core/vibecode-linter": ["@ton-ai-core/vibecode-linter@1.0.11", "", { "dependencies": { "ajv": "8.17.1", "effect": "3.21.0", "jiti": "2.6.1", "jscpd": "4.0.8", "jscpd-sarif-reporter": "4.0.5", "loop-controls": "1.1.0", "ts-pattern": "5.9.0" }, "bin": { "vibecode-linter": "dist/bin/vibecode-linter.js" } }, "sha512-CSert5rYENM7MMvY3AcKdtBTYBnqeb2ti4CS4lNMWoDbyGqA6PmOH7/WK8+fcl6VyGJiPBTzq5Hp+1LYHUUuJA=="], + + "@ts-morph/common": ["@ts-morph/common@0.28.1", "", { "dependencies": { "minimatch": "10.2.4", "path-browserify": "1.0.1", "tinyglobby": "0.2.15" } }, "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "7.28.5", "@babel/types": "7.28.5", "@types/babel__generator": "7.27.0", "@types/babel__template": "7.4.4", "@types/babel__traverse": "7.28.0" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "7.28.5" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "7.28.5", "@babel/types": "7.28.5" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "7.28.5" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "4.0.2", "assertion-error": "2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/dedent": ["@types/dedent@0.7.0", "", {}, "sha512-EGlKlgMhnLt/cM4DbUSafFdrkeJoC9Mvnj0PUCU7tFmTjMjNRT957kXCx0wYm3JuEq4o4ZsS5vG+NlkM2DMd2A=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/eslint": ["@types/eslint@8.56.12", "", { "dependencies": { "@types/estree": "1.0.8", "@types/json-schema": "7.0.15" } }, "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g=="], + + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/glob": ["@types/glob@7.1.3", "", { "dependencies": { "@types/minimatch": "6.0.0", "@types/node": "24.12.0" } }, "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w=="], + + "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], + + "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "2.0.6" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="], + + "@types/istanbul-reports": ["@types/istanbul-reports@3.0.4", "", { "dependencies": { "@types/istanbul-lib-report": "3.0.3" } }, "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ=="], + + "@types/js-yaml": ["@types/js-yaml@3.12.5", "", {}, "sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + + "@types/lodash": ["@types/lodash@4.17.21", "", {}, "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ=="], + + "@types/mdast": ["@types/mdast@3.0.15", "", { "dependencies": { "@types/unist": "2.0.11" } }, "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ=="], + + "@types/minimatch": ["@types/minimatch@6.0.0", "", { "dependencies": { "minimatch": "10.2.4" } }, "sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA=="], + + "@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="], + + "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], + + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "3.2.3" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "19.2.14" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@types/sarif": ["@types/sarif@2.1.7", "", {}, "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ=="], + + "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], + + "@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "24.12.0" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "21.0.3" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="], + + "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.1", "", { "dependencies": { "@eslint-community/regexpp": "4.12.2", "@typescript-eslint/scope-manager": "8.57.1", "@typescript-eslint/type-utils": "8.57.1", "@typescript-eslint/utils": "8.57.1", "@typescript-eslint/visitor-keys": "8.57.1", "ignore": "7.0.5", "natural-compare": "1.4.0", "ts-api-utils": "2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "8.57.1", "eslint": "10.1.0", "typescript": "5.9.3" } }, "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.1", "@typescript-eslint/types": "8.57.1", "@typescript-eslint/typescript-estree": "8.57.1", "@typescript-eslint/visitor-keys": "8.57.1", "debug": "4.4.3" }, "peerDependencies": { "eslint": "10.1.0", "typescript": "5.9.3" } }, "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "8.57.1", "@typescript-eslint/types": "8.57.1", "debug": "4.4.3" }, "peerDependencies": { "typescript": "5.9.3" } }, "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.1", "", { "dependencies": { "@typescript-eslint/types": "8.57.1", "@typescript-eslint/visitor-keys": "8.57.1" } }, "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.1", "", { "peerDependencies": { "typescript": "5.9.3" } }, "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.1", "", { "dependencies": { "@typescript-eslint/types": "8.57.1", "@typescript-eslint/typescript-estree": "8.57.1", "@typescript-eslint/utils": "8.57.1", "debug": "4.4.3", "ts-api-utils": "2.4.0" }, "peerDependencies": { "eslint": "10.1.0", "typescript": "5.9.3" } }, "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.57.1", "", {}, "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.1", "@typescript-eslint/tsconfig-utils": "8.57.1", "@typescript-eslint/types": "8.57.1", "@typescript-eslint/visitor-keys": "8.57.1", "debug": "4.4.3", "minimatch": "10.2.4", "semver": "7.7.4", "tinyglobby": "0.2.15", "ts-api-utils": "2.4.0" }, "peerDependencies": { "typescript": "5.9.3" } }, "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "@typescript-eslint/scope-manager": "8.57.1", "@typescript-eslint/types": "8.57.1", "@typescript-eslint/typescript-estree": "8.57.1" }, "peerDependencies": { "eslint": "10.1.0", "typescript": "5.9.3" } }, "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.1", "", { "dependencies": { "@typescript-eslint/types": "8.57.1", "eslint-visitor-keys": "5.0.1" } }, "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A=="], + + "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], + + "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="], + + "@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.11.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g=="], + + "@unrs/resolver-binding-darwin-x64": ["@unrs/resolver-binding-darwin-x64@1.11.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ=="], + + "@unrs/resolver-binding-freebsd-x64": ["@unrs/resolver-binding-freebsd-x64@1.11.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw=="], + + "@unrs/resolver-binding-linux-arm-gnueabihf": ["@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw=="], + + "@unrs/resolver-binding-linux-arm-musleabihf": ["@unrs/resolver-binding-linux-arm-musleabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw=="], + + "@unrs/resolver-binding-linux-arm64-gnu": ["@unrs/resolver-binding-linux-arm64-gnu@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ=="], + + "@unrs/resolver-binding-linux-arm64-musl": ["@unrs/resolver-binding-linux-arm64-musl@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w=="], + + "@unrs/resolver-binding-linux-ppc64-gnu": ["@unrs/resolver-binding-linux-ppc64-gnu@1.11.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA=="], + + "@unrs/resolver-binding-linux-riscv64-gnu": ["@unrs/resolver-binding-linux-riscv64-gnu@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ=="], + + "@unrs/resolver-binding-linux-riscv64-musl": ["@unrs/resolver-binding-linux-riscv64-musl@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew=="], + + "@unrs/resolver-binding-linux-s390x-gnu": ["@unrs/resolver-binding-linux-s390x-gnu@1.11.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg=="], + + "@unrs/resolver-binding-linux-x64-gnu": ["@unrs/resolver-binding-linux-x64-gnu@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w=="], + + "@unrs/resolver-binding-linux-x64-musl": ["@unrs/resolver-binding-linux-x64-musl@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA=="], + + "@unrs/resolver-binding-wasm32-wasi": ["@unrs/resolver-binding-wasm32-wasi@1.11.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "0.2.12" }, "cpu": "none" }, "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ=="], + + "@unrs/resolver-binding-win32-arm64-msvc": ["@unrs/resolver-binding-win32-arm64-msvc@1.11.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw=="], + + "@unrs/resolver-binding-win32-ia32-msvc": ["@unrs/resolver-binding-win32-ia32-msvc@1.11.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ=="], + + "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "vite": "8.0.1" } }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], + + "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.0", "", { "dependencies": { "@bcoe/v8-coverage": "1.0.2", "@vitest/utils": "4.1.0", "ast-v8-to-istanbul": "1.0.0", "istanbul-lib-coverage": "3.2.2", "istanbul-lib-report": "3.0.1", "istanbul-reports": "3.2.0", "magicast": "0.5.2", "obug": "2.1.1", "std-env": "4.0.0", "tinyrainbow": "3.0.3" }, "peerDependencies": { "vitest": "4.1.0" } }, "sha512-nDWulKeik2bL2Va/Wl4x7DLuTKAXa906iRFooIRPR+huHkcvp9QDkPQ2RJdmjOFrqOqvNfoSQLF68deE3xC3CQ=="], + + "@vitest/eslint-plugin": ["@vitest/eslint-plugin@1.6.13", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.1", "@typescript-eslint/utils": "8.57.1" }, "optionalDependencies": { "@typescript-eslint/eslint-plugin": "8.57.1", "typescript": "5.9.3", "vitest": "4.1.0" }, "peerDependencies": { "eslint": "10.1.0" } }, "sha512-ui7JGWBoQpS5NKKW0FDb1eTuFEZ5EupEv2Psemuyfba7DfA5K52SeDLelt6P4pQJJ/4UGkker/BgMk/KrjH3WQ=="], + + "@vitest/expect": ["@vitest/expect@4.1.0", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "@types/chai": "5.2.3", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "chai": "6.2.2", "tinyrainbow": "3.0.3" } }, "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.0", "", { "dependencies": { "@vitest/spy": "4.1.0", "estree-walker": "3.0.3", "magic-string": "0.30.21" }, "optionalDependencies": { "vite": "8.0.1" } }, "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.0", "", { "dependencies": { "tinyrainbow": "3.0.3" } }, "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A=="], + + "@vitest/runner": ["@vitest/runner@4.1.0", "", { "dependencies": { "@vitest/utils": "4.1.0", "pathe": "2.0.3" } }, "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.0", "", { "dependencies": { "@vitest/pretty-format": "4.1.0", "@vitest/utils": "4.1.0", "magic-string": "0.30.21", "pathe": "2.0.3" } }, "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg=="], + + "@vitest/spy": ["@vitest/spy@4.1.0", "", {}, "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw=="], + + "@vitest/utils": ["@vitest/utils@4.1.0", "", { "dependencies": { "@vitest/pretty-format": "4.1.0", "convert-source-map": "2.0.0", "tinyrainbow": "3.0.3" } }, "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "8.16.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "3.1.3", "fast-json-stable-stringify": "2.1.0", "json-schema-traverse": "0.4.1", "uri-js": "4.4.1" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "1.0.4", "is-array-buffer": "3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + + "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "1.0.8", "call-bound": "1.0.4", "define-properties": "1.2.1", "es-abstract": "1.24.0", "es-object-atoms": "1.1.1", "get-intrinsic": "1.3.0", "is-string": "1.1.1", "math-intrinsics": "1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "1.0.8", "call-bound": "1.0.4", "define-properties": "1.2.1", "es-abstract": "1.24.0", "es-errors": "1.3.0", "es-object-atoms": "1.1.1", "es-shim-unscopables": "1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "1.0.8", "define-properties": "1.2.1", "es-abstract": "1.24.0", "es-shim-unscopables": "1.1.0" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "1.0.8", "define-properties": "1.2.1", "es-abstract": "1.24.0", "es-shim-unscopables": "1.1.0" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "1.0.2", "call-bind": "1.0.8", "define-properties": "1.2.1", "es-abstract": "1.24.0", "es-errors": "1.3.0", "get-intrinsic": "1.3.0", "is-array-buffer": "3.0.5" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + + "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], + + "assert-never": ["assert-never@1.4.0", "", {}, "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], + + "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.0", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.31", "estree-walker": "3.0.3", "js-tokens": "10.0.0" } }, "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg=="], + + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "1.1.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "babel-walk": ["babel-walk@3.0.0-canary-5", "", { "dependencies": { "@babel/types": "7.28.5" } }, "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw=="], + + "badgen": ["badgen@3.2.3", "", {}, "sha512-svDuwkc63E/z0ky3drpUppB83s/nlgDciH9m+STwwQoWyq7yCgew1qEfJ+9axkKdNq7MskByptWUN9j1PGMwFA=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.8.32", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw=="], + + "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "1.0.2" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], + + "biome": ["@biomejs/biome@2.4.8", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.8", "@biomejs/cli-darwin-x64": "2.4.8", "@biomejs/cli-linux-arm64": "2.4.8", "@biomejs/cli-linux-arm64-musl": "2.4.8", "@biomejs/cli-linux-x64": "2.4.8", "@biomejs/cli-linux-x64-musl": "2.4.8", "@biomejs/cli-win32-arm64": "2.4.8", "@biomejs/cli-win32-x64": "2.4.8" }, "bin": { "biome": "bin/biome" } }, "sha512-ponn0oKOky1oRXBV+rlSaUlixUxf1aZvWC19Z41zBfUOUesthrQqL3OtiAlSB1EjFjyWpn98Q64DHelhA6jNlA=="], + + "blamer": ["blamer@1.0.7", "", { "dependencies": { "execa": "4.1.0", "which": "2.0.2" } }, "sha512-GbBStl/EVlSWkiJQBZps3H1iARBrC7vt++Jb/TTmCNu/jZ04VW7tSN1nScbFXBUy1AN+jzeL7Zep9sbQxLhXKA=="], + + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + + "brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "4.0.4" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "2.8.32", "caniuse-lite": "1.0.30001759", "electron-to-chromium": "1.5.263", "node-releases": "2.0.27", "update-browserslist-db": "1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + + "builtin-modules": ["builtin-modules@3.3.0", "", {}, "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "1.0.2", "es-define-property": "1.0.1", "get-intrinsic": "1.3.0", "set-function-length": "1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "1.3.0", "function-bind": "1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "1.0.2", "get-intrinsic": "1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001759", "", {}, "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "4.3.0", "supports-color": "7.2.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="], + + "character-entities": ["character-entities@1.2.4", "", {}, "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw=="], + + "character-entities-legacy": ["character-entities-legacy@1.1.4", "", {}, "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA=="], + + "character-parser": ["character-parser@2.2.0", "", { "dependencies": { "is-regex": "1.2.1" } }, "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw=="], + + "character-reference-invalid": ["character-reference-invalid@1.1.4", "", {}, "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="], + + "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], + + "cheerio": ["cheerio@1.1.2", "", { "dependencies": { "cheerio-select": "2.1.0", "dom-serializer": "2.0.0", "domhandler": "5.0.3", "domutils": "3.2.2", "encoding-sniffer": "0.2.1", "htmlparser2": "10.0.0", "parse5": "7.3.0", "parse5-htmlparser2-tree-adapter": "7.1.0", "parse5-parser-stream": "7.1.2", "undici": "7.16.0", "whatwg-mimetype": "4.0.0" } }, "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg=="], + + "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "1.0.0", "css-select": "5.2.2", "css-what": "6.2.2", "domelementtype": "2.3.0", "domhandler": "5.0.3", "domutils": "3.2.2" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], + + "ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], + + "clean-regexp": ["clean-regexp@1.0.0", "", { "dependencies": { "escape-string-regexp": "1.0.5" } }, "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw=="], + + "cli-table3": ["cli-table3@0.6.5", "", { "dependencies": { "string-width": "4.2.3" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ=="], + + "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "colors": ["colors@1.4.0", "", {}, "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="], + + "commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "constantinople": ["constantinople@4.0.1", "", { "dependencies": { "@babel/parser": "7.28.5", "@babel/types": "7.28.5" } }, "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "core-js-compat": ["core-js-compat@3.47.0", "", { "dependencies": { "browserslist": "4.28.0" } }, "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "3.1.1", "shebang-command": "2.0.0", "which": "2.0.2" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "1.0.0", "css-what": "6.2.2", "domhandler": "5.0.3", "domutils": "3.2.2", "nth-check": "2.1.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], + + "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "is-data-view": "1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "is-data-view": "1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "is-data-view": "1.0.2" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + + "dataloader": ["dataloader@1.4.0", "", {}, "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "dedent": ["dedent@1.7.0", "", {}, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "1.0.1", "es-errors": "1.3.0", "gopd": "1.2.0" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "1.1.4", "has-property-descriptors": "1.0.2", "object-keys": "1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + + "diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "2.0.3" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + + "doctypes": ["doctypes@1.1.0", "", {}, "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ=="], + + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "2.3.0", "domhandler": "5.0.3", "entities": "4.5.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "2.0.0", "domelementtype": "2.3.0", "domhandler": "5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + + "dotenv": ["dotenv@8.6.0", "", {}, "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "1.0.2", "es-errors": "1.3.0", "gopd": "1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "effect": ["effect@3.21.0", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "fast-check": "3.23.2" } }, "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.263", "", {}, "sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "0.6.3", "whatwg-encoding": "3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "4.1.3", "strip-ansi": "6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "1.0.2", "arraybuffer.prototype.slice": "1.0.4", "available-typed-arrays": "1.0.7", "call-bind": "1.0.8", "call-bound": "1.0.4", "data-view-buffer": "1.0.2", "data-view-byte-length": "1.0.2", "data-view-byte-offset": "1.0.1", "es-define-property": "1.0.1", "es-errors": "1.3.0", "es-object-atoms": "1.1.1", "es-set-tostringtag": "2.1.0", "es-to-primitive": "1.3.0", "function.prototype.name": "1.1.8", "get-intrinsic": "1.3.0", "get-proto": "1.0.1", "get-symbol-description": "1.1.0", "globalthis": "1.0.4", "gopd": "1.2.0", "has-property-descriptors": "1.0.2", "has-proto": "1.2.0", "has-symbols": "1.1.0", "hasown": "2.0.2", "internal-slot": "1.1.0", "is-array-buffer": "3.0.5", "is-callable": "1.2.7", "is-data-view": "1.0.2", "is-negative-zero": "2.0.3", "is-regex": "1.2.1", "is-set": "2.0.3", "is-shared-array-buffer": "1.0.4", "is-string": "1.1.1", "is-typed-array": "1.1.15", "is-weakref": "1.1.1", "math-intrinsics": "1.1.0", "object-inspect": "1.13.4", "object-keys": "1.1.1", "object.assign": "4.1.7", "own-keys": "1.0.1", "regexp.prototype.flags": "1.5.4", "safe-array-concat": "1.1.3", "safe-push-apply": "1.0.0", "safe-regex-test": "1.1.0", "set-proto": "1.0.0", "stop-iteration-iterator": "1.1.0", "string.prototype.trim": "1.2.10", "string.prototype.trimend": "1.0.9", "string.prototype.trimstart": "1.0.8", "typed-array-buffer": "1.0.3", "typed-array-byte-length": "1.0.3", "typed-array-byte-offset": "1.0.4", "typed-array-length": "1.0.7", "unbox-primitive": "1.1.0", "which-typed-array": "1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "1.3.0", "get-intrinsic": "1.3.0", "has-tostringtag": "1.0.2", "hasown": "2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + + "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "1.2.7", "is-date-object": "1.1.0", "is-symbol": "1.1.1" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + + "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@10.1.0", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "@eslint-community/regexpp": "4.12.2", "@eslint/config-array": "0.23.3", "@eslint/config-helpers": "0.5.3", "@eslint/core": "1.1.1", "@eslint/plugin-kit": "0.6.1", "@humanfs/node": "0.16.7", "@humanwhocodes/module-importer": "1.0.1", "@humanwhocodes/retry": "0.4.3", "@types/estree": "1.0.8", "ajv": "6.14.0", "cross-spawn": "7.0.6", "debug": "4.4.3", "escape-string-regexp": "4.0.0", "eslint-scope": "9.1.2", "eslint-visitor-keys": "5.0.1", "espree": "11.2.0", "esquery": "1.7.0", "esutils": "2.0.3", "fast-deep-equal": "3.1.3", "file-entry-cache": "8.0.0", "find-up": "5.0.0", "glob-parent": "6.0.2", "ignore": "5.3.2", "imurmurhash": "0.1.4", "is-glob": "4.0.3", "json-stable-stringify-without-jsonify": "1.0.1", "minimatch": "10.2.4", "natural-compare": "1.4.0", "optionator": "0.9.4" }, "optionalDependencies": { "jiti": "2.6.1" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA=="], + + "eslint-import-context": ["eslint-import-context@0.1.9", "", { "dependencies": { "get-tsconfig": "4.13.0", "stable-hash-x": "0.2.0" }, "optionalDependencies": { "unrs-resolver": "1.11.1" } }, "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg=="], + + "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "3.2.7", "is-core-module": "2.16.1", "resolve": "1.22.11" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="], + + "eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@4.4.4", "", { "dependencies": { "debug": "4.4.3", "eslint-import-context": "0.1.9", "get-tsconfig": "4.13.0", "is-bun-module": "2.0.0", "stable-hash-x": "0.2.0", "tinyglobby": "0.2.15", "unrs-resolver": "1.11.1" }, "optionalDependencies": { "eslint-plugin-import": "2.32.0" }, "peerDependencies": { "eslint": "10.1.0" } }, "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw=="], + + "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "3.2.7" }, "optionalDependencies": { "@typescript-eslint/parser": "8.57.1", "eslint": "10.1.0", "eslint-import-resolver-node": "0.3.9", "eslint-import-resolver-typescript": "4.4.4" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], + + "eslint-plugin-codegen": ["eslint-plugin-codegen@0.34.1", "", { "dependencies": { "@babel/core": "7.28.5", "@babel/generator": "7.28.5", "@babel/parser": "7.28.5", "@babel/traverse": "7.28.5", "@babel/types": "7.28.5", "@pnpm/deps.graph-sequencer": "1.0.0", "@types/babel__core": "7.20.5", "@types/babel__generator": "7.27.0", "@types/dedent": "0.7.0", "@types/eslint": "8.56.12", "@types/glob": "7.1.3", "@types/js-yaml": "3.12.5", "@types/lodash": "4.17.21", "cheerio": "1.1.2", "dedent": "1.7.0", "eslint-plugin-markdown": "4.0.1", "expect": "29.7.0", "fp-ts": "2.16.11", "glob": "10.5.0", "io-ts": "2.2.22", "io-ts-extra": "0.11.6", "js-yaml": "3.14.2", "lodash": "4.17.21", "ms": "2.1.3", "read-pkg-up": "7.0.1", "recast": "0.23.11", "safe-stringify": "1.2.0", "strip-ansi": "6.0.1", "zod": "3.25.76", "zx": "8.8.5" } }, "sha512-Z9N+8eIP5G61Ta+kYf87h9fN8RkxtT6Kjy9goHVGeSgAPryPhcU2SrS4265z2qtKhrNlpSU6gYIcETMbUySfXg=="], + + "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "1.1.0", "array-includes": "3.1.9", "array.prototype.findlastindex": "1.2.6", "array.prototype.flat": "1.3.3", "array.prototype.flatmap": "1.3.3", "debug": "3.2.7", "doctrine": "2.1.0", "eslint-import-resolver-node": "0.3.9", "eslint-module-utils": "2.12.1", "hasown": "2.0.2", "is-core-module": "2.16.1", "is-glob": "4.0.3", "minimatch": "3.1.2", "object.fromentries": "2.0.8", "object.groupby": "1.0.3", "object.values": "1.2.1", "semver": "6.3.1", "string.prototype.trimend": "1.0.9", "tsconfig-paths": "3.15.0" }, "optionalDependencies": { "@typescript-eslint/parser": "8.57.1" }, "peerDependencies": { "eslint": "10.1.0" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], + + "eslint-plugin-markdown": ["eslint-plugin-markdown@4.0.1", "", { "dependencies": { "mdast-util-from-markdown": "0.8.5" }, "peerDependencies": { "eslint": "10.1.0" } }, "sha512-5/MnGvYU0i8MbHH5cg8S+Vl3DL+bqRNYshk1xUO86DilNBaxtTkhH+5FD0/yO03AmlI6+lfNFdk2yOw72EPzpA=="], + + "eslint-plugin-simple-import-sort": ["eslint-plugin-simple-import-sort@12.1.1", "", { "peerDependencies": { "eslint": "10.1.0" } }, "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA=="], + + "eslint-plugin-sonarjs": ["eslint-plugin-sonarjs@4.0.2", "", { "dependencies": { "@eslint-community/regexpp": "4.12.2", "builtin-modules": "3.3.0", "bytes": "3.1.2", "functional-red-black-tree": "1.0.1", "globals": "17.4.0", "jsx-ast-utils-x": "0.1.0", "lodash.merge": "4.6.2", "minimatch": "10.2.4", "scslre": "0.3.0", "semver": "7.7.4", "ts-api-utils": "2.4.0", "typescript": "5.9.3" }, "peerDependencies": { "eslint": "10.1.0" } }, "sha512-BTcT1zr1iTbmJtVlcesISwnXzh+9uhf9LEOr+RRNf4kR8xA0HQTPft4oiyOCzCOGKkpSJxjR8ZYF6H7VPyplyw=="], + + "eslint-plugin-sort-destructure-keys": ["eslint-plugin-sort-destructure-keys@3.0.0", "", { "dependencies": { "natural-compare-lite": "1.4.0" }, "peerDependencies": { "eslint": "10.1.0" } }, "sha512-ian2KEdGi8xZW50SVz9HIP9PDQN4XWeo3Hax3LsDk0ojL+wrwk40az8bKCnt3q2J7I3q5xF2ncZ0arj2q8Ou+A=="], + + "eslint-plugin-unicorn": ["eslint-plugin-unicorn@63.0.0", "", { "dependencies": { "@babel/helper-validator-identifier": "7.28.5", "@eslint-community/eslint-utils": "4.9.1", "change-case": "5.4.4", "ci-info": "4.3.1", "clean-regexp": "1.0.0", "core-js-compat": "3.47.0", "find-up-simple": "1.0.1", "globals": "16.5.0", "indent-string": "5.0.0", "is-builtin-module": "5.0.0", "jsesc": "3.1.0", "pluralize": "8.0.0", "regexp-tree": "0.1.27", "regjsparser": "0.13.0", "semver": "7.7.3", "strip-indent": "4.1.1" }, "peerDependencies": { "eslint": "10.1.0" } }, "sha512-Iqecl9118uQEXYh7adylgEmGfkn5es3/mlQTLLkd4pXkIk9CTGrAbeUux+YljSa2ohXCBmQQ0+Ej1kZaFgcfkA=="], + + "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "4.3.1", "@types/estree": "1.0.8", "esrecurse": "4.3.0", "estraverse": "5.3.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "8.16.0", "acorn-jsx": "5.3.2", "eslint-visitor-keys": "5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "5.3.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "5.3.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "1.0.8" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "execa": ["execa@4.1.0", "", { "dependencies": { "cross-spawn": "7.0.6", "get-stream": "5.2.0", "human-signals": "1.1.1", "is-stream": "2.0.1", "merge-stream": "2.0.0", "npm-run-path": "4.0.1", "onetime": "5.1.2", "signal-exit": "3.0.7", "strip-final-newline": "2.0.0" } }, "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA=="], + + "expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "29.7.0", "jest-get-type": "29.6.3", "jest-matcher-utils": "29.7.0", "jest-message-util": "29.7.0", "jest-util": "29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], + + "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "@nodelib/fs.walk": "1.2.8", "glob-parent": "5.1.2", "merge2": "1.4.1", "micromatch": "4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "1.1.0" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + + "fdir": ["fdir@6.5.0", "", { "optionalDependencies": { "picomatch": "4.0.3" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "4.0.1" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "6.0.0", "path-exists": "4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "find-up-simple": ["find-up-simple@1.0.1", "", {}, "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "3.3.3", "keyv": "4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "7.0.6", "signal-exit": "4.1.0" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "fp-ts": ["fp-ts@2.16.11", "", {}, "sha512-LaI+KaX2NFkfn1ZGHoKCmcfv7yrZsC3b8NtWsTVQeHkq4F27vI5igUuO53sxqDEa2gNQMHFPmpojDw/1zmUK7w=="], + + "fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "4.0.0", "universalify": "0.1.2" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "1.0.8", "call-bound": "1.0.4", "define-properties": "1.2.1", "functions-have-names": "1.2.3", "hasown": "2.0.2", "is-callable": "1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], + + "functional-red-black-tree": ["functional-red-black-tree@1.0.1", "", {}, "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "1.0.2", "es-define-property": "1.0.1", "es-errors": "1.3.0", "es-object-atoms": "1.1.1", "function-bind": "1.1.2", "get-proto": "1.0.1", "gopd": "1.2.0", "has-symbols": "1.1.0", "hasown": "2.0.2", "math-intrinsics": "1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "1.0.1", "es-object-atoms": "1.1.1" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "3.0.3" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "get-intrinsic": "1.3.0" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + + "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + + "gitignore-to-glob": ["gitignore-to-glob@0.3.0", "", {}, "sha512-mk74BdnK7lIwDHnotHddx1wsjMOFIThpLY3cPNniJ/2fA/tlLzHnFxIdR+4sLOu5KGgQJdij4kjJ2RoUNnCNMA=="], + + "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "3.3.1", "jackspeak": "3.4.3", "minimatch": "9.0.5", "minipass": "7.1.2", "package-json-from-dist": "1.0.1", "path-scurry": "1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@17.4.0", "", {}, "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "1.2.1", "gopd": "1.2.0" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "2.1.0", "dir-glob": "3.0.1", "fast-glob": "3.3.3", "ignore": "5.3.2", "merge2": "1.4.1", "slash": "3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "1.0.1" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "1.0.1" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "1.1.0" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="], + + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + + "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "2.3.0", "domhandler": "5.0.3", "domutils": "3.2.2", "entities": "6.0.1" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], + + "human-id": ["human-id@4.1.3", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q=="], + + "human-signals": ["human-signals@1.1.1", "", {}, "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw=="], + + "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": "2.1.2" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], + + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "1.0.1", "resolve-from": "4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], + + "ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="], + + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "1.3.0", "hasown": "2.0.2", "side-channel": "1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + + "io-ts": ["io-ts@2.2.22", "", { "peerDependencies": { "fp-ts": "2.16.11" } }, "sha512-FHCCztTkHoV9mdBsHpocLpdTAfh956ZQcIkWQxxS0U5HT53vtrcuYdQneEJKH6xILaLNzXVl2Cvwtoy8XNN0AA=="], + + "io-ts-extra": ["io-ts-extra@0.11.6", "", { "dependencies": { "fp-ts": "2.16.11", "io-ts": "2.2.22" } }, "sha512-rTsvx3W5B2nx7p/eGf+OsEaBTmjSjLzxBDEiweCjwqIL9ZN6CZjG7hFK8zyGJyM0I2uCsRU4uYUhaTgg2SKHkQ=="], + + "is-alphabetical": ["is-alphabetical@1.0.4", "", {}, "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="], + + "is-alphanumerical": ["is-alphanumerical@1.0.4", "", { "dependencies": { "is-alphabetical": "1.0.4", "is-decimal": "1.0.4" } }, "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A=="], + + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "1.0.8", "call-bound": "1.0.4", "get-intrinsic": "1.3.0" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "1.0.0", "call-bound": "1.0.4", "get-proto": "1.0.1", "has-tostringtag": "1.0.2", "safe-regex-test": "1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "1.1.0" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "1.0.4", "has-tostringtag": "1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-builtin-module": ["is-builtin-module@5.0.0", "", { "dependencies": { "builtin-modules": "5.0.0" } }, "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA=="], + + "is-bun-module": ["is-bun-module@2.0.0", "", { "dependencies": { "semver": "7.7.4" } }, "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "1.0.4", "get-intrinsic": "1.3.0", "is-typed-array": "1.1.15" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "1.0.4", "has-tostringtag": "1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + + "is-decimal": ["is-decimal@1.0.4", "", {}, "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw=="], + + "is-expression": ["is-expression@4.0.0", "", { "dependencies": { "acorn": "7.4.1", "object-assign": "4.1.1" } }, "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "1.0.4" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "1.0.4", "generator-function": "2.0.1", "get-proto": "1.0.1", "has-tostringtag": "1.0.2", "safe-regex-test": "1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-hexadecimal": ["is-hexadecimal@1.0.4", "", {}, "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="], + + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "1.0.4", "has-tostringtag": "1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + + "is-promise": ["is-promise@2.2.2", "", {}, "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="], + + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "1.0.4", "gopd": "1.2.0", "has-tostringtag": "1.0.2", "hasown": "2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "1.0.4" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "1.0.4", "has-tostringtag": "1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-subdir": ["is-subdir@1.2.0", "", { "dependencies": { "better-path-resolve": "1.0.0" } }, "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw=="], + + "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "1.0.4", "has-symbols": "1.1.0", "safe-regex-test": "1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "1.1.19" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "1.0.4" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], + + "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "1.0.4", "get-intrinsic": "1.3.0" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "3.2.2", "make-dir": "4.0.0", "supports-color": "7.2.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], + + "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "2.0.2", "istanbul-lib-report": "3.0.1" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], + + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "4.1.2", "diff-sequences": "29.6.3", "jest-get-type": "29.6.3", "pretty-format": "29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], + + "jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], + + "jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "4.1.2", "jest-diff": "29.7.0", "jest-get-type": "29.6.3", "pretty-format": "29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], + + "jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@jest/types": "29.6.3", "@types/stack-utils": "2.0.3", "chalk": "4.1.2", "graceful-fs": "4.2.11", "micromatch": "4.0.8", "pretty-format": "29.7.0", "slash": "3.0.0", "stack-utils": "2.0.6" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], + + "jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "29.6.3", "@types/node": "24.12.0", "chalk": "4.1.2", "ci-info": "3.9.0", "graceful-fs": "4.2.11", "picomatch": "2.3.1" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "js-stringify": ["js-stringify@1.0.2", "", {}, "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g=="], + + "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jscpd": ["jscpd@4.0.8", "", { "dependencies": { "@jscpd/badge-reporter": "4.0.4", "@jscpd/core": "4.0.4", "@jscpd/finder": "4.0.4", "@jscpd/html-reporter": "4.0.4", "@jscpd/tokenizer": "4.0.4", "colors": "1.4.0", "commander": "5.1.0", "fs-extra": "11.3.2", "gitignore-to-glob": "0.3.0", "jscpd-sarif-reporter": "4.0.6" }, "bin": { "jscpd": "bin/jscpd" } }, "sha512-d2VNT/2Hv4dxT2/59He8Lyda4DYOxPRyRG9zBaOpTZAqJCVf2xLrBlZkT8Va6Lo9u3X2qz8Bpq4HrDi4JsrQhA=="], + + "jscpd-sarif-reporter": ["jscpd-sarif-reporter@4.0.5", "", { "dependencies": { "colors": "1.4.0", "fs-extra": "11.3.2", "node-sarif-builder": "3.4.0" } }, "sha512-cD1MtUdpomUPM5C0YD0vKZmdj+Gyr0KD5Bk47yGMrPCtwtgsK+7v59OzBIUjYOL8AuxNAt6hvPFo0PH+PYJh0Q=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "4.2.11" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "jstransformer": ["jstransformer@1.0.0", "", { "dependencies": { "is-promise": "2.2.2", "promise": "7.3.1" } }, "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A=="], + + "jsx-ast-utils-x": ["jsx-ast-utils-x@0.1.0", "", {}, "sha512-eQQBjBnsVtGacsG9uJNB8qOr3yA8rga4wAaGG1qRcBzSIvfhERLrWxMAM1hp5fcS6Abo8M4+bUBTekYR0qTPQw=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "1.2.1", "type-check": "0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], + + "loop-controls": ["loop-controls@1.1.0", "", {}, "sha512-otnxF3ngIuLecg99p7On7nJF6ws1mT2kNOiGOPFykEHQfhJtdsjcQMxM4LEHsUi3LeMrm2Ic0hFdykJcG0N1YQ=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "3.1.1" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "7.29.0", "@babel/types": "7.29.0", "source-map-js": "1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="], + + "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "7.7.4" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + + "markdown-table": ["markdown-table@2.0.0", "", { "dependencies": { "repeat-string": "1.6.1" } }, "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A=="], + + "marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@0.8.5", "", { "dependencies": { "@types/mdast": "3.0.15", "mdast-util-to-string": "2.0.0", "micromark": "2.11.4", "parse-entities": "2.0.0", "unist-util-stringify-position": "2.0.3" } }, "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ=="], + + "mdast-util-to-string": ["mdast-util-to-string@2.0.0", "", {}, "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromark": ["micromark@2.11.4", "", { "dependencies": { "debug": "4.4.3", "parse-entities": "2.0.0" } }, "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "3.0.3", "picomatch": "2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "5.0.4" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "msgpackr": ["msgpackr@1.11.5", "", { "optionalDependencies": { "msgpackr-extract": "3.0.3" } }, "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA=="], + + "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], + + "multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "natural-compare-lite": ["natural-compare-lite@1.4.0", "", {}, "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g=="], + + "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "5.0.0" } }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "2.1.2" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], + + "node-pty": ["node-pty@1.1.0", "", { "dependencies": { "node-addon-api": "7.1.1" } }, "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg=="], + + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + + "node-sarif-builder": ["node-sarif-builder@3.4.0", "", { "dependencies": { "@types/sarif": "2.1.7", "fs-extra": "11.3.2" } }, "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg=="], + + "normalize-package-data": ["normalize-package-data@2.5.0", "", { "dependencies": { "hosted-git-info": "2.8.9", "resolve": "1.22.11", "semver": "5.7.2", "validate-npm-package-license": "3.0.4" } }, "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA=="], + + "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "3.1.1" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "1.0.8", "call-bound": "1.0.4", "define-properties": "1.2.1", "es-object-atoms": "1.1.1", "has-symbols": "1.1.0", "object-keys": "1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "1.0.8", "define-properties": "1.2.1", "es-abstract": "1.24.0", "es-object-atoms": "1.1.1" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "1.0.8", "define-properties": "1.2.1", "es-abstract": "1.24.0" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], + + "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "1.0.8", "call-bound": "1.0.4", "define-properties": "1.2.1", "es-object-atoms": "1.1.1" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1.0.2" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "0.1.4", "fast-levenshtein": "2.0.6", "levn": "0.4.1", "prelude-ls": "1.2.1", "type-check": "0.4.0", "word-wrap": "1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="], + + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "1.3.0", "object-keys": "1.1.1", "safe-push-apply": "1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + + "p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "2.1.0" } }, "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "3.1.0" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "0.2.11" } }, "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "3.1.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-entities": ["parse-entities@2.0.0", "", { "dependencies": { "character-entities": "1.2.4", "character-entities-legacy": "1.1.4", "character-reference-invalid": "1.1.4", "is-alphanumerical": "1.0.4", "is-decimal": "1.0.4", "is-hexadecimal": "1.0.4" } }, "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "7.27.1", "error-ex": "1.3.4", "json-parse-even-better-errors": "2.3.1", "lines-and-columns": "1.2.4" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "6.0.1" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "5.0.3", "parse5": "7.3.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], + + "parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "7.3.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="], + + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "10.4.3", "minipass": "7.1.2" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], + + "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "3.3.11", "picocolors": "1.1.1", "source-map-js": "1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + + "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "1.3.0" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="], + + "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "29.6.3", "ansi-styles": "5.2.0", "react-is": "18.3.1" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "promise": ["promise@7.3.1", "", { "dependencies": { "asap": "2.0.6" } }, "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg=="], + + "pug": ["pug@3.0.3", "", { "dependencies": { "pug-code-gen": "3.0.3", "pug-filters": "4.0.0", "pug-lexer": "5.0.1", "pug-linker": "4.0.0", "pug-load": "3.0.0", "pug-parser": "6.0.0", "pug-runtime": "3.0.1", "pug-strip-comments": "2.0.0" } }, "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g=="], + + "pug-attrs": ["pug-attrs@3.0.0", "", { "dependencies": { "constantinople": "4.0.1", "js-stringify": "1.0.2", "pug-runtime": "3.0.1" } }, "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA=="], + + "pug-code-gen": ["pug-code-gen@3.0.3", "", { "dependencies": { "constantinople": "4.0.1", "doctypes": "1.1.0", "js-stringify": "1.0.2", "pug-attrs": "3.0.0", "pug-error": "2.1.0", "pug-runtime": "3.0.1", "void-elements": "3.1.0", "with": "7.0.2" } }, "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw=="], + + "pug-error": ["pug-error@2.1.0", "", {}, "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg=="], + + "pug-filters": ["pug-filters@4.0.0", "", { "dependencies": { "constantinople": "4.0.1", "jstransformer": "1.0.0", "pug-error": "2.1.0", "pug-walk": "2.0.0", "resolve": "1.22.11" } }, "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A=="], + + "pug-lexer": ["pug-lexer@5.0.1", "", { "dependencies": { "character-parser": "2.2.0", "is-expression": "4.0.0", "pug-error": "2.1.0" } }, "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w=="], + + "pug-linker": ["pug-linker@4.0.0", "", { "dependencies": { "pug-error": "2.1.0", "pug-walk": "2.0.0" } }, "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw=="], + + "pug-load": ["pug-load@3.0.0", "", { "dependencies": { "object-assign": "4.1.1", "pug-walk": "2.0.0" } }, "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ=="], + + "pug-parser": ["pug-parser@6.0.0", "", { "dependencies": { "pug-error": "2.1.0", "token-stream": "1.0.0" } }, "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw=="], + + "pug-runtime": ["pug-runtime@3.0.1", "", {}, "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg=="], + + "pug-strip-comments": ["pug-strip-comments@2.0.0", "", { "dependencies": { "pug-error": "2.1.0" } }, "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ=="], + + "pug-walk": ["pug-walk@2.0.0", "", {}, "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ=="], + + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "1.4.5", "once": "1.4.0" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "0.27.0" }, "peerDependencies": { "react": "19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "0.27.0" }, "peerDependencies": { "react": "19.2.4" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="], + + "read-pkg": ["read-pkg@5.2.0", "", { "dependencies": { "@types/normalize-package-data": "2.4.4", "normalize-package-data": "2.5.0", "parse-json": "5.2.0", "type-fest": "0.6.0" } }, "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg=="], + + "read-pkg-up": ["read-pkg-up@7.0.1", "", { "dependencies": { "find-up": "4.1.0", "read-pkg": "5.2.0", "type-fest": "0.8.1" } }, "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg=="], + + "read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "4.2.11", "js-yaml": "3.14.2", "pify": "4.0.1", "strip-bom": "3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="], + + "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "0.16.1", "esprima": "4.0.1", "source-map": "0.6.1", "tiny-invariant": "1.3.3", "tslib": "2.8.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], + + "refa": ["refa@0.12.1", "", { "dependencies": { "@eslint-community/regexpp": "4.12.2" } }, "sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g=="], + + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "1.0.8", "define-properties": "1.2.1", "es-abstract": "1.24.0", "es-errors": "1.3.0", "es-object-atoms": "1.1.1", "get-intrinsic": "1.3.0", "get-proto": "1.0.1", "which-builtin-type": "1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + + "regexp-ast-analysis": ["regexp-ast-analysis@0.7.1", "", { "dependencies": { "@eslint-community/regexpp": "4.12.2", "refa": "0.12.1" } }, "sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A=="], + + "regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "1.0.8", "define-properties": "1.2.1", "es-errors": "1.3.0", "get-proto": "1.0.1", "gopd": "1.2.0", "set-function-name": "2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], + + "repeat-string": ["repeat-string@1.6.1", "", {}, "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w=="], + + "reprism": ["reprism@0.0.11", "", {}, "sha512-VsxDR5QxZo08M/3nRypNlScw5r3rKeSOPdU/QhDmu3Ai3BJxHn/qgfXGWQp/tAxUtzwYNo9W6997JZR0tPLZsA=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "2.16.1", "path-parse": "1.0.7", "supports-preserve-symlinks-flag": "1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rolldown": ["rolldown@1.0.0-rc.10", "", { "dependencies": { "@oxc-project/types": "0.120.0", "@rolldown/pluginutils": "1.0.0-rc.10" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-x64": "1.0.0-rc.10", "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "1.2.3" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "1.0.8", "call-bound": "1.0.4", "get-intrinsic": "1.3.0", "has-symbols": "1.1.0", "isarray": "2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "1.3.0", "isarray": "2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "is-regex": "1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + + "safe-stringify": ["safe-stringify@1.2.0", "", {}, "sha512-C+LbapLbyGhP/WeMTrnYhIPjUoNTXZ/A3Znli8D5iF+IZXrDlgvfruykOq/bZ/5ncGy/K6RsavHlkirgWDFNdA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "scslre": ["scslre@0.3.0", "", { "dependencies": { "@eslint-community/regexpp": "4.12.2", "refa": "0.12.1", "regexp-ast-analysis": "0.7.1" } }, "sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ=="], + + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "1.1.4", "es-errors": "1.3.0", "function-bind": "1.1.2", "get-intrinsic": "1.3.0", "gopd": "1.2.0", "has-property-descriptors": "1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "1.1.4", "es-errors": "1.3.0", "functions-have-names": "1.2.3", "has-property-descriptors": "1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "1.0.1", "es-errors": "1.3.0", "es-object-atoms": "1.1.1" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "1.3.0", "object-inspect": "1.13.4", "side-channel-list": "1.0.0", "side-channel-map": "1.0.1", "side-channel-weakmap": "1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "1.3.0", "object-inspect": "1.13.4" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "get-intrinsic": "1.3.0", "object-inspect": "1.13.4" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "get-intrinsic": "1.3.0", "object-inspect": "1.13.4", "side-channel-map": "1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "spark-md5": ["spark-md5@3.0.2", "", {}, "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw=="], + + "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "7.0.6", "signal-exit": "4.1.0" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="], + + "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "3.0.1", "spdx-license-ids": "3.0.22" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], + + "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], + + "spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "2.5.0", "spdx-license-ids": "3.0.22" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], + + "spdx-license-ids": ["spdx-license-ids@3.0.22", "", {}, "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "stable-hash-x": ["stable-hash-x@0.2.0", "", {}, "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ=="], + + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "1.3.0", "internal-slot": "1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "8.0.0", "is-fullwidth-code-point": "3.0.0", "strip-ansi": "6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "8.0.0", "is-fullwidth-code-point": "3.0.0", "strip-ansi": "6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "1.0.8", "call-bound": "1.0.4", "define-data-property": "1.1.4", "define-properties": "1.2.1", "es-abstract": "1.24.0", "es-object-atoms": "1.1.1", "has-property-descriptors": "1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "1.0.8", "call-bound": "1.0.4", "define-properties": "1.2.1", "es-object-atoms": "1.1.1" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "1.0.8", "define-properties": "1.2.1", "es-object-atoms": "1.1.1" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "strip-indent": ["strip-indent@4.1.1", "", {}, "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "6.5.0", "picomatch": "4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "token-stream": ["token-stream@1.0.0", "", {}, "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg=="], + + "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": "5.9.3" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + + "ts-morph": ["ts-morph@27.0.2", "", { "dependencies": { "@ts-morph/common": "0.28.1", "code-block-writer": "13.0.3" } }, "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w=="], + + "ts-pattern": ["ts-pattern@5.9.0", "", {}, "sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg=="], + + "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "0.0.29", "json5": "1.0.2", "minimist": "1.2.8", "strip-bom": "3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "0.27.2", "get-tsconfig": "4.13.0" }, "optionalDependencies": { "fsevents": "2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "type-fest": ["type-fest@0.8.1", "", {}, "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "is-typed-array": "1.1.15" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "1.0.8", "for-each": "0.3.5", "gopd": "1.2.0", "has-proto": "1.2.0", "is-typed-array": "1.1.15" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "1.0.7", "call-bind": "1.0.8", "for-each": "0.3.5", "gopd": "1.2.0", "has-proto": "1.2.0", "is-typed-array": "1.1.15", "reflect.getprototypeof": "1.0.10" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], + + "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "1.0.8", "for-each": "0.3.5", "gopd": "1.2.0", "is-typed-array": "1.1.15", "possible-typed-array-names": "1.1.0", "reflect.getprototypeof": "1.0.10" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "typescript-eslint": ["typescript-eslint@8.57.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.1", "@typescript-eslint/parser": "8.57.1", "@typescript-eslint/typescript-estree": "8.57.1", "@typescript-eslint/utils": "8.57.1" }, "peerDependencies": { "eslint": "10.1.0", "typescript": "5.9.3" } }, "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA=="], + + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "1.0.4", "has-bigints": "1.1.0", "has-symbols": "1.1.0", "which-boxed-primitive": "1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + + "undici": ["undici@7.16.0", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@2.0.3", "", { "dependencies": { "@types/unist": "2.0.11" } }, "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g=="], + + "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "0.3.4" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "3.2.0", "picocolors": "1.1.1" }, "peerDependencies": { "browserslist": "4.28.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "2.3.1" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + + "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "3.2.0", "spdx-expression-parse": "3.0.1" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], + + "vite": ["vite@8.0.1", "", { "dependencies": { "lightningcss": "1.32.0", "picomatch": "4.0.3", "postcss": "8.5.8", "rolldown": "1.0.0-rc.10", "tinyglobby": "0.2.15" }, "optionalDependencies": { "@types/node": "24.12.0", "esbuild": "0.27.2", "fsevents": "2.3.3", "jiti": "2.6.1", "tsx": "4.21.0", "yaml": "2.8.2" }, "bin": { "vite": "bin/vite.js" } }, "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw=="], + + "vitest": ["vitest@4.1.0", "", { "dependencies": { "@vitest/expect": "4.1.0", "@vitest/mocker": "4.1.0", "@vitest/pretty-format": "4.1.0", "@vitest/runner": "4.1.0", "@vitest/snapshot": "4.1.0", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "es-module-lexer": "2.0.0", "expect-type": "1.3.0", "magic-string": "0.30.21", "obug": "2.1.1", "pathe": "2.0.3", "picomatch": "4.0.3", "std-env": "4.0.0", "tinybench": "2.9.0", "tinyexec": "1.0.2", "tinyglobby": "0.2.15", "tinyrainbow": "3.0.3", "why-is-node-running": "2.3.0" }, "optionalDependencies": { "@types/node": "24.12.0" }, "peerDependencies": { "vite": "8.0.1" }, "bin": { "vitest": "vitest.mjs" } }, "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw=="], + + "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "0.0.3", "webidl-conversions": "3.0.1" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "1.1.0", "is-boolean-object": "1.2.2", "is-number-object": "1.1.1", "is-string": "1.1.1", "is-symbol": "1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "1.0.4", "function.prototype.name": "1.1.8", "has-tostringtag": "1.0.2", "is-async-function": "2.1.1", "is-date-object": "1.1.0", "is-finalizationregistry": "1.1.1", "is-generator-function": "1.1.2", "is-regex": "1.2.1", "is-weakref": "1.1.1", "isarray": "2.0.5", "which-boxed-primitive": "1.1.1", "which-collection": "1.0.2", "which-typed-array": "1.1.19" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "2.0.3", "is-set": "2.0.3", "is-weakmap": "2.0.2", "is-weakset": "2.0.4" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "1.0.7", "call-bind": "1.0.8", "call-bound": "1.0.4", "for-each": "0.3.5", "get-proto": "1.0.1", "gopd": "1.2.0", "has-tostringtag": "1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "with": ["with@7.0.2", "", { "dependencies": { "@babel/parser": "7.28.5", "@babel/types": "7.28.5", "assert-never": "1.4.0", "babel-walk": "3.0.0-canary-5" } }, "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "6.2.3", "string-width": "5.1.2", "strip-ansi": "7.1.2" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "4.3.0", "string-width": "4.2.3", "strip-ansi": "6.0.1" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "ws": ["ws@8.18.3", "", {}, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + + "xterm": ["xterm@5.3.0", "", {}, "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg=="], + + "xterm-addon-fit": ["xterm-addon-fit@0.8.0", "", { "peerDependencies": { "xterm": "5.3.0" } }, "sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "zx": ["zx@8.8.5", "", { "bin": { "zx": "build/cli.js" } }, "sha512-SNgDF5L0gfN7FwVOdEFguY3orU5AkfFZm9B5YSHog/UDHv+lvmd82ZAsOenOkQixigwH2+yyH198AwNdKhj+RA=="], + + "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@changesets/apply-release-plan/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@changesets/assemble-release-plan/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@changesets/get-dependents-graph/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "8.16.0", "acorn-jsx": "5.3.2", "eslint-visitor-keys": "4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "@eslint/eslintrc/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "1.1.12" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "0.2.0", "emoji-regex": "9.2.2", "strip-ansi": "7.1.2" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "6.2.2" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "@jscpd/badge-reporter/fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "6.2.0", "universalify": "2.0.1" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], + + "@jscpd/finder/fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "6.2.0", "universalify": "2.0.1" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], + + "@jscpd/html-reporter/fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "6.2.0", "universalify": "2.0.1" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], + + "@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], + + "@manypkg/find-root/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "5.0.0", "path-exists": "4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "4.0.0", "universalify": "0.1.2" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "@manypkg/get-packages/@changesets/types": ["@changesets/types@4.1.0", "", {}, "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw=="], + + "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "4.0.0", "universalify": "0.1.2" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], + + "@prover-coder-ai/dist-deps-prune/@effect/platform": ["@effect/platform@0.94.5", "", { "dependencies": { "find-my-way-ts": "0.1.6", "msgpackr": "1.11.5", "multipasta": "0.2.7" }, "peerDependencies": { "effect": "3.21.0" } }, "sha512-z05APUiDDPbodhTkH/RJqOLoCU11bU2IZLfcwLFrld03+ob1VeqRnELQlmueLIYm6NZifHAtjl32V+GRt34y4A=="], + + "@prover-coder-ai/dist-deps-prune/@effect/platform-node": ["@effect/platform-node@0.104.1", "", { "dependencies": { "@effect/platform-node-shared": "0.57.1", "mime": "3.0.0", "undici": "7.16.0", "ws": "8.18.3" }, "peerDependencies": { "@effect/cluster": "0.58.0", "@effect/platform": "0.94.5", "@effect/rpc": "0.75.0", "@effect/sql": "0.51.0", "effect": "3.21.0" } }, "sha512-jT1a/z98niK6fnEU8pWHPPCdJMVDRCIdB65lolcOjse5rsTwVbczMjvKkhVQpF63mNWoOnol7OTRNkw5L54llg=="], + + "@prover-coder-ai/eslint-plugin-suggest-members/@effect/platform": ["@effect/platform@0.94.5", "", { "dependencies": { "find-my-way-ts": "0.1.6", "msgpackr": "1.11.5", "multipasta": "0.2.7" }, "peerDependencies": { "effect": "3.21.0" } }, "sha512-z05APUiDDPbodhTkH/RJqOLoCU11bU2IZLfcwLFrld03+ob1VeqRnELQlmueLIYm6NZifHAtjl32V+GRt34y4A=="], + + "@prover-coder-ai/eslint-plugin-suggest-members/@effect/platform-node": ["@effect/platform-node@0.104.1", "", { "dependencies": { "@effect/platform-node-shared": "0.57.1", "mime": "3.0.0", "undici": "7.16.0", "ws": "8.18.3" }, "peerDependencies": { "@effect/cluster": "0.58.0", "@effect/platform": "0.94.5", "@effect/rpc": "0.75.0", "@effect/sql": "0.51.0", "effect": "3.21.0" } }, "sha512-jT1a/z98niK6fnEU8pWHPPCdJMVDRCIdB65lolcOjse5rsTwVbczMjvKkhVQpF63mNWoOnol7OTRNkw5L54llg=="], + + "@prover-coder-ai/eslint-plugin-suggest-members/@typescript-eslint/utils": ["@typescript-eslint/utils@8.55.0", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/types": "8.55.0", "@typescript-eslint/typescript-estree": "8.55.0" }, "peerDependencies": { "eslint": "10.1.0", "typescript": "5.9.3" } }, "sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow=="], + + "@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "1.7.1", "@emnapi/runtime": "1.7.1", "@tybys/wasm-util": "0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + + "@ton-ai-core/vibecode-linter/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "3.1.3", "fast-uri": "3.1.0", "json-schema-traverse": "1.0.0", "require-from-string": "2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "clean-regexp/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": "2.1.2" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-codegen/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "1.0.10", "esprima": "4.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + + "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "1.1.12" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "eslint-plugin-sonarjs/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "eslint-plugin-unicorn/globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], + + "execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "4.0.3" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "2.0.2" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "htmlparser2/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "is-builtin-module/builtin-modules": ["builtin-modules@5.0.0", "", {}, "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg=="], + + "is-bun-module/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "is-expression/acorn": ["acorn@7.4.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="], + + "jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + + "jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "jscpd/fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "6.2.0", "universalify": "2.0.1" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], + + "jscpd/jscpd-sarif-reporter": ["jscpd-sarif-reporter@4.0.6", "", { "dependencies": { "colors": "1.4.0", "fs-extra": "11.3.2", "node-sarif-builder": "3.4.0" } }, "sha512-b9Sm3IPZ3+m8Lwa4gZa+4/LhDhlc/ZLEsLXKSOy1DANQ6kx0ueqZT+fUHWEdQ6m0o3+RIVIa7DmvLSojQD05ng=="], + + "jscpd-sarif-reporter/fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "6.2.0", "universalify": "2.0.1" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], + + "magicast/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + + "magicast/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "7.27.1", "@babel/helper-validator-identifier": "7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "make-dir/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "node-sarif-builder/fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "6.2.0", "universalify": "2.0.1" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], + + "normalize-package-data/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "read-pkg/type-fest": ["type-fest@0.6.0", "", {}, "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg=="], + + "read-pkg-up/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "5.0.0", "path-exists": "4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "1.0.10", "esprima": "4.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + + "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.10", "", {}, "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg=="], + + "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + + "tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "1.2.8" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + + "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": "2.1.2" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "0.2.0", "emoji-regex": "9.2.2", "strip-ansi": "7.1.2" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "wrap-ansi/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "6.2.2" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "1.0.2", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "@jscpd/badge-reporter/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "2.0.1" }, "optionalDependencies": { "graceful-fs": "4.2.11" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + + "@jscpd/badge-reporter/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "@jscpd/finder/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "2.0.1" }, "optionalDependencies": { "graceful-fs": "4.2.11" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + + "@jscpd/finder/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "@jscpd/html-reporter/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "2.0.1" }, "optionalDependencies": { "graceful-fs": "4.2.11" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + + "@jscpd/html-reporter/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "@prover-coder-ai/dist-deps-prune/@effect/platform-node/@effect/platform-node-shared": ["@effect/platform-node-shared@0.57.1", "", { "dependencies": { "@parcel/watcher": "2.5.1", "multipasta": "0.2.7", "ws": "8.18.3" }, "peerDependencies": { "@effect/cluster": "0.58.0", "@effect/platform": "0.94.5", "@effect/rpc": "0.75.0", "@effect/sql": "0.51.0", "effect": "3.21.0" } }, "sha512-oX/bApMdoKsyrDiNdJxo7U9Rz1RXsjRv+ecfAPp1qGlSdGIo32wVRvJ2XCHqYj0sqaYJS0pU0/GCulRfVGuJag=="], + + "@prover-coder-ai/eslint-plugin-suggest-members/@effect/platform-node/@effect/platform-node-shared": ["@effect/platform-node-shared@0.57.1", "", { "dependencies": { "@parcel/watcher": "2.5.1", "multipasta": "0.2.7", "ws": "8.18.3" }, "peerDependencies": { "@effect/cluster": "0.58.0", "@effect/platform": "0.94.5", "@effect/rpc": "0.75.0", "@effect/sql": "0.51.0", "effect": "3.21.0" } }, "sha512-oX/bApMdoKsyrDiNdJxo7U9Rz1RXsjRv+ecfAPp1qGlSdGIo32wVRvJ2XCHqYj0sqaYJS0pU0/GCulRfVGuJag=="], + + "@prover-coder-ai/eslint-plugin-suggest-members/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.55.0", "", { "dependencies": { "@typescript-eslint/types": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0" } }, "sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q=="], + + "@prover-coder-ai/eslint-plugin-suggest-members/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.55.0", "", {}, "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w=="], + + "@prover-coder-ai/eslint-plugin-suggest-members/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.55.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.55.0", "@typescript-eslint/tsconfig-utils": "8.55.0", "@typescript-eslint/types": "8.55.0", "@typescript-eslint/visitor-keys": "8.55.0", "debug": "4.4.3", "minimatch": "9.0.5", "semver": "7.7.4", "tinyglobby": "0.2.15", "ts-api-utils": "2.4.0" }, "peerDependencies": { "typescript": "5.9.3" } }, "sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw=="], + + "@ton-ai-core/vibecode-linter/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "eslint-plugin-codegen/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "1.0.3" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "eslint-plugin-import/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "1.0.2", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "1.0.2" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "jscpd-sarif-reporter/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "2.0.1" }, "optionalDependencies": { "graceful-fs": "4.2.11" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + + "jscpd-sarif-reporter/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "jscpd/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "2.0.1" }, "optionalDependencies": { "graceful-fs": "4.2.11" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + + "jscpd/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "node-sarif-builder/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "2.0.1" }, "optionalDependencies": { "graceful-fs": "4.2.11" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + + "node-sarif-builder/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "read-pkg-up/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "1.0.3" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "@eslint/eslintrc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "@manypkg/find-root/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "2.3.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "@prover-coder-ai/eslint-plugin-suggest-members/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.55.0", "", { "dependencies": { "@typescript-eslint/types": "8.55.0", "eslint-visitor-keys": "4.2.1" } }, "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA=="], + + "@prover-coder-ai/eslint-plugin-suggest-members/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.55.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "8.57.0", "@typescript-eslint/types": "8.57.1", "debug": "4.4.3" }, "peerDependencies": { "typescript": "5.9.3" } }, "sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ=="], + + "@prover-coder-ai/eslint-plugin-suggest-members/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.55.0", "", { "peerDependencies": { "typescript": "5.9.3" } }, "sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q=="], + + "@prover-coder-ai/eslint-plugin-suggest-members/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.55.0", "", { "dependencies": { "@typescript-eslint/types": "8.55.0", "eslint-visitor-keys": "4.2.1" } }, "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA=="], + + "@prover-coder-ai/eslint-plugin-suggest-members/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "2.0.2" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@prover-coder-ai/eslint-plugin-suggest-members/@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "eslint-plugin-import/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "read-pkg-up/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "2.3.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "@manypkg/find-root/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "2.2.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "@prover-coder-ai/eslint-plugin-suggest-members/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "@prover-coder-ai/eslint-plugin-suggest-members/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.0", "", { "peerDependencies": { "typescript": "5.9.3" } }, "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA=="], + + "@prover-coder-ai/eslint-plugin-suggest-members/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.57.1", "", {}, "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ=="], + + "@prover-coder-ai/eslint-plugin-suggest-members/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "@prover-coder-ai/eslint-plugin-suggest-members/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "1.0.2" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "2.2.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "@prover-coder-ai/eslint-plugin-suggest-members/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + } +} diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 00000000..6e536f29 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[install] +linkWorkspacePackages = true diff --git a/ctl b/ctl index 5e2b59f3..44b2846a 100755 --- a/ctl +++ b/ctl @@ -7,7 +7,7 @@ # FORMAT THEOREM: forall cmd: valid(cmd) -> controller_action(cmd) terminates # PURITY: SHELL # EFFECT: Effect -# INVARIANT: every API request is executed from inside the controller container; host does not need curl/node/pnpm +# INVARIANT: every API request is executed from inside the controller container; host does not need curl or a package manager # COMPLEXITY: O(1) + network/docker set -euo pipefail @@ -68,7 +68,7 @@ normalize_api_path() { fi local normalized - normalized="$("${DOCKER_CMD[@]}" exec -i "$CONTAINER_NAME" node - "$raw_path" <<'NODE' + normalized="$("${DOCKER_CMD[@]}" exec -i "$CONTAINER_NAME" bun - "$raw_path" <<'NODE' const raw = process.argv[2] ?? "" const [pathname, query = ""] = raw.split(/\?(.*)/s, 2) const prefix = "/projects/" diff --git a/flake.nix b/flake.nix index 50ca0dcc..06d736eb 100644 --- a/flake.nix +++ b/flake.nix @@ -11,7 +11,7 @@ devShells = forAllSystems (pkgs: { default = pkgs.mkShell { packages = with pkgs; [ - corepack + bun nodejs_22 # For systems that do not ship with Python by default (required by `node-gyp`) python3 diff --git a/package.json b/package.json index 7ae1aa64..1b2837f9 100644 --- a/package.json +++ b/package.json @@ -1,65 +1,66 @@ { - "name": "effect-template-workspace", + "name": "docker-git-workspace", "version": "1.0.0", "private": true, - "description": "Monorepo workspace for effect-template", - "packageManager": "pnpm@10.32.1", + "description": "Monorepo workspace for docker-git", + "packageManager": "bun@1.3.11", "workspaces": [ "packages/api", "packages/app", "packages/lib" ], "scripts": { - "setup:pre-commit-hook": "node scripts/setup-pre-commit-hook.js", - "build": "pnpm --filter ./packages/app build", - "api:build": "pnpm --filter ./packages/api build", - "api:start": "pnpm --filter ./packages/api start", - "api:dev": "pnpm --filter ./packages/api dev", - "api:test": "pnpm --filter ./packages/api test", - "api:typecheck": "pnpm --filter ./packages/api typecheck", - "check": "pnpm --filter ./packages/app check && pnpm --filter ./packages/lib typecheck", + "setup:pre-commit-hook": "bun scripts/setup-pre-commit-hook.js", + "build": "bun run --filter @prover-coder-ai/docker-git build", + "api:build": "bun run --filter @effect-template/api build", + "api:start": "bun run --filter @effect-template/api start", + "api:dev": "bun run --filter @effect-template/api dev", + "api:test": "bun run --filter @effect-template/api test", + "api:typecheck": "bun run --filter @effect-template/api typecheck", + "check": "bun run --filter @prover-coder-ai/docker-git check && bun run --filter @effect-template/lib typecheck", + "check:dist-deps-prune": "bun node_modules/@prover-coder-ai/dist-deps-prune/dist/main.js scan --package ./packages/app/package.json --prune-dev true --silent", "changeset": "changeset", - "changeset-publish": "node -e \"if (!process.env.NPM_TOKEN) { console.log('Skipping publish: NPM_TOKEN is not set'); process.exit(0); }\" && changeset publish", + "changeset-publish": "bun -e \"if (!process.env.NPM_TOKEN) { console.log('Skipping publish: NPM_TOKEN is not set'); process.exit(0); }\" && changeset publish", "changeset-version": "changeset version", - "clone": "pnpm --filter ./packages/app build && node packages/app/dist/main.js clone", - "open": "pnpm --filter ./packages/app build && node packages/app/dist/main.js open", - "docker-git": "pnpm --filter ./packages/app build:docker-git && node packages/app/dist/src/docker-git/main.js", + "clone": "bun run --filter @prover-coder-ai/docker-git clone", + "open": "bun run --filter @prover-coder-ai/docker-git open", + "docker-git": "bun run --filter @prover-coder-ai/docker-git docker-git", "e2e": "bash scripts/e2e/run-all.sh", "e2e:clone-cache": "bash scripts/e2e/clone-cache.sh", "e2e:login-context": "bash scripts/e2e/login-context.sh", "e2e:runtime-volumes-ssh": "bash scripts/e2e/runtime-volumes-ssh.sh", "e2e:opencode-autoconnect": "bash scripts/e2e/opencode-autoconnect.sh", - "list": "pnpm --filter ./packages/app build && node packages/app/dist/main.js list", - "dev": "pnpm --filter ./packages/app dev", - "lint": "pnpm --filter ./packages/app lint && pnpm --filter ./packages/lib lint", - "lint:tests": "pnpm --filter ./packages/app lint:tests", - "lint:effect": "pnpm --filter ./packages/app lint:effect && pnpm --filter ./packages/lib lint:effect", - "test": "pnpm --filter ./packages/app test && pnpm --filter ./packages/lib test", - "typecheck": "pnpm --filter ./packages/app typecheck && pnpm --filter ./packages/lib typecheck", - "start": "pnpm --filter ./packages/app start" + "list": "bun run --filter @prover-coder-ai/docker-git list", + "dev": "bun run --filter @prover-coder-ai/docker-git dev", + "web:dev": "bun run --filter @prover-coder-ai/docker-git dev:web", + "web:build": "bun run --filter @prover-coder-ai/docker-git build:web", + "web:preview": "bun run --filter @prover-coder-ai/docker-git preview:web", + "web:serve": "bun run --filter @prover-coder-ai/docker-git serve:web", + "lint": "bun run --filter @prover-coder-ai/docker-git lint && bun run --filter @effect-template/lib lint", + "lint:tests": "bun run --filter @prover-coder-ai/docker-git lint:tests", + "lint:effect": "bun run --filter @prover-coder-ai/docker-git lint:effect && bun run --filter @effect-template/lib lint:effect", + "test": "bun run --filter @prover-coder-ai/docker-git test && bun run --filter @effect-template/lib test", + "typecheck": "bun run --filter @prover-coder-ai/docker-git typecheck && bun run --filter @effect-template/lib typecheck", + "start": "bun run --filter @prover-coder-ai/docker-git start" }, "devDependencies": { "@changesets/changelog-github": "^0.6.0", - "@changesets/cli": "^2.30.0" + "@changesets/cli": "^2.30.0", + "@prover-coder-ai/dist-deps-prune": "^1.0.17" }, + "trustedDependencies": [ + "@parcel/watcher", + "msgpackr-extract", + "node-pty", + "unrs-resolver" + ], "repository": { "type": "git", - "url": "git+https://github.com/ProverCoderAI/effect-template.git" + "url": "git+https://github.com/ProverCoderAI/docker-git.git" }, "bugs": { - "url": "https://github.com/ProverCoderAI/effect-template/issues" + "url": "https://github.com/ProverCoderAI/docker-git/issues" }, - "homepage": "https://github.com/ProverCoderAI/effect-template#readme", - "license": "ISC", - "pnpm": { - "ignoredBuiltDependencies": [ - "esbuild", - "node-pty" - ], - "onlyBuiltDependencies": [ - "@parcel/watcher", - "msgpackr-extract", - "unrs-resolver" - ] - } + "homepage": "https://github.com/ProverCoderAI/docker-git#readme", + "license": "ISC" } diff --git a/packages/api/Dockerfile b/packages/api/Dockerfile index e9e76599..c616778d 100644 --- a/packages/api/Dockerfile +++ b/packages/api/Dockerfile @@ -5,27 +5,30 @@ LABEL io.prover-coder-ai.docker-git.controller-rev=$DOCKER_GIT_CONTROLLER_REV ENV DEBIAN_FRONTEND=noninteractive ENV DOCKER_GIT_CONTROLLER_REV=$DOCKER_GIT_CONTROLLER_REV +ENV BUN_INSTALL=/opt/bun +ENV PATH=/opt/bun/bin:$PATH WORKDIR /workspace RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates curl git docker.io docker-compose-v2 sshpass \ + ca-certificates curl git docker.io docker-compose-v2 openssh-client sshpass python3 make g++ unzip \ && rm -rf /var/lib/apt/lists/* RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ - && npm i -g pnpm@10.28.0 \ + && curl -fsSL https://bun.sh/install | bash \ + && npm i -g node-gyp \ && rm -rf /var/lib/apt/lists/* -COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json tsconfig.json ./ +COPY package.json bun.lock bunfig.toml tsconfig.base.json tsconfig.json ./ COPY patches ./patches COPY scripts ./scripts COPY packages ./packages -RUN pnpm install --frozen-lockfile -RUN pnpm --filter ./packages/lib build -RUN pnpm --filter ./packages/api build +RUN bun install --frozen-lockfile --silent +RUN bun run --cwd packages/lib build +RUN bun run --cwd packages/api build ENV DOCKER_GIT_API_PORT=3334 EXPOSE 3334 -CMD ["node", "packages/api/dist/src/main.js"] +CMD ["bun", "packages/api/dist/src/main.js"] diff --git a/packages/api/README.md b/packages/api/README.md index 6be1d4b5..e1bcb2c5 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -1,4 +1,4 @@ -# @effect-template/api +# docker-git API HTTP API for docker-git orchestration (projects, agents, logs/events, federation). @@ -19,8 +19,8 @@ This page is a built-in UI shell for manual API checks without CLI. ## Run (local) ```bash -pnpm --filter ./packages/api build -pnpm --filter ./packages/api start +bun run --cwd packages/api build +bun run --cwd packages/api start ``` ## Run (dedicated Docker for API) diff --git a/packages/api/package.json b/packages/api/package.json index 815dfa1e..a9f3ed96 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -2,19 +2,20 @@ "name": "@effect-template/api", "version": "0.1.0", "private": true, - "description": "docker-git clean-slate v1 API", + "description": "docker-git API controller", "main": "dist/src/main.js", "type": "module", + "packageManager": "bun@1.3.11", "scripts": { - "prebuild": "pnpm -C ../lib build", + "prebuild": "bun run --cwd ../lib build", "build": "tsc -p tsconfig.json", "dev": "tsc -p tsconfig.json --watch", - "prestart": "pnpm run build", - "start": "node dist/src/main.js", - "pretypecheck": "pnpm -C ../lib build", + "prestart": "bun run build", + "start": "bun dist/src/main.js", + "pretypecheck": "bun run --cwd ../lib build", "typecheck": "tsc --noEmit -p tsconfig.json", "lint": "eslint .", - "pretest": "pnpm -C ../lib build", + "pretest": "bun run --cwd ../lib build", "test": "vitest run" }, "dependencies": { @@ -22,12 +23,23 @@ "@effect/platform": "^0.96.0", "@effect/platform-node": "^0.106.0", "@effect/schema": "^0.75.5", - "effect": "^3.21.0" + "effect": "^3.21.0", + "node-pty": "^1.0.0", + "ws": "^8.18.3" }, + "repository": { + "type": "git", + "url": "git+https://github.com/ProverCoderAI/docker-git.git" + }, + "bugs": { + "url": "https://github.com/ProverCoderAI/docker-git/issues" + }, + "homepage": "https://github.com/ProverCoderAI/docker-git#readme", "devDependencies": { "@effect/vitest": "^0.29.0", "@eslint/js": "10.0.1", "@types/node": "^24.12.0", + "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.57.1", "@typescript-eslint/parser": "^8.57.1", "eslint": "^10.1.0", diff --git a/packages/api/src/api/contracts.ts b/packages/api/src/api/contracts.ts index 1d37466b..27037339 100644 --- a/packages/api/src/api/contracts.ts +++ b/packages/api/src/api/contracts.ts @@ -11,6 +11,10 @@ export type ProjectSummary = { readonly repoRef: string readonly status: ProjectStatus readonly statusLabel: string + readonly sshSessions: number + readonly startedAtIso: string | null + readonly startedAtEpochMs: number | null + readonly clonedOnHostname?: string | undefined } export type ProjectDetails = ProjectSummary & { @@ -21,11 +25,12 @@ export type ProjectDetails = ProjectSummary & { readonly targetDir: string readonly projectDir: string readonly sshCommand: string + readonly authorizedKeysPath: string + readonly authorizedKeysExists: boolean readonly envGlobalPath: string readonly envProjectPath: string readonly codexAuthPath: string readonly codexHome: string - readonly clonedOnHostname?: string | undefined } export type GithubAuthTokenStatus = { @@ -46,6 +51,41 @@ export type GithubAuthLoginRequest = { readonly scopes?: string | null | undefined } +export type AuthMenuFlow = + | "GithubRemove" + | "GitSet" + | "GitRemove" + | "ClaudeLogout" + | "GeminiApiKey" + | "GeminiLogout" + +export type AuthTerminalFlow = "ClaudeOauth" | "GeminiOauth" + +export type AuthSnapshot = { + readonly globalEnvPath: string + readonly claudeAuthPath: string + readonly geminiAuthPath: string + readonly totalEntries: number + readonly githubTokenEntries: number + readonly gitTokenEntries: number + readonly gitUserEntries: number + readonly claudeAuthEntries: number + readonly geminiAuthEntries: number +} + +export type AuthMenuRequest = { + readonly flow: AuthMenuFlow + readonly label?: string | null | undefined + readonly token?: string | null | undefined + readonly user?: string | null | undefined + readonly apiKey?: string | null | undefined +} + +export type AuthTerminalSessionRequest = { + readonly flow: AuthTerminalFlow + readonly label?: string | null | undefined +} + export type GithubAuthLogoutRequest = { readonly label?: string | null | undefined } @@ -71,6 +111,38 @@ export type CodexAuthLogoutRequest = { readonly label?: string | null | undefined } +export type ProjectAuthFlow = + | "ProjectGithubConnect" + | "ProjectGithubDisconnect" + | "ProjectGitConnect" + | "ProjectGitDisconnect" + | "ProjectClaudeConnect" + | "ProjectClaudeDisconnect" + | "ProjectGeminiConnect" + | "ProjectGeminiDisconnect" + +export type ProjectAuthSnapshot = { + readonly projectDir: string + readonly projectName: string + readonly envGlobalPath: string + readonly envProjectPath: string + readonly claudeAuthPath: string + readonly geminiAuthPath: string + readonly githubTokenEntries: number + readonly gitTokenEntries: number + readonly claudeAuthEntries: number + readonly geminiAuthEntries: number + readonly activeGithubLabel: string | null + readonly activeGitLabel: string | null + readonly activeClaudeLabel: string | null + readonly activeGeminiLabel: string | null +} + +export type ProjectAuthRequest = { + readonly flow: ProjectAuthFlow + readonly label?: string | null | undefined +} + export type StateInitRequest = { readonly repoUrl: string readonly repoRef?: string | undefined @@ -90,6 +162,7 @@ export type ApplyAllRequest = { export type UpProjectRequest = { readonly authorizedKeysContents?: string | undefined + readonly useManagedAuthorizedKeys?: boolean | undefined } export type ApiAuthRequired = { @@ -110,6 +183,7 @@ export type CreateProjectRequest = { readonly secretsRoot?: string | undefined readonly authorizedKeysPath?: string | undefined readonly authorizedKeysContents?: string | undefined + readonly useManagedAuthorizedKeys?: boolean | undefined readonly envGlobalPath?: string | undefined readonly envProjectPath?: string | undefined readonly codexAuthPath?: string | undefined @@ -179,6 +253,20 @@ export type AgentAttachInfo = { readonly shellCommand: string } +export type TerminalSessionStatus = "ready" | "attached" | "exited" | "failed" + +export type TerminalSession = { + readonly id: string + readonly projectId: string + readonly sshCommand: string + readonly status: TerminalSessionStatus + readonly createdAt: string + readonly startedAt?: string | undefined + readonly closedAt?: string | undefined + readonly exitCode?: number | undefined + readonly signal?: number | undefined +} + export type ForgeFedTicket = { readonly id: string readonly attributedTo: string @@ -288,6 +376,7 @@ export type ApiEventType = | "project.deleted" | "project.deployment.status" | "project.deployment.log" + | "project.ssh.session" | "agent.started" | "agent.output" | "agent.exited" diff --git a/packages/api/src/api/schema.ts b/packages/api/src/api/schema.ts index 4bbb8374..d3b6b1ea 100644 --- a/packages/api/src/api/schema.ts +++ b/packages/api/src/api/schema.ts @@ -16,6 +16,7 @@ export const CreateProjectRequestSchema = Schema.Struct({ secretsRoot: OptionalString, authorizedKeysPath: OptionalString, authorizedKeysContents: OptionalString, + useManagedAuthorizedKeys: OptionalBoolean, envGlobalPath: OptionalString, envProjectPath: OptionalString, codexAuthPath: OptionalString, @@ -44,6 +45,30 @@ export const GithubAuthLoginRequestSchema = Schema.Struct({ scopes: OptionalNullableString }) +export const AuthMenuFlowSchema = Schema.Literal( + "GithubRemove", + "GitSet", + "GitRemove", + "ClaudeLogout", + "GeminiApiKey", + "GeminiLogout" +) + +export const AuthTerminalFlowSchema = Schema.Literal("ClaudeOauth", "GeminiOauth") + +export const AuthMenuRequestSchema = Schema.Struct({ + flow: AuthMenuFlowSchema, + label: OptionalNullableString, + token: OptionalNullableString, + user: OptionalNullableString, + apiKey: OptionalNullableString +}) + +export const AuthTerminalSessionRequestSchema = Schema.Struct({ + flow: AuthTerminalFlowSchema, + label: OptionalNullableString +}) + export const GithubAuthLogoutRequestSchema = Schema.Struct({ label: OptionalNullableString }) @@ -61,6 +86,22 @@ export const CodexAuthLogoutRequestSchema = Schema.Struct({ label: OptionalNullableString }) +export const ProjectAuthFlowSchema = Schema.Literal( + "ProjectGithubConnect", + "ProjectGithubDisconnect", + "ProjectGitConnect", + "ProjectGitDisconnect", + "ProjectClaudeConnect", + "ProjectClaudeDisconnect", + "ProjectGeminiConnect", + "ProjectGeminiDisconnect" +) + +export const ProjectAuthRequestSchema = Schema.Struct({ + flow: ProjectAuthFlowSchema, + label: OptionalNullableString +}) + export const StateInitRequestSchema = Schema.Struct({ repoUrl: Schema.String, repoRef: OptionalString @@ -79,7 +120,8 @@ export const ApplyAllRequestSchema = Schema.Struct({ }) export const UpProjectRequestSchema = Schema.Struct({ - authorizedKeysContents: OptionalString + authorizedKeysContents: OptionalString, + useManagedAuthorizedKeys: OptionalBoolean }) export const AgentProviderSchema = Schema.Literal("codex", "opencode", "claude", "custom") @@ -131,12 +173,29 @@ export const AgentLogLineSchema = Schema.Struct({ line: Schema.String }) +export const TerminalSessionStatusSchema = Schema.Literal("ready", "attached", "exited", "failed") + +export const TerminalSessionSchema = Schema.Struct({ + id: Schema.String, + projectId: Schema.String, + sshCommand: Schema.String, + status: TerminalSessionStatusSchema, + createdAt: Schema.String, + startedAt: OptionalString, + closedAt: OptionalString, + exitCode: Schema.optional(Schema.Number), + signal: Schema.optional(Schema.Number) +}) + export type CreateProjectRequestInput = Schema.Schema.Type export type GithubAuthLoginRequestInput = Schema.Schema.Type +export type AuthMenuRequestInput = Schema.Schema.Type +export type AuthTerminalSessionRequestInput = Schema.Schema.Type export type GithubAuthLogoutRequestInput = Schema.Schema.Type export type CodexAuthImportRequestInput = Schema.Schema.Type export type CodexAuthLoginRequestInput = Schema.Schema.Type export type CodexAuthLogoutRequestInput = Schema.Schema.Type +export type ProjectAuthRequestInput = Schema.Schema.Type export type StateInitRequestInput = Schema.Schema.Type export type StateCommitRequestInput = Schema.Schema.Type export type StateSyncRequestInput = Schema.Schema.Type diff --git a/packages/api/src/auth-terminal-runner.ts b/packages/api/src/auth-terminal-runner.ts new file mode 100644 index 00000000..2b624d6d --- /dev/null +++ b/packages/api/src/auth-terminal-runner.ts @@ -0,0 +1,35 @@ +import { NodeContext, NodeRuntime } from "@effect/platform-node" +import { authClaudeLogin, authGeminiLoginOauth } from "@effect-template/lib" +import { Effect, Match } from "effect" + +type AuthTerminalRunnerFlow = "ClaudeOauth" | "GeminiOauth" + +const parseFlow = (value: string | undefined): AuthTerminalRunnerFlow => + value === "ClaudeOauth" || value === "GeminiOauth" ? value : "ClaudeOauth" + +const parseLabel = (value: string | undefined): string | null => { + const trimmed = value?.trim() ?? "" + return trimmed.length === 0 ? null : trimmed +} + +const flow = parseFlow(process.argv[2]) +const label = parseLabel(process.argv[3]) + +const program = Match.value(flow).pipe( + Match.when("ClaudeOauth", () => + authClaudeLogin({ + _tag: "AuthClaudeLogin", + label, + claudeAuthPath: ".docker-git/.orch/auth/claude" + })), + Match.when("GeminiOauth", () => + authGeminiLoginOauth({ + _tag: "AuthGeminiLogin", + label, + geminiAuthPath: ".docker-git/.orch/auth/gemini", + isWeb: false + })), + Match.exhaustive +) + +NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer))) diff --git a/packages/api/src/http.ts b/packages/api/src/http.ts index 158dcfa0..3c04805c 100644 --- a/packages/api/src/http.ts +++ b/packages/api/src/http.ts @@ -12,6 +12,8 @@ import { renderError, type AppError } from "@effect-template/lib/usecases/errors import { ApiAuthRequiredError, ApiBadRequestError, ApiConflictError, ApiInternalError, ApiNotFoundError, describeUnknown } from "./api/errors.js" import { + AuthMenuRequestSchema, + AuthTerminalSessionRequestSchema, ApplyAllRequestSchema, CodexAuthImportRequestSchema, CodexAuthLoginRequestSchema, @@ -21,20 +23,26 @@ import { CreateProjectRequestSchema, GithubAuthLoginRequestSchema, GithubAuthLogoutRequestSchema, + ProjectAuthRequestSchema, StateCommitRequestSchema, StateInitRequestSchema, StateSyncRequestSchema, UpProjectRequestSchema } from "./api/schema.js" +import type { UpProjectRequestInput } from "./api/schema.js" import { uiHtml, uiScript, uiStyles } from "./ui.js" +import { defaultProjectsRoot } from "@effect-template/lib/usecases/menu-helpers" +import { resolveWorkspaceRoot } from "@effect-template/lib/shell/workspace-root" import { importCodexAuth, loginGithubAuth, logoutCodexAuth, logoutGithubAuth, readCodexAuthStatus, - readGithubAuthStatus + readGithubAuthStatus, } from "./services/auth.js" +import { readAuthMenuSnapshot, runAuthMenuFlow } from "./services/auth-menu.js" +import { createAuthTerminalSession, deleteAuthTerminalSession } from "./services/auth-terminal-sessions.js" import { streamCodexAuthLogin } from "./services/auth-codex-login-stream.js" import { getAgent, getAgentAttachInfo, listAgents, readAgentLogs, startAgent, stopAgent } from "./services/agents.js" import { latestProjectCursor, listProjectEventsSince } from "./services/events.js" @@ -63,6 +71,8 @@ import { recreateProject, upProject } from "./services/projects.js" +import { readProjectAuthSnapshot, runProjectAuthFlow } from "./services/project-auth.js" +import { createTerminalSession, deleteTerminalSession } from "./services/terminal-sessions.js" import { commitStateFromRequest, initStateFromRequest, @@ -82,6 +92,15 @@ const AgentParamsSchema = Schema.Struct({ agentId: Schema.String }) +const TerminalSessionParamsSchema = Schema.Struct({ + projectId: Schema.String, + sessionId: Schema.String +}) + +const AuthTerminalSessionParamsSchema = Schema.Struct({ + sessionId: Schema.String +}) + type ApiError = | ApiAuthRequiredError | ApiBadRequestError @@ -93,6 +112,11 @@ type ApiError = | HttpServerError.RequestError | PlatformError +const noStoreHeaders = { + "cache-control": "no-store, no-cache, must-revalidate, max-age=0", + pragma: "no-cache" +} + const appErrorTags = new Set([ "FileExistsError", "CloneFailedError", @@ -120,12 +144,15 @@ const isAppError = (error: unknown): error is AppError => appErrorTags.has(error["_tag"]) const jsonResponse = (data: unknown, status: number) => - Effect.map(HttpServerResponse.json(data), (response) => HttpServerResponse.setStatus(response, status)) + Effect.map( + HttpServerResponse.json(data, { headers: noStoreHeaders }), + (response) => HttpServerResponse.setStatus(response, status) + ) const textResponse = (data: string, contentType: string, status = 200) => Effect.succeed( HttpServerResponse.setStatus( - HttpServerResponse.text(data, { contentType }), + HttpServerResponse.text(data, { contentType, headers: noStoreHeaders }), status ) ) @@ -138,6 +165,9 @@ const parseQueryInt = (url: string, key: string, fallback: number): number => { return Math.floor(parsed) } +const hasQueryParam = (url: string, key: string): boolean => + new URL(url, "http://localhost").searchParams.has(key) + const errorResponse = (error: ApiError | unknown) => { if (ParseResult.isParseError(error)) { return jsonResponse( @@ -198,21 +228,27 @@ const errorResponse = (error: ApiError | unknown) => { const projectParams = HttpRouter.schemaParams(ProjectParamsSchema) const agentParams = HttpRouter.schemaParams(AgentParamsSchema) +const terminalSessionParams = HttpRouter.schemaParams(TerminalSessionParamsSchema) +const authTerminalSessionParams = HttpRouter.schemaParams(AuthTerminalSessionParamsSchema) const readCreateProjectRequest = () => HttpServerRequest.schemaBodyJson(CreateProjectRequestSchema) const readCreateFollowRequest = () => HttpServerRequest.schemaBodyJson(CreateFollowRequestSchema) const readGithubAuthLoginRequest = () => HttpServerRequest.schemaBodyJson(GithubAuthLoginRequestSchema) const readGithubAuthLogoutRequest = () => HttpServerRequest.schemaBodyJson(GithubAuthLogoutRequestSchema) +const readAuthMenuRequest = () => HttpServerRequest.schemaBodyJson(AuthMenuRequestSchema) +const readAuthTerminalSessionRequest = () => HttpServerRequest.schemaBodyJson(AuthTerminalSessionRequestSchema) const readCodexAuthImportRequest = () => HttpServerRequest.schemaBodyJson(CodexAuthImportRequestSchema) const readCodexAuthLoginRequest = () => HttpServerRequest.schemaBodyJson(CodexAuthLoginRequestSchema) const readCodexAuthLogoutRequest = () => HttpServerRequest.schemaBodyJson(CodexAuthLogoutRequestSchema) +const readProjectAuthRequest = () => HttpServerRequest.schemaBodyJson(ProjectAuthRequestSchema) const readStateInitRequest = () => HttpServerRequest.schemaBodyJson(StateInitRequestSchema) const readStateCommitRequest = () => HttpServerRequest.schemaBodyJson(StateCommitRequestSchema) const readStateSyncRequest = () => HttpServerRequest.schemaBodyJson(StateSyncRequestSchema) const readApplyAllRequest = () => HttpServerRequest.schemaBodyJson(ApplyAllRequestSchema) +const emptyUpProjectRequest: UpProjectRequestInput = {} const readUpProjectRequest = () => HttpServerRequest.schemaBodyJson(UpProjectRequestSchema).pipe( - Effect.catchAll(() => Effect.succeed({ authorizedKeysContents: undefined })) + Effect.catchAll(() => Effect.succeed(emptyUpProjectRequest)) ) const readInboxPayload = () => HttpServerRequest.schemaBodyJson(Schema.Unknown) @@ -267,8 +303,27 @@ const resolveFederationContext = ( }) } +const terminalWebSocketUpgradeResponse = Effect.gen(function*(_) { + const request = yield* _(HttpServerRequest.HttpServerRequest) + const upgrade = readHeader(request, "upgrade")?.toLowerCase() + if (upgrade === "websocket") { + return yield* _(Effect.never) + } + return yield* _( + jsonResponse( + { + error: { + type: "UpgradeRequired", + message: "Use a websocket upgrade request for terminal sessions." + } + }, + 426 + ) + ) +}) + export const makeRouter = () => { - const base = HttpRouter.empty.pipe( + const withUi = HttpRouter.empty.pipe( HttpRouter.get("/", Effect.gen(function*(_) { const request = yield* _(HttpServerRequest.HttpServerRequest) @@ -278,7 +333,17 @@ export const makeRouter = () => { ), HttpRouter.get("/ui/styles.css", textResponse(uiStyles, "text/css; charset=utf-8", 200)), HttpRouter.get("/ui/app.js", textResponse(uiScript, "application/javascript; charset=utf-8", 200)), - HttpRouter.get("/health", jsonResponse({ ok: true, revision: controllerRevision }, 200)), + HttpRouter.get( + "/health", + Effect.gen(function*(_) { + const cwd = yield* _(resolveWorkspaceRoot(process.cwd()).pipe(Effect.orElseSucceed(() => process.cwd()))) + const projectsRoot = defaultProjectsRoot(cwd) + return yield* _(jsonResponse({ ok: true, revision: controllerRevision, cwd, projectsRoot }, 200)) + }).pipe(Effect.catchAll(errorResponse)) + ) + ) + + const withAuth = withUi.pipe( HttpRouter.get( "/auth/github/status", Effect.gen(function*(_) { @@ -286,6 +351,13 @@ export const makeRouter = () => { return yield* _(jsonResponse({ status }, 200)) }).pipe(Effect.catchAll(errorResponse)) ), + HttpRouter.get( + "/auth/menu", + Effect.gen(function*(_) { + const snapshot = yield* _(readAuthMenuSnapshot()) + return yield* _(jsonResponse({ snapshot }, 200)) + }).pipe(Effect.catchAll(errorResponse)) + ), HttpRouter.post( "/auth/github/login", Effect.gen(function*(_) { @@ -294,6 +366,34 @@ export const makeRouter = () => { return yield* _(jsonResponse({ ok: true, status }, 201)) }).pipe(Effect.catchAll(errorResponse)) ), + HttpRouter.post( + "/auth/menu", + Effect.gen(function*(_) { + const request = yield* _(readAuthMenuRequest()) + const snapshot = yield* _(runAuthMenuFlow(request)) + return yield* _(jsonResponse({ ok: true, snapshot }, 200)) + }).pipe(Effect.catchAll(errorResponse)) + ), + HttpRouter.post( + "/auth/terminal-sessions", + Effect.gen(function*(_) { + const request = yield* _(readAuthTerminalSessionRequest()) + const created = yield* _(createAuthTerminalSession(request)) + return yield* _(jsonResponse(created, 201)) + }).pipe(Effect.catchAll(errorResponse)) + ), + HttpRouter.get( + "/auth/terminal-sessions/:sessionId/ws", + terminalWebSocketUpgradeResponse.pipe(Effect.catchAll(errorResponse)) + ), + HttpRouter.del( + "/auth/terminal-sessions/:sessionId", + Effect.gen(function*(_) { + const params = yield* _(authTerminalSessionParams) + yield* _(deleteAuthTerminalSession(params.sessionId)) + return yield* _(jsonResponse({ ok: true }, 200)) + }).pipe(Effect.catchAll(errorResponse)) + ), HttpRouter.post( "/auth/github/logout", Effect.gen(function*(_) { @@ -340,7 +440,10 @@ export const makeRouter = () => { const status = yield* _(logoutCodexAuth(request)) return yield* _(jsonResponse({ ok: true, status }, 200)) }).pipe(Effect.catchAll(errorResponse)) - ), + ) + ) + + const base = withAuth.pipe( HttpRouter.get( "/federation/issues", Effect.sync(() => ({ issues: listFederationIssues() })).pipe( @@ -509,6 +612,30 @@ export const makeRouter = () => { Effect.catchAll(errorResponse) ) ), + HttpRouter.get( + "/projects/:projectId/auth/menu", + projectParams.pipe( + Effect.flatMap(({ projectId }) => + Effect.gen(function*(_) { + const project = yield* _(getProject(projectId)) + const snapshot = yield* _(readProjectAuthSnapshot(project)) + return { snapshot } + }) + ), + Effect.flatMap((payload) => jsonResponse(payload, 200)), + Effect.catchAll(errorResponse) + ) + ), + HttpRouter.post( + "/projects/:projectId/auth/menu", + Effect.gen(function*(_) { + const { projectId } = yield* _(projectParams) + const request = yield* _(readProjectAuthRequest()) + const project = yield* _(getProject(projectId)) + const snapshot = yield* _(runProjectAuthFlow(project, request)) + return yield* _(jsonResponse({ ok: true, snapshot }, 200)) + }).pipe(Effect.catchAll(errorResponse)) + ), HttpRouter.del( "/projects/:projectId", projectParams.pipe( @@ -522,8 +649,10 @@ export const makeRouter = () => { Effect.gen(function*(_) { const { projectId } = yield* _(projectParams) const request = yield* _(readUpProjectRequest()) - yield* _(upProject(projectId, request.authorizedKeysContents)) - return yield* _(jsonResponse({ ok: true }, 200)) + const project = yield* _( + upProject(projectId, request.authorizedKeysContents, request.useManagedAuthorizedKeys) + ) + return yield* _(jsonResponse({ ok: true, project }, 200)) }).pipe( Effect.catchAll(errorResponse) ) @@ -536,6 +665,26 @@ export const makeRouter = () => { Effect.catchAll(errorResponse) ) ), + HttpRouter.post( + "/projects/:projectId/terminal-sessions", + projectParams.pipe( + Effect.flatMap(({ projectId }) => createTerminalSession(projectId)), + Effect.flatMap(({ project, session }) => jsonResponse({ ok: true, project, session }, 201)), + Effect.catchAll(errorResponse) + ) + ), + HttpRouter.get( + "/projects/:projectId/terminal-sessions/:sessionId/ws", + terminalWebSocketUpgradeResponse.pipe(Effect.catchAll(errorResponse)) + ), + HttpRouter.del( + "/projects/:projectId/terminal-sessions/:sessionId", + terminalSessionParams.pipe( + Effect.flatMap(({ projectId, sessionId }) => deleteTerminalSession(projectId, sessionId)), + Effect.flatMap(() => jsonResponse({ ok: true }, 200)), + Effect.catchAll(errorResponse) + ) + ), HttpRouter.post( "/projects/:projectId/recreate", projectParams.pipe( @@ -627,6 +776,31 @@ export const makeRouter = () => { ) return withAgents.pipe( + HttpRouter.get( + "/projects/:projectId/events-poll", + projectParams.pipe( + Effect.flatMap(({ projectId }) => + Effect.gen(function*(_) { + const request = yield* _(HttpServerRequest.HttpServerRequest) + const hasCursor = hasQueryParam(request.url, "cursor") + if (!hasCursor) { + return { + cursor: latestProjectCursor(projectId), + events: [] + } + } + const currentCursor = parseQueryInt(request.url, "cursor", 0) + const events = listProjectEventsSince(projectId, currentCursor) + return { + cursor: events[events.length - 1]?.seq ?? currentCursor, + events + } + }) + ), + Effect.flatMap((payload) => jsonResponse(payload, 200)), + Effect.catchAll(errorResponse) + ) + ), HttpRouter.get( "/projects/:projectId/events", projectParams.pipe( @@ -642,6 +816,8 @@ export const makeRouter = () => { const idLine = id === undefined ? "" : `id: ${id}\n` return encoder.encode(`${idLine}event: ${event}\ndata: ${JSON.stringify(data)}\n\n`) } + const encodeComment = (comment: string): Uint8Array => + encoder.encode(`: ${comment}\n\n`) const poll = Effect.gen(function* (_) { const snapshotSent = yield* _(Ref.get(snapshotRef)) @@ -650,20 +826,22 @@ export const makeRouter = () => { yield* _(Ref.set(snapshotRef, true)) const cursor = latestProjectCursor(projectId) yield* _(Ref.set(cursorRef, cursor)) - return Chunk.of( + return Chunk.fromIterable([ + encodeComment(" ".repeat(2048)), encodeSse("snapshot", { projectId, cursor, agents: listAgents(projectId) - }, cursor) - ) + }, cursor), + encodeComment("connected") + ]) } const currentCursor = yield* _(Ref.get(cursorRef)) const events = listProjectEventsSince(projectId, currentCursor) if (events.length === 0) { yield* _(Effect.sleep(Duration.millis(500))) - return Chunk.empty() + return Chunk.of(encodeComment("keep-alive")) } const nextCursor = events[events.length - 1]?.seq ?? currentCursor @@ -674,9 +852,10 @@ export const makeRouter = () => { return HttpServerResponse.stream(Stream.repeatEffectChunk(poll), { headers: { - "content-type": "text/event-stream", - "cache-control": "no-cache", - "connection": "keep-alive" + "content-type": "text/event-stream; charset=utf-8", + "cache-control": "no-cache, no-transform", + "connection": "keep-alive", + "x-accel-buffering": "no" } }) }) diff --git a/packages/api/src/program.ts b/packages/api/src/program.ts index 981b59fb..2b0eb93b 100644 --- a/packages/api/src/program.ts +++ b/packages/api/src/program.ts @@ -5,7 +5,9 @@ import { createServer } from "node:http" import { makeRouter } from "./http.js" import { initializeAgentState } from "./services/agents.js" +import { attachAuthTerminalWebSocketServer } from "./services/auth-terminal-sessions.js" import { startOutboxPolling } from "./services/federation.js" +import { attachTerminalWebSocketServer } from "./services/terminal-sessions.js" const resolvePort = (env: Record): number => { const raw = env["DOCKER_GIT_API_PORT"] ?? env["PORT"] @@ -42,6 +44,8 @@ export const program = (() => { const router = makeRouter() const app = router.pipe(HttpServer.serve(requestLogger), HttpServer.withLogAddress) const server = createServer() + attachAuthTerminalWebSocketServer(server) + attachTerminalWebSocketServer(server) const serverLayer = NodeHttpServer.layer(() => server, { port }) const pollingInterval = parseInt(process.env["DOCKER_GIT_OUTBOX_POLLING_INTERVAL_MS"] ?? "5000", 10) diff --git a/packages/api/src/services/auth-menu.ts b/packages/api/src/services/auth-menu.ts new file mode 100644 index 00000000..ed948d95 --- /dev/null +++ b/packages/api/src/services/auth-menu.ts @@ -0,0 +1,227 @@ +import * as FileSystem from "@effect/platform/FileSystem" +import type * as CommandExecutor from "@effect/platform/CommandExecutor" +import type { PlatformError } from "@effect/platform/Error" +import * as Path from "@effect/platform/Path" +import { authClaudeLogout, authGeminiLogin, authGeminiLogout } from "@effect-template/lib/usecases/auth" +import { ensureEnvFile, parseEnvEntries, readEnvText, upsertEnvKey } from "@effect-template/lib/usecases/env-file" +import { renderError, type AppError } from "@effect-template/lib/usecases/errors" +import { defaultProjectsRoot } from "@effect-template/lib/usecases/menu-helpers" +import { autoSyncState } from "@effect-template/lib/usecases/state-repo" +import { Effect, pipe } from "effect" + +import type { AuthMenuRequest, AuthSnapshot } from "../api/contracts.js" +import { ApiBadRequestError } from "../api/errors.js" + +type MenuAuthRuntime = FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor + +const claudeAuthRoot = `${defaultProjectsRoot(process.cwd())}/.orch/auth/claude` +const geminiAuthRoot = `${defaultProjectsRoot(process.cwd())}/.orch/auth/gemini` +const globalEnvPath = `${defaultProjectsRoot(process.cwd())}/.orch/env/global.env` + +const normalizeLabel = (value: string): string => { + const trimmed = value.trim() + if (trimmed.length === 0) { + return "" + } + + const normalized = trimmed.toUpperCase().replaceAll(/[^A-Z0-9]+/g, "_") + let start = 0 + while (start < normalized.length && normalized[start] === "_") { + start += 1 + } + let end = normalized.length + while (end > start && normalized[end - 1] === "_") { + end -= 1 + } + return normalized.slice(start, end) +} + +const buildLabeledEnvKey = (baseKey: string, label: string): string => { + const normalized = normalizeLabel(label) + if (normalized.length === 0 || normalized === "DEFAULT") { + return baseKey + } + return `${baseKey}__${normalized}` +} + +const countKeyEntries = (envText: string, baseKey: string): number => { + const prefix = `${baseKey}__` + return parseEnvEntries(envText) + .filter((entry) => entry.value.trim().length > 0 && (entry.key === baseKey || entry.key.startsWith(prefix))) + .length +} + +const countAuthAccountDirectories = ( + fs: FileSystem.FileSystem, + path: Path.Path, + root: string +): Effect.Effect => + Effect.gen(function*(_) { + const exists = yield* _(fs.exists(root)) + if (!exists) { + return 0 + } + + const entries = yield* _(fs.readDirectory(root)) + let count = 0 + for (const entry of entries) { + if (entry === ".image") { + continue + } + + const fullPath = path.join(root, entry) + const info = yield* _(fs.stat(fullPath)) + if (info.type === "Directory") { + count += 1 + } + } + + return count + }) + +const loadAuthEnvText = (): Effect.Effect< + { + readonly fs: FileSystem.FileSystem + readonly path: Path.Path + readonly envText: string + }, + PlatformError, + FileSystem.FileSystem | Path.Path +> => + Effect.gen(function*(_) { + const fs = yield* _(FileSystem.FileSystem) + const path = yield* _(Path.Path) + yield* _(ensureEnvFile(fs, path, globalEnvPath)) + const envText = yield* _(readEnvText(fs, globalEnvPath)) + return { fs, path, envText } + }) + +export const readAuthMenuSnapshot = (): Effect.Effect => + pipe( + loadAuthEnvText(), + Effect.flatMap(({ envText, fs, path }) => + Effect.all({ + claudeAuthEntries: countAuthAccountDirectories(fs, path, claudeAuthRoot), + geminiAuthEntries: countAuthAccountDirectories(fs, path, geminiAuthRoot) + }).pipe( + Effect.map(({ claudeAuthEntries, geminiAuthEntries }) => ({ + globalEnvPath, + claudeAuthPath: claudeAuthRoot, + geminiAuthPath: geminiAuthRoot, + totalEntries: parseEnvEntries(envText).filter((entry) => entry.value.trim().length > 0).length, + githubTokenEntries: countKeyEntries(envText, "GITHUB_TOKEN"), + gitTokenEntries: countKeyEntries(envText, "GIT_AUTH_TOKEN"), + gitUserEntries: countKeyEntries(envText, "GIT_AUTH_USER"), + claudeAuthEntries, + geminiAuthEntries + })) + ) + ) + ) + +const canonicalLabel = (value: string | null | undefined): string => { + const normalized = normalizeLabel(value ?? "") + return normalized.length === 0 || normalized === "DEFAULT" ? "default" : normalized +} + +const syncMessage = (request: AuthMenuRequest): string => + request.flow === "GithubRemove" + ? `chore(state): auth gh logout ${canonicalLabel(request.label)}` + : request.flow === "GitSet" + ? `chore(state): auth git ${canonicalLabel(request.label)}` + : request.flow === "GitRemove" + ? `chore(state): auth git logout ${canonicalLabel(request.label)}` + : request.flow === "ClaudeLogout" + ? `chore(state): auth claude logout ${canonicalLabel(request.label)}` + : request.flow === "GeminiApiKey" + ? `chore(state): auth gemini ${canonicalLabel(request.label)}` + : `chore(state): auth gemini logout ${canonicalLabel(request.label)}` + +const writeEnvBackedAuthFlow = ( + request: AuthMenuRequest +): Effect.Effect => + pipe( + loadAuthEnvText(), + Effect.flatMap(({ envText, fs }) => { + const label = request.label ?? "" + const token = (request.token ?? "").trim() + const user = (request.user ?? "").trim() + const nextText = request.flow === "GithubRemove" + ? upsertEnvKey(envText, buildLabeledEnvKey("GITHUB_TOKEN", label), "") + : request.flow === "GitSet" + ? upsertEnvKey( + upsertEnvKey(envText, buildLabeledEnvKey("GIT_AUTH_TOKEN", label), token), + buildLabeledEnvKey("GIT_AUTH_USER", label), + user.length > 0 ? user : "x-access-token" + ) + : upsertEnvKey( + upsertEnvKey(envText, buildLabeledEnvKey("GIT_AUTH_TOKEN", label), ""), + buildLabeledEnvKey("GIT_AUTH_USER", label), + "" + ) + + return pipe( + fs.writeFileString(globalEnvPath, nextText), + Effect.zipRight(autoSyncState(syncMessage(request))) + ) + }), + Effect.asVoid + ) + +const mapMenuAuthError = (error: AppError): ApiBadRequestError => + new ApiBadRequestError({ message: renderError(error) }) + +export const runAuthMenuFlow = ( + request: AuthMenuRequest +): Effect.Effect => + request.flow === "GithubRemove" || request.flow === "GitSet" || request.flow === "GitRemove" + ? pipe( + writeEnvBackedAuthFlow(request), + Effect.mapError((error) => new ApiBadRequestError({ message: String(error) })), + Effect.zipRight(readAuthMenuSnapshot()), + Effect.mapError((error) => + error instanceof ApiBadRequestError ? error : new ApiBadRequestError({ message: String(error) }) + ) + ) + : request.flow === "ClaudeLogout" + ? pipe( + authClaudeLogout({ + _tag: "AuthClaudeLogout", + label: request.label ?? null, + claudeAuthPath: claudeAuthRoot + }), + Effect.mapError(mapMenuAuthError), + Effect.zipRight(readAuthMenuSnapshot()), + Effect.mapError((error) => + error instanceof ApiBadRequestError ? error : new ApiBadRequestError({ message: String(error) }) + ) + ) + : request.flow === "GeminiApiKey" + ? pipe( + authGeminiLogin( + { + _tag: "AuthGeminiLogin", + label: request.label ?? null, + geminiAuthPath: geminiAuthRoot, + isWeb: true + }, + request.apiKey ?? "" + ), + Effect.mapError(mapMenuAuthError), + Effect.zipRight(readAuthMenuSnapshot()), + Effect.mapError((error) => + error instanceof ApiBadRequestError ? error : new ApiBadRequestError({ message: String(error) }) + ) + ) + : pipe( + authGeminiLogout({ + _tag: "AuthGeminiLogout", + label: request.label ?? null, + geminiAuthPath: geminiAuthRoot + }), + Effect.mapError(mapMenuAuthError), + Effect.zipRight(readAuthMenuSnapshot()), + Effect.mapError((error) => + error instanceof ApiBadRequestError ? error : new ApiBadRequestError({ message: String(error) }) + ) + ) diff --git a/packages/api/src/services/auth-terminal-sessions.ts b/packages/api/src/services/auth-terminal-sessions.ts new file mode 100644 index 00000000..94f3ca97 --- /dev/null +++ b/packages/api/src/services/auth-terminal-sessions.ts @@ -0,0 +1,313 @@ +import * as ParseResult from "@effect/schema/ParseResult" +import * as Schema from "@effect/schema/Schema" +import { Either, Effect } from "effect" +import { randomUUID } from "node:crypto" +import { fileURLToPath } from "node:url" +import type { IncomingMessage, Server as HttpServer } from "node:http" +import type { Duplex } from "node:stream" +import { spawn, type IPty } from "node-pty" +import { WebSocket, WebSocketServer, type RawData } from "ws" + +import type { AuthTerminalFlow, AuthTerminalSessionRequest, TerminalSession, TerminalSessionStatus } from "../api/contracts.js" +import { ApiConflictError, ApiNotFoundError, describeUnknown } from "../api/errors.js" + +type TerminalClientMessage = + | { readonly type: "input"; readonly data: string } + | { readonly type: "resize"; readonly cols: number; readonly rows: number } + | { readonly type: "close" } + +type TerminalServerMessage = + | { readonly type: "ready"; readonly session: TerminalSession } + | { readonly type: "output"; readonly data: string } + | { readonly type: "exit"; readonly exitCode: number | null; readonly signal: number | null } + | { readonly type: "error"; readonly message: string } + +type AuthTerminalRecord = { + attachTimeout: ReturnType | null + args: ReadonlyArray + cwd: string + pty: IPty | null + session: TerminalSession + socket: WebSocket | null +} + +const attachTimeoutMs = 30_000 +const authTerminalProjectId = "__controller__" +const authTerminalWsPathPattern = /^(?:\/api)?\/auth\/terminal-sessions\/([^/]+)\/ws$/u +const authRunnerPath = fileURLToPath(new URL("../auth-terminal-runner.js", import.meta.url)) +const records = new Map() + +const TerminalClientMessageSchema = Schema.parseJson( + Schema.Union( + Schema.Struct({ + type: Schema.Literal("input"), + data: Schema.String + }), + Schema.Struct({ + type: Schema.Literal("resize"), + cols: Schema.Number, + rows: Schema.Number + }), + Schema.Struct({ + type: Schema.Literal("close") + }) + ) +) + +const nowIso = (): string => new Date().toISOString() + +const resolveCommandLabel = (request: AuthTerminalSessionRequest): string => { + const label = request.label?.trim() + const suffix = label === undefined || label.length === 0 ? "" : ` [${label}]` + return request.flow === "ClaudeOauth" + ? `docker-git menu auth claude oauth${suffix}` + : `docker-git menu auth gemini oauth${suffix}` +} + +const resolveRunnerArgs = (flow: AuthTerminalFlow, label: string | null | undefined): ReadonlyArray => { + const args = [authRunnerPath, flow] + const normalizedLabel = label?.trim() ?? "" + return normalizedLabel.length === 0 ? args : [...args, normalizedLabel] +} + +const updateSession = (record: AuthTerminalRecord, patch: Partial): void => { + record.session = { + ...record.session, + ...patch + } + records.set(record.session.id, record) +} + +const encodeServerMessage = (message: TerminalServerMessage): string => JSON.stringify(message) + +const sendServerMessage = (socket: WebSocket | null, message: TerminalServerMessage): void => { + if (socket === null || socket.readyState !== WebSocket.OPEN) { + return + } + socket.send(encodeServerMessage(message)) +} + +const clearAttachTimeout = (record: AuthTerminalRecord): void => { + if (record.attachTimeout !== null) { + clearTimeout(record.attachTimeout) + record.attachTimeout = null + } +} + +const closeSocket = (socket: WebSocket | null): void => { + if (socket === null || socket.readyState === WebSocket.CLOSED) { + return + } + socket.close() +} + +const cleanupRecord = (record: AuthTerminalRecord): void => { + clearAttachTimeout(record) + if (record.pty !== null) { + record.pty.kill() + record.pty = null + } + closeSocket(record.socket) + record.socket = null + records.delete(record.session.id) +} + +const finalizeRecord = ( + record: AuthTerminalRecord, + status: Extract, + exitCode: number | null, + signal: number | null +): void => { + updateSession(record, { + closedAt: nowIso(), + exitCode: exitCode ?? undefined, + signal: signal ?? undefined, + status + }) + sendServerMessage(record.socket, { type: "exit", exitCode, signal }) + closeSocket(record.socket) + record.socket = null + record.pty = null + clearAttachTimeout(record) + records.delete(record.session.id) +} + +const decodeClientMessage = (raw: RawData): TerminalClientMessage | null => + Either.getOrNull( + ParseResult.decodeUnknownEither(TerminalClientMessageSchema)( + typeof raw === "string" + ? raw + : Array.isArray(raw) + ? Buffer.concat(raw).toString("utf8") + : raw instanceof ArrayBuffer + ? Buffer.from(new Uint8Array(raw)).toString("utf8") + : raw.toString("utf8") + ) + ) + +const clampTerminalSize = (value: number, fallback: number): number => + Number.isFinite(value) && value > 0 ? Math.max(1, Math.floor(value)) : fallback + +const startTerminalPty = (record: AuthTerminalRecord, cols: number, rows: number): void => { + const pty = spawn(process.execPath, [...record.args], { + cols: clampTerminalSize(cols, 120), + cwd: record.cwd, + env: { + ...process.env, + TERM: "xterm-256color" + }, + name: "xterm-256color", + rows: clampTerminalSize(rows, 32) + }) + record.pty = pty + updateSession(record, { + startedAt: nowIso(), + status: "attached" + }) + pty.onData((data) => { + sendServerMessage(record.socket, { type: "output", data }) + }) + pty.onExit(({ exitCode, signal }) => { + finalizeRecord( + record, + exitCode === 0 || exitCode === 130 ? "exited" : "failed", + exitCode ?? null, + signal ?? null + ) + }) +} + +const createAttachTimeout = (sessionId: string): ReturnType => + setTimeout(() => { + const record = records.get(sessionId) + if (record !== undefined) { + cleanupRecord(record) + } + }, attachTimeoutMs) + +const registerRecord = (request: AuthTerminalSessionRequest): TerminalSession => { + const session: TerminalSession = { + createdAt: nowIso(), + id: randomUUID(), + projectId: authTerminalProjectId, + sshCommand: resolveCommandLabel(request), + status: "ready" + } + const record: AuthTerminalRecord = { + args: resolveRunnerArgs(request.flow, request.label), + attachTimeout: null, + cwd: process.cwd(), + pty: null, + session, + socket: null + } + record.attachTimeout = createAttachTimeout(session.id) + records.set(session.id, record) + return session +} + +const handleSocketMessage = (record: AuthTerminalRecord, raw: RawData): void => { + const message = decodeClientMessage(raw) + if (message === null) { + sendServerMessage(record.socket, { type: "error", message: "Invalid terminal payload." }) + return + } + if (message.type === "input") { + record.pty?.write(message.data) + return + } + if (message.type === "resize") { + record.pty?.resize(clampTerminalSize(message.cols, 120), clampTerminalSize(message.rows, 32)) + return + } + cleanupRecord(record) +} + +const attachSocketToRecord = ( + record: AuthTerminalRecord, + socket: WebSocket, + cols: number, + rows: number +): void => { + if (record.socket !== null) { + throw new ApiConflictError({ message: `Auth terminal session already attached: ${record.session.id}` }) + } + clearAttachTimeout(record) + record.socket = socket + startTerminalPty(record, cols, rows) + sendServerMessage(socket, { type: "ready", session: record.session }) + socket.on("message", (raw: RawData) => { + handleSocketMessage(record, raw) + }) + socket.on("close", () => { + const current = records.get(record.session.id) + if (current !== undefined) { + cleanupRecord(current) + } + }) +} + +const parseTerminalPath = ( + request: IncomingMessage +): { readonly cols: number; readonly rows: number; readonly sessionId: string } | null => { + const url = request.url + if (url === undefined) { + return null + } + const parsed = new URL(url, "http://localhost") + const match = authTerminalWsPathPattern.exec(parsed.pathname) + if (match === null) { + return null + } + return { + cols: clampTerminalSize(Number(parsed.searchParams.get("cols") ?? ""), 120), + rows: clampTerminalSize(Number(parsed.searchParams.get("rows") ?? ""), 32), + sessionId: decodeURIComponent(match[1] ?? "") + } +} + +const denyUpgrade = (socket: Duplex): void => { + socket.write("HTTP/1.1 404 Not Found\r\n\r\n") + socket.destroy() +} + +export const createAuthTerminalSession = ( + request: AuthTerminalSessionRequest +): Effect.Effect<{ readonly session: TerminalSession }, never> => + Effect.succeed({ session: registerRecord(request) }) + +export const deleteAuthTerminalSession = ( + sessionId: string +): Effect.Effect => + Effect.gen(function*(_) { + const record = records.get(sessionId) + if (record === undefined) { + return yield* _( + Effect.fail(new ApiNotFoundError({ message: `Auth terminal session not found: ${sessionId}` })) + ) + } + cleanupRecord(record) + }) + +export const attachAuthTerminalWebSocketServer = (server: HttpServer): void => { + const webSocketServer = new WebSocketServer({ noServer: true }) + server.on("upgrade", (request, socket, head) => { + const parsed = parseTerminalPath(request) + if (parsed === null) { + return + } + const record = records.get(parsed.sessionId) + if (record === undefined) { + denyUpgrade(socket) + return + } + webSocketServer.handleUpgrade(request, socket, head, (webSocket: WebSocket) => { + try { + attachSocketToRecord(record, webSocket, parsed.cols, parsed.rows) + } catch (error) { + sendServerMessage(webSocket, { type: "error", message: describeUnknown(error) }) + webSocket.close() + } + }) + }) +} diff --git a/packages/api/src/services/project-auth.ts b/packages/api/src/services/project-auth.ts new file mode 100644 index 00000000..f5a4e18f --- /dev/null +++ b/packages/api/src/services/project-auth.ts @@ -0,0 +1,439 @@ +import * as FileSystem from "@effect/platform/FileSystem" +import type * as CommandExecutor from "@effect/platform/CommandExecutor" +import type { PlatformError } from "@effect/platform/Error" +import * as Path from "@effect/platform/Path" +import { Effect, Match, pipe } from "effect" + +import { parseEnvEntries, readEnvText, upsertEnvKey, findEnvValue, ensureEnvFile } from "@effect-template/lib/usecases/env-file" +import { renderError, type AppError } from "@effect-template/lib/usecases/errors" +import { defaultProjectsRoot } from "@effect-template/lib/usecases/menu-helpers" +import { autoSyncState } from "@effect-template/lib/usecases/state-repo" +import { normalizeAccountLabel } from "@effect-template/lib/usecases/auth-helpers" + +import type { ProjectAuthFlow, ProjectAuthRequest, ProjectAuthSnapshot, ProjectDetails } from "../api/contracts.js" +import { ApiBadRequestError } from "../api/errors.js" + +type ProjectAuthRuntime = FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor + +const claudeAuthRoot = `${defaultProjectsRoot(process.cwd())}/.orch/auth/claude` +const geminiAuthRoot = `${defaultProjectsRoot(process.cwd())}/.orch/auth/gemini` +const globalEnvPath = `${defaultProjectsRoot(process.cwd())}/.orch/env/global.env` + +const githubTokenBaseKey = "GITHUB_TOKEN" +const gitTokenBaseKey = "GIT_AUTH_TOKEN" +const gitUserBaseKey = "GIT_AUTH_USER" +const projectGithubLabelKey = "GITHUB_AUTH_LABEL" +const projectGitLabelKey = "GIT_AUTH_LABEL" +const projectClaudeLabelKey = "CLAUDE_AUTH_LABEL" +const projectGeminiLabelKey = "GEMINI_AUTH_LABEL" +const defaultGitUser = "x-access-token" + +const normalizeLabel = (value: string): string => { + const trimmed = value.trim() + if (trimmed.length === 0) { + return "" + } + + const normalized = trimmed.toUpperCase().replaceAll(/[^A-Z0-9]+/g, "_") + let start = 0 + while (start < normalized.length && normalized[start] === "_") { + start += 1 + } + let end = normalized.length + while (end > start && normalized[end - 1] === "_") { + end -= 1 + } + return normalized.slice(start, end) +} + +const canonicalLabel = (value: string | null | undefined): string => { + const normalized = normalizeLabel(value ?? "") + return normalized.length === 0 || normalized === "DEFAULT" ? "default" : normalized +} + +const buildLabeledEnvKey = (baseKey: string, label: string): string => { + const normalized = normalizeLabel(label) + if (normalized.length === 0 || normalized === "DEFAULT") { + return baseKey + } + return `${baseKey}__${normalized}` +} + +const countKeyEntries = (envText: string, baseKey: string): number => { + const prefix = `${baseKey}__` + return parseEnvEntries(envText) + .filter((entry) => entry.value.trim().length > 0 && (entry.key === baseKey || entry.key.startsWith(prefix))) + .length +} + +const hasFileAtPath = ( + fs: FileSystem.FileSystem, + filePath: string +): Effect.Effect => + Effect.gen(function*(_) { + const exists = yield* _(fs.exists(filePath)) + if (!exists) { + return false + } + const info = yield* _(fs.stat(filePath)) + return info.type === "File" + }) + +const countAuthAccountDirectories = ( + fs: FileSystem.FileSystem, + path: Path.Path, + root: string +): Effect.Effect => + Effect.gen(function*(_) { + const exists = yield* _(fs.exists(root)) + if (!exists) { + return 0 + } + + const entries = yield* _(fs.readDirectory(root)) + let count = 0 + for (const entry of entries) { + if (entry === ".image") { + continue + } + + const fullPath = path.join(root, entry) + const info = yield* _(fs.stat(fullPath)) + if (info.type === "Directory") { + count += 1 + } + } + + return count + }) + +const hasNonEmptyOauthToken = ( + fs: FileSystem.FileSystem, + tokenPath: string +): Effect.Effect => + Effect.gen(function*(_) { + const hasFile = yield* _(hasFileAtPath(fs, tokenPath)) + if (!hasFile) { + return false + } + + const tokenValue = yield* _(fs.readFileString(tokenPath), Effect.orElseSucceed(() => "")) + return tokenValue.trim().length > 0 + }) + +const hasLegacyClaudeAuthFile = ( + fs: FileSystem.FileSystem, + accountPath: string +): Effect.Effect => + Effect.gen(function*(_) { + const entries = yield* _(fs.readDirectory(accountPath)) + for (const entry of entries) { + if (!entry.startsWith(".claude") || !entry.endsWith(".json")) { + continue + } + + const isFile = yield* _(hasFileAtPath(fs, `${accountPath}/${entry}`)) + if (isFile) { + return true + } + } + + return false + }) + +const hasClaudeAccountCredentials = ( + fs: FileSystem.FileSystem, + accountPath: string +): Effect.Effect => + hasFileAtPath(fs, `${accountPath}/.credentials.json`).pipe( + Effect.flatMap((hasCredentialsFile) => { + if (hasCredentialsFile) { + return Effect.succeed(true) + } + return hasFileAtPath(fs, `${accountPath}/.claude/.credentials.json`) + }), + Effect.flatMap((hasNestedCredentialsFile) => { + if (hasNestedCredentialsFile) { + return Effect.succeed(true) + } + return hasFileAtPath(fs, `${accountPath}/.config.json`) + }), + Effect.flatMap((hasConfig) => { + if (hasConfig) { + return Effect.succeed(true) + } + return hasNonEmptyOauthToken(fs, `${accountPath}/.oauth-token`).pipe( + Effect.flatMap((hasOauthToken) => hasOauthToken ? Effect.succeed(true) : hasLegacyClaudeAuthFile(fs, accountPath)) + ) + }) + ) + +const hasApiKeyInEnvFile = ( + fs: FileSystem.FileSystem, + envFilePath: string +): Effect.Effect => + Effect.gen(function*(_) { + const hasFile = yield* _(hasFileAtPath(fs, envFilePath)) + if (!hasFile) { + return false + } + + const envContent = yield* _(fs.readFileString(envFilePath), Effect.orElseSucceed(() => "")) + for (const line of envContent.split("\n")) { + const trimmed = line.trim() + if (!trimmed.startsWith("GEMINI_API_KEY=")) { + continue + } + const value = trimmed.slice("GEMINI_API_KEY=".length).replaceAll(/^['"]|['"]$/g, "").trim() + if (value.length > 0) { + return true + } + } + return false + }) + +const checkAnyFileExists = ( + fs: FileSystem.FileSystem, + basePath: string, + fileNames: ReadonlyArray +): Effect.Effect => { + const [first, ...rest] = fileNames + if (first === undefined) { + return Effect.succeed(false) + } + + return hasFileAtPath(fs, `${basePath}/${first}`).pipe( + Effect.flatMap((exists) => exists ? Effect.succeed(true) : checkAnyFileExists(fs, basePath, rest)) + ) +} + +const hasGeminiAccountCredentials = ( + fs: FileSystem.FileSystem, + accountPath: string +): Effect.Effect => + hasFileAtPath(fs, `${accountPath}/.api-key`).pipe( + Effect.flatMap((hasApiKey) => { + if (hasApiKey) { + return Effect.succeed(true) + } + + return hasApiKeyInEnvFile(fs, `${accountPath}/.env`).pipe( + Effect.flatMap((hasEnvApiKey) => { + if (hasEnvApiKey) { + return Effect.succeed(true) + } + return checkAnyFileExists(fs, `${accountPath}/.gemini`, [ + "oauth-tokens.json", + "credentials.json", + "application_default_credentials.json" + ]) + }) + ) + }) + ) + +const resolveAccountCandidates = (authPath: string, accountLabel: string): ReadonlyArray => + accountLabel === "default" ? [`${authPath}/default`, authPath] : [`${authPath}/${accountLabel}`] + +const findFirstCredentialsMatch = ( + fs: FileSystem.FileSystem, + candidates: ReadonlyArray, + hasCredentials: ( + fs: FileSystem.FileSystem, + accountPath: string + ) => Effect.Effect +): Effect.Effect => + Effect.gen(function*(_) { + for (const accountPath of candidates) { + const exists = yield* _(fs.exists(accountPath)) + if (!exists) { + continue + } + + const valid = yield* _(hasCredentials(fs, accountPath), Effect.orElseSucceed(() => false)) + if (valid) { + return accountPath + } + } + + return null + }) + +const missingSecret = (provider: string, label: string, envPath: string): ApiBadRequestError => + new ApiBadRequestError({ message: `${provider} not connected: label '${label}' not found in ${envPath}` }) + +const clearProjectGitLabels = (envText: string): string => { + const withoutGhToken = upsertEnvKey(envText, "GH_TOKEN", "") + const withoutGitLabel = upsertEnvKey(withoutGhToken, projectGitLabelKey, "") + return upsertEnvKey(withoutGitLabel, projectGithubLabelKey, "") +} + +const loadProjectAuthEnvText = ( + project: ProjectDetails +): Effect.Effect< + { + readonly fs: FileSystem.FileSystem + readonly path: Path.Path + readonly globalEnvText: string + readonly projectEnvText: string + }, + PlatformError, + FileSystem.FileSystem | Path.Path +> => + Effect.gen(function*(_) { + const fs = yield* _(FileSystem.FileSystem) + const path = yield* _(Path.Path) + yield* _(ensureEnvFile(fs, path, globalEnvPath)) + yield* _(ensureEnvFile(fs, path, project.envProjectPath)) + const globalEnvText = yield* _(readEnvText(fs, globalEnvPath)) + const projectEnvText = yield* _(readEnvText(fs, project.envProjectPath)) + return { fs, path, globalEnvText, projectEnvText } + }) + +export const readProjectAuthSnapshot = ( + project: ProjectDetails +): Effect.Effect => + pipe( + loadProjectAuthEnvText(project), + Effect.flatMap(({ fs, path, globalEnvText, projectEnvText }) => + Effect.all({ + claudeAuthEntries: countAuthAccountDirectories(fs, path, claudeAuthRoot), + geminiAuthEntries: countAuthAccountDirectories(fs, path, geminiAuthRoot) + }).pipe( + Effect.map(({ claudeAuthEntries, geminiAuthEntries }) => ({ + projectDir: project.projectDir, + projectName: project.displayName, + envGlobalPath: globalEnvPath, + envProjectPath: project.envProjectPath, + claudeAuthPath: claudeAuthRoot, + geminiAuthPath: geminiAuthRoot, + githubTokenEntries: countKeyEntries(globalEnvText, githubTokenBaseKey), + gitTokenEntries: countKeyEntries(globalEnvText, gitTokenBaseKey), + claudeAuthEntries, + geminiAuthEntries, + activeGithubLabel: findEnvValue(projectEnvText, projectGithubLabelKey), + activeGitLabel: findEnvValue(projectEnvText, projectGitLabelKey), + activeClaudeLabel: findEnvValue(projectEnvText, projectClaudeLabelKey), + activeGeminiLabel: findEnvValue(projectEnvText, projectGeminiLabelKey) + })) + ) + ) + ) + +const resolveSyncMessage = (flow: ProjectAuthFlow, label: string, displayName: string): string => + Match.value(flow).pipe( + Match.when("ProjectGithubConnect", () => `chore(state): project auth gh ${label} ${displayName}`), + Match.when("ProjectGithubDisconnect", () => `chore(state): project auth gh logout ${displayName}`), + Match.when("ProjectGitConnect", () => `chore(state): project auth git ${label} ${displayName}`), + Match.when("ProjectGitDisconnect", () => `chore(state): project auth git logout ${displayName}`), + Match.when("ProjectClaudeConnect", () => `chore(state): project auth claude ${label} ${displayName}`), + Match.when("ProjectClaudeDisconnect", () => `chore(state): project auth claude logout ${displayName}`), + Match.when("ProjectGeminiConnect", () => `chore(state): project auth gemini ${label} ${displayName}`), + Match.when("ProjectGeminiDisconnect", () => `chore(state): project auth gemini logout ${displayName}`), + Match.exhaustive + ) + +const resolveProjectEnvUpdate = ( + project: ProjectDetails, + request: ProjectAuthRequest +): Effect.Effect => + pipe( + loadProjectAuthEnvText(project), + Effect.flatMap(({ fs, globalEnvText, projectEnvText }) => { + const rawLabel = request.label ?? "" + const normalizedLabel = canonicalLabel(rawLabel) + return Match.value(request.flow).pipe( + Match.when("ProjectGithubConnect", () => { + const token = findEnvValue(globalEnvText, buildLabeledEnvKey(githubTokenBaseKey, rawLabel)) + if (token === null) { + return Effect.fail(missingSecret("GitHub token", normalizedLabel, globalEnvPath)) + } + const withGitToken = upsertEnvKey(projectEnvText, "GIT_AUTH_TOKEN", token) + const withGhToken = upsertEnvKey(withGitToken, "GH_TOKEN", token) + const withoutGitLabel = upsertEnvKey(withGhToken, projectGitLabelKey, "") + return Effect.succeed(upsertEnvKey(withoutGitLabel, projectGithubLabelKey, normalizedLabel)) + }), + Match.when("ProjectGithubDisconnect", () => { + const withoutGitToken = upsertEnvKey(projectEnvText, "GIT_AUTH_TOKEN", "") + return Effect.succeed(clearProjectGitLabels(withoutGitToken)) + }), + Match.when("ProjectGitConnect", () => { + const token = findEnvValue(globalEnvText, buildLabeledEnvKey(gitTokenBaseKey, rawLabel)) + if (token === null) { + return Effect.fail(missingSecret("Git credentials", normalizedLabel, globalEnvPath)) + } + const user = findEnvValue(globalEnvText, buildLabeledEnvKey(gitUserBaseKey, rawLabel)) ?? + findEnvValue(globalEnvText, gitUserBaseKey) ?? defaultGitUser + const withToken = upsertEnvKey(projectEnvText, "GIT_AUTH_TOKEN", token) + const withUser = upsertEnvKey(withToken, "GIT_AUTH_USER", user) + const withGhToken = upsertEnvKey(withUser, "GH_TOKEN", token) + const withGitLabel = upsertEnvKey(withGhToken, projectGitLabelKey, normalizedLabel) + return Effect.succeed(upsertEnvKey(withGitLabel, projectGithubLabelKey, normalizedLabel)) + }), + Match.when("ProjectGitDisconnect", () => { + const withoutToken = upsertEnvKey(projectEnvText, "GIT_AUTH_TOKEN", "") + const withoutUser = upsertEnvKey(withoutToken, "GIT_AUTH_USER", "") + return Effect.succeed(clearProjectGitLabels(withoutUser)) + }), + Match.when("ProjectClaudeConnect", () => + findFirstCredentialsMatch( + fs, + resolveAccountCandidates(claudeAuthRoot, normalizeAccountLabel(request.label ?? null, "default")), + hasClaudeAccountCredentials + ).pipe( + Effect.flatMap((matched) => + matched === null + ? Effect.fail(missingSecret("Claude Code login", normalizedLabel, claudeAuthRoot)) + : Effect.succeed(upsertEnvKey(projectEnvText, projectClaudeLabelKey, normalizedLabel)) + ) + )), + Match.when("ProjectClaudeDisconnect", () => + Effect.succeed(upsertEnvKey(projectEnvText, projectClaudeLabelKey, "")) + ), + Match.when("ProjectGeminiConnect", () => + findFirstCredentialsMatch( + fs, + resolveAccountCandidates(geminiAuthRoot, normalizeAccountLabel(request.label ?? null, "default")), + hasGeminiAccountCredentials + ).pipe( + Effect.flatMap((matched) => + matched === null + ? Effect.fail(missingSecret("Gemini CLI API key", normalizedLabel, geminiAuthRoot)) + : Effect.succeed(upsertEnvKey(projectEnvText, projectGeminiLabelKey, normalizedLabel)) + ) + )), + Match.when("ProjectGeminiDisconnect", () => + Effect.succeed(upsertEnvKey(projectEnvText, projectGeminiLabelKey, "")) + ), + Match.exhaustive + ) + }), + Effect.mapError((error) => + error instanceof ApiBadRequestError ? error : new ApiBadRequestError({ message: String(error) }) + ) + ) + +const toBadRequestError = (error: AppError | ApiBadRequestError | PlatformError): ApiBadRequestError => + error instanceof ApiBadRequestError + ? error + : new ApiBadRequestError({ message: "_tag" in error ? renderError(error as AppError) : String(error) }) + +export const runProjectAuthFlow = ( + project: ProjectDetails, + request: ProjectAuthRequest +): Effect.Effect => + pipe( + resolveProjectEnvUpdate(project, request), + Effect.flatMap((nextText) => + Effect.gen(function*(_) { + const fs = yield* _(FileSystem.FileSystem) + yield* _(fs.writeFileString(project.envProjectPath, nextText)) + }) + ), + Effect.zipRight(autoSyncState(resolveSyncMessage(request.flow, canonicalLabel(request.label), project.displayName))), + Effect.mapError(toBadRequestError), + Effect.zipRight(readProjectAuthSnapshot(project)), + Effect.mapError((error) => + toBadRequestError(error as AppError | ApiBadRequestError | PlatformError) + ) + ) diff --git a/packages/app/src/docker-git/host-ssh-material.ts b/packages/api/src/services/project-authorized-keys.ts similarity index 67% rename from packages/app/src/docker-git/host-ssh-material.ts rename to packages/api/src/services/project-authorized-keys.ts index 652c88f8..2d5dba3b 100644 --- a/packages/app/src/docker-git/host-ssh-material.ts +++ b/packages/api/src/services/project-authorized-keys.ts @@ -1,17 +1,10 @@ +import { defaultTemplateConfig } from "@effect-template/lib/core/domain" +import { runCommandCapture, runCommandWithExitCodes } from "@effect-template/lib/shell/command-runner" +import { CommandFailedError } from "@effect-template/lib/shell/errors" +import { defaultProjectsRoot, findSshPrivateKey, resolvePathFromCwd } from "@effect-template/lib/usecases/path-helpers" +import { withFsPathContext } from "@effect-template/lib/usecases/runtime" import { Effect } from "effect" -import type { CreateCommand } from "@lib/core/domain" -import { defaultTemplateConfig } from "@lib/core/domain" -import { runCommandCapture, runCommandWithExitCodes } from "@lib/shell/command-runner" -import { CommandFailedError } from "@lib/shell/errors" -import { defaultProjectsRoot, findSshPrivateKey, resolvePathFromCwd } from "@lib/usecases/path-helpers" -import { withFsPathContext } from "@lib/usecases/runtime" - -export type HostSshMaterial = { - readonly privateKeyPath: string - readonly authorizedKeysContents: string -} - const normalizeAuthorizedKeys = (value: string): ReadonlyArray => value .split(/\r?\n/u) @@ -31,9 +24,7 @@ const mergeAuthorizedKeys = ( return merged.length === 0 ? "" : `${merged.join("\n")}\n` } -const resolvePublicKeyFromPrivate = ( - privateKeyPath: string -) => +const resolvePublicKeyFromPrivate = (privateKeyPath: string) => withFsPathContext(({ fs }) => Effect.gen(function*(_) { const publicKeyPath = `${privateKeyPath}.pub` @@ -56,7 +47,7 @@ const resolvePublicKeyFromPrivate = ( }) ) -export const resolveHostPrivateKeyPath = () => +const resolveHostPrivateKeyPath = () => withFsPathContext(({ fs, path }) => Effect.gen(function*(_) { const existing = yield* _(findSshPrivateKey(fs, path, process.cwd())) @@ -91,16 +82,28 @@ export const resolveHostPrivateKeyPath = () => }) ) +const resolveManagedHostPublicKey = () => + Effect.gen(function*(_) { + const privateKeyPath = yield* _(resolveHostPrivateKeyPath()) + const publicKey = yield* _(resolvePublicKeyFromPrivate(privateKeyPath)) + + return { + privateKeyPath, + publicKey + } + }) + const readLocalAuthorizedKeysOverride = ( - command: CreateCommand + projectDir: string, + authorizedKeysPath: string ) => withFsPathContext(({ fs, path }) => Effect.gen(function*(_) { - if (command.config.authorizedKeysPath === defaultTemplateConfig.authorizedKeysPath) { + if (authorizedKeysPath === defaultTemplateConfig.authorizedKeysPath) { return "" } - const resolved = resolvePathFromCwd(path, process.cwd(), command.config.authorizedKeysPath) + const resolved = resolvePathFromCwd(path, projectDir, authorizedKeysPath) const exists = yield* _(fs.exists(resolved)) if (!exists) { return "" @@ -110,42 +113,21 @@ const readLocalAuthorizedKeysOverride = ( }) ) -const resolveManagedHostPublicKey = () => +export const resolveManagedAuthorizedKeysContents = () => Effect.gen(function*(_) { - const privateKeyPath = yield* _(resolveHostPrivateKeyPath()) - const publicKey = yield* _(resolvePublicKeyFromPrivate(privateKeyPath)) - - return { - privateKeyPath, - publicKey - } + const { publicKey } = yield* _(resolveManagedHostPublicKey()) + return mergeAuthorizedKeys([], normalizeAuthorizedKeys(publicKey)) }) -export const resolveHostSshMaterial = ( - command: CreateCommand +export const resolveCreateAuthorizedKeysContents = ( + projectDir: string, + authorizedKeysPath: string ) => Effect.gen(function*(_) { - const { privateKeyPath, publicKey } = yield* _(resolveManagedHostPublicKey()) - const authorizedKeysOverride = yield* _(readLocalAuthorizedKeysOverride(command)) - - return { - privateKeyPath, - authorizedKeysContents: mergeAuthorizedKeys( - normalizeAuthorizedKeys(authorizedKeysOverride), - normalizeAuthorizedKeys(publicKey) - ) - } - }) - -export const resolveManagedHostSshMaterial = () => - Effect.gen(function*(_) { - const { privateKeyPath, publicKey } = yield* _(resolveManagedHostPublicKey()) - - return { - privateKeyPath, - authorizedKeysContents: mergeAuthorizedKeys( - [], - normalizeAuthorizedKeys(publicKey) - ) - } + const { publicKey } = yield* _(resolveManagedHostPublicKey()) + const authorizedKeysOverride = yield* _(readLocalAuthorizedKeysOverride(projectDir, authorizedKeysPath)) + return mergeAuthorizedKeys( + normalizeAuthorizedKeys(authorizedKeysOverride), + normalizeAuthorizedKeys(publicKey) + ) }) diff --git a/packages/api/src/services/project-runtime.ts b/packages/api/src/services/project-runtime.ts new file mode 100644 index 00000000..76971ba6 --- /dev/null +++ b/packages/api/src/services/project-runtime.ts @@ -0,0 +1,138 @@ +import { runCommandCapture } from "@effect-template/lib/shell/command-runner" +import { runDockerPsNames } from "@effect-template/lib/shell/docker" +import type { ProjectItem } from "@effect-template/lib/usecases/projects" +import { Effect, pipe } from "effect" + +import { CommandFailedError } from "@effect-template/lib/shell/errors" + +type ProjectRuntime = { + readonly running: boolean + readonly sshSessions: number + readonly startedAtIso: string | null + readonly startedAtEpochMs: number | null +} + +const emptyRuntimeByProject = (): Readonly> => ({}) + +export const stoppedProjectRuntime = (): ProjectRuntime => ({ + running: false, + sshSessions: 0, + startedAtIso: null, + startedAtEpochMs: null +}) + +const countSshSessionsScript = "who -u 2>/dev/null | wc -l | tr -d '[:space:]'" +const dockerZeroStartedAt = "0001-01-01T00:00:00Z" + +type ContainerStartTime = { + readonly startedAtIso: string + readonly startedAtEpochMs: number +} + +const parseSshSessionCount = (raw: string): number => { + const parsed = Number.parseInt(raw.trim(), 10) + if (Number.isNaN(parsed) || parsed < 0) { + return 0 + } + return parsed +} + +const parseContainerStartedAt = (raw: string): ContainerStartTime | null => { + const trimmed = raw.trim() + if (trimmed.length === 0 || trimmed === dockerZeroStartedAt) { + return null + } + const startedAtEpochMs = Date.parse(trimmed) + if (Number.isNaN(startedAtEpochMs)) { + return null + } + return { + startedAtIso: trimmed, + startedAtEpochMs + } +} + +const toRuntimeMap = ( + entries: ReadonlyArray +): Readonly> => { + const runtimeByProject: Record = {} + for (const [projectDir, runtime] of entries) { + runtimeByProject[projectDir] = runtime + } + return runtimeByProject +} + +const countContainerSshSessions = ( + containerName: string +) => + pipe( + runCommandCapture( + { + cwd: process.cwd(), + command: "docker", + args: ["exec", containerName, "bash", "-lc", countSshSessionsScript] + }, + [0], + (exitCode) => new CommandFailedError({ command: "docker exec who -u", exitCode }) + ), + Effect.match({ + onFailure: () => 0, + onSuccess: (raw) => parseSshSessionCount(raw) + }) + ) + +const inspectContainerStartedAt = (containerName: string) => + pipe( + runCommandCapture( + { + cwd: process.cwd(), + command: "docker", + args: ["inspect", "--format", "{{.State.StartedAt}}", containerName] + }, + [0], + (exitCode) => new CommandFailedError({ command: "docker inspect .State.StartedAt", exitCode }) + ), + Effect.match({ + onFailure: () => null, + onSuccess: (raw) => parseContainerStartedAt(raw) + }) + ) + +export const loadProjectRuntimeByProject = ( + items: ReadonlyArray +) => + pipe( + runDockerPsNames(process.cwd()), + Effect.flatMap((runningNames) => + Effect.forEach( + items, + (item) => { + const running = runningNames.includes(item.containerName) + const sshSessionsEffect = running + ? countContainerSshSessions(item.containerName) + : Effect.succeed(0) + return pipe( + Effect.all([sshSessionsEffect, inspectContainerStartedAt(item.containerName)]), + Effect.map(([sshSessions, startedAt]): ProjectRuntime => ({ + running, + sshSessions, + startedAtIso: startedAt?.startedAtIso ?? null, + startedAtEpochMs: startedAt?.startedAtEpochMs ?? null + })), + Effect.map((runtime): readonly [string, ProjectRuntime] => [item.projectDir, runtime]) + ) + }, + { concurrency: 4 } + ) + ), + Effect.map((entries) => toRuntimeMap(entries)), + Effect.match({ + onFailure: () => emptyRuntimeByProject(), + onSuccess: (runtimeByProject) => runtimeByProject + }) + ) + +export const runtimeForProject = ( + runtimeByProject: Readonly>, + project: ProjectItem +): ProjectRuntime => runtimeByProject[project.projectDir] ?? stoppedProjectRuntime() diff --git a/packages/api/src/services/projects.ts b/packages/api/src/services/projects.ts index 8e1dc278..6a61ea72 100644 --- a/packages/api/src/services/projects.ts +++ b/packages/api/src/services/projects.ts @@ -1,5 +1,7 @@ import { + type AppError, buildCreateCommand, + defaultTemplateConfig, createProject, formatParseError, applyAllDockerGitProjects, @@ -16,13 +18,16 @@ import { CommandFailedError } from "@effect-template/lib/shell/errors" import { defaultProjectsRoot, resolvePathFromCwd } from "@effect-template/lib/usecases/path-helpers" import { deleteDockerGitProject } from "@effect-template/lib/usecases/projects" import type { RawOptions } from "@effect-template/lib/core/command-options" +import type { CreateCommand as LibCreateCommand } from "@effect-template/lib/core/domain" import type { ProjectItem } from "@effect-template/lib/usecases/projects" import { Effect, Either } from "effect" import type { CreateProjectRequest, ProjectDetails, ProjectStatus, ProjectSummary } from "../api/contracts.js" -import { ApiConflictError, ApiInternalError, ApiNotFoundError, ApiBadRequestError } from "../api/errors.js" +import { ApiAuthRequiredError, ApiConflictError, ApiInternalError, ApiNotFoundError, ApiBadRequestError } from "../api/errors.js" import { ensureGithubAuthForCreate } from "./auth.js" import { emitProjectEvent } from "./events.js" +import { resolveCreateAuthorizedKeysContents, resolveManagedAuthorizedKeysContents } from "./project-authorized-keys.js" +import { loadProjectRuntimeByProject, runtimeForProject } from "./project-runtime.js" const readComposePsFormatted = (cwd: string) => runCommandCapture( @@ -100,7 +105,10 @@ const statusLabelFromPs = (raw: string): string => { return statuses.length > 0 ? statuses.join(", ") : "unknown" } -const withProjectRuntime = (project: ProjectItem) => +const withProjectRuntime = ( + project: ProjectItem, + runtime: ReturnType +) => readComposePsFormatted(project.projectDir).pipe( Effect.catchAll(() => Effect.succeed("")), Effect.map((rawStatus) => ({ @@ -109,7 +117,11 @@ const withProjectRuntime = (project: ProjectItem) => repoUrl: project.repoUrl, repoRef: project.repoRef, status: toProjectStatus(rawStatus), - statusLabel: statusLabelFromPs(rawStatus) + statusLabel: statusLabelFromPs(rawStatus), + sshSessions: runtime.sshSessions, + startedAtIso: runtime.startedAtIso, + startedAtEpochMs: runtime.startedAtEpochMs, + clonedOnHostname: project.clonedOnHostname })) ) @@ -125,11 +137,12 @@ const toProjectDetails = ( targetDir: project.targetDir, projectDir: project.projectDir, sshCommand: project.sshCommand, + authorizedKeysPath: project.authorizedKeysPath, + authorizedKeysExists: project.authorizedKeysExists, envGlobalPath: project.envGlobalPath, envProjectPath: project.envProjectPath, codexAuthPath: project.codexAuthPath, - codexHome: project.codexHome, - clonedOnHostname: project.clonedOnHostname + codexHome: project.codexHome }) const findProjectById = (projectId: string) => @@ -142,6 +155,8 @@ const findProjectById = (projectId: string) => }) ) +export const getProjectItemById = (projectId: string) => findProjectById(projectId) + const resolveCreatedProject = ( containerName: string, repoUrl: string, @@ -169,6 +184,28 @@ const normalizeAuthorizedKeys = (value: string): ReadonlyArray => .map((line) => line.trim()) .filter((line) => line.length > 0) +type ProjectApiError = + | AppError + | ApiAuthRequiredError + | ApiBadRequestError + | ApiConflictError + | ApiInternalError + | ApiNotFoundError + +const toProjectApiError = ( + error: ProjectApiError +): ApiAuthRequiredError | ApiBadRequestError | ApiConflictError | ApiInternalError | ApiNotFoundError => + error instanceof ApiAuthRequiredError || + error instanceof ApiBadRequestError || + error instanceof ApiConflictError || + error instanceof ApiInternalError || + error instanceof ApiNotFoundError + ? error + : new ApiInternalError({ + message: renderError(error), + cause: error + }) + const mergeAuthorizedKeys = ( current: ReadonlyArray, next: ReadonlyArray @@ -182,6 +219,20 @@ const mergeAuthorizedKeys = ( return merged.length === 0 ? "" : `${merged.join("\n")}\n` } +const withManagedAuthorizedKeysForCreate = ( + command: LibCreateCommand, + authorizedKeysContents: string | undefined +) => + authorizedKeysContents === undefined + ? command + : { + ...command, + config: { + ...command.config, + authorizedKeysPath: defaultTemplateConfig.authorizedKeysPath + } + } + export const seedAuthorizedKeysForCreate = ( outDir: string, authorizedKeysContents: string | undefined @@ -211,7 +262,17 @@ export const seedAuthorizedKeysForCreate = ( export const listProjects = () => listProjectItems.pipe( - Effect.flatMap((projects) => Effect.forEach(projects, withProjectRuntime, { concurrency: "unbounded" })), + Effect.flatMap((projects) => + loadProjectRuntimeByProject(projects).pipe( + Effect.flatMap((runtimeByProject) => + Effect.forEach( + projects, + (project) => withProjectRuntime(project, runtimeForProject(runtimeByProject, project)), + { concurrency: "unbounded" } + ) + ) + ) + ), Effect.catchAll(() => Effect.succeed([] as ReadonlyArray)) ) @@ -228,7 +289,8 @@ export const getProject = ( ) => Effect.gen(function*(_) { const project = yield* _(findProjectById(projectId)) - const summary = yield* _(withProjectRuntime(project)) + const runtimeByProject = yield* _(loadProjectRuntimeByProject([project])) + const summary = yield* _(withProjectRuntime(project, runtimeForProject(runtimeByProject, project))) return toProjectDetails(project, summary) }) @@ -290,13 +352,21 @@ export const createProjectFromRequest = ( ) } - const command = { + const parsedCommand = { ...parsed.right, openSsh: false, waitForClone: request.waitForClone ?? parsed.right.waitForClone } - yield* _(seedAuthorizedKeysForCreate(command.outDir, request.authorizedKeysContents)) + const resolvedAuthorizedKeysContents = request.authorizedKeysContents ?? ( + request.useManagedAuthorizedKeys === true + ? yield* _(resolveCreateAuthorizedKeysContents(parsedCommand.outDir, parsedCommand.config.authorizedKeysPath)) + : undefined + ) + + const command = withManagedAuthorizedKeysForCreate(parsedCommand, resolvedAuthorizedKeysContents) + + yield* _(seedAuthorizedKeysForCreate(command.outDir, resolvedAuthorizedKeysContents)) yield* _(ensureGithubAuthForCreate(command.config)) @@ -324,7 +394,8 @@ export const createProjectFromRequest = ( command.config.repoRef ) ) - const summary = yield* _(withProjectRuntime(project)) + const runtimeByProject = yield* _(loadProjectRuntimeByProject([project])) + const summary = yield* _(withProjectRuntime(project, runtimeForProject(runtimeByProject, project))) yield* _( Effect.sync(() => { @@ -336,7 +407,7 @@ export const createProjectFromRequest = ( ) return toProjectDetails(project, summary) - }) + }).pipe(Effect.mapError(toProjectApiError)) export const deleteProjectById = ( projectId: string @@ -349,7 +420,7 @@ export const deleteProjectById = ( emitProjectEvent(projectId, "project.deleted", { projectId }) }) ) - }) + }).pipe(Effect.mapError(toProjectApiError)) const markDeployment = (projectId: string, phase: string, message: string) => Effect.sync(() => { @@ -429,18 +500,27 @@ const syncContainerAuthorizedKeys = ( export const upProject = ( projectId: string, - authorizedKeysContents?: string + authorizedKeysContents?: string, + useManagedAuthorizedKeys?: boolean ) => Effect.gen(function*(_) { const project = yield* _(findProjectById(projectId)) - yield* _(seedAuthorizedKeysForCreate(project.projectDir, authorizedKeysContents)) + const resolvedAuthorizedKeysContents = authorizedKeysContents ?? ( + useManagedAuthorizedKeys === true + ? yield* _(resolveManagedAuthorizedKeysContents()) + : undefined + ) + yield* _(seedAuthorizedKeysForCreate(project.projectDir, resolvedAuthorizedKeysContents)) yield* _(markDeployment(projectId, "build", "docker compose up -d --build")) yield* _(runDockerComposeUpWithPortCheck(project.projectDir)) - if ((authorizedKeysContents ?? "").trim().length > 0) { + if ((resolvedAuthorizedKeysContents ?? "").trim().length > 0) { yield* _(syncContainerAuthorizedKeys(project)) } yield* _(markDeployment(projectId, "running", "Container running")) - }) + const runtimeByProject = yield* _(loadProjectRuntimeByProject([project])) + const summary = yield* _(withProjectRuntime(project, runtimeForProject(runtimeByProject, project))) + return toProjectDetails(project, summary) + }).pipe(Effect.mapError(toProjectApiError)) export const downProject = ( projectId: string @@ -450,7 +530,7 @@ export const downProject = ( yield* _(markDeployment(projectId, "down", "docker compose down")) yield* _(runComposeCapture(projectId, project.projectDir, ["down"], [0, 1])) yield* _(markDeployment(projectId, "idle", "Container stopped")) - }) + }).pipe(Effect.mapError(toProjectApiError)) export const recreateProject = ( projectId: string @@ -477,7 +557,7 @@ export const recreateProject = ( yield* _(runComposeCapture(projectId, project.projectDir, ["down"], [0, 1])) yield* _(runDockerComposeUpWithPortCheck(project.projectDir)) yield* _(markDeployment(projectId, "running", "Recreate completed")) - }) + }).pipe(Effect.mapError(toProjectApiError)) export const readProjectPs = ( projectId: string @@ -485,7 +565,7 @@ export const readProjectPs = ( Effect.gen(function*(_) { const project = yield* _(findProjectById(projectId)) return yield* _(runComposeCapture(projectId, project.projectDir, ["ps"], [0])) - }) + }).pipe(Effect.mapError(toProjectApiError)) export const readProjectLogs = ( projectId: string @@ -493,6 +573,6 @@ export const readProjectLogs = ( Effect.gen(function*(_) { const project = yield* _(findProjectById(projectId)) return yield* _(runComposeCapture(projectId, project.projectDir, ["logs", "--tail", "200"], [0, 1])) - }) + }).pipe(Effect.mapError(toProjectApiError)) export const resolveProjectById = findProjectById diff --git a/packages/api/src/services/terminal-sessions.ts b/packages/api/src/services/terminal-sessions.ts new file mode 100644 index 00000000..b0ddd007 --- /dev/null +++ b/packages/api/src/services/terminal-sessions.ts @@ -0,0 +1,387 @@ +import { prepareProjectSsh, waitForProjectSshReady } from "@effect-template/lib" +import * as ParseResult from "@effect/schema/ParseResult" +import * as Schema from "@effect/schema/Schema" +import { Effect, Either } from "effect" +import { randomUUID } from "node:crypto" +import type { IncomingMessage, Server as HttpServer } from "node:http" +import type { Duplex } from "node:stream" +import { spawn, type IPty } from "node-pty" +import { WebSocket, WebSocketServer, type RawData } from "ws" + +import type { TerminalSession, TerminalSessionStatus } from "../api/contracts.js" +import { ApiConflictError, ApiInternalError, ApiNotFoundError, describeUnknown } from "../api/errors.js" +import { emitProjectEvent } from "./events.js" +import { getProjectItemById, upProject } from "./projects.js" + +type TerminalClientMessage = + | { readonly type: "input"; readonly data: string } + | { readonly type: "resize"; readonly cols: number; readonly rows: number } + | { readonly type: "close" } + +type TerminalServerMessage = + | { readonly type: "ready"; readonly session: TerminalSession } + | { readonly type: "output"; readonly data: string } + | { readonly type: "exit"; readonly exitCode: number | null; readonly signal: number | null } + | { readonly type: "error"; readonly message: string } + +type TerminalRecord = { + session: TerminalSession + pty: IPty | null + socket: WebSocket | null + attachTimeout: ReturnType | null + projectId: string + prepared: ReturnType +} + +const records = new Map() +const attachTimeoutMs = 30_000 +const terminalWsPathPattern = /^(?:\/api)?\/projects\/([^/]+)\/terminal-sessions\/([^/]+)\/ws$/u + +const TerminalClientMessageSchema = Schema.parseJson( + Schema.Union( + Schema.Struct({ + type: Schema.Literal("input"), + data: Schema.String + }), + Schema.Struct({ + type: Schema.Literal("resize"), + cols: Schema.Number, + rows: Schema.Number + }), + Schema.Struct({ + type: Schema.Literal("close") + }) + ) +) + +const nowIso = (): string => new Date().toISOString() + +const updateSession = ( + record: TerminalRecord, + patch: Partial +): void => { + record.session = { + ...record.session, + ...patch + } + records.set(record.session.id, record) +} + +const toApiInternalError = (error: unknown): ApiInternalError => + error instanceof ApiInternalError + ? error + : new ApiInternalError({ + message: describeUnknown(error), + cause: error + }) + +const encodeServerMessage = (message: TerminalServerMessage): string => JSON.stringify(message) + +const sendServerMessage = (socket: WebSocket | null, message: TerminalServerMessage): void => { + if (socket === null || socket.readyState !== WebSocket.OPEN) { + return + } + socket.send(encodeServerMessage(message)) +} + +const clearAttachTimeout = (record: TerminalRecord): void => { + if (record.attachTimeout !== null) { + clearTimeout(record.attachTimeout) + record.attachTimeout = null + } +} + +const closeSocket = (socket: WebSocket | null): void => { + if (socket === null || socket.readyState === WebSocket.CLOSED) { + return + } + socket.close() +} + +const cleanupRecord = (record: TerminalRecord): void => { + clearAttachTimeout(record) + if (record.pty !== null) { + record.pty.kill() + record.pty = null + } + closeSocket(record.socket) + record.socket = null + records.delete(record.session.id) +} + +const finalizeRecord = ( + record: TerminalRecord, + status: Extract, + exitCode: number | null, + signal: number | null +): void => { + updateSession(record, { + closedAt: nowIso(), + exitCode: exitCode ?? undefined, + signal: signal ?? undefined, + status + }) + sendServerMessage(record.socket, { type: "exit", exitCode, signal }) + closeSocket(record.socket) + record.socket = null + record.pty = null + clearAttachTimeout(record) + records.delete(record.session.id) +} + +const decodeClientMessage = (raw: RawData): TerminalClientMessage | null => + Either.getOrNull( + ParseResult.decodeUnknownEither(TerminalClientMessageSchema)( + typeof raw === "string" + ? raw + : Array.isArray(raw) + ? Buffer.concat(raw).toString("utf8") + : raw instanceof ArrayBuffer + ? Buffer.from(new Uint8Array(raw)).toString("utf8") + : raw.toString("utf8") + ) + ) + +const clampTerminalSize = (value: number, fallback: number): number => + Number.isFinite(value) && value > 0 ? Math.max(1, Math.floor(value)) : fallback + +const startTerminalPty = ( + record: TerminalRecord, + cols: number, + rows: number +): void => { + const resolvedCols = clampTerminalSize(cols, 120) + const resolvedRows = clampTerminalSize(rows, 32) + const pty = spawn(record.prepared.command, [...record.prepared.args], { + cols: resolvedCols, + cwd: record.prepared.cwd, + env: { + ...process.env, + TERM: "xterm-256color" + }, + name: "xterm-256color", + rows: resolvedRows + }) + record.pty = pty + updateSession(record, { + startedAt: nowIso(), + status: "attached" + }) + pty.onData((data) => { + sendServerMessage(record.socket, { type: "output", data }) + }) + pty.onExit(({ exitCode, signal }) => { + finalizeRecord( + record, + exitCode === 0 || exitCode === 130 ? "exited" : "failed", + exitCode ?? null, + signal ?? null + ) + }) +} + +const createAttachTimeout = (sessionId: string): ReturnType => + setTimeout(() => { + const record = records.get(sessionId) + if (record !== undefined) { + cleanupRecord(record) + } + }, attachTimeoutMs) + +const registerRecord = ( + projectId: string, + prepared: ReturnType +): TerminalSession => { + const session: TerminalSession = { + createdAt: nowIso(), + id: randomUUID(), + projectId, + sshCommand: prepared.item.sshCommand, + status: "ready" + } + const record: TerminalRecord = { + attachTimeout: null, + prepared, + projectId, + pty: null, + session, + socket: null + } + record.attachTimeout = createAttachTimeout(session.id) + records.set(session.id, record) + return session +} + +export const createTerminalSession = ( + projectId: string +) => + Effect.gen(function*(_) { + yield* _( + Effect.sync(() => { + emitProjectEvent(projectId, "project.deployment.status", { + phase: "ssh.prepare", + message: "Preparing SSH session" + }) + }) + ) + const project = yield* _(upProject(projectId, undefined, true)) + const projectItem = yield* _(getProjectItemById(projectId)) + yield* _( + Effect.sync(() => { + emitProjectEvent(projectId, "project.deployment.status", { + phase: "ssh.wait", + message: "Waiting for SSH" + }) + }) + ) + yield* _(waitForProjectSshReady(projectItem).pipe(Effect.mapError(toApiInternalError))) + yield* _( + Effect.sync(() => { + emitProjectEvent(projectId, "project.deployment.status", { + phase: "ssh.ready", + message: "SSH is ready" + }) + }) + ) + const prepared = prepareProjectSsh(projectItem) + const session = registerRecord(projectId, prepared) + yield* _( + Effect.sync(() => { + emitProjectEvent(projectId, "project.ssh.session", { + phase: "created", + sessionId: session.id + }) + }) + ) + return { project, session } + }) + +export const deleteTerminalSession = ( + projectId: string, + sessionId: string +): Effect.Effect => + Effect.gen(function*(_) { + const record = records.get(sessionId) + if (record === undefined || record.projectId !== projectId) { + return yield* _( + Effect.fail(new ApiNotFoundError({ message: `Terminal session not found: ${sessionId}` })) + ) + } + cleanupRecord(record) + yield* _( + Effect.sync(() => { + emitProjectEvent(projectId, "project.ssh.session", { + phase: "closed", + sessionId + }) + }) + ) + }) + +const handleCloseMessage = (record: TerminalRecord): void => { + cleanupRecord(record) +} + +const handleSocketMessage = (record: TerminalRecord, raw: RawData): void => { + const message = decodeClientMessage(raw) + if (message === null) { + sendServerMessage(record.socket, { type: "error", message: "Invalid terminal payload." }) + return + } + if (message.type === "input") { + record.pty?.write(message.data) + return + } + if (message.type === "resize") { + record.pty?.resize(clampTerminalSize(message.cols, 120), clampTerminalSize(message.rows, 32)) + return + } + handleCloseMessage(record) +} + +const attachSocketToRecord = ( + record: TerminalRecord, + socket: WebSocket, + cols: number, + rows: number +): void => { + if (record.socket !== null) { + throw new ApiConflictError({ message: `Terminal session already attached: ${record.session.id}` }) + } + + clearAttachTimeout(record) + record.socket = socket + startTerminalPty(record, cols, rows) + sendServerMessage(socket, { type: "ready", session: record.session }) + socket.on("message", (raw: RawData) => { + handleSocketMessage(record, raw) + }) + socket.on("close", () => { + const current = records.get(record.session.id) + if (current !== undefined) { + cleanupRecord(current) + } + }) +} + +const parseTerminalPath = ( + request: IncomingMessage +): { readonly cols: number; readonly projectId: string; readonly rows: number; readonly sessionId: string } | null => { + const url = request.url + if (url === undefined) { + return null + } + const parsed = new URL(url, "http://localhost") + const match = terminalWsPathPattern.exec(parsed.pathname) + if (match === null) { + return null + } + return { + cols: clampTerminalSize(Number(parsed.searchParams.get("cols") ?? ""), 120), + projectId: decodeURIComponent(match[1] ?? ""), + rows: clampTerminalSize(Number(parsed.searchParams.get("rows") ?? ""), 32), + sessionId: decodeURIComponent(match[2] ?? "") + } +} + +const denyUpgrade = (socket: Duplex): void => { + socket.write("HTTP/1.1 404 Not Found\r\n\r\n") + socket.destroy() +} + +export const attachTerminalWebSocketServer = (server: HttpServer): void => { + const webSocketServer = new WebSocketServer({ noServer: true }) + server.on("upgrade", (request, socket, head) => { + const parsed = parseTerminalPath(request) + if (parsed === null) { + return + } + const record = records.get(parsed.sessionId) + if (record === undefined || record.projectId !== parsed.projectId) { + denyUpgrade(socket) + return + } + webSocketServer.handleUpgrade(request, socket, head, (webSocket: WebSocket) => { + try { + attachSocketToRecord(record, webSocket, parsed.cols, parsed.rows) + } catch (error) { + sendServerMessage(webSocket, { type: "error", message: describeUnknown(error) }) + webSocket.close() + } + }) + }) +} + +export const verifyTerminalSession = ( + projectId: string, + sessionId: string +): Effect.Effect => + Effect.gen(function*(_) { + const record = records.get(sessionId) + if (record === undefined || record.projectId !== projectId) { + return yield* _( + Effect.fail(new ApiNotFoundError({ message: `Terminal session not found: ${sessionId}` })) + ) + } + return record.session + }) diff --git a/packages/api/tests/auth.test.ts b/packages/api/tests/auth.test.ts index 0461e5c4..edbfca1c 100644 --- a/packages/api/tests/auth.test.ts +++ b/packages/api/tests/auth.test.ts @@ -1,8 +1,10 @@ import * as FileSystem from "@effect/platform/FileSystem" +import type { PlatformError } from "@effect/platform/Error" import * as Path from "@effect/platform/Path" import { NodeContext } from "@effect/platform-node" import { describe, expect, it } from "@effect/vitest" import { Effect } from "effect" +import * as Scope from "effect/Scope" import { vi } from "vitest" import { githubRepoAccessMessage } from "@effect-template/lib/usecases/github-token-preflight" @@ -19,7 +21,7 @@ import { createProjectFromRequest } from "../src/services/projects.js" const withTempDir = ( use: (tempDir: string) => Effect.Effect -) => +): Effect.Effect> => Effect.scoped( Effect.gen(function*(_) { const fs = yield* _(FileSystem.FileSystem) @@ -35,18 +37,19 @@ const withTempDir = ( const withWorkingDirectory = ( cwd: string, effect: Effect.Effect -) => - Effect.acquireUseRelease( - Effect.sync(() => { - const previous = process.cwd() - process.chdir(cwd) - return previous - }), - () => effect, - (previous) => +): Effect.Effect => + Effect.scoped( + Effect.acquireRelease( Effect.sync(() => { - process.chdir(previous) - }) + const previous = process.cwd() + process.chdir(cwd) + return previous + }), + (previous) => + Effect.sync(() => { + process.chdir(previous) + }) + ).pipe(Effect.flatMap(() => effect)) ) const resolveFetchUrl = (input: Parameters[0]): string => @@ -59,39 +62,41 @@ const resolveFetchUrl = (input: Parameters[0]): string const withProjectsRoot = ( projectsRoot: string, effect: Effect.Effect -) => - Effect.acquireUseRelease( - Effect.sync(() => { - const previous = process.env["DOCKER_GIT_PROJECTS_ROOT"] - process.env["DOCKER_GIT_PROJECTS_ROOT"] = projectsRoot - return previous - }), - () => effect, - (previous) => +): Effect.Effect => + Effect.scoped( + Effect.acquireRelease( Effect.sync(() => { - if (previous === undefined) { - delete process.env["DOCKER_GIT_PROJECTS_ROOT"] - return - } - process.env["DOCKER_GIT_PROJECTS_ROOT"] = previous - }) + const previous = process.env["DOCKER_GIT_PROJECTS_ROOT"] + process.env["DOCKER_GIT_PROJECTS_ROOT"] = projectsRoot + return previous + }), + (previous) => + Effect.sync(() => { + if (previous === undefined) { + delete process.env["DOCKER_GIT_PROJECTS_ROOT"] + return + } + process.env["DOCKER_GIT_PROJECTS_ROOT"] = previous + }) + ).pipe(Effect.flatMap(() => effect)) ) const withPatchedFetch = ( fetchImpl: typeof globalThis.fetch, effect: Effect.Effect -) => - Effect.acquireUseRelease( - Effect.sync(() => { - const previous = globalThis.fetch - globalThis.fetch = fetchImpl - return previous - }), - () => effect, - (previous) => +): Effect.Effect => + Effect.scoped( + Effect.acquireRelease( Effect.sync(() => { - globalThis.fetch = previous - }) + const previous = globalThis.fetch + globalThis.fetch = fetchImpl + return previous + }), + (previous) => + Effect.sync(() => { + globalThis.fetch = previous + }) + ).pipe(Effect.flatMap(() => effect)) ) describe("api auth", () => { diff --git a/packages/api/tests/projects.test.ts b/packages/api/tests/projects.test.ts index 600d51ff..9f381612 100644 --- a/packages/api/tests/projects.test.ts +++ b/packages/api/tests/projects.test.ts @@ -1,15 +1,17 @@ import * as FileSystem from "@effect/platform/FileSystem" +import type { PlatformError } from "@effect/platform/Error" import * as Path from "@effect/platform/Path" import { NodeContext } from "@effect/platform-node" import { describe, expect, it } from "@effect/vitest" import { Effect } from "effect" +import * as Scope from "effect/Scope" -import { ApiConflictError } from "../src/api/errors.js" +import { ApiConflictError, ApiInternalError } from "../src/api/errors.js" import { createProjectFromRequest, seedAuthorizedKeysForCreate } from "../src/services/projects.js" const withTempDir = ( use: (tempDir: string) => Effect.Effect -) => +): Effect.Effect> => Effect.scoped( Effect.gen(function*(_) { const fs = yield* _(FileSystem.FileSystem) @@ -25,39 +27,68 @@ const withTempDir = ( const withWorkingDirectory = ( cwd: string, effect: Effect.Effect -) => - Effect.acquireUseRelease( - Effect.sync(() => { - const previous = process.cwd() - process.chdir(cwd) - return previous - }), - () => effect, - (previous) => +): Effect.Effect => + Effect.scoped( + Effect.acquireRelease( Effect.sync(() => { - process.chdir(previous) - }) + const previous = process.cwd() + process.chdir(cwd) + return previous + }), + (previous) => + Effect.sync(() => { + process.chdir(previous) + }) + ).pipe(Effect.flatMap(() => effect)) ) const withProjectsRoot = ( projectsRoot: string, effect: Effect.Effect -) => - Effect.acquireUseRelease( - Effect.sync(() => { - const previous = process.env["DOCKER_GIT_PROJECTS_ROOT"] - process.env["DOCKER_GIT_PROJECTS_ROOT"] = projectsRoot - return previous - }), - () => effect, - (previous) => +): Effect.Effect => + Effect.scoped( + Effect.acquireRelease( + Effect.sync(() => { + const previous = process.env["DOCKER_GIT_PROJECTS_ROOT"] + process.env["DOCKER_GIT_PROJECTS_ROOT"] = projectsRoot + return previous + }), + (previous) => + Effect.sync(() => { + if (previous === undefined) { + delete process.env["DOCKER_GIT_PROJECTS_ROOT"] + } else { + process.env["DOCKER_GIT_PROJECTS_ROOT"] = previous + } + }) + ).pipe(Effect.flatMap(() => effect)) + ) + +const withEnvVar = ( + key: string, + value: string | undefined, + effect: Effect.Effect +): Effect.Effect => + Effect.scoped( + Effect.acquireRelease( Effect.sync(() => { - if (previous === undefined) { - delete process.env["DOCKER_GIT_PROJECTS_ROOT"] + const previous = process.env[key] + if (value === undefined) { + delete process.env[key] } else { - process.env["DOCKER_GIT_PROJECTS_ROOT"] = previous + process.env[key] = value } - }) + return previous + }), + (previous) => + Effect.sync(() => { + if (previous === undefined) { + delete process.env[key] + } else { + process.env[key] = previous + } + }) + ).pipe(Effect.flatMap(() => effect)) ) describe("projects service", () => { @@ -88,6 +119,37 @@ describe("projects service", () => { }) ).pipe(Effect.provide(NodeContext.layer))) + it.effect("renders docker access failures for API create without leaking stack traces", () => + withTempDir((root) => + Effect.gen(function*(_) { + const path = yield* _(Path.Path) + const projectsRoot = path.join(root, ".docker-git") + + const failure = yield* _( + withProjectsRoot( + projectsRoot, + withEnvVar( + "DOCKER_HOST", + "unix:///definitely-missing-docker.sock", + withWorkingDirectory( + root, + createProjectFromRequest({ + repoUrl: "https://example.com/org/repo.git", + skipGithubAuth: true + }).pipe(Effect.flip) + ) + ) + ) + ) + + expect(failure).toBeInstanceOf(ApiInternalError) + if (failure instanceof ApiInternalError) { + expect(failure.message).toContain("Cannot connect to Docker daemon.") + expect(failure.message).not.toContain("docker-daemon-access.js") + } + }) + ).pipe(Effect.provide(NodeContext.layer))) + it.effect("maps duplicate docker identities to API conflict for create", () => withTempDir((root) => Effect.gen(function*(_) { diff --git a/packages/api/tests/schema.test.ts b/packages/api/tests/schema.test.ts index 39cb945b..82d837ad 100644 --- a/packages/api/tests/schema.test.ts +++ b/packages/api/tests/schema.test.ts @@ -14,6 +14,7 @@ import { StateCommitRequestSchema, StateInitRequestSchema, StateSyncRequestSchema, + TerminalSessionSchema, UpProjectRequestSchema } from "../src/api/schema.js" @@ -247,4 +248,30 @@ describe("api schemas", () => { } }) })) + + it.effect("decodes terminal session payload", () => + Effect.sync(() => { + const result = Schema.decodeUnknownEither(TerminalSessionSchema)({ + id: "session-1", + projectId: "project-1", + sshCommand: "ssh dev@127.0.0.1", + status: "attached", + createdAt: "2026-04-08T10:00:00.000Z", + startedAt: "2026-04-08T10:00:01.000Z", + exitCode: 0 + }) + + Either.match(result, { + onLeft: (error) => { + throw new Error(ParseResult.TreeFormatter.formatIssueSync(error.issue)) + }, + onRight: (value) => { + expect(value.id).toBe("session-1") + expect(value.projectId).toBe("project-1") + expect(value.status).toBe("attached") + expect(value.exitCode).toBe(0) + expect(value.signal).toBeUndefined() + } + }) + })) }) diff --git a/packages/app/eslint.config.mts b/packages/app/eslint.config.mts index 1f1fb193..0a1fba61 100644 --- a/packages/app/eslint.config.mts +++ b/packages/app/eslint.config.mts @@ -59,7 +59,7 @@ export default defineConfig( "simple-import-sort": simpleImportSort, codegen: codegenPlugin, }, - files: ["**/*.ts", '**/*.{test,spec}.{ts,tsx}', '**/tests/**', '**/__tests__/**'], + files: ["**/*.{ts,tsx}", '**/*.{test,spec}.{ts,tsx}', '**/tests/**', '**/__tests__/**'], settings: { "import/parsers": { "@typescript-eslint/parser": [".ts", ".tsx"], diff --git a/packages/app/eslint/no-lib-imports.mjs b/packages/app/eslint/no-lib-imports.mjs index 485c6c2f..af0abc36 100644 --- a/packages/app/eslint/no-lib-imports.mjs +++ b/packages/app/eslint/no-lib-imports.mjs @@ -1,6 +1,27 @@ // @ts-check const bannedPackageName = "@effect-template/lib" +const bannedLocalAlias = "@lib" + +/** @param {string} value */ +const isDirectLocalAliasImport = (value) => + value === bannedLocalAlias || value.startsWith(`${bannedLocalAlias}/`) + +/** @param {string} value */ +const isRelativeLocalLibImport = (value) => /^(?:\.\.\/|\.\/)+(?:src\/)?lib(?:\/|$)/u.test(value) + +/** @param {string | undefined} filePath */ +const isFrontendSurfaceFile = (filePath) => { + const normalized = (filePath ?? "").replaceAll("\\", "/") + return normalized.startsWith("src/app/") || + normalized.startsWith("src/docker-git/") || + normalized.startsWith("src/web/") || + normalized.startsWith("tests/") || + normalized.includes("/src/app/") || + normalized.includes("/src/docker-git/") || + normalized.includes("/src/web/") || + normalized.includes("/tests/") +} /** * @typedef {{ readonly type: "Literal", readonly value: unknown }} LiteralSourceNode @@ -74,11 +95,27 @@ const readSourceText = (source) => { return null } +/** + * @param {import("eslint").Rule.RuleContext} context + * @returns {string} + */ +const readFilename = (context) => { + const candidate = /** @type {Record} */ (/** @type {unknown} */ (context))["getFilename"] + if (typeof candidate === "function") { + const filename = candidate.call(context) + return typeof filename === "string" ? filename : "" + } + + return "filename" in context && typeof context.filename === "string" ? context.filename : "" +} + /** * @param {import("eslint").Rule.RuleContext} context * @returns {import("eslint").Rule.RuleListener} */ const createRuleListener = (context) => { + const filePath = readFilename(context) + /** @param {unknown} source */ const checkSource = (source) => { if (source == null) { @@ -86,7 +123,20 @@ const createRuleListener = (context) => { } const sourceText = readSourceText(source) - if (sourceText === null || !isDirectLibImport(sourceText)) { + if (sourceText === null) { + return + } + + if (isDirectLibImport(sourceText) || isDirectLocalAliasImport(sourceText)) { + context.report({ + node: /** @type {import("eslint").JSSyntaxElement} */ (source), + messageId: "noLibImport", + data: { source: sourceText } + }) + return + } + + if (!isFrontendSurfaceFile(filePath) || !isRelativeLocalLibImport(sourceText)) { return } @@ -148,12 +198,12 @@ export const noLibImportsRule = { type: "problem", docs: { description: - "forbid direct imports, re-exports, and require calls from @effect-template/lib inside package/app" + "forbid direct imports, re-exports, and require calls from legacy lib surfaces inside package/app frontend surfaces and tests" }, schema: [], messages: { noLibImport: - "Direct import or require '{{source}}' from @effect-template/lib is forbidden in package/app. Use the API client or a local app adapter instead." + "Direct import or require '{{source}}' from legacy lib surfaces is forbidden in package/app frontend surfaces and tests. Use the API client or a local app adapter instead." } }, create: createRuleListener diff --git a/packages/app/index.html b/packages/app/index.html new file mode 100644 index 00000000..8d6518cb --- /dev/null +++ b/packages/app/index.html @@ -0,0 +1,49 @@ + + + + + + docker-git browser + + + + + + +
+ + + diff --git a/packages/app/package.json b/packages/app/package.json index 54514dcc..78955ad4 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,7 +1,7 @@ { "name": "@prover-coder-ai/docker-git", "version": "1.0.77", - "description": "Minimal Vite-powered TypeScript console starter using Effect", + "description": "docker-git Bun and Gridland CLI plus browser frontend", "main": "dist/src/docker-git/main.js", "bin": { "docker-git": "dist/src/docker-git/main.js" @@ -13,27 +13,30 @@ "doc": "doc" }, "scripts": { - "prebuild": "pnpm -C ../lib build", - "build": "pnpm run build:app && pnpm run build:docker-git", + "prebuild": "bun run --cwd ../lib build", + "build": "bun run build:app && bun run build:docker-git", "build:app": "vite build --ssr src/app/main.ts", - "prepack": "pnpm run build:docker-git", + "build:web": "vite build --config vite.web.config.ts", + "prepack": "bun run build:docker-git", "dev": "vite build --watch --ssr src/app/main.ts", - "prelint": "pnpm -C ../lib build", + "dev:web": "vite --config vite.web.config.ts", + "serve:web": "bun scripts/serve-dist-web.mjs", + "prelint": "bun run --cwd ../lib build", "lint": "NODE_OPTIONS=--max-old-space-size=4096 PATH=../../scripts:$PATH vibecode-linter src/", "lint:tests": "NODE_OPTIONS=--max-old-space-size=4096 PATH=../../scripts:$PATH vibecode-linter tests/", "lint:effect": "NODE_OPTIONS=--max-old-space-size=4096 PATH=../../scripts:$PATH eslint --config eslint.effect-ts-check.config.mjs .", - "prebuild:docker-git": "pnpm -C ../lib build", + "prebuild:docker-git": "bun run --cwd ../lib build", "build:docker-git": "vite build --config vite.docker-git.config.ts", - "check": "pnpm run typecheck", - "clone": "pnpm -C ../.. run clone", - "open": "pnpm -C ../.. run open", - "docker-git": "node dist/src/docker-git/main.js", - "list": "pnpm -C ../.. run list", - "prestart": "pnpm run build", - "start": "node dist/main.js", - "pretest": "pnpm -C ../lib build", - "test": "pnpm run lint:tests && vitest run", - "pretypecheck": "pnpm -C ../lib build", + "check": "bun run typecheck", + "clone": "bash -lc 'bun run build:docker-git >/dev/null && bun dist/src/docker-git/main.js clone \"$@\"' --", + "open": "bash -lc 'bun run build:docker-git >/dev/null && bun dist/src/docker-git/main.js open \"$@\"' --", + "docker-git": "bash -lc 'bun run build:docker-git >/dev/null && bun dist/src/docker-git/main.js \"$@\"' --", + "list": "bash -lc 'bun run build:docker-git >/dev/null && bun dist/src/docker-git/main.js ps \"$@\"' --", + "preview:web": "vite preview --config vite.web.config.ts", + "start": "bash -lc 'bun run build:docker-git >/dev/null && bun dist/src/docker-git/main.js \"$@\"' --", + "pretest": "bun run --cwd ../lib build", + "test": "bun run lint:tests && vitest run", + "pretypecheck": "bun run --cwd ../lib build", "typecheck": "tsc --noEmit" }, "repository": { @@ -56,7 +59,7 @@ "access": "public" }, "homepage": "https://github.com/ProverCoderAI/docker-git#readme", - "packageManager": "pnpm@10.32.1", + "packageManager": "bun@1.3.11", "dependencies": { "@effect/cli": "^0.75.0", "@effect/cluster": "^0.58.0", @@ -70,15 +73,19 @@ "@effect/sql": "^0.51.0", "@effect/typeclass": "^0.40.0", "@effect/workflow": "^0.18.0", + "@gridland/bun": "0.2.53", + "@gridland/web": "0.2.53", "effect": "^3.21.0", - "ink": "^6.8.0", "react": "^19.2.4", + "react-dom": "^19.2.4", "react-reconciler": "^0.33.0", - "ts-morph": "^27.0.2" + "ts-morph": "^27.0.2", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0" }, "devDependencies": { - "@effect-template/lib": "workspace:*", "@biomejs/biome": "^2.4.8", + "@effect-template/lib": "workspace:*", "@effect/eslint-plugin": "^0.3.2", "@effect/language-service": "latest", "@effect/vitest": "^0.29.0", @@ -89,10 +96,14 @@ "@prover-coder-ai/eslint-plugin-suggest-members": "^0.0.25", "@ton-ai-core/vibecode-linter": "^1.0.11", "@types/node": "^24.12.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@typescript-eslint/eslint-plugin": "^8.57.1", "@typescript-eslint/parser": "^8.57.1", - "typescript-eslint": "^8.57.1", + "@vitejs/plugin-react": "^6.0.1", "@vitest/coverage-v8": "^4.1.0", + "@vitest/eslint-plugin": "^1.6.13", + "biome": "npm:@biomejs/biome@^2.4.8", "eslint": "^10.1.0", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-codegen": "0.34.1", @@ -101,14 +112,11 @@ "eslint-plugin-sonarjs": "^4.0.2", "eslint-plugin-sort-destructure-keys": "^3.0.0", "eslint-plugin-unicorn": "^63.0.0", - "@vitest/eslint-plugin": "^1.6.13", - "@types/react": "^19.2.14", - "biome": "npm:@biomejs/biome@^2.4.8", "globals": "^17.4.0", "jscpd": "^4.0.8", "typescript": "^5.9.3", + "typescript-eslint": "^8.57.1", "vite": "^8.0.1", - "vite-tsconfig-paths": "^6.1.1", "vitest": "^4.1.0" } } diff --git a/packages/app/scripts/serve-dist-web.mjs b/packages/app/scripts/serve-dist-web.mjs new file mode 100644 index 00000000..b0335c11 --- /dev/null +++ b/packages/app/scripts/serve-dist-web.mjs @@ -0,0 +1,153 @@ +import { createReadStream, existsSync, statSync } from "node:fs" +import { createServer, request as httpRequest } from "node:http" +import { extname, join, normalize } from "node:path" + +import { WebSocket, WebSocketServer } from "ws" + +const appRoot = "/home/dev/workspaces/provercoderai/docker-git/packages/app" +const staticRoot = join(appRoot, "dist-web") +const apiHost = process.env["DOCKER_GIT_API_HOST"]?.trim() || "127.0.0.1" +const apiPort = Number(process.env["DOCKER_GIT_API_PORT"]?.trim() || "3334") +const host = process.env["DOCKER_GIT_WEB_HOST"]?.trim() || "127.0.0.1" +const port = Number(process.env["DOCKER_GIT_WEB_PORT"]?.trim() || "4191") + +const contentTypes = { + ".css": "text/css; charset=utf-8", + ".html": "text/html; charset=utf-8", + ".ico": "image/x-icon", + ".jpeg": "image/jpeg", + ".jpg": "image/jpeg", + ".js": "application/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".map": "application/json; charset=utf-8", + ".png": "image/png", + ".svg": "image/svg+xml" +} + +const noStoreHeaders = { + "cache-control": "no-store" +} + +const resolveStaticPath = (pathname) => { + const normalized = normalize(pathname) + return normalized.startsWith(staticRoot) + ? normalized + : join(staticRoot, normalized) +} + +const serveFile = ( + pathname, + response +) => { + const filePath = resolveStaticPath(pathname) + if (!existsSync(filePath) || statSync(filePath).isDirectory()) { + response.writeHead(404, noStoreHeaders) + response.end("Not found") + return + } + + response.writeHead(200, { + ...noStoreHeaders, + "content-type": contentTypes[extname(filePath)] ?? "application/octet-stream" + }) + createReadStream(filePath).pipe(response) +} + +const resolveUpstreamPath = (url) => { + const parsed = new URL(url, "http://localhost") + const pathname = parsed.pathname.replace(/^\/api/u, "") || "/" + return `${pathname}${parsed.search}` +} + +const proxyHttp = ( + request, + response +) => { + const upstream = httpRequest( + { + headers: { ...request.headers, host: `${apiHost}:${apiPort}` }, + host: apiHost, + method: request.method, + path: resolveUpstreamPath(request.url ?? "/"), + port: apiPort + }, + (upstreamResponse) => { + response.writeHead(upstreamResponse.statusCode ?? 502, { + ...upstreamResponse.headers, + ...noStoreHeaders + }) + upstreamResponse.pipe(response) + } + ) + + upstream.on("error", (error) => { + response.writeHead(502, { + ...noStoreHeaders, + "content-type": "text/plain; charset=utf-8" + }) + response.end(String(error)) + }) + + request.pipe(upstream) +} + +const webSocketServer = new WebSocketServer({ noServer: true }) + +const server = createServer((request, response) => { + const parsed = new URL(request.url ?? "/", "http://localhost") + if (parsed.pathname.startsWith("/api/")) { + proxyHttp(request, response) + return + } + + if (parsed.pathname === "/" || parsed.pathname === "/index.html") { + serveFile(join(staticRoot, "index.html"), response) + return + } + + const candidate = join(staticRoot, parsed.pathname) + if (existsSync(candidate) && statSync(candidate).isFile()) { + serveFile(candidate, response) + return + } + + serveFile(join(staticRoot, "index.html"), response) +}) + +server.on("upgrade", (request, socket, head) => { + const parsed = new URL(request.url ?? "/", "http://localhost") + if (!parsed.pathname.startsWith("/api/") || !parsed.pathname.endsWith("/ws")) { + socket.destroy() + return + } + + const upstream = new WebSocket(`ws://${apiHost}:${apiPort}${resolveUpstreamPath(request.url ?? "/")}`) + + upstream.on("open", () => { + webSocketServer.handleUpgrade(request, socket, head, (clientSocket) => { + clientSocket.on("message", (data, isBinary) => { + upstream.send(data, { binary: isBinary }) + }) + clientSocket.on("close", () => { + upstream.close() + }) + upstream.on("message", (data, isBinary) => { + clientSocket.send(data, { binary: isBinary }) + }) + upstream.on("close", () => { + clientSocket.close() + }) + upstream.on("error", () => { + clientSocket.close() + }) + }) + }) + + upstream.on("error", () => { + socket.destroy() + }) +}) + +server.listen(port, host, () => { + console.log(`docker-git web runtime listening on http://${host}:${port}`) +}) diff --git a/packages/app/src/app/program.ts b/packages/app/src/app/program.ts index 2b098677..12d48e22 100644 --- a/packages/app/src/app/program.ts +++ b/packages/app/src/app/program.ts @@ -1,5 +1,6 @@ -import { listProjects, readCloneRequest, runDockerGitClone, runDockerGitOpen } from "@lib" import { Console, Effect, Match, pipe } from "effect" +import { listProjects, renderProjectSummaryLine } from "../docker-git/api-client.js" +import { readCloneRequest, runDockerGitClone, runDockerGitOpen } from "../docker-git/frontend-lib/shell/clone.js" /** * Compose the CLI program as a single effect. @@ -26,10 +27,10 @@ import { Console, Effect, Match, pipe } from "effect" // COMPLEXITY: O(1) const usageText = [ "Usage:", - " pnpm docker-git", - " pnpm clone [ref]", - " pnpm open ", - " pnpm list", + " bun run docker-git", + " bun run clone [ref]", + " bun run open ", + " bun run list", "", "Notes:", " - docker-git is the interactive TUI.", @@ -42,7 +43,7 @@ const usageText = [ const runHelp = Console.log(usageText) // CHANGE: route between shortcut runners and help based on CLI context -// WHY: allow pnpm run clone/open while keeping a single entrypoint +// WHY: allow bun run clone/open while keeping a single entrypoint // QUOTE(ТЗ): "Добавить команду open." // REF: user-request-2026-01-27 // SOURCE: n/a @@ -71,7 +72,14 @@ const readListFlag = Effect.sync(() => { export const program = Effect.gen(function*(_) { const isList = yield* _(readListFlag) if (isList) { - yield* _(listProjects) + const projects = yield* _(listProjects()) + if (projects.length === 0) { + yield* _(Console.log("No docker-git projects found.")) + return + } + for (const project of projects) { + yield* _(Console.log(renderProjectSummaryLine(project))) + } return } yield* _(runDockerGit) diff --git a/packages/app/src/docker-git/api-auth-codec.ts b/packages/app/src/docker-git/api-auth-codec.ts new file mode 100644 index 00000000..c8ceeb86 --- /dev/null +++ b/packages/app/src/docker-git/api-auth-codec.ts @@ -0,0 +1,156 @@ +import { asObject, asString, type JsonValue } from "./api-json.js" +import type { AuthSnapshot, ProjectAuthSnapshot } from "./menu-types.js" + +type RawAuthSnapshot = { + readonly globalEnvPath: string | null + readonly claudeAuthPath: string | null + readonly geminiAuthPath: string | null + readonly totalEntries: number | null + readonly githubTokenEntries: number | null + readonly gitTokenEntries: number | null + readonly gitUserEntries: number | null + readonly claudeAuthEntries: number | null + readonly geminiAuthEntries: number | null +} + +type RawProjectAuthSnapshot = { + readonly projectDir: string | null + readonly projectName: string | null + readonly envGlobalPath: string | null + readonly envProjectPath: string | null + readonly claudeAuthPath: string | null + readonly geminiAuthPath: string | null + readonly githubTokenEntries: number | null + readonly gitTokenEntries: number | null + readonly claudeAuthEntries: number | null + readonly geminiAuthEntries: number | null + readonly activeGithubLabel: string | null + readonly activeGitLabel: string | null + readonly activeClaudeLabel: string | null + readonly activeGeminiLabel: string | null +} + +const readNumber = (value: JsonValue | undefined): number | null => typeof value === "number" ? value : null +const stringOrEmpty = (value: string | null): string => value ?? "" +const numberOrZero = (value: number | null): number => value ?? 0 +const hasNullValue = ( + values: ReadonlyArray +): boolean => values.includes(null) + +const resolveSnapshotObject = (payload: JsonValue) => { + const object = asObject(payload) + return asObject(object?.["snapshot"] ?? payload) +} + +const readAuthSnapshot = ( + snapshot: ReturnType +): RawAuthSnapshot | null => { + if (snapshot === null) { + return null + } + + return { + globalEnvPath: asString(snapshot["globalEnvPath"]), + claudeAuthPath: asString(snapshot["claudeAuthPath"]), + geminiAuthPath: asString(snapshot["geminiAuthPath"]), + totalEntries: readNumber(snapshot["totalEntries"]), + githubTokenEntries: readNumber(snapshot["githubTokenEntries"]), + gitTokenEntries: readNumber(snapshot["gitTokenEntries"]), + gitUserEntries: readNumber(snapshot["gitUserEntries"]), + claudeAuthEntries: readNumber(snapshot["claudeAuthEntries"]), + geminiAuthEntries: readNumber(snapshot["geminiAuthEntries"]) + } +} + +const decodeRequiredAuthSnapshot = (snapshot: RawAuthSnapshot): AuthSnapshot | null => { + if (hasNullValue(Object.values(snapshot))) { + return null + } + + return { + globalEnvPath: stringOrEmpty(snapshot.globalEnvPath), + claudeAuthPath: stringOrEmpty(snapshot.claudeAuthPath), + geminiAuthPath: stringOrEmpty(snapshot.geminiAuthPath), + totalEntries: numberOrZero(snapshot.totalEntries), + githubTokenEntries: numberOrZero(snapshot.githubTokenEntries), + gitTokenEntries: numberOrZero(snapshot.gitTokenEntries), + gitUserEntries: numberOrZero(snapshot.gitUserEntries), + claudeAuthEntries: numberOrZero(snapshot.claudeAuthEntries), + geminiAuthEntries: numberOrZero(snapshot.geminiAuthEntries) + } +} + +const readProjectAuthSnapshot = ( + snapshot: ReturnType +): RawProjectAuthSnapshot | null => { + if (snapshot === null) { + return null + } + + return { + projectDir: asString(snapshot["projectDir"]), + projectName: asString(snapshot["projectName"]), + envGlobalPath: asString(snapshot["envGlobalPath"]), + envProjectPath: asString(snapshot["envProjectPath"]), + claudeAuthPath: asString(snapshot["claudeAuthPath"]), + geminiAuthPath: asString(snapshot["geminiAuthPath"]), + githubTokenEntries: readNumber(snapshot["githubTokenEntries"]), + gitTokenEntries: readNumber(snapshot["gitTokenEntries"]), + claudeAuthEntries: readNumber(snapshot["claudeAuthEntries"]), + geminiAuthEntries: readNumber(snapshot["geminiAuthEntries"]), + activeGithubLabel: asString(snapshot["activeGithubLabel"]), + activeGitLabel: asString(snapshot["activeGitLabel"]), + activeClaudeLabel: asString(snapshot["activeClaudeLabel"]), + activeGeminiLabel: asString(snapshot["activeGeminiLabel"]) + } +} + +const decodeRequiredProjectAuthSnapshot = ( + snapshot: RawProjectAuthSnapshot +): ProjectAuthSnapshot | null => { + const requiredValues = [ + snapshot.projectDir, + snapshot.projectName, + snapshot.envGlobalPath, + snapshot.envProjectPath, + snapshot.claudeAuthPath, + snapshot.geminiAuthPath, + snapshot.githubTokenEntries, + snapshot.gitTokenEntries, + snapshot.claudeAuthEntries, + snapshot.geminiAuthEntries + ] + + if (hasNullValue(requiredValues)) { + return null + } + + return { + projectDir: stringOrEmpty(snapshot.projectDir), + projectName: stringOrEmpty(snapshot.projectName), + envGlobalPath: stringOrEmpty(snapshot.envGlobalPath), + envProjectPath: stringOrEmpty(snapshot.envProjectPath), + claudeAuthPath: stringOrEmpty(snapshot.claudeAuthPath), + geminiAuthPath: stringOrEmpty(snapshot.geminiAuthPath), + githubTokenEntries: numberOrZero(snapshot.githubTokenEntries), + gitTokenEntries: numberOrZero(snapshot.gitTokenEntries), + claudeAuthEntries: numberOrZero(snapshot.claudeAuthEntries), + geminiAuthEntries: numberOrZero(snapshot.geminiAuthEntries), + activeGithubLabel: snapshot.activeGithubLabel, + activeGitLabel: snapshot.activeGitLabel, + activeClaudeLabel: snapshot.activeClaudeLabel, + activeGeminiLabel: snapshot.activeGeminiLabel + } +} + +export const decodeAuthSnapshot = (payload: JsonValue): AuthSnapshot | null => { + const snapshot = readAuthSnapshot(resolveSnapshotObject(payload)) + return snapshot === null ? null : decodeRequiredAuthSnapshot(snapshot) +} + +export const decodeProjectAuthSnapshot = ( + payload: JsonValue +): ProjectAuthSnapshot | null => { + const snapshot = readProjectAuthSnapshot(resolveSnapshotObject(payload)) + return snapshot === null ? null : decodeRequiredProjectAuthSnapshot(snapshot) +} diff --git a/packages/app/src/docker-git/api-auth-menu-client.ts b/packages/app/src/docker-git/api-auth-menu-client.ts new file mode 100644 index 00000000..cbb72a5e --- /dev/null +++ b/packages/app/src/docker-git/api-auth-menu-client.ts @@ -0,0 +1,39 @@ +import { Effect } from "effect" + +import type { AuthMenuRequestBody, ProjectAuthMenuRequestBody } from "../shared/auth-menu-request.js" +import { decodeAuthSnapshot, decodeProjectAuthSnapshot } from "./api-auth-codec.js" +import { request } from "./api-http.js" + +const projectPath = (projectId: string, suffix = ""): string => `/projects/${encodeURIComponent(projectId)}${suffix}` + +export const loadAuthSnapshot = () => + request("GET", "/auth/menu").pipe( + Effect.map((payload) => decodeAuthSnapshot(payload)) + ) + +export const runAuthMenuFlow = (requestBody: AuthMenuRequestBody) => + request("POST", "/auth/menu", { + flow: requestBody.flow, + label: requestBody.label ?? undefined, + token: requestBody.token ?? undefined, + user: requestBody.user ?? undefined, + apiKey: requestBody.apiKey ?? undefined + }).pipe( + Effect.map((payload) => decodeAuthSnapshot(payload)) + ) + +export const loadProjectAuthSnapshot = (projectId: string) => + request("GET", projectPath(projectId, "/auth/menu")).pipe( + Effect.map((payload) => decodeProjectAuthSnapshot(payload)) + ) + +export const runProjectAuthFlow = ( + projectId: string, + requestBody: ProjectAuthMenuRequestBody +) => + request("POST", projectPath(projectId, "/auth/menu"), { + flow: requestBody.flow, + label: requestBody.label ?? undefined + }).pipe( + Effect.map((payload) => decodeProjectAuthSnapshot(payload)) + ) diff --git a/packages/app/src/docker-git/api-client-create.ts b/packages/app/src/docker-git/api-client-create.ts new file mode 100644 index 00000000..cdb4d466 --- /dev/null +++ b/packages/app/src/docker-git/api-client-create.ts @@ -0,0 +1,49 @@ +import type { JsonRequest } from "./api-json.js" +import { type CreateCommand, defaultTemplateConfig } from "./frontend-lib/core/domain.js" + +type ResolvedCreateRequestPaths = { + readonly authorizedKeysPath: string + readonly authorizedKeysContents?: string | undefined +} + +export const buildCreateProjectRequest = ( + command: CreateCommand, + resolvedPaths: ResolvedCreateRequestPaths +) => { + const config = command.config + return { + repoUrl: config.repoUrl, + repoRef: config.repoRef, + targetDir: config.targetDir, + sshPort: String(config.sshPort), + sshUser: config.sshUser, + containerName: config.containerName, + serviceName: config.serviceName, + volumeName: config.volumeName, + authorizedKeysPath: resolvedPaths.authorizedKeysContents === undefined + ? resolvedPaths.authorizedKeysPath + : defaultTemplateConfig.authorizedKeysPath, + authorizedKeysContents: resolvedPaths.authorizedKeysContents, + envGlobalPath: config.envGlobalPath, + envProjectPath: config.envProjectPath, + codexAuthPath: config.codexAuthPath, + codexHome: config.codexHome, + cpuLimit: config.cpuLimit, + ramLimit: config.ramLimit, + dockerNetworkMode: config.dockerNetworkMode, + dockerSharedNetworkName: config.dockerSharedNetworkName, + enableMcpPlaywright: config.enableMcpPlaywright, + outDir: command.outDir, + gitTokenLabel: config.gitTokenLabel, + skipGithubAuth: config.skipGithubAuth, + useManagedAuthorizedKeys: true, + codexTokenLabel: config.codexAuthLabel, + claudeTokenLabel: config.claudeAuthLabel, + agentAutoMode: config.agentAuto ? (config.agentMode ?? "auto") : undefined, + up: command.runUp, + openSsh: false, + force: command.force, + forceEnv: command.forceEnv, + waitForClone: command.waitForClone + } satisfies JsonRequest +} diff --git a/packages/app/src/docker-git/api-client-helpers.ts b/packages/app/src/docker-git/api-client-helpers.ts new file mode 100644 index 00000000..0b0bd62a --- /dev/null +++ b/packages/app/src/docker-git/api-client-helpers.ts @@ -0,0 +1,62 @@ +import * as FileSystem from "@effect/platform/FileSystem" +import * as Path from "@effect/platform/Path" +import { Effect } from "effect" + +import { asObject, asString, type JsonValue } from "./api-json.js" +import { defaultTemplateConfig } from "./frontend-lib/core/domain.js" +import type { CreateCommand } from "./frontend-lib/core/domain.js" +import { resolvePathFromCwd } from "./frontend-lib/usecases/path-helpers.js" + +export const readProjectOutput = (payload: JsonValue): string => { + const object = asObject(payload) + return asString(object?.["output"]) ?? "" +} + +const normalizeRelativePath = (value: string): string => + value + .replaceAll("\\", "/") + .replace(/^\.\//, "") + .trim() + +const isManagedCreatePath = (value: string): boolean => { + const normalized = normalizeRelativePath(value) + return normalized === ".docker-git" || + normalized === ".orch" || + normalized.startsWith(".docker-git/") || + normalized.startsWith(".orch/") +} + +const resolveClientCreatePath = ( + path: Path.Path, + cwd: string, + targetPath: string +): string => + path.isAbsolute(targetPath) || isManagedCreatePath(targetPath) + ? targetPath + : resolvePathFromCwd(path, cwd, targetPath) + +const missingAuthorizedKeysContents = (): string | undefined => undefined + +export const resolveCreateRequestPaths = (command: CreateCommand) => + Effect.gen(function*(_) { + const fs = yield* _(FileSystem.FileSystem) + const path = yield* _(Path.Path) + const cwd = process.cwd() + const authorizedKeysPath = command.config.authorizedKeysPath === defaultTemplateConfig.authorizedKeysPath + ? command.config.authorizedKeysPath + : resolveClientCreatePath(path, cwd, command.config.authorizedKeysPath) + const authorizedKeysContents = authorizedKeysPath === defaultTemplateConfig.authorizedKeysPath + ? undefined + : yield* _( + fs.exists(authorizedKeysPath).pipe( + Effect.flatMap((exists) => + exists ? fs.readFileString(authorizedKeysPath) : Effect.sync(missingAuthorizedKeysContents) + ) + ) + ) + + return { + authorizedKeysPath, + authorizedKeysContents + } + }) diff --git a/packages/app/src/docker-git/api-client.ts b/packages/app/src/docker-git/api-client.ts index 72ddf4d3..67eb1e63 100644 --- a/packages/app/src/docker-git/api-client.ts +++ b/packages/app/src/docker-git/api-client.ts @@ -1,7 +1,13 @@ -import * as FileSystem from "@effect/platform/FileSystem" -import * as Path from "@effect/platform/Path" +import * as FsPlatform from "@effect/platform/FileSystem" +import * as PathPlatform from "@effect/platform/Path" import { Effect } from "effect" +import { buildCreateProjectRequest } from "./api-client-create.js" +import { readProjectOutput, resolveCreateRequestPaths } from "./api-client-helpers.js" +import { request, requestTextStream, requestVoid } from "./api-http.js" +import { asArray, asObject, type JsonValue } from "./api-json.js" +import { decodeProjectDetails, decodeProjectSummary } from "./api-project-codec.js" +import { decodeTerminalSession } from "./api-terminal-codec.js" import type { AuthCodexImportCommand, AuthCodexLoginCommand, @@ -14,14 +20,9 @@ import type { StateCommitCommand, StateInitCommand, StateSyncCommand -} from "@lib/core/domain" -import { resolvePathFromCwd } from "@lib/usecases/path-helpers" - -import { request, requestTextStream, requestVoid } from "./api-http.js" -import { asArray, asObject, asString, type JsonRequest, type JsonValue } from "./api-json.js" -import { decodeProjectDetails, decodeProjectSummary } from "./api-project-codec.js" +} from "./frontend-lib/core/domain.js" +import { resolvePathFromCwd } from "./frontend-lib/usecases/path-helpers.js" import type { ApiRequestError } from "./host-errors.js" -import { resolveHostSshMaterial, resolveManagedHostSshMaterial } from "./host-ssh-material.js" export { type JsonObject, type JsonRequest, type JsonValue, renderJsonPayload } from "./api-json.js" export { @@ -31,11 +32,19 @@ export { decodeProjectSummary, renderProjectSummaryLine } from "./api-project-codec.js" +export { type ApiTerminalSession } from "./api-terminal-codec.js" const projectPath = (projectId: string, suffix = ""): string => `/projects/${encodeURIComponent(projectId)}${suffix}` const codexLoginSuccessMarker = "__DOCKER_GIT_CODEX_LOGIN_STATUS__:ok" const codexLoginErrorMarkerPrefix = "__DOCKER_GIT_CODEX_LOGIN_STATUS__:error:" +const decodeProjectResponse = (payload: JsonValue) => { + const object = asObject(payload) + return object === null + ? decodeProjectDetails(payload) + : decodeProjectDetails(object["project"] ?? payload) +} + const codexLoginFailureMessage = (output: string, exitCode: string | null): string => { if (output.includes("429 Too Many Requests")) { return "Codex device auth is rate-limited by OpenAI (429 Too Many Requests). Wait a few minutes and retry." @@ -65,11 +74,6 @@ const codexLoginFailureMessage = (output: string, exitCode: string | null): stri : `Codex login failed (${exitCode}).` } -const readProjectOutput = (payload: JsonValue): string => { - const object = asObject(payload) - return asString(object?.["output"]) ?? "" -} - export const listProjects = () => request("GET", "/projects").pipe( Effect.map((payload) => { @@ -83,64 +87,32 @@ export const listProjects = () => export const getProject = (projectId: string) => request("GET", projectPath(projectId)).pipe( - Effect.map((payload) => { - const object = asObject(payload) - return object === null ? decodeProjectDetails(payload) : decodeProjectDetails(object["project"] ?? payload) - }) + Effect.map((payload) => decodeProjectResponse(payload)) ) +const createProjectWithResolvedPaths = ( + command: CreateCommand, + resolvedPaths: { + readonly authorizedKeysPath: string + readonly authorizedKeysContents?: string | undefined + } +) => + Effect.gen(function*(_) { + const createRequest = buildCreateProjectRequest(command, resolvedPaths) + const payload = yield* _(request("POST", "/projects", createRequest)) + return decodeProjectResponse(payload) + }) + export const createProject = (command: CreateCommand) => Effect.gen(function*(_) { - const config = command.config - const sshMaterial = yield* _(resolveHostSshMaterial(command)) - const body = { - repoUrl: config.repoUrl, - repoRef: config.repoRef, - targetDir: config.targetDir, - sshPort: String(config.sshPort), - sshUser: config.sshUser, - containerName: config.containerName, - serviceName: config.serviceName, - volumeName: config.volumeName, - cpuLimit: config.cpuLimit, - ramLimit: config.ramLimit, - dockerNetworkMode: config.dockerNetworkMode, - dockerSharedNetworkName: config.dockerSharedNetworkName, - enableMcpPlaywright: config.enableMcpPlaywright, - outDir: command.outDir, - gitTokenLabel: config.gitTokenLabel, - skipGithubAuth: config.skipGithubAuth, - authorizedKeysContents: sshMaterial.authorizedKeysContents.length > 0 - ? sshMaterial.authorizedKeysContents - : undefined, - codexTokenLabel: config.codexAuthLabel, - claudeTokenLabel: config.claudeAuthLabel, - agentAutoMode: config.agentAuto ? (config.agentMode ?? "auto") : undefined, - up: command.runUp, - openSsh: false, - force: command.force, - forceEnv: command.forceEnv, - waitForClone: command.waitForClone - } satisfies JsonRequest - - const payload = yield* _(request("POST", "/projects", body)) - const object = asObject(payload) - return object === null ? decodeProjectDetails(payload) : decodeProjectDetails(object["project"] ?? payload) + const resolvedPaths = yield* _(resolveCreateRequestPaths(command)) + return yield* _(createProjectWithResolvedPaths(command, resolvedPaths)) }) export const deleteProject = (projectId: string) => requestVoid("DELETE", projectPath(projectId)) export const upProject = (projectId: string) => - Effect.gen(function*(_) { - const sshMaterial = yield* _(resolveManagedHostSshMaterial()) - return yield* _( - requestVoid("POST", projectPath(projectId, "/up"), { - authorizedKeysContents: sshMaterial.authorizedKeysContents.length > 0 - ? sshMaterial.authorizedKeysContents - : undefined - }) - ) - }) + requestVoid("POST", projectPath(projectId, "/up"), { useManagedAuthorizedKeys: true }) export const downProject = (projectId: string) => requestVoid("POST", projectPath(projectId, "/down")) @@ -154,6 +126,29 @@ export const readProjectLogs = (projectId: string) => Effect.map((payload) => readProjectOutput(payload)) ) +export const createProjectTerminalSession = (projectId: string) => + request("POST", projectPath(projectId, "/terminal-sessions")).pipe( + Effect.map((payload) => { + const object = asObject(payload) + const project = decodeProjectDetails(object?.["project"] ?? payload) + const session = decodeTerminalSession(object?.["session"] ?? payload) + return project === null || session === null ? null : { project, session } + }) + ) + +export const createAuthTerminalSession = ( + flow: "ClaudeOauth" | "GeminiOauth", + label: string | null +) => + request("POST", "/auth/terminal-sessions", { flow, label: label ?? undefined }).pipe( + Effect.map((payload) => { + const object = asObject(payload) + return decodeTerminalSession(object?.["session"] ?? payload) + }) + ) + +export const deleteTerminalSessionByPath = (path: string) => requestVoid("DELETE", path) + export const applyAllProjects = (activeOnly: boolean) => requestVoid("POST", "/projects/apply-all", { activeOnly }) export const downAllProjects = () => requestVoid("POST", "/projects/down-all") @@ -273,8 +268,8 @@ export const codexLogin = (command: AuthCodexLoginCommand) => const readCodexAuthText = (command: AuthCodexImportCommand) => Effect.gen(function*(_) { - const fs = yield* _(FileSystem.FileSystem) - const path = yield* _(Path.Path) + const fs = yield* _(FsPlatform.FileSystem) + const path = yield* _(PathPlatform.Path) const resolvedCodexAuthDir = resolvePathFromCwd(path, process.cwd(), command.codexAuthPath) const authFilePath = path.join(resolvedCodexAuthDir, "auth.json") return yield* _(fs.readFileString(authFilePath)) diff --git a/packages/app/src/docker-git/api-json.ts b/packages/app/src/docker-git/api-json.ts index af8f8004..e91fd4ef 100644 --- a/packages/app/src/docker-git/api-json.ts +++ b/packages/app/src/docker-git/api-json.ts @@ -2,26 +2,19 @@ import * as ParseResult from "@effect/schema/ParseResult" import * as Schema from "@effect/schema/Schema" import { Effect, Either } from "effect" -type JsonPrimitive = boolean | number | string | null -export type JsonValue = JsonPrimitive | JsonObject | ReadonlyArray -export type JsonObject = Readonly<{ [key: string]: JsonValue }> +import { type JsonObject, type JsonValue, JsonValueSchema } from "../shared/json-schema.js" + +export type { JsonObject, JsonValue } from "../shared/json-schema.js" + export type JsonRequest = - | JsonPrimitive + | boolean + | number + | string + | null | { readonly [key: string]: JsonRequest | undefined } | ReadonlyArray -const JsonValueSchema: Schema.Schema = Schema.suspend(() => - Schema.Union( - Schema.Null, - Schema.Boolean, - Schema.Number, - Schema.String, - Schema.Array(JsonValueSchema), - Schema.Record({ key: Schema.String, value: JsonValueSchema }) - ) -) - -const JsonValueFromStringSchema = Schema.parseJson(JsonValueSchema) +const JsonValueFromStringSchema: Schema.Schema = Schema.parseJson(JsonValueSchema) const decodeJsonText = (input: string): Effect.Effect => Either.match(ParseResult.decodeUnknownEither(JsonValueFromStringSchema)(input), { @@ -88,6 +81,43 @@ const renderGithubStatusLike = (value: JsonObject): string | null => { return lines.length === 0 ? summary : [summary, ...lines].join("\n") } +const readNestedMessage = ( + object: JsonObject, + key: string +): string | null => { + const nested = asObject(object[key]) + if (nested === null) { + return null + } + return asString(nested["message"]) +} + +const renderNestedStatusPayload = ( + payload: JsonValue, + object: JsonObject +): string | null => { + const nestedStatus = asObject(object["status"]) + if (nestedStatus === null) { + return null + } + + const renderedNestedStatus = renderGithubStatusLike(nestedStatus) + if (renderedNestedStatus !== null) { + return renderedNestedStatus + } + + return readNestedMessage(object, "status") ?? JSON.stringify(payload, null, 2) +} + +const renderDirectObjectPayload = (object: JsonObject): string | null => { + const directStatus = renderGithubStatusLike(object) + if (directStatus !== null) { + return directStatus + } + + return asString(object["message"]) +} + export const renderJsonPayload = (payload: JsonValue): string => { if (typeof payload === "string") { return payload @@ -98,27 +128,19 @@ export const renderJsonPayload = (payload: JsonValue): string => { return JSON.stringify(payload, null, 2) } - const directStatus = renderGithubStatusLike(object) - if (directStatus !== null) { - return directStatus + const directPayload = renderDirectObjectPayload(object) + if (directPayload !== null) { + return directPayload } - const message = asString(object["message"]) - if (message !== null) { - return message + const nestedStatus = renderNestedStatusPayload(payload, object) + if (nestedStatus !== null) { + return nestedStatus } - const nestedStatus = asObject(object["status"]) - if (nestedStatus !== null) { - const renderedNestedStatus = renderGithubStatusLike(nestedStatus) - if (renderedNestedStatus !== null) { - return renderedNestedStatus - } - - const nestedMessage = asString(nestedStatus["message"]) - if (nestedMessage !== null) { - return nestedMessage - } + const nestedErrorMessage = readNestedMessage(object, "error") + if (nestedErrorMessage !== null) { + return nestedErrorMessage } return JSON.stringify(payload, null, 2) diff --git a/packages/app/src/docker-git/api-project-codec.ts b/packages/app/src/docker-git/api-project-codec.ts index 36a00b18..3d7ac836 100644 --- a/packages/app/src/docker-git/api-project-codec.ts +++ b/packages/app/src/docker-git/api-project-codec.ts @@ -7,6 +7,10 @@ export type ApiProjectSummary = { readonly repoRef: string readonly status: "running" | "stopped" | "unknown" readonly statusLabel: string + readonly sshSessions: number + readonly startedAtIso: string | null + readonly startedAtEpochMs: number | null + readonly clonedOnHostname?: string | undefined } export type ApiProjectDetails = ApiProjectSummary & { @@ -17,15 +21,30 @@ export type ApiProjectDetails = ApiProjectSummary & { readonly targetDir: string readonly projectDir: string readonly sshCommand: string + readonly authorizedKeysPath: string + readonly authorizedKeysExists: boolean readonly envGlobalPath: string readonly envProjectPath: string readonly codexAuthPath: string readonly codexHome: string - readonly clonedOnHostname?: string | undefined } type ProjectDetailFields = Omit -type RequiredProjectDetailFields = Omit +type RawProjectDetailFields = { + readonly containerName: string | null + readonly serviceName: string | null + readonly sshUser: string | null + readonly sshPort: number | null + readonly targetDir: string | null + readonly projectDir: string | null + readonly sshCommand: string | null + readonly authorizedKeysPath: string | null + readonly authorizedKeysExists: boolean | null + readonly envGlobalPath: string | null + readonly envProjectPath: string | null + readonly codexAuthPath: string | null + readonly codexHome: string | null +} const isProjectStatus = ( value: string @@ -34,6 +53,7 @@ const isProjectStatus = ( const stringOrEmpty = (value: string | null): string => value ?? "" const numberOrZero = (value: number | null): number => value ?? 0 +const readNullableNumber = (value: JsonValue | undefined): number | null => typeof value === "number" ? value : null const readSummaryBaseFields = ( object: ReturnType @@ -48,7 +68,10 @@ const readSummaryBaseFields = ( const repoRef = asString(object["repoRef"]) const status = asString(object["status"]) const statusLabel = asString(object["statusLabel"]) - const values = [id, displayName, repoUrl, repoRef, status, statusLabel] + const sshSessions = typeof object["sshSessions"] === "number" ? object["sshSessions"] : null + const startedAtIso = object["startedAtIso"] === null ? null : asString(object["startedAtIso"]) + const startedAtEpochMs = readNullableNumber(object["startedAtEpochMs"]) + const values = [id, displayName, repoUrl, repoRef, status, statusLabel, sshSessions] if (values.includes(null)) { return null @@ -60,58 +83,62 @@ const readSummaryBaseFields = ( repoUrl: stringOrEmpty(repoUrl), repoRef: stringOrEmpty(repoRef), status: stringOrEmpty(status), - statusLabel: stringOrEmpty(statusLabel) + statusLabel: stringOrEmpty(statusLabel), + sshSessions: numberOrZero(sshSessions), + startedAtIso, + startedAtEpochMs } } const readRequiredProjectDetails = ( object: ReturnType -): RequiredProjectDetailFields | null => { +): RawProjectDetailFields | null => { if (object === null) { return null } - const containerName = asString(object["containerName"]) - const serviceName = asString(object["serviceName"]) - const sshUser = asString(object["sshUser"]) - const sshPort = typeof object["sshPort"] === "number" ? object["sshPort"] : null - const targetDir = asString(object["targetDir"]) - const projectDir = asString(object["projectDir"]) - const sshCommand = asString(object["sshCommand"]) - const envGlobalPath = asString(object["envGlobalPath"]) - const envProjectPath = asString(object["envProjectPath"]) - const codexAuthPath = asString(object["codexAuthPath"]) - const codexHome = asString(object["codexHome"]) - const values = [ - containerName, - serviceName, - sshUser, - sshPort, - targetDir, - projectDir, - sshCommand, - envGlobalPath, - envProjectPath, - codexAuthPath, - codexHome - ] + return { + containerName: asString(object["containerName"]), + serviceName: asString(object["serviceName"]), + sshUser: asString(object["sshUser"]), + sshPort: typeof object["sshPort"] === "number" ? object["sshPort"] : null, + targetDir: asString(object["targetDir"]), + projectDir: asString(object["projectDir"]), + sshCommand: asString(object["sshCommand"]), + authorizedKeysPath: asString(object["authorizedKeysPath"]), + authorizedKeysExists: typeof object["authorizedKeysExists"] === "boolean" + ? object["authorizedKeysExists"] + : null, + envGlobalPath: asString(object["envGlobalPath"]), + envProjectPath: asString(object["envProjectPath"]), + codexAuthPath: asString(object["codexAuthPath"]), + codexHome: asString(object["codexHome"]) + } +} - if (values.includes(null)) { +const decodeRequiredProjectDetails = ( + object: ReturnType +): ProjectDetailFields | null => { + const rawFields = readRequiredProjectDetails(object) + + if (rawFields === null || Object.values(rawFields).includes(null)) { return null } return { - containerName: stringOrEmpty(containerName), - serviceName: stringOrEmpty(serviceName), - sshUser: stringOrEmpty(sshUser), - sshPort: numberOrZero(sshPort), - targetDir: stringOrEmpty(targetDir), - projectDir: stringOrEmpty(projectDir), - sshCommand: stringOrEmpty(sshCommand), - envGlobalPath: stringOrEmpty(envGlobalPath), - envProjectPath: stringOrEmpty(envProjectPath), - codexAuthPath: stringOrEmpty(codexAuthPath), - codexHome: stringOrEmpty(codexHome) + containerName: stringOrEmpty(rawFields.containerName), + serviceName: stringOrEmpty(rawFields.serviceName), + sshUser: stringOrEmpty(rawFields.sshUser), + sshPort: numberOrZero(rawFields.sshPort), + targetDir: stringOrEmpty(rawFields.targetDir), + projectDir: stringOrEmpty(rawFields.projectDir), + sshCommand: stringOrEmpty(rawFields.sshCommand), + authorizedKeysPath: stringOrEmpty(rawFields.authorizedKeysPath), + authorizedKeysExists: rawFields.authorizedKeysExists === true, + envGlobalPath: stringOrEmpty(rawFields.envGlobalPath), + envProjectPath: stringOrEmpty(rawFields.envProjectPath), + codexAuthPath: stringOrEmpty(rawFields.codexAuthPath), + codexHome: stringOrEmpty(rawFields.codexHome) } } @@ -127,20 +154,8 @@ const readProjectSummaryFields = (value: JsonValue): ApiProjectSummary | null => } } -const readProjectDetailFields = (value: JsonValue): ProjectDetailFields | null => { - const object = asObject(value) - if (object === null) { - return null - } - - const details = readRequiredProjectDetails(object) - if (details === null) { - return null - } - - const clonedOnHostname = asString(object["clonedOnHostname"]) - return clonedOnHostname === null ? details : { ...details, clonedOnHostname } -} +const readProjectDetailFields = (value: JsonValue): ProjectDetailFields | null => + decodeRequiredProjectDetails(asObject(value)) export const decodeProjectSummary = (value: JsonValue): ApiProjectSummary | null => readProjectSummaryFields(value) diff --git a/packages/app/src/docker-git/api-terminal-codec.ts b/packages/app/src/docker-git/api-terminal-codec.ts new file mode 100644 index 00000000..c14aaab7 --- /dev/null +++ b/packages/app/src/docker-git/api-terminal-codec.ts @@ -0,0 +1,70 @@ +import type { TerminalSession } from "../shared/terminal-session-schema.js" +import { asObject, asString, type JsonValue } from "./api-json.js" + +export type ApiTerminalSession = TerminalSession + +type RawTerminalSession = { + readonly id: string | null + readonly projectId: string | null + readonly sshCommand: string | null + readonly status: string | null + readonly createdAt: string | null + readonly startedAt: string | undefined + readonly closedAt: string | undefined + readonly exitCode: number | undefined + readonly signal: number | undefined +} + +const isTerminalSessionStatus = ( + value: string +): value is ApiTerminalSession["status"] => + value === "ready" || value === "attached" || value === "exited" || value === "failed" + +const readOptionalNumber = (value: JsonValue | undefined): number | undefined => + typeof value === "number" ? value : undefined + +const readTerminalSession = (payload: JsonValue): RawTerminalSession | null => { + const object = asObject(payload) + if (object === null) { + return null + } + + return { + id: asString(object["id"]), + projectId: asString(object["projectId"]), + sshCommand: asString(object["sshCommand"]), + status: asString(object["status"]), + createdAt: asString(object["createdAt"]), + startedAt: asString(object["startedAt"]) ?? undefined, + closedAt: asString(object["closedAt"]) ?? undefined, + exitCode: readOptionalNumber(object["exitCode"]), + signal: readOptionalNumber(object["signal"]) + } +} + +export const decodeTerminalSession = (payload: JsonValue): ApiTerminalSession | null => { + const session = readTerminalSession(payload) + if (session === null) { + return null + } + + if ( + session.id === null || + session.projectId === null || + session.sshCommand === null || + session.createdAt === null || + session.status === null || + !isTerminalSessionStatus(session.status) + ) { + return null + } + + return { + ...session, + id: session.id, + projectId: session.projectId, + sshCommand: session.sshCommand, + status: session.status, + createdAt: session.createdAt + } +} diff --git a/packages/app/src/docker-git/cli/input.ts b/packages/app/src/docker-git/cli/input.ts index 6dc30da1..3fb5270c 100644 --- a/packages/app/src/docker-git/cli/input.ts +++ b/packages/app/src/docker-git/cli/input.ts @@ -1,7 +1,7 @@ import * as Terminal from "@effect/platform/Terminal" import { Effect } from "effect" -import { InputCancelledError, InputReadError } from "@lib/shell/errors" +import { InputCancelledError, InputReadError } from "../frontend-lib/shell/errors.js" const normalizeMessage = (error: Error): string => error.message diff --git a/packages/app/src/docker-git/cli/parser-apply.ts b/packages/app/src/docker-git/cli/parser-apply.ts index e1250b4f..384265e9 100644 --- a/packages/app/src/docker-git/cli/parser-apply.ts +++ b/packages/app/src/docker-git/cli/parser-apply.ts @@ -1,7 +1,7 @@ import { Either } from "effect" -import { type ApplyCommand, type ParseError } from "@lib/core/domain" -import { normalizeCpuLimit, normalizeRamLimit } from "@lib/core/resource-limits" +import { type ApplyCommand, type ParseError } from "../frontend-lib/core/domain.js" +import { normalizeCpuLimit, normalizeRamLimit } from "../frontend-lib/core/resource-limits.js" import { parseProjectDirWithOptions } from "./parser-shared.js" diff --git a/packages/app/src/docker-git/cli/parser-attach.ts b/packages/app/src/docker-git/cli/parser-attach.ts index 60f160d0..097ceb5f 100644 --- a/packages/app/src/docker-git/cli/parser-attach.ts +++ b/packages/app/src/docker-git/cli/parser-attach.ts @@ -1,6 +1,6 @@ import { Either } from "effect" -import { type AttachCommand, type ParseError } from "@lib/core/domain" +import { type AttachCommand, type ParseError } from "../frontend-lib/core/domain.js" import { parseProjectDirArgs } from "./parser-shared.js" diff --git a/packages/app/src/docker-git/cli/parser-auth.ts b/packages/app/src/docker-git/cli/parser-auth.ts index c92dffb9..abf1a049 100644 --- a/packages/app/src/docker-git/cli/parser-auth.ts +++ b/packages/app/src/docker-git/cli/parser-auth.ts @@ -1,7 +1,8 @@ import { Either, Match } from "effect" -import type { RawOptions } from "@lib/core/command-options" -import { type AuthCommand, type Command, type ParseError } from "@lib/core/domain" +import { normalizeOptionalText } from "../../shared/optional-text.js" +import type { RawOptions } from "../frontend-lib/core/command-options.js" +import { type AuthCommand, type Command, type ParseError } from "../frontend-lib/core/domain.js" import { parseRawOptions } from "./parser-options.js" @@ -27,11 +28,6 @@ const invalidArgument = (name: string, reason: string): ParseError => ({ reason }) -const normalizeLabel = (value: string | undefined): string | null => { - const trimmed = value?.trim() ?? "" - return trimmed.length === 0 ? null : trimmed -} - const defaultEnvGlobalPath = ".docker-git/.orch/env/global.env" const defaultCodexAuthPath = ".docker-git/.orch/auth/codex" const defaultClaudeAuthPath = ".docker-git/.orch/auth/claude" @@ -42,9 +38,9 @@ const resolveAuthOptions = (raw: RawOptions): AuthOptions => ({ codexAuthPath: raw.codexAuthPath ?? defaultCodexAuthPath, claudeAuthPath: defaultClaudeAuthPath, geminiAuthPath: defaultGeminiAuthPath, - label: normalizeLabel(raw.label), - token: normalizeLabel(raw.token), - scopes: normalizeLabel(raw.scopes), + label: normalizeOptionalText(raw.label), + token: normalizeOptionalText(raw.token), + scopes: normalizeOptionalText(raw.scopes), authWeb: raw.authWeb === true }) diff --git a/packages/app/src/docker-git/cli/parser-clone.ts b/packages/app/src/docker-git/cli/parser-clone.ts index 98125461..7a890410 100644 --- a/packages/app/src/docker-git/cli/parser-clone.ts +++ b/packages/app/src/docker-git/cli/parser-clone.ts @@ -1,8 +1,8 @@ import { Either } from "effect" -import { buildCreateCommand, nonEmpty } from "@lib/core/command-builders" -import type { RawOptions } from "@lib/core/command-options" -import { type Command, type ParseError, resolveRepoInput } from "@lib/core/domain" +import { buildCreateCommand, nonEmpty } from "../frontend-lib/core/command-builders.js" +import type { RawOptions } from "../frontend-lib/core/command-options.js" +import { type Command, type ParseError, resolveRepoInput } from "../frontend-lib/core/domain.js" import { parseRawOptions } from "./parser-options.js" import { resolveWorkspaceRepoPath, splitPositionalRepo } from "./parser-shared.js" diff --git a/packages/app/src/docker-git/cli/parser-create.ts b/packages/app/src/docker-git/cli/parser-create.ts index 69ba3436..c37a3969 100644 --- a/packages/app/src/docker-git/cli/parser-create.ts +++ b/packages/app/src/docker-git/cli/parser-create.ts @@ -1,3 +1,3 @@ -export { buildCreateCommand, nonEmpty } from "@lib/core/command-builders" -export type { RawOptions } from "@lib/core/command-options" -export type { CreateCommand, ParseError } from "@lib/core/domain" +export { buildCreateCommand, nonEmpty } from "../frontend-lib/core/command-builders.js" +export type { RawOptions } from "../frontend-lib/core/command-options.js" +export type { CreateCommand, ParseError } from "../frontend-lib/core/domain.js" diff --git a/packages/app/src/docker-git/cli/parser-mcp-playwright.ts b/packages/app/src/docker-git/cli/parser-mcp-playwright.ts index 94b77c11..295732ff 100644 --- a/packages/app/src/docker-git/cli/parser-mcp-playwright.ts +++ b/packages/app/src/docker-git/cli/parser-mcp-playwright.ts @@ -1,6 +1,6 @@ import { Either } from "effect" -import { type McpPlaywrightUpCommand, type ParseError } from "@lib/core/domain" +import { type McpPlaywrightUpCommand, type ParseError } from "../frontend-lib/core/domain.js" import { parseProjectDirWithOptions } from "./parser-shared.js" diff --git a/packages/app/src/docker-git/cli/parser-open.ts b/packages/app/src/docker-git/cli/parser-open.ts index ee605079..19bb6610 100644 --- a/packages/app/src/docker-git/cli/parser-open.ts +++ b/packages/app/src/docker-git/cli/parser-open.ts @@ -1,7 +1,8 @@ import { Either } from "effect" -import { type OpenCommand, type ParseError } from "@lib/core/domain" +import { type OpenCommand, type ParseError } from "../frontend-lib/core/domain.js" +import { trimToUndefined } from "../../shared/trimmed-text.js" import { parseRawOptions } from "./parser-options.js" type OpenParts = { @@ -26,11 +27,6 @@ const buildOpenCommand = (parts: OpenParts): OpenCommand => ({ ...(parts.projectDir === undefined ? {} : { projectDir: parts.projectDir }) }) -const normalizeSelector = (value: string | undefined): string | undefined => { - const trimmed = value?.trim() ?? "" - return trimmed.length > 0 ? trimmed : undefined -} - // CHANGE: parse open as a distinct selector-based command // WHY: open must resolve existing projects by raw selector without tmux semantics // QUOTE(ТЗ): "open should parse to a distinct _tag: \"Open\" command" @@ -46,12 +42,12 @@ export const parseOpen = (args: ReadonlyArray): Either.Either Either.right( buildOpenCommand({ - ...(normalizeSelector(raw.projectDir) === undefined + ...(trimToUndefined(raw.projectDir) === undefined ? {} - : { projectDir: normalizeSelector(raw.projectDir) }), - ...(normalizeSelector(raw.containerName ?? raw.repoUrl ?? positionalRef) === undefined + : { projectDir: trimToUndefined(raw.projectDir) }), + ...(trimToUndefined(raw.containerName ?? raw.repoUrl ?? positionalRef) === undefined ? {} - : { projectRef: normalizeSelector(raw.containerName ?? raw.repoUrl ?? positionalRef) }) + : { projectRef: trimToUndefined(raw.containerName ?? raw.repoUrl ?? positionalRef) }) }) )) } diff --git a/packages/app/src/docker-git/cli/parser-options.ts b/packages/app/src/docker-git/cli/parser-options.ts index 1022af98..2835726c 100644 --- a/packages/app/src/docker-git/cli/parser-options.ts +++ b/packages/app/src/docker-git/cli/parser-options.ts @@ -1,7 +1,7 @@ import { Either } from "effect" -import type { RawOptions } from "@lib/core/command-options" -import type { ParseError } from "@lib/core/domain" +import type { RawOptions } from "../frontend-lib/core/command-options.js" +import type { ParseError } from "../frontend-lib/core/domain.js" interface ValueOptionSpec { readonly flag: string @@ -281,4 +281,4 @@ export const parseRawOptions = (args: ReadonlyArray): Either.Either] [options] diff --git a/packages/app/src/docker-git/controller-docker.ts b/packages/app/src/docker-git/controller-docker.ts index 0e6ec565..b97aaacf 100644 --- a/packages/app/src/docker-git/controller-docker.ts +++ b/packages/app/src/docker-git/controller-docker.ts @@ -4,7 +4,7 @@ import * as FileSystem from "@effect/platform/FileSystem" import * as Path from "@effect/platform/Path" import { Effect } from "effect" -import { runCommandCapture, runCommandExitCode } from "@lib/shell/command-runner" +import { runCommandCapture, runCommandExitCode } from "./frontend-lib/shell/command-runner.js" import { type DockerNetworkIps, parseDockerNetworkIps, uniqueStrings } from "./controller-reachability.js" import { @@ -57,6 +57,11 @@ const mapComposePathError = (error: PlatformError): ControllerBootstrapError => const mapControllerRevisionError = (error: PlatformError): ControllerBootstrapError => controllerBootstrapError(`Failed to compute docker-git controller revision.\nDetails: ${String(error)}`) +const currentProcessEnv = (): Readonly> => + Object.fromEntries( + Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined) + ) + const renderDockerAccessDeniedMessage = (): string => [ "docker-git host CLI cannot access Docker from the client process.", @@ -166,7 +171,18 @@ export const runCompose = ( composePath, ...args ]) - const exitCode = yield* _(runExitCode(invocation.command, invocation.args)) + const exitCode = yield* _( + runCommandExitCode({ + cwd: process.cwd(), + command: invocation.command, + args: invocation.args, + env: currentProcessEnv() + }).pipe( + Effect.mapError((error) => + controllerBootstrapError(`Failed to start docker-git controller.\nDetails: ${String(error)}`) + ) + ) + ) if (exitCode === 0) { return diff --git a/packages/app/src/docker-git/controller-revision.ts b/packages/app/src/docker-git/controller-revision.ts index da19d0bd..56acecbd 100644 --- a/packages/app/src/docker-git/controller-revision.ts +++ b/packages/app/src/docker-git/controller-revision.ts @@ -8,8 +8,8 @@ export const controllerRevisionEnvKey = "DOCKER_GIT_CONTROLLER_REV" const controllerRevisionInputs: ReadonlyArray = [ "docker-compose.yml", "package.json", - "pnpm-lock.yaml", - "pnpm-workspace.yaml", + "bun.lock", + "bunfig.toml", "tsconfig.base.json", "tsconfig.json", "patches", diff --git a/packages/app/src/docker-git/frontend-lib/core/auth-domain.ts b/packages/app/src/docker-git/frontend-lib/core/auth-domain.ts new file mode 100644 index 00000000..e53d796b --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/core/auth-domain.ts @@ -0,0 +1,106 @@ +/* jscpd:ignore-start */ +export interface AuthGithubLoginCommand { + readonly _tag: "AuthGithubLogin" + readonly label: string | null + readonly token: string | null + readonly scopes: string | null + readonly envGlobalPath: string +} + +export interface AuthGithubStatusCommand { + readonly _tag: "AuthGithubStatus" + readonly envGlobalPath: string +} + +export interface AuthGithubLogoutCommand { + readonly _tag: "AuthGithubLogout" + readonly label: string | null + readonly envGlobalPath: string +} + +export interface AuthCodexLoginCommand { + readonly _tag: "AuthCodexLogin" + readonly label: string | null + readonly codexAuthPath: string +} + +export interface AuthCodexImportCommand { + readonly _tag: "AuthCodexImport" + readonly label: string | null + readonly codexAuthPath: string +} + +export interface AuthCodexStatusCommand { + readonly _tag: "AuthCodexStatus" + readonly label: string | null + readonly codexAuthPath: string +} + +export interface AuthCodexLogoutCommand { + readonly _tag: "AuthCodexLogout" + readonly label: string | null + readonly codexAuthPath: string +} + +export interface AuthClaudeLoginCommand { + readonly _tag: "AuthClaudeLogin" + readonly label: string | null + readonly claudeAuthPath: string +} + +export interface AuthClaudeStatusCommand { + readonly _tag: "AuthClaudeStatus" + readonly label: string | null + readonly claudeAuthPath: string +} + +export interface AuthClaudeLogoutCommand { + readonly _tag: "AuthClaudeLogout" + readonly label: string | null + readonly claudeAuthPath: string +} + +// CHANGE: add Gemini CLI auth commands +// WHY: enable Gemini CLI authentication management similar to Claude/Codex +// QUOTE(ТЗ): "Добавь поддержку gemini CLI" +// REF: issue-146 +// SOURCE: https://geminicli.com/docs/get-started/authentication/ +// FORMAT THEOREM: forall cmd ∈ AuthGeminiCommand: cmd.geminiAuthPath is valid path +// PURITY: CORE +// EFFECT: n/a +// INVARIANT: authentication state is isolated by label +// COMPLEXITY: O(1) +export interface AuthGeminiLoginCommand { + readonly _tag: "AuthGeminiLogin" + readonly label: string | null + readonly geminiAuthPath: string + readonly isWeb: boolean +} + +export interface AuthGeminiStatusCommand { + readonly _tag: "AuthGeminiStatus" + readonly label: string | null + readonly geminiAuthPath: string +} + +export interface AuthGeminiLogoutCommand { + readonly _tag: "AuthGeminiLogout" + readonly label: string | null + readonly geminiAuthPath: string +} + +export type AuthCommand = + | AuthGithubLoginCommand + | AuthGithubStatusCommand + | AuthGithubLogoutCommand + | AuthCodexLoginCommand + | AuthCodexImportCommand + | AuthCodexStatusCommand + | AuthCodexLogoutCommand + | AuthClaudeLoginCommand + | AuthClaudeStatusCommand + | AuthClaudeLogoutCommand + | AuthGeminiLoginCommand + | AuthGeminiStatusCommand + | AuthGeminiLogoutCommand +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/core/auto-agent-flags.ts b/packages/app/src/docker-git/frontend-lib/core/auto-agent-flags.ts new file mode 100644 index 00000000..c1683f36 --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/core/auto-agent-flags.ts @@ -0,0 +1,26 @@ +/* jscpd:ignore-start */ +import { Either } from "effect" + +import type { RawOptions } from "./command-options.js" +import type { AgentMode, ParseError } from "./domain.js" + +export const resolveAutoAgentFlags = ( + raw: RawOptions +): Either.Either<{ readonly agentMode: AgentMode | undefined; readonly agentAuto: boolean }, ParseError> => { + const requested = raw.agentAutoMode + if (requested === undefined) { + return Either.right({ agentMode: undefined, agentAuto: false }) + } + if (requested === "auto") { + return Either.right({ agentMode: undefined, agentAuto: true }) + } + if (requested === "claude" || requested === "codex") { + return Either.right({ agentMode: requested, agentAuto: true }) + } + return Either.left({ + _tag: "InvalidOption", + option: "--auto", + reason: "expected one of: claude, codex" + }) +} +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/core/clone.ts b/packages/app/src/docker-git/frontend-lib/core/clone.ts new file mode 100644 index 00000000..6a61c5a8 --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/core/clone.ts @@ -0,0 +1,62 @@ +/* jscpd:ignore-start */ +export type CloneRequest = + | { readonly _tag: "Clone"; readonly args: ReadonlyArray } + | { readonly _tag: "Open"; readonly args: ReadonlyArray } + | { readonly _tag: "None" } + +const emptyRequest: CloneRequest = { _tag: "None" } + +const toCloneRequest = (args: ReadonlyArray): CloneRequest => ({ + _tag: "Clone", + args +}) + +const toOpenRequest = (args: ReadonlyArray): CloneRequest => ({ + _tag: "Open", + args +}) + +const resolveLifecycleArgs = ( + argv: ReadonlyArray, + command: "clone" | "open" +): ReadonlyArray => { + if (argv.length === 0) { + return [] + } + const [first, ...rest] = argv + return first === command ? rest : argv +} + +// CHANGE: resolve clone/open shortcut requests from argv + package lifecycle metadata +// WHY: support bun run clone/open without requiring "--" +// QUOTE(ТЗ): "Добавить команду open. ... Просто открывает существующий по ссылке" +// REF: user-request-2026-01-27 +// SOURCE: n/a +// FORMAT THEOREM: forall a,e: resolve(a,e) -> deterministic +// PURITY: CORE +// EFFECT: Effect +// INVARIANT: command requested only when argv[0] or npmLifecycleEvent is clone/open +// COMPLEXITY: O(n) +export const resolveCloneRequest = ( + argv: ReadonlyArray, + npmLifecycleEvent: string | undefined +): CloneRequest => { + if (npmLifecycleEvent === "clone") { + return toCloneRequest(resolveLifecycleArgs(argv, "clone")) + } + + if (npmLifecycleEvent === "open") { + return toOpenRequest(resolveLifecycleArgs(argv, "open")) + } + + if (argv.length > 0 && argv[0] === "clone") { + return toCloneRequest(argv.slice(1)) + } + + if (argv.length > 0 && argv[0] === "open") { + return toOpenRequest(argv.slice(1)) + } + + return emptyRequest +} +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/core/command-builders-shared.ts b/packages/app/src/docker-git/frontend-lib/core/command-builders-shared.ts new file mode 100644 index 00000000..984811d9 --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/core/command-builders-shared.ts @@ -0,0 +1,55 @@ +/* jscpd:ignore-start */ +import { Either } from "effect" + +import { type CreateCommand, defaultTemplateConfig, isDockerNetworkMode, type ParseError } from "./domain.js" + +const parsePort = (value: string): Either.Either => { + const parsed = Number(value) + if (!Number.isInteger(parsed)) { + return Either.left({ + _tag: "InvalidOption", + option: "--ssh-port", + reason: `expected integer, got: ${value}` + }) + } + if (parsed < 1 || parsed > 65_535) { + return Either.left({ + _tag: "InvalidOption", + option: "--ssh-port", + reason: "must be between 1 and 65535" + }) + } + return Either.right(parsed) +} + +export const parseSshPort = (value: string): Either.Either => parsePort(value) + +export const parseDockerNetworkMode = ( + value: string | undefined +): Either.Either => { + const candidate = value?.trim() ?? defaultTemplateConfig.dockerNetworkMode + if (isDockerNetworkMode(candidate)) { + return Either.right(candidate) + } + return Either.left({ + _tag: "InvalidOption", + option: "--network-mode", + reason: "expected one of: shared, project" + }) +} + +export const nonEmpty = ( + option: string, + value: string | undefined, + fallback?: string +): Either.Either => { + const candidate = value?.trim() ?? fallback + if (candidate === undefined || candidate.length === 0) { + return Either.left({ + _tag: "MissingRequiredOption", + option + }) + } + return Either.right(candidate) +} +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/core/command-builders.ts b/packages/app/src/docker-git/frontend-lib/core/command-builders.ts new file mode 100644 index 00000000..d3c74b06 --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/core/command-builders.ts @@ -0,0 +1,312 @@ +/* jscpd:ignore-start */ +import { Either } from "effect" + +import { expandContainerHome } from "../usecases/scrap-path.js" +import { resolveAutoAgentFlags } from "./auto-agent-flags.js" +import { nonEmpty, parseDockerNetworkMode, parseSshPort } from "./command-builders-shared.js" +import { type RawOptions } from "./command-options.js" +import { + type AgentMode, + type CreateCommand, + defaultCpuLimit, + defaultRamLimit, + defaultTemplateConfig, + deriveRepoPathParts, + deriveRepoSlug, + type ParseError, + resolveRepoInput +} from "./domain.js" +import { normalizeCpuLimit, normalizeRamLimit } from "./resource-limits.js" +import { trimRightChar } from "./strings.js" +import { normalizeAuthLabel, normalizeGitTokenLabel } from "./token-labels.js" + +export { nonEmpty } from "./command-builders-shared.js" + +const normalizeSecretsRoot = (value: string): string => trimRightChar(value, "/") + +type RepoBasics = { + readonly repoUrl: string + readonly repoSlug: string + readonly projectSlug: string + readonly repoPath: string + readonly repoRef: string + readonly targetDir: string + readonly sshUser: string + readonly sshPort: number +} + +const resolveRepoBasics = (raw: RawOptions): Either.Either => + Either.gen(function*(_) { + const rawRepoUrl = raw.repoUrl?.trim() ?? "" + const resolvedRepo = resolveRepoInput(rawRepoUrl) + const repoUrl = resolvedRepo.repoUrl + const repoSlug = deriveRepoSlug(repoUrl) + const repoPathParts = deriveRepoPathParts(repoUrl).pathParts + const workspaceSuffix = resolvedRepo.workspaceSuffix + const projectSlug = workspaceSuffix ? `${repoSlug}-${workspaceSuffix}` : repoSlug + const repoPath = workspaceSuffix ? [...repoPathParts, workspaceSuffix].join("/") : repoPathParts.join("/") + const repoRef = yield* _( + nonEmpty("--repo-ref", raw.repoRef ?? resolvedRepo.repoRef, defaultTemplateConfig.repoRef) + ) + const sshUser = yield* _(nonEmpty("--ssh-user", raw.sshUser, defaultTemplateConfig.sshUser)) + const rawTargetDir = yield* _( + nonEmpty("--target-dir", raw.targetDir, defaultTemplateConfig.targetDir) + ) + const targetDir = expandContainerHome(sshUser, rawTargetDir) + const sshPort = yield* _(parseSshPort(raw.sshPort ?? String(defaultTemplateConfig.sshPort))) + + return { repoUrl, repoSlug, projectSlug, repoPath, repoRef, targetDir, sshUser, sshPort } + }) + +type NameConfig = { + readonly containerName: string + readonly serviceName: string + readonly volumeName: string +} + +const resolveNames = ( + raw: RawOptions, + projectSlug: string +): Either.Either => + Either.gen(function*(_) { + const derivedContainerName = `dg-${projectSlug}` + const derivedServiceName = `dg-${projectSlug}` + const derivedVolumeName = `dg-${projectSlug}-home` + const containerName = yield* _( + nonEmpty("--container-name", raw.containerName, derivedContainerName) + ) + const serviceName = yield* _(nonEmpty("--service-name", raw.serviceName, derivedServiceName)) + const volumeName = yield* _(nonEmpty("--volume-name", raw.volumeName, derivedVolumeName)) + + return { containerName, serviceName, volumeName } + }) + +type PathConfig = { + readonly dockerGitPath: string + readonly authorizedKeysPath: string + readonly envGlobalPath: string + readonly envProjectPath: string + readonly codexAuthPath: string + readonly codexSharedAuthPath: string + readonly codexHome: string + readonly geminiAuthPath: string + readonly geminiHome: string + readonly outDir: string +} + +type DefaultPathConfig = { + readonly dockerGitPath: string + readonly authorizedKeysPath: string + readonly envGlobalPath: string + readonly envProjectPath: string + readonly codexAuthPath: string + readonly geminiAuthPath: string +} + +const resolveNormalizedSecretsRoot = (value: string | undefined): string | undefined => { + const trimmed = value?.trim() ?? "" + return trimmed.length === 0 ? undefined : normalizeSecretsRoot(trimmed) +} + +const buildDefaultPathConfig = ( + normalizedSecretsRoot: string | undefined +): DefaultPathConfig => + normalizedSecretsRoot === undefined + ? { + dockerGitPath: defaultTemplateConfig.dockerGitPath, + authorizedKeysPath: defaultTemplateConfig.authorizedKeysPath, + envGlobalPath: defaultTemplateConfig.envGlobalPath, + envProjectPath: defaultTemplateConfig.envProjectPath, + codexAuthPath: defaultTemplateConfig.codexAuthPath, + geminiAuthPath: defaultTemplateConfig.geminiAuthPath + } + : { + // NOTE: Keep docker-git root mount stable (projects root) so caches like + // `.cache/git-mirrors` remain outside the secrets dir. + dockerGitPath: defaultTemplateConfig.dockerGitPath, + authorizedKeysPath: defaultTemplateConfig.authorizedKeysPath, + envGlobalPath: `${normalizedSecretsRoot}/global.env`, + envProjectPath: defaultTemplateConfig.envProjectPath, + codexAuthPath: `${normalizedSecretsRoot}/codex`, + geminiAuthPath: `${normalizedSecretsRoot}/gemini` + } + +const resolvePaths = ( + raw: RawOptions, + repoPath: string +): Either.Either => + Either.gen(function*(_) { + const normalizedSecretsRoot = resolveNormalizedSecretsRoot(raw.secretsRoot) + const defaults = buildDefaultPathConfig(normalizedSecretsRoot) + const dockerGitPath = defaults.dockerGitPath + const authorizedKeysPath = yield* _( + nonEmpty("--authorized-keys", raw.authorizedKeysPath, defaults.authorizedKeysPath) + ) + const envGlobalPath = yield* _(nonEmpty("--env-global", raw.envGlobalPath, defaults.envGlobalPath)) + const envProjectPath = yield* _( + nonEmpty("--env-project", raw.envProjectPath, defaults.envProjectPath) + ) + const codexAuthPath = yield* _( + nonEmpty("--codex-auth", raw.codexAuthPath, defaults.codexAuthPath) + ) + const codexSharedAuthPath = codexAuthPath + const codexHome = yield* _(nonEmpty("--codex-home", raw.codexHome, defaultTemplateConfig.codexHome)) + const geminiAuthPath = defaults.geminiAuthPath + const geminiHome = defaultTemplateConfig.geminiHome + const outDir = yield* _(nonEmpty("--out-dir", raw.outDir, `.docker-git/${repoPath}`)) + + return { + dockerGitPath, + authorizedKeysPath, + envGlobalPath, + envProjectPath, + codexAuthPath, + codexSharedAuthPath, + codexHome, + geminiAuthPath, + geminiHome, + outDir + } + }) + +type CreateBehavior = { + readonly runUp: boolean + readonly openSsh: boolean + readonly skipGithubAuth: boolean + readonly force: boolean + readonly forceEnv: boolean + readonly enableMcpPlaywright: boolean +} + +const resolveCreateBehavior = (raw: RawOptions): CreateBehavior => ({ + runUp: raw.up ?? true, + openSsh: raw.openSsh ?? false, + skipGithubAuth: raw.skipGithubAuth ?? false, + force: raw.force ?? false, + forceEnv: raw.forceEnv ?? false, + enableMcpPlaywright: raw.enableMcpPlaywright ?? false +}) + +type BuildTemplateConfigInput = { + readonly repo: RepoBasics + readonly names: NameConfig + readonly paths: PathConfig + readonly cpuLimit: string | undefined + readonly ramLimit: string | undefined + readonly dockerNetworkMode: CreateCommand["config"]["dockerNetworkMode"] + readonly dockerSharedNetworkName: string + readonly gitTokenLabel: string | undefined + readonly skipGithubAuth: boolean + readonly codexAuthLabel: string | undefined + readonly claudeAuthLabel: string | undefined + readonly enableMcpPlaywright: boolean + readonly agentMode: AgentMode | undefined + readonly agentAuto: boolean + readonly clonedOnHostname?: string | undefined +} + +const buildTemplateConfig = ({ + agentAuto, + agentMode, + claudeAuthLabel, + clonedOnHostname, + codexAuthLabel, + cpuLimit, + dockerNetworkMode, + dockerSharedNetworkName, + enableMcpPlaywright, + gitTokenLabel, + names, + paths, + ramLimit, + repo, + skipGithubAuth +}: BuildTemplateConfigInput): CreateCommand["config"] => ({ + containerName: names.containerName, + serviceName: names.serviceName, + sshUser: repo.sshUser, + sshPort: repo.sshPort, + repoUrl: repo.repoUrl, + repoRef: repo.repoRef, + gitTokenLabel, + skipGithubAuth, + codexAuthLabel, + claudeAuthLabel, + targetDir: repo.targetDir, + volumeName: names.volumeName, + dockerGitPath: paths.dockerGitPath, + authorizedKeysPath: paths.authorizedKeysPath, + envGlobalPath: paths.envGlobalPath, + envProjectPath: paths.envProjectPath, + codexAuthPath: paths.codexAuthPath, + codexSharedAuthPath: paths.codexSharedAuthPath, + codexHome: paths.codexHome, + geminiAuthPath: paths.geminiAuthPath, + geminiHome: paths.geminiHome, + cpuLimit, + ramLimit, + dockerNetworkMode, + dockerSharedNetworkName, + enableMcpPlaywright, + bunVersion: defaultTemplateConfig.bunVersion, + agentMode, + agentAuto, + clonedOnHostname +}) + +// CHANGE: build a typed create command from raw options (CLI or API) +// WHY: share deterministic command construction across CLI and server +// QUOTE(ТЗ): "В lib ты оставляешь бизнес логику, а все CLI морду хранишь в app" +// REF: user-request-2026-02-02-cli-split +// SOURCE: n/a +// FORMAT THEOREM: forall raw: build(raw) -> deterministic(command) +// PURITY: CORE +// EFFECT: Effect +// INVARIANT: uses defaults for unset fields +// COMPLEXITY: O(1) +export const buildCreateCommand = ( + raw: RawOptions +): Either.Either => + Either.gen(function*(_) { + const repo = yield* _(resolveRepoBasics(raw)) + const names = yield* _(resolveNames(raw, repo.projectSlug)) + const paths = yield* _(resolvePaths(raw, repo.repoPath)) + const behavior = resolveCreateBehavior(raw) + const gitTokenLabel = normalizeGitTokenLabel(raw.gitTokenLabel) + const codexAuthLabel = normalizeAuthLabel(raw.codexTokenLabel) + const claudeAuthLabel = normalizeAuthLabel(raw.claudeTokenLabel) + const cpuLimit = yield* _(normalizeCpuLimit(raw.cpuLimit ?? defaultCpuLimit, "--cpu")) + const ramLimit = yield* _(normalizeRamLimit(raw.ramLimit ?? defaultRamLimit, "--ram")) + const dockerNetworkMode = yield* _(parseDockerNetworkMode(raw.dockerNetworkMode)) + const dockerSharedNetworkName = yield* _( + nonEmpty("--shared-network", raw.dockerSharedNetworkName, defaultTemplateConfig.dockerSharedNetworkName) + ) + const { agentAuto, agentMode } = yield* _(resolveAutoAgentFlags(raw)) + + return { + _tag: "Create", + outDir: paths.outDir, + runUp: behavior.runUp, + openSsh: behavior.openSsh, + force: behavior.force, + forceEnv: behavior.forceEnv, + waitForClone: false, + config: buildTemplateConfig({ + repo, + names, + paths, + cpuLimit, + ramLimit, + dockerNetworkMode, + dockerSharedNetworkName, + gitTokenLabel, + skipGithubAuth: behavior.skipGithubAuth, + codexAuthLabel, + claudeAuthLabel, + enableMcpPlaywright: behavior.enableMcpPlaywright, + agentMode, + agentAuto + }) + } + }) +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/core/command-options.ts b/packages/app/src/docker-git/frontend-lib/core/command-options.ts new file mode 100644 index 00000000..654be97e --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/core/command-options.ts @@ -0,0 +1,75 @@ +/* jscpd:ignore-start */ +import { type ParseError } from "./domain.js" + +// CHANGE: define reusable command option shape for create/clone/auth builders +// WHY: decouple pure command construction from CLI parsing locations +// QUOTE(ТЗ): "В lib ты оставляешь бизнес логику, а все CLI морду хранишь в app" +// REF: user-request-2026-02-02-cli-split +// SOURCE: n/a +// FORMAT THEOREM: forall o: RawOptions -> deterministic(o) +// PURITY: CORE +// EFFECT: Effect +// INVARIANT: all fields are optional and represent raw user intent +// COMPLEXITY: O(1) +export interface RawOptions { + readonly repoUrl?: string + readonly repoRef?: string + readonly targetDir?: string + readonly sshPort?: string + readonly sshUser?: string + readonly containerName?: string + readonly serviceName?: string + readonly volumeName?: string + readonly secretsRoot?: string + readonly authorizedKeysPath?: string + readonly envGlobalPath?: string + readonly envProjectPath?: string + readonly codexAuthPath?: string + readonly codexHome?: string + readonly cpuLimit?: string + readonly ramLimit?: string + readonly dockerNetworkMode?: string + readonly dockerSharedNetworkName?: string + readonly enableMcpPlaywright?: boolean + readonly archivePath?: string + readonly scrapMode?: string + readonly wipe?: boolean + readonly label?: string + readonly gitTokenLabel?: string + readonly codexTokenLabel?: string + readonly claudeTokenLabel?: string + readonly token?: string + readonly scopes?: string + readonly message?: string + readonly authWeb?: boolean + readonly authOauth?: boolean + readonly outDir?: string + readonly projectDir?: string + readonly lines?: string + readonly includeDefault?: boolean + readonly up?: boolean + readonly openSsh?: boolean + readonly skipGithubAuth?: boolean + readonly force?: boolean + readonly forceEnv?: boolean + readonly agentAutoMode?: string + // Session gist options (issue-143) + readonly prNumber?: string + readonly repo?: string + readonly noComment?: boolean + readonly limit?: string + readonly output?: string +} + +// CHANGE: helper type alias for builder signatures that produce parse errors +// WHY: keep error typing consistent without CLI parsing +// QUOTE(ТЗ): "Ошибки типизированы" +// REF: user-request-2026-02-02-cli-split +// SOURCE: n/a +// FORMAT THEOREM: forall e: ParseError -> typed(e) +// PURITY: CORE +// EFFECT: Effect +// INVARIANT: ParseError tags are preserved +// COMPLEXITY: O(1) +export type CommandBuildError = ParseError +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/core/domain.ts b/packages/app/src/docker-git/frontend-lib/core/domain.ts new file mode 100644 index 00000000..60cb7674 --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/core/domain.ts @@ -0,0 +1,286 @@ +/* jscpd:ignore-start */ +import type { AuthCommand } from "./auth-domain.js" +import type { SessionsCommand } from "./sessions-domain.js" +import type { StateCommand } from "./state-domain.js" + +export type { + AuthClaudeLoginCommand, + AuthClaudeLogoutCommand, + AuthClaudeStatusCommand, + AuthCodexImportCommand, + AuthCodexLoginCommand, + AuthCodexLogoutCommand, + AuthCodexStatusCommand, + AuthCommand, + AuthGeminiLoginCommand, + AuthGeminiLogoutCommand, + AuthGeminiStatusCommand, + AuthGithubLoginCommand, + AuthGithubLogoutCommand, + AuthGithubStatusCommand +} from "./auth-domain.js" +export type { MenuAction, ParseError } from "./menu.js" +export { parseMenuSelection } from "./menu.js" +export { deriveRepoPathParts, deriveRepoSlug, resolveRepoInput } from "./repo.js" +export type { + SessionsCommand, + SessionsKillCommand, + SessionsListCommand, + SessionsLogsCommand +} from "./sessions-domain.js" +export type { + StateCommand, + StateCommitCommand, + StateInitCommand, + StatePathCommand, + StatePullCommand, + StatePushCommand, + StateStatusCommand, + StateSyncCommand +} from "./state-domain.js" +export { + defaultCpuLimit, + defaultDockerNetworkMode, + defaultDockerSharedNetworkName, + defaultRamLimit, + defaultTemplateConfig, + dockerGitSharedCacheVolumeName, + dockerGitSharedCodexVolumeName +} from "./template-defaults.js" + +export type AgentMode = "claude" | "codex" | "gemini" + +export type DockerNetworkMode = "shared" | "project" + +export interface TemplateConfig { + readonly containerName: string + readonly serviceName: string + readonly sshUser: string + readonly sshPort: number + readonly repoUrl: string + readonly repoRef: string + readonly forkRepoUrl?: string + readonly gitTokenLabel?: string | undefined + readonly skipGithubAuth: boolean + readonly codexAuthLabel?: string | undefined + readonly claudeAuthLabel?: string | undefined + readonly targetDir: string + readonly volumeName: string + readonly dockerGitPath: string + readonly authorizedKeysPath: string + readonly envGlobalPath: string + readonly envProjectPath: string + readonly codexAuthPath: string + readonly codexSharedAuthPath: string + readonly codexHome: string + readonly geminiAuthLabel?: string | undefined + readonly geminiAuthPath: string + readonly geminiHome: string + readonly cpuLimit?: string | undefined + readonly ramLimit?: string | undefined + readonly dockerNetworkMode: DockerNetworkMode + readonly dockerSharedNetworkName: string + readonly enableMcpPlaywright: boolean + readonly bunVersion: string + readonly agentMode?: AgentMode | undefined + readonly agentAuto?: boolean | undefined + readonly clonedOnHostname?: string | undefined +} + +export interface ProjectConfig { + readonly schemaVersion: 1 + readonly template: TemplateConfig +} + +export interface CreateCommand { + readonly _tag: "Create" + readonly config: TemplateConfig + readonly outDir: string + readonly runUp: boolean + readonly force: boolean + readonly forceEnv: boolean + readonly waitForClone: boolean + readonly openSsh: boolean +} + +export interface MenuCommand { + readonly _tag: "Menu" +} + +export interface AttachCommand { + readonly _tag: "Attach" + readonly projectDir: string +} + +export interface OpenCommand { + readonly _tag: "Open" + readonly projectRef?: string | undefined + readonly projectDir?: string | undefined +} + +export interface PanesCommand { + readonly _tag: "Panes" + readonly projectDir: string +} + +// CHANGE: remove scrap cache mode and keep only the reproducible session snapshot. +// WHY: cache archives include large, easily-rebuildable artifacts (e.g. node_modules) that should not be stored in git. +// QUOTE(ТЗ): "не должно быть старого режима где он качает весь шлак типо node_modules" +// REF: user-request-2026-02-15 +// SOURCE: n/a +// FORMAT THEOREM: forall m: ScrapMode, m = "session" +// PURITY: CORE +// EFFECT: Effect +// INVARIANT: scrap exports/imports are always recipe-like (git state + small secrets), never full workspace caches +// COMPLEXITY: O(1) +export type ScrapMode = "session" + +export interface ScrapExportCommand { + readonly _tag: "ScrapExport" + readonly projectDir: string + readonly archivePath: string + readonly mode: ScrapMode +} + +export interface ScrapImportCommand { + readonly _tag: "ScrapImport" + readonly projectDir: string + readonly archivePath: string + readonly wipe: boolean + readonly mode: ScrapMode +} + +export interface McpPlaywrightUpCommand { + readonly _tag: "McpPlaywrightUp" + readonly projectDir: string + readonly runUp: boolean +} + +export interface ApplyCommand { + readonly _tag: "Apply" + readonly projectDir: string + readonly runUp: boolean + readonly gitTokenLabel?: string | undefined + readonly codexTokenLabel?: string | undefined + readonly claudeTokenLabel?: string | undefined + readonly geminiTokenLabel?: string | undefined + readonly cpuLimit?: string | undefined + readonly ramLimit?: string | undefined + readonly enableMcpPlaywright?: boolean | undefined +} + +// CHANGE: add apply-all command to apply docker-git config to every known project; support --active flag +// WHY: allow bulk-updating all containers in one command; --active restricts to currently running containers only +// QUOTE(ТЗ): "Сделать команду которая сама на все контейнеры применит новые настройки" +// QUOTE(ТЗ): "сделать это возможным через атрибут --active применять только к активным контейнерам, а не ко всем" +// REF: issue-164, issue-185 +// PURITY: CORE +// EFFECT: n/a +// INVARIANT: when activeOnly=false applies to all discovered projects; when activeOnly=true applies only to running containers; individual failures do not abort the batch +// COMPLEXITY: O(1) +export interface ApplyAllCommand { + readonly _tag: "ApplyAll" + readonly activeOnly: boolean +} + +export interface HelpCommand { + readonly _tag: "Help" + readonly message: string +} + +export interface StatusCommand { + readonly _tag: "Status" +} + +export interface DownAllCommand { + readonly _tag: "DownAll" +} + +export type { + SessionGistBackupCommand, + SessionGistCommand, + SessionGistDownloadCommand, + SessionGistListCommand, + SessionGistViewCommand +} from "./session-gist-domain.js" + +export type ScrapCommand = + | ScrapExportCommand + | ScrapImportCommand + +export type Command = + | CreateCommand + | MenuCommand + | AttachCommand + | OpenCommand + | PanesCommand + | SessionsCommand + | ScrapCommand + | McpPlaywrightUpCommand + | ApplyCommand + | ApplyAllCommand + | HelpCommand + | StatusCommand + | DownAllCommand + | StateCommand + | AuthCommand + +// CHANGE: validate docker network mode values at the CLI/config boundary +// WHY: keep compose network behavior explicit and type-safe +// QUOTE(ТЗ): "Что бы среды были изолированы?" +// REF: user-request-2026-02-20-networks +// SOURCE: n/a +// FORMAT THEOREM: ∀x: isDockerNetworkMode(x) -> x ∈ {"shared","project"} +// PURITY: CORE +// EFFECT: n/a +// INVARIANT: returns true only for known modes +// COMPLEXITY: O(1) +export const isDockerNetworkMode = (value: string): value is DockerNetworkMode => + value === "shared" || value === "project" + +// CHANGE: derive compose network name from typed template config +// WHY: keep network naming deterministic across template generation and runtime checks +// QUOTE(ТЗ): "Если я хочу уникальную сеть на каждый контейнер?" +// REF: user-request-2026-02-20-networks +// SOURCE: n/a +// FORMAT THEOREM: ∀cfg: resolveComposeNetworkName(cfg) = n -> deterministic(n) +// PURITY: CORE +// EFFECT: n/a +// INVARIANT: shared mode always resolves to dockerSharedNetworkName; project mode to "-net" +// COMPLEXITY: O(1) +export const resolveComposeNetworkName = ( + config: Pick +): string => + config.dockerNetworkMode === "shared" + ? config.dockerSharedNetworkName + : `${config.serviceName}-net` + +// CHANGE: derive a stable bootstrap volume name for per-project runtime bootstrap data +// WHY: API/controller mode cannot rely on host bind mounts for auth/env material +// QUOTE(ТЗ): "У нас есть CLI который вызывает docker ? ... Поднимается сервер и ты через него можешь общаться с контейнером" +// REF: user-request-2026-03-15-api-controller +// SOURCE: n/a +// FORMAT THEOREM: ∀cfg: resolveProjectBootstrapVolumeName(cfg) = v -> deterministic(v) +// PURITY: CORE +// EFFECT: n/a +// INVARIANT: bootstrap volume name is derived solely from project volumeName +// COMPLEXITY: O(1) +export const resolveProjectBootstrapVolumeName = ( + config: Pick +): string => `${config.volumeName}-bootstrap` + +// CHANGE: derive a stable docker compose project name from typed template config +// WHY: managed project lifecycle must not depend on the output directory basename, otherwise "docker compose down -v" +// can target an unrelated stack that happens to share the same folder name (for example the controller repo itself). +// QUOTE(ТЗ): "Весь процесс должен быть высроен так что основной бекенд крутится в docker" +// REF: user-request-2026-04-03-compose-project-isolation +// SOURCE: n/a +// FORMAT THEOREM: ∀cfg: resolveComposeProjectName(cfg) = p -> deterministic(p) ∧ isolated_compose_project(p) +// PURITY: CORE +// EFFECT: n/a +// INVARIANT: compose project identity is derived solely from serviceName, never cwd basename +// COMPLEXITY: O(1) +export const resolveComposeProjectName = ( + config: Pick +): string => config.serviceName +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/core/menu.ts b/packages/app/src/docker-git/frontend-lib/core/menu.ts new file mode 100644 index 00000000..e6e4f686 --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/core/menu.ts @@ -0,0 +1,113 @@ +/* jscpd:ignore-start */ +import { Either } from "effect" + +export type MenuAction = + | { readonly _tag: "Create" } + | { readonly _tag: "Select" } + | { readonly _tag: "Auth" } + | { readonly _tag: "ProjectAuth" } + | { readonly _tag: "Info" } + | { readonly _tag: "Up" } + | { readonly _tag: "Status" } + | { readonly _tag: "Logs" } + | { readonly _tag: "Down" } + | { readonly _tag: "DownAll" } + | { readonly _tag: "Delete" } + | { readonly _tag: "Quit" } + +export type ParseError = + | { readonly _tag: "UnknownCommand"; readonly command: string } + | { readonly _tag: "UnknownOption"; readonly option: string } + | { readonly _tag: "MissingOptionValue"; readonly option: string } + | { readonly _tag: "MissingRequiredOption"; readonly option: string } + | { readonly _tag: "InvalidOption"; readonly option: string; readonly reason: string } + | { readonly _tag: "UnexpectedArgument"; readonly value: string } + +const normalizeMenuInput = (input: string): string => input.trim().toLowerCase() + +const menuAliasMap = new Map([ + ["1", { _tag: "Create" }], + ["create", { _tag: "Create" }], + ["c", { _tag: "Create" }], + ["2", { _tag: "Select" }], + ["select", { _tag: "Select" }], + ["s", { _tag: "Select" }], + ["3", { _tag: "Auth" }], + ["auth", { _tag: "Auth" }], + ["a", { _tag: "Auth" }], + ["4", { _tag: "ProjectAuth" }], + ["project-auth", { _tag: "ProjectAuth" }], + ["projectauth", { _tag: "ProjectAuth" }], + ["pa", { _tag: "ProjectAuth" }], + ["5", { _tag: "Info" }], + ["info", { _tag: "Info" }], + ["i", { _tag: "Info" }], + ["up", { _tag: "Up" }], + ["u", { _tag: "Up" }], + ["start", { _tag: "Up" }], + ["6", { _tag: "Status" }], + ["status", { _tag: "Status" }], + ["ps", { _tag: "Status" }], + ["7", { _tag: "Logs" }], + ["logs", { _tag: "Logs" }], + ["log", { _tag: "Logs" }], + ["l", { _tag: "Logs" }], + ["8", { _tag: "Down" }], + ["down", { _tag: "Down" }], + ["stop", { _tag: "Down" }], + ["d", { _tag: "Down" }], + ["9", { _tag: "DownAll" }], + ["down-all", { _tag: "DownAll" }], + ["downall", { _tag: "DownAll" }], + ["stop-all", { _tag: "DownAll" }], + ["stopall", { _tag: "DownAll" }], + ["kill-all", { _tag: "DownAll" }], + ["killall", { _tag: "DownAll" }], + ["da", { _tag: "DownAll" }], + ["10", { _tag: "Delete" }], + ["delete", { _tag: "Delete" }], + ["del", { _tag: "Delete" }], + ["remove", { _tag: "Delete" }], + ["rm", { _tag: "Delete" }], + ["0", { _tag: "Quit" }], + ["11", { _tag: "Quit" }], + ["quit", { _tag: "Quit" }], + ["q", { _tag: "Quit" }], + ["exit", { _tag: "Quit" }] +]) + +const resolveMenuAction = (normalized: string): MenuAction | undefined => menuAliasMap.get(normalized) + +// CHANGE: decode interactive menu input into a typed action +// WHY: keep menu parsing pure and reusable across shells +// QUOTE(ТЗ): "Хочу что бы открылось менюшка" +// REF: user-request-2026-01-07 +// SOURCE: n/a +// FORMAT THEOREM: forall s: parseMenu(s) = a -> deterministic(a) +// PURITY: CORE +// EFFECT: Effect +// INVARIANT: unknown input maps to InvalidOption +// COMPLEXITY: O(1) +export const parseMenuSelection = (input: string): Either.Either => { + const normalized = normalizeMenuInput(input) + + if (normalized.length === 0) { + return Either.left({ + _tag: "InvalidOption", + option: "menu", + reason: "empty selection" + }) + } + + const action = resolveMenuAction(normalized) + if (action === undefined) { + return Either.left({ + _tag: "InvalidOption", + option: "menu", + reason: `unknown selection: ${input}` + }) + } + + return Either.right(action) +} +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/core/parse-errors.ts b/packages/app/src/docker-git/frontend-lib/core/parse-errors.ts new file mode 100644 index 00000000..c519ee34 --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/core/parse-errors.ts @@ -0,0 +1,26 @@ +/* jscpd:ignore-start */ +import { Match } from "effect" + +import type { ParseError } from "./domain.js" + +// CHANGE: normalize parse errors into deterministic messages +// WHY: reuse parse error formatting across CLI and server flows +// QUOTE(ТЗ): "ошибки должны быть описывающими" +// REF: user-request-2026-02-02-cli-split +// SOURCE: n/a +// FORMAT THEOREM: forall e: format(e) = s -> deterministic(s) +// PURITY: CORE +// EFFECT: Effect +// INVARIANT: each ParseError maps to exactly one message +// COMPLEXITY: O(1) +export const formatParseError = (error: ParseError): string => + Match.value(error).pipe( + Match.when({ _tag: "UnknownCommand" }, ({ command }) => `Unknown command: ${command}`), + Match.when({ _tag: "UnknownOption" }, ({ option }) => `Unknown option: ${option}`), + Match.when({ _tag: "MissingOptionValue" }, ({ option }) => `Missing value for option: ${option}`), + Match.when({ _tag: "MissingRequiredOption" }, ({ option }) => `Missing required option: ${option}`), + Match.when({ _tag: "InvalidOption" }, ({ option, reason }) => `Invalid option ${option}: ${reason}`), + Match.when({ _tag: "UnexpectedArgument" }, ({ value }) => `Unexpected argument: ${value}`), + Match.exhaustive + ) +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/core/repo.ts b/packages/app/src/docker-git/frontend-lib/core/repo.ts new file mode 100644 index 00000000..dfadf7fa --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/core/repo.ts @@ -0,0 +1,317 @@ +/* jscpd:ignore-start */ +import { trimLeftChar, trimRightChar } from "./strings.js" + +const slugify = (value: string): string => { + const normalized = value + .trim() + .toLowerCase() + .replaceAll(/[^a-z0-9_-]+/g, "-") + .replaceAll(/-+/g, "-") + const withoutLeading = trimLeftChar(normalized, "-") + const cleaned = trimRightChar(withoutLeading, "-") + + return cleaned.length > 0 ? cleaned : "app" +} + +// CHANGE: derive a stable repo slug from a repo URL +// WHY: generate deterministic container/service names per repository +// QUOTE(ТЗ): "по факту он должен создавтаь постоянно новый контейнер для нового репозитория" +// REF: user-request-2026-01-07 +// SOURCE: n/a +// FORMAT THEOREM: forall url: slug(url) = s -> deterministic(s) +// PURITY: CORE +// EFFECT: Effect +// INVARIANT: slug is lowercase and non-empty +// COMPLEXITY: O(n) where n = |url| +export const deriveRepoSlug = (repoUrl: string): string => { + const trimmed = trimRightChar(repoUrl.trim(), "/") + if (trimmed.length === 0) { + return "app" + } + + const lastSlash = trimmed.lastIndexOf("/") + const lastColon = trimmed.lastIndexOf(":") + const pivot = Math.max(lastSlash, lastColon) + const segment = pivot >= 0 ? trimmed.slice(pivot + 1) : trimmed + const withoutGit = segment.endsWith(".git") ? segment.slice(0, -4) : segment + + return slugify(withoutGit) +} + +type RepoPathParts = { + readonly ownerParts: ReadonlyArray + readonly repo: string + readonly pathParts: ReadonlyArray +} + +const stripGitSuffix = (segment: string): string => segment.endsWith(".git") ? segment.slice(0, -4) : segment + +const normalizePathParts = (pathPart: string): ReadonlyArray => { + const cleaned = trimLeftChar(pathPart, "/") + if (cleaned.length === 0) { + return [] + } + const rawParts = cleaned.split("/").filter(Boolean) + return rawParts.map((part, index) => index === rawParts.length - 1 ? stripGitSuffix(part) : part) +} + +const extractFromScheme = (trimmed: string): ReadonlyArray | null => { + const schemeIndex = trimmed.indexOf("://") + if (schemeIndex === -1) { + return null + } + const afterScheme = trimmed.slice(schemeIndex + 3) + const firstSlash = afterScheme.indexOf("/") + if (firstSlash === -1) { + return [] + } + return normalizePathParts(afterScheme.slice(firstSlash + 1)) +} + +const extractFromColon = (trimmed: string): ReadonlyArray | null => { + const colonIndex = trimmed.indexOf(":") + if (colonIndex === -1) { + return null + } + return normalizePathParts(trimmed.slice(colonIndex + 1)) +} + +const extractFromSlash = (trimmed: string): ReadonlyArray | null => { + const slashIndex = trimmed.indexOf("/") + if (slashIndex === -1) { + return null + } + return normalizePathParts(trimmed.slice(slashIndex + 1)) +} + +const extractRepoPathParts = (repoUrl: string): ReadonlyArray => { + const trimmed = trimRightChar(repoUrl.trim(), "/") + if (trimmed.length === 0) { + return [] + } + + const fromScheme = extractFromScheme(trimmed) + if (fromScheme !== null) { + return fromScheme + } + + const fromColon = extractFromColon(trimmed) + if (fromColon !== null) { + return fromColon + } + + const fromSlash = extractFromSlash(trimmed) + if (fromSlash !== null) { + return fromSlash + } + + return [stripGitSuffix(trimmed)] +} + +const normalizeRepoSegment = (segment: string, fallback: string): string => { + const normalized = slugify(segment) + return normalized.length > 0 ? normalized : fallback +} + +// CHANGE: derive stable owner/repo path parts from a repo URL +// WHY: avoid collisions when orgs have identical repo names +// QUOTE(ТЗ): "пути учитывают организацию в которой это лежит" +// REF: user-request-2026-01-27 +// SOURCE: n/a +// FORMAT THEOREM: forall url: parts(url) -> deterministic(parts) +// PURITY: CORE +// EFFECT: Effect +// INVARIANT: path parts are slugified and non-empty +// COMPLEXITY: O(n) where n = |url| +export const deriveRepoPathParts = (repoUrl: string): RepoPathParts => { + const repoSlug = deriveRepoSlug(repoUrl) + const rawParts = extractRepoPathParts(repoUrl) + if (rawParts.length === 0) { + return { ownerParts: [], repo: repoSlug, pathParts: [repoSlug] } + } + + const rawRepo = rawParts.at(-1) ?? repoSlug + const repo = normalizeRepoSegment(rawRepo, repoSlug) + const ownerParts = rawParts + .slice(0, -1) + .map((part) => normalizeRepoSegment(part, "org")) + .filter((part) => part.length > 0) + const pathParts = ownerParts.length > 0 ? [...ownerParts, repo] : [repo] + + return { ownerParts, repo, pathParts } +} + +export type GithubRepo = { + readonly owner: string + readonly repo: string +} + +const stripQueryHash = (value: string): string => { + const queryIndex = value.indexOf("?") + const hashIndex = value.indexOf("#") + const indices = [queryIndex, hashIndex].filter((index) => index >= 0) + if (indices.length === 0) { + return value + } + const cutIndex = Math.min(...indices) + return value.slice(0, cutIndex) +} + +const splitGithubPath = (input: string): ReadonlyArray | null => { + const trimmed = input.trim() + const httpsPrefix = "https://github.com/" + const sshPrefix = "ssh://git@github.com/" + const gitPrefix = "git@github.com:" + let rest: string | null = null + if (trimmed.startsWith(httpsPrefix)) { + rest = trimmed.slice(httpsPrefix.length) + } else if (trimmed.startsWith(sshPrefix)) { + rest = trimmed.slice(sshPrefix.length) + } else if (trimmed.startsWith(gitPrefix)) { + rest = trimmed.slice(gitPrefix.length) + } + if (rest === null) { + return null + } + const cleaned = trimRightChar(stripQueryHash(rest), "/") + if (cleaned.length === 0) { + return [] + } + return cleaned.split("/").filter((part) => part.length > 0) +} + +// CHANGE: parse GitHub owner/repo from common URL formats +// WHY: enable auto-fork logic without relying on slugified paths +// QUOTE(ТЗ): "Сразу на issues и он бы делал форк репы если это надо" +// REF: user-request-2026-02-05-issues-fork +// SOURCE: n/a +// FORMAT THEOREM: ∀u: github(u) → repo(u) = {owner, repo} +// PURITY: CORE +// EFFECT: n/a +// INVARIANT: returns null for non-GitHub inputs +// COMPLEXITY: O(n) where n = |input| +export const parseGithubRepoUrl = (input: string): GithubRepo | null => { + const parts = splitGithubPath(input) + if (!parts || parts.length < 2) { + return null + } + + const owner = parts[0]?.trim() + const repoRaw = parts[1]?.trim() + if (!owner || !repoRaw) { + return null + } + + const repo = stripGitSuffix(repoRaw) + return { owner, repo } +} + +export type ResolvedRepoInput = { + readonly repoUrl: string + readonly repoRef?: string + readonly workspaceSuffix?: string +} + +type GithubRefParts = { + readonly owner: string + readonly repoRaw: string + readonly marker: string + readonly ref: string +} + +const readGithubPart = (value: string | undefined): string | null => { + const trimmed = value?.trim() ?? "" + return trimmed.length > 0 ? trimmed : null +} + +const parseGithubRefParts = (input: string): GithubRefParts | null => { + const parts = splitGithubPath(input) + if (!parts || parts.length < 4) { + return null + } + const owner = readGithubPart(parts[0]) + const repoRaw = readGithubPart(parts[1]) + const markerRaw = readGithubPart(parts[2]) + const ref = readGithubPart(parts[3]) + if (!owner || !repoRaw || !markerRaw || !ref) { + return null + } + return { owner, repoRaw, marker: markerRaw.toLowerCase(), ref } +} + +const parseGithubPrUrl = (input: string): ResolvedRepoInput | null => { + const parsed = parseGithubRefParts(input) + if (!parsed || parsed.marker !== "pull") { + return null + } + + const repo = stripGitSuffix(parsed.repoRaw) + const workspaceSuffix = `pr-${slugify(parsed.ref)}` + return { + repoUrl: `https://github.com/${parsed.owner}/${repo}.git`, + repoRef: `refs/pull/${parsed.ref}/head`, + workspaceSuffix + } +} + +// CHANGE: normalize GitHub tree/blob URLs into repo + ref +// WHY: allow docker-git clone to accept branch URLs like /tree/ +// QUOTE(ТЗ): "вызови --force на https://github.com/agiens/crm/tree/vova-fork" +// REF: user-request-2026-02-10-github-tree-url +// SOURCE: n/a +// FORMAT THEOREM: ∀u: tree(u) → repo(u)=git(u) ∧ ref(u)=branch(u) +// PURITY: CORE +// EFFECT: n/a +// INVARIANT: ignores additional path segments after the ref +// COMPLEXITY: O(n) where n = |url| +const parseGithubTreeUrl = (input: string): ResolvedRepoInput | null => { + const parsed = parseGithubRefParts(input) + if (!parsed || (parsed.marker !== "tree" && parsed.marker !== "blob")) { + return null + } + + const repo = stripGitSuffix(parsed.repoRaw) + return { repoUrl: `https://github.com/${parsed.owner}/${repo}.git`, repoRef: parsed.ref } +} + +// CHANGE: normalize GitHub issue URLs into repo URLs +// WHY: allow docker-git clone to accept issue links directly +// QUOTE(ТЗ): "Сразу на issues" +// REF: user-request-2026-02-05-issues +// SOURCE: n/a +// FORMAT THEOREM: ∀u: issue(u) → repo(u) +// PURITY: CORE +// EFFECT: n/a +// INVARIANT: issue URL yields repoUrl + deterministic issue branch +// COMPLEXITY: O(n) where n = |url| +const parseGithubIssueUrl = (input: string): ResolvedRepoInput | null => { + const parsed = parseGithubRefParts(input) + if (!parsed || parsed.marker !== "issues") { + return null + } + + const repo = stripGitSuffix(parsed.repoRaw) + const workspaceSuffix = `issue-${slugify(parsed.ref)}` + return { + repoUrl: `https://github.com/${parsed.owner}/${repo}.git`, + repoRef: workspaceSuffix, + workspaceSuffix + } +} + +// CHANGE: normalize repo input and PR/issue URLs into repo + ref +// WHY: allow cloning GitHub PR links and issue links directly +// QUOTE(ТЗ): "клонировть по cсылке на PR" | "Сразу на issues" +// REF: user-request-2026-01-28-pr | user-request-2026-02-05-issues +// SOURCE: n/a +// FORMAT THEOREM: forall url: resolve(url) -> deterministic(url, ref) +// PURITY: CORE +// EFFECT: Effect +// INVARIANT: PR URL yields repoUrl + refs/pull//head +// COMPLEXITY: O(n) where n = |url| +export const resolveRepoInput = (repoUrl: string): ResolvedRepoInput => + parseGithubPrUrl(repoUrl) + ?? parseGithubTreeUrl(repoUrl) + ?? parseGithubIssueUrl(repoUrl) + ?? { repoUrl: repoUrl.trim() } +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/core/resource-limits.ts b/packages/app/src/docker-git/frontend-lib/core/resource-limits.ts new file mode 100644 index 00000000..1402a4b8 --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/core/resource-limits.ts @@ -0,0 +1,145 @@ +/* jscpd:ignore-start */ +import { Either } from "effect" + +import { defaultCpuLimit, defaultRamLimit, type ParseError, type TemplateConfig } from "./domain.js" + +const mebibyte = 1024 ** 2 +const minimumResolvedCpuLimit = 0.25 +const minimumResolvedRamLimitMib = 512 +const precisionScale = 100 + +type HostResources = { + readonly cpuCount: number + readonly totalMemoryBytes: number +} + +export type ResolvedComposeResourceLimits = { + readonly cpuLimit: number + readonly ramLimit: string +} + +const cpuAbsolutePattern = /^\d+(?:\.\d+)?$/u +const ramAbsolutePattern = /^\d+(?:\.\d+)?(?:b|k|kb|m|mb|g|gb|t|tb)$/iu +const percentPattern = /^\d+(?:\.\d+)?%$/u + +const normalizePrecision = (value: number): number => Math.round(value * precisionScale) / precisionScale + +const missingLimit = (): string | undefined => undefined + +const parsePercent = (candidate: string): number | null => { + if (!percentPattern.test(candidate)) { + return null + } + const parsed = Number(candidate.slice(0, -1)) + if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 100) { + return null + } + return normalizePrecision(parsed) +} + +const percentReason = (kind: "cpu" | "ram"): string => + kind === "cpu" + ? "expected CPU like 30% or 1.5" + : "expected RAM like 30%, 512m or 4g" + +const normalizePercent = (candidate: string, kind: "cpu" | "ram"): Either.Either => { + const parsed = parsePercent(candidate) + if (parsed === null) { + return Either.left({ + _tag: "InvalidOption", + option: kind === "cpu" ? "--cpu" : "--ram", + reason: percentReason(kind) + }) + } + return Either.right(`${parsed}%`) +} + +export const normalizeCpuLimit = ( + value: string | undefined, + option: string +): Either.Either => { + const candidate = value?.trim().toLowerCase() ?? "" + if (candidate.length === 0) { + return Either.right(missingLimit()) + } + if (candidate.endsWith("%")) { + return normalizePercent(candidate, "cpu") + } + if (!cpuAbsolutePattern.test(candidate)) { + return Either.left({ + _tag: "InvalidOption", + option, + reason: "expected CPU like 30% or 1.5" + }) + } + const parsed = Number(candidate) + if (!Number.isFinite(parsed) || parsed <= 0) { + return Either.left({ + _tag: "InvalidOption", + option, + reason: "must be greater than 0" + }) + } + return Either.right(String(normalizePrecision(parsed))) +} + +export const normalizeRamLimit = ( + value: string | undefined, + option: string +): Either.Either => { + const candidate = value?.trim().toLowerCase() ?? "" + if (candidate.length === 0) { + return Either.right(missingLimit()) + } + if (candidate.endsWith("%")) { + return normalizePercent(candidate, "ram") + } + if (!ramAbsolutePattern.test(candidate)) { + return Either.left({ + _tag: "InvalidOption", + option, + reason: "expected RAM like 30%, 512m or 4g" + }) + } + return Either.right(candidate) +} + +export const withDefaultResourceLimitIntent = ( + template: TemplateConfig +): TemplateConfig => ({ + ...template, + cpuLimit: template.cpuLimit ?? defaultCpuLimit, + ramLimit: template.ramLimit ?? defaultRamLimit +}) + +const resolvePercentCpuLimit = (percent: number, cpuCount: number): number => + Math.max( + minimumResolvedCpuLimit, + normalizePrecision((Math.max(1, cpuCount) * percent) / 100) + ) + +const resolvePercentRamLimit = (percent: number, totalMemoryBytes: number): string => { + const totalMib = Math.max(minimumResolvedRamLimitMib, Math.floor(totalMemoryBytes / mebibyte)) + const targetMib = Math.max(minimumResolvedRamLimitMib, Math.floor((totalMib * percent) / 100)) + return `${targetMib}m` +} + +export const resolveComposeResourceLimits = ( + template: Pick, + hostResources: HostResources +): ResolvedComposeResourceLimits => { + const cpuLimitIntent = template.cpuLimit ?? defaultCpuLimit + const ramLimitIntent = template.ramLimit ?? defaultRamLimit + const cpuPercent = parsePercent(cpuLimitIntent) + const ramPercent = parsePercent(ramLimitIntent) + + return { + cpuLimit: cpuPercent === null + ? Number(cpuLimitIntent) + : resolvePercentCpuLimit(cpuPercent, hostResources.cpuCount), + ramLimit: ramPercent === null + ? ramLimitIntent + : resolvePercentRamLimit(ramPercent, hostResources.totalMemoryBytes) + } +} +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/core/session-gist-domain.ts b/packages/app/src/docker-git/frontend-lib/core/session-gist-domain.ts new file mode 100644 index 00000000..e3f29856 --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/core/session-gist-domain.ts @@ -0,0 +1,38 @@ +/* jscpd:ignore-start */ +// CHANGE: session backup commands for PR-based session history +// WHY: enables returning to old AI sessions via a private backup repository +// QUOTE(ТЗ): "иметь возможность возвращаться ко всем старым сессиям с агентами" +// REF: issue-143 +// PURITY: CORE + +export interface SessionGistBackupCommand { + readonly _tag: "SessionGistBackup" + readonly projectDir: string + readonly prNumber: number | null + readonly repo: string | null + readonly postComment: boolean +} + +export interface SessionGistListCommand { + readonly _tag: "SessionGistList" + readonly limit: number + readonly repo: string | null +} + +export interface SessionGistViewCommand { + readonly _tag: "SessionGistView" + readonly snapshotRef: string +} + +export interface SessionGistDownloadCommand { + readonly _tag: "SessionGistDownload" + readonly snapshotRef: string + readonly outputDir: string +} + +export type SessionGistCommand = + | SessionGistBackupCommand + | SessionGistListCommand + | SessionGistViewCommand + | SessionGistDownloadCommand +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/core/sessions-domain.ts b/packages/app/src/docker-git/frontend-lib/core/sessions-domain.ts new file mode 100644 index 00000000..1abead3c --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/core/sessions-domain.ts @@ -0,0 +1,28 @@ +/* jscpd:ignore-start */ +import type { SessionGistCommand } from "./session-gist-domain.js" + +export interface SessionsListCommand { + readonly _tag: "SessionsList" + readonly projectDir: string + readonly includeDefault: boolean +} + +export interface SessionsKillCommand { + readonly _tag: "SessionsKill" + readonly projectDir: string + readonly pid: number +} + +export interface SessionsLogsCommand { + readonly _tag: "SessionsLogs" + readonly projectDir: string + readonly pid: number + readonly lines: number +} + +export type SessionsCommand = + | SessionsListCommand + | SessionsKillCommand + | SessionsLogsCommand + | SessionGistCommand +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/core/state-domain.ts b/packages/app/src/docker-git/frontend-lib/core/state-domain.ts new file mode 100644 index 00000000..0cbd356d --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/core/state-domain.ts @@ -0,0 +1,42 @@ +/* jscpd:ignore-start */ +export interface StatePathCommand { + readonly _tag: "StatePath" +} + +export interface StateInitCommand { + readonly _tag: "StateInit" + readonly repoUrl: string + readonly repoRef: string +} + +export interface StatePullCommand { + readonly _tag: "StatePull" +} + +export interface StatePushCommand { + readonly _tag: "StatePush" +} + +export interface StateStatusCommand { + readonly _tag: "StateStatus" +} + +export interface StateCommitCommand { + readonly _tag: "StateCommit" + readonly message: string +} + +export interface StateSyncCommand { + readonly _tag: "StateSync" + readonly message: string | null +} + +export type StateCommand = + | StatePathCommand + | StateInitCommand + | StatePullCommand + | StatePushCommand + | StateStatusCommand + | StateCommitCommand + | StateSyncCommand +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/core/strings.ts b/packages/app/src/docker-git/frontend-lib/core/strings.ts new file mode 100644 index 00000000..e51a5dc1 --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/core/strings.ts @@ -0,0 +1,17 @@ +/* jscpd:ignore-start */ +export const trimLeftChar = (value: string, char: string): string => { + let start = 0 + while (start < value.length && value[start] === char) { + start += 1 + } + return value.slice(start) +} + +export const trimRightChar = (value: string, char: string): string => { + let end = value.length + while (end > 0 && value[end - 1] === char) { + end -= 1 + } + return value.slice(0, end) +} +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/core/template-defaults.ts b/packages/app/src/docker-git/frontend-lib/core/template-defaults.ts new file mode 100644 index 00000000..8743d8ff --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/core/template-defaults.ts @@ -0,0 +1,66 @@ +/* jscpd:ignore-start */ +import type { TemplateConfig } from "./domain.js" + +type DefaultTemplateConfig = Pick< + TemplateConfig, + | "containerName" + | "serviceName" + | "sshUser" + | "sshPort" + | "repoRef" + | "targetDir" + | "volumeName" + | "skipGithubAuth" + | "dockerGitPath" + | "authorizedKeysPath" + | "envGlobalPath" + | "envProjectPath" + | "codexAuthPath" + | "codexSharedAuthPath" + | "codexHome" + | "geminiAuthPath" + | "geminiHome" + | "cpuLimit" + | "ramLimit" + | "dockerNetworkMode" + | "dockerSharedNetworkName" + | "enableMcpPlaywright" + | "bunVersion" +> + +export const defaultDockerNetworkMode: TemplateConfig["dockerNetworkMode"] = "shared" + +export const defaultDockerSharedNetworkName = "docker-git-shared" +export const dockerGitSharedCacheVolumeName = "docker-git-shared-cache" +export const dockerGitSharedCodexVolumeName = "docker-git-shared-codex" + +export const defaultCpuLimit = "30%" + +export const defaultRamLimit = "30%" + +export const defaultTemplateConfig = { + containerName: "dev-ssh", + serviceName: "dev", + sshUser: "dev", + sshPort: 2222, + repoRef: "main", + targetDir: "/home/dev/app", + volumeName: "dev_home", + skipGithubAuth: false, + dockerGitPath: "./.docker-git", + authorizedKeysPath: "./.docker-git/authorized_keys", + envGlobalPath: "./.docker-git/.orch/env/global.env", + envProjectPath: "./.orch/env/project.env", + codexAuthPath: "./.docker-git/.orch/auth/codex", + codexSharedAuthPath: "./.docker-git/.orch/auth/codex", + codexHome: "/home/dev/.codex", + geminiAuthPath: "./.docker-git/.orch/auth/gemini", + geminiHome: "/home/dev/.gemini", + cpuLimit: defaultCpuLimit, + ramLimit: defaultRamLimit, + dockerNetworkMode: defaultDockerNetworkMode, + dockerSharedNetworkName: defaultDockerSharedNetworkName, + enableMcpPlaywright: false, + bunVersion: "1.3.11" +} satisfies DefaultTemplateConfig +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/core/token-labels.ts b/packages/app/src/docker-git/frontend-lib/core/token-labels.ts new file mode 100644 index 00000000..20afa22d --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/core/token-labels.ts @@ -0,0 +1,53 @@ +/* jscpd:ignore-start */ +import { trimLeftChar, trimRightChar } from "./strings.js" + +const trimEdgeUnderscores = (value: string): string => { + let start = 0 + while (start < value.length && value[start] === "_") { + start += 1 + } + + let end = value.length + while (end > start && value[end - 1] === "_") { + end -= 1 + } + return value.slice(start, end) +} + +const trimEdgeHyphens = (value: string): string => { + const withoutLeading = trimLeftChar(value, "-") + return trimRightChar(withoutLeading, "-") +} + +export const normalizeGitTokenLabel = (value: string | undefined): string | undefined => { + const trimmed = value?.trim() ?? "" + if (trimmed.length === 0) { + return undefined + } + + const normalized = trimmed + .toUpperCase() + .replaceAll(/[^A-Z0-9]+/g, "_") + const cleaned = trimEdgeUnderscores(normalized) + if (cleaned.length === 0 || cleaned === "DEFAULT") { + return undefined + } + return cleaned +} + +export const normalizeAuthLabel = (value: string | undefined): string | undefined => { + const trimmed = value?.trim() ?? "" + if (trimmed.length === 0) { + return undefined + } + + const normalized = trimmed + .toLowerCase() + .replaceAll(/[^a-z0-9]+/g, "-") + const cleaned = trimEdgeHyphens(normalized) + if (cleaned.length === 0 || cleaned === "default") { + return undefined + } + return cleaned +} +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/shell/clone.ts b/packages/app/src/docker-git/frontend-lib/shell/clone.ts new file mode 100644 index 00000000..aab0c7a6 --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/shell/clone.ts @@ -0,0 +1,95 @@ +/* jscpd:ignore-start */ +import type * as CommandExecutor from "@effect/platform/CommandExecutor" +import { ExitCode } from "@effect/platform/CommandExecutor" +import type { PlatformError } from "@effect/platform/Error" +import * as Path from "@effect/platform/Path" +import { Effect } from "effect" + +import { type CloneRequest, resolveCloneRequest } from "../core/clone.js" +import { runCommandWithExitCodes } from "./command-runner.js" +import { CommandFailedError } from "./errors.js" + +const successExitCode = Number(ExitCode(0)) + +// CHANGE: read shortcut requests from process argv and npm lifecycle metadata +// WHY: allow bun run clone/open to work without "--" +// QUOTE(ТЗ): "Добавить команду open. ... Просто открывает существующий по ссылке" +// REF: user-request-2026-01-27 +// SOURCE: n/a +// FORMAT THEOREM: forall env: read(env) -> deterministic(request) +// PURITY: SHELL +// EFFECT: Effect +// INVARIANT: only argv/env are read +// COMPLEXITY: O(n) +export const readCloneRequest: Effect.Effect = Effect.sync(() => + resolveCloneRequest(process.argv.slice(2), process.env["npm_lifecycle_event"]) +) + +const runDockerGitCommand = ( + commandName: "clone" | "open", + args: ReadonlyArray +): Effect.Effect< + void, + CommandFailedError | PlatformError, + CommandExecutor.CommandExecutor | Path.Path +> => + Effect.gen(function*(_) { + const path = yield* _(Path.Path) + const workspaceRoot = process.cwd() + const appRoot = path.join(workspaceRoot, "packages", "app") + const dockerGitCli = path.join(appRoot, "dist", "src", "docker-git", "main.js") + const buildLabel = `bun run --cwd ${appRoot} build:docker-git` + const runLabel = `bun ${dockerGitCli} ${commandName}` + + yield* _( + runCommandWithExitCodes( + { cwd: workspaceRoot, command: "bun", args: ["run", "--cwd", appRoot, "build:docker-git"] }, + [successExitCode], + (exitCode) => new CommandFailedError({ command: buildLabel, exitCode }) + ) + ) + yield* _( + runCommandWithExitCodes( + { cwd: workspaceRoot, command: "bun", args: [dockerGitCli, commandName, ...args] }, + [successExitCode], + (exitCode) => new CommandFailedError({ command: runLabel, exitCode }) + ) + ) + }) + +// CHANGE: run docker-git clone by building and invoking its CLI +// WHY: reuse docker-git without mutating its codebase +// QUOTE(ТЗ): "docker git мы никак не изменяем" +// REF: user-request-2026-01-27 +// SOURCE: n/a +// FORMAT THEOREM: forall args: build && run(args) -> docker_git_invoked(args) +// PURITY: SHELL +// EFFECT: Effect +// INVARIANT: build runs before clone command +// COMPLEXITY: O(build + clone) +export const runDockerGitClone = ( + args: ReadonlyArray +): Effect.Effect< + void, + CommandFailedError | PlatformError, + CommandExecutor.CommandExecutor | Path.Path +> => runDockerGitCommand("clone", args) + +// CHANGE: run docker-git open by building and invoking its CLI +// WHY: mirror clone shortcut behavior for opening an existing repo workspace +// QUOTE(ТЗ): "Добавить команду open. ... Просто открывает существующий по ссылке" +// REF: user-request-2026-02-20-open-command +// SOURCE: n/a +// FORMAT THEOREM: forall args: build && run(args) -> docker_git_open_invoked(args) +// PURITY: SHELL +// EFFECT: Effect +// INVARIANT: build runs before open command +// COMPLEXITY: O(build + open) +export const runDockerGitOpen = ( + args: ReadonlyArray +): Effect.Effect< + void, + CommandFailedError | PlatformError, + CommandExecutor.CommandExecutor | Path.Path +> => runDockerGitCommand("open", args) +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/shell/command-runner.ts b/packages/app/src/docker-git/frontend-lib/shell/command-runner.ts new file mode 100644 index 00000000..596d70f0 --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/shell/command-runner.ts @@ -0,0 +1,146 @@ +/* jscpd:ignore-start */ +import * as Command from "@effect/platform/Command" +import * as CommandExecutor from "@effect/platform/CommandExecutor" +import type { PlatformError } from "@effect/platform/Error" +import { Effect, pipe } from "effect" +import * as Chunk from "effect/Chunk" +import * as Stream from "effect/Stream" + +type RunCommandSpec = { + readonly cwd: string + readonly command: string + readonly args: ReadonlyArray + readonly env?: Readonly> +} + +const buildCommand = ( + spec: RunCommandSpec, + stdout: "inherit" | "pipe", + stderr: "inherit" | "pipe", + stdin: Command.CommandInput = "pipe" +) => + pipe( + Command.make(spec.command, ...spec.args), + Command.workingDirectory(spec.cwd), + spec.env ? Command.env(spec.env) : (value) => value, + Command.stdin(stdin), + Command.stdout(stdout), + Command.stderr(stderr) + ) + +const ensureExitCode = ( + exitCode: number, + okExitCodes: ReadonlyArray, + onFailure: (exitCode: number) => E +): Effect.Effect => + okExitCodes.includes(exitCode) + ? Effect.succeed(exitCode) + : Effect.fail(onFailure(exitCode)) + +export const runCommandWithExitCodes = ( + spec: RunCommandSpec, + okExitCodes: ReadonlyArray, + onFailure: (exitCode: number) => E +): Effect.Effect => + Effect.gen(function*(_) { + const exitCode = yield* _(Command.exitCode(buildCommand(spec, "inherit", "inherit", "inherit"))) + yield* _(ensureExitCode(exitCode, okExitCodes, onFailure)) + }) + +// CHANGE: run a command and return the exit code, draining stdout/stderr to prevent buffer deadlock +// WHY: piped stdout/stderr fill the OS buffer (~64 KB) causing the child process to hang indefinitely +// QUOTE(ТЗ): "система авторизации" +// REF: user-request-2026-01-28-auth +// SOURCE: n/a +// FORMAT THEOREM: forall cmd: exitCode(cmd) = n +// PURITY: SHELL +// EFFECT: Effect +// INVARIANT: stdout/stderr are drained asynchronously so the child process never blocks +// COMPLEXITY: O(command) +export const runCommandExitCode = ( + spec: RunCommandSpec +): Effect.Effect => + Effect.scoped( + Effect.gen(function*(_) { + const executor = yield* _(CommandExecutor.CommandExecutor) + const process = yield* _(executor.start(buildCommand(spec, "pipe", "pipe", "pipe"))) + yield* _(Effect.forkDaemon(Stream.runDrain(process.stdout))) + yield* _(Effect.forkDaemon(Stream.runDrain(process.stderr))) + const exitCode = yield* _(process.exitCode) + return exitCode + }) + ) + +const collectUint8Array = (chunks: Chunk.Chunk): Uint8Array => + Chunk.reduce(chunks, new Uint8Array(), (acc, curr) => { + const next = new Uint8Array(acc.length + curr.length) + next.set(acc) + next.set(curr, acc.length) + return next + }) + +const decodeUint8Array = (bytes: Uint8Array): string => new TextDecoder("utf-8").decode(bytes) + +const collectStreamText = ( + stream: Stream.Stream +): Effect.Effect => + pipe(stream, Stream.runCollect, Effect.map((chunks) => decodeUint8Array(collectUint8Array(chunks)))) + +const combineCommandOutput = (stdout: string, stderr: string): string => + [stdout.trim(), stderr.trim()].filter((chunk) => chunk.length > 0).join("\n") + +// CHANGE: run a command and capture stdout, draining stderr to prevent buffer deadlock +// WHY: if stderr fills the OS buffer (~64 KB) the child process hangs; drain it asynchronously +// QUOTE(ТЗ): "система авторизации" +// REF: user-request-2026-01-28-auth +// SOURCE: n/a +// FORMAT THEOREM: forall cmd: capture(cmd) -> stdout(cmd) +// PURITY: SHELL +// EFFECT: Effect +// INVARIANT: stderr is drained asynchronously; stdout is fully collected before returning +// COMPLEXITY: O(command) +export const runCommandCapture = ( + spec: RunCommandSpec, + okExitCodes: ReadonlyArray, + onFailure: (exitCode: number) => E +): Effect.Effect => + Effect.scoped( + Effect.gen(function*(_) { + const executor = yield* _(CommandExecutor.CommandExecutor) + const process = yield* _(executor.start(buildCommand(spec, "pipe", "pipe", "pipe"))) + yield* _(Effect.forkDaemon(Stream.runDrain(process.stderr))) + const bytes = yield* _( + pipe(process.stdout, Stream.runCollect, Effect.map((chunks) => collectUint8Array(chunks))) + ) + const exitCode = yield* _(process.exitCode) + yield* _(ensureExitCode(exitCode, okExitCodes, onFailure)) + return decodeUint8Array(bytes) + }) + ) + +export const runCommandWithCapturedOutput = ( + spec: RunCommandSpec, + okExitCodes: ReadonlyArray, + onFailure: (exitCode: number, output: string) => E +): Effect.Effect => + Effect.scoped( + Effect.gen(function*(_) { + const executor = yield* _(CommandExecutor.CommandExecutor) + const process = yield* _(executor.start(buildCommand(spec, "pipe", "pipe", "pipe"))) + const [stdout, stderr] = yield* _( + Effect.all( + [ + collectStreamText(process.stdout), + collectStreamText(process.stderr) + ], + { concurrency: "unbounded" } + ) + ) + const exitCode = yield* _(process.exitCode) + const output = combineCommandOutput(stdout, stderr) + yield* _( + ensureExitCode(exitCode, okExitCodes, (numericExitCode) => onFailure(numericExitCode, output)) + ) + }) + ) +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/shell/errors.ts b/packages/app/src/docker-git/frontend-lib/shell/errors.ts new file mode 100644 index 00000000..bbd01194 --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/shell/errors.ts @@ -0,0 +1,101 @@ +/* jscpd:ignore-start */ +import { Data } from "effect" + +export class FileExistsError extends Data.TaggedError("FileExistsError")<{ + readonly path: string +}> {} + +export class ConfigNotFoundError extends Data.TaggedError("ConfigNotFoundError")<{ + readonly path: string +}> {} + +export class ConfigDecodeError extends Data.TaggedError("ConfigDecodeError")<{ + readonly path: string + readonly message: string +}> {} + +export class InputCancelledError extends Data.TaggedError("InputCancelledError")< + Record +> {} + +export class InputReadError extends Data.TaggedError("InputReadError")<{ + readonly message: string +}> {} + +export class DockerCommandError extends Data.TaggedError("DockerCommandError")<{ + readonly exitCode: number + readonly details?: string +}> {} + +export type DockerIdentityConflictKind = + | "containerName" + | "browserContainerName" + | "serviceName" + | "volumeName" + | "browserVolumeName" + | "bootstrapVolumeName" + +export type DockerIdentityConflict = { + readonly conflictingProjectDir: string + readonly kind: DockerIdentityConflictKind + readonly name: string +} + +export class DockerIdentityConflictError extends Data.TaggedError("DockerIdentityConflictError")<{ + readonly projectDir: string + readonly conflicts: ReadonlyArray +}> {} + +export type DockerAccessIssue = "PermissionDenied" | "DaemonUnavailable" + +export class DockerAccessError extends Data.TaggedError("DockerAccessError")<{ + readonly issue: DockerAccessIssue + readonly details: string +}> {} + +export class CloneFailedError extends Data.TaggedError("CloneFailedError")<{ + readonly repoUrl: string + readonly repoRef: string + readonly targetDir: string +}> {} + +export class AgentFailedError extends Data.TaggedError("AgentFailedError")<{ + readonly agentMode: string + readonly targetDir: string +}> {} + +export class PortProbeError extends Data.TaggedError("PortProbeError")<{ + readonly port: number + readonly message: string +}> {} + +export class CommandFailedError extends Data.TaggedError("CommandFailedError")<{ + readonly command: string + readonly exitCode: number +}> {} + +export class AuthError extends Data.TaggedError("AuthError")<{ + readonly message: string +}> {} + +export class ScrapArchiveNotFoundError extends Data.TaggedError("ScrapArchiveNotFoundError")<{ + readonly path: string +}> {} + +export class ScrapArchiveInvalidError extends Data.TaggedError("ScrapArchiveInvalidError")<{ + readonly path: string + readonly message: string +}> {} + +export class ScrapTargetDirUnsupportedError extends Data.TaggedError("ScrapTargetDirUnsupportedError")<{ + readonly sshUser: string + readonly targetDir: string + readonly reason: string +}> {} + +export class ScrapWipeRefusedError extends Data.TaggedError("ScrapWipeRefusedError")<{ + readonly sshUser: string + readonly targetDir: string + readonly reason: string +}> {} +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/usecases/menu-helpers.ts b/packages/app/src/docker-git/frontend-lib/usecases/menu-helpers.ts new file mode 100644 index 00000000..820706bc --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/usecases/menu-helpers.ts @@ -0,0 +1,52 @@ +/* jscpd:ignore-start */ +import type { ProjectConfig } from "../core/domain.js" + +export { defaultProjectsRoot, findSshPrivateKey, resolveAuthorizedKeysPath } from "./path-helpers.js" + +export const isRepoUrlInput = (input: string): boolean => { + const trimmed = input.trim().toLowerCase() + return trimmed.startsWith("http://") || + trimmed.startsWith("https://") || + trimmed.startsWith("ssh://") || + trimmed.startsWith("git@") +} + +type ConnectionInfoOptions = { + readonly authorizedKeysPath: string + readonly authorizedKeysExists: boolean + readonly sshCommand: string + readonly editorAccessDetails?: string +} + +export const formatConnectionInfo = ( + cwd: string, + config: ProjectConfig, + options: ConnectionInfoOptions +): string => { + const hostnameLabel = config.template.clonedOnHostname === undefined + ? "" + : `\nCloned on device: ${config.template.clonedOnHostname}` + const editorAccessLabel = options.editorAccessDetails === undefined ? "" : `\n${options.editorAccessDetails}` + return `Project directory: ${cwd} +` + + `Container: ${config.template.containerName} +` + + `Service: ${config.template.serviceName} +` + + `SSH command: ${options.sshCommand} +` + + `Repo: ${config.template.repoUrl} (${config.template.repoRef}) +` + + `Workspace: ${config.template.targetDir} +` + + `Authorized keys: ${options.authorizedKeysPath}${options.authorizedKeysExists ? "" : " (missing)"} +` + + `Env global: ${config.template.envGlobalPath} +` + + `Env project: ${config.template.envProjectPath} +` + + `Codex auth: ${config.template.codexAuthPath} -> ${config.template.codexHome}` + + editorAccessLabel + + hostnameLabel +} +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/usecases/path-helpers.ts b/packages/app/src/docker-git/frontend-lib/usecases/path-helpers.ts new file mode 100644 index 00000000..6f1cbf7e --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/usecases/path-helpers.ts @@ -0,0 +1,216 @@ +/* jscpd:ignore-start */ +import type { PlatformError } from "@effect/platform/Error" +import type * as FileSystem from "@effect/platform/FileSystem" +import type * as Path from "@effect/platform/Path" +import { Effect } from "effect" + +export const resolveAuthorizedKeysPath = ( + path: Path.Path, + baseDir: string, + authorizedKeysPath: string +): string => + path.isAbsolute(authorizedKeysPath) + ? authorizedKeysPath + : path.resolve(baseDir, authorizedKeysPath) + +const resolveHomeDir = (): string | null => { + const raw = process.env["HOME"] ?? process.env["USERPROFILE"] + const home = raw?.trim() ?? "" + return home.length > 0 ? home : null +} + +const expandHome = (value: string, home: string | null): string => { + if (home === null) { + return value + } + if (value === "~") { + return home + } + if (value.startsWith("~/") || value.startsWith("~\\")) { + return `${home}${value.slice(1)}` + } + return value +} + +const trimTrailingSlash = (value: string): string => { + let end = value.length + while (end > 0) { + const char = value[end - 1] + if (char !== "/" && char !== "\\") { + break + } + end -= 1 + } + return value.slice(0, end) +} + +export const defaultProjectsRoot = (cwd: string): string => { + const home = resolveHomeDir() + const explicit = process.env["DOCKER_GIT_PROJECTS_ROOT"]?.trim() + if (explicit && explicit.length > 0) { + return expandHome(explicit, home) + } + if (home !== null) { + return `${trimTrailingSlash(home)}/.docker-git` + } + return `${cwd}/.docker-git` +} + +const normalizeRelativePath = (value: string): string => + value + .replaceAll("\\", "/") + .replace(/^\.\//, "") + .trim() + +export const resolvePathFromCwd = ( + path: Path.Path, + cwd: string, + targetPath: string +): string => + path.isAbsolute(targetPath) + ? targetPath + : (() => { + const projectsRoot = path.resolve(defaultProjectsRoot(cwd)) + const normalized = normalizeRelativePath(targetPath) + if (normalized === ".docker-git") { + return projectsRoot + } + const prefix = ".docker-git/" + if (normalized.startsWith(prefix)) { + return path.join(projectsRoot, normalized.slice(prefix.length)) + } + return path.resolve(cwd, targetPath) + })() + +export const findExistingUpwards = ( + fs: FileSystem.FileSystem, + path: Path.Path, + startDir: string, + fileName: string, + maxDepth: number +): Effect.Effect => + Effect.gen(function*(_) { + let current = startDir + + for (let depth = 0; depth <= maxDepth; depth += 1) { + const candidate = path.join(current, fileName) + const exists = yield* _(fs.exists(candidate)) + if (exists) { + return candidate + } + + const parent = path.dirname(current) + if (parent === current) { + return null + } + + current = parent + } + + return null + }) + +export const resolveEnvPath = (key: string): string | null => { + const value = process.env[key]?.trim() + return value && value.length > 0 ? value : null +} + +export const findExistingPath = ( + fs: FileSystem.FileSystem, + candidate: string | null +): Effect.Effect => + candidate === null + ? Effect.succeed(null) + : Effect.flatMap(fs.exists(candidate), (exists) => (exists ? Effect.succeed(candidate) : Effect.succeed(null))) + +export const findFirstExisting = ( + fs: FileSystem.FileSystem, + candidates: ReadonlyArray +): Effect.Effect => + Effect.gen(function*(_) { + for (const candidate of candidates) { + const existing = yield* _(findExistingPath(fs, candidate)) + if (existing !== null) { + return existing + } + } + + return null + }) + +export type KeyLookupSpec = { + readonly envVar: string + readonly devKeyName: string + readonly fallbackName?: string + readonly homeCandidates: ReadonlyArray +} + +export const findKeyByPriority = ( + fs: FileSystem.FileSystem, + path: Path.Path, + cwd: string, + spec: KeyLookupSpec +): Effect.Effect => + Effect.gen(function*(_) { + const envPath = resolveEnvPath(spec.envVar) + const envExisting = yield* _(findExistingPath(fs, envPath)) + if (envExisting !== null) { + return envExisting + } + + const devKey = yield* _(findExistingUpwards(fs, path, cwd, spec.devKeyName, 6)) + if (devKey !== null) { + return devKey + } + + if (spec.fallbackName !== undefined) { + const fallback = yield* _(findExistingUpwards(fs, path, cwd, spec.fallbackName, 6)) + if (fallback !== null) { + return fallback + } + } + + const dockerGitHomeKey = path.join(defaultProjectsRoot(cwd), spec.devKeyName) + const dockerGitHomeExisting = yield* _(findExistingPath(fs, dockerGitHomeKey)) + if (dockerGitHomeExisting !== null) { + return dockerGitHomeExisting + } + + const home = resolveHomeDir() + if (home === null) { + return null + } + + return yield* _( + findFirstExisting( + fs, + spec.homeCandidates.map((candidate) => path.join(home, ".ssh", candidate)) + ) + ) + }) + +const authorizedKeysSpec: KeyLookupSpec = { + envVar: "DOCKER_GIT_AUTHORIZED_KEYS", + devKeyName: "dev_ssh_key.pub", + fallbackName: "authorized_keys", + homeCandidates: ["id_ed25519.pub", "id_rsa.pub"] +} + +const sshPrivateKeySpec: KeyLookupSpec = { + envVar: "DOCKER_GIT_SSH_KEY", + devKeyName: "dev_ssh_key", + homeCandidates: ["id_ed25519", "id_rsa"] +} + +const makeKeyFinder = (spec: KeyLookupSpec) => +( + fs: FileSystem.FileSystem, + path: Path.Path, + cwd: string +): Effect.Effect => + findKeyByPriority(fs, path, cwd, spec) + +export const findAuthorizedKeysSource = makeKeyFinder(authorizedKeysSpec) + +export const findSshPrivateKey = makeKeyFinder(sshPrivateKeySpec) +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/frontend-lib/usecases/scrap-path.ts b/packages/app/src/docker-git/frontend-lib/usecases/scrap-path.ts new file mode 100644 index 00000000..d2d947a8 --- /dev/null +++ b/packages/app/src/docker-git/frontend-lib/usecases/scrap-path.ts @@ -0,0 +1,72 @@ +/* jscpd:ignore-start */ +import { Either } from "effect" + +import { ScrapTargetDirUnsupportedError } from "../shell/errors.js" + +const normalizeContainerPath = (value: string): string => value.replaceAll("\\", "/").trim() + +export const expandContainerHome = (sshUser: string, value: string): string => { + if (value === "~") { + return `/home/${sshUser}` + } + if (value.startsWith("~/")) { + return `/home/${sshUser}${value.slice(1)}` + } + return value +} + +const trimTrailingPosixSlashes = (value: string): string => { + let end = value.length + while (end > 0 && value[end - 1] === "/") { + end -= 1 + } + return value.slice(0, end) +} + +const hasParentTraversalSegment = (value: string): boolean => value.split("/").includes("..") + +const unsupportedTargetDir = ( + sshUser: string, + targetDir: string, + reason: string +): ScrapTargetDirUnsupportedError => new ScrapTargetDirUnsupportedError({ sshUser, targetDir, reason }) + +export const deriveScrapWorkspaceRelativePath = ( + sshUser: string, + targetDir: string +): Either.Either => { + const normalizedTarget = trimTrailingPosixSlashes( + normalizeContainerPath(expandContainerHome(sshUser, targetDir)) + ) + const normalizedHome = trimTrailingPosixSlashes(`/home/${sshUser}`) + + if (hasParentTraversalSegment(normalizedTarget)) { + return Either.left(unsupportedTargetDir(sshUser, targetDir, "targetDir must not contain '..' path segments")) + } + + if (normalizedTarget === normalizedHome) { + return Either.right("") + } + + const prefix = `${normalizedHome}/` + if (!normalizedTarget.startsWith(prefix)) { + return Either.left(unsupportedTargetDir(sshUser, targetDir, `targetDir must be under ${normalizedHome}`)) + } + + const relative = normalizedTarget + .slice(prefix.length) + .split("/") + .filter((segment) => segment.length > 0 && segment !== ".") + .join("/") + + if (relative.length === 0) { + return Either.right("") + } + + if (hasParentTraversalSegment(relative)) { + return Either.left(unsupportedTargetDir(sshUser, targetDir, "targetDir must not contain '..' path segments")) + } + + return Either.right(relative) +} +/* jscpd:ignore-end */ diff --git a/packages/app/src/docker-git/gridland-bun.d.ts b/packages/app/src/docker-git/gridland-bun.d.ts new file mode 100644 index 00000000..6d4c51f0 --- /dev/null +++ b/packages/app/src/docker-git/gridland-bun.d.ts @@ -0,0 +1,93 @@ +declare module "@gridland/bun" { + import type { ComponentType, CSSProperties, ReactNode } from "react" + + type GridlandSize = number | string + type GridlandMaybe = A | undefined + + export type GridlandBoxProps = { + readonly alignItems?: GridlandMaybe + readonly backgroundColor?: GridlandMaybe + readonly border?: GridlandMaybe + readonly borderColor?: GridlandMaybe + readonly borderStyle?: GridlandMaybe<"rounded" | "single"> + readonly children?: ReactNode + readonly color?: GridlandMaybe + readonly flexDirection?: GridlandMaybe + readonly flexGrow?: GridlandMaybe + readonly flexWrap?: GridlandMaybe + readonly gap?: GridlandMaybe + readonly height?: GridlandMaybe + readonly justifyContent?: GridlandMaybe + readonly marginBottom?: GridlandMaybe + readonly marginLeft?: GridlandMaybe + readonly marginRight?: GridlandMaybe + readonly marginTop?: GridlandMaybe + readonly padding?: GridlandMaybe + readonly width?: GridlandMaybe + } + + export type GridlandKeyEvent = { + readonly ctrl?: boolean + readonly meta?: boolean + readonly name?: string + readonly raw?: string + readonly sequence?: string + readonly shift?: boolean + } + + export type GridlandRenderer = { + readonly destroy: () => void + readonly once: (event: string, listener: () => void) => void + readonly start: () => void + } + + export type GridlandRoot = { + readonly render: (node: ReactNode) => void + readonly unmount: () => void + } + + export type GridlandKeyboardOptions = { + readonly focusId?: GridlandMaybe + readonly global?: GridlandMaybe + readonly release?: GridlandMaybe + readonly selectedOnly?: GridlandMaybe + } + + export type GridlandRendererOptions = { + readonly exitOnCtrlC?: GridlandMaybe + readonly useConsole?: GridlandMaybe + readonly useMouse?: GridlandMaybe + } + + export type GridlandInputProps = { + readonly ariaLabel?: GridlandMaybe + readonly autoFocus?: GridlandMaybe + readonly placeholder?: GridlandMaybe + readonly value: string + } + + export type GridlandTextProps = GridlandBoxProps & { + readonly bold?: GridlandMaybe + readonly truncate?: GridlandMaybe + } + + export const Box: ComponentType + export const Input: ComponentType + export const Text: ComponentType + + export const createCliRenderer: (config?: GridlandRendererOptions) => PromiseLike + export const createRoot: (renderer: GridlandRenderer) => GridlandRoot + export const useKeyboard: ( + handler: (key: GridlandKeyEvent) => void, + options?: GridlandKeyboardOptions + ) => void + + export type GridlandModule = { + readonly Box: typeof Box + readonly Input: typeof Input + readonly Text: typeof Text + readonly createCliRenderer: typeof createCliRenderer + readonly createRoot: typeof createRoot + readonly useKeyboard: typeof useKeyboard + } +} diff --git a/packages/app/src/docker-git/host-errors.ts b/packages/app/src/docker-git/host-errors.ts index 1da7b78e..75fc03d2 100644 --- a/packages/app/src/docker-git/host-errors.ts +++ b/packages/app/src/docker-git/host-errors.ts @@ -1,7 +1,10 @@ -import type { ParseError } from "@lib/core/domain" -import type { AppError } from "@lib/usecases/errors" -import { renderError } from "@lib/usecases/errors" +import type { PlatformError } from "@effect/platform/Error" +import { Match } from "effect" + import { formatParseError } from "./cli/usage.js" +import type { ParseError } from "./frontend-lib/core/domain.js" +import type { CommandFailedError, InputReadError } from "./frontend-lib/shell/errors.js" +import type { TerminalSessionClientError } from "./terminal-session-client.js" export type ControllerBootstrapError = { readonly _tag: "ControllerBootstrapError" @@ -39,9 +42,13 @@ export type HostError = | ApiRequestError | ApiAuthRequiredError | ProjectResolutionError + | PlatformError + | CommandFailedError + | InputReadError + | TerminalSessionClientError | UnsupportedCommandError -export type CliError = AppError | HostError +export type CliError = ParseError | HostError const isParseError = (error: CliError): error is ParseError => error._tag === "UnknownCommand" || @@ -51,8 +58,6 @@ const isParseError = (error: CliError): error is ParseError => error._tag === "InvalidOption" || error._tag === "UnexpectedArgument" -const isApiRequestError = (error: CliError): error is ApiRequestError => "method" in error && "path" in error - const renderApiRequestError = (error: ApiRequestError): string => error.displayOnlyMessage === true ? error.message @@ -61,30 +66,20 @@ const renderApiRequestError = (error: ApiRequestError): string => error.message ].join("\n") +const renderHostCliError = (error: HostError): string => + Match.value(error).pipe( + Match.when({ _tag: "ControllerBootstrapError" }, ({ message }) => message), + Match.when({ _tag: "UnsupportedCommandError" }, ({ message }) => message), + Match.when({ _tag: "ProjectResolutionError" }, ({ message }) => message), + Match.when({ _tag: "TerminalSessionClientError" }, ({ message }) => message), + Match.when({ _tag: "ApiAuthRequiredError" }, ({ command, message }) => [message, `Run: ${command}`].join("\n")), + Match.when({ _tag: "ApiRequestError" }, renderApiRequestError), + Match.orElse((unknownError) => "message" in unknownError ? unknownError.message : String(unknownError)) + ) + export const renderCliError = (error: CliError): string => { if (isParseError(error)) { return formatParseError(error) } - - if (error._tag === "ControllerBootstrapError") { - return error.message - } - - if (error._tag === "ApiAuthRequiredError") { - return [error.message, `Run: ${error.command}`].join("\n") - } - - if (error._tag === "UnsupportedCommandError") { - return error.message - } - - if (error._tag === "ProjectResolutionError") { - return error.message - } - - if (isApiRequestError(error)) { - return renderApiRequestError(error) - } - - return renderError(error) + return renderHostCliError(error) } diff --git a/packages/app/src/docker-git/host-ssh.ts b/packages/app/src/docker-git/host-ssh.ts index 960df6b2..367793b7 100644 --- a/packages/app/src/docker-git/host-ssh.ts +++ b/packages/app/src/docker-git/host-ssh.ts @@ -1,45 +1,34 @@ import { Effect } from "effect" -import type { CreateCommand } from "@lib/core/domain" -import { connectProjectSsh, waitForProjectSshReady } from "@lib/usecases/projects" - +import { shouldAutoOpenSsh } from "../shared/auto-open-ssh.js" +import { createProjectTerminalSession } from "./api-client.js" import type { ApiProjectDetails } from "./api-project-codec.js" -import { resolveHostSshMaterial } from "./host-ssh-material.js" -import { resolveApiProjectItemWithSshKeyPath } from "./project-item.js" +import { projectItemFromApiDetails } from "./project-item.js" +import { attachTerminalSession } from "./terminal-session-client.js" -const isInteractiveTty = (): boolean => process.stdin.isTTY && process.stdout.isTTY +type AutoOpenSshCommand = { + readonly openSsh: boolean + readonly runUp: boolean +} type RenderableError = Error | { readonly message: string } const renderKnownError = (error: RenderableError): string => error.message -const shouldOpenSsh = (command: CreateCommand): boolean => command.openSsh - -const resolveProjectItem = ( - command: CreateCommand, - project: ApiProjectDetails -) => - Effect.gen(function*(_) { - const sshMaterial = yield* _(resolveHostSshMaterial(command)) - return yield* _(resolveApiProjectItemWithSshKeyPath(project, sshMaterial.privateKeyPath)) - }) +const shouldOpenSsh = (command: AutoOpenSshCommand): boolean => command.openSsh export const autoOpenProjectSsh = ( - command: CreateCommand, + command: AutoOpenSshCommand, project: ApiProjectDetails | null ) => Effect.gen(function*(_) { - if (!shouldOpenSsh(command)) { - return - } - - if (!command.runUp) { - yield* _(Effect.logWarning("Skipping SSH auto-open: docker compose up disabled (--no-up).")) - return - } - - if (!isInteractiveTty()) { - yield* _(Effect.logWarning("Skipping SSH auto-open: not running in an interactive TTY.")) + const autoOpenSsh = yield* _( + shouldAutoOpenSsh({ + shouldOpen: shouldOpenSsh(command), + runUp: command.runUp + }) + ) + if (!autoOpenSsh) { return } @@ -48,10 +37,21 @@ export const autoOpenProjectSsh = ( return } - const item = yield* _(resolveProjectItem(command, project)) - yield* _(Effect.log(`Opening SSH: ${item.sshCommand}`)) - yield* _(waitForProjectSshReady(item)) - yield* _(connectProjectSsh(item)) + const item = projectItemFromApiDetails(project) + const terminal = yield* _(createProjectTerminalSession(item.projectDir)) + if (terminal === null) { + yield* _(Effect.logWarning(`Skipping SSH auto-open: terminal session was not created for ${item.displayName}.`)) + return + } + yield* _( + attachTerminalSession({ + header: `SSH terminal: ${item.displayName}`, + session: terminal.session, + websocketPath: `/projects/${encodeURIComponent(item.projectDir)}/terminal-sessions/${ + encodeURIComponent(terminal.session.id) + }/ws` + }) + ) }).pipe( Effect.matchEffect({ onFailure: (error) => Effect.logWarning(`SSH auto-open failed: ${renderKnownError(error)}`), diff --git a/packages/app/src/docker-git/main.ts b/packages/app/src/docker-git/main.ts index 16895f77..f59f6434 100644 --- a/packages/app/src/docker-git/main.ts +++ b/packages/app/src/docker-git/main.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env bun import { NodeContext, NodeRuntime } from "@effect/platform-node" import { Effect, pipe } from "effect" diff --git a/packages/app/src/docker-git/menu-actions.ts b/packages/app/src/docker-git/menu-actions.ts index 0c989453..efd79da2 100644 --- a/packages/app/src/docker-git/menu-actions.ts +++ b/packages/app/src/docker-git/menu-actions.ts @@ -1,21 +1,21 @@ -import type { MenuAction } from "@lib/core/domain" import { Effect, Match, pipe } from "effect" import { downAllProjects, downProject, upProject } from "./api-client.js" import { listMenuProjectItems, listMenuRunningProjectItems, - renderGithubAuthStatusSummary, renderMenuProjectLogs, renderMenuProjectPs, renderMenuProjectSummaries } from "./menu-api.js" +import { openAuthMenu } from "./menu-auth.js" import { startCreateView } from "./menu-create.js" import type { MenuError } from "./menu-errors.js" import { renderMenuError } from "./menu-errors.js" +import { openProjectAuthSelection } from "./menu-project-auth.js" import { loadSelectView } from "./menu-select-load.js" import { withSuspendedTui, writeErrorAndPause } from "./menu-shared.js" -import { type MenuEnv, type MenuRunner, type MenuState, type MenuViewContext } from "./menu-types.js" +import { type MenuAction, type MenuEnv, type MenuRunner, type MenuState, type MenuViewContext } from "./menu-types.js" // CHANGE: keep menu actions and input parsing in a dedicated module // WHY: reduce cognitive complexity in the TUI entry @@ -98,23 +98,40 @@ const runSelectAction = (context: MenuContext) => { } const runAuthProfilesAction = (context: MenuContext) => { - context.runner.runEffect( - pipe( - renderGithubAuthStatusSummary(), - Effect.tap((summary) => - Effect.sync(() => { - context.setMessage( - `${summary} Use \`docker-git auth github login --web\` or \`docker-git auth github logout\`.` - ) - }) - ), - Effect.asVoid - ) - ) + openAuthMenu({ + state: context.state, + runner: context.runner, + setView: context.setView, + setMessage: context.setMessage, + setActiveDir: context.setActiveDir + }) } const runProjectAuthAction = (context: MenuContext) => { - context.setMessage("Project auth binding is not routed through the controller yet.") + if (context.state.activeDir !== null) { + context.runner.runEffect( + pipe( + listMenuProjectItems, + Effect.flatMap((items) => { + const selected = items.find((item) => item.projectDir === context.state.activeDir) + if (selected === undefined) { + return Effect.sync(() => { + context.setActiveDir(null) + context.setMessage("Active project is no longer available. Select a project again.") + context.runner.runEffect(loadSelectView(listMenuProjectItems, "Auth", context)) + }) + } + return Effect.sync(() => { + openProjectAuthSelection(selected, context) + }) + }) + ) + ) + return + } + + context.setMessage(null) + context.runner.runEffect(loadSelectView(listMenuProjectItems, "Auth", context)) } const runDownAllAction = (context: MenuContext) => { diff --git a/packages/app/src/docker-git/menu-api.ts b/packages/app/src/docker-git/menu-api.ts index 5f3fa889..d818dd25 100644 --- a/packages/app/src/docker-git/menu-api.ts +++ b/packages/app/src/docker-git/menu-api.ts @@ -1,8 +1,5 @@ import { Effect, pipe } from "effect" -import type { AuthGithubStatusCommand } from "@lib/core/domain" -import { connectProjectSsh, type ProjectItem, waitForProjectSshReady } from "@lib/usecases/projects" - import { deleteProject, downProject, @@ -11,18 +8,18 @@ import { listProjects, readProjectLogs, readProjectPs, - renderProjectSummaryLine, - upProject + renderProjectSummaryLine } from "./api-client.js" import { asObject, asString, type JsonValue } from "./api-json.js" +import type { AuthGithubStatusCommand } from "./frontend-lib/core/auth-domain.js" import type { MenuError } from "./menu-errors.js" import type { MenuEnv } from "./menu-types.js" -import { resolveApiProjectItem } from "./project-item.js" +import { type ProjectItem, resolveApiProjectItem } from "./project-item.js" -const menuGithubStatusCommand = { +const menuGithubStatusCommand: AuthGithubStatusCommand = { _tag: "AuthGithubStatus", envGlobalPath: "" -} satisfies AuthGithubStatusCommand +} const compact = (values: ReadonlyArray): ReadonlyArray => values.filter((value): value is A => value !== null) @@ -41,7 +38,7 @@ const listProjectDetails = ( (item) => pipe( getProject(item.id), - Effect.flatMap((project) => (project === null ? Effect.succeed(null) : resolveApiProjectItem(project))), + Effect.map((project) => (project === null ? null : resolveApiProjectItem(project))), Effect.match({ onFailure: () => null, onSuccess: (project) => project @@ -89,26 +86,6 @@ export const renderMenuProjectSummaries = () => }) ) -export const connectMenuProjectSshWithUp = ( - item: ProjectItem -) => - pipe( - upProject(item.projectDir), - Effect.zipRight(getProject(item.projectDir)), - Effect.flatMap((project) => { - const resolved = project === null ? Effect.succeed(item) : resolveApiProjectItem(project) - return pipe( - resolved, - Effect.flatMap((resolvedItem) => - pipe( - waitForProjectSshReady(resolvedItem), - Effect.zipRight(connectProjectSsh(resolvedItem)) - ) - ) - ) - }) - ) - export const deleteMenuProject = (item: ProjectItem) => deleteProject(item.projectDir) export const downMenuProject = (item: ProjectItem) => downProject(item.projectDir) diff --git a/packages/app/src/docker-git/menu-auth-data.ts b/packages/app/src/docker-git/menu-auth-data.ts index a12ff738..6c5e05b0 100644 --- a/packages/app/src/docker-git/menu-auth-data.ts +++ b/packages/app/src/docker-git/menu-auth-data.ts @@ -1,207 +1,48 @@ -import * as FileSystem from "@effect/platform/FileSystem" -import * as Path from "@effect/platform/Path" -import { Effect, Match, pipe } from "effect" - -import { ensureEnvFile, parseEnvEntries, readEnvText, upsertEnvKey } from "@lib/usecases/env-file" -import { type AppError } from "@lib/usecases/errors" -import { defaultProjectsRoot } from "@lib/usecases/menu-helpers" -import { autoSyncState } from "@lib/usecases/state-repo" - -import { countAuthAccountEntries } from "./menu-auth-snapshot-builder.js" -import { buildLabeledEnvKey, countKeyEntries, normalizeLabel } from "./menu-labeled-env.js" -import type { AuthFlow, AuthSnapshot, MenuEnv } from "./menu-types.js" - -export type AuthMenuAction = AuthFlow | "Refresh" | "Back" - -type AuthMenuItem = { - readonly action: AuthMenuAction - readonly label: string -} - -export type AuthEnvFlow = Extract - -export type AuthPromptStep = { - readonly key: "label" | "token" | "user" | "apiKey" - readonly label: string - readonly required: boolean - readonly secret: boolean -} - -const authMenuItems: ReadonlyArray = [ - { action: "GithubOauth", label: "GitHub: login via OAuth (web)" }, - { action: "GithubRemove", label: "GitHub: remove token" }, - { action: "GitSet", label: "Git: add/update credentials" }, - { action: "GitRemove", label: "Git: remove credentials" }, - { action: "ClaudeOauth", label: "Claude Code: login via OAuth (web)" }, - { action: "ClaudeLogout", label: "Claude Code: logout (clear cache)" }, - { action: "GeminiOauth", label: "Gemini CLI: login via OAuth (Google account)" }, - { action: "GeminiApiKey", label: "Gemini CLI: set API key" }, - { action: "GeminiLogout", label: "Gemini CLI: logout (clear credentials)" }, - { action: "Refresh", label: "Refresh snapshot" }, - { action: "Back", label: "Back to main menu" } -] - -const flowSteps: Readonly>> = { - GithubOauth: [ - { key: "label", label: "Label (empty = default)", required: false, secret: false } - ], - GithubRemove: [ - { key: "label", label: "Label to remove (empty = default)", required: false, secret: false } - ], - GitSet: [ - { key: "label", label: "Label (empty = default)", required: false, secret: false }, - { key: "token", label: "Git auth token", required: true, secret: true }, - { key: "user", label: "Git auth user (empty = x-access-token)", required: false, secret: false } - ], - GitRemove: [ - { key: "label", label: "Label to remove (empty = default)", required: false, secret: false } - ], - ClaudeOauth: [ - { key: "label", label: "Label (empty = default)", required: false, secret: false } - ], - ClaudeLogout: [ - { key: "label", label: "Label to logout (empty = default)", required: false, secret: false } - ], - GeminiOauth: [ - { key: "label", label: "Label (empty = default)", required: false, secret: false } - ], - GeminiApiKey: [ - { key: "label", label: "Label (empty = default)", required: false, secret: false }, - { key: "apiKey", label: "Gemini API key (from ai.google.dev)", required: true, secret: true } - ], - GeminiLogout: [ - { key: "label", label: "Label to logout (empty = default)", required: false, secret: false } - ] -} - -const flowTitle = (flow: AuthFlow): string => - Match.value(flow).pipe( - Match.when("GithubOauth", () => "GitHub OAuth"), - Match.when("GithubRemove", () => "GitHub remove"), - Match.when("GitSet", () => "Git credentials"), - Match.when("GitRemove", () => "Git remove"), - Match.when("ClaudeOauth", () => "Claude Code OAuth"), - Match.when("ClaudeLogout", () => "Claude Code logout"), - Match.when("GeminiOauth", () => "Gemini CLI OAuth"), - Match.when("GeminiApiKey", () => "Gemini CLI API key"), - Match.when("GeminiLogout", () => "Gemini CLI logout"), - Match.exhaustive - ) - -export const successMessage = (flow: AuthFlow, label: string): string => - Match.value(flow).pipe( - Match.when("GithubOauth", () => `Saved GitHub token (${label}).`), - Match.when("GithubRemove", () => `Removed GitHub token (${label}).`), - Match.when("GitSet", () => `Saved Git credentials (${label}).`), - Match.when("GitRemove", () => `Removed Git credentials (${label}).`), - Match.when("ClaudeOauth", () => `Saved Claude Code login (${label}).`), - Match.when("ClaudeLogout", () => `Logged out Claude Code (${label}).`), - Match.when("GeminiOauth", () => `Saved Gemini CLI OAuth login (${label}).`), - Match.when("GeminiApiKey", () => `Saved Gemini API key (${label}).`), - Match.when("GeminiLogout", () => `Logged out Gemini CLI (${label}).`), - Match.exhaustive - ) - -const buildGlobalEnvPath = (cwd: string): string => `${defaultProjectsRoot(cwd)}/.orch/env/global.env` -const buildClaudeAuthPath = (cwd: string): string => `${defaultProjectsRoot(cwd)}/.orch/auth/claude` -const buildGeminiAuthPath = (cwd: string): string => `${defaultProjectsRoot(cwd)}/.orch/auth/gemini` - -type AuthEnvText = { - readonly fs: FileSystem.FileSystem - readonly path: Path.Path - readonly globalEnvPath: string - readonly claudeAuthPath: string - readonly geminiAuthPath: string - readonly envText: string -} - -const loadAuthEnvText = ( - cwd: string -): Effect.Effect => - Effect.gen(function*(_) { - const fs = yield* _(FileSystem.FileSystem) - const path = yield* _(Path.Path) - const globalEnvPath = buildGlobalEnvPath(cwd) - const claudeAuthPath = buildClaudeAuthPath(cwd) - const geminiAuthPath = buildGeminiAuthPath(cwd) - yield* _(ensureEnvFile(fs, path, globalEnvPath)) - const envText = yield* _(readEnvText(fs, globalEnvPath)) - return { fs, path, globalEnvPath, claudeAuthPath, geminiAuthPath, envText } - }) +import { Effect } from "effect" + +import { normalizeOptionalText } from "../shared/optional-text.js" +import { loadAuthSnapshot, runAuthMenuFlow as submitAuthMenuFlow } from "./api-auth-menu-client.js" +import type { AuthEnvFlow } from "./menu-auth-shared.js" +import type { MenuError } from "./menu-errors.js" +import type { AuthSnapshot, MenuEnv } from "./menu-types.js" + +export { + authMenuActionByIndex, + authMenuLabels, + authMenuSize, + authViewSteps, + authViewTitle, + successMessage +} from "./menu-auth-shared.js" +export type { AuthEnvFlow, AuthMenuAction, AuthPromptStep } from "./menu-auth-shared.js" + +const decodeSnapshot = (snapshot: AuthSnapshot | null): Effect.Effect => + snapshot === null + ? Effect.fail({ + _tag: "ApiRequestError", + method: "GET", + path: "/auth/menu", + message: "Controller returned an invalid auth snapshot." + }) + : Effect.succeed(snapshot) export const readAuthSnapshot = ( - cwd: string -): Effect.Effect => - pipe( - loadAuthEnvText(cwd), - Effect.flatMap(({ claudeAuthPath, envText, fs, geminiAuthPath, globalEnvPath, path }) => - countAuthAccountEntries(fs, path, claudeAuthPath, geminiAuthPath).pipe( - Effect.map(({ claudeAuthEntries, geminiAuthEntries }) => ({ - globalEnvPath, - claudeAuthPath, - geminiAuthPath, - totalEntries: parseEnvEntries(envText).filter((entry) => entry.value.trim().length > 0).length, - githubTokenEntries: countKeyEntries(envText, "GITHUB_TOKEN"), - gitTokenEntries: countKeyEntries(envText, "GIT_AUTH_TOKEN"), - gitUserEntries: countKeyEntries(envText, "GIT_AUTH_USER"), - claudeAuthEntries, - geminiAuthEntries - })) - ) - ) - ) + _cwd: string +): Effect.Effect => + loadAuthSnapshot().pipe(Effect.flatMap((snapshot) => decodeSnapshot(snapshot))) export const writeAuthFlow = ( - cwd: string, + _cwd: string, flow: AuthEnvFlow, values: Readonly> -): Effect.Effect => - pipe( - loadAuthEnvText(cwd), - Effect.flatMap(({ envText, fs, globalEnvPath }) => { - const label = values["label"] ?? "" - const canonicalLabel = (() => { - const normalized = normalizeLabel(label) - return normalized.length === 0 || normalized === "DEFAULT" ? "default" : normalized - })() - const token = (values["token"] ?? "").trim() - const user = (values["user"] ?? "").trim() - const nextText = Match.value(flow).pipe( - Match.when("GithubRemove", () => upsertEnvKey(envText, buildLabeledEnvKey("GITHUB_TOKEN", label), "")), - Match.when("GitSet", () => { - const withToken = upsertEnvKey(envText, buildLabeledEnvKey("GIT_AUTH_TOKEN", label), token) - const resolvedUser = user.length > 0 ? user : "x-access-token" - return upsertEnvKey(withToken, buildLabeledEnvKey("GIT_AUTH_USER", label), resolvedUser) - }), - Match.when("GitRemove", () => { - const withoutToken = upsertEnvKey(envText, buildLabeledEnvKey("GIT_AUTH_TOKEN", label), "") - return upsertEnvKey(withoutToken, buildLabeledEnvKey("GIT_AUTH_USER", label), "") - }), - Match.exhaustive - ) - const syncMessage = Match.value(flow).pipe( - Match.when("GithubRemove", () => `chore(state): auth gh logout ${canonicalLabel}`), - Match.when("GitSet", () => `chore(state): auth git ${canonicalLabel}`), - Match.when("GitRemove", () => `chore(state): auth git logout ${canonicalLabel}`), - Match.exhaustive - ) - return pipe( - fs.writeFileString(globalEnvPath, nextText), - Effect.zipRight(autoSyncState(syncMessage)) - ) - }), +): Effect.Effect => + submitAuthMenuFlow({ + flow, + label: normalizeOptionalText(values["label"]), + token: normalizeOptionalText(values["token"]), + user: normalizeOptionalText(values["user"]), + apiKey: normalizeOptionalText(values["apiKey"]) + }).pipe( + Effect.flatMap((snapshot) => decodeSnapshot(snapshot)), Effect.asVoid ) - -export const authViewTitle = (flow: AuthFlow): string => flowTitle(flow) - -export const authViewSteps = (flow: AuthFlow): ReadonlyArray => flowSteps[flow] - -export const authMenuLabels = (): ReadonlyArray => authMenuItems.map((item) => item.label) - -export const authMenuActionByIndex = (index: number): AuthMenuAction | null => { - const item = authMenuItems[index] - return item ? item.action : null -} - -export const authMenuSize = (): number => authMenuItems.length diff --git a/packages/app/src/docker-git/menu-auth-effects.ts b/packages/app/src/docker-git/menu-auth-effects.ts index a4d34601..c52a0930 100644 --- a/packages/app/src/docker-git/menu-auth-effects.ts +++ b/packages/app/src/docker-git/menu-auth-effects.ts @@ -1,73 +1,69 @@ import { Effect, Match, pipe } from "effect" -import { - authClaudeLogin, - authClaudeLogout, - authGeminiLogin, - authGeminiLoginOauth, - authGeminiLogout, - authGithubLogin, - claudeAuthRoot -} from "@lib/usecases/auth" -import { geminiAuthRoot } from "@lib/usecases/auth-gemini-helpers" -import type { AppError } from "@lib/usecases/errors" -import { renderError } from "@lib/usecases/errors" - +import { createAuthTerminalSession, githubLogin } from "./api-client.js" import { readAuthSnapshot, successMessage, writeAuthFlow } from "./menu-auth-data.js" +import { type MenuError, renderMenuError } from "./menu-errors.js" import { pauseOnError, resumeSshWithSkipInputs, withSuspendedTui } from "./menu-shared.js" import type { AuthSnapshot, MenuEnv, MenuViewContext, ViewState } from "./menu-types.js" +import { attachTerminalSession } from "./terminal-session-client.js" type AuthPromptView = Extract type AuthEffectContext = MenuViewContext & { - readonly runner: { readonly runEffect: (effect: Effect.Effect) => void } + readonly runner: { readonly runEffect: (effect: Effect.Effect) => void } readonly setSshActive: (active: boolean) => void readonly setSkipInputs: (update: (value: number) => number) => void readonly cwd: string } +const missingAuthTerminalSessionError = (provider: "ClaudeOauth" | "GeminiOauth"): MenuError => ({ + _tag: "ApiRequestError", + method: "POST", + path: "/auth/terminal-sessions", + message: `Controller did not create a terminal session for ${provider}.` +}) + const resolveLabelOption = (values: Readonly>): string | null => { const labelValue = (values["label"] ?? "").trim() return labelValue.length > 0 ? labelValue : null } -const resolveGithubOauthEffect = (labelOption: string | null, globalEnvPath: string) => - authGithubLogin({ - _tag: "AuthGithubLogin", - label: labelOption, - token: null, - scopes: null, - envGlobalPath: globalEnvPath - }) - -const resolveClaudeOauthEffect = (labelOption: string | null) => - authClaudeLogin({ _tag: "AuthClaudeLogin", label: labelOption, claudeAuthPath: claudeAuthRoot }) - -const resolveClaudeLogoutEffect = (labelOption: string | null) => - authClaudeLogout({ _tag: "AuthClaudeLogout", label: labelOption, claudeAuthPath: claudeAuthRoot }) - -const resolveGeminiOauthEffect = (labelOption: string | null) => - authGeminiLoginOauth({ _tag: "AuthGeminiLogin", label: labelOption, geminiAuthPath: geminiAuthRoot, isWeb: false }) - -const resolveGeminiApiKeyEffect = (labelOption: string | null, apiKey: string) => - authGeminiLogin({ _tag: "AuthGeminiLogin", label: labelOption, geminiAuthPath: geminiAuthRoot, isWeb: false }, apiKey) - -const resolveGeminiLogoutEffect = (labelOption: string | null) => - authGeminiLogout({ _tag: "AuthGeminiLogout", label: labelOption, geminiAuthPath: geminiAuthRoot }) +const resolveTerminalAuthEffect = ( + provider: "ClaudeOauth" | "GeminiOauth", + labelOption: string | null +): Effect.Effect => + createAuthTerminalSession(provider, labelOption).pipe( + Effect.flatMap((session) => + session === null + ? Effect.fail(missingAuthTerminalSessionError(provider)) + : attachTerminalSession({ + header: provider === "ClaudeOauth" ? "Claude Code OAuth" : "Gemini CLI OAuth", + session, + websocketPath: `/auth/terminal-sessions/${encodeURIComponent(session.id)}/ws` + }) + ) + ) export const resolveAuthPromptEffect = ( view: AuthPromptView, cwd: string, values: Readonly> -): Effect.Effect => { +): Effect.Effect => { const labelOption = resolveLabelOption(values) return Match.value(view.flow).pipe( - Match.when("GithubOauth", () => resolveGithubOauthEffect(labelOption, view.snapshot.globalEnvPath)), - Match.when("ClaudeOauth", () => resolveClaudeOauthEffect(labelOption)), - Match.when("ClaudeLogout", () => resolveClaudeLogoutEffect(labelOption)), - Match.when("GeminiOauth", () => resolveGeminiOauthEffect(labelOption)), - Match.when("GeminiApiKey", () => resolveGeminiApiKeyEffect(labelOption, (values["apiKey"] ?? "").trim())), - Match.when("GeminiLogout", () => resolveGeminiLogoutEffect(labelOption)), + Match.when("GithubOauth", () => + githubLogin({ + _tag: "AuthGithubLogin", + label: labelOption, + token: null, + scopes: null, + envGlobalPath: view.snapshot.globalEnvPath + }).pipe(Effect.asVoid)), + Match.when("ClaudeOauth", () => resolveTerminalAuthEffect("ClaudeOauth", labelOption)), + Match.when("ClaudeLogout", (flow) => writeAuthFlow(cwd, flow, values)), + Match.when("GeminiOauth", () => resolveTerminalAuthEffect("GeminiOauth", labelOption)), + Match.when("GeminiApiKey", (flow) => writeAuthFlow(cwd, flow, values)), + Match.when("GeminiLogout", (flow) => writeAuthFlow(cwd, flow, values)), Match.when("GithubRemove", (flow) => writeAuthFlow(cwd, flow, values)), Match.when("GitSet", (flow) => writeAuthFlow(cwd, flow, values)), Match.when("GitRemove", (flow) => writeAuthFlow(cwd, flow, values)), @@ -84,7 +80,7 @@ export const startAuthMenuWithSnapshot = ( } export const runAuthPromptEffect = ( - effect: Effect.Effect, + effect: Effect.Effect, view: AuthPromptView, label: string, context: AuthEffectContext, @@ -92,7 +88,7 @@ export const runAuthPromptEffect = ( ): void => { const withOptionalSuspension = options.suspendTui ? withSuspendedTui(effect, { - onError: pauseOnError(renderError), + onError: pauseOnError(renderMenuError), onResume: resumeSshWithSkipInputs(context) }) : effect diff --git a/packages/app/src/docker-git/menu-auth-helpers.ts b/packages/app/src/docker-git/menu-auth-helpers.ts index 3e3a34be..cdd3a4bc 100644 --- a/packages/app/src/docker-git/menu-auth-helpers.ts +++ b/packages/app/src/docker-git/menu-auth-helpers.ts @@ -1,14 +1,13 @@ +import type { PlatformError } from "@effect/platform/Error" import type * as FileSystem from "@effect/platform/FileSystem" import type * as Path from "@effect/platform/Path" import { Effect } from "effect" -import type { AppError } from "@lib/usecases/errors" - export const countAuthAccountDirectories = ( fs: FileSystem.FileSystem, path: Path.Path, root: string -): Effect.Effect => +): Effect.Effect => Effect.gen(function*(_) { const exists = yield* _(fs.exists(root)) if (!exists) { diff --git a/packages/app/src/docker-git/menu-auth-shared.ts b/packages/app/src/docker-git/menu-auth-shared.ts new file mode 100644 index 00000000..7e185224 --- /dev/null +++ b/packages/app/src/docker-git/menu-auth-shared.ts @@ -0,0 +1,105 @@ +import { Match } from "effect" + +import type { AuthFlow } from "./menu-types.js" + +export type AuthMenuAction = AuthFlow | "Refresh" | "Back" + +export type AuthEnvFlow = Exclude + +export type AuthPromptStep = { + readonly key: "label" | "token" | "user" | "apiKey" + readonly label: string + readonly required: boolean + readonly secret: boolean +} + +type AuthMenuItem = { + readonly action: AuthMenuAction + readonly label: string +} + +const authMenuItems: ReadonlyArray = [ + { action: "GithubOauth", label: "GitHub: login via OAuth (web)" }, + { action: "GithubRemove", label: "GitHub: remove token" }, + { action: "GitSet", label: "Git: add/update credentials" }, + { action: "GitRemove", label: "Git: remove credentials" }, + { action: "ClaudeOauth", label: "Claude Code: login via OAuth (web)" }, + { action: "ClaudeLogout", label: "Claude Code: logout (clear cache)" }, + { action: "GeminiOauth", label: "Gemini CLI: login via OAuth (Google account)" }, + { action: "GeminiApiKey", label: "Gemini CLI: set API key" }, + { action: "GeminiLogout", label: "Gemini CLI: logout (clear credentials)" }, + { action: "Refresh", label: "Refresh snapshot" }, + { action: "Back", label: "Back to main menu" } +] + +const flowSteps: Readonly>> = { + GithubOauth: [ + { key: "label", label: "Label (empty = default)", required: false, secret: false } + ], + GithubRemove: [ + { key: "label", label: "Label to remove (empty = default)", required: false, secret: false } + ], + GitSet: [ + { key: "label", label: "Label (empty = default)", required: false, secret: false }, + { key: "token", label: "Git auth token", required: true, secret: true }, + { key: "user", label: "Git auth user (empty = x-access-token)", required: false, secret: false } + ], + GitRemove: [ + { key: "label", label: "Label to remove (empty = default)", required: false, secret: false } + ], + ClaudeOauth: [ + { key: "label", label: "Label (empty = default)", required: false, secret: false } + ], + ClaudeLogout: [ + { key: "label", label: "Label to logout (empty = default)", required: false, secret: false } + ], + GeminiOauth: [ + { key: "label", label: "Label (empty = default)", required: false, secret: false } + ], + GeminiApiKey: [ + { key: "label", label: "Label (empty = default)", required: false, secret: false }, + { key: "apiKey", label: "Gemini API key (from ai.google.dev)", required: true, secret: true } + ], + GeminiLogout: [ + { key: "label", label: "Label to logout (empty = default)", required: false, secret: false } + ] +} + +export const successMessage = (flow: AuthFlow, label: string): string => + Match.value(flow).pipe( + Match.when("GithubOauth", () => `Saved GitHub token (${label}).`), + Match.when("GithubRemove", () => `Removed GitHub token (${label}).`), + Match.when("GitSet", () => `Saved Git credentials (${label}).`), + Match.when("GitRemove", () => `Removed Git credentials (${label}).`), + Match.when("ClaudeOauth", () => `Saved Claude Code login (${label}).`), + Match.when("ClaudeLogout", () => `Logged out Claude Code (${label}).`), + Match.when("GeminiOauth", () => `Saved Gemini CLI OAuth login (${label}).`), + Match.when("GeminiApiKey", () => `Saved Gemini API key (${label}).`), + Match.when("GeminiLogout", () => `Logged out Gemini CLI (${label}).`), + Match.exhaustive + ) + +export const authViewTitle = (flow: AuthFlow): string => + Match.value(flow).pipe( + Match.when("GithubOauth", () => "GitHub OAuth"), + Match.when("GithubRemove", () => "GitHub remove"), + Match.when("GitSet", () => "Git credentials"), + Match.when("GitRemove", () => "Git remove"), + Match.when("ClaudeOauth", () => "Claude Code OAuth"), + Match.when("ClaudeLogout", () => "Claude Code logout"), + Match.when("GeminiOauth", () => "Gemini CLI OAuth"), + Match.when("GeminiApiKey", () => "Gemini CLI API key"), + Match.when("GeminiLogout", () => "Gemini CLI logout"), + Match.exhaustive + ) + +export const authViewSteps = (flow: AuthFlow): ReadonlyArray => flowSteps[flow] + +export const authMenuLabels = (): ReadonlyArray => authMenuItems.map((item) => item.label) + +export const authMenuActionByIndex = (index: number): AuthMenuAction | null => { + const item = authMenuItems[index] + return item ? item.action : null +} + +export const authMenuSize = (): number => authMenuItems.length diff --git a/packages/app/src/docker-git/menu-auth-snapshot-builder.ts b/packages/app/src/docker-git/menu-auth-snapshot-builder.ts index deee36d3..1f241b4c 100644 --- a/packages/app/src/docker-git/menu-auth-snapshot-builder.ts +++ b/packages/app/src/docker-git/menu-auth-snapshot-builder.ts @@ -1,8 +1,8 @@ +import type { PlatformError } from "@effect/platform/Error" import type * as FileSystem from "@effect/platform/FileSystem" import type * as Path from "@effect/platform/Path" import { Effect, pipe } from "effect" -import type { AppError } from "@lib/usecases/errors" import { countAuthAccountDirectories } from "./menu-auth-helpers.js" export type AuthAccountCounts = { @@ -15,7 +15,7 @@ export const countAuthAccountEntries = ( path: Path.Path, claudeAuthPath: string, geminiAuthPath: string -): Effect.Effect => +): Effect.Effect => pipe( Effect.all({ claudeAuthEntries: countAuthAccountDirectories(fs, path, claudeAuthPath), diff --git a/packages/app/src/docker-git/menu-auth.ts b/packages/app/src/docker-git/menu-auth.ts index d860d6a3..314969c0 100644 --- a/packages/app/src/docker-git/menu-auth.ts +++ b/packages/app/src/docker-git/menu-auth.ts @@ -1,7 +1,5 @@ import { Effect, pipe } from "effect" -import type { AppError } from "@lib/usecases/errors" - import { type AuthMenuAction, authMenuActionByIndex, @@ -11,6 +9,7 @@ import { } from "./menu-auth-data.js" import { resolveAuthPromptEffect, runAuthPromptEffect, startAuthMenuWithSnapshot } from "./menu-auth-effects.js" import { nextBufferValue } from "./menu-buffer-input.js" +import type { MenuError } from "./menu-errors.js" import { handleMenuNumberInput, submitPromptStep } from "./menu-input-utils.js" import { resetToMenu } from "./menu-shared.js" import type { @@ -60,7 +59,7 @@ const startAuthPrompt = ( const loadAuthMenuView = ( cwd: string, context: Pick -): Effect.Effect => +): Effect.Effect => pipe( readAuthSnapshot(cwd), Effect.tap((snapshot) => diff --git a/packages/app/src/docker-git/menu-create-shared.ts b/packages/app/src/docker-git/menu-create-shared.ts new file mode 100644 index 00000000..41e82863 --- /dev/null +++ b/packages/app/src/docker-git/menu-create-shared.ts @@ -0,0 +1,267 @@ +import { Match } from "effect" +import { deriveRepoPathParts, resolveRepoInput } from "./frontend-lib/core/domain.js" +import { defaultProjectsRoot, isRepoUrlInput } from "./frontend-lib/usecases/menu-helpers.js" + +import { type CreateInputs, type CreateStep, createSteps } from "./menu-types.js" + +type Mutable = { -readonly [K in keyof T]: T[K] } + +export type CreateFlowContext = { + readonly cwd: string + readonly projectsRoot?: string | undefined +} + +export type CreateFlowView = { + readonly step: number + readonly buffer: string + readonly values: Partial +} + +type AdvanceCreateFlowResult = + | { readonly _tag: "Continue"; readonly view: CreateFlowView } + | { readonly _tag: "Complete"; readonly inputs: CreateInputs } + +type AdvanceCreateFlowOptions = { + readonly forceWizard?: boolean +} + +const trimLeftSlash = (value: string): string => { + let start = 0 + while (start < value.length && value[start] === "/") { + start += 1 + } + return value.slice(start) +} + +const trimRightSlash = (value: string): string => { + let end = value.length + while (end > 0 && value[end - 1] === "/") { + end -= 1 + } + return value.slice(0, end) +} + +const joinPath = (...parts: ReadonlyArray): string => { + const cleaned = parts + .filter((part) => part.length > 0) + .map((part, index) => { + if (index === 0) { + return trimRightSlash(part) + } + return trimRightSlash(trimLeftSlash(part)) + }) + return cleaned.join("/") +} + +export const renderCreateStepLabel = (step: CreateStep, defaults: CreateInputs): string => + Match.value(step).pipe( + Match.when("repoUrl", () => "Repo URL (optional for empty workspace)"), + Match.when("repoRef", () => `Repo ref [${defaults.repoRef}]`), + Match.when("outDir", () => `Output dir [${defaults.outDir}]`), + Match.when("cpuLimit", () => `CPU limit [${defaults.cpuLimit || "30%"}]`), + Match.when("ramLimit", () => `RAM limit [${defaults.ramLimit || "30%"}]`), + Match.when("runUp", () => `Run docker compose up now? [${defaults.runUp ? "Y" : "n"}]`), + Match.when( + "mcpPlaywright", + () => `Enable Playwright MCP (Chromium sidecar)? [${defaults.enableMcpPlaywright ? "y" : "N"}]` + ), + Match.when( + "force", + () => `Force recreate (overwrite files + wipe volumes)? [${defaults.force ? "y" : "N"}]` + ), + Match.exhaustive + ) + +const normalizeCreateFlowContext = ( + context: string | CreateFlowContext +): CreateFlowContext => + typeof context === "string" + ? { cwd: context } + : context + +const resolveProjectsRoot = (context: CreateFlowContext): string => + context.projectsRoot?.trim().length + ? context.projectsRoot + : defaultProjectsRoot(context.cwd) + +const resolveDefaultOutDir = (context: CreateFlowContext, repoUrl: string): string => { + const resolvedRepo = resolveRepoInput(repoUrl) + const baseParts = deriveRepoPathParts(resolvedRepo.repoUrl).pathParts + const projectParts = resolvedRepo.workspaceSuffix ? [...baseParts, resolvedRepo.workspaceSuffix] : baseParts + return joinPath(resolveProjectsRoot(context), ...projectParts) +} + +export const resolveCreateInputs = ( + contextOrCwd: string | CreateFlowContext, + values: Partial +): CreateInputs => { + const context = normalizeCreateFlowContext(contextOrCwd) + const repoUrl = values.repoUrl ?? "" + const resolvedRepoRef = resolveRepoInput(repoUrl).repoRef + const outDir = values.outDir ?? resolveDefaultOutDir(context, repoUrl) + + return { + repoUrl, + repoRef: values.repoRef ?? resolvedRepoRef ?? "main", + outDir, + cpuLimit: values.cpuLimit ?? "", + ramLimit: values.ramLimit ?? "", + runUp: values.runUp !== false, + enableMcpPlaywright: values.enableMcpPlaywright === true, + force: values.force === true, + forceEnv: values.forceEnv === true + } +} + +const parseYesDefault = (input: string, fallback: boolean): boolean => { + const normalized = input.trim().toLowerCase() + if (normalized === "y" || normalized === "yes") { + return true + } + if (normalized === "n" || normalized === "no") { + return false + } + return fallback +} + +const applyCreateStep = (input: { + readonly step: CreateStep + readonly buffer: string + readonly currentDefaults: CreateInputs + readonly nextValues: Partial> + readonly context: CreateFlowContext +}): boolean => + Match.value(input.step).pipe( + Match.when("repoUrl", () => { + input.nextValues.repoUrl = input.buffer + input.nextValues.outDir = resolveDefaultOutDir(input.context, input.buffer) + return true + }), + Match.when("repoRef", () => { + input.nextValues.repoRef = input.buffer.length > 0 ? input.buffer : input.currentDefaults.repoRef + return true + }), + Match.when("outDir", () => { + input.nextValues.outDir = input.buffer.length > 0 ? input.buffer : input.currentDefaults.outDir + return true + }), + Match.when("cpuLimit", () => { + input.nextValues.cpuLimit = input.buffer.length > 0 ? input.buffer : input.currentDefaults.cpuLimit + return true + }), + Match.when("ramLimit", () => { + input.nextValues.ramLimit = input.buffer.length > 0 ? input.buffer : input.currentDefaults.ramLimit + return true + }), + Match.when("runUp", () => { + input.nextValues.runUp = parseYesDefault(input.buffer, input.currentDefaults.runUp) + return true + }), + Match.when("mcpPlaywright", () => { + input.nextValues.enableMcpPlaywright = parseYesDefault( + input.buffer, + input.currentDefaults.enableMcpPlaywright + ) + return true + }), + Match.when("force", () => { + input.nextValues.force = parseYesDefault(input.buffer, input.currentDefaults.force) + return true + }), + Match.exhaustive + ) + +export const createInitialFlowView = (buffer = ""): CreateFlowView => ({ + step: 0, + buffer, + values: {} +}) + +const shouldQuickCreate = ( + step: CreateStep, + buffer: string, + options: AdvanceCreateFlowOptions +): boolean => + step === "repoUrl" && + buffer.length > 0 && + isRepoUrlInput(buffer) && + options.forceWizard !== true + +const continueCreateFlow = ( + nextStep: number, + nextValues: Partial> +): AdvanceCreateFlowResult => ({ + _tag: "Continue", + view: { + step: nextStep, + buffer: "", + values: nextValues + } +}) + +export const advanceCreateFlow = ( + contextOrCwd: string | CreateFlowContext, + view: CreateFlowView, + options: AdvanceCreateFlowOptions = {} +): AdvanceCreateFlowResult | null => { + const context = normalizeCreateFlowContext(contextOrCwd) + const step = createSteps[view.step] + if (step === undefined) { + return null + } + + const buffer = view.buffer.trim() + const currentDefaults = resolveCreateInputs(context, view.values) + const nextValues: Partial> = { ...view.values } + const updated = applyCreateStep({ + step, + buffer, + currentDefaults, + nextValues, + context + }) + if (!updated) { + return null + } + + if (shouldQuickCreate(step, buffer, options)) { + return { + _tag: "Complete", + inputs: resolveCreateInputs(context, nextValues) + } + } + + const nextStep = view.step + 1 + if (nextStep < createSteps.length) { + return continueCreateFlow(nextStep, nextValues) + } + + return { + _tag: "Complete", + inputs: resolveCreateInputs(context, nextValues) + } +} + +export const createProjectDraftFromInputs = ( + input: CreateInputs +): { + readonly repoUrl: string + readonly repoRef: string + readonly outDir: string + readonly cpuLimit: string + readonly ramLimit: string + readonly up: boolean + readonly enableMcpPlaywright: boolean + readonly force: boolean + readonly forceEnv: boolean +} => ({ + repoUrl: input.repoUrl, + repoRef: input.repoRef, + outDir: input.outDir, + cpuLimit: input.cpuLimit, + ramLimit: input.ramLimit, + up: input.runUp, + enableMcpPlaywright: input.enableMcpPlaywright, + force: input.force, + forceEnv: input.forceEnv +}) diff --git a/packages/app/src/docker-git/menu-create.ts b/packages/app/src/docker-git/menu-create.ts index b2312d7c..51be89bf 100644 --- a/packages/app/src/docker-git/menu-create.ts +++ b/packages/app/src/docker-git/menu-create.ts @@ -1,6 +1,5 @@ -import { type CreateCommand, deriveRepoPathParts, resolveRepoInput } from "@lib/core/domain" -import { defaultProjectsRoot } from "@lib/usecases/menu-helpers" -import { Effect, Either, Match, pipe } from "effect" +import { Effect, Either, pipe } from "effect" +import { type CreateCommand } from "./frontend-lib/core/domain.js" import { createProject as createProjectViaApi } from "./api-client.js" import { parseArgs } from "./cli/parser.js" @@ -8,15 +7,9 @@ import { formatParseError, usageText } from "./cli/usage.js" import type { MenuError } from "./menu-errors.js" import { nextBufferValue } from "./menu-buffer-input.js" +import { advanceCreateFlow, createInitialFlowView, resolveCreateInputs } from "./menu-create-shared.js" import { resetToMenu } from "./menu-shared.js" -import { - type CreateInputs, - type CreateStep, - createSteps, - type MenuEnv, - type MenuState, - type ViewState -} from "./menu-types.js" +import { type CreateInputs, type MenuEnv, type MenuState, type ViewState } from "./menu-types.js" // CHANGE: move create-flow handling into a dedicated module // WHY: keep TUI entry slim and satisfy lint constraints @@ -29,8 +22,6 @@ import { // INVARIANT: outDir resolves to a stable repo path // COMPLEXITY: O(1) per keypress -type Mutable = { -readonly [K in keyof T]: T[K] } - type CreateRunner = { readonly runEffect: (effect: Effect.Effect) => void } type CreateContext = { @@ -81,73 +72,6 @@ export const buildCreateArgs = (input: CreateInputs): ReadonlyArray => { return args } -const trimLeftSlash = (value: string): string => { - let start = 0 - while (start < value.length && value[start] === "/") { - start += 1 - } - return value.slice(start) -} - -const trimRightSlash = (value: string): string => { - let end = value.length - while (end > 0 && value[end - 1] === "/") { - end -= 1 - } - return value.slice(0, end) -} - -const joinPath = (...parts: ReadonlyArray): string => { - const cleaned = parts - .filter((part) => part.length > 0) - .map((part, index) => { - if (index === 0) { - return trimRightSlash(part) - } - return trimRightSlash(trimLeftSlash(part)) - }) - return cleaned.join("/") -} - -const resolveDefaultOutDir = (cwd: string, repoUrl: string): string => { - const resolvedRepo = resolveRepoInput(repoUrl) - const baseParts = deriveRepoPathParts(resolvedRepo.repoUrl).pathParts - const projectParts = resolvedRepo.workspaceSuffix ? [...baseParts, resolvedRepo.workspaceSuffix] : baseParts - return joinPath(defaultProjectsRoot(cwd), ...projectParts) -} - -export const resolveCreateInputs = ( - cwd: string, - values: Partial -): CreateInputs => { - const repoUrl = values.repoUrl ?? "" - const resolvedRepoRef = resolveRepoInput(repoUrl).repoRef - const outDir = values.outDir ?? resolveDefaultOutDir(cwd, repoUrl) - - return { - repoUrl, - repoRef: values.repoRef ?? resolvedRepoRef ?? "main", - outDir, - cpuLimit: values.cpuLimit ?? "", - ramLimit: values.ramLimit ?? "", - runUp: values.runUp !== false, - enableMcpPlaywright: values.enableMcpPlaywright === true, - force: values.force === true, - forceEnv: values.forceEnv === true - } -} - -const parseYesDefault = (input: string, fallback: boolean): boolean => { - const normalized = input.trim().toLowerCase() - if (normalized === "y" || normalized === "yes") { - return true - } - if (normalized === "n" || normalized === "no") { - return false - } - return fallback -} - const applyCreateCommand = ( state: MenuState, create: CreateCommand @@ -187,54 +111,6 @@ const buildCreateEffect = ( return Effect.void } -const applyCreateStep = (input: { - readonly step: CreateStep - readonly buffer: string - readonly currentDefaults: CreateInputs - readonly nextValues: Partial> - readonly cwd: string - readonly setMessage: (message: string | null) => void -}): boolean => - Match.value(input.step).pipe( - Match.when("repoUrl", () => { - input.nextValues.repoUrl = input.buffer - input.nextValues.outDir = resolveDefaultOutDir(input.cwd, input.buffer) - return true - }), - Match.when("repoRef", () => { - input.nextValues.repoRef = input.buffer.length > 0 ? input.buffer : input.currentDefaults.repoRef - return true - }), - Match.when("outDir", () => { - input.nextValues.outDir = input.buffer.length > 0 ? input.buffer : input.currentDefaults.outDir - return true - }), - Match.when("cpuLimit", () => { - input.nextValues.cpuLimit = input.buffer.length > 0 ? input.buffer : input.currentDefaults.cpuLimit - return true - }), - Match.when("ramLimit", () => { - input.nextValues.ramLimit = input.buffer.length > 0 ? input.buffer : input.currentDefaults.ramLimit - return true - }), - Match.when("runUp", () => { - input.nextValues.runUp = parseYesDefault(input.buffer, input.currentDefaults.runUp) - return true - }), - Match.when("mcpPlaywright", () => { - input.nextValues.enableMcpPlaywright = parseYesDefault( - input.buffer, - input.currentDefaults.enableMcpPlaywright - ) - return true - }), - Match.when("force", () => { - input.nextValues.force = parseYesDefault(input.buffer, input.currentDefaults.force) - return true - }), - Match.exhaustive - ) - const finalizeCreateFlow = (input: { readonly state: MenuState readonly nextValues: Partial @@ -257,38 +133,22 @@ const finalizeCreateFlow = (input: { input.setMessage(null) } -const handleCreateReturn = (context: CreateReturnContext) => { - const step = createSteps[context.view.step] - if (!step) { - context.setView({ _tag: "Menu" }) - return - } - - const buffer = context.view.buffer.trim() - const currentDefaults = resolveCreateInputs(context.state.cwd, context.view.values) - const nextValues: Partial> = { ...context.view.values } - const updated = applyCreateStep({ - step, - buffer, - currentDefaults, - nextValues, - cwd: context.state.cwd, - setMessage: context.setMessage - }) - if (!updated) { +const handleCreateReturn = ( + context: CreateReturnContext, + forceWizard = false +) => { + const next = advanceCreateFlow(context.state.cwd, context.view, { forceWizard }) + if (next === null) { return } - - const nextStep = context.view.step + 1 - if (nextStep < createSteps.length) { - context.setView({ _tag: "Create", step: nextStep, buffer: "", values: nextValues }) + if (next._tag === "Continue") { + context.setView({ _tag: "Create", ...next.view }) context.setMessage(null) return } - finalizeCreateFlow({ state: context.state, - nextValues, + nextValues: next.inputs, setView: context.setView, setMessage: context.setMessage, runner: context.runner, @@ -301,7 +161,7 @@ export const startCreateView = ( setMessage: (message: string | null) => void, buffer = "" ) => { - setView({ _tag: "Create", step: 0, buffer, values: {} }) + setView({ _tag: "Create", ...createInitialFlowView(buffer) }) setMessage(null) } @@ -310,6 +170,7 @@ export const handleCreateInput = ( key: { readonly escape?: boolean readonly return?: boolean + readonly shift?: boolean readonly backspace?: boolean readonly delete?: boolean }, @@ -321,7 +182,7 @@ export const handleCreateInput = ( return } if (key.return) { - handleCreateReturn({ ...context, view }) + handleCreateReturn({ ...context, view }, key.shift === true) return } const nextBuffer = nextBufferValue(input, key, view.buffer) @@ -329,3 +190,5 @@ export const handleCreateInput = ( context.setView({ ...view, buffer: nextBuffer }) } } + +export { resolveCreateInputs } from "./menu-create-shared.js" diff --git a/packages/app/src/docker-git/menu-errors.ts b/packages/app/src/docker-git/menu-errors.ts index 78e10ef1..b9d72727 100644 --- a/packages/app/src/docker-git/menu-errors.ts +++ b/packages/app/src/docker-git/menu-errors.ts @@ -1,17 +1,6 @@ -import type { AppError } from "@lib/usecases/errors" -import { renderError } from "@lib/usecases/errors" - import type { HostError } from "./host-errors.js" import { renderCliError } from "./host-errors.js" -export type MenuError = AppError | HostError - -const isHostError = (error: MenuError): error is HostError => - error._tag === "ControllerBootstrapError" || - error._tag === "ApiRequestError" || - error._tag === "ApiAuthRequiredError" || - error._tag === "ProjectResolutionError" || - error._tag === "UnsupportedCommandError" +export type MenuError = HostError -export const renderMenuError = (error: MenuError): string => - isHostError(error) ? renderCliError(error) : renderError(error) +export const renderMenuError = (error: MenuError): string => renderCliError(error) diff --git a/packages/app/src/docker-git/menu-gridland-runtime.tsx b/packages/app/src/docker-git/menu-gridland-runtime.tsx new file mode 100644 index 00000000..b9dcd79f --- /dev/null +++ b/packages/app/src/docker-git/menu-gridland-runtime.tsx @@ -0,0 +1,189 @@ +import { Effect } from "effect" +import React, { useMemo } from "react" + +import type { GridlandKeyEvent, GridlandModule, GridlandRenderer } from "@gridland/bun" + +import { createGridlandPrimitives } from "../ui/primitives-gridland.js" +import { UiProvider } from "../ui/primitives.js" +import { handleUserInput, type MenuInputContext } from "./menu-input-handler.js" + +type InputReadError = { + readonly _tag: "InputReadError" + readonly message: string +} + +const blockedInputNames = new Set([ + "backspace", + "del", + "delete", + "down", + "enter", + "escape", + "pagedown", + "pageup", + "return", + "tab", + "up" +]) + +const isBlockedInputName = (name: string | undefined): boolean => blockedInputNames.has(name ?? "") + +const resolveSequencedKeyboardInput = (event: GridlandKeyEvent): string | null => { + if (typeof event.sequence !== "string" || event.sequence.length === 0 || isBlockedInputName(event.name)) { + return null + } + return event.sequence +} + +const resolveNamedKeyboardInput = (event: GridlandKeyEvent): string | null => { + if (typeof event.name !== "string" || event.name.length !== 1 || isBlockedInputName(event.name)) { + return null + } + return event.name +} + +const resolveKeyboardInput = (event: GridlandKeyEvent): string => { + if (event.ctrl || event.meta) { + return "" + } + return resolveSequencedKeyboardInput(event) ?? resolveNamedKeyboardInput(event) ?? "" +} + +const toMenuKeyInput = (event: GridlandKeyEvent) => { + const name = event.name + return { + backspace: name === "backspace", + delete: name === "delete" || name === "del", + downArrow: name === "down", + escape: name === "escape", + return: name === "enter" || name === "return", + shift: event.shift === true, + upArrow: name === "up" + } as const +} + +const toInputReadError = (error: Error | string): InputReadError => ({ + _tag: "InputReadError", + message: error instanceof Error ? error.message : error +}) + +const waitForRendererDestroy = (renderer: GridlandRenderer): Effect.Effect => + Effect.async((resume) => { + renderer.once("destroy", () => { + resume(Effect.void) + }) + }) + +const loadGridlandModule = (): Effect.Effect => + Effect.tryPromise({ + try: () => import("@gridland/bun"), + catch: (error) => toInputReadError(error instanceof Error ? error : String(error)) + }) + +const createGridlandRenderer = (gridland: GridlandModule): Effect.Effect => + Effect.tryPromise({ + try: () => + gridland.createCliRenderer({ + exitOnCtrlC: false, + useConsole: false, + useMouse: false + }), + catch: (error) => toInputReadError(error instanceof Error ? error : String(error)) + }) + +type GridlandAppFactory = (args: { + readonly exit: () => void + readonly gridland: GridlandModule +}) => React.ReactElement + +const runEmbeddedGridlandMenu = (renderApp: GridlandAppFactory): Effect.Effect => + Effect.gen(function*() { + const gridland = yield* loadGridlandModule() + const renderer = yield* createGridlandRenderer(gridland) + const root = gridland.createRoot(renderer) + + root.render( + renderApp({ + exit: () => { + renderer.destroy() + }, + gridland + }) + ) + + renderer.start() + yield* waitForRendererDestroy(renderer) + }) + +export const runGridlandMenu = (renderApp: GridlandAppFactory): Effect.Effect => + runEmbeddedGridlandMenu(renderApp) + +type GridlandMenuRuntimeContext = + & Pick< + MenuInputContext, + | "busy" + | "exit" + | "inputStage" + | "runner" + | "selected" + | "setActiveDir" + | "setInputStage" + | "setMessage" + | "setSelected" + | "setSkipInputs" + | "setSshActive" + | "setView" + | "sshActive" + | "state" + | "view" + > + & { + readonly ignoreUntil: number + readonly ready: boolean + readonly skipInputs: number + } + +const shouldIgnoreKeyEvent = (context: GridlandMenuRuntimeContext): boolean => + !context.ready || Date.now() < context.ignoreUntil + +const shouldConsumeSkippedInput = (context: GridlandMenuRuntimeContext): boolean => context.skipInputs > 0 + +const consumeSkippedInput = (context: GridlandMenuRuntimeContext): void => { + context.setSkipInputs((value) => (value > 0 ? value - 1 : 0)) +} + +const handleCtrlC = (event: GridlandKeyEvent, context: GridlandMenuRuntimeContext): boolean => { + if (!(event.ctrl && event.name === "c")) { + return false + } + if (!context.sshActive) { + context.exit() + } + return true +} + +export const useGridlandMenuInput = (gridland: GridlandModule, context: GridlandMenuRuntimeContext): void => { + gridland.useKeyboard((event) => { + if (handleCtrlC(event, context) || shouldIgnoreKeyEvent(context)) { + return + } + if (shouldConsumeSkippedInput(context)) { + consumeSkippedInput(context) + return + } + handleUserInput(resolveKeyboardInput(event), toMenuKeyInput(event), context) + }) +} + +export const GridlandMenuProvider = ( + { + children, + gridland + }: { + readonly children: React.ReactNode + readonly gridland: GridlandModule + } +): React.ReactElement => { + const primitives = useMemo(() => createGridlandPrimitives(gridland), [gridland]) + return React.createElement(UiProvider, { primitives }, children) +} diff --git a/packages/app/src/docker-git/menu-input.ts b/packages/app/src/docker-git/menu-input.ts index d3fb7926..4c154067 100644 --- a/packages/app/src/docker-git/menu-input.ts +++ b/packages/app/src/docker-git/menu-input.ts @@ -1,2 +1,3 @@ -export { buildCreateArgs, handleCreateInput, resolveCreateInputs, startCreateView } from "./menu-create.js" +export { resolveCreateInputs } from "./menu-create-shared.js" +export { buildCreateArgs, handleCreateInput, startCreateView } from "./menu-create.js" export { handleMenuInput } from "./menu-menu.js" diff --git a/packages/app/src/docker-git/menu-labeled-env.ts b/packages/app/src/docker-git/menu-labeled-env.ts index 1e69b069..179248dc 100644 --- a/packages/app/src/docker-git/menu-labeled-env.ts +++ b/packages/app/src/docker-git/menu-labeled-env.ts @@ -1,4 +1,29 @@ -import { parseEnvEntries } from "@lib/usecases/env-file" +type EnvEntry = { + readonly key: string + readonly value: string +} + +const parseEnvEntries = (input: string): ReadonlyArray => { + const entries: Array = [] + for (const rawLine of input.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n")) { + const line = rawLine.trim() + if (line.length === 0 || line.startsWith("#")) { + continue + } + const normalized = line.startsWith("export ") ? line.slice("export ".length).trimStart() : line + const equalsIndex = normalized.indexOf("=") + if (equalsIndex <= 0) { + continue + } + const key = normalized.slice(0, equalsIndex).trim() + const value = normalized.slice(equalsIndex + 1).trim() + if (key.length === 0) { + continue + } + entries.push({ key, value }) + } + return entries +} export const normalizeLabel = (value: string): string => { const trimmed = value.trim() diff --git a/packages/app/src/docker-git/menu-menu.ts b/packages/app/src/docker-git/menu-menu.ts index f09adc5e..021ab4c2 100644 --- a/packages/app/src/docker-git/menu-menu.ts +++ b/packages/app/src/docker-git/menu-menu.ts @@ -1,10 +1,82 @@ -import { parseMenuSelection } from "@lib/core/domain" -import { isRepoUrlInput } from "@lib/usecases/menu-helpers" import { Either } from "effect" import { handleMenuActionSelection, type MenuSelectionContext } from "./menu-actions.js" import { startCreateView } from "./menu-create.js" -import { menuItems } from "./menu-types.js" +import { type MenuAction, menuItems } from "./menu-types.js" + +const isRepoUrlInput = (input: string): boolean => { + const trimmed = input.trim().toLowerCase() + return trimmed.startsWith("http://") || + trimmed.startsWith("https://") || + trimmed.startsWith("ssh://") || + trimmed.startsWith("git@") +} + +const menuAliasMap = new Map([ + ["1", { _tag: "Create" }], + ["create", { _tag: "Create" }], + ["c", { _tag: "Create" }], + ["2", { _tag: "Select" }], + ["select", { _tag: "Select" }], + ["s", { _tag: "Select" }], + ["3", { _tag: "Auth" }], + ["auth", { _tag: "Auth" }], + ["a", { _tag: "Auth" }], + ["4", { _tag: "ProjectAuth" }], + ["project-auth", { _tag: "ProjectAuth" }], + ["projectauth", { _tag: "ProjectAuth" }], + ["pa", { _tag: "ProjectAuth" }], + ["5", { _tag: "Info" }], + ["info", { _tag: "Info" }], + ["i", { _tag: "Info" }], + ["6", { _tag: "Up" }], + ["up", { _tag: "Up" }], + ["u", { _tag: "Up" }], + ["start", { _tag: "Up" }], + ["7", { _tag: "Status" }], + ["status", { _tag: "Status" }], + ["ps", { _tag: "Status" }], + ["8", { _tag: "Logs" }], + ["logs", { _tag: "Logs" }], + ["log", { _tag: "Logs" }], + ["l", { _tag: "Logs" }], + ["9", { _tag: "Down" }], + ["down", { _tag: "Down" }], + ["stop", { _tag: "Down" }], + ["d", { _tag: "Down" }], + ["10", { _tag: "DownAll" }], + ["down-all", { _tag: "DownAll" }], + ["downall", { _tag: "DownAll" }], + ["stop-all", { _tag: "DownAll" }], + ["stopall", { _tag: "DownAll" }], + ["kill-all", { _tag: "DownAll" }], + ["killall", { _tag: "DownAll" }], + ["da", { _tag: "DownAll" }], + ["11", { _tag: "Delete" }], + ["delete", { _tag: "Delete" }], + ["del", { _tag: "Delete" }], + ["remove", { _tag: "Delete" }], + ["rm", { _tag: "Delete" }], + ["0", { _tag: "Quit" }], + ["12", { _tag: "Quit" }], + ["quit", { _tag: "Quit" }], + ["q", { _tag: "Quit" }], + ["exit", { _tag: "Quit" }] +]) + +const parseMenuSelection = ( + input: string +): Either.Either => { + const normalized = input.trim().toLowerCase() + if (normalized.length === 0) { + return Either.left({ _tag: "InvalidOption", option: "menu", reason: "empty selection" }) + } + + const action = menuAliasMap.get(normalized) + return action === undefined + ? Either.left({ _tag: "InvalidOption", option: "menu", reason: `unknown selection: ${input}` }) + : Either.right(action) +} const handleMenuNavigation = ( key: { readonly upArrow?: boolean; readonly downArrow?: boolean }, diff --git a/packages/app/src/docker-git/menu-project-auth-data.ts b/packages/app/src/docker-git/menu-project-auth-data.ts index 8e40590c..08852e4a 100644 --- a/packages/app/src/docker-git/menu-project-auth-data.ts +++ b/packages/app/src/docker-git/menu-project-auth-data.ts @@ -1,205 +1,49 @@ -import * as FileSystem from "@effect/platform/FileSystem" -import * as Path from "@effect/platform/Path" -import { Effect, Match, pipe } from "effect" +import { Effect } from "effect" -import { ensureEnvFile, findEnvValue, readEnvText } from "@lib/usecases/env-file" -import type { AppError } from "@lib/usecases/errors" -import { defaultProjectsRoot } from "@lib/usecases/menu-helpers" -import type { ProjectItem } from "@lib/usecases/projects" -import { autoSyncState } from "@lib/usecases/state-repo" - -import { countAuthAccountEntries } from "./menu-auth-snapshot-builder.js" -import { countKeyEntries, normalizeLabel } from "./menu-labeled-env.js" -import { type ProjectEnvUpdateSpec, resolveProjectEnvUpdate } from "./menu-project-auth-flows.js" +import { normalizeOptionalText } from "../shared/optional-text.js" +import { loadProjectAuthSnapshot, runProjectAuthFlow as submitProjectAuthFlow } from "./api-auth-menu-client.js" +import type { MenuError } from "./menu-errors.js" import type { MenuEnv, ProjectAuthFlow, ProjectAuthSnapshot } from "./menu-types.js" - -export type ProjectAuthMenuAction = ProjectAuthFlow | "Refresh" | "Back" - -type ProjectAuthMenuItem = { - readonly action: ProjectAuthMenuAction - readonly label: string -} - -export type ProjectAuthPromptStep = { - readonly key: "label" - readonly label: string - readonly required: boolean - readonly secret: boolean -} - -const projectAuthMenuItems: ReadonlyArray = [ - { action: "ProjectGithubConnect", label: "Project: GitHub connect label" }, - { action: "ProjectGithubDisconnect", label: "Project: GitHub disconnect" }, - { action: "ProjectGitConnect", label: "Project: Git connect label" }, - { action: "ProjectGitDisconnect", label: "Project: Git disconnect" }, - { action: "ProjectClaudeConnect", label: "Project: Claude connect label" }, - { action: "ProjectClaudeDisconnect", label: "Project: Claude disconnect" }, - { action: "ProjectGeminiConnect", label: "Project: Gemini connect label" }, - { action: "ProjectGeminiDisconnect", label: "Project: Gemini disconnect" }, - { action: "Refresh", label: "Refresh snapshot" }, - { action: "Back", label: "Back to main menu" } -] - -const flowSteps: Readonly>> = { - ProjectGithubConnect: [ - { key: "label", label: "Label (empty = default)", required: false, secret: false } - ], - ProjectGithubDisconnect: [], - ProjectGitConnect: [ - { key: "label", label: "Label (empty = default)", required: false, secret: false } - ], - ProjectGitDisconnect: [], - ProjectClaudeConnect: [ - { key: "label", label: "Label (empty = default)", required: false, secret: false } - ], - ProjectClaudeDisconnect: [], - ProjectGeminiConnect: [ - { key: "label", label: "Label (empty = default)", required: false, secret: false } - ], - ProjectGeminiDisconnect: [] -} - -const resolveCanonicalLabel = (value: string): string => { - const normalized = normalizeLabel(value) - return normalized.length === 0 || normalized === "DEFAULT" ? "default" : normalized -} - -const githubTokenBaseKey = "GITHUB_TOKEN" -const gitTokenBaseKey = "GIT_AUTH_TOKEN" -const projectGithubLabelKey = "GITHUB_AUTH_LABEL" -const projectGitLabelKey = "GIT_AUTH_LABEL" -const projectClaudeLabelKey = "CLAUDE_AUTH_LABEL" -const projectGeminiLabelKey = "GEMINI_AUTH_LABEL" - -type ProjectAuthEnvText = { - readonly fs: FileSystem.FileSystem - readonly path: Path.Path - readonly globalEnvPath: string - readonly projectEnvPath: string - readonly claudeAuthPath: string - readonly geminiAuthPath: string - readonly globalEnvText: string - readonly projectEnvText: string -} - -const buildGlobalEnvPath = (cwd: string): string => `${defaultProjectsRoot(cwd)}/.orch/env/global.env` -const buildClaudeAuthPath = (cwd: string): string => `${defaultProjectsRoot(cwd)}/.orch/auth/claude` -const buildGeminiAuthPath = (cwd: string): string => `${defaultProjectsRoot(cwd)}/.orch/auth/gemini` - -const loadProjectAuthEnvText = ( - project: ProjectItem -): Effect.Effect => - Effect.gen(function*(_) { - const fs = yield* _(FileSystem.FileSystem) - const path = yield* _(Path.Path) - const globalEnvPath = buildGlobalEnvPath(process.cwd()) - const claudeAuthPath = buildClaudeAuthPath(process.cwd()) - const geminiAuthPath = buildGeminiAuthPath(process.cwd()) - yield* _(ensureEnvFile(fs, path, globalEnvPath)) - yield* _(ensureEnvFile(fs, path, project.envProjectPath)) - const globalEnvText = yield* _(readEnvText(fs, globalEnvPath)) - const projectEnvText = yield* _(readEnvText(fs, project.envProjectPath)) - return { - fs, - path, - globalEnvPath, - projectEnvPath: project.envProjectPath, - claudeAuthPath, - geminiAuthPath, - globalEnvText, - projectEnvText - } - }) +import type { ProjectItem } from "./project-item.js" + +export { + projectAuthMenuActionByIndex, + projectAuthMenuLabels, + projectAuthMenuSize, + projectAuthSuccessMessage, + projectAuthViewSteps +} from "./menu-project-auth-shared.js" +export type { ProjectAuthMenuAction, ProjectAuthPromptStep } from "./menu-project-auth-shared.js" + +const decodeSnapshot = ( + projectId: string, + snapshot: ProjectAuthSnapshot | null +): Effect.Effect => + snapshot === null + ? Effect.fail({ + _tag: "ApiRequestError", + method: "GET", + path: `/projects/${projectId}/auth/menu`, + message: `Controller returned an invalid project auth snapshot for ${projectId}.` + }) + : Effect.succeed(snapshot) export const readProjectAuthSnapshot = ( project: ProjectItem -): Effect.Effect => - pipe( - loadProjectAuthEnvText(project), - Effect.flatMap(({ - claudeAuthPath, - fs, - geminiAuthPath, - globalEnvPath, - globalEnvText, - path, - projectEnvPath, - projectEnvText - }) => - countAuthAccountEntries(fs, path, claudeAuthPath, geminiAuthPath).pipe( - Effect.map(({ claudeAuthEntries, geminiAuthEntries }) => ({ - projectDir: project.projectDir, - projectName: project.displayName, - envGlobalPath: globalEnvPath, - envProjectPath: projectEnvPath, - claudeAuthPath, - geminiAuthPath, - githubTokenEntries: countKeyEntries(globalEnvText, githubTokenBaseKey), - gitTokenEntries: countKeyEntries(globalEnvText, gitTokenBaseKey), - claudeAuthEntries, - geminiAuthEntries, - activeGithubLabel: findEnvValue(projectEnvText, projectGithubLabelKey), - activeGitLabel: findEnvValue(projectEnvText, projectGitLabelKey), - activeClaudeLabel: findEnvValue(projectEnvText, projectClaudeLabelKey), - activeGeminiLabel: findEnvValue(projectEnvText, projectGeminiLabelKey) - })) - ) - ) - ) - -const resolveSyncMessage = (flow: ProjectAuthFlow, canonicalLabel: string, displayName: string): string => - Match.value(flow).pipe( - Match.when("ProjectGithubConnect", () => `chore(state): project auth gh ${canonicalLabel} ${displayName}`), - Match.when("ProjectGithubDisconnect", () => `chore(state): project auth gh logout ${displayName}`), - Match.when("ProjectGitConnect", () => `chore(state): project auth git ${canonicalLabel} ${displayName}`), - Match.when("ProjectGitDisconnect", () => `chore(state): project auth git logout ${displayName}`), - Match.when("ProjectClaudeConnect", () => `chore(state): project auth claude ${canonicalLabel} ${displayName}`), - Match.when("ProjectClaudeDisconnect", () => `chore(state): project auth claude logout ${displayName}`), - Match.when("ProjectGeminiConnect", () => `chore(state): project auth gemini ${canonicalLabel} ${displayName}`), - Match.when("ProjectGeminiDisconnect", () => `chore(state): project auth gemini logout ${displayName}`), - Match.exhaustive +): Effect.Effect => + loadProjectAuthSnapshot(project.projectDir).pipe( + Effect.flatMap((snapshot) => decodeSnapshot(project.projectDir, snapshot)) ) export const writeProjectAuthFlow = ( project: ProjectItem, flow: ProjectAuthFlow, values: Readonly> -): Effect.Effect => - pipe( - loadProjectAuthEnvText(project), - Effect.flatMap( - ({ claudeAuthPath, fs, geminiAuthPath, globalEnvPath, globalEnvText, projectEnvPath, projectEnvText }) => { - const rawLabel = values["label"] ?? "" - const canonicalLabel = resolveCanonicalLabel(rawLabel) - const spec: ProjectEnvUpdateSpec = { - fs, - rawLabel, - canonicalLabel, - globalEnvPath, - globalEnvText, - projectEnvText, - claudeAuthPath, - geminiAuthPath - } - const nextProjectEnv = resolveProjectEnvUpdate(flow, spec) - const syncMessage = resolveSyncMessage(flow, canonicalLabel, project.displayName) - return pipe( - nextProjectEnv, - Effect.flatMap((nextText) => fs.writeFileString(projectEnvPath, nextText)), - Effect.zipRight(autoSyncState(syncMessage)) - ) - } - ), +): Effect.Effect => + submitProjectAuthFlow(project.projectDir, { + flow, + label: normalizeOptionalText(values["label"]) + }).pipe( + Effect.flatMap((snapshot) => decodeSnapshot(project.projectDir, snapshot)), Effect.asVoid ) - -export const projectAuthViewSteps = (flow: ProjectAuthFlow): ReadonlyArray => flowSteps[flow] - -export const projectAuthMenuLabels = (): ReadonlyArray => projectAuthMenuItems.map((item) => item.label) - -export const projectAuthMenuActionByIndex = (index: number): ProjectAuthMenuAction | null => { - const item = projectAuthMenuItems[index] - return item ? item.action : null -} - -export const projectAuthMenuSize = (): number => projectAuthMenuItems.length diff --git a/packages/app/src/docker-git/menu-project-auth-flows.ts b/packages/app/src/docker-git/menu-project-auth-flows.ts deleted file mode 100644 index 6e08210d..00000000 --- a/packages/app/src/docker-git/menu-project-auth-flows.ts +++ /dev/null @@ -1,150 +0,0 @@ -import type { PlatformError } from "@effect/platform/Error" -import type * as FileSystem from "@effect/platform/FileSystem" -import { Effect, Match } from "effect" - -import { AuthError } from "@lib/shell/errors" -import { normalizeAccountLabel } from "@lib/usecases/auth-helpers" -import { findEnvValue, upsertEnvKey } from "@lib/usecases/env-file" -import type { AppError } from "@lib/usecases/errors" - -import { buildLabeledEnvKey } from "./menu-labeled-env.js" -import { hasClaudeAccountCredentials } from "./menu-project-auth-claude.js" -import { hasGeminiAccountCredentials } from "./menu-project-auth-gemini.js" -import type { ProjectAuthFlow } from "./menu-types.js" - -export type ProjectEnvUpdateSpec = { - readonly fs: FileSystem.FileSystem - readonly rawLabel: string - readonly canonicalLabel: string - readonly globalEnvPath: string - readonly globalEnvText: string - readonly projectEnvText: string - readonly claudeAuthPath: string - readonly geminiAuthPath: string -} - -const githubTokenBaseKey = "GITHUB_TOKEN" -const gitTokenBaseKey = "GIT_AUTH_TOKEN" -const gitUserBaseKey = "GIT_AUTH_USER" -const projectGithubLabelKey = "GITHUB_AUTH_LABEL" -const projectGitLabelKey = "GIT_AUTH_LABEL" -const projectClaudeLabelKey = "CLAUDE_AUTH_LABEL" -const projectGeminiLabelKey = "GEMINI_AUTH_LABEL" -const defaultGitUser = "x-access-token" - -const missingSecret = (provider: string, label: string, envPath: string): AuthError => - new AuthError({ message: `${provider} not connected: label '${label}' not found in ${envPath}` }) - -const clearProjectGitLabels = (envText: string): string => { - const withoutGhToken = upsertEnvKey(envText, "GH_TOKEN", "") - const withoutGitLabel = upsertEnvKey(withoutGhToken, projectGitLabelKey, "") - return upsertEnvKey(withoutGitLabel, projectGithubLabelKey, "") -} - -const updateProjectGithubConnect = (spec: ProjectEnvUpdateSpec): Effect.Effect => { - const key = buildLabeledEnvKey(githubTokenBaseKey, spec.rawLabel) - const token = findEnvValue(spec.globalEnvText, key) - if (token === null) { - return Effect.fail(missingSecret("GitHub token", spec.canonicalLabel, spec.globalEnvPath)) - } - const withGitToken = upsertEnvKey(spec.projectEnvText, "GIT_AUTH_TOKEN", token) - const withGhToken = upsertEnvKey(withGitToken, "GH_TOKEN", token) - const withoutGitLabel = upsertEnvKey(withGhToken, projectGitLabelKey, "") - return Effect.succeed(upsertEnvKey(withoutGitLabel, projectGithubLabelKey, spec.canonicalLabel)) -} - -const updateProjectGithubDisconnect = (spec: ProjectEnvUpdateSpec): Effect.Effect => { - const withoutGitToken = upsertEnvKey(spec.projectEnvText, "GIT_AUTH_TOKEN", "") - return Effect.succeed(clearProjectGitLabels(withoutGitToken)) -} - -const updateProjectGitConnect = (spec: ProjectEnvUpdateSpec): Effect.Effect => { - const tokenKey = buildLabeledEnvKey(gitTokenBaseKey, spec.rawLabel) - const userKey = buildLabeledEnvKey(gitUserBaseKey, spec.rawLabel) - const token = findEnvValue(spec.globalEnvText, tokenKey) - if (token === null) { - return Effect.fail(missingSecret("Git credentials", spec.canonicalLabel, spec.globalEnvPath)) - } - const defaultUser = findEnvValue(spec.globalEnvText, gitUserBaseKey) ?? defaultGitUser - const user = findEnvValue(spec.globalEnvText, userKey) ?? defaultUser - const withToken = upsertEnvKey(spec.projectEnvText, "GIT_AUTH_TOKEN", token) - const withUser = upsertEnvKey(withToken, "GIT_AUTH_USER", user) - const withGhToken = upsertEnvKey(withUser, "GH_TOKEN", token) - const withGitLabel = upsertEnvKey(withGhToken, projectGitLabelKey, spec.canonicalLabel) - return Effect.succeed(upsertEnvKey(withGitLabel, projectGithubLabelKey, spec.canonicalLabel)) -} - -const updateProjectGitDisconnect = (spec: ProjectEnvUpdateSpec): Effect.Effect => { - const withoutToken = upsertEnvKey(spec.projectEnvText, "GIT_AUTH_TOKEN", "") - const withoutUser = upsertEnvKey(withoutToken, "GIT_AUTH_USER", "") - return Effect.succeed(clearProjectGitLabels(withoutUser)) -} - -type CredentialsChecker = ( - fs: FileSystem.FileSystem, - accountPath: string -) => Effect.Effect - -const resolveAccountCandidates = (authPath: string, accountLabel: string): ReadonlyArray => - accountLabel === "default" ? [`${authPath}/default`, authPath] : [`${authPath}/${accountLabel}`] - -const findFirstCredentialsMatch = ( - fs: FileSystem.FileSystem, - candidates: ReadonlyArray, - hasCredentials: CredentialsChecker -): Effect.Effect => - Effect.gen(function*(_) { - for (const accountPath of candidates) { - const exists = yield* _(fs.exists(accountPath)) - if (!exists) continue - const valid = yield* _(hasCredentials(fs, accountPath), Effect.orElseSucceed(() => false)) - if (valid) return accountPath - } - return null - }) - -const updateProjectClaudeConnect = (spec: ProjectEnvUpdateSpec): Effect.Effect => { - const accountLabel = normalizeAccountLabel(spec.rawLabel, "default") - const accountCandidates = resolveAccountCandidates(spec.claudeAuthPath, accountLabel) - return findFirstCredentialsMatch(spec.fs, accountCandidates, hasClaudeAccountCredentials).pipe( - Effect.flatMap((matched) => - matched === null - ? Effect.fail(missingSecret("Claude Code login", spec.canonicalLabel, spec.claudeAuthPath)) - : Effect.succeed(upsertEnvKey(spec.projectEnvText, projectClaudeLabelKey, spec.canonicalLabel)) - ) - ) -} - -const updateProjectClaudeDisconnect = (spec: ProjectEnvUpdateSpec): Effect.Effect => - Effect.succeed(upsertEnvKey(spec.projectEnvText, projectClaudeLabelKey, "")) - -const updateProjectGeminiConnect = (spec: ProjectEnvUpdateSpec): Effect.Effect => { - const accountLabel = normalizeAccountLabel(spec.rawLabel, "default") - const accountCandidates = resolveAccountCandidates(spec.geminiAuthPath, accountLabel) - return findFirstCredentialsMatch(spec.fs, accountCandidates, hasGeminiAccountCredentials).pipe( - Effect.flatMap((matched) => - matched === null - ? Effect.fail(missingSecret("Gemini CLI API key", spec.canonicalLabel, spec.geminiAuthPath)) - : Effect.succeed(upsertEnvKey(spec.projectEnvText, projectGeminiLabelKey, spec.canonicalLabel)) - ) - ) -} - -const updateProjectGeminiDisconnect = (spec: ProjectEnvUpdateSpec): Effect.Effect => - Effect.succeed(upsertEnvKey(spec.projectEnvText, projectGeminiLabelKey, "")) - -export const resolveProjectEnvUpdate = ( - flow: ProjectAuthFlow, - spec: ProjectEnvUpdateSpec -): Effect.Effect => - Match.value(flow).pipe( - Match.when("ProjectGithubConnect", () => updateProjectGithubConnect(spec)), - Match.when("ProjectGithubDisconnect", () => updateProjectGithubDisconnect(spec)), - Match.when("ProjectGitConnect", () => updateProjectGitConnect(spec)), - Match.when("ProjectGitDisconnect", () => updateProjectGitDisconnect(spec)), - Match.when("ProjectClaudeConnect", () => updateProjectClaudeConnect(spec)), - Match.when("ProjectClaudeDisconnect", () => updateProjectClaudeDisconnect(spec)), - Match.when("ProjectGeminiConnect", () => updateProjectGeminiConnect(spec)), - Match.when("ProjectGeminiDisconnect", () => updateProjectGeminiDisconnect(spec)), - Match.exhaustive - ) diff --git a/packages/app/src/docker-git/menu-project-auth-shared.ts b/packages/app/src/docker-git/menu-project-auth-shared.ts new file mode 100644 index 00000000..6250fe5f --- /dev/null +++ b/packages/app/src/docker-git/menu-project-auth-shared.ts @@ -0,0 +1,76 @@ +import { Match } from "effect" + +import type { ProjectAuthFlow } from "./menu-types.js" + +export type ProjectAuthMenuAction = ProjectAuthFlow | "Refresh" | "Back" + +export type ProjectAuthPromptStep = { + readonly key: "label" + readonly label: string + readonly required: boolean + readonly secret: boolean +} + +type ProjectAuthMenuItem = { + readonly action: ProjectAuthMenuAction + readonly label: string +} + +const projectAuthMenuItems: ReadonlyArray = [ + { action: "ProjectGithubConnect", label: "Project: GitHub connect label" }, + { action: "ProjectGithubDisconnect", label: "Project: GitHub disconnect" }, + { action: "ProjectGitConnect", label: "Project: Git connect label" }, + { action: "ProjectGitDisconnect", label: "Project: Git disconnect" }, + { action: "ProjectClaudeConnect", label: "Project: Claude connect label" }, + { action: "ProjectClaudeDisconnect", label: "Project: Claude disconnect" }, + { action: "ProjectGeminiConnect", label: "Project: Gemini connect label" }, + { action: "ProjectGeminiDisconnect", label: "Project: Gemini disconnect" }, + { action: "Refresh", label: "Refresh snapshot" }, + { action: "Back", label: "Back to main menu" } +] + +const flowSteps: Readonly>> = { + ProjectGithubConnect: [ + { key: "label", label: "Label (empty = default)", required: false, secret: false } + ], + ProjectGithubDisconnect: [], + ProjectGitConnect: [ + { key: "label", label: "Label (empty = default)", required: false, secret: false } + ], + ProjectGitDisconnect: [], + ProjectClaudeConnect: [ + { key: "label", label: "Label (empty = default)", required: false, secret: false } + ], + ProjectClaudeDisconnect: [], + ProjectGeminiConnect: [ + { key: "label", label: "Label (empty = default)", required: false, secret: false } + ], + ProjectGeminiDisconnect: [] +} + +export const projectAuthSuccessMessage = ( + flow: ProjectAuthFlow, + label: string +): string => + Match.value(flow).pipe( + Match.when("ProjectGithubConnect", () => `Connected GitHub label (${label}) to project.`), + Match.when("ProjectGithubDisconnect", () => "Disconnected GitHub from project."), + Match.when("ProjectGitConnect", () => `Connected Git label (${label}) to project.`), + Match.when("ProjectGitDisconnect", () => "Disconnected Git from project."), + Match.when("ProjectClaudeConnect", () => `Connected Claude label (${label}) to project.`), + Match.when("ProjectClaudeDisconnect", () => "Disconnected Claude from project."), + Match.when("ProjectGeminiConnect", () => `Connected Gemini label (${label}) to project.`), + Match.when("ProjectGeminiDisconnect", () => "Disconnected Gemini from project."), + Match.exhaustive + ) + +export const projectAuthViewSteps = (flow: ProjectAuthFlow): ReadonlyArray => flowSteps[flow] + +export const projectAuthMenuLabels = (): ReadonlyArray => projectAuthMenuItems.map((item) => item.label) + +export const projectAuthMenuActionByIndex = (index: number): ProjectAuthMenuAction | null => { + const item = projectAuthMenuItems[index] + return item ? item.action : null +} + +export const projectAuthMenuSize = (): number => projectAuthMenuItems.length diff --git a/packages/app/src/docker-git/menu-project-auth.ts b/packages/app/src/docker-git/menu-project-auth.ts index 6b9b50cc..d355e64a 100644 --- a/packages/app/src/docker-git/menu-project-auth.ts +++ b/packages/app/src/docker-git/menu-project-auth.ts @@ -1,14 +1,13 @@ -import { Effect, Match, pipe } from "effect" - -import type { AppError } from "@lib/usecases/errors" -import type { ProjectItem } from "@lib/usecases/projects" +import { Effect, pipe } from "effect" import { nextBufferValue } from "./menu-buffer-input.js" +import type { MenuError } from "./menu-errors.js" import { handleMenuNumberInput, submitPromptStep } from "./menu-input-utils.js" import { type ProjectAuthMenuAction, projectAuthMenuActionByIndex, projectAuthMenuSize, + projectAuthSuccessMessage, projectAuthViewSteps, readProjectAuthSnapshot, writeProjectAuthFlow @@ -23,6 +22,7 @@ import type { ProjectAuthSnapshot, ViewState } from "./menu-types.js" +import type { ProjectItem } from "./project-item.js" type ProjectAuthContext = Pick & { readonly runner: MenuRunner @@ -62,7 +62,7 @@ const startProjectAuthPrompt = ( const loadProjectAuthMenuView = ( project: ProjectItem, context: Pick -): Effect.Effect => +): Effect.Effect => pipe( readProjectAuthSnapshot(project), Effect.tap((snapshot) => @@ -73,19 +73,6 @@ const loadProjectAuthMenuView = ( Effect.asVoid ) -const successMessage = (flow: ProjectAuthFlow, label: string): string => - Match.value(flow).pipe( - Match.when("ProjectGithubConnect", () => `Connected GitHub label (${label}) to project.`), - Match.when("ProjectGithubDisconnect", () => "Disconnected GitHub from project."), - Match.when("ProjectGitConnect", () => `Connected Git label (${label}) to project.`), - Match.when("ProjectGitDisconnect", () => "Disconnected Git from project."), - Match.when("ProjectClaudeConnect", () => `Connected Claude label (${label}) to project.`), - Match.when("ProjectClaudeDisconnect", () => "Disconnected Claude from project."), - Match.when("ProjectGeminiConnect", () => `Connected Gemini label (${label}) to project.`), - Match.when("ProjectGeminiDisconnect", () => "Disconnected Gemini from project."), - Match.exhaustive - ) - const runProjectAuthEffect = ( project: ProjectItem, flow: ProjectAuthFlow, @@ -100,7 +87,7 @@ const runProjectAuthEffect = ( Effect.tap((snapshot) => Effect.sync(() => { startProjectAuthMenu(project, snapshot, context) - context.setMessage(successMessage(flow, label)) + context.setMessage(projectAuthSuccessMessage(flow, label)) }) ), Effect.asVoid @@ -262,6 +249,13 @@ export const openProjectAuthMenu = (context: ProjectAuthContextWithProject): voi context.runner.runEffect(loadProjectAuthMenuView(context.project, context)) } +export const openProjectAuthSelection = ( + project: ProjectItem, + context: ProjectAuthContext +): void => { + openProjectAuthMenu({ project, ...context }) +} + export const handleProjectAuthInput = ( input: string, key: MenuKeyInput, diff --git a/packages/app/src/docker-git/menu-render-auth.ts b/packages/app/src/docker-git/menu-render-auth.ts index e4a249fe..7d0f0d56 100644 --- a/packages/app/src/docker-git/menu-render-auth.ts +++ b/packages/app/src/docker-git/menu-render-auth.ts @@ -1,6 +1,6 @@ -import { Box, Text } from "ink" import React from "react" +import { Box, Text } from "../ui/primitives.js" import { authMenuLabels, authViewSteps, authViewTitle } from "./menu-auth-data.js" import { renderMenuHelp, @@ -25,11 +25,11 @@ export const renderAuthMenu = ( [ el(Text, null, `Global env: ${snapshot.globalEnvPath}`), el(Text, null, `Claude auth: ${snapshot.claudeAuthPath}`), - el(Text, { color: "gray" }, renderCountLine("Entries", snapshot.totalEntries)), - el(Text, { color: "gray" }, renderCountLine("GitHub tokens", snapshot.githubTokenEntries)), - el(Text, { color: "gray" }, renderCountLine("Git tokens", snapshot.gitTokenEntries)), - el(Text, { color: "gray" }, renderCountLine("Git users", snapshot.gitUserEntries)), - el(Text, { color: "gray" }, renderCountLine("Claude logins", snapshot.claudeAuthEntries)), + el(Text, { fg: "gray" }, renderCountLine("Entries", snapshot.totalEntries)), + el(Text, { fg: "gray" }, renderCountLine("GitHub tokens", snapshot.githubTokenEntries)), + el(Text, { fg: "gray" }, renderCountLine("Git tokens", snapshot.gitTokenEntries)), + el(Text, { fg: "gray" }, renderCountLine("Git users", snapshot.gitUserEntries)), + el(Text, { fg: "gray" }, renderCountLine("Claude logins", snapshot.claudeAuthEntries)), el(Box, { flexDirection: "column", marginTop: 1 }, ...list), renderMenuHelp("Use arrows + Enter, or type a number.") ], @@ -52,9 +52,9 @@ export const renderAuthPrompt = ( return renderPromptLayout({ title: `docker-git / Auth / ${authViewTitle(view.flow)}`, header: [ - el(Text, { color: "gray" }, `Global env: ${view.snapshot.globalEnvPath}`), + el(Text, { fg: "gray" }, `Global env: ${view.snapshot.globalEnvPath}`), ...(view.flow === "ClaudeOauth" || view.flow === "ClaudeLogout" - ? [el(Text, { color: "gray" }, `Claude auth: ${view.snapshot.claudeAuthPath}`)] + ? [el(Text, { fg: "gray" }, `Claude auth: ${view.snapshot.claudeAuthPath}`)] : []) ], prompt, diff --git a/packages/app/src/docker-git/menu-render-common.ts b/packages/app/src/docker-git/menu-render-common.ts index bb50fbae..ac5858c2 100644 --- a/packages/app/src/docker-git/menu-render-common.ts +++ b/packages/app/src/docker-git/menu-render-common.ts @@ -1,31 +1,19 @@ -import { Box, Text } from "ink" import React from "react" -import { renderLayout } from "./menu-render-layout.js" +import { HelpLines, PromptScreen, SelectableList } from "../ui/shared.js" export const renderSelectableMenuList = ( labels: ReadonlyArray, selected: number ): ReadonlyArray => { - const el = React.createElement - return labels.map((label, index) => - el( - Text, - { key: `${index}-${label}`, color: index === selected ? "green" : "white" }, - `${index === selected ? ">" : " "} ${index + 1}) ${label}` - ) - ) + return SelectableList({ + labels: labels.map((label, index) => `${index + 1}) ${label}`), + selectedIndex: selected + }) } -export const renderMenuHelp = (primaryLine: string): React.ReactElement => { - const el = React.createElement - return el( - Box, - { marginTop: 1, flexDirection: "column" }, - el(Text, { color: "gray" }, primaryLine), - el(Text, { color: "gray" }, "Esc returns to the main menu.") - ) -} +export const renderMenuHelp = (primaryLine: string): React.ReactElement => + HelpLines({ lines: [primaryLine, "Esc returns to the main menu."] }) type PromptStepLike = { readonly label: string @@ -54,14 +42,12 @@ type RenderPromptArgs = { } export const renderPromptLayout = (args: RenderPromptArgs): React.ReactElement => { - const el = React.createElement - return renderLayout( - args.title, - [ - ...args.header, - el(Box, { marginTop: 1 }, el(Text, null, `${args.prompt}: `), el(Text, { color: "green" }, args.visibleBuffer)), - el(Box, { marginTop: 1, flexDirection: "column" }, el(Text, { color: "gray" }, args.helpLine)) - ], - args.message - ) + return React.createElement(PromptScreen, { + header: [...args.header], + helpLines: [args.helpLine], + message: args.message, + prompt: args.prompt, + title: args.title, + value: args.visibleBuffer + }) } diff --git a/packages/app/src/docker-git/menu-render-layout.ts b/packages/app/src/docker-git/menu-render-layout.ts index efb081b6..066c156b 100644 --- a/packages/app/src/docker-git/menu-render-layout.ts +++ b/packages/app/src/docker-git/menu-render-layout.ts @@ -1,30 +1,15 @@ -import { Box, Text } from "ink" import React from "react" -const renderMessage = (message: string | null): React.ReactElement | null => { - if (!message) { - return null - } - return React.createElement( - Box, - { marginTop: 1 }, - React.createElement(Text, { color: "magenta" }, message) - ) -} +import { ScreenLayout } from "../ui/shared.js" export const renderLayout = ( title: string, body: ReadonlyArray, message: string | null ): React.ReactElement => { - const el = React.createElement - const messageView = renderMessage(message) - const tail = messageView ? [messageView] : [] - return el( - Box, - { flexDirection: "column", padding: 1, borderStyle: "round" }, - el(Text, { color: "cyan", bold: true }, title), - ...body, - ...tail - ) + return React.createElement(ScreenLayout, { + body: [...body], + message, + title + }) } diff --git a/packages/app/src/docker-git/menu-render-project-auth.ts b/packages/app/src/docker-git/menu-render-project-auth.ts index 2f8fe4ae..c151f9d4 100644 --- a/packages/app/src/docker-git/menu-render-project-auth.ts +++ b/packages/app/src/docker-git/menu-render-project-auth.ts @@ -1,6 +1,6 @@ -import { Box, Text } from "ink" import React from "react" +import { Box, Text } from "../ui/primitives.js" import { projectAuthMenuLabels, projectAuthViewSteps } from "./menu-project-auth-data.js" import { renderMenuHelp, @@ -27,19 +27,19 @@ export const renderProjectAuthMenu = ( "docker-git / Project auth", [ el(Text, null, `Project: ${snapshot.projectName}`), - el(Text, { color: "gray" }, `Dir: ${snapshot.projectDir}`), - el(Text, { color: "gray" }, `Project env: ${snapshot.envProjectPath}`), - el(Text, { color: "gray" }, `Global env: ${snapshot.envGlobalPath}`), - el(Text, { color: "gray" }, `Claude auth: ${snapshot.claudeAuthPath}`), + el(Text, { fg: "gray" }, `Dir: ${snapshot.projectDir}`), + el(Text, { fg: "gray" }, `Project env: ${snapshot.envProjectPath}`), + el(Text, { fg: "gray" }, `Global env: ${snapshot.envGlobalPath}`), + el(Text, { fg: "gray" }, `Claude auth: ${snapshot.claudeAuthPath}`), el( Box, { marginTop: 1, flexDirection: "column" }, - el(Text, { color: "gray" }, `GitHub label: ${renderActiveLabel(snapshot.activeGithubLabel)}`), - el(Text, { color: "gray" }, renderCountLine("Available GitHub tokens", snapshot.githubTokenEntries)), - el(Text, { color: "gray" }, `Git label: ${renderActiveLabel(snapshot.activeGitLabel)}`), - el(Text, { color: "gray" }, renderCountLine("Available Git tokens", snapshot.gitTokenEntries)), - el(Text, { color: "gray" }, `Claude label: ${renderActiveLabel(snapshot.activeClaudeLabel)}`), - el(Text, { color: "gray" }, renderCountLine("Available Claude logins", snapshot.claudeAuthEntries)) + el(Text, { fg: "gray" }, `GitHub label: ${renderActiveLabel(snapshot.activeGithubLabel)}`), + el(Text, { fg: "gray" }, renderCountLine("Available GitHub tokens", snapshot.githubTokenEntries)), + el(Text, { fg: "gray" }, `Git label: ${renderActiveLabel(snapshot.activeGitLabel)}`), + el(Text, { fg: "gray" }, renderCountLine("Available Git tokens", snapshot.gitTokenEntries)), + el(Text, { fg: "gray" }, `Claude label: ${renderActiveLabel(snapshot.activeClaudeLabel)}`), + el(Text, { fg: "gray" }, renderCountLine("Available Claude logins", snapshot.claudeAuthEntries)) ), el(Box, { flexDirection: "column", marginTop: 1 }, ...list), renderMenuHelp("Use arrows + Enter, or type a number from the list.") @@ -58,9 +58,9 @@ export const renderProjectAuthPrompt = ( return renderPromptLayout({ title: "docker-git / Project auth / Set label", header: [ - el(Text, { color: "gray" }, `Project: ${view.snapshot.projectName}`), - el(Text, { color: "gray" }, `Project env: ${view.snapshot.envProjectPath}`), - el(Text, { color: "gray" }, `Global env: ${view.snapshot.envGlobalPath}`) + el(Text, { fg: "gray" }, `Project: ${view.snapshot.projectName}`), + el(Text, { fg: "gray" }, `Project env: ${view.snapshot.envProjectPath}`), + el(Text, { fg: "gray" }, `Global env: ${view.snapshot.envGlobalPath}`) ], prompt, visibleBuffer, diff --git a/packages/app/src/docker-git/menu-render-select.ts b/packages/app/src/docker-git/menu-render-select.ts index a7ec8b43..fb17755b 100644 --- a/packages/app/src/docker-git/menu-render-select.ts +++ b/packages/app/src/docker-git/menu-render-select.ts @@ -1,210 +1,31 @@ -import { Match } from "effect" -import { Text } from "ink" import type React from "react" -import type { ProjectItem } from "@lib/usecases/projects" +import { Text } from "../ui/primitives.js" +import { buildSelectDetailsModel, type SelectPurpose } from "./menu-select-presenter.js" import type { SelectProjectRuntime } from "./menu-types.js" +import type { ProjectItem } from "./project-item.js" -export type SelectPurpose = "Connect" | "Down" | "Info" | "Delete" | "Auth" - -const formatRepoRef = (repoRef: string): string => { - const trimmed = repoRef.trim() - const prPrefix = "refs/pull/" - if (trimmed.startsWith(prPrefix)) { - const rest = trimmed.slice(prPrefix.length) - const number = rest.split("/")[0] ?? rest - return `PR#${number}` - } - return trimmed.length > 0 ? trimmed : "main" -} - -const stoppedRuntime = (): SelectProjectRuntime => ({ - running: false, - sshSessions: 0, - startedAtIso: null, - startedAtEpochMs: null -}) - -const pad2 = (value: number): string => value.toString().padStart(2, "0") - -const formatUtcTimestamp = (epochMs: number, withSeconds: boolean): string => { - const date = new Date(epochMs) - const seconds = withSeconds ? `:${pad2(date.getUTCSeconds())}` : "" - return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ${ - pad2( - date.getUTCHours() - ) - }:${pad2(date.getUTCMinutes())}${seconds} UTC` -} - -const renderStartedAtCompact = (runtime: SelectProjectRuntime): string => - runtime.startedAtEpochMs === null ? "-" : formatUtcTimestamp(runtime.startedAtEpochMs, false) - -const renderStartedAtDetailed = (runtime: SelectProjectRuntime): string => - runtime.startedAtEpochMs === null ? "not available" : formatUtcTimestamp(runtime.startedAtEpochMs, true) - -const runtimeForProject = ( - runtimeByProject: Readonly>, - item: ProjectItem -): SelectProjectRuntime => runtimeByProject[item.projectDir] ?? stoppedRuntime() - -const renderRuntimeLabel = (runtime: SelectProjectRuntime): string => - `${runtime.running ? "running" : "stopped"}, ssh=${runtime.sshSessions}, started=${ - renderStartedAtCompact( - runtime - ) - }` - -export const selectTitle = (purpose: SelectPurpose): string => - Match.value(purpose).pipe( - Match.when("Connect", () => "docker-git / Select project"), - Match.when("Auth", () => "docker-git / Project auth"), - Match.when("Down", () => "docker-git / Stop container"), - Match.when("Info", () => "docker-git / Show connection info"), - Match.when("Delete", () => "docker-git / Delete project"), - Match.exhaustive - ) - -export const selectHint = ( - purpose: SelectPurpose, - _connectEnableMcpPlaywright: boolean -): string => - Match.value(purpose).pipe( - Match.when("Connect", () => "Enter = select + SSH, Esc = back"), - Match.when("Auth", () => "Enter = open project auth menu, Esc = back"), - Match.when("Down", () => "Enter = stop container, Esc = back"), - Match.when("Info", () => "Use arrows to browse details, Enter = set active, Esc = back"), - Match.when("Delete", () => "Enter = ask/confirm delete, Esc = cancel"), - Match.exhaustive - ) - -export const buildSelectLabels = ( - items: ReadonlyArray, - selected: number, - purpose: SelectPurpose, - runtimeByProject: Readonly> -): ReadonlyArray => - items.map((item, index) => { - const prefix = index === selected ? ">" : " " - const refLabel = formatRepoRef(item.repoRef) - const hostLabel = item.clonedOnHostname === undefined ? "" : ` @${item.clonedOnHostname}` - const runtime = runtimeForProject(runtimeByProject, item) - const runtimeSuffix = purpose === "Down" || purpose === "Delete" - ? ` [${renderRuntimeLabel(runtime)}]` - : ` [started=${renderStartedAtCompact(runtime)}]` - return `${prefix} ${index + 1}. ${item.displayName} (${refLabel})${hostLabel}${runtimeSuffix}` - }) - -export type SelectListWindow = { - readonly start: number - readonly end: number +const computeListWidth = (labels: ReadonlyArray): number => { + const maxLabelWidth = labels.length > 0 ? Math.max(...labels.map((label) => label.length)) : 24 + return Math.min(Math.max(maxLabelWidth + 2, 28), 54) } -export const buildSelectListWindow = ( - total: number, - selected: number, - maxVisible: number -): SelectListWindow => { - if (total <= 0) { - return { start: 0, end: 0 } +const readStdoutRows = (): number | null => { + const rows = process.stdout.rows + if (typeof rows !== "number" || !Number.isFinite(rows) || rows <= 0) { + return null } - const visible = Math.max(1, maxVisible) - if (total <= visible) { - return { start: 0, end: total } - } - const boundedSelected = Math.min(Math.max(selected, 0), total - 1) - const half = Math.floor(visible / 2) - const maxStart = total - visible - const start = Math.min(Math.max(boundedSelected - half, 0), maxStart) - return { start, end: start + visible } -} - -type SelectDetailsContext = { - readonly item: ProjectItem - readonly refLabel: string - readonly authSuffix: string - readonly runtime: SelectProjectRuntime - readonly sshSessionsLabel: string + return rows } -const buildDetailsContext = ( - item: ProjectItem, - runtimeByProject: Readonly> -): SelectDetailsContext => { - const runtime = runtimeForProject(runtimeByProject, item) - return { - item, - refLabel: formatRepoRef(item.repoRef), - authSuffix: item.authorizedKeysExists ? "" : " (missing)", - runtime, - sshSessionsLabel: runtime.sshSessions === 1 - ? "1 active SSH session" - : `${runtime.sshSessions} active SSH sessions` +const computeSelectListMaxRows = (): number => { + const rows = readStdoutRows() + if (rows === null) { + return 12 } + return Math.max(6, rows - 14) } -const titleRow = (el: typeof React.createElement, value: string): React.ReactElement => - el(Text, { color: "cyan", bold: true, wrap: "truncate" }, value) - -const commonRows = ( - el: typeof React.createElement, - context: SelectDetailsContext -): ReadonlyArray => [ - el(Text, { wrap: "wrap" }, `Project directory: ${context.item.projectDir}`), - el(Text, { wrap: "wrap" }, `Container: ${context.item.containerName}`), - el(Text, { wrap: "wrap" }, `State: ${context.runtime.running ? "running" : "stopped"}`), - el(Text, { wrap: "wrap" }, `Started at: ${renderStartedAtDetailed(context.runtime)}`), - el(Text, { wrap: "wrap" }, `SSH sessions now: ${context.sshSessionsLabel}`) -] - -const renderInfoDetails = ( - el: typeof React.createElement, - context: SelectDetailsContext, - common: ReadonlyArray -): ReadonlyArray => [ - titleRow(el, "Connection info"), - ...common, - el(Text, { wrap: "wrap" }, `Service: ${context.item.serviceName}`), - el(Text, { wrap: "wrap" }, `SSH command: ${context.item.sshCommand}`), - el(Text, { wrap: "wrap" }, `Repo: ${context.item.repoUrl} (${context.refLabel})`), - el(Text, { wrap: "wrap" }, `Workspace: ${context.item.targetDir}`), - el(Text, { wrap: "wrap" }, `Authorized keys: ${context.item.authorizedKeysPath}${context.authSuffix}`), - el(Text, { wrap: "wrap" }, `Env global: ${context.item.envGlobalPath}`), - el(Text, { wrap: "wrap" }, `Env project: ${context.item.envProjectPath}`), - el(Text, { wrap: "wrap" }, `Codex auth: ${context.item.codexAuthPath} -> ${context.item.codexHome}`) -] - -const renderDefaultDetails = ( - el: typeof React.createElement, - context: SelectDetailsContext -): ReadonlyArray => [ - titleRow(el, "Details"), - el(Text, { wrap: "truncate" }, `Repo: ${context.item.repoUrl}`), - el(Text, { wrap: "truncate" }, `Ref: ${context.item.repoRef}`), - el(Text, { wrap: "truncate" }, `Project dir: ${context.item.projectDir}`), - el(Text, { wrap: "truncate" }, `Workspace: ${context.item.targetDir}`), - el(Text, { wrap: "truncate" }, `SSH: ${context.item.sshCommand}`) -] - -const renderConnectDetails = ( - el: typeof React.createElement, - context: SelectDetailsContext, - common: ReadonlyArray, - connectEnableMcpPlaywright: boolean -): ReadonlyArray => [ - titleRow(el, "Connect + SSH"), - ...common, - el( - Text, - { color: connectEnableMcpPlaywright ? "green" : "gray", wrap: "wrap" }, - connectEnableMcpPlaywright - ? "Playwright MCP: will be enabled before SSH (P to disable)." - : "Playwright MCP: keep current project setting (P to enable before SSH)." - ), - el(Text, { wrap: "wrap" }, `Repo: ${context.item.repoUrl} (${context.refLabel})`), - el(Text, { wrap: "wrap" }, `SSH command: ${context.item.sshCommand}`) -] - export const renderSelectDetails = ( el: typeof React.createElement, purpose: SelectPurpose, @@ -212,37 +33,27 @@ export const renderSelectDetails = ( runtimeByProject: Readonly>, connectEnableMcpPlaywright: boolean ): ReadonlyArray => { - if (!item) { - return [el(Text, { color: "gray", wrap: "truncate" }, "No project selected.")] - } - const context = buildDetailsContext(item, runtimeByProject) - const common = commonRows(el, context) - - return Match.value(purpose).pipe( - Match.when("Connect", () => renderConnectDetails(el, context, common, connectEnableMcpPlaywright)), - Match.when("Auth", () => [ - titleRow(el, "Project auth"), - ...common, - el(Text, { wrap: "wrap" }, `Repo: ${context.item.repoUrl} (${context.refLabel})`), - el(Text, { wrap: "wrap" }, `Env global: ${context.item.envGlobalPath}`), - el(Text, { wrap: "wrap" }, `Env project: ${context.item.envProjectPath}`), - el(Text, { color: "gray", wrap: "wrap" }, "Press Enter to manage labels for this project.") - ]), - Match.when("Info", () => renderInfoDetails(el, context, common)), - Match.when("Down", () => [ - titleRow(el, "Stop container"), - ...common, - el(Text, { wrap: "wrap" }, `Repo: ${context.item.repoUrl} (${context.refLabel})`) - ]), - Match.when("Delete", () => [ - titleRow(el, "Delete project"), - ...common, - context.runtime.sshSessions > 0 - ? el(Text, { color: "yellow", wrap: "wrap" }, "Warning: project has active SSH sessions.") - : el(Text, { color: "gray", wrap: "wrap" }, "No active SSH sessions detected."), - el(Text, { wrap: "wrap" }, `Repo: ${context.item.repoUrl} (${context.refLabel})`), - el(Text, { wrap: "wrap" }, "Removes project folder and runs docker compose down -v.") - ]), - Match.orElse(() => renderDefaultDetails(el, context)) - ) + const runtime = item === undefined + ? { running: false, sshSessions: 0, startedAtIso: null, startedAtEpochMs: null } + : (runtimeByProject[item.projectDir] ?? { + running: false, + sshSessions: 0, + startedAtIso: null, + startedAtEpochMs: null + }) + const details = buildSelectDetailsModel(purpose, item, runtime, connectEnableMcpPlaywright) + return [ + el(Text, { fg: "cyan", bold: true, wrap: "truncate" }, details.title), + ...details.lines.map((line, index) => el(Text, { key: `${details.title}-${index}`, wrap: "wrap" }, line)) + ] } + +export { computeListWidth, computeSelectListMaxRows } + +export { + buildSelectLabels, + buildSelectListWindow, + selectHint, + type SelectPurpose, + selectTitle +} from "./menu-select-presenter.js" diff --git a/packages/app/src/docker-git/menu-render.ts b/packages/app/src/docker-git/menu-render.ts index cd34d29e..0ae72a6e 100644 --- a/packages/app/src/docker-git/menu-render.ts +++ b/packages/app/src/docker-git/menu-render.ts @@ -1,19 +1,21 @@ -import { Match } from "effect" -import { Box, Text } from "ink" import React from "react" -import type { ProjectItem } from "@lib/usecases/projects" +import { Box, Text } from "../ui/primitives.js" +import { renderCreateStepLabel } from "./menu-create-shared.js" import { renderLayout } from "./menu-render-layout.js" import { buildSelectLabels, buildSelectListWindow, + computeListWidth, + computeSelectListMaxRows, renderSelectDetails, selectHint, type SelectPurpose, selectTitle } from "./menu-render-select.js" -import type { CreateInputs, CreateStep, SelectProjectRuntime } from "./menu-types.js" +import type { CreateInputs, SelectProjectRuntime } from "./menu-types.js" import { createSteps, menuItems } from "./menu-types.js" +import type { ProjectItem } from "./project-item.js" // CHANGE: render menu views with Ink without JSX // WHY: keep UI logic separate from input/state reducers @@ -26,25 +28,6 @@ import { createSteps, menuItems } from "./menu-types.js" // INVARIANT: menu renders all items once // COMPLEXITY: O(n) -export const renderStepLabel = (step: CreateStep, defaults: CreateInputs): string => - Match.value(step).pipe( - Match.when("repoUrl", () => "Repo URL (optional for empty workspace)"), - Match.when("repoRef", () => `Repo ref [${defaults.repoRef}]`), - Match.when("outDir", () => `Output dir [${defaults.outDir}]`), - Match.when("cpuLimit", () => `CPU limit [${defaults.cpuLimit || "30%"}]`), - Match.when("ramLimit", () => `RAM limit [${defaults.ramLimit || "30%"}]`), - Match.when("runUp", () => `Run docker compose up now? [${defaults.runUp ? "Y" : "n"}]`), - Match.when( - "mcpPlaywright", - () => `Enable Playwright MCP (Chromium sidecar)? [${defaults.enableMcpPlaywright ? "y" : "N"}]` - ), - Match.when( - "force", - () => `Force recreate (overwrite files + wipe volumes)? [${defaults.force ? "y" : "N"}]` - ), - Match.exhaustive - ) - const compactElements = ( items: ReadonlyArray ): ReadonlyArray => items.filter((item): item is React.ReactElement => item !== null) @@ -53,14 +36,14 @@ const renderMenuHints = (el: typeof React.createElement): React.ReactElement => el( Box, { marginTop: 1, flexDirection: "column" }, - el(Text, { color: "gray" }, "Hints:"), - el(Text, { color: "gray" }, " - Paste repo URL to create directly."), + el(Text, { fg: "gray" }, "Hints:"), + el(Text, { fg: "gray" }, " - Paste repo URL to create directly."), el( Text, - { color: "gray" }, + { fg: "gray" }, " - Aliases: create/c, select/s, auth/a, project-auth/pa, info/i, status/ps, logs/l, down/d, down-all/da, delete/del, quit/q" ), - el(Text, { color: "gray" }, " - Use arrows and Enter to run.") + el(Text, { fg: "gray" }, " - Use arrows and Enter to run.") ) const renderMenuMessage = ( @@ -75,7 +58,7 @@ const renderMenuMessage = ( { marginTop: 1, flexDirection: "column" }, ...message .split("\n") - .map((line, index) => el(Text, { key: `${index}-${line}`, color: "magenta" }, line)) + .map((line, index) => el(Text, { key: `${index}-${line}`, fg: "magenta" }, line)) ) } @@ -99,13 +82,13 @@ export const renderMenu = (input: MenuRenderInput): React.ReactElement => { const prefix = index === selected ? ">" : " " return el( Text, - { key: item.label, color: index === selected ? "green" : "white" }, + { key: item.label, fg: index === selected ? "green" : "white" }, `${prefix} ${indexLabel} ${item.label}` ) }) const busyView = busy - ? el(Box, { marginTop: 1 }, el(Text, { color: "yellow" }, "Running...")) + ? el(Box, { marginTop: 1 }, el(Text, { fg: "yellow" }, "Running...")) : null const messageView = renderMenuMessage(el, message) @@ -134,11 +117,14 @@ export const renderCreate = ( defaults: CreateInputs ): React.ReactElement => { const el = React.createElement + const hint = stepIndex === 0 + ? "Enter = create with defaults, Shift+Enter = advanced, Esc = cancel." + : "Enter = next, Esc = cancel." const steps = createSteps.map((step, index) => el( Text, - { key: step, color: index === stepIndex ? "green" : "gray" }, - `${index === stepIndex ? ">" : " "} ${renderStepLabel(step, defaults)}` + { key: step, fg: index === stepIndex ? "green" : "gray" }, + `${index === stepIndex ? ">" : " "} ${renderCreateStepLabel(step, defaults)}` ) ) return renderLayout( @@ -149,9 +135,9 @@ export const renderCreate = ( Box, { marginTop: 1 }, el(Text, null, `${label}: `), - el(Text, { color: "green" }, buffer) + el(Text, { fg: "green" }, buffer) ), - el(Box, { marginTop: 1 }, el(Text, { color: "gray" }, "Enter = next, Esc = cancel.")) + el(Box, { marginTop: 1 }, el(Text, { fg: "gray" }, hint)) ], message ) @@ -160,27 +146,6 @@ export const renderCreate = ( export { renderAuthMenu, renderAuthPrompt } from "./menu-render-auth.js" export { renderProjectAuthMenu, renderProjectAuthPrompt } from "./menu-render-project-auth.js" -const computeListWidth = (labels: ReadonlyArray): number => { - const maxLabelWidth = labels.length > 0 ? Math.max(...labels.map((label) => label.length)) : 24 - return Math.min(Math.max(maxLabelWidth + 2, 28), 54) -} - -const readStdoutRows = (): number | null => { - const rows = process.stdout.rows - if (typeof rows !== "number" || !Number.isFinite(rows) || rows <= 0) { - return null - } - return rows -} - -const computeSelectListMaxRows = (): number => { - const rows = readStdoutRows() - if (rows === null) { - return 12 - } - return Math.max(6, rows - 14) -} - const renderSelectListBox = ( el: typeof React.createElement, items: ReadonlyArray, @@ -198,7 +163,7 @@ const renderSelectListBox = ( Text, { key: items[index]?.projectDir ?? String(index), - color: index === selected ? "green" : "white", + fg: index === selected ? "green" : "white", wrap: "truncate" }, label @@ -206,12 +171,12 @@ const renderSelectListBox = ( }) const before = hiddenAbove > 0 - ? [el(Text, { color: "gray", wrap: "truncate" }, `[scroll] ${hiddenAbove} more above`)] + ? [el(Text, { fg: "gray", wrap: "truncate" }, `[scroll] ${hiddenAbove} more above`)] : [] const after = hiddenBelow > 0 - ? [el(Text, { color: "gray", wrap: "truncate" }, `[scroll] ${hiddenBelow} more below`)] + ? [el(Text, { fg: "gray", wrap: "truncate" }, `[scroll] ${hiddenBelow} more below`)] : [] - const listBody = list.length > 0 ? list : [el(Text, { color: "gray" }, "No projects found.")] + const listBody = list.length > 0 ? list : [el(Text, { fg: "gray" }, "No projects found.")] return el( Box, @@ -281,7 +246,7 @@ export const renderSelect = ( } return baseHint })() - const hints = el(Box, { marginTop: 1 }, el(Text, { color: "gray" }, confirmHint)) + const hints = el(Box, { marginTop: 1 }, el(Text, { fg: "gray" }, confirmHint)) return renderLayout( selectTitle(purpose), @@ -292,3 +257,5 @@ export const renderSelect = ( message ) } + +export { renderCreateStepLabel as renderStepLabel } from "./menu-create-shared.js" diff --git a/packages/app/src/docker-git/menu-select-actions.ts b/packages/app/src/docker-git/menu-select-actions.ts index 87ed2bf1..29303daa 100644 --- a/packages/app/src/docker-git/menu-select-actions.ts +++ b/packages/app/src/docker-git/menu-select-actions.ts @@ -1,8 +1,8 @@ -import type { ProjectItem } from "@lib/usecases/projects" import { Effect, pipe } from "effect" import { deleteMenuProject, downMenuProject, listMenuRunningProjectItems } from "./menu-api.js" import { renderMenuError } from "./menu-errors.js" +import { openProjectAuthSelection } from "./menu-project-auth.js" import { buildConnectEffect } from "./menu-select-connect.js" import { loadRuntimeByProject } from "./menu-select-runtime.js" import { startSelectView } from "./menu-select-view.js" @@ -15,6 +15,7 @@ import { } from "./menu-shared.js" import type { MenuRunner, MenuViewContext } from "./menu-types.js" import { openResolvedProjectSsh } from "./open-project.js" +import type { ProjectItem } from "./project-item.js" export type SelectContext = MenuViewContext & { readonly activeDir: string | null @@ -97,8 +98,8 @@ export const runInfoSelection = (selected: ProjectItem, context: SelectContext) context.setMessage(`Details for ${selected.displayName} are shown on the right. Press Esc to return to the menu.`) } -export const runAuthSelection = (_selected: ProjectItem, context: SelectContext) => { - context.setMessage("Project auth binding is not routed through the controller yet.") +export const runAuthSelection = (selected: ProjectItem, context: SelectContext) => { + openProjectAuthSelection(selected, context) } export const runDeleteSelection = (selected: ProjectItem, context: SelectContext) => { diff --git a/packages/app/src/docker-git/menu-select-connect.ts b/packages/app/src/docker-git/menu-select-connect.ts index f4eb49b1..e0673237 100644 --- a/packages/app/src/docker-git/menu-select-connect.ts +++ b/packages/app/src/docker-git/menu-select-connect.ts @@ -1,6 +1,6 @@ import { Effect } from "effect" -import type { ProjectItem } from "@lib/usecases/projects" +import type { ProjectItem } from "./project-item.js" type ConnectDeps = { readonly connectWithUp: ( diff --git a/packages/app/src/docker-git/menu-select-load.ts b/packages/app/src/docker-git/menu-select-load.ts index cd749e47..192132dd 100644 --- a/packages/app/src/docker-git/menu-select-load.ts +++ b/packages/app/src/docker-git/menu-select-load.ts @@ -1,9 +1,9 @@ -import type { ProjectItem } from "@lib/usecases/projects" import { Effect, pipe } from "effect" import { loadRuntimeByProject } from "./menu-select-runtime.js" import { startSelectView } from "./menu-select.js" import type { MenuEnv, MenuViewContext } from "./menu-types.js" +import type { ProjectItem } from "./project-item.js" export const loadSelectView = ( effect: Effect.Effect, E, MenuEnv>, diff --git a/packages/app/src/docker-git/menu-select-order.ts b/packages/app/src/docker-git/menu-select-order.ts index 44bc0190..82d05479 100644 --- a/packages/app/src/docker-git/menu-select-order.ts +++ b/packages/app/src/docker-git/menu-select-order.ts @@ -1,4 +1,4 @@ -import type { ProjectItem } from "@lib/usecases/projects" +import type { ProjectItem } from "./project-item.js" import type { SelectProjectRuntime } from "./menu-types.js" diff --git a/packages/app/src/docker-git/menu-select-presenter.ts b/packages/app/src/docker-git/menu-select-presenter.ts new file mode 100644 index 00000000..70d62806 --- /dev/null +++ b/packages/app/src/docker-git/menu-select-presenter.ts @@ -0,0 +1,247 @@ +import { Match } from "effect" + +import type { SelectProjectRuntime } from "./menu-types.js" + +export type SelectPurpose = "Connect" | "Down" | "Info" | "Delete" | "Auth" + +export type SelectListProject = { + readonly displayName: string + readonly repoRef: string + readonly projectDir: string + readonly clonedOnHostname?: string | undefined +} + +export type SelectDetailProject = SelectListProject & { + readonly repoUrl: string + readonly containerName: string + readonly serviceName: string + readonly sshCommand: string + readonly targetDir: string + readonly authorizedKeysPath: string + readonly authorizedKeysExists: boolean + readonly envGlobalPath: string + readonly envProjectPath: string + readonly codexAuthPath: string + readonly codexHome: string +} + +export type SelectDetailsModel = { + readonly title: string + readonly lines: ReadonlyArray +} + +type SelectDetailsContext = { + readonly authSuffix: string + readonly common: ReadonlyArray + readonly connectEnableMcpPlaywright: boolean + readonly item: SelectDetailProject + readonly refLabel: string + readonly runtime: SelectProjectRuntime +} + +const formatRepoRef = (repoRef: string): string => { + const trimmed = repoRef.trim() + const prPrefix = "refs/pull/" + if (trimmed.startsWith(prPrefix)) { + const rest = trimmed.slice(prPrefix.length) + const number = rest.split("/")[0] ?? rest + return `PR#${number}` + } + return trimmed.length > 0 ? trimmed : "main" +} + +export const stoppedRuntime = (): SelectProjectRuntime => ({ + running: false, + sshSessions: 0, + startedAtIso: null, + startedAtEpochMs: null +}) + +const pad2 = (value: number): string => value.toString().padStart(2, "0") + +const formatUtcTimestamp = (epochMs: number, withSeconds: boolean): string => { + const date = new Date(epochMs) + const seconds = withSeconds ? `:${pad2(date.getUTCSeconds())}` : "" + return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ${ + pad2( + date.getUTCHours() + ) + }:${pad2(date.getUTCMinutes())}${seconds} UTC` +} + +const renderStartedAtCompact = (runtime: SelectProjectRuntime): string => + runtime.startedAtEpochMs === null ? "-" : formatUtcTimestamp(runtime.startedAtEpochMs, false) + +const renderStartedAtDetailed = (runtime: SelectProjectRuntime): string => + runtime.startedAtEpochMs === null ? "not available" : formatUtcTimestamp(runtime.startedAtEpochMs, true) + +const renderRuntimeLabel = (runtime: SelectProjectRuntime): string => + `${runtime.running ? "running" : "stopped"}, ssh=${runtime.sshSessions}, started=${ + renderStartedAtCompact( + runtime + ) + }` + +export const selectTitle = (purpose: SelectPurpose): string => + Match.value(purpose).pipe( + Match.when("Connect", () => "docker-git / Select project"), + Match.when("Auth", () => "docker-git / Project auth"), + Match.when("Down", () => "docker-git / Stop container"), + Match.when("Info", () => "docker-git / Show connection info"), + Match.when("Delete", () => "docker-git / Delete project"), + Match.exhaustive + ) + +export const selectHint = ( + purpose: SelectPurpose, + _connectEnableMcpPlaywright: boolean +): string => + Match.value(purpose).pipe( + Match.when("Connect", () => "Enter = select + SSH, Esc = back"), + Match.when("Auth", () => "Enter = open project auth menu, Esc = back"), + Match.when("Down", () => "Enter = stop container, Esc = back"), + Match.when("Info", () => "Use arrows to browse details, Enter = set active, Esc = back"), + Match.when("Delete", () => "Enter = ask/confirm delete, Esc = cancel"), + Match.exhaustive + ) + +export const buildSelectLabels = ( + items: ReadonlyArray, + selected: number, + purpose: SelectPurpose, + runtimeByProject: Readonly> +): ReadonlyArray => + items.map((item, index) => { + const prefix = index === selected ? ">" : " " + const refLabel = formatRepoRef(item.repoRef) + const hostLabel = item.clonedOnHostname === undefined ? "" : ` @${item.clonedOnHostname}` + const runtime = runtimeByProject[item.projectDir] ?? stoppedRuntime() + const runtimeSuffix = purpose === "Down" || purpose === "Delete" + ? ` [${renderRuntimeLabel(runtime)}]` + : ` [started=${renderStartedAtCompact(runtime)}]` + return `${prefix} ${index + 1}. ${item.displayName} (${refLabel})${hostLabel}${runtimeSuffix}` + }) + +export type SelectListWindow = { + readonly start: number + readonly end: number +} + +export const buildSelectListWindow = ( + total: number, + selected: number, + maxVisible: number +): SelectListWindow => { + if (total <= 0) { + return { start: 0, end: 0 } + } + const visible = Math.max(1, maxVisible) + if (total <= visible) { + return { start: 0, end: total } + } + const boundedSelected = Math.min(Math.max(selected, 0), total - 1) + const half = Math.floor(visible / 2) + const maxStart = total - visible + const start = Math.min(Math.max(boundedSelected - half, 0), maxStart) + return { start, end: start + visible } +} + +const buildSshSessionsLabel = (runtime: SelectProjectRuntime): string => + runtime.sshSessions === 1 + ? "1 active SSH session" + : `${runtime.sshSessions} active SSH sessions` + +const commonLines = ( + item: SelectDetailProject, + runtime: SelectProjectRuntime +): ReadonlyArray => [ + `Project directory: ${item.projectDir}`, + `Container: ${item.containerName}`, + `State: ${runtime.running ? "running" : "stopped"}`, + `Started at: ${renderStartedAtDetailed(runtime)}`, + `SSH sessions now: ${buildSshSessionsLabel(runtime)}` +] + +const connectDetails = (context: SelectDetailsContext): SelectDetailsModel => ({ + title: "Connect + SSH", + lines: [ + ...context.common, + context.connectEnableMcpPlaywright + ? "Playwright MCP: will be enabled before SSH (P to disable)." + : "Playwright MCP: keep current project setting (P to enable before SSH).", + `Repo: ${context.item.repoUrl} (${context.refLabel})`, + `SSH command: ${context.item.sshCommand}` + ] +}) + +const authDetails = (context: SelectDetailsContext): SelectDetailsModel => ({ + title: "Project auth", + lines: [ + ...context.common, + `Repo: ${context.item.repoUrl} (${context.refLabel})`, + `Env global: ${context.item.envGlobalPath}`, + `Env project: ${context.item.envProjectPath}`, + "Press Enter to manage labels for this project." + ] +}) + +const infoDetails = (context: SelectDetailsContext): SelectDetailsModel => ({ + title: "Connection info", + lines: [ + ...context.common, + `Service: ${context.item.serviceName}`, + `SSH command: ${context.item.sshCommand}`, + `Repo: ${context.item.repoUrl} (${context.refLabel})`, + `Workspace: ${context.item.targetDir}`, + `Authorized keys: ${context.item.authorizedKeysPath}${context.authSuffix}`, + `Env global: ${context.item.envGlobalPath}`, + `Env project: ${context.item.envProjectPath}`, + `Codex auth: ${context.item.codexAuthPath} -> ${context.item.codexHome}` + ] +}) + +const downDetails = (context: SelectDetailsContext): SelectDetailsModel => ({ + title: "Stop container", + lines: [...context.common, `Repo: ${context.item.repoUrl} (${context.refLabel})`] +}) + +const deleteDetails = (context: SelectDetailsContext): SelectDetailsModel => ({ + title: "Delete project", + lines: [ + ...context.common, + context.runtime.sshSessions > 0 + ? "Warning: project has active SSH sessions." + : "No active SSH sessions detected.", + `Repo: ${context.item.repoUrl} (${context.refLabel})`, + "Removes project folder and runs docker compose down -v." + ] +}) + +export const buildSelectDetailsModel = ( + purpose: SelectPurpose, + item: SelectDetailProject | undefined, + runtime: SelectProjectRuntime, + connectEnableMcpPlaywright: boolean +): SelectDetailsModel => { + if (item === undefined) { + return { title: "No project selected", lines: ["No project selected."] } + } + + const context: SelectDetailsContext = { + authSuffix: item.authorizedKeysExists ? "" : " (missing)", + common: commonLines(item, runtime), + connectEnableMcpPlaywright, + item, + refLabel: formatRepoRef(item.repoRef), + runtime + } + + return Match.value(purpose).pipe( + Match.when("Connect", () => connectDetails(context)), + Match.when("Auth", () => authDetails(context)), + Match.when("Info", () => infoDetails(context)), + Match.when("Down", () => downDetails(context)), + Match.when("Delete", () => deleteDetails(context)), + Match.exhaustive + ) +} diff --git a/packages/app/src/docker-git/menu-select-runtime.ts b/packages/app/src/docker-git/menu-select-runtime.ts index 27c3727e..8ec1753e 100644 --- a/packages/app/src/docker-git/menu-select-runtime.ts +++ b/packages/app/src/docker-git/menu-select-runtime.ts @@ -1,11 +1,7 @@ -import { runCommandCapture } from "@lib/shell/command-runner" -import { runDockerPsNames } from "@lib/shell/docker" -import type { ProjectItem } from "@lib/usecases/projects" -import { Effect, pipe } from "effect" +import { Effect } from "effect" import type { MenuEnv, SelectProjectRuntime, ViewState } from "./menu-types.js" - -const emptyRuntimeByProject = (): Readonly> => ({}) +import type { ProjectItem } from "./project-item.js" const stoppedRuntime = (): SelectProjectRuntime => ({ running: false, @@ -14,37 +10,6 @@ const stoppedRuntime = (): SelectProjectRuntime => ({ startedAtEpochMs: null }) -const countSshSessionsScript = "who -u 2>/dev/null | wc -l | tr -d '[:space:]'" -const dockerZeroStartedAt = "0001-01-01T00:00:00Z" - -type ContainerStartTime = { - readonly startedAtIso: string - readonly startedAtEpochMs: number -} - -const parseSshSessionCount = (raw: string): number => { - const parsed = Number.parseInt(raw.trim(), 10) - if (Number.isNaN(parsed) || parsed < 0) { - return 0 - } - return parsed -} - -const parseContainerStartedAt = (raw: string): ContainerStartTime | null => { - const trimmed = raw.trim() - if (trimmed.length === 0 || trimmed === dockerZeroStartedAt) { - return null - } - const startedAtEpochMs = Date.parse(trimmed) - if (Number.isNaN(startedAtEpochMs)) { - return null - } - return { - startedAtIso: trimmed, - startedAtEpochMs - } -} - const toRuntimeMap = ( entries: ReadonlyArray ): Readonly> => { @@ -55,86 +20,31 @@ const toRuntimeMap = ( return runtimeByProject } -const countContainerSshSessions = ( - containerName: string -): Effect.Effect => - pipe( - runCommandCapture( - { - cwd: process.cwd(), - command: "docker", - args: ["exec", containerName, "bash", "-lc", countSshSessionsScript] - }, - [0], - (exitCode) => ({ _tag: "CommandFailedError", command: "docker exec who -u", exitCode }) - ), - Effect.match({ - onFailure: () => 0, - onSuccess: (raw) => parseSshSessionCount(raw) - }) - ) - -const inspectContainerStartedAt = ( - containerName: string -): Effect.Effect => - pipe( - runCommandCapture( - { - cwd: process.cwd(), - command: "docker", - args: ["inspect", "--format", "{{.State.StartedAt}}", containerName] - }, - [0], - (exitCode) => ({ _tag: "CommandFailedError", command: "docker inspect .State.StartedAt", exitCode }) - ), - Effect.match({ - onFailure: () => null, - onSuccess: (raw) => parseContainerStartedAt(raw) - }) - ) - // CHANGE: enrich select items with runtime state and SSH session counts // WHY: prevent stopping/deleting containers that are currently used via SSH // QUOTE(ТЗ): "писать скок SSH подключений к контейнеру сейчас" // REF: issue-47 // SOURCE: n/a -// FORMAT THEOREM: forall p: runtime(p) -> {running(p), ssh_sessions(p), started_at(p)} -// PURITY: SHELL +// FORMAT THEOREM: forall p: api_runtime(p) -> {running(p), ssh_sessions(p), started_at(p)} +// PURITY: CORE // EFFECT: Effect, never, MenuEnv> -// INVARIANT: projects without a known container start have startedAt = null -// COMPLEXITY: O(n + docker_ps + docker_exec + docker_inspect) +// INVARIANT: runtime map is derived only from API payload already loaded for the view +// COMPLEXITY: O(n) export const loadRuntimeByProject = ( items: ReadonlyArray ): Effect.Effect>, never, MenuEnv> => - pipe( - runDockerPsNames(process.cwd()), - Effect.flatMap((runningNames) => - Effect.forEach( - items, - (item) => { - const running = runningNames.includes(item.containerName) - const sshSessionsEffect = running - ? countContainerSshSessions(item.containerName) - : Effect.succeed(0) - return pipe( - Effect.all([sshSessionsEffect, inspectContainerStartedAt(item.containerName)]), - Effect.map(([sshSessions, startedAt]): SelectProjectRuntime => ({ - running, - sshSessions, - startedAtIso: startedAt?.startedAtIso ?? null, - startedAtEpochMs: startedAt?.startedAtEpochMs ?? null - })), - Effect.map((runtime): readonly [string, SelectProjectRuntime] => [item.projectDir, runtime]) - ) - }, - { concurrency: 4 } - ) - ), - Effect.map((entries) => toRuntimeMap(entries)), - Effect.match({ - onFailure: () => emptyRuntimeByProject(), - onSuccess: (runtimeByProject) => runtimeByProject - }) + Effect.succeed( + toRuntimeMap( + items.map((item): readonly [string, SelectProjectRuntime] => [ + item.projectDir, + { + running: item.status === "running", + sshSessions: item.sshSessions, + startedAtIso: item.startedAtIso, + startedAtEpochMs: item.startedAtEpochMs + } + ]) + ) ) export const runtimeForSelection = ( diff --git a/packages/app/src/docker-git/menu-select-view.ts b/packages/app/src/docker-git/menu-select-view.ts index d40c8180..5ca2a771 100644 --- a/packages/app/src/docker-git/menu-select-view.ts +++ b/packages/app/src/docker-git/menu-select-view.ts @@ -1,4 +1,4 @@ -import type { ProjectItem } from "@lib/usecases/projects" +import type { ProjectItem } from "./project-item.js" import { sortItemsByLaunchTime } from "./menu-select-order.js" import type { MenuViewContext, SelectProjectRuntime } from "./menu-types.js" diff --git a/packages/app/src/docker-git/menu-startup.ts b/packages/app/src/docker-git/menu-startup.ts index f49139fd..e1ad08c0 100644 --- a/packages/app/src/docker-git/menu-startup.ts +++ b/packages/app/src/docker-git/menu-startup.ts @@ -1,4 +1,4 @@ -import type { ProjectItem } from "@lib/usecases/projects" +import type { ProjectItem } from "./project-item.js" export type MenuStartupSnapshot = { readonly activeDir: string | null @@ -44,10 +44,13 @@ const renderRunningHint = (runningCount: number): string => // INVARIANT: activeDir is set only when exactly one known project is running // COMPLEXITY: O(|containers| + |projects|) export const resolveMenuStartupSnapshot = ( - items: ReadonlyArray, - runningContainerNames: ReadonlyArray + items: ReadonlyArray ): MenuStartupSnapshot => { - const runningDockerGitNames = uniqueDockerGitContainerNames(runningContainerNames) + const runningDockerGitNames = uniqueDockerGitContainerNames( + items + .filter((item) => item.status === "running") + .map((item) => item.containerName) + ) if (runningDockerGitNames.length === 0) { return emptySnapshot() } diff --git a/packages/app/src/docker-git/menu-types.ts b/packages/app/src/docker-git/menu-types.ts index 16886f9d..fb237355 100644 --- a/packages/app/src/docker-git/menu-types.ts +++ b/packages/app/src/docker-git/menu-types.ts @@ -3,9 +3,8 @@ import type * as FileSystem from "@effect/platform/FileSystem" import type * as Path from "@effect/platform/Path" import type * as Effect from "effect/Effect" -import type { MenuAction } from "@lib/core/domain" -import type { ProjectItem } from "@lib/usecases/projects" import type { MenuError } from "./menu-errors.js" +import type { ProjectItem } from "./project-item.js" // CHANGE: isolate TUI types/constants into a shared module // WHY: keep menu rendering and input handling small and focused @@ -40,6 +39,7 @@ export type MenuKeyInput = { readonly downArrow?: boolean readonly return?: boolean readonly escape?: boolean + readonly shift?: boolean readonly backspace?: boolean readonly delete?: boolean } @@ -171,12 +171,27 @@ export type SelectProjectRuntime = { readonly startedAtEpochMs: number | null } +export type MenuAction = + | { readonly _tag: "Create" } + | { readonly _tag: "Select" } + | { readonly _tag: "Auth" } + | { readonly _tag: "ProjectAuth" } + | { readonly _tag: "Info" } + | { readonly _tag: "Up" } + | { readonly _tag: "Status" } + | { readonly _tag: "Logs" } + | { readonly _tag: "Down" } + | { readonly _tag: "DownAll" } + | { readonly _tag: "Delete" } + | { readonly _tag: "Quit" } + export const menuItems: ReadonlyArray<{ readonly id: MenuAction; readonly label: string }> = [ { id: { _tag: "Create" }, label: "Create project" }, { id: { _tag: "Select" }, label: "Select project" }, { id: { _tag: "Auth" }, label: "Auth profiles (keys)" }, { id: { _tag: "ProjectAuth" }, label: "Project auth (bind labels)" }, { id: { _tag: "Info" }, label: "Show connection info" }, + { id: { _tag: "Up" }, label: "docker compose up" }, { id: { _tag: "Status" }, label: "docker compose ps" }, { id: { _tag: "Logs" }, label: "docker compose logs --tail=200" }, { id: { _tag: "Down" }, label: "docker compose down" }, diff --git a/packages/app/src/docker-git/menu.ts b/packages/app/src/docker-git/menu.ts index 9d9a14c3..3817b4e5 100644 --- a/packages/app/src/docker-git/menu.ts +++ b/packages/app/src/docker-git/menu.ts @@ -1,15 +1,15 @@ import { NodeContext } from "@effect/platform-node" -import { runDockerPsNames } from "@lib/shell/docker" -import { InputReadError } from "@lib/shell/errors" import { Effect, pipe } from "effect" -import { render, useApp, useInput } from "ink" import React, { useEffect, useMemo, useState } from "react" +import type { GridlandModule } from "@gridland/bun" + import { listMenuProjectItems, renderMenuProjectSummaries } from "./menu-api.js" -import { resolveCreateInputs } from "./menu-create.js" +import { renderCreateStepLabel, resolveCreateInputs } from "./menu-create-shared.js" import type { MenuError } from "./menu-errors.js" import { renderMenuError } from "./menu-errors.js" -import { handleUserInput, type InputStage } from "./menu-input-handler.js" +import { GridlandMenuProvider, runGridlandMenu, useGridlandMenuInput } from "./menu-gridland-runtime.js" +import type { InputStage } from "./menu-input-handler.js" import { renderAuthMenu, renderAuthPrompt, @@ -17,13 +17,17 @@ import { renderMenu, renderProjectAuthMenu, renderProjectAuthPrompt, - renderSelect, - renderStepLabel + renderSelect } from "./menu-render.js" -import { leaveTui, resumeTui } from "./menu-shared.js" +import { leaveTui } from "./menu-shared.js" import { defaultMenuStartupSnapshot, resolveMenuStartupSnapshot } from "./menu-startup.js" import { createSteps, type MenuEnv, type MenuState, type ViewState } from "./menu-types.js" +const gridlandBootstrapError = (message: string): MenuError => ({ + _tag: "TerminalSessionClientError", + message +}) + // CHANGE: keep menu state in the TUI layer // WHY: provide a dynamic interface with live selection and inputs // QUOTE(ТЗ): "TUI? Красивый, удобный" @@ -87,7 +91,7 @@ const renderView = (context: RenderContext) => { if (context.view._tag === "Create") { const currentDefaults = resolveCreateInputs(context.state.cwd, context.view.values) const step = createSteps[context.view.step] ?? "repoUrl" - const label = renderStepLabel(step, currentDefaults) + const label = renderCreateStepLabel(step, currentDefaults) return renderCreate(label, context.view.buffer, context.message, context.view.step, currentDefaults) } @@ -180,8 +184,8 @@ const useStartupSnapshot = ( let cancelled = false const startup = pipe( - Effect.all([listMenuProjectItems, runDockerPsNames(process.cwd())]), - Effect.map(([items, runningNames]) => resolveMenuStartupSnapshot(items, runningNames)), + listMenuProjectItems, + Effect.map((items) => resolveMenuStartupSnapshot(items)), Effect.match({ onFailure: (error: MenuError) => ({ ...defaultMenuStartupSnapshot(), @@ -224,56 +228,29 @@ const useSigintGuard = (exit: () => void, sshActive: boolean) => { }, [exit, sshActive]) } -const TuiApp = () => { - const { exit } = useApp() +const GridlandTuiApp = ({ exit, gridland }: { readonly exit: () => void; readonly gridland: GridlandModule }) => { const menu = useMenuState() useReadyGate(menu.setReady) useStartupSnapshot(menu.setActiveDir, menu.setRunningDockerGitContainers, menu.setMessage) useSigintGuard(exit, menu.sshActive) + useGridlandMenuInput(gridland, { ...menu, exit }) - useInput( - (input, key) => { - if (!menu.ready) { - return - } - if (Date.now() < menu.ignoreUntil) { - return - } - if (menu.skipInputs > 0) { - menu.setSkipInputs((value) => (value > 0 ? value - 1 : 0)) - return - } - handleUserInput(input, key, { - busy: menu.busy, + return React.createElement( + GridlandMenuProvider, + { + children: renderView({ + state: menu.state, view: menu.view, - inputStage: menu.inputStage, - setInputStage: menu.setInputStage, + activeDir: menu.activeDir, + runningDockerGitContainers: menu.runningDockerGitContainers, selected: menu.selected, - setSelected: menu.setSelected, - setSkipInputs: menu.setSkipInputs, - sshActive: menu.sshActive, - setSshActive: menu.setSshActive, - state: menu.state, - runner: menu.runner, - exit, - setView: menu.setView, - setMessage: menu.setMessage, - setActiveDir: menu.setActiveDir - }) - }, - { isActive: !menu.sshActive } + busy: menu.busy, + message: menu.message + }), + gridland + } ) - - return renderView({ - state: menu.state, - view: menu.view, - activeDir: menu.activeDir, - runningDockerGitContainers: menu.runningDockerGitContainers, - selected: menu.selected, - busy: menu.busy, - message: menu.message - }) } // CHANGE: provide an interactive TUI menu for docker-git @@ -288,25 +265,17 @@ const TuiApp = () => { // COMPLEXITY: O(1) per input // // CHANGE: guard against non-TTY environments (Docker without -t) -// WHY: Ink calls setRawMode(true) on mount — without a TTY stdin does not support -// raw mode, causing an unhandled error and a hang in waitUntilExit(). -// Fall back to listProjectStatus in non-interactive environments. +// WHY: interactive Gridland host still requires a real TTY; without one +// fall back to the project summary renderer. // QUOTE(ТЗ): "вечный цикл зависания на TUI из за ошибки Raw mode is not supported" // REF: issue-100 -// SOURCE: https://github.com/vadimdemedes/ink/#israwmodesupported +// SOURCE: n/a // FORMAT THEOREM: ∀ env: isTTY(env) → renderTui ∧ ¬isTTY(env) → listProjects(api) -// INVARIANT: render() is only called when stdin.isTTY ∧ setRawMode ∈ stdin +// INVARIANT: Gridland host only starts when stdin.isTTY ∧ stdout.isTTY const runInteractiveMenu = (): Effect.Effect => pipe( - Effect.sync(() => { - resumeTui() - }), - Effect.zipRight( - Effect.tryPromise({ - try: () => render(React.createElement(TuiApp)).waitUntilExit(), - catch: (error) => new InputReadError({ message: error instanceof Error ? error.message : String(error) }) - }) - ), + runGridlandMenu((args) => React.createElement(GridlandTuiApp, args)), + Effect.mapError((error) => gridlandBootstrapError(error.message)), Effect.ensuring( Effect.sync(() => { leaveTui() @@ -316,7 +285,7 @@ const runInteractiveMenu = (): Effect.Effect => ) export const runMenu: Effect.Effect = pipe( - Effect.sync(() => process.stdin.isTTY && typeof process.stdin.setRawMode === "function"), + Effect.sync(() => process.stdin.isTTY && process.stdout.isTTY), Effect.flatMap((hasTty) => (hasTty ? runInteractiveMenu() : renderMenuProjectSummaries())) ) diff --git a/packages/app/src/docker-git/open-project-ssh.ts b/packages/app/src/docker-git/open-project-ssh.ts new file mode 100644 index 00000000..dc511cd7 --- /dev/null +++ b/packages/app/src/docker-git/open-project-ssh.ts @@ -0,0 +1,56 @@ +import { Effect } from "effect" + +import { createProjectTerminalSession } from "./api-client.js" +import type { ApiTerminalSession } from "./api-terminal-codec.js" +import type { ControllerRuntime } from "./controller.js" +import type { HostError } from "./host-errors.js" +import type { ProjectItem } from "./project-item.js" +import { attachTerminalSession } from "./terminal-session-client.js" + +export type OpenResolvedProjectSshDeps = { + readonly createSession: ( + projectId: string + ) => Effect.Effect< + { + readonly project: Readonly> + readonly session: ApiTerminalSession + } | null, + HostError, + ControllerRuntime + > + readonly attach: ( + project: ProjectItem, + session: ApiTerminalSession + ) => Effect.Effect +} + +const missingTerminalSessionError = (item: ProjectItem): HostError => ({ + _tag: "TerminalSessionClientError", + message: `Terminal session was not created for ${item.displayName}.` +}) + +export const openResolvedProjectSshEffect = ( + item: ProjectItem, + deps: OpenResolvedProjectSshDeps +) => + Effect.gen(function*(_) { + const prepared = yield* _(deps.createSession(item.projectDir)) + if (prepared === null) { + return yield* _(Effect.fail(missingTerminalSessionError(item))) + } + + yield* _(deps.attach(item, prepared.session)) + }) + +export const openResolvedProjectSsh = (item: ProjectItem) => + openResolvedProjectSshEffect(item, { + createSession: (projectId) => createProjectTerminalSession(projectId), + attach: (project, session) => + attachTerminalSession({ + header: `SSH terminal: ${project.displayName}`, + session, + websocketPath: `/projects/${encodeURIComponent(project.projectDir)}/terminal-sessions/${ + encodeURIComponent(session.id) + }/ws` + }) + }) diff --git a/packages/app/src/docker-git/open-project.ts b/packages/app/src/docker-git/open-project.ts index 0ec6b433..53362e93 100644 --- a/packages/app/src/docker-git/open-project.ts +++ b/packages/app/src/docker-git/open-project.ts @@ -1,25 +1,25 @@ -import { defaultTemplateConfig } from "@lib/core/domain" -import { runDockerInspectContainerRuntimeInfo, type DockerContainerRuntimeInfo } from "@lib/shell/docker" -import { buildSshCommand, connectProjectSsh, probeProjectSshReady, type ProjectItem } from "@lib/usecases/projects" -import { Effect, pipe } from "effect" +import { Effect } from "effect" -import type { OpenCommand } from "@lib/core/domain" -import { parseGithubRepoUrl, resolveRepoInput } from "@lib/core/repo" +import type { OpenCommand } from "./frontend-lib/core/domain.js" +import { parseGithubRepoUrl, resolveRepoInput } from "./frontend-lib/core/repo.js" import { getProject, listProjects } from "./api-client.js" import type { ApiProjectDetails } from "./api-project-codec.js" import type { ProjectResolutionError } from "./host-errors.js" -import { connectMenuProjectSshWithUp } from "./menu-api.js" +import { openResolvedProjectSsh } from "./open-project-ssh.js" import { resolveApiProjectItem } from "./project-item.js" -type OpenResolvedProjectSshDeps = { - readonly log: (message: string) => Effect.Effect - readonly resolvePreferredItem: (item: ProjectItem) => Effect.Effect - readonly probeReady: (item: ProjectItem) => Effect.Effect - readonly connect: (item: ProjectItem) => Effect.Effect - readonly connectWithUp: (item: ProjectItem) => Effect.Effect +export type DockerContainerRuntimeInfo = { + readonly ipAddress: string + readonly projectWorkingDir?: string | undefined } +export { + openResolvedProjectSsh, + type OpenResolvedProjectSshDeps, + openResolvedProjectSshEffect +} from "./open-project-ssh.js" + type ResolveOpenProjectDeps = { readonly inspectRuntime: (containerName: string) => Effect.Effect } @@ -221,8 +221,9 @@ export const selectOpenProject = ( ) } -const uniqueContainerNames = (projects: ReadonlyArray): ReadonlyArray => - Array.from(new Set(projects.map((project) => project.containerName))) +const uniqueContainerNames = ( + projects: ReadonlyArray +): ReadonlyArray => [...new Set(projects.map((project) => project.containerName))] export const resolveRuntimeOwnedProject = ( projects: ReadonlyArray, @@ -257,7 +258,9 @@ export const resolveOpenProjectEffect = ( deps: ResolveOpenProjectDeps ): Effect.Effect => resolveRuntimeOwnedProject(projects, selector, deps).pipe( - Effect.flatMap((ownedProject) => ownedProject === null ? selectOpenProject(projects, selector) : Effect.succeed(ownedProject)) + Effect.flatMap((ownedProject) => + ownedProject === null ? selectOpenProject(projects, selector) : Effect.succeed(ownedProject) + ) ) const listProjectDetails = () => @@ -273,110 +276,13 @@ const listProjectDetails = () => return details.filter((project): project is ApiProjectDetails => project !== null) }) -const withProjectItemIpAddress = ( - item: ProjectItem, - ipAddress: string -): ProjectItem => ({ - ...item, - ipAddress, - sshCommand: buildSshCommand( - { - ...defaultTemplateConfig, - containerName: item.containerName, - serviceName: item.serviceName, - sshUser: item.sshUser, - sshPort: item.sshPort, - repoUrl: item.repoUrl, - repoRef: item.repoRef, - targetDir: item.targetDir, - envGlobalPath: item.envGlobalPath, - envProjectPath: item.envProjectPath, - codexAuthPath: item.codexAuthPath, - codexSharedAuthPath: item.codexAuthPath, - codexHome: item.codexHome, - clonedOnHostname: item.clonedOnHostname - }, - item.sshKeyPath, - ipAddress - ) -}) - -const sameConnectionTarget = (left: ProjectItem, right: ProjectItem): boolean => - left.ipAddress === right.ipAddress && - left.sshPort === right.sshPort && - left.sshKeyPath === right.sshKeyPath && - left.sshUser === right.sshUser - -const attemptDirectConnect = ( - item: ProjectItem, - deps: Pick, "connect" | "log" | "probeReady"> -): Effect.Effect => - deps.probeReady(item).pipe( - Effect.flatMap((ready) => - ready - ? pipe( - deps.log(`Opening SSH: ${item.sshCommand}`), - Effect.zipRight(deps.connect(item)), - Effect.as(true) - ) - : Effect.succeed(false) - ) - ) - -export const openResolvedProjectSshEffect = ( - item: ProjectItem, - deps: OpenResolvedProjectSshDeps -) => - Effect.gen(function*(_) { - const preferredItem = yield* _(deps.resolvePreferredItem(item)) - if (preferredItem !== null) { - const connected = yield* _(attemptDirectConnect(preferredItem, deps)) - if (connected) { - return - } - } - - const shouldRetryOriginal = preferredItem === null || !sameConnectionTarget(preferredItem, item) - if (shouldRetryOriginal) { - const connected = yield* _(attemptDirectConnect(item, deps)) - if (connected) { - return - } - } - - yield* _(deps.log(`Opening SSH: ${item.sshCommand}`)) - yield* _(deps.connectWithUp(item)) - }) - -export const openResolvedProjectSsh = ( - item: ProjectItem -) => - openResolvedProjectSshEffect(item, { - log: (message) => Effect.log(message), - resolvePreferredItem: (selected) => - runDockerInspectContainerRuntimeInfo(process.cwd(), selected.containerName).pipe( - Effect.map((runtime) => - runtime !== null && runtime.ipAddress.length > 0 - ? withProjectItemIpAddress(selected, runtime.ipAddress) - : null - ) - ), - probeReady: (selected) => probeProjectSshReady(selected), - connect: (selected) => connectProjectSsh(selected), - connectWithUp: (selected) => connectMenuProjectSshWithUp(selected) - }) - export const openExistingProjectSsh = ( command: OpenCommand ) => Effect.gen(function*(_) { const projects = yield* _(listProjectDetails()) const selector = command.projectDir ?? command.projectRef - const project = yield* _( - resolveOpenProjectEffect(projects, selector, { - inspectRuntime: (containerName) => runDockerInspectContainerRuntimeInfo(process.cwd(), containerName) - }) - ) - const item = yield* _(resolveApiProjectItem(project)) + const project = yield* _(selectOpenProject(projects, selector)) + const item = resolveApiProjectItem(project) yield* _(openResolvedProjectSsh(item)) }) diff --git a/packages/app/src/docker-git/program.ts b/packages/app/src/docker-git/program.ts index 47a33316..b9218893 100644 --- a/packages/app/src/docker-git/program.ts +++ b/packages/app/src/docker-git/program.ts @@ -1,5 +1,5 @@ -import type { Command } from "@lib/core/domain" import { Effect, Match, pipe } from "effect" +import type { Command } from "./frontend-lib/core/domain.js" import { type ApiProjectDetails, diff --git a/packages/app/src/docker-git/project-item.ts b/packages/app/src/docker-git/project-item.ts index f2438ec9..efdd19ef 100644 --- a/packages/app/src/docker-git/project-item.ts +++ b/packages/app/src/docker-git/project-item.ts @@ -1,82 +1,7 @@ -import * as FileSystem from "@effect/platform/FileSystem" -import { Effect } from "effect" - -import { defaultTemplateConfig } from "@lib/core/domain" -import { buildSshCommand, getContainerIpIfInsideContainer, type ProjectItem } from "@lib/usecases/projects" - import type { ApiProjectDetails } from "./api-project-codec.js" -import { resolveHostPrivateKeyPath } from "./host-ssh-material.js" - -const controllerManagedAuthorizedKeysPath = (projectDir: string): string => `${projectDir}/authorized_keys` - -export const projectItemFromApiDetails = ( - project: ApiProjectDetails, - sshKeyPath: string | null, - ipAddress?: string -): ProjectItem => ({ - projectDir: project.projectDir, - displayName: project.displayName, - repoUrl: project.repoUrl, - repoRef: project.repoRef, - containerName: project.containerName, - serviceName: project.serviceName, - sshUser: project.sshUser, - sshPort: project.sshPort, - targetDir: project.targetDir, - sshCommand: buildSshCommand( - { - ...defaultTemplateConfig, - containerName: project.containerName, - serviceName: project.serviceName, - sshUser: project.sshUser, - sshPort: project.sshPort, - repoUrl: project.repoUrl, - repoRef: project.repoRef, - targetDir: project.targetDir, - envGlobalPath: project.envGlobalPath, - envProjectPath: project.envProjectPath, - codexAuthPath: project.codexAuthPath, - codexSharedAuthPath: project.codexAuthPath, - codexHome: project.codexHome, - clonedOnHostname: project.clonedOnHostname - }, - sshKeyPath, - ipAddress - ), - ipAddress, - sshKeyPath, - authorizedKeysPath: controllerManagedAuthorizedKeysPath(project.projectDir), - authorizedKeysExists: true, - envGlobalPath: project.envGlobalPath, - envProjectPath: project.envProjectPath, - codexAuthPath: project.codexAuthPath, - codexHome: project.codexHome, - clonedOnHostname: project.clonedOnHostname -}) -export const resolveApiProjectItem = ( - project: ApiProjectDetails -) => - Effect.gen(function*(_) { - const sshKeyPath = yield* _(resolveHostPrivateKeyPath()) - return yield* _(resolveApiProjectItemWithSshKeyPath(project, sshKeyPath)) - }) +export type ProjectItem = ApiProjectDetails -const resolveProjectItemIpAddress = (containerName: string) => - Effect.gen(function*(_) { - const fs = yield* _(FileSystem.FileSystem) - return yield* _( - getContainerIpIfInsideContainer(fs, process.cwd(), containerName).pipe( - Effect.orElse(() => Effect.succeed("")) - ) - ) - }) +export const projectItemFromApiDetails = (project: ApiProjectDetails): ProjectItem => project -export const resolveApiProjectItemWithSshKeyPath = ( - project: ApiProjectDetails, - sshKeyPath: string | null -) => - Effect.gen(function*(_) { - const ipAddress = yield* _(resolveProjectItemIpAddress(project.containerName)) - return projectItemFromApiDetails(project, sshKeyPath, ipAddress) - }) +export const resolveApiProjectItem = (project: ApiProjectDetails): ProjectItem => projectItemFromApiDetails(project) diff --git a/packages/app/src/docker-git/terminal-session-client.ts b/packages/app/src/docker-git/terminal-session-client.ts new file mode 100644 index 00000000..6311ff8b --- /dev/null +++ b/packages/app/src/docker-git/terminal-session-client.ts @@ -0,0 +1,232 @@ +import * as ParseResult from "@effect/schema/ParseResult" +import { Effect, Either } from "effect" + +import { type TerminalServerMessage, TerminalServerMessageSchema } from "../shared/terminal-session-schema.js" +import type { ApiTerminalSession } from "./api-client.js" +import { resolveApiBaseUrl } from "./controller.js" +import { writeToTerminal } from "./menu-shared.js" + +export type TerminalSessionClientError = { + readonly _tag: "TerminalSessionClientError" + readonly message: string +} + +type TerminalClientMessage = + | { readonly type: "input"; readonly data: string } + | { readonly type: "resize"; readonly cols: number; readonly rows: number } + | { readonly type: "close" } + +type TerminalAttachment = { + readonly header: string + readonly session: ApiTerminalSession + readonly websocketPath: string +} + +type TerminalHandlers = { + readonly handleClose: () => void + readonly handleError: () => void + readonly handleMessage: (event: MessageEvent) => void + readonly handleOpen: () => void + readonly inputHandler: (chunk: Buffer) => void + readonly resizeHandler: () => void +} + +const terminalSessionError = (message: string): TerminalSessionClientError => ({ + _tag: "TerminalSessionClientError", + message +}) + +const encodeClientMessage = (message: TerminalClientMessage): string => JSON.stringify(message) + +const parseServerMessage = (value: string): TerminalServerMessage | null => + Either.getOrNull(ParseResult.decodeUnknownEither(TerminalServerMessageSchema)(value)) + +const resolveTerminalSize = (): { readonly cols: number; readonly rows: number } => + process.stdout.isTTY ? { cols: process.stdout.columns, rows: process.stdout.rows } : { cols: 120, rows: 32 } + +const resolveTerminalWebSocketUrl = (websocketPath: string): string => { + const apiBaseUrl = new URL(resolveApiBaseUrl()) + const { cols, rows } = resolveTerminalSize() + apiBaseUrl.protocol = apiBaseUrl.protocol === "https:" ? "wss:" : "ws:" + apiBaseUrl.pathname = `${apiBaseUrl.pathname.replace(/\/$/u, "")}${websocketPath}` + apiBaseUrl.searchParams.set("cols", String(cols)) + apiBaseUrl.searchParams.set("rows", String(rows)) + return apiBaseUrl.toString() +} + +const sendResize = (socket: WebSocket): void => { + const { cols, rows } = resolveTerminalSize() + socket.send(encodeClientMessage({ + type: "resize", + cols, + rows + })) +} + +const setRawMode = (enabled: boolean): void => { + if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") { + process.stdin.setRawMode(enabled) + } +} + +const cleanupTerminalHandlers = ( + socket: WebSocket, + inputHandler: (chunk: Buffer) => void, + resizeHandler: () => void +): void => { + process.stdin.off("data", inputHandler) + process.stdout.off("resize", resizeHandler) + setRawMode(false) + if (socket.readyState === WebSocket.OPEN) { + socket.send(encodeClientMessage({ type: "close" })) + } +} + +const writeHeader = (attachment: TerminalAttachment): void => { + writeToTerminal(`\n[docker-git] ${attachment.header}\n`) + writeToTerminal(`[docker-git] ${attachment.session.sshCommand}\n\n`) +} + +const handleTerminalServerMessage = ( + message: TerminalServerMessage, + finish: (effect: Effect.Effect) => void, + markExit: () => void +): void => { + if (message.type === "ready") { + return + } + + if (message.type === "output") { + writeToTerminal(message.data) + return + } + + if (message.type === "error") { + finish(Effect.fail(terminalSessionError(message.message))) + return + } + + markExit() + const suffix = message.exitCode === null ? "" : ` (exit ${message.exitCode})` + writeToTerminal(`\n[docker-git] terminal finished${suffix}\n`) + finish(Effect.void) +} + +const createTerminalInputHandler = (socket: WebSocket) => (chunk: Buffer): void => { + if (socket.readyState !== WebSocket.OPEN) { + return + } + socket.send(encodeClientMessage({ type: "input", data: chunk.toString("utf8") })) +} + +const createTerminalResizeHandler = (socket: WebSocket) => (): void => { + if (socket.readyState !== WebSocket.OPEN) { + return + } + sendResize(socket) +} + +const createTerminalOpenHandler = ( + attachment: TerminalAttachment, + socket: WebSocket, + inputHandler: (chunk: Buffer) => void, + resizeHandler: () => void +) => +(): void => { + writeHeader(attachment) + process.stdin.resume() + setRawMode(true) + process.stdin.on("data", inputHandler) + process.stdout.on("resize", resizeHandler) + sendResize(socket) +} + +const createTerminalMessageHandler = ( + finish: (effect: Effect.Effect) => void, + markExit: () => void +) => +(event: MessageEvent): void => { + const payload = typeof event.data === "string" ? event.data : String(event.data) + const message = parseServerMessage(payload) + if (message === null) { + finish(Effect.fail(terminalSessionError("Invalid terminal protocol message."))) + return + } + handleTerminalServerMessage(message, finish, markExit) +} + +const createTerminalErrorHandler = ( + finish: (effect: Effect.Effect) => void +) => +(): void => { + finish(Effect.fail(terminalSessionError("Terminal websocket error."))) +} + +const createTerminalCloseHandler = ( + socket: WebSocket, + inputHandler: (chunk: Buffer) => void, + resizeHandler: () => void, + finish: (effect: Effect.Effect) => void, + hasSeenExit: () => boolean +) => +(): void => { + cleanupTerminalHandlers(socket, inputHandler, resizeHandler) + if (!hasSeenExit()) { + finish(Effect.fail(terminalSessionError("Terminal websocket closed before exit."))) + } +} + +const createTerminalHandlers = ( + attachment: TerminalAttachment, + socket: WebSocket, + finish: (effect: Effect.Effect) => void, + hasSeenExit: () => boolean, + markExit: () => void +): TerminalHandlers => { + const inputHandler = createTerminalInputHandler(socket) + const resizeHandler = createTerminalResizeHandler(socket) + const handleOpen = createTerminalOpenHandler(attachment, socket, inputHandler, resizeHandler) + const handleMessage = createTerminalMessageHandler(finish, markExit) + const handleError = createTerminalErrorHandler(finish) + const handleClose = createTerminalCloseHandler(socket, inputHandler, resizeHandler, finish, hasSeenExit) + return { handleClose, handleError, handleMessage, handleOpen, inputHandler, resizeHandler } +} + +export const attachTerminalSession = ( + attachment: TerminalAttachment +): Effect.Effect => + Effect.async((resume) => { + const socket = new WebSocket(resolveTerminalWebSocketUrl(attachment.websocketPath)) + let settled = false + let sawExit = false + + const finish = (effect: Effect.Effect): void => { + if (settled) { + return + } + settled = true + resume(effect) + } + + const handlers = createTerminalHandlers( + attachment, + socket, + finish, + () => sawExit, + () => { + sawExit = true + } + ) + + socket.addEventListener("open", handlers.handleOpen) + socket.addEventListener("message", handlers.handleMessage) + socket.addEventListener("error", handlers.handleError) + socket.addEventListener("close", handlers.handleClose) + + return Effect.sync(() => { + cleanupTerminalHandlers(socket, handlers.inputHandler, handlers.resizeHandler) + if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) { + socket.close() + } + }) + }) diff --git a/packages/app/src/docker-git/tmux.ts b/packages/app/src/docker-git/tmux.ts deleted file mode 100644 index b07ff26c..00000000 --- a/packages/app/src/docker-git/tmux.ts +++ /dev/null @@ -1,288 +0,0 @@ -import type * as CommandExecutor from "@effect/platform/CommandExecutor" -import type { PlatformError } from "@effect/platform/Error" -import type * as FileSystem from "@effect/platform/FileSystem" -import type * as Path from "@effect/platform/Path" -import { Effect, pipe } from "effect" - -import type { AttachCommand, PanesCommand } from "@lib/core/domain" -import { deriveRepoPathParts, deriveRepoSlug } from "@lib/core/domain" -import { runCommandCapture, runCommandExitCode, runCommandWithExitCodes } from "@lib/shell/command-runner" -import { readProjectConfig } from "@lib/shell/config" -import type { - ConfigDecodeError, - ConfigNotFoundError, - DockerCommandError, - FileExistsError, - PortProbeError -} from "@lib/shell/errors" -import { CommandFailedError } from "@lib/shell/errors" -import { resolveBaseDir } from "@lib/shell/paths" -import { findSshPrivateKey } from "@lib/usecases/path-helpers" -import { buildSshCommand } from "@lib/usecases/projects" -import { runDockerComposeUpWithPortCheck } from "@lib/usecases/projects-up" - -const tmuxOk = [0] -const layoutVersion = "v14" - -const makeTmuxSpec = (args: ReadonlyArray) => ({ - cwd: process.cwd(), - command: "tmux", - args -}) - -const runTmux = ( - args: ReadonlyArray -): Effect.Effect => - runCommandWithExitCodes( - makeTmuxSpec(args), - tmuxOk, - (exitCode) => new CommandFailedError({ command: "tmux", exitCode }) - ) - -const runTmuxExitCode = ( - args: ReadonlyArray -): Effect.Effect => runCommandExitCode(makeTmuxSpec(args)) - -const runTmuxCapture = ( - args: ReadonlyArray -): Effect.Effect => - runCommandCapture( - makeTmuxSpec(args), - tmuxOk, - (exitCode) => new CommandFailedError({ command: "tmux", exitCode }) - ) - -const sendKeys = ( - session: string, - pane: string, - text: string -): Effect.Effect => - pipe( - runTmux(["send-keys", "-t", `${session}:0.${pane}`, "-l", text]), - Effect.zipRight(runTmux(["send-keys", "-t", `${session}:0.${pane}`, "C-m"])) - ) - -const shellEscape = (value: string): string => { - if (value.length === 0) { - return "''" - } - if (!/[^\w@%+=:,./-]/.test(value)) { - return value - } - const escaped = value.replaceAll("'", "'\"'\"'") - return `'${escaped}'` -} - -const wrapBash = (command: string): string => `bash -lc ${shellEscape(command)}` - -const buildJobsCommand = (containerName: string): string => - [ - "while true; do", - "clear", - "echo \"LIVE TERMINALS / JOBS (container, refresh 1s)\"", - "echo \"\"", - `docker exec ${containerName} ps -eo pid,tty,cmd,etime --sort=start_time 2>/dev/null | awk 'NR==1 {print; next} $2 != "?" && $3 !~ /(sshd|^-?bash$|^bash$|^sh$|^zsh$|^fish$)/ {print; found=1} END { if (!found) print "(no interactive jobs)" }'`, - "|| echo \"container not running\"", - "sleep 1", - "done" - ].join("; ") - -const readLayoutVersion = ( - session: string -): Effect.Effect => - runTmuxCapture(["show-options", "-t", session, "-v", "@docker-git-layout"]).pipe( - Effect.map((value) => value.trim()), - Effect.catchTag("CommandFailedError", () => Effect.succeed(null)) - ) - -const buildBottomBarCommand = (): string => - [ - "clear", - "echo \"[Focus: Alt+1/2/3] [Select: Alt+s] [Detach: Alt+d]\"", - "echo \"Tip: Mouse click = focus pane, Ctrl+a z = zoom\"", - "while true; do sleep 3600; done" - ].join("; ") - -const formatRepoRefLabel = (repoRef: string): string => { - const match = /refs\/pull\/(\d+)\/head/.exec(repoRef) - const pr = match?.[1] - return pr ? `PR#${pr}` : repoRef -} - -const formatRepoDisplayName = (repoUrl: string): string => { - const parts = deriveRepoPathParts(repoUrl) - return parts.pathParts.length > 0 ? parts.pathParts.join("/") : repoUrl -} - -type PaneRow = { - readonly id: string - readonly window: string - readonly title: string - readonly command: string -} - -const normalizePaneCell = (value: string | undefined): string => value?.trim() ?? "-" - -const parsePaneRow = (line: string): PaneRow => { - const [id, window, title, command] = line.split("\t") - return { - id: normalizePaneCell(id), - window: normalizePaneCell(window), - title: normalizePaneCell(title), - command: normalizePaneCell(command) - } -} - -const renderPaneRow = (row: PaneRow): string => - `- ${row.id} ${row.window} ${row.title === "-" ? row.command : row.title} ${row.command}` - -const configureSession = ( - session: string, - repoDisplayName: string, - statusRight: string -): Effect.Effect => - Effect.gen(function*(_) { - yield* _(runTmux(["set-option", "-t", session, "@docker-git-layout", layoutVersion])) - yield* _(runTmux(["set-option", "-t", session, "window-size", "largest"])) - yield* _(runTmux(["set-option", "-t", session, "aggressive-resize", "on"])) - yield* _(runTmux(["set-option", "-t", session, "mouse", "on"])) - yield* _(runTmux(["set-option", "-t", session, "focus-events", "on"])) - yield* _(runTmux(["set-option", "-t", session, "prefix", "C-a"])) - yield* _(runTmux(["unbind-key", "C-b"])) - yield* _(runTmux(["set-option", "-t", session, "status", "on"])) - yield* _(runTmux(["set-option", "-t", session, "status-position", "top"])) - yield* _(runTmux(["set-option", "-t", session, "status-left", ` docker-git :: ${repoDisplayName} `])) - yield* _(runTmux(["set-option", "-t", session, "status-right", ` ${statusRight} `])) - }) - -const createLayout = ( - session: string -): Effect.Effect => - Effect.gen(function*(_) { - yield* _(runTmux(["new-session", "-d", "-s", session, "-n", "main"])) - yield* _(runTmux(["split-window", "-v", "-p", "12", "-t", `${session}:0`])) - yield* _(runTmux(["split-window", "-h", "-p", "35", "-t", `${session}:0.0`])) - }) - -const setupPanes = ( - session: string, - sshCommand: string, - containerName: string -): Effect.Effect => - Effect.gen(function*(_) { - const leftPane = "0" - const bottomPane = "1" - const rightPane = "2" - yield* _(sendKeys(session, leftPane, sshCommand)) - yield* _(sendKeys(session, rightPane, wrapBash(buildJobsCommand(containerName)))) - yield* _(sendKeys(session, bottomPane, wrapBash(buildBottomBarCommand()))) - yield* _(runTmux(["bind-key", "-n", "M-1", "select-pane", "-t", `${session}:0.${leftPane}`])) - yield* _(runTmux(["bind-key", "-n", "M-2", "select-pane", "-t", `${session}:0.${rightPane}`])) - yield* _(runTmux(["bind-key", "-n", "M-3", "select-pane", "-t", `${session}:0.${bottomPane}`])) - yield* _(runTmux(["bind-key", "-n", "M-d", "detach-client"])) - yield* _(runTmux(["bind-key", "-n", "M-s", "choose-tree", "-Z"])) - yield* _(runTmux(["select-pane", "-t", `${session}:0.${leftPane}`])) - }) - -// CHANGE: list tmux panes for a docker-git project -// WHY: allow non-interactive inspection of terminal panes (CI/automation friendly) -// QUOTE(ТЗ): "сделай команду ... которая отобразит терминалы в докере" -// REF: user-request-2026-02-02-panes -// SOURCE: n/a -// FORMAT THEOREM: forall p: panes(p) -> deterministic output -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: session name is deterministic from repo url -// COMPLEXITY: O(n) where n = number of panes -export const listTmuxPanes = ( - command: PanesCommand -): Effect.Effect< - void, - CommandFailedError | ConfigNotFoundError | ConfigDecodeError | PlatformError, - CommandExecutor.CommandExecutor | FileSystem.FileSystem | Path.Path -> => - Effect.gen(function*(_) { - const { resolved } = yield* _(resolveBaseDir(command.projectDir)) - const config = yield* _(readProjectConfig(resolved)) - const session = `dg-${deriveRepoSlug(config.template.repoUrl)}` - const hasSessionCode = yield* _(runTmuxExitCode(["has-session", "-t", session])) - if (hasSessionCode !== 0) { - yield* _(Effect.logWarning(`tmux session ${session} not found. Run 'docker-git attach' first.`)) - return - } - const raw = yield* _( - runTmuxCapture([ - "list-panes", - "-s", - "-t", - session, - "-F", - "#{pane_id}\t#{window_name}\t#{pane_title}\t#{pane_current_command}" - ]) - ) - const lines = raw - .split(/\r?\n/) - .map((line) => line.trimEnd()) - .filter((line) => line.length > 0) - const rows = lines.map((line) => parsePaneRow(line)) - yield* _(Effect.log(`Project: ${resolved}`)) - yield* _(Effect.log(`Session: ${session}`)) - if (rows.length === 0) { - yield* _(Effect.log("No panes found.")) - return - } - for (const row of rows) { - yield* _(Effect.log(renderPaneRow(row))) - } - }) - -// CHANGE: attach a tmux workspace for a docker-git project -// WHY: provide multi-pane terminal layout for sandbox work -// QUOTE(ТЗ): "окей Давай подключим tmux" -// REF: user-request-2026-02-02-tmux -// SOURCE: n/a -// FORMAT THEOREM: forall p: attach(p) -> tmux(p) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: tmux session name is deterministic from repo url -// COMPLEXITY: O(1) -export const attachTmux = ( - command: AttachCommand -): Effect.Effect< - void, - | CommandFailedError - | DockerCommandError - | ConfigNotFoundError - | ConfigDecodeError - | FileExistsError - | PortProbeError - | PlatformError, - CommandExecutor.CommandExecutor | FileSystem.FileSystem | Path.Path -> => - Effect.gen(function*(_) { - const { fs, path, resolved } = yield* _(resolveBaseDir(command.projectDir)) - const sshKey = yield* _(findSshPrivateKey(fs, path, process.cwd())) - const template = yield* _(runDockerComposeUpWithPortCheck(resolved)) - const sshCommand = buildSshCommand(template, sshKey) - const repoDisplayName = formatRepoDisplayName(template.repoUrl) - const refLabel = formatRepoRefLabel(template.repoRef) - const statusRight = - `SSH: ${template.sshUser}@localhost:${template.sshPort} | Repo: ${repoDisplayName} | Ref: ${refLabel} | Status: Running` - const session = `dg-${deriveRepoSlug(template.repoUrl)}` - const hasSessionCode = yield* _(runTmuxExitCode(["has-session", "-t", session])) - - if (hasSessionCode === 0) { - const existingLayout = yield* _(readLayoutVersion(session)) - if (existingLayout === layoutVersion) { - yield* _(runTmux(["attach", "-t", session])) - return - } - yield* _(Effect.logWarning(`tmux session ${session} uses an old layout; recreating.`)) - yield* _(runTmux(["kill-session", "-t", session])) - } - - yield* _(createLayout(session)) - yield* _(configureSession(session, repoDisplayName, statusRight)) - yield* _(setupPanes(session, sshCommand, template.containerName)) - yield* _(runTmux(["attach", "-t", session])) - }) diff --git a/packages/app/src/lib/core/clone.ts b/packages/app/src/lib/core/clone.ts index ab4948ad..6a61c5a8 100644 --- a/packages/app/src/lib/core/clone.ts +++ b/packages/app/src/lib/core/clone.ts @@ -27,8 +27,8 @@ const resolveLifecycleArgs = ( return first === command ? rest : argv } -// CHANGE: resolve clone/open shortcut requests from argv + npm lifecycle metadata -// WHY: support pnpm run clone/open without requiring "--" +// CHANGE: resolve clone/open shortcut requests from argv + package lifecycle metadata +// WHY: support bun run clone/open without requiring "--" // QUOTE(ТЗ): "Добавить команду open. ... Просто открывает существующий по ссылке" // REF: user-request-2026-01-27 // SOURCE: n/a diff --git a/packages/app/src/lib/core/command-builders.ts b/packages/app/src/lib/core/command-builders.ts index f4468389..d3c74b06 100644 --- a/packages/app/src/lib/core/command-builders.ts +++ b/packages/app/src/lib/core/command-builders.ts @@ -248,7 +248,7 @@ const buildTemplateConfig = ({ dockerNetworkMode, dockerSharedNetworkName, enableMcpPlaywright, - pnpmVersion: defaultTemplateConfig.pnpmVersion, + bunVersion: defaultTemplateConfig.bunVersion, agentMode, agentAuto, clonedOnHostname diff --git a/packages/app/src/lib/core/domain.ts b/packages/app/src/lib/core/domain.ts index e996bb17..60cb7674 100644 --- a/packages/app/src/lib/core/domain.ts +++ b/packages/app/src/lib/core/domain.ts @@ -81,7 +81,7 @@ export interface TemplateConfig { readonly dockerNetworkMode: DockerNetworkMode readonly dockerSharedNetworkName: string readonly enableMcpPlaywright: boolean - readonly pnpmVersion: string + readonly bunVersion: string readonly agentMode?: AgentMode | undefined readonly agentAuto?: boolean | undefined readonly clonedOnHostname?: string | undefined diff --git a/packages/app/src/lib/core/template-defaults.ts b/packages/app/src/lib/core/template-defaults.ts index 772e06b0..8743d8ff 100644 --- a/packages/app/src/lib/core/template-defaults.ts +++ b/packages/app/src/lib/core/template-defaults.ts @@ -25,7 +25,7 @@ type DefaultTemplateConfig = Pick< | "dockerNetworkMode" | "dockerSharedNetworkName" | "enableMcpPlaywright" - | "pnpmVersion" + | "bunVersion" > export const defaultDockerNetworkMode: TemplateConfig["dockerNetworkMode"] = "shared" @@ -61,6 +61,6 @@ export const defaultTemplateConfig = { dockerNetworkMode: defaultDockerNetworkMode, dockerSharedNetworkName: defaultDockerSharedNetworkName, enableMcpPlaywright: false, - pnpmVersion: "10.27.0" + bunVersion: "1.3.11" } satisfies DefaultTemplateConfig /* jscpd:ignore-end */ diff --git a/packages/app/src/lib/core/templates-entrypoint/agents-notice.ts b/packages/app/src/lib/core/templates-entrypoint/agents-notice.ts index 51c46ac9..813ee0f6 100644 --- a/packages/app/src/lib/core/templates-entrypoint/agents-notice.ts +++ b/packages/app/src/lib/core/templates-entrypoint/agents-notice.ts @@ -56,7 +56,7 @@ $MANAGED_END EOF )" cat < "$AGENTS_PATH" -Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, codex, opencode, oh-my-opencode, sshpass, git, node, pnpm и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~ +Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, bun, codex, opencode, oh-my-opencode, sshpass, git, node и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~ $MANAGED_BLOCK Если ты видишь файлы AGENTS.md внутри проекта, ты обязан их читать и соблюдать инструкции. EOF diff --git a/packages/app/src/lib/core/templates-entrypoint/base.ts b/packages/app/src/lib/core/templates-entrypoint/base.ts index 816a55d9..a4dab050 100644 --- a/packages/app/src/lib/core/templates-entrypoint/base.ts +++ b/packages/app/src/lib/core/templates-entrypoint/base.ts @@ -55,24 +55,22 @@ docker_git_upsert_ssh_env() { export const renderEntrypointPackageCache = (config: TemplateConfig): string => `# Keep package manager caches inside the project home volume PACKAGE_CACHE_ROOT="/home/${config.sshUser}/.docker-git/.cache/packages" -PACKAGE_PNPM_STORE="\${npm_config_store_dir:-\${PNPM_STORE_DIR:-$PACKAGE_CACHE_ROOT/pnpm/store}}" +PACKAGE_BUN_CACHE="\${BUN_INSTALL_CACHE_DIR:-$PACKAGE_CACHE_ROOT/bun/install/cache}" PACKAGE_NPM_CACHE="\${npm_config_cache:-\${NPM_CONFIG_CACHE:-$PACKAGE_CACHE_ROOT/npm}}" PACKAGE_YARN_CACHE="\${YARN_CACHE_FOLDER:-$PACKAGE_CACHE_ROOT/yarn}" -mkdir -p "$PACKAGE_PNPM_STORE" "$PACKAGE_NPM_CACHE" "$PACKAGE_YARN_CACHE" +mkdir -p "$PACKAGE_BUN_CACHE" "$PACKAGE_NPM_CACHE" "$PACKAGE_YARN_CACHE" chown -R 1000:1000 "$PACKAGE_CACHE_ROOT" || true cat < /etc/profile.d/docker-git-package-cache.sh -export PNPM_STORE_DIR="$PACKAGE_PNPM_STORE" -export npm_config_store_dir="$PACKAGE_PNPM_STORE" +export BUN_INSTALL_CACHE_DIR="$PACKAGE_BUN_CACHE" export NPM_CONFIG_CACHE="$PACKAGE_NPM_CACHE" export npm_config_cache="$PACKAGE_NPM_CACHE" export YARN_CACHE_FOLDER="$PACKAGE_YARN_CACHE" EOF chmod 0644 /etc/profile.d/docker-git-package-cache.sh -docker_git_upsert_ssh_env "PNPM_STORE_DIR" "$PACKAGE_PNPM_STORE" -docker_git_upsert_ssh_env "npm_config_store_dir" "$PACKAGE_PNPM_STORE" +docker_git_upsert_ssh_env "BUN_INSTALL_CACHE_DIR" "$PACKAGE_BUN_CACHE" docker_git_upsert_ssh_env "NPM_CONFIG_CACHE" "$PACKAGE_NPM_CACHE" docker_git_upsert_ssh_env "npm_config_cache" "$PACKAGE_NPM_CACHE" docker_git_upsert_ssh_env "YARN_CACHE_FOLDER" "$PACKAGE_YARN_CACHE"` diff --git a/packages/app/src/lib/core/templates-entrypoint/claude-extra-config.ts b/packages/app/src/lib/core/templates-entrypoint/claude-extra-config.ts index 9956bdca..7eb71354 100644 --- a/packages/app/src/lib/core/templates-entrypoint/claude-extra-config.ts +++ b/packages/app/src/lib/core/templates-entrypoint/claude-extra-config.ts @@ -47,7 +47,7 @@ if [[ "$CLAUDE_AUTO_SYSTEM_PROMPT" == "1" ]]; then if [[ ! -f "$CLAUDE_GLOBAL_PROMPT_FILE" ]] || grep -q "^$" "$CLAUDE_GLOBAL_PROMPT_FILE"; then cat < "$CLAUDE_GLOBAL_PROMPT_FILE" -Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, codex, opencode, oh-my-opencode, sshpass, claude, git, node, pnpm и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~ +Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, bun, codex, opencode, oh-my-opencode, sshpass, claude, git, node и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~ Рабочая папка проекта (git clone): __TARGET_DIR__ Доступные workspace пути: __TARGET_DIR__ $CLAUDE_WORKSPACE_CONTEXT diff --git a/packages/app/src/lib/core/templates-entrypoint/gemini.ts b/packages/app/src/lib/core/templates-entrypoint/gemini.ts index fac66f92..36c86eeb 100644 --- a/packages/app/src/lib/core/templates-entrypoint/gemini.ts +++ b/packages/app/src/lib/core/templates-entrypoint/gemini.ts @@ -267,7 +267,7 @@ fi cat < "$GEMINI_MD_PATH" -Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, codex, gemini, claude, opencode, oh-my-opencode, sshpass, git, node, pnpm и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~ +Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, bun, codex, gemini, claude, opencode, oh-my-opencode, sshpass, git, node и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~ Рабочая папка проекта (git clone): __TARGET_DIR__ Доступные workspace пути: __TARGET_DIR__ $GEMINI_WORKSPACE_CONTEXT diff --git a/packages/app/src/lib/core/templates-entrypoint/tasks.ts b/packages/app/src/lib/core/templates-entrypoint/tasks.ts index 4b22c7dd..fb1aa7e8 100644 --- a/packages/app/src/lib/core/templates-entrypoint/tasks.ts +++ b/packages/app/src/lib/core/templates-entrypoint/tasks.ts @@ -194,7 +194,7 @@ const renderCloneBody = (config: TemplateConfig): string => ].join("\n") // CHANGE: provision docker-git scripts into workspace after successful clone -// WHY: git hooks reference scripts/ relative to repo root (e.g. "node scripts/session-backup-gist.js"); +// WHY: git hooks reference scripts/ relative to repo root (e.g. "bun scripts/session-backup-gist.js"); // symlinking embedded /opt/docker-git/scripts makes them available in any cloned repo // REF: issue-176 // PURITY: SHELL diff --git a/packages/app/src/lib/core/templates/docker-compose.ts b/packages/app/src/lib/core/templates/docker-compose.ts index f193c749..a73d6d6e 100644 --- a/packages/app/src/lib/core/templates/docker-compose.ts +++ b/packages/app/src/lib/core/templates/docker-compose.ts @@ -1,9 +1,9 @@ /* jscpd:ignore-start */ import { - resolveComposeProjectName, dockerGitSharedCacheVolumeName, dockerGitSharedCodexVolumeName, resolveComposeNetworkName, + resolveComposeProjectName, resolveProjectBootstrapVolumeName, type TemplateConfig } from "../domain.js" diff --git a/packages/app/src/lib/core/templates/dockerfile.ts b/packages/app/src/lib/core/templates/dockerfile.ts index ecc7368a..239a6388 100644 --- a/packages/app/src/lib/core/templates/dockerfile.ts +++ b/packages/app/src/lib/core/templates/dockerfile.ts @@ -32,13 +32,12 @@ RUN printf "export NVM_DIR=/usr/local/nvm\\n[ -s /usr/local/nvm/nvm.sh ] && . /u > /etc/profile.d/nvm.sh && chmod 0644 /etc/profile.d/nvm.sh` const renderDockerfileBunPrelude = (config: TemplateConfig): string => - `# Tooling: pnpm + Codex CLI (bun) + oh-my-opencode (npm + platform binary) + Claude Code CLI (npm) -RUN corepack enable && corepack prepare pnpm@${config.pnpmVersion} --activate + `# Tooling: Bun + Codex CLI (bun) + oh-my-opencode (npm + platform binary) + Claude Code CLI (npm) ENV TERM=xterm-256color RUN set -eu; \ for attempt in 1 2 3 4 5; do \ if curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 https://bun.sh/install -o /tmp/bun-install.sh \ - && BUN_INSTALL=/usr/local/bun bash /tmp/bun-install.sh; then \ + && BUN_INSTALL=/usr/local/bun BUN_VERSION=${config.bunVersion} bash /tmp/bun-install.sh; then \ rm -f /tmp/bun-install.sh; \ exit 0; \ fi; \ diff --git a/packages/app/src/lib/shell/clone.ts b/packages/app/src/lib/shell/clone.ts index 20119770..aab0c7a6 100644 --- a/packages/app/src/lib/shell/clone.ts +++ b/packages/app/src/lib/shell/clone.ts @@ -12,7 +12,7 @@ import { CommandFailedError } from "./errors.js" const successExitCode = Number(ExitCode(0)) // CHANGE: read shortcut requests from process argv and npm lifecycle metadata -// WHY: allow pnpm run clone/open to work without "--" +// WHY: allow bun run clone/open to work without "--" // QUOTE(ТЗ): "Добавить команду open. ... Просто открывает существующий по ссылке" // REF: user-request-2026-01-27 // SOURCE: n/a @@ -38,19 +38,19 @@ const runDockerGitCommand = ( const workspaceRoot = process.cwd() const appRoot = path.join(workspaceRoot, "packages", "app") const dockerGitCli = path.join(appRoot, "dist", "src", "docker-git", "main.js") - const buildLabel = `pnpm -C ${appRoot} build:docker-git` - const runLabel = `node ${dockerGitCli} ${commandName}` + const buildLabel = `bun run --cwd ${appRoot} build:docker-git` + const runLabel = `bun ${dockerGitCli} ${commandName}` yield* _( runCommandWithExitCodes( - { cwd: workspaceRoot, command: "pnpm", args: ["-C", appRoot, "build:docker-git"] }, + { cwd: workspaceRoot, command: "bun", args: ["run", "--cwd", appRoot, "build:docker-git"] }, [successExitCode], (exitCode) => new CommandFailedError({ command: buildLabel, exitCode }) ) ) yield* _( runCommandWithExitCodes( - { cwd: workspaceRoot, command: "node", args: [dockerGitCli, commandName, ...args] }, + { cwd: workspaceRoot, command: "bun", args: [dockerGitCli, commandName, ...args] }, [successExitCode], (exitCode) => new CommandFailedError({ command: runLabel, exitCode }) ) diff --git a/packages/app/src/lib/shell/command-runner.ts b/packages/app/src/lib/shell/command-runner.ts index 0cf27cff..596d70f0 100644 --- a/packages/app/src/lib/shell/command-runner.ts +++ b/packages/app/src/lib/shell/command-runner.ts @@ -44,8 +44,7 @@ export const runCommandWithExitCodes = ( ): Effect.Effect => Effect.gen(function*(_) { const exitCode = yield* _(Command.exitCode(buildCommand(spec, "inherit", "inherit", "inherit"))) - const numericExitCode = Number(exitCode) - yield* _(ensureExitCode(numericExitCode, okExitCodes, onFailure)) + yield* _(ensureExitCode(exitCode, okExitCodes, onFailure)) }) // CHANGE: run a command and return the exit code, draining stdout/stderr to prevent buffer deadlock @@ -68,7 +67,7 @@ export const runCommandExitCode = ( yield* _(Effect.forkDaemon(Stream.runDrain(process.stdout))) yield* _(Effect.forkDaemon(Stream.runDrain(process.stderr))) const exitCode = yield* _(process.exitCode) - return Number(exitCode) + return exitCode }) ) @@ -114,8 +113,7 @@ export const runCommandCapture = ( pipe(process.stdout, Stream.runCollect, Effect.map((chunks) => collectUint8Array(chunks))) ) const exitCode = yield* _(process.exitCode) - const numericExitCode = Number(exitCode) - yield* _(ensureExitCode(numericExitCode, okExitCodes, onFailure)) + yield* _(ensureExitCode(exitCode, okExitCodes, onFailure)) return decodeUint8Array(bytes) }) ) @@ -129,20 +127,19 @@ export const runCommandWithCapturedOutput = ( Effect.gen(function*(_) { const executor = yield* _(CommandExecutor.CommandExecutor) const process = yield* _(executor.start(buildCommand(spec, "pipe", "pipe", "pipe"))) - const [stdout, stderr, exitCode] = yield* _( + const [stdout, stderr] = yield* _( Effect.all( [ collectStreamText(process.stdout), - collectStreamText(process.stderr), - Effect.map(process.exitCode, (value) => Number(value)) + collectStreamText(process.stderr) ], { concurrency: "unbounded" } ) ) + const exitCode = yield* _(process.exitCode) + const output = combineCommandOutput(stdout, stderr) yield* _( - ensureExitCode(exitCode, okExitCodes, (numericExitCode) => - onFailure(numericExitCode, combineCommandOutput(stdout, stderr)) - ) + ensureExitCode(exitCode, okExitCodes, (numericExitCode) => onFailure(numericExitCode, output)) ) }) ) diff --git a/packages/app/src/lib/shell/config.ts b/packages/app/src/lib/shell/config.ts index 0e78bb29..852e1efd 100644 --- a/packages/app/src/lib/shell/config.ts +++ b/packages/app/src/lib/shell/config.ts @@ -11,7 +11,7 @@ import { defaultTemplateConfig, type ProjectConfig } from "../core/domain.js" import { ConfigDecodeError, ConfigNotFoundError } from "./errors.js" import { resolveBaseDir } from "./paths.js" -const TemplateConfigSchema = Schema.Struct({ +const TemplateConfigInputSchema = Schema.Struct({ containerName: Schema.String, serviceName: Schema.String, sshUser: Schema.String, @@ -63,16 +63,32 @@ const TemplateConfigSchema = Schema.Struct({ enableMcpPlaywright: Schema.optionalWith(Schema.Boolean, { default: () => defaultTemplateConfig.enableMcpPlaywright }), - pnpmVersion: Schema.String, + bunVersion: Schema.optional(Schema.String), + pnpmVersion: Schema.optional(Schema.String), clonedOnHostname: Schema.optional(Schema.String) }) -const ProjectConfigSchema = Schema.Struct({ +type DecodedProjectConfigInput = Schema.Schema.Type + +const normalizeLegacyProjectConfig = ( + config: DecodedProjectConfigInput +): ProjectConfig => { + const { bunVersion, pnpmVersion, ...template } = config.template + return { + schemaVersion: config.schemaVersion, + template: { + ...template, + bunVersion: bunVersion ?? pnpmVersion ?? defaultTemplateConfig.bunVersion + } + } +} + +const ProjectConfigInputSchema = Schema.Struct({ schemaVersion: Schema.Literal(1), - template: TemplateConfigSchema + template: TemplateConfigInputSchema }) -const ProjectConfigJsonSchema = Schema.parseJson(ProjectConfigSchema) +const ProjectConfigJsonSchema = Schema.parseJson(ProjectConfigInputSchema) const decodeProjectConfig = ( path: string, @@ -86,7 +102,7 @@ const decodeProjectConfig = ( message: TreeFormatter.formatIssueSync(issue) }) ), - onRight: (value) => Effect.succeed(value) + onRight: (value) => Effect.succeed(normalizeLegacyProjectConfig(value)) }) // CHANGE: read and decode docker-git.json from disk diff --git a/packages/app/src/lib/shell/docker-compose.ts b/packages/app/src/lib/shell/docker-compose.ts new file mode 100644 index 00000000..d4ad1927 --- /dev/null +++ b/packages/app/src/lib/shell/docker-compose.ts @@ -0,0 +1,123 @@ +import { ExitCode } from "@effect/platform/CommandExecutor" +import type * as CommandExecutor from "@effect/platform/CommandExecutor" +import type { PlatformError } from "@effect/platform/Error" +import { Duration, Effect, pipe, Schedule } from "effect" + +import { runCommandCapture, runCommandWithCapturedOutput } from "./command-runner.js" +import { composeSpec, resolveDockerComposeEnv } from "./docker-compose-env.js" +import { DockerCommandError } from "./errors.js" + +const buildComposeCommand = ( + cwd: string, + args: ReadonlyArray, + env: Record +) => ({ + ...composeSpec(cwd, args), + ...(Object.keys(env).length > 0 ? { env } : {}) +}) + +const runCompose = ( + cwd: string, + args: ReadonlyArray, + okExitCodes: ReadonlyArray +): Effect.Effect => + Effect.gen(function*(_) { + const env = yield* _(resolveDockerComposeEnv(cwd)) + yield* _( + runCommandWithCapturedOutput( + buildComposeCommand(cwd, args, env), + okExitCodes, + (exitCode, output) => new DockerCommandError({ exitCode, ...(output.length > 0 ? { details: output } : {}) }) + ) + ) + }) + +const runComposeCapture = ( + cwd: string, + args: ReadonlyArray, + okExitCodes: ReadonlyArray +): Effect.Effect => + Effect.gen(function*(_) { + const env = yield* _(resolveDockerComposeEnv(cwd)) + return yield* _( + runCommandCapture( + buildComposeCommand(cwd, args, env), + okExitCodes, + (exitCode) => new DockerCommandError({ exitCode }) + ) + ) + }) + +const dockerComposeUpRetrySchedule = Schedule.addDelay( + Schedule.recurs(2), + () => Duration.seconds(2) +) + +const retryDockerComposeUp = ( + cwd: string, + effect: Effect.Effect +): Effect.Effect => + effect.pipe( + Effect.tapError(() => + Effect.logWarning( + `docker compose up failed in ${cwd}; retrying (possible transient Docker Hub/DNS issue)...` + ) + ), + Effect.retry(dockerComposeUpRetrySchedule) + ) + +export const runDockerComposeUp = ( + cwd: string +): Effect.Effect => + retryDockerComposeUp(cwd, runCompose(cwd, ["up", "-d", "--build"], [Number(ExitCode(0))])) + +export const dockerComposeUpRecreateArgs: ReadonlyArray = [ + "up", + "-d", + "--build", + "--force-recreate" +] + +export const runDockerComposeUpRecreate = ( + cwd: string +): Effect.Effect => + retryDockerComposeUp(cwd, runCompose(cwd, dockerComposeUpRecreateArgs, [Number(ExitCode(0))])) + +export const runDockerComposeDown = ( + cwd: string +): Effect.Effect => + runCompose(cwd, ["down"], [Number(ExitCode(0))]) + +export const runDockerComposeDownVolumes = ( + cwd: string +): Effect.Effect => + runCompose(cwd, ["down", "-v", "--remove-orphans"], [Number(ExitCode(0))]) + +export const runDockerComposeRecreate = ( + cwd: string +): Effect.Effect => + pipe(runDockerComposeDown(cwd), Effect.zipRight(runDockerComposeUp(cwd))) + +export const runDockerComposePs = ( + cwd: string +): Effect.Effect => + runCompose(cwd, ["ps"], [Number(ExitCode(0))]) + +export const runDockerComposePsFormatted = ( + cwd: string +): Effect.Effect => + runComposeCapture( + cwd, + ["ps", "--format", "{{.Name}}\t{{.Status}}\t{{.Ports}}\t{{.Image}}"], + [Number(ExitCode(0))] + ) + +export const runDockerComposeLogs = ( + cwd: string +): Effect.Effect => + runCompose(cwd, ["logs", "--tail", "200"], [Number(ExitCode(0)), 130]) + +export const runDockerComposeLogsFollow = ( + cwd: string +): Effect.Effect => + runCompose(cwd, ["logs", "--follow", "--tail", "0"], [Number(ExitCode(0)), 130]) diff --git a/packages/app/src/lib/shell/docker-network.ts b/packages/app/src/lib/shell/docker-network.ts new file mode 100644 index 00000000..bf370004 --- /dev/null +++ b/packages/app/src/lib/shell/docker-network.ts @@ -0,0 +1,86 @@ +import { ExitCode } from "@effect/platform/CommandExecutor" +import type * as CommandExecutor from "@effect/platform/CommandExecutor" +import type { PlatformError } from "@effect/platform/Error" +import { Effect } from "effect" + +import { runCommandCapture, runCommandExitCode, runCommandWithExitCodes } from "./command-runner.js" +import { DockerCommandError } from "./errors.js" + +const dockerSuccessExitCodes = [Number(ExitCode(0))] + +const createDockerNetworkError = (exitCode: number): DockerCommandError => new DockerCommandError({ exitCode }) + +const buildDockerNetworkCommand = ( + cwd: string, + args: ReadonlyArray +): { readonly cwd: string; readonly command: string; readonly args: ReadonlyArray } => ({ + cwd, + command: "docker", + args +}) + +const runDockerNetworkCommand = ( + cwd: string, + args: ReadonlyArray +): Effect.Effect => + runCommandWithExitCodes( + buildDockerNetworkCommand(cwd, args), + dockerSuccessExitCodes, + createDockerNetworkError + ) + +const runDockerNetworkCapture = ( + cwd: string, + args: ReadonlyArray +): Effect.Effect => + runCommandCapture( + buildDockerNetworkCommand(cwd, args), + dockerSuccessExitCodes, + createDockerNetworkError + ) + +export const runDockerNetworkConnectBridge = ( + cwd: string, + containerName: string +): Effect.Effect => + runDockerNetworkCapture(cwd, ["network", "connect", "bridge", containerName]).pipe(Effect.asVoid) + +export const runDockerNetworkExists = ( + cwd: string, + networkName: string +): Effect.Effect => + runCommandExitCode({ + cwd, + command: "docker", + args: ["network", "inspect", networkName] + }).pipe(Effect.map((exitCode) => exitCode === 0)) + +export const runDockerNetworkCreateBridge = ( + cwd: string, + networkName: string +): Effect.Effect => + runDockerNetworkCommand(cwd, ["network", "create", "--driver", "bridge", networkName]) + +export const runDockerNetworkCreateBridgeWithSubnet = ( + cwd: string, + networkName: string, + subnet: string +): Effect.Effect => + runDockerNetworkCommand(cwd, ["network", "create", "--driver", "bridge", "--subnet", subnet, networkName]) + +export const runDockerNetworkContainerCount = ( + cwd: string, + networkName: string +): Effect.Effect => + runDockerNetworkCapture(cwd, ["network", "inspect", "-f", "{{len .Containers}}", networkName]).pipe( + Effect.map((output) => { + const parsed = Number.parseInt(output.trim(), 10) + return Number.isNaN(parsed) ? 0 : parsed + }) + ) + +export const runDockerNetworkRemove = ( + cwd: string, + networkName: string +): Effect.Effect => + runDockerNetworkCommand(cwd, ["network", "rm", networkName]) diff --git a/packages/app/src/lib/shell/docker-runtime.ts b/packages/app/src/lib/shell/docker-runtime.ts new file mode 100644 index 00000000..b644ebca --- /dev/null +++ b/packages/app/src/lib/shell/docker-runtime.ts @@ -0,0 +1,100 @@ +import * as Command from "@effect/platform/Command" +import { ExitCode } from "@effect/platform/CommandExecutor" +import type * as CommandExecutor from "@effect/platform/CommandExecutor" +import type { PlatformError } from "@effect/platform/Error" +import { Effect, pipe } from "effect" + +import { runCommandCapture } from "./command-runner.js" +import { parseInspectNetworkEntry } from "./docker-inspect-parse.js" +import { CommandFailedError, DockerCommandError } from "./errors.js" + +type DockerInspectReader = ( + cwd: string, + containerName: string +) => Effect.Effect + +const createDockerInspectReader = ( + format: string, + parse: (output: string) => A +): DockerInspectReader => +(cwd, containerName) => + pipe( + runDockerInspectValue(cwd, containerName, format), + Effect.map((output) => parse(output)) + ) + +const runDockerInspectValue = ( + cwd: string, + containerName: string, + format: string +): Effect.Effect => + runCommandCapture( + { + cwd, + command: "docker", + args: ["inspect", "-f", format, containerName] + }, + [Number(ExitCode(0))], + (exitCode) => new DockerCommandError({ exitCode }) + ) + +export const runDockerExecExitCode = ( + cwd: string, + containerName: string, + args: ReadonlyArray +): Effect.Effect => + Effect.gen(function*(_) { + const command = pipe( + Command.make("docker", "exec", containerName, ...args), + Command.workingDirectory(cwd), + Command.stdout("pipe"), + Command.stderr("pipe") + ) + const exitCode = yield* _(Command.exitCode(command)) + return Number(exitCode) + }) + +export const runDockerInspectContainerIp = createDockerInspectReader( + String.raw`{{range $k,$v := .NetworkSettings.Networks}}{{printf "%s=%s\n" $k $v.IPAddress}}{{end}}`, + (output) => { + const lines = output + .trim() + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line.length > 0) + + const entries = lines.flatMap((line) => parseInspectNetworkEntry(line)) + if (entries.length === 0) { + return "" + } + + const entryMap = new Map(entries) + return entryMap.get("bridge") ?? entries[0]![1] + } +) + +export const runDockerInspectContainerBridgeIp = createDockerInspectReader( + "{{with (index .NetworkSettings.Networks \"bridge\")}}{{.IPAddress}}{{end}}", + (output) => output.trim() +) + +export const runDockerPsNames = ( + cwd: string +): Effect.Effect, CommandFailedError | PlatformError, CommandExecutor.CommandExecutor> => + pipe( + runCommandCapture( + { + cwd, + command: "docker", + args: ["ps", "--format", "{{.Names}}"] + }, + [Number(ExitCode(0))], + (exitCode) => new CommandFailedError({ command: "docker ps", exitCode }) + ), + Effect.map((output) => + output + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line.length > 0) + ) + ) diff --git a/packages/app/src/lib/shell/docker.ts b/packages/app/src/lib/shell/docker.ts index fd4df315..f75a89cc 100644 --- a/packages/app/src/lib/shell/docker.ts +++ b/packages/app/src/lib/shell/docker.ts @@ -1,576 +1,5 @@ -/* jscpd:ignore-start */ -import * as Command from "@effect/platform/Command" -import type * as CommandExecutor from "@effect/platform/CommandExecutor" -import { ExitCode } from "@effect/platform/CommandExecutor" -import type { PlatformError } from "@effect/platform/Error" -import { Duration, Effect, pipe, Schedule } from "effect" - -import { runCommandCapture, runCommandExitCode, runCommandWithCapturedOutput, runCommandWithExitCodes } from "./command-runner.js" -import { composeSpec, resolveDockerComposeEnv } from "./docker-compose-env.js" -import { parseInspectNetworkEntry } from "./docker-inspect-parse.js" -import { CommandFailedError, DockerCommandError } from "./errors.js" - +export * from "./docker-compose.js" export { classifyDockerAccessIssue, ensureDockerDaemonAccess } from "./docker-daemon-access.js" +export * from "./docker-network.js" export { parseDockerPublishedHostPorts, runDockerPsPublishedHostPorts } from "./docker-published-ports.js" - -export type DockerContainerRuntimeInfo = { - readonly containerName: string - readonly running: boolean - readonly ipAddress: string - readonly projectWorkingDir?: string | undefined - readonly composeService?: string | undefined -} - -const parseOptionalInspectField = (value: string | undefined): string | undefined => { - const trimmed = value?.trim() ?? "" - return trimmed.length > 0 ? trimmed : undefined -} - -const runCompose = ( - cwd: string, - args: ReadonlyArray, - okExitCodes: ReadonlyArray -): Effect.Effect => - Effect.gen(function*(_) { - const env = yield* _(resolveDockerComposeEnv(cwd)) - yield* _( - runCommandWithCapturedOutput( - { - ...composeSpec(cwd, args), - ...(Object.keys(env).length > 0 ? { env } : {}) - }, - okExitCodes, - (exitCode, output) => new DockerCommandError({ exitCode, ...(output.length > 0 ? { details: output } : {}) }) - ) - ) - }) - -const runComposeCapture = ( - cwd: string, - args: ReadonlyArray, - okExitCodes: ReadonlyArray -): Effect.Effect => - Effect.gen(function*(_) { - const env = yield* _(resolveDockerComposeEnv(cwd)) - return yield* _( - runCommandCapture( - { - ...composeSpec(cwd, args), - ...(Object.keys(env).length > 0 ? { env } : {}) - }, - okExitCodes, - (exitCode) => new DockerCommandError({ exitCode }) - ) - ) - }) - -const dockerComposeUpRetrySchedule = Schedule.addDelay( - Schedule.recurs(2), - () => Duration.seconds(2) -) - -const retryDockerComposeUp = ( - cwd: string, - effect: Effect.Effect -): Effect.Effect => - effect.pipe( - Effect.tapError(() => - Effect.logWarning( - `docker compose up failed in ${cwd}; retrying (possible transient Docker Hub/DNS issue)...` - ) - ), - Effect.retry(dockerComposeUpRetrySchedule) - ) - -// CHANGE: run docker compose up -d --build in the target directory -// WHY: provide a controlled shell effect for image creation -// QUOTE(ТЗ): "создавать докер образы" -// REF: user-request-2026-01-07 -// SOURCE: n/a -// FORMAT THEOREM: forall dir: exitCode(cmd(dir)) = 0 -> image_built(dir) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: command output is inherited from the parent process -// COMPLEXITY: O(command) -export const runDockerComposeUp = ( - cwd: string -): Effect.Effect => - retryDockerComposeUp( - cwd, - runCompose(cwd, ["up", "-d", "--build"], [Number(ExitCode(0))]) - ) - -export const dockerComposeUpRecreateArgs: ReadonlyArray = [ - "up", - "-d", - "--build", - "--force-recreate" -] - -// CHANGE: recreate running containers and refresh images when needed -// WHY: apply env/template updates while preserving workspace volumes -// QUOTE(ТЗ): "сбросит только окружение" -// REF: user-request-2026-02-11-force-env -// SOURCE: n/a -// FORMAT THEOREM: ∀dir: up_force_recreate(dir) → recreated(containers(dir)) ∧ preserved(volumes(dir)) ∧ updated(images(dir)) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: may rebuild images but does not remove volumes -// COMPLEXITY: O(command) -export const runDockerComposeUpRecreate = ( - cwd: string -): Effect.Effect => - retryDockerComposeUp( - cwd, - runCompose(cwd, dockerComposeUpRecreateArgs, [Number(ExitCode(0))]) - ) - -// CHANGE: run docker compose down in the target directory -// WHY: allow stopping managed containers from the CLI/menu -// QUOTE(ТЗ): "Могу удалить / Отключить" -// REF: user-request-2026-01-07 -// SOURCE: n/a -// FORMAT THEOREM: forall dir: exitCode(cmd(dir)) = 0 -> containers_stopped(dir) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: command output is inherited from the parent process -// COMPLEXITY: O(command) -export const runDockerComposeDown = ( - cwd: string -): Effect.Effect => - runCompose(cwd, ["down"], [Number(ExitCode(0))]) - -// CHANGE: run docker compose down -v in the target directory -// WHY: allow a truly fresh environment by wiping the named volumes (e.g. /home/dev) -// QUOTE(ТЗ): "контейнер полностью должен же очищаться при --force" -// REF: user-request-2026-02-07-force-wipe-volumes -// SOURCE: n/a -// FORMAT THEOREM: ∀dir: down_v(dir) → removed(volumes(dir)) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: removes only resources within the compose project (containers, networks, volumes) -// COMPLEXITY: O(command) -export const runDockerComposeDownVolumes = ( - cwd: string -): Effect.Effect => - runCompose(cwd, ["down", "-v", "--remove-orphans"], [Number(ExitCode(0))]) - -// CHANGE: recreate docker compose environment in the target directory -// WHY: allow a clean rebuild of the container from the UI -// QUOTE(ТЗ): "дропнул контейнер и заново его создал" -// REF: user-request-2026-01-13 -// SOURCE: n/a -// FORMAT THEOREM: forall dir: down(dir) && up(dir) -> recreated(dir) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: down completes before up starts -// COMPLEXITY: O(command) -export const runDockerComposeRecreate = ( - cwd: string -): Effect.Effect => - pipe( - runDockerComposeDown(cwd), - Effect.zipRight(runDockerComposeUp(cwd)) - ) - -// CHANGE: run docker compose ps in the target directory -// WHY: expose runtime status in the interactive menu -// QUOTE(ТЗ): "вижу всю инфу по ним" -// REF: user-request-2026-01-07 -// SOURCE: n/a -// FORMAT THEOREM: forall dir: exitCode(cmd(dir)) = 0 -> status_listed(dir) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: command output is inherited from the parent process -// COMPLEXITY: O(command) -export const runDockerComposePs = ( - cwd: string -): Effect.Effect => - runCompose(cwd, ["ps"], [Number(ExitCode(0))]) - -// CHANGE: capture docker compose ps output in a parseable format -// WHY: allow structured, readable status output for CLI -// QUOTE(ТЗ): "информация отображалиась удобно" -// REF: user-request-2026-01-28 -// SOURCE: n/a -// FORMAT THEOREM: forall dir: ps_fmt(dir) -> tabbed_string -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: output is tab-delimited columns from docker compose ps -// COMPLEXITY: O(command) -export const runDockerComposePsFormatted = ( - cwd: string -): Effect.Effect => - runComposeCapture( - cwd, - ["ps", "--format", "{{.Name}}\t{{.Status}}\t{{.Ports}}\t{{.Image}}"], - [Number(ExitCode(0))] - ) - -// CHANGE: run docker compose logs in the target directory -// WHY: allow quick inspection of container output without leaving the menu -// QUOTE(ТЗ): "вижу всю инфу по ним" -// REF: user-request-2026-01-07 -// SOURCE: n/a -// FORMAT THEOREM: forall dir: exitCode(cmd(dir)) in {0,130} -> logs_shown(dir) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: command output is inherited from the parent process -// COMPLEXITY: O(command) -export const runDockerComposeLogs = ( - cwd: string -): Effect.Effect => - runCompose(cwd, ["logs", "--tail", "200"], [Number(ExitCode(0)), 130]) - -// CHANGE: stream docker compose logs until interrupted -// WHY: allow synchronous clone flow to surface container output -// QUOTE(ТЗ): "должно работать синхронно отображая весь процесс" -// REF: user-request-2026-01-28 -// SOURCE: n/a -// FORMAT THEOREM: forall dir: logs_follow(dir) -> stdout(stream) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: command output is inherited from the parent process -// COMPLEXITY: O(command) -export const runDockerComposeLogsFollow = ( - cwd: string -): Effect.Effect => - runCompose(cwd, ["logs", "--follow", "--tail", "0"], [Number(ExitCode(0)), 130]) - -// CHANGE: run docker exec and return its exit code -// WHY: allow polling for clone completion markers inside the container -// QUOTE(ТЗ): "весь процесс от и до" -// REF: user-request-2026-01-28 -// SOURCE: n/a -// FORMAT THEOREM: forall cmd: exitCode(docker exec cmd) = n -> deterministic(n) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: stdout/stderr are suppressed for polling commands -// COMPLEXITY: O(command) -export const runDockerExecExitCode = ( - cwd: string, - containerName: string, - args: ReadonlyArray -): Effect.Effect => - Effect.gen(function*(_) { - const command = pipe( - Command.make("docker", "exec", containerName, ...args), - Command.workingDirectory(cwd), - Command.stdout("pipe"), - Command.stderr("pipe") - ) - const exitCode = yield* _(Command.exitCode(command)) - return Number(exitCode) - }) - -// CHANGE: inspect container IP address -// WHY: enable per-container DNS mapping on the host -// QUOTE(ТЗ): "У каждого контейнера свой IP т.е свой домен" -// REF: user-request-2026-01-30-dns -// SOURCE: n/a -// FORMAT THEOREM: forall c: inspect(c) -> ip(c) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: returns empty string when not available -// COMPLEXITY: O(command) -export const runDockerInspectContainerIp = ( - cwd: string, - containerName: string -): Effect.Effect => - pipe( - runCommandCapture( - { - cwd, - command: "docker", - args: [ - "inspect", - "-f", - // Prefer the built-in `bridge` network IP when present so the printed IP - // works from "external" containers that default to `bridge`. - // Example output: - // bridge=172.17.0.4 - // _dg--net=192.168.64.3 - String.raw`{{range $k,$v := .NetworkSettings.Networks}}{{printf "%s=%s\n" $k $v.IPAddress}}{{end}}`, - containerName - ] - }, - [Number(ExitCode(0))], - (exitCode) => new DockerCommandError({ exitCode }) - ), - Effect.map((output) => { - const lines = output - .trim() - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => line.length > 0) - - const entries = lines.flatMap((line) => parseInspectNetworkEntry(line)) - - if (entries.length === 0) { - return "" - } - - const map = new Map(entries) - return map.get("bridge") ?? entries[0]![1] - }) - ) - -// CHANGE: inspect live Docker runtime ownership and preferred IP for a container -// WHY: allow SSH-open flows to reuse an already running container even when indexed compose state is stale -// QUOTE(ТЗ): "если такой контейнер уже есть то он его и должен был открыть" -// REF: user-request-2026-04-07-open-existing-runtime -// SOURCE: n/a -// FORMAT THEOREM: ∀c: running(c) → inspect_runtime(c) = owner(c), ip(c) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: returns null when the container is missing or not running -// COMPLEXITY: O(command) -export const runDockerInspectContainerRuntimeInfo = ( - cwd: string, - containerName: string -): Effect.Effect => - pipe( - runCommandCapture( - { - cwd, - command: "docker", - args: [ - "inspect", - "-f", - `{{.State.Status}}\t{{with index .Config.Labels "com.docker.compose.project.working_dir"}}{{.}}{{end}}\t{{with index .Config.Labels "com.docker.compose.service"}}{{.}}{{end}}`, - containerName - ] - }, - [Number(ExitCode(0))], - (exitCode) => new DockerCommandError({ exitCode }) - ), - Effect.flatMap((output) => { - const [status, projectWorkingDir, composeService] = output.trim().replaceAll("\\t", "\t").split("\t") - if ((status?.trim() ?? "") !== "running") { - return Effect.succeed(null) - } - return runDockerInspectContainerIp(cwd, containerName).pipe( - Effect.map((ipAddress) => ({ - containerName, - running: true, - ipAddress, - projectWorkingDir: parseOptionalInspectField(projectWorkingDir), - composeService: parseOptionalInspectField(composeService) - })) - ) - }), - Effect.catchAll(() => Effect.succeed(null)) - ) - -// CHANGE: inspect the container IP address on the default `bridge` network -// WHY: allow callers to decide whether `docker network connect bridge` is needed -// QUOTE(ТЗ): "подключиться с внешнего контейнера" -// REF: user-request-2026-02-10-bridge-ip -// SOURCE: n/a -// FORMAT THEOREM: ∀c: bridge(c) → ip_bridge(c) ≠ "" -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: returns "" when the container is not connected to `bridge` -// COMPLEXITY: O(command) -export const runDockerInspectContainerBridgeIp = ( - cwd: string, - containerName: string -): Effect.Effect => - pipe( - runCommandCapture( - { - cwd, - command: "docker", - args: [ - "inspect", - "-f", - "{{with (index .NetworkSettings.Networks \"bridge\")}}{{.IPAddress}}{{end}}", - containerName - ] - }, - [Number(ExitCode(0))], - (exitCode) => new DockerCommandError({ exitCode }) - ), - Effect.map((output) => output.trim()) - ) - -// CHANGE: connect an existing container to the default `bridge` network -// WHY: allow "external" containers (which default to `bridge`) to reach services by container IP -// QUOTE(ТЗ): "Всё что запущено в докере должно быть публично наружу" -// REF: user-request-2026-02-10-public-ports -// SOURCE: n/a -// FORMAT THEOREM: ∀c: up(c) → reachable(bridge_ip(c), ports(c)) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: does not fail the overall flow when already connected (handled by caller) -// COMPLEXITY: O(command) -export const runDockerNetworkConnectBridge = ( - cwd: string, - containerName: string -): Effect.Effect => - pipe( - runCommandCapture( - { - cwd, - command: "docker", - args: ["network", "connect", "bridge", containerName] - }, - [Number(ExitCode(0))], - (exitCode) => new DockerCommandError({ exitCode }) - ), - Effect.asVoid - ) - -// CHANGE: check whether a Docker network already exists -// WHY: allow shared-network mode to create the network only when missing -// QUOTE(ТЗ): "Что бы текущие проекты не ложились" -// REF: user-request-2026-02-20-network-shared -// SOURCE: n/a -// FORMAT THEOREM: ∀n: exists(n) ∈ {true,false} -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: returns false for non-zero inspect exit codes -// COMPLEXITY: O(command) -export const runDockerNetworkExists = ( - cwd: string, - networkName: string -): Effect.Effect => - runCommandExitCode({ - cwd, - command: "docker", - args: ["network", "inspect", networkName] - }).pipe(Effect.map((exitCode) => exitCode === 0)) - -// CHANGE: create a Docker bridge network with a deterministic name -// WHY: shared-network mode requires an external network before compose up -// QUOTE(ТЗ): "сделай что бы я эту ошибку больше не видел" -// REF: user-request-2026-02-20-network-shared -// SOURCE: n/a -// FORMAT THEOREM: ∀n: create(n)=0 -> network_exists(n) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: network driver is always `bridge` -// COMPLEXITY: O(command) -export const runDockerNetworkCreateBridge = ( - cwd: string, - networkName: string -): Effect.Effect => - runCommandWithExitCodes( - { - cwd, - command: "docker", - args: ["network", "create", "--driver", "bridge", networkName] - }, - [Number(ExitCode(0))], - (exitCode) => new DockerCommandError({ exitCode }) - ) - -// CHANGE: create a Docker bridge network with an explicit subnet -// WHY: allow callers to bypass default address-pool allocation when it is exhausted -// QUOTE(ТЗ): "научилось создавать сети правильно" -// REF: user-request-2026-02-20-network-fallback -// SOURCE: n/a -// FORMAT THEOREM: ∀(n,s): create(n,s)=0 -> exists(n) ∧ subnet(n)=s -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: network driver is always `bridge` -// COMPLEXITY: O(command) -export const runDockerNetworkCreateBridgeWithSubnet = ( - cwd: string, - networkName: string, - subnet: string -): Effect.Effect => - runCommandWithExitCodes( - { - cwd, - command: "docker", - args: ["network", "create", "--driver", "bridge", "--subnet", subnet, networkName] - }, - [Number(ExitCode(0))], - (exitCode) => new DockerCommandError({ exitCode }) - ) - -// CHANGE: inspect how many containers are attached to a network -// WHY: network GC must remove only detached networks -// QUOTE(ТЗ): "Только так что бы текущие проекты не ложились" -// REF: user-request-2026-02-20-network-gc -// SOURCE: n/a -// FORMAT THEOREM: ∀n: count(n) = |containers(n)| -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: parse fallback is 0 when docker inspect output is empty -// COMPLEXITY: O(command) -export const runDockerNetworkContainerCount = ( - cwd: string, - networkName: string -): Effect.Effect => - runCommandCapture( - { - cwd, - command: "docker", - args: ["network", "inspect", "-f", "{{len .Containers}}", networkName] - }, - [Number(ExitCode(0))], - (exitCode) => new DockerCommandError({ exitCode }) - ).pipe( - Effect.map((output) => { - const parsed = Number.parseInt(output.trim(), 10) - return Number.isNaN(parsed) ? 0 : parsed - }) - ) - -// CHANGE: remove a Docker network by name -// WHY: network GC should reclaim detached project-scoped networks -// QUOTE(ТЗ): "убирать мусорные сети автоматически" -// REF: user-request-2026-02-20-network-gc -// SOURCE: n/a -// FORMAT THEOREM: ∀n: rm(n)=0 -> !exists(n) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: removes exactly the named network -// COMPLEXITY: O(command) -export const runDockerNetworkRemove = ( - cwd: string, - networkName: string -): Effect.Effect => - runCommandWithExitCodes( - { - cwd, - command: "docker", - args: ["network", "rm", networkName] - }, - [Number(ExitCode(0))], - (exitCode) => new DockerCommandError({ exitCode }) - ) - -// CHANGE: list names of running Docker containers -// WHY: support TUI filtering (e.g. stop only running docker-git containers) -// QUOTE(ТЗ): "Если я выбираю остановку контейнера значит он мне должен показывать контейнеры которые запущены" -// REF: user-request-2026-02-07-stop-only-running -// SOURCE: n/a -// FORMAT THEOREM: forall c: c in ps -> running(c) -// PURITY: SHELL -// EFFECT: Effect, CommandFailedError | PlatformError, CommandExecutor> -// INVARIANT: result contains only non-empty container names -// COMPLEXITY: O(command) -export const runDockerPsNames = ( - cwd: string -): Effect.Effect, CommandFailedError | PlatformError, CommandExecutor.CommandExecutor> => - pipe( - runCommandCapture( - { - cwd, - command: "docker", - args: ["ps", "--format", "{{.Names}}"] - }, - [Number(ExitCode(0))], - (exitCode) => new CommandFailedError({ command: "docker ps", exitCode }) - ), - Effect.map((output) => - output - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => line.length > 0) - ) - ) -/* jscpd:ignore-end */ +export * from "./docker-runtime.js" diff --git a/packages/app/src/lib/shell/files.ts b/packages/app/src/lib/shell/files.ts index dfaeda7e..1f5be499 100644 --- a/packages/app/src/lib/shell/files.ts +++ b/packages/app/src/lib/shell/files.ts @@ -10,6 +10,7 @@ import { resolveComposeResourceLimits, withDefaultResourceLimitIntent } from ".. import { type FileSpec, planFiles } from "../core/templates.js" import { FileExistsError } from "./errors.js" import { resolveBaseDir } from "./paths.js" +import { resolveWorkspaceRoot } from "./workspace-root.js" const ensureParentDir = (path: Path.Path, fs: FileSystem.FileSystem, filePath: string) => fs.makeDirectory(path.dirname(filePath), { recursive: true }) @@ -115,9 +116,9 @@ const provisionDockerGitScripts = ( fs: FileSystem.FileSystem, path: Path.Path, baseDir: string -): Effect.Effect => +): Effect.Effect => Effect.gen(function*(_) { - const workspaceRoot = process.cwd() + const workspaceRoot = yield* _(resolveWorkspaceRoot(process.cwd())) const sourceScriptsDir = path.join(workspaceRoot, "scripts") const targetScriptsDir = path.join(baseDir, "scripts") @@ -133,7 +134,8 @@ const provisionDockerGitScripts = ( const targetPath = path.join(targetScriptsDir, scriptName) const exists = yield* _(fs.exists(sourcePath)) if (exists) { - yield* _(fs.copyFile(sourcePath, targetPath)) + const contents = yield* _(fs.readFileString(sourcePath)) + yield* _(fs.writeFileString(targetPath, contents)) } } }) diff --git a/packages/app/src/lib/shell/workspace-root.ts b/packages/app/src/lib/shell/workspace-root.ts new file mode 100644 index 00000000..98592fe3 --- /dev/null +++ b/packages/app/src/lib/shell/workspace-root.ts @@ -0,0 +1,51 @@ +/* jscpd:ignore-start */ +import type { PlatformError } from "@effect/platform/Error" +import * as FileSystem from "@effect/platform/FileSystem" +import * as Path from "@effect/platform/Path" +import { Effect } from "effect" + +const workspaceMarkers: ReadonlyArray = ["bunfig.toml", ".git"] + +const hasWorkspaceMarker = ( + fs: FileSystem.FileSystem, + path: Path.Path, + directory: string +): Effect.Effect => + Effect.gen(function*(_) { + for (const marker of workspaceMarkers) { + if (yield* _(fs.exists(path.join(directory, marker)))) { + return true + } + } + return false + }) + +const resolveWorkspaceRootFrom = ( + fs: FileSystem.FileSystem, + path: Path.Path, + startDir: string, + currentDir: string +): Effect.Effect => + Effect.gen(function*(_) { + if (yield* _(hasWorkspaceMarker(fs, path, currentDir))) { + return currentDir + } + + const parent = path.dirname(currentDir) + if (parent === currentDir) { + return startDir + } + + return yield* _(resolveWorkspaceRootFrom(fs, path, startDir, parent)) + }) + +export const resolveWorkspaceRoot = ( + startDir: string +): Effect.Effect => + Effect.gen(function*(_) { + const fs = yield* _(FileSystem.FileSystem) + const path = yield* _(Path.Path) + const resolvedStartDir = path.resolve(startDir) + return yield* _(resolveWorkspaceRootFrom(fs, path, resolvedStartDir, resolvedStartDir)) + }) +/* jscpd:ignore-end */ diff --git a/packages/app/src/lib/usecases/actions/create-project-conflicts.ts b/packages/app/src/lib/usecases/actions/create-project-conflicts.ts new file mode 100644 index 00000000..3d3c44c3 --- /dev/null +++ b/packages/app/src/lib/usecases/actions/create-project-conflicts.ts @@ -0,0 +1,179 @@ +/* jscpd:ignore-start */ +import type * as CommandExecutor from "@effect/platform/CommandExecutor" +import type { PlatformError } from "@effect/platform/Error" +import type * as FileSystem from "@effect/platform/FileSystem" +import type * as Path from "@effect/platform/Path" +import { Effect } from "effect" + +import type { TemplateConfig } from "../../core/domain.js" +import { resolveComposeProjectName, resolveProjectBootstrapVolumeName } from "../../core/domain.js" +import { type DockerCommandError, DockerIdentityConflictError } from "../../shell/errors.js" +import type { ProjectStatus } from "../projects-core.js" +import { loadProjectIndex, loadProjectStatus } from "../projects-core.js" +import { deleteDockerGitProject } from "../projects-delete.js" + +type CreateProjectRuntime = FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor + +type DockerIdentityOwner = Pick< + TemplateConfig, + "containerName" | "serviceName" | "volumeName" | "enableMcpPlaywright" +> + +type DockerIdentityNamespace = "container" | "composeProject" | "volume" + +type DockerIdentityClaim = Omit & { + readonly namespace: DockerIdentityNamespace +} + +type ConflictState = { + readonly conflicts: Array + readonly conflictingProjects: Map< + string, + { + readonly projectDir: string + readonly repoUrl: string + readonly containerName: string + readonly serviceName: string + } + > +} + +const resolveBrowserContainerClaims = ( + config: DockerIdentityOwner +): ReadonlyArray => + config.enableMcpPlaywright + ? [{ namespace: "container", kind: "browserContainerName", name: `${config.containerName}-browser` }] + : [] + +const resolveBrowserVolumeClaims = ( + config: DockerIdentityOwner +): ReadonlyArray => + config.enableMcpPlaywright + ? [{ namespace: "volume", kind: "browserVolumeName", name: `${config.volumeName}-browser` }] + : [] + +const resolveDockerIdentityClaims = ( + config: DockerIdentityOwner +): ReadonlyArray => [ + { namespace: "container", kind: "containerName", name: config.containerName }, + ...resolveBrowserContainerClaims(config), + { namespace: "composeProject", kind: "serviceName", name: resolveComposeProjectName(config) }, + { namespace: "volume", kind: "volumeName", name: config.volumeName }, + ...resolveBrowserVolumeClaims(config), + { namespace: "volume", kind: "bootstrapVolumeName", name: resolveProjectBootstrapVolumeName(config) } +] + +const loadProjectStatusOrNull = (configPath: string) => + loadProjectStatus(configPath).pipe( + Effect.match({ + onFailure: () => null, + onSuccess: (value) => value + }) + ) + +const collectSharedClaims = ( + candidateClaims: ReadonlyArray, + existingClaims: ReadonlyArray, + projectDir: string +): ReadonlyArray => + candidateClaims.flatMap((candidate) => + existingClaims.some( + (existing) => existing.namespace === candidate.namespace && existing.name === candidate.name + ) + ? [{ conflictingProjectDir: projectDir, kind: candidate.kind, name: candidate.name }] + : [] + ) + +const appendClaims = ( + conflicts: Array, + sharedClaims: ReadonlyArray +): void => { + for (const claim of sharedClaims) { + conflicts.push(claim) + } +} + +const rememberConflictingProject = ( + conflictingProjects: ConflictState["conflictingProjects"], + status: ProjectStatus +): void => { + conflictingProjects.set(status.projectDir, { + projectDir: status.projectDir, + repoUrl: status.config.template.repoUrl, + containerName: status.config.template.containerName, + serviceName: status.config.template.serviceName + }) +} + +const scanConflicts = ( + resolvedOutDir: string, + config: DockerIdentityOwner +): Effect.Effect => + Effect.gen(function*(_) { + const index = yield* _(loadProjectIndex()) + if (index === null) { + return null + } + + const candidateClaims = resolveDockerIdentityClaims(config) + const state: ConflictState = { + conflicts: [], + conflictingProjects: new Map() + } + + for (const configPath of index.configPaths) { + const status = yield* _(loadProjectStatusOrNull(configPath)) + if (status === null || status.projectDir === resolvedOutDir) { + continue + } + + const sharedClaims = collectSharedClaims( + candidateClaims, + resolveDockerIdentityClaims(status.config.template), + status.projectDir + ) + if (sharedClaims.length === 0) { + continue + } + + appendClaims(state.conflicts, sharedClaims) + rememberConflictingProject(state.conflictingProjects, status) + } + + return state + }) + +const deleteConflictingProjects = ( + conflictingProjects: ConflictState["conflictingProjects"] +): Effect.Effect => + Effect.gen(function*(_) { + for (const conflictingProject of conflictingProjects.values()) { + yield* _( + Effect.logWarning( + `Force enabled: replacing conflicting docker-git project ${conflictingProject.projectDir}` + ) + ) + yield* _(deleteDockerGitProject(conflictingProject)) + } + }) + +export const deleteConflictingProjectsIfNeeded = ( + resolvedOutDir: string, + config: DockerIdentityOwner, + force: boolean +): Effect.Effect => + Effect.gen(function*(_) { + const state = yield* _(scanConflicts(resolvedOutDir, config)) + if (state === null || state.conflicts.length === 0) { + return + } + + if (!force) { + return yield* _( + Effect.fail(new DockerIdentityConflictError({ projectDir: resolvedOutDir, conflicts: state.conflicts })) + ) + } + + yield* _(deleteConflictingProjects(state.conflictingProjects)) + }) +/* jscpd:ignore-end */ diff --git a/packages/app/src/lib/usecases/actions/create-project-open-ssh.ts b/packages/app/src/lib/usecases/actions/create-project-open-ssh.ts new file mode 100644 index 00000000..a632f3c9 --- /dev/null +++ b/packages/app/src/lib/usecases/actions/create-project-open-ssh.ts @@ -0,0 +1,116 @@ +import type * as CommandExecutor from "@effect/platform/CommandExecutor" +import * as FileSystem from "@effect/platform/FileSystem" +import * as Path from "@effect/platform/Path" +import { Effect } from "effect" + +import type { CreateCommand } from "../../core/domain.js" +import { runCommandWithExitCodes } from "../../shell/command-runner.js" +import { CommandFailedError } from "../../shell/errors.js" +import { shouldAutoOpenSsh } from "../auto-open-ssh.js" +import { renderError } from "../errors.js" +import { findSshPrivateKey } from "../path-helpers.js" +import { buildSshCommand, getContainerIpIfInsideContainer } from "../projects-core.js" +import { ensureTerminalCursorVisible } from "../terminal-cursor.js" + +type CreateProjectOpenSshRuntime = + | FileSystem.FileSystem + | Path.Path + | CommandExecutor.CommandExecutor + +const buildSshArgs = ( + config: CreateCommand["config"], + sshKeyPath: string | null, + remoteCommand?: string, + ipAddress?: string +): ReadonlyArray => { + const host = ipAddress ?? "localhost" + const port = ipAddress ? 22 : config.sshPort + const args: Array = [] + if (sshKeyPath !== null) { + args.push("-i", sshKeyPath) + } + args.push( + "-tt", + "-Y", + "-o", + "LogLevel=ERROR", + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + "-p", + String(port), + `${config.sshUser}@${host}` + ) + if (remoteCommand !== undefined) { + args.push(remoteCommand) + } + return args +} + +const resolveInteractiveRemoteCommand = ( + projectConfig: CreateCommand["config"], + interactiveAgent: boolean +): string | undefined => + interactiveAgent && projectConfig.agentMode !== undefined + ? `cd '${projectConfig.targetDir}' && ${projectConfig.agentMode}` + : undefined + +const openSshBestEffort = ( + template: CreateCommand["config"], + remoteCommand?: string +): Effect.Effect => + Effect.gen(function*(_) { + const fs = yield* _(FileSystem.FileSystem) + const path = yield* _(Path.Path) + const ipAddress = yield* _( + getContainerIpIfInsideContainer(fs, process.cwd(), template.containerName).pipe( + Effect.orElse(() => Effect.succeed("")) + ) + ) + const sshKey = yield* _(findSshPrivateKey(fs, path, process.cwd())) + const sshCommand = buildSshCommand(template, sshKey, ipAddress) + const remoteCommandLabel = remoteCommand === undefined ? "" : ` (${remoteCommand})` + + yield* _(Effect.log(`Opening SSH: ${sshCommand}${remoteCommandLabel}`)) + yield* _(ensureTerminalCursorVisible()) + yield* _( + runCommandWithExitCodes( + { + cwd: process.cwd(), + command: "ssh", + args: buildSshArgs(template, sshKey, remoteCommand, ipAddress) + }, + [0, 130], + (exitCode) => new CommandFailedError({ command: "ssh", exitCode }) + ).pipe(Effect.ensuring(ensureTerminalCursorVisible())) + ) + }).pipe( + Effect.asVoid, + Effect.matchEffect({ + onFailure: (error) => Effect.logWarning(`SSH auto-open failed: ${renderError(error)}`), + onSuccess: () => Effect.void + }) + ) + +export const maybeOpenSsh = ( + command: CreateCommand, + hasAgent: boolean, + waitForAgent: boolean, + projectConfig: CreateCommand["config"] +): Effect.Effect => + Effect.gen(function*(_) { + const interactiveAgent = hasAgent && !waitForAgent + const autoOpenSsh = yield* _( + shouldAutoOpenSsh({ + shouldOpen: command.openSsh && (!hasAgent || interactiveAgent), + runUp: command.runUp + }) + ) + if (!autoOpenSsh) { + return + } + + const remoteCommand = resolveInteractiveRemoteCommand(projectConfig, interactiveAgent) + yield* _(openSshBestEffort(projectConfig, remoteCommand)) + }).pipe(Effect.asVoid) diff --git a/packages/app/src/lib/usecases/actions/create-project.ts b/packages/app/src/lib/usecases/actions/create-project.ts index cf6d19a0..7689d05c 100644 --- a/packages/app/src/lib/usecases/actions/create-project.ts +++ b/packages/app/src/lib/usecases/actions/create-project.ts @@ -1,42 +1,32 @@ /* jscpd:ignore-start */ import type * as CommandExecutor from "@effect/platform/CommandExecutor" import type { PlatformError } from "@effect/platform/Error" -import * as FileSystem from "@effect/platform/FileSystem" +import type * as FileSystem from "@effect/platform/FileSystem" import * as Path from "@effect/platform/Path" import { Effect } from "effect" -import type { CreateCommand, ParseError, TemplateConfig } from "../../core/domain.js" -import { deriveRepoPathParts, resolveComposeProjectName, resolveProjectBootstrapVolumeName } from "../../core/domain.js" -import { runCommandWithExitCodes } from "../../shell/command-runner.js" +import type { CreateCommand, ParseError } from "../../core/domain.js" +import { deriveRepoPathParts } from "../../core/domain.js" import { ensureDockerDaemonAccess } from "../../shell/docker.js" -import { CommandFailedError, DockerIdentityConflictError } from "../../shell/errors.js" import type { AgentFailedError, AuthError, CloneFailedError, DockerAccessError, DockerCommandError, - DockerIdentityConflict, + DockerIdentityConflictError, FileExistsError, PortProbeError } from "../../shell/errors.js" import { logDockerAccessInfo } from "../access-log.js" import { resolveAutoAgentMode } from "../agent-auto-select.js" -import { renderError } from "../errors.js" import { applyGithubForkConfig } from "../github-fork.js" import { validateGithubCloneAuthTokenPreflight } from "../github-token-preflight.js" import { defaultProjectsRoot } from "../menu-helpers.js" -import { findSshPrivateKey } from "../path-helpers.js" -import { - buildSshCommand, - getContainerIpIfInsideContainer, - loadProjectIndex, - loadProjectStatus -} from "../projects-core.js" -import { deleteDockerGitProject } from "../projects-delete.js" import { resolveTemplateResourceLimits } from "../resource-limits.js" import { autoSyncState } from "../state-repo.js" -import { ensureTerminalCursorVisible } from "../terminal-cursor.js" +import { deleteConflictingProjectsIfNeeded } from "./create-project-conflicts.js" +import { maybeOpenSsh } from "./create-project-open-ssh.js" import { runDockerDownCleanup, runDockerUpIfNeeded } from "./docker-up.js" import { buildProjectConfigs, resolveDockerGitRootRelativePath } from "./paths.js" import { resolveSshPort } from "./ports.js" @@ -61,6 +51,14 @@ type CreateContext = { readonly resolveRootPath: (value: string) => string } +type BuiltProjectConfigs = ReturnType +type PreparedProject = { + readonly resolvedOutDir: string + readonly finalConfig: CreateCommand["config"] + readonly globalConfig: BuiltProjectConfigs["globalConfig"] + readonly projectConfig: BuiltProjectConfigs["projectConfig"] +} + const resolveClonedOnHostname = (): Effect.Effect => Effect.tryPromise({ try: () => import("node:os").then((os) => os.hostname()), @@ -114,123 +112,6 @@ const formatStateSyncLabel = (repoUrl: string): string => { return repoPath.length > 0 ? repoPath : repoUrl } -const isInteractiveTty = (): boolean => process.stdin.isTTY && process.stdout.isTTY - -const buildSshArgs = ( - config: CreateCommand["config"], - sshKeyPath: string | null, - remoteCommand?: string, - ipAddress?: string -): ReadonlyArray => { - const host = ipAddress ?? "localhost" - const port = ipAddress ? 22 : config.sshPort - const args: Array = [] - if (sshKeyPath !== null) { - args.push("-i", sshKeyPath) - } - args.push( - "-tt", - "-Y", - "-o", - "LogLevel=ERROR", - "-o", - "StrictHostKeyChecking=no", - "-o", - "UserKnownHostsFile=/dev/null", - "-p", - String(port), - `${config.sshUser}@${host}` - ) - if (remoteCommand !== undefined) { - args.push(remoteCommand) - } - return args -} - -// CHANGE: auto-open SSH after environment is created (best-effort) -// WHY: clone flow should drop the user into the container without manual copy/paste -// QUOTE(ТЗ): "Мне надо что бы он сразу открыл SSH" -// REF: issue-39 -// SOURCE: n/a -// FORMAT THEOREM: forall c: openSsh(c) -> ssh_session_started(c) || warning_logged(c) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: SSH failures do not fail the create/clone command -// COMPLEXITY: O(1) + ssh -const openSshBestEffort = ( - template: CreateCommand["config"], - remoteCommand?: string -): Effect.Effect => - Effect.gen(function*(_) { - const fs = yield* _(FileSystem.FileSystem) - const path = yield* _(Path.Path) - - const ipAddress = yield* _( - getContainerIpIfInsideContainer(fs, process.cwd(), template.containerName).pipe( - Effect.orElse(() => Effect.succeed("")) - ) - ) - - const sshKey = yield* _(findSshPrivateKey(fs, path, process.cwd())) - const sshCommand = buildSshCommand(template, sshKey, ipAddress) - - const remoteCommandLabel = remoteCommand === undefined ? "" : ` (${remoteCommand})` - - yield* _(Effect.log(`Opening SSH: ${sshCommand}${remoteCommandLabel}`)) - yield* _(ensureTerminalCursorVisible()) - yield* _( - runCommandWithExitCodes( - { - cwd: process.cwd(), - command: "ssh", - args: buildSshArgs(template, sshKey, remoteCommand, ipAddress) - }, - [0, 130], - (exitCode) => new CommandFailedError({ command: "ssh", exitCode }) - ).pipe(Effect.ensuring(ensureTerminalCursorVisible())) - ) - }).pipe( - Effect.asVoid, - Effect.matchEffect({ - onFailure: (error) => Effect.logWarning(`SSH auto-open failed: ${renderError(error)}`), - onSuccess: () => Effect.void - }) - ) - -const resolveInteractiveRemoteCommand = ( - projectConfig: CreateCommand["config"], - interactiveAgent: boolean -): string | undefined => - interactiveAgent && projectConfig.agentMode !== undefined - ? `cd '${projectConfig.targetDir}' && ${projectConfig.agentMode}` - : undefined - -const maybeOpenSsh = ( - command: CreateCommand, - hasAgent: boolean, - waitForAgent: boolean, - projectConfig: CreateCommand["config"] -): Effect.Effect => - Effect.gen(function*(_) { - const interactiveAgent = hasAgent && !waitForAgent - if (!command.openSsh || (hasAgent && !interactiveAgent)) { - return - } - - if (!command.runUp) { - yield* _(Effect.logWarning("Skipping SSH auto-open: docker compose up disabled (--no-up).")) - return - } - - if (!isInteractiveTty()) { - yield* _(Effect.logWarning("Skipping SSH auto-open: not running in an interactive TTY.")) - return - } - - const remoteCommand = resolveInteractiveRemoteCommand(projectConfig, interactiveAgent) - yield* _(openSshBestEffort(projectConfig, remoteCommand)) - }).pipe(Effect.asVoid) - const resolveFinalAgentConfig = ( resolvedConfig: CreateCommand["config"] ): Effect.Effect => @@ -255,97 +136,6 @@ const resolveRuntimeConfig = ( : { ...finalAgentConfig, clonedOnHostname } }) -type DockerIdentityOwner = Pick - -type DockerIdentityNamespace = "container" | "composeProject" | "volume" - -type DockerIdentityClaim = Omit & { - readonly namespace: DockerIdentityNamespace -} - -const resolveDockerIdentityClaims = ( - config: DockerIdentityOwner -): ReadonlyArray => [ - { namespace: "container", kind: "containerName", name: config.containerName }, - ...(config.enableMcpPlaywright - ? [{ namespace: "container" as const, kind: "browserContainerName" as const, name: `${config.containerName}-browser` }] - : []), - { namespace: "composeProject", kind: "serviceName", name: resolveComposeProjectName(config) }, - { namespace: "volume", kind: "volumeName", name: config.volumeName }, - ...(config.enableMcpPlaywright - ? [{ namespace: "volume" as const, kind: "browserVolumeName" as const, name: `${config.volumeName}-browser` }] - : []), - { namespace: "volume", kind: "bootstrapVolumeName", name: resolveProjectBootstrapVolumeName(config) } -] - -const deleteConflictingProjectsIfNeeded = ( - resolvedOutDir: string, - config: DockerIdentityOwner, - force: boolean -): Effect.Effect => - Effect.gen(function*(_) { - const index = yield* _(loadProjectIndex()) - if (index === null) { - return - } - - const candidateClaims = resolveDockerIdentityClaims(config) - const conflicts: Array = [] - const conflictingProjects = new Map() - - for (const configPath of index.configPaths) { - const status = yield* _( - loadProjectStatus(configPath).pipe( - Effect.match({ - onFailure: () => null, - onSuccess: (value) => value - }) - ) - ) - if (status === null || status.projectDir === resolvedOutDir) { - continue - } - - const existingClaims = resolveDockerIdentityClaims(status.config.template) - const sharedClaims = candidateClaims.flatMap((candidate) => - existingClaims.some( - (existing) => existing.namespace === candidate.namespace && existing.name === candidate.name - ) - ? [{ conflictingProjectDir: status.projectDir, kind: candidate.kind, name: candidate.name }] - : [] - ) - - if (sharedClaims.length === 0) { - continue - } - - conflicts.push(...sharedClaims) - conflictingProjects.set(status.projectDir, { - projectDir: status.projectDir, - repoUrl: status.config.template.repoUrl, - containerName: status.config.template.containerName, - serviceName: status.config.template.serviceName - }) - } - - if (conflicts.length === 0) { - return - } - - if (!force) { - return yield* _(Effect.fail(new DockerIdentityConflictError({ projectDir: resolvedOutDir, conflicts }))) - } - - for (const conflictingProject of conflictingProjects.values()) { - yield* _( - Effect.logWarning( - `Force enabled: replacing conflicting docker-git project ${conflictingProject.projectDir}` - ) - ) - yield* _(deleteDockerGitProject(conflictingProject)) - } - }) - const maybeCleanupAfterAgent = ( waitForAgent: boolean, resolvedOutDir: string @@ -358,10 +148,10 @@ const maybeCleanupAfterAgent = ( yield* _(runDockerDownCleanup(resolvedOutDir)) }) -const runCreateProject = ( +export const prepareProject = ( path: Path.Path, command: CreateCommand -): Effect.Effect => +): Effect.Effect => Effect.gen(function*(_) { if (command.runUp) { yield* _(ensureDockerDaemonAccess(process.cwd())) @@ -374,7 +164,6 @@ const runCreateProject = ( yield* _( deleteConflictingProjectsIfNeeded(resolvedOutDir, rootedConfig, command.force) ) - yield* _(validateGithubCloneAuthTokenPreflight(rootedConfig)) const resolvedConfig = yield* _(resolveCreateConfig(rootedConfig, resolvedOutDir)) @@ -382,7 +171,6 @@ const runCreateProject = ( const { globalConfig, projectConfig } = buildProjectConfigs(path, ctx.baseDir, resolvedOutDir, finalConfig) yield* _(migrateProjectOrchLayout(ctx.baseDir, globalConfig, ctx.resolveRootPath)) - const createdFiles = yield* _( prepareProjectFiles(resolvedOutDir, ctx.baseDir, globalConfig, projectConfig, { force: command.force, @@ -390,25 +178,20 @@ const runCreateProject = ( }) ) yield* _(logCreatedProject(resolvedOutDir, createdFiles)) + return { resolvedOutDir, finalConfig, globalConfig, projectConfig } + }) - const hasAgent = finalConfig.agentMode !== undefined - const waitForAgent = hasAgent && (finalConfig.agentAuto ?? false) +export const runPreparedProject = ( + command: CreateCommand, + prepared: PreparedProject +): Effect.Effect => + Effect.gen(function*(_) { + const hasAgent = prepared.finalConfig.agentMode !== undefined + const waitForAgent = hasAgent && (prepared.finalConfig.agentAuto ?? false) - // CHANGE: run autoSyncState before docker compose up to prevent bind-mount inode invalidation - // WHY: git reset --hard in autoSyncState deletes and recreates .orch/auth/codex; if docker is - // already running with a bind-mount on that directory, the old inode becomes unreachable - // inside the container — codex fails with "No such file or directory" - // QUOTE(ТЗ): n/a - // REF: issue-158 - // SOURCE: n/a - // FORMAT THEOREM: ∀p: synced(p) ∧ stable_inode(.orch/auth/codex, p) → valid_mount(docker_up(p)) - // PURITY: SHELL - // EFFECT: Effect - // INVARIANT: .orch/auth/codex inode is stable when docker compose up runs - // COMPLEXITY: O(git_sync) before O(docker_up) - yield* _(autoSyncState(`chore(state): update ${formatStateSyncLabel(projectConfig.repoUrl)}`)) + yield* _(autoSyncState(`chore(state): update ${formatStateSyncLabel(prepared.projectConfig.repoUrl)}`)) yield* _( - runDockerUpIfNeeded(resolvedOutDir, projectConfig, { + runDockerUpIfNeeded(prepared.resolvedOutDir, prepared.projectConfig, { runUp: command.runUp, waitForClone: command.waitForClone, waitForAgent, @@ -417,12 +200,20 @@ const runCreateProject = ( }) ) if (command.runUp) { - yield* _(logDockerAccessInfo(resolvedOutDir, projectConfig)) + yield* _(logDockerAccessInfo(prepared.resolvedOutDir, prepared.projectConfig)) } - yield* _(maybeCleanupAfterAgent(waitForAgent, resolvedOutDir)) + yield* _(maybeCleanupAfterAgent(waitForAgent, prepared.resolvedOutDir)) + yield* _(maybeOpenSsh(command, hasAgent, waitForAgent, prepared.projectConfig)) + }).pipe(Effect.asVoid) - yield* _(maybeOpenSsh(command, hasAgent, waitForAgent, projectConfig)) +const runCreateProject = ( + path: Path.Path, + command: CreateCommand +): Effect.Effect => + Effect.gen(function*(_) { + const prepared = yield* _(prepareProject(path, command)) + yield* _(runPreparedProject(command, prepared)) }).pipe(Effect.asVoid) export const createProject = (command: CreateCommand): Effect.Effect => diff --git a/packages/app/src/lib/usecases/actions/ports.ts b/packages/app/src/lib/usecases/actions/ports.ts index 974e7aca..4ad92b28 100644 --- a/packages/app/src/lib/usecases/actions/ports.ts +++ b/packages/app/src/lib/usecases/actions/ports.ts @@ -9,7 +9,7 @@ import type { CreateCommand } from "../../core/domain.js" import type { PortProbeError } from "../../shell/errors.js" import { loadReservedPorts, selectAvailablePort } from "../ports-reserve.js" -const maxPortAttempts = 25 +const maxPortAttempts = 1024 export const resolveSshPort = ( config: CreateCommand["config"], diff --git a/packages/app/src/lib/usecases/auth-sync.ts b/packages/app/src/lib/usecases/auth-sync.ts index 3849755c..fd46e3d5 100644 --- a/packages/app/src/lib/usecases/auth-sync.ts +++ b/packages/app/src/lib/usecases/auth-sync.ts @@ -169,7 +169,14 @@ export const syncAuthArtifacts = ( yield* _(copyFileIfNeeded(sourceGlobal, targetGlobal)) yield* _(syncGithubTokenKeysInFile(sourceGlobal, targetGlobal)) yield* _(copyFileIfNeeded(sourceProject, targetProject)) - yield* _(copyCodexFile(fs, path, { sourceDir: sourceCodex, targetDir: targetCodex, fileName: "auth.json", label: "auth" })) + yield* _( + copyCodexFile(fs, path, { + sourceDir: sourceCodex, + targetDir: targetCodex, + fileName: "auth.json", + label: "auth" + }) + ) if (sourceCodex !== targetCodex) { yield* _( copyCodexFile(fs, path, { diff --git a/packages/app/src/lib/usecases/auto-open-ssh.ts b/packages/app/src/lib/usecases/auto-open-ssh.ts new file mode 100644 index 00000000..eb823d43 --- /dev/null +++ b/packages/app/src/lib/usecases/auto-open-ssh.ts @@ -0,0 +1 @@ +export { shouldAutoOpenSsh } from "../../shared/auto-open-ssh.js" diff --git a/packages/app/src/lib/usecases/projects-ssh.ts b/packages/app/src/lib/usecases/projects-ssh.ts index 3294e8e7..c89bf9d5 100644 --- a/packages/app/src/lib/usecases/projects-ssh.ts +++ b/packages/app/src/lib/usecases/projects-ssh.ts @@ -30,6 +30,15 @@ import { runDockerComposeUpWithPortCheck } from "./projects-up.js" import { buildEditorSshAccess, formatEditorSshAccessSummary } from "./ssh-access.js" import { ensureTerminalCursorVisible } from "./terminal-cursor.js" +export type PreparedProjectSsh = { + readonly item: ProjectItem + readonly cwd: string + readonly command: "ssh" + readonly args: ReadonlyArray +} + +type ProjectSshUpRequirements = CommandExecutor.CommandExecutor | FileSystem.FileSystem | Path.Path + const buildSshArgs = (item: ProjectItem): ReadonlyArray => { const host = item.ipAddress ?? "localhost" const port = item.ipAddress ? 22 : item.sshPort @@ -118,6 +127,32 @@ export const waitForProjectSshReady = ( ) } +export const prepareProjectSsh = (item: ProjectItem): PreparedProjectSsh => ({ + item, + cwd: process.cwd(), + command: "ssh", + args: buildSshArgs(item) +}) + +const connectPreparedProjectSsh = ( + prepared: PreparedProjectSsh +): Effect.Effect => + pipe( + ensureTerminalCursorVisible(), + Effect.zipRight( + runCommandWithExitCodes( + { + cwd: prepared.cwd, + command: prepared.command, + args: prepared.args + }, + [0, 130], + (exitCode) => new CommandFailedError({ command: prepared.command, exitCode }) + ) + ), + Effect.ensuring(ensureTerminalCursorVisible()) + ) + // CHANGE: connect to a project via SSH using its resolved settings // WHY: allow TUI to open a shell immediately after selection // QUOTE(ТЗ): "выбор проекта сразу подключает по SSH" @@ -131,21 +166,7 @@ export const waitForProjectSshReady = ( export const connectProjectSsh = ( item: ProjectItem ): Effect.Effect => - pipe( - ensureTerminalCursorVisible(), - Effect.zipRight( - runCommandWithExitCodes( - { - cwd: process.cwd(), - command: "ssh", - args: buildSshArgs(item) - }, - [0, 130], - (exitCode) => new CommandFailedError({ command: "ssh", exitCode }) - ) - ), - Effect.ensuring(ensureTerminalCursorVisible()) - ) + connectPreparedProjectSsh(prepareProjectSsh(item)) // CHANGE: ensure docker compose is up before SSH connection // WHY: selected project should auto-start when not running @@ -166,7 +187,24 @@ export const connectProjectSshWithUp = ( | PortProbeError | DockerCommandError | PlatformError, - CommandExecutor.CommandExecutor | FileSystem.FileSystem | Path.Path + ProjectSshUpRequirements +> => + prepareProjectSshWithUp(item).pipe( + Effect.flatMap((prepared) => connectPreparedProjectSsh(prepared)) + ) + +export const prepareProjectSshWithUp = ( + item: ProjectItem +): Effect.Effect< + PreparedProjectSsh, + | CommandFailedError + | ConfigNotFoundError + | ConfigDecodeError + | FileExistsError + | PortProbeError + | DockerCommandError + | PlatformError, + ProjectSshUpRequirements > => Effect.gen(function*(_) { const fs = yield* _(FileSystem.FileSystem) @@ -193,7 +231,7 @@ export const connectProjectSshWithUp = ( } yield* _(waitForProjectSshReady(updated)) - yield* _(connectProjectSsh(updated)) + return prepareProjectSsh(updated) }) // CHANGE: show docker compose status for all known docker-git projects diff --git a/packages/app/src/lib/usecases/projects.ts b/packages/app/src/lib/usecases/projects.ts index 7a520144..dfeea6fe 100644 --- a/packages/app/src/lib/usecases/projects.ts +++ b/packages/app/src/lib/usecases/projects.ts @@ -17,6 +17,9 @@ export { connectProjectSsh, connectProjectSshWithUp, listProjectStatus, + type PreparedProjectSsh, + prepareProjectSsh, + prepareProjectSshWithUp, probeProjectSshReady, waitForProjectSshReady } from "./projects-ssh.js" diff --git a/packages/app/src/lib/usecases/scrap-session-export.ts b/packages/app/src/lib/usecases/scrap-session-export.ts index 519255ba..2e364af9 100644 --- a/packages/app/src/lib/usecases/scrap-session-export.ts +++ b/packages/app/src/lib/usecases/scrap-session-export.ts @@ -126,7 +126,8 @@ const detectRebuildCommands = ( const script = [ "set -e", `cd ${targetDir}`, - // Priority: pnpm > npm > yarn. Keep commands deterministic and rebuildable. + // Priority: bun > pnpm > npm > yarn. Keep commands deterministic and rebuildable. + "if [ -f bun.lock ]; then echo 'bun install --frozen-lockfile'; exit 0; fi", "if [ -f pnpm-lock.yaml ]; then echo 'pnpm install --frozen-lockfile'; exit 0; fi", "if [ -f package-lock.json ]; then echo 'npm ci'; exit 0; fi", "if [ -f yarn.lock ]; then echo 'yarn install --frozen-lockfile'; exit 0; fi", diff --git a/packages/app/src/shared/auth-menu-request.ts b/packages/app/src/shared/auth-menu-request.ts new file mode 100644 index 00000000..70fc39f9 --- /dev/null +++ b/packages/app/src/shared/auth-menu-request.ts @@ -0,0 +1,12 @@ +export type AuthMenuRequestBody = { + readonly flow: string + readonly label?: string | null + readonly token?: string | null + readonly user?: string | null + readonly apiKey?: string | null +} + +export type ProjectAuthMenuRequestBody = { + readonly flow: string + readonly label?: string | null +} diff --git a/packages/app/src/shared/auto-open-ssh.ts b/packages/app/src/shared/auto-open-ssh.ts new file mode 100644 index 00000000..77d6174f --- /dev/null +++ b/packages/app/src/shared/auto-open-ssh.ts @@ -0,0 +1,27 @@ +import { Effect } from "effect" + +type AutoOpenSshOptions = { + readonly shouldOpen: boolean + readonly runUp: boolean +} + +const isInteractiveTty = (): boolean => process.stdin.isTTY && process.stdout.isTTY + +export const shouldAutoOpenSsh = ({ + runUp, + shouldOpen +}: AutoOpenSshOptions): Effect.Effect => + Effect.gen(function*(_) { + if (!shouldOpen) { + return false + } + if (!runUp) { + yield* _(Effect.logWarning("Skipping SSH auto-open: docker compose up disabled (--no-up).")) + return false + } + if (!isInteractiveTty()) { + yield* _(Effect.logWarning("Skipping SSH auto-open: not running in an interactive TTY.")) + return false + } + return true + }) diff --git a/packages/app/src/shared/json-schema.ts b/packages/app/src/shared/json-schema.ts new file mode 100644 index 00000000..65c83b40 --- /dev/null +++ b/packages/app/src/shared/json-schema.ts @@ -0,0 +1,16 @@ +import * as Schema from "@effect/schema/Schema" + +export type JsonPrimitive = boolean | number | string | null +export type JsonValue = JsonPrimitive | JsonObject | ReadonlyArray +export type JsonObject = Readonly<{ [key: string]: JsonValue }> + +export const JsonValueSchema: Schema.Schema = Schema.suspend(() => + Schema.Union( + Schema.Null, + Schema.Boolean, + Schema.Number, + Schema.String, + Schema.Array(JsonValueSchema), + Schema.Record({ key: Schema.String, value: JsonValueSchema }) + ) +) diff --git a/packages/app/src/shared/optional-text.ts b/packages/app/src/shared/optional-text.ts new file mode 100644 index 00000000..45000db9 --- /dev/null +++ b/packages/app/src/shared/optional-text.ts @@ -0,0 +1,4 @@ +export const normalizeOptionalText = (value: string | undefined): string | null => { + const trimmed = value?.trim() ?? "" + return trimmed.length === 0 ? null : trimmed +} diff --git a/packages/app/src/shared/terminal-session-schema.ts b/packages/app/src/shared/terminal-session-schema.ts new file mode 100644 index 00000000..b0dd8b71 --- /dev/null +++ b/packages/app/src/shared/terminal-session-schema.ts @@ -0,0 +1,43 @@ +import * as Schema from "@effect/schema/Schema" + +export const TerminalSessionSchema = Schema.Struct({ + id: Schema.String, + projectId: Schema.String, + sshCommand: Schema.String, + status: Schema.Union( + Schema.Literal("ready"), + Schema.Literal("attached"), + Schema.Literal("exited"), + Schema.Literal("failed") + ), + createdAt: Schema.String, + startedAt: Schema.optional(Schema.String), + closedAt: Schema.optional(Schema.String), + exitCode: Schema.optional(Schema.Number), + signal: Schema.optional(Schema.Number) +}) + +const TerminalServerMessagePayloadSchema = Schema.Union( + Schema.Struct({ + type: Schema.Literal("ready"), + session: TerminalSessionSchema + }), + Schema.Struct({ + type: Schema.Literal("output"), + data: Schema.String + }), + Schema.Struct({ + type: Schema.Literal("exit"), + exitCode: Schema.NullOr(Schema.Number), + signal: Schema.NullOr(Schema.Number) + }), + Schema.Struct({ + type: Schema.Literal("error"), + message: Schema.String + }) +) + +export const TerminalServerMessageSchema = Schema.parseJson(TerminalServerMessagePayloadSchema) + +export type TerminalSession = Schema.Schema.Type +export type TerminalServerMessage = Schema.Schema.Type diff --git a/packages/app/src/shared/trimmed-text.ts b/packages/app/src/shared/trimmed-text.ts new file mode 100644 index 00000000..bf69c9b2 --- /dev/null +++ b/packages/app/src/shared/trimmed-text.ts @@ -0,0 +1,4 @@ +export const trimToUndefined = (value: string | undefined): string | undefined => { + const trimmed = value?.trim() ?? "" + return trimmed.length > 0 ? trimmed : undefined +} diff --git a/packages/app/src/ui/primitives-gridland.tsx b/packages/app/src/ui/primitives-gridland.tsx new file mode 100644 index 00000000..9e6ae898 --- /dev/null +++ b/packages/app/src/ui/primitives-gridland.tsx @@ -0,0 +1,68 @@ +import type { JSX } from "react" + +import type { GridlandInputProps, GridlandModule } from "@gridland/bun" + +import type { UiBoxProps, UiButtonProps, UiTextInputProps, UiTextProps } from "./primitives.js" + +const renderInputValue = (props: UiTextInputProps): string => { + if (props.value.length === 0) { + return props.placeholder ?? "" + } + return props.secret ? "*".repeat(props.value.length) : props.value +} + +const inputProps = (props: UiTextInputProps): GridlandInputProps => ({ + ariaLabel: props.ariaLabel, + autoFocus: props.autoFocus, + placeholder: props.placeholder, + value: renderInputValue(props) +}) + +export const createGridlandPrimitives = (gridland: GridlandModule) => { + const GridlandBox = gridland.Box + const GridlandInput = gridland.Input + const GridlandText = gridland.Text + + return { + Box: ({ children, ...props }: UiBoxProps): JSX.Element => ( + + {children} + + ), + Button: ({ label }: UiButtonProps): JSX.Element => [{label}], + Text: ({ children, ...props }: UiTextProps): JSX.Element => ( + + {children} + + ), + TextInput: (props: UiTextInputProps): JSX.Element => + } as const +} diff --git a/packages/app/src/ui/primitives-web.tsx b/packages/app/src/ui/primitives-web.tsx new file mode 100644 index 00000000..3a5bbd30 --- /dev/null +++ b/packages/app/src/ui/primitives-web.tsx @@ -0,0 +1,138 @@ +import { createElement, type CSSProperties, type JSX } from "react" + +import type { UiBoxProps, UiButtonProps, UiTextInputProps, UiTextProps } from "./primitives.js" + +const unit = (value: number | string | undefined): string | number | undefined => { + if (value === undefined) { + return undefined + } + return typeof value === "number" ? `${value * 8}px` : value +} + +const borderRadius = (borderStyle: UiBoxProps["borderStyle"]): string => borderStyle === "rounded" ? "12px" : "0" + +const borderValue = ( + enabled: boolean | undefined, + borderColor: string | undefined +): string | undefined => enabled ? `1px solid ${borderColor ?? "#24537d"}` : undefined + +const baseStyle = (props: UiBoxProps): CSSProperties => ({ + alignItems: props.alignItems, + backgroundColor: props.backgroundColor, + border: borderValue(props.border, props.borderColor), + borderRadius: borderRadius(props.borderStyle), + boxSizing: "border-box", + color: props.fg, + display: "flex", + flexDirection: props.flexDirection ?? "row", + flexGrow: props.flexGrow, + flexWrap: props.flexWrap, + gap: unit(props.gap), + height: unit(props.height), + justifyContent: props.justifyContent, + marginBottom: unit(props.marginBottom), + marginLeft: unit(props.marginLeft), + marginRight: unit(props.marginRight), + marginTop: unit(props.marginTop), + padding: unit(props.padding), + width: unit(props.width) +}) + +const textStyle = (props: UiTextProps): CSSProperties => ({ + ...baseStyle(props), + display: "block", + fontWeight: props.bold ? 700 : 400, + lineHeight: 1.45, + overflow: props.wrap === "truncate" ? "hidden" : undefined, + textOverflow: props.wrap === "truncate" ? "ellipsis" : undefined, + whiteSpace: props.wrap === "truncate" ? "nowrap" : "pre-wrap", + width: unit(props.width) ?? "auto" +}) + +const interactiveStyle = (width: UiBoxProps["width"]): CSSProperties => ({ + background: "transparent", + cursor: "pointer", + font: "inherit", + textAlign: "left", + width: width === undefined ? "100%" : unit(width) +}) + +const inputStyle: CSSProperties = { + background: "#07101c", + border: "1px solid #24537d", + borderRadius: "10px", + boxSizing: "border-box", + color: "#56f39a", + font: "inherit", + outline: "none", + padding: "10px 12px", + width: "100%" +} + +const buttonStyle: CSSProperties = { + background: "#10253c", + border: "1px solid #24537d", + borderRadius: "8px", + color: "#d6e5f7", + cursor: "pointer", + font: "inherit", + padding: "8px 12px" +} + +export const webPrimitives = { + Box: ({ children, onClick, ...props }: UiBoxProps): JSX.Element => + createElement(onClick === undefined ? "div" : "button", { + children, + onClick, + style: { + ...baseStyle(props), + ...(onClick === undefined ? {} : interactiveStyle(props.width)) + }, + type: onClick === undefined ? undefined : "button" + }), + Button: ({ label, onPress }: UiButtonProps): JSX.Element => ( + + ), + Text: ({ children, ...props }: UiTextProps): JSX.Element => + createElement("div", { + children, + style: textStyle(props) + }), + TextInput: ({ + ariaLabel, + autoFocus, + onChange, + onEnter, + onEscape, + placeholder, + secret, + value + }: UiTextInputProps): JSX.Element => ( + { + onChange(event.currentTarget.value) + }} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault() + event.stopPropagation() + onEnter?.(event.shiftKey) + return + } + if (event.key === "Escape") { + event.preventDefault() + event.stopPropagation() + onEscape?.() + } + }} + placeholder={placeholder} + style={inputStyle} + type={secret ? "password" : "text"} + value={value} + /> + ) +} as const diff --git a/packages/app/src/ui/primitives.tsx b/packages/app/src/ui/primitives.tsx new file mode 100644 index 00000000..596bd735 --- /dev/null +++ b/packages/app/src/ui/primitives.tsx @@ -0,0 +1,104 @@ +import { + type ComponentType, + createContext, + type CSSProperties, + type JSX, + type MouseEventHandler, + type ReactNode, + useContext +} from "react" + +export type UiBoxProps = { + readonly alignItems?: CSSProperties["alignItems"] + readonly backgroundColor?: string + readonly border?: boolean + readonly borderColor?: string + readonly borderStyle?: "rounded" | "single" + readonly children?: ReactNode + readonly fg?: string + readonly flexDirection?: CSSProperties["flexDirection"] + readonly flexGrow?: number + readonly flexWrap?: CSSProperties["flexWrap"] + readonly gap?: number | string + readonly height?: number | string + readonly justifyContent?: CSSProperties["justifyContent"] + readonly marginBottom?: number | string + readonly marginLeft?: number | string + readonly marginRight?: number | string + readonly marginTop?: number | string + readonly onClick?: MouseEventHandler | (() => void) + readonly padding?: number | string + readonly width?: number | string +} + +export type UiTextProps = UiBoxProps & { + readonly bold?: boolean + readonly wrap?: "truncate" | "wrap" +} + +export type UiButtonProps = { + readonly label: string + readonly onPress: () => void +} + +export type UiTextInputProps = { + readonly ariaLabel: string + readonly autoFocus?: boolean + readonly onChange: (value: string) => void + readonly onEnter?: (shift: boolean) => void + readonly onEscape?: () => void + readonly placeholder?: string + readonly secret?: boolean + readonly value: string +} + +type UiPrimitives = { + readonly Box: ComponentType + readonly Button: ComponentType + readonly Text: ComponentType + readonly TextInput: ComponentType +} + +const UiPrimitivesContext = createContext(null) + +const useUiPrimitives = (): UiPrimitives => { + const primitives = useContext(UiPrimitivesContext) + if (primitives === null) { + throw new Error("UI primitives provider is missing.") + } + return primitives +} + +export const UiProvider = ( + { + children, + primitives + }: { + readonly children?: ReactNode + readonly primitives: UiPrimitives + } +): JSX.Element => ( + + {children} + +) + +export const Box = (props: UiBoxProps): JSX.Element => { + const primitive = useUiPrimitives() + return +} + +export const Text = (props: UiTextProps): JSX.Element => { + const primitive = useUiPrimitives() + return +} + +export const Button = (props: UiButtonProps): JSX.Element => { + const primitive = useUiPrimitives() + return +} + +export const TextInput = (props: UiTextInputProps): JSX.Element => { + const primitive = useUiPrimitives() + return +} diff --git a/packages/app/src/ui/shared.tsx b/packages/app/src/ui/shared.tsx new file mode 100644 index 00000000..6698c2ed --- /dev/null +++ b/packages/app/src/ui/shared.tsx @@ -0,0 +1,195 @@ +import type { JSX, ReactNode } from "react" + +import type { ActionPromptState } from "../web/action-prompt.js" +import { Box, Button, Text, TextInput } from "./primitives.js" + +export const ScreenLayout = ( + { + body, + message, + title + }: { + readonly body: ReadonlyArray + readonly message: string | null | undefined + readonly title: string + } +): JSX.Element => ( + + {title} + {body} + {message === undefined || message === null || message.length === 0 + ? null + : ( + + {message} + + )} + +) + +export const SelectableList = ( + { + labels, + selectedIndex + }: { + readonly labels: ReadonlyArray + readonly selectedIndex: number + } +): ReadonlyArray => + labels.map((label, index) => ( + + {index === selectedIndex ? "> " : " "} + {label} + + )) + +export const HelpLines = ({ lines }: { readonly lines: ReadonlyArray }): JSX.Element => ( + + {lines.map((line, index) => {line})} + +) + +export const SnapshotLine = ( + { label, value }: { readonly label: string; readonly value: ReactNode } +): JSX.Element => {label}: {value} + +export const ActionLine = ( + { + hint, + label, + onClick + }: { + readonly hint?: string + readonly label: string + readonly onClick?: () => void + } +): JSX.Element => ( + + {label} + {hint === undefined ? null : {hint}} + +) + +export const PromptScreen = ( + { + header, + helpLines, + message, + prompt, + title, + value + }: { + readonly header: ReadonlyArray + readonly helpLines: ReadonlyArray + readonly message: string | null | undefined + readonly prompt: string + readonly title: string + readonly value: string + } +): JSX.Element => + ScreenLayout({ + title, + body: [ + ...header, + ( + + {prompt}: + {value} + + ), + + ], + message + }) + +const ActionPromptField = ( + { + actionPrompt, + index, + onActionPromptCancel, + onActionPromptChange, + onActionPromptSubmit + }: { + readonly actionPrompt: ActionPromptState + readonly index: number + readonly onActionPromptCancel: () => void + readonly onActionPromptChange: (key: string, value: string) => void + readonly onActionPromptSubmit: () => void + } +): JSX.Element => { + const step = actionPrompt.steps[index] + if (step === undefined) { + return <> + } + return ( + + {step.label} + { + onActionPromptChange(step.key, value) + }} + onEnter={() => { + onActionPromptSubmit() + }} + onEscape={onActionPromptCancel} + secret={step.secret} + value={actionPrompt.values[step.key] ?? ""} + /> + + ) +} + +export const ActionPromptPanel = ( + { + actionPrompt, + onActionPromptCancel, + onActionPromptChange, + onActionPromptSubmit + }: { + readonly actionPrompt: ActionPromptState + readonly onActionPromptCancel: () => void + readonly onActionPromptChange: (key: string, value: string) => void + readonly onActionPromptSubmit: () => void + } +): JSX.Element => ( + + {actionPrompt.title} + Enter = submit, Esc = cancel. + + {actionPrompt.steps.map((step, index) => ( + + ))} + + + + +) + +export const TerminalPanel = ( + { onClose, onMessage, session }: TerminalPanelProps +): JSX.Element => { + const connectionRef = useRef({ opened: false }) + const hostRef = useRef(null) + const [status, setStatus] = useState("connecting") + const notifyMessage = useEffectEvent(onMessage) + + useTerminalSessionLifecycle({ + connectionRef, + hostRef, + notifyMessage, + session, + setStatus + }) + + return ( +
+ +
+
+ ) +} diff --git a/packages/app/src/web/panels.tsx b/packages/app/src/web/panels.tsx new file mode 100644 index 00000000..c8f7b230 --- /dev/null +++ b/packages/app/src/web/panels.tsx @@ -0,0 +1,10 @@ +export { ContentPanel } from "./panel-content.js" +export { + ErrorScreen, + LoadingScreen, + MenuSidebar, + OutputPanel, + ProjectListPanel, + projectSelectionLabel, + showsProjectPanel +} from "./panel-layout.js" diff --git a/packages/app/src/web/project-events.ts b/packages/app/src/web/project-events.ts new file mode 100644 index 00000000..ccb852e2 --- /dev/null +++ b/packages/app/src/web/project-events.ts @@ -0,0 +1,167 @@ +import { Effect } from "effect" + +import { asObject, asString } from "../docker-git/api-json.js" +import type { JsonValue } from "../docker-git/api-json.js" +import type { ApiEvent } from "./api.js" +import { loadProjectEvents } from "./api.js" + +type EventStreamControls = { + readonly close: () => void +} + +type EventStreamHandlers = { + readonly onLine: (line: string) => void + readonly onRateLimit: () => void +} + +const readPayloadString = ( + payload: JsonValue | undefined, + key: string +): string | null => { + const object = asObject(payload) + if (object === null) { + return null + } + return asString(object[key]) +} + +const formatStatusLine = (payload: JsonValue | undefined): string | null => { + const phase = readPayloadString(payload, "phase") + const message = readPayloadString(payload, "message") + if (message === null) { + return null + } + return phase === null ? message : `[${phase}] ${message}` +} + +const formatLogLine = (payload: JsonValue | undefined): string | null => readPayloadString(payload, "line") + +const formatSshLine = (payload: JsonValue | undefined): string | null => { + const phase = readPayloadString(payload, "phase") + const sessionId = readPayloadString(payload, "sessionId") + if (phase === null) { + return null + } + if (sessionId === null) { + return `[ssh] ${phase}` + } + return `[ssh] ${phase} (${sessionId})` +} + +const formatEventLine = (event: ApiEvent): string | null => { + if (event.type === "project.deployment.status") { + return formatStatusLine(event.payload) + } + if (event.type === "project.deployment.log") { + return formatLogLine(event.payload) + } + if (event.type === "project.ssh.session") { + return formatSshLine(event.payload) + } + return null +} + +type PollState = { + closed: boolean + cursor: number | undefined + timeout: ReturnType | null +} + +type EventPollSuccess = { + readonly cursor: number + readonly events: ReadonlyArray +} + +const schedulePoll = ( + state: PollState, + runPoll: () => void, + delayMs: number +): void => { + state.timeout = globalThis.setTimeout(runPoll, delayMs) +} + +const handlePollFailure = ( + state: PollState, + onLine: (line: string) => void, + onRateLimit: () => void, + error: string, + runPoll: () => void +): void => { + if (state.closed) { + return + } + if (error.includes("HTTP 429")) { + onRateLimit() + return + } + onLine(`[events] ${error}`) + schedulePoll(state, runPoll, 1000) +} + +const handlePollSuccess = ( + state: PollState, + onLine: (line: string) => void, + response: EventPollSuccess, + runPoll: () => void +): void => { + if (state.closed) { + return + } + const isInitialPoll = state.cursor === undefined + if (isInitialPoll) { + onLine("[events] connected") + } + state.cursor = response.cursor + for (const event of response.events) { + const line = formatEventLine(event) + if (line !== null) { + onLine(line) + } + } + let delayMs = 150 + if (isInitialPoll) { + delayMs = 100 + } else if (response.events.length === 0) { + delayMs = 500 + } + schedulePoll(state, runPoll, delayMs) +} + +export const openProjectEventStream = ( + projectId: string, + { onLine, onRateLimit }: EventStreamHandlers +): EventStreamControls => { + const state: PollState = { + closed: false, + cursor: undefined, + timeout: null + } + + const runPoll = () => { + void Effect.runPromise( + poll().pipe( + Effect.match({ + onFailure: (error) => { + handlePollFailure(state, onLine, onRateLimit, error, runPoll) + }, + onSuccess: (response) => { + handlePollSuccess(state, onLine, response, runPoll) + } + }) + ) + ) + } + + const poll = () => loadProjectEvents(projectId, state.cursor) + + runPoll() + + return { + close: () => { + state.closed = true + if (state.timeout !== null) { + globalThis.clearTimeout(state.timeout) + } + } + } +} diff --git a/packages/app/src/web/terminal-panel-runtime.ts b/packages/app/src/web/terminal-panel-runtime.ts new file mode 100644 index 00000000..3f122ff5 --- /dev/null +++ b/packages/app/src/web/terminal-panel-runtime.ts @@ -0,0 +1,280 @@ +import { Effect } from "effect" +import { useEffect } from "react" +import { Terminal } from "xterm" +import { FitAddon } from "xterm-addon-fit" + +import { deleteTerminalSessionByPath } from "./api.js" +import type { ActiveTerminalSession } from "./terminal.js" +import { parseTerminalServerMessage, resolveTerminalWebSocketUrl } from "./terminal.js" + +export type TerminalStatus = "attached" | "connecting" | "error" | "exited" + +export type TerminalConnectionState = { + opened: boolean +} + +type TerminalRuntime = { + readonly fitAddon: FitAddon + readonly terminal: Terminal +} + +type TerminalMessageHandlers = { + readonly notifyMessage: (message: string) => void + readonly session: ActiveTerminalSession + readonly setStatus: (status: TerminalStatus) => void + readonly terminal: Terminal +} + +type TerminalCleanupArgs = { + readonly removeInput: () => void + readonly removeResize: () => void + readonly resizeObserver: ResizeObserver | null + readonly socket: WebSocket + readonly terminal: Terminal +} + +type TerminalLifecycleArgs = { + readonly connectionRef: { current: TerminalConnectionState } + readonly hostRef: { readonly current: HTMLDivElement | null } + readonly notifyMessage: (message: string) => void + readonly session: ActiveTerminalSession + readonly setStatus: (status: TerminalStatus) => void +} + +const requestSessionClose = (closePath: string): void => { + void Effect.runPromise(deleteTerminalSessionByPath(closePath).pipe(Effect.either, Effect.asVoid)) +} + +const createTerminalRuntime = (host: HTMLDivElement): TerminalRuntime => { + const terminal = new Terminal({ + convertEol: false, + cursorBlink: true, + fontFamily: "'IBM Plex Mono', 'SFMono-Regular', monospace", + fontSize: 14, + theme: { + background: "#050b14", + foreground: "#d6e5f7" + } + }) + const fitAddon = new FitAddon() + terminal.loadAddon(fitAddon) + terminal.open(host) + fitAddon.fit() + terminal.focus() + return { fitAddon, terminal } +} + +const createTerminalSocket = ( + session: ActiveTerminalSession, + terminal: Terminal +): WebSocket => + new WebSocket( + resolveTerminalWebSocketUrl( + session.websocketPath, + terminal.cols, + terminal.rows + ) + ) + +const sendTerminalResize = ( + fitAddon: FitAddon, + socket: WebSocket, + terminal: Terminal +): void => { + fitAddon.fit() + if (socket.readyState !== WebSocket.OPEN) { + return + } + socket.send(JSON.stringify({ + cols: terminal.cols, + rows: terminal.rows, + type: "resize" + })) +} + +const observeTerminalResize = ( + host: HTMLDivElement, + onResize: () => void +): ResizeObserver | null => { + if (typeof ResizeObserver !== "function") { + return null + } + const resizeObserver = new ResizeObserver(() => { + onResize() + }) + resizeObserver.observe(host) + return resizeObserver +} + +const attachTerminalInput = ( + terminal: Terminal, + socket: WebSocket +) => + terminal.onData((data) => { + if (socket.readyState !== WebSocket.OPEN) { + return + } + socket.send(JSON.stringify({ data, type: "input" })) + }) + +const handleTerminalServerMessage = ( + handlers: TerminalMessageHandlers, + payload: string +): void => { + const message = parseTerminalServerMessage(payload) + if (message === null) { + handlers.terminal.writeln("\r\n[terminal protocol error]") + handlers.setStatus("error") + handlers.notifyMessage("Terminal protocol error.") + return + } + if (message.type === "ready") { + handlers.setStatus("attached") + handlers.notifyMessage(handlers.session.readyMessage) + handlers.session.onReady?.() + return + } + if (message.type === "output") { + handlers.terminal.write(message.data) + return + } + if (message.type === "error") { + handlers.terminal.writeln(`\r\n[error] ${message.message}`) + handlers.setStatus("error") + handlers.notifyMessage(message.message) + return + } + handlers.terminal.writeln("\r\n[session ended]") + handlers.setStatus("exited") + handlers.notifyMessage(handlers.session.exitMessage) + handlers.session.onExit?.() +} + +const attachTerminalSocketListeners = ( + connectionRef: { current: TerminalConnectionState }, + socket: WebSocket, + onOpen: () => void, + onMessage: (payload: string) => void, + onError: () => void +): void => { + socket.addEventListener("open", () => { + connectionRef.current.opened = true + onOpen() + }) + socket.addEventListener("message", (event) => { + onMessage(typeof event.data === "string" ? event.data : "") + }) + socket.addEventListener("error", onError) +} + +const cleanupTerminalResources = ( + { removeInput, removeResize, resizeObserver, socket, terminal }: TerminalCleanupArgs +): void => { + removeInput() + resizeObserver?.disconnect() + removeResize() + if (socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify({ type: "close" })) + } + socket.close() + terminal.dispose() +} + +const createMessageHandlers = ( + notifyMessage: (message: string) => void, + session: ActiveTerminalSession, + setStatus: (status: TerminalStatus) => void, + terminal: Terminal +): TerminalMessageHandlers => ({ + notifyMessage, + session, + setStatus, + terminal +}) + +const createSocketErrorHandler = ( + notifyMessage: (message: string) => void, + setStatus: (status: TerminalStatus) => void, + terminal: Terminal +) => +() => { + terminal.writeln("\r\n[websocket error]") + setStatus("error") + notifyMessage("Terminal websocket error.") +} + +const maybeDeletePendingSession = ( + connectionRef: { current: TerminalConnectionState }, + notifyMessage: (message: string) => void, + session: ActiveTerminalSession +): void => { + if (!connectionRef.current.opened) { + requestSessionClose(session.closePath) + notifyMessage(session.pendingDeleteMessage) + session.onExit?.() + } +} + +const mountTerminalSession = ( + { connectionRef, hostRef, notifyMessage, session, setStatus }: TerminalLifecycleArgs +): (() => void) | undefined => { + const host = hostRef.current + if (host === null) { + return undefined + } + + connectionRef.current = { opened: false } + const { fitAddon, terminal } = createTerminalRuntime(host) + const socket = createTerminalSocket(session, terminal) + const sendResize = () => { + sendTerminalResize(fitAddon, socket, terminal) + } + const resizeObserver = observeTerminalResize(host, sendResize) + const inputDisposable = attachTerminalInput(terminal, socket) + const handlers = createMessageHandlers( + notifyMessage, + session, + setStatus, + terminal + ) + + globalThis.addEventListener("resize", sendResize) + attachTerminalSocketListeners( + connectionRef, + socket, + sendResize, + (payload) => { + handleTerminalServerMessage(handlers, payload) + }, + createSocketErrorHandler(notifyMessage, setStatus, terminal) + ) + + return () => { + cleanupTerminalResources({ + removeInput: () => { + inputDisposable.dispose() + }, + removeResize: () => { + globalThis.removeEventListener("resize", sendResize) + }, + resizeObserver, + socket, + terminal + }) + maybeDeletePendingSession(connectionRef, notifyMessage, session) + } +} + +export const useTerminalSessionLifecycle = ( + { connectionRef, hostRef, notifyMessage, session, setStatus }: TerminalLifecycleArgs +): void => { + useEffect(() => { + return mountTerminalSession({ + connectionRef, + hostRef, + notifyMessage, + session, + setStatus + }) + }, [connectionRef, hostRef, session, setStatus]) +} diff --git a/packages/app/src/web/terminal.ts b/packages/app/src/web/terminal.ts new file mode 100644 index 00000000..2f9fb8f5 --- /dev/null +++ b/packages/app/src/web/terminal.ts @@ -0,0 +1,61 @@ +import * as ParseResult from "@effect/schema/ParseResult" +import { Either } from "effect" + +import { TerminalServerMessageSchema } from "../shared/terminal-session-schema.js" +import type { TerminalServerMessage as ParsedTerminalServerMessage } from "../shared/terminal-session-schema.js" +import { resolveApiBaseUrl, trimTrailingSlash } from "./api-http.js" +import type { TerminalSession } from "./api-schema.js" + +export type ActiveTerminalSession = { + readonly closePath: string + readonly exitMessage: string + readonly header: string + readonly onExit?: () => void + readonly onReady?: () => void + readonly pendingDeleteMessage: string + readonly readyMessage: string + readonly session: TerminalSession + readonly subtitle: string + readonly websocketPath: string +} + +const resolveTerminalApiBaseUrl = (): string => { + const configured = import.meta.env.VITE_DOCKER_GIT_TERMINAL_API_BASE_URL + if (configured !== undefined && configured.trim().length > 0) { + return trimTrailingSlash(configured.trim()) + } + + const apiBaseUrl = resolveApiBaseUrl() + if (apiBaseUrl.startsWith("http://") || apiBaseUrl.startsWith("https://")) { + return apiBaseUrl + } + + if (globalThis.location.protocol === "http:") { + const apiPort = import.meta.env.VITE_DOCKER_GIT_TERMINAL_API_PORT?.trim() || "3334" + return `http://${globalThis.location.hostname}:${apiPort}` + } + + return apiBaseUrl +} + +const resolveApiUrl = (): URL => { + const configured = resolveTerminalApiBaseUrl() + if (configured.startsWith("http://") || configured.startsWith("https://")) { + return new URL(configured) + } + return new URL(configured, globalThis.location.origin) +} + +export const resolveTerminalWebSocketUrl = (websocketPath: string, cols: number, rows: number): string => { + const apiUrl = resolveApiUrl() + apiUrl.protocol = apiUrl.protocol === "https:" ? "wss:" : "ws:" + apiUrl.pathname = `${apiUrl.pathname.replace(/\/$/u, "")}${websocketPath}` + apiUrl.searchParams.set("cols", String(cols)) + apiUrl.searchParams.set("rows", String(rows)) + return apiUrl.toString() +} + +export const parseTerminalServerMessage = (value: string): ParsedTerminalServerMessage | null => + Either.getOrNull(ParseResult.decodeUnknownEither(TerminalServerMessageSchema)(value)) + +export { type TerminalServerMessage } from "../shared/terminal-session-schema.js" diff --git a/packages/app/src/web/vite-env.d.ts b/packages/app/src/web/vite-env.d.ts new file mode 100644 index 00000000..5a7a5309 --- /dev/null +++ b/packages/app/src/web/vite-env.d.ts @@ -0,0 +1,11 @@ +/// + +interface ImportMetaEnv { + readonly VITE_DOCKER_GIT_API_BASE_URL?: string + readonly VITE_DOCKER_GIT_TERMINAL_API_BASE_URL?: string + readonly VITE_DOCKER_GIT_TERMINAL_API_PORT?: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/packages/app/tests/app/main.test.ts b/packages/app/tests/app/main.test.ts index 5d953c1c..9c52b143 100644 --- a/packages/app/tests/app/main.test.ts +++ b/packages/app/tests/app/main.test.ts @@ -32,7 +32,7 @@ type UsageCase = { } const usageCases: ReadonlyArray = [ - { argv: ["node", "main"], needle: "pnpm docker-git" }, + { argv: ["node", "main"], needle: "bun run docker-git" }, { argv: ["node", "main", "Alice"], needle: "Usage:" } ] diff --git a/packages/app/tests/docker-git/app-ready-shortcuts.test.ts b/packages/app/tests/docker-git/app-ready-shortcuts.test.ts new file mode 100644 index 00000000..aa8574f2 --- /dev/null +++ b/packages/app/tests/docker-git/app-ready-shortcuts.test.ts @@ -0,0 +1,150 @@ +import { describe, expect, it, vi } from "vitest" + +import type { DashboardData } from "../../src/web/api.js" +import { + handleMenuNavigationKey, + handleProjectNavigationKey, + shortcutHintText, + type ShortcutKeyboardEvent, + shouldRefreshProjectDetails, + usesProjectPrimaryNavigation +} from "../../src/web/app-ready-shortcuts.js" + +const makeEvent = (key: string): ShortcutKeyboardEvent => { + const event: ShortcutKeyboardEvent = { + altKey: false, + ctrlKey: false, + defaultPrevented: false, + key, + metaKey: false, + target: null, + preventDefault: () => { + event.defaultPrevented = true + } + } + return event +} + +const runProjectNavigation = (projectNavigationArmed: boolean) => { + const event = makeEvent("ArrowDown") + const setSelectedProjectId = vi.fn() + const handled = handleProjectNavigationKey(event, { + currentMenu: "Select", + dashboard, + projectNavigationArmed, + selectedProjectId: "project-a", + setSelectedProjectId + }) + + return { handled, setSelectedProjectId } +} + +const dashboard: DashboardData = { + apiBaseUrl: "/api", + health: { + cwd: process.cwd(), + ok: true, + projectsRoot: "/home/dev/.docker-git", + revision: null + }, + projects: [ + { + clonedOnHostname: "host", + displayName: "org/repo-a", + id: "project-a", + repoUrl: "https://github.com/org/repo-a.git", + repoRef: "main", + sshSessions: 0, + startedAtEpochMs: null, + startedAtIso: null, + status: "stopped", + statusLabel: "Stopped" + }, + { + clonedOnHostname: "host", + displayName: "org/repo-b", + id: "project-b", + repoUrl: "https://github.com/org/repo-b.git", + repoRef: "main", + sshSessions: 1, + startedAtEpochMs: null, + startedAtIso: null, + status: "running", + statusLabel: "Up" + } + ] +} + +describe("app-ready-shortcuts", () => { + it("uses project-first arrows in Select-like screens", () => { + expect(usesProjectPrimaryNavigation("Select")).toBe(true) + expect(usesProjectPrimaryNavigation("Info")).toBe(false) + expect(usesProjectPrimaryNavigation("ProjectAuth")).toBe(false) + expect(usesProjectPrimaryNavigation("Create")).toBe(false) + }) + + it("does not move projects in Select until project mode is armed", () => { + const { handled, setSelectedProjectId } = runProjectNavigation(false) + + expect(handled).toBe(false) + expect(setSelectedProjectId).not.toHaveBeenCalled() + }) + + it("moves projects with up/down in armed Select", () => { + const { handled, setSelectedProjectId } = runProjectNavigation(true) + + expect(handled).toBe(true) + expect(setSelectedProjectId).toHaveBeenCalledWith("project-b") + }) + + it("moves menu with left/right in Select", () => { + const event = makeEvent("ArrowDown") + const setSelectedMenuIndex = vi.fn() + + const handled = handleMenuNavigationKey(event, "Select", false, setSelectedMenuIndex) + + expect(handled).toBe(true) + expect(setSelectedMenuIndex).toHaveBeenCalledTimes(1) + }) + + it("stops menu movement in armed Select", () => { + const event = makeEvent("ArrowDown") + const setSelectedMenuIndex = vi.fn() + + const handled = handleMenuNavigationKey(event, "Select", true, setSelectedMenuIndex) + + expect(handled).toBe(false) + expect(setSelectedMenuIndex).not.toHaveBeenCalled() + }) + + it("keeps only menu arrows outside Select", () => { + const menuEvent = makeEvent("ArrowDown") + const projectEvent = makeEvent("ArrowRight") + const setSelectedMenuIndex = vi.fn() + const setSelectedProjectId = vi.fn() + + expect(handleMenuNavigationKey(menuEvent, "Create", false, setSelectedMenuIndex)).toBe(true) + expect(handleProjectNavigationKey(projectEvent, { + currentMenu: "Create", + dashboard, + projectNavigationArmed: false, + selectedProjectId: "project-a", + setSelectedProjectId + })).toBe(false) + }) + + it("renders dynamic shortcut hint", () => { + expect(shortcutHintText("Select", false)).toBe("↑/↓ menu, Enter/→ choose project") + expect(shortcutHintText("Select", true)).toBe("↑/↓ project, Enter run, Esc/← back") + expect(shortcutHintText("Create", false)).toBe("↑/↓ menu") + }) + + it("autoloads selected project details on project tabs after dashboard refresh", () => { + expect(shouldRefreshProjectDetails("Info", false, "project-a")).toBe(true) + expect(shouldRefreshProjectDetails("ProjectAuth", false, "project-a")).toBe(true) + expect(shouldRefreshProjectDetails("Select", false, "project-a")).toBe(false) + expect(shouldRefreshProjectDetails("Select", true, "project-a")).toBe(true) + expect(shouldRefreshProjectDetails("Status", false, "project-a")).toBe(false) + expect(shouldRefreshProjectDetails("Info", false, null)).toBe(false) + }) +}) diff --git a/packages/app/tests/docker-git/create-project-identity-conflict.test.ts b/packages/app/tests/docker-git/create-project-identity-conflict.test.ts deleted file mode 100644 index c9cf3643..00000000 --- a/packages/app/tests/docker-git/create-project-identity-conflict.test.ts +++ /dev/null @@ -1,243 +0,0 @@ -import * as FileSystem from "@effect/platform/FileSystem" -import * as Path from "@effect/platform/Path" -import { NodeContext } from "@effect/platform-node" -import type { PlatformError } from "@effect/platform/Error" -import { describe, expect, it } from "@effect/vitest" -import { Effect } from "effect" -import { beforeEach, vi } from "vitest" - -import { defaultTemplateConfig, type CreateCommand } from "@lib/core/domain" - -import { DockerIdentityConflictError } from "../../src/lib/shell/errors.js" -import { createProject } from "../../src/lib/usecases/actions/create-project.js" -import type { ProjectStatus } from "../../src/lib/usecases/projects-core.js" - -const resolveSshPortMock = vi.hoisted(() => vi.fn((config: CreateCommand["config"]) => Effect.succeed(config))) -const buildSshCommandMock = vi.hoisted(() => vi.fn(() => "ssh -p 2222 dev@localhost")) -const getContainerIpIfInsideContainerMock = vi.hoisted(() => vi.fn(() => Effect.succeed(undefined))) -const loadProjectIndexMock = vi.hoisted(() => vi.fn()) -const loadProjectStatusMock = vi.hoisted(() => vi.fn()) -const migrateProjectOrchLayoutMock = vi.hoisted(() => vi.fn(() => Effect.void)) -const prepareProjectFilesMock = vi.hoisted(() => vi.fn(() => Effect.succeed([]))) -const autoSyncStateMock = vi.hoisted(() => vi.fn(() => Effect.void)) -const deleteDockerGitProjectMock = vi.hoisted(() => vi.fn(() => Effect.void)) -const runDockerDownCleanupMock = vi.hoisted(() => vi.fn(() => Effect.void)) -const runDockerUpIfNeededMock = vi.hoisted(() => vi.fn(() => Effect.void)) - -vi.mock("../../src/lib/usecases/actions/ports.js", () => ({ - resolveSshPort: resolveSshPortMock -})) - -vi.mock("../../src/lib/usecases/projects-core.js", () => ({ - buildSshCommand: buildSshCommandMock, - getContainerIpIfInsideContainer: getContainerIpIfInsideContainerMock, - loadProjectIndex: loadProjectIndexMock, - loadProjectStatus: loadProjectStatusMock -})) - -vi.mock("../../src/lib/usecases/actions/prepare-files.js", () => ({ - migrateProjectOrchLayout: migrateProjectOrchLayoutMock, - prepareProjectFiles: prepareProjectFilesMock -})) - -vi.mock("../../src/lib/usecases/state-repo.js", () => ({ - autoSyncState: autoSyncStateMock -})) - -vi.mock("../../src/lib/usecases/projects-delete.js", () => ({ - deleteDockerGitProject: deleteDockerGitProjectMock -})) - -vi.mock("../../src/lib/usecases/actions/docker-up.js", () => ({ - runDockerDownCleanup: runDockerDownCleanupMock, - runDockerUpIfNeeded: runDockerUpIfNeededMock -})) - -const withTempDir = ( - use: (tempDir: string) => Effect.Effect -): Effect.Effect => - Effect.scoped( - Effect.gen(function*(_) { - const fs = yield* _(FileSystem.FileSystem) - const tempDir = yield* _( - fs.makeTempDirectoryScoped({ - prefix: "docker-git-identity-conflict-" - }) - ) - return yield* _(use(tempDir)) - }) - ) - -const makeTemplate = ( - root: string, - overrides: Partial = {} -): CreateCommand["config"] => ({ - ...defaultTemplateConfig, - containerName: "dg-test", - serviceName: "dg-test", - volumeName: "dg-test-home", - repoUrl: "https://git.example.test/test-owner-a/repo.git", - repoRef: "main", - targetDir: "/home/dev/org/repo", - dockerGitPath: `${root}/.docker-git`, - authorizedKeysPath: `${root}/authorized_keys`, - envGlobalPath: `${root}/.orch/env/global.env`, - envProjectPath: `${root}/.orch/env/project.env`, - codexAuthPath: `${root}/.orch/auth/codex`, - codexSharedAuthPath: `${root}/.orch/auth/codex-shared`, - codexHome: "/home/dev/.codex", - dockerNetworkMode: "shared", - dockerSharedNetworkName: "docker-git-shared", - enableMcpPlaywright: false, - ...overrides -}) - -const makeStatus = ( - projectDir: string, - root: string, - overrides: Partial = {} -): ProjectStatus => ({ - projectDir, - config: { - schemaVersion: 1, - template: makeTemplate(root, overrides) - } -}) - -const makeCommand = ( - root: string, - outDir: string, - force: boolean -): CreateCommand => ({ - _tag: "Create", - config: makeTemplate(root), - outDir, - runUp: false, - openSsh: false, - force, - forceEnv: false, - waitForClone: false -}) - -describe("createProject docker identity guard", () => { - beforeEach(() => { - loadProjectIndexMock.mockReset() - loadProjectStatusMock.mockReset() - resolveSshPortMock.mockReset() - migrateProjectOrchLayoutMock.mockReset() - prepareProjectFilesMock.mockReset() - autoSyncStateMock.mockReset() - deleteDockerGitProjectMock.mockReset() - runDockerUpIfNeededMock.mockReset() - runDockerDownCleanupMock.mockReset() - }) - - it.effect("fails when another project already uses the same Docker identity", () => - withTempDir((root) => - Effect.gen(function*(_) { - const path = yield* _(Path.Path) - const outDir = path.join(root, "candidate") - const existingDir = path.join(root, "existing") - const existingConfigPath = path.join(existingDir, "docker-git.json") - const command = makeCommand(root, outDir, false) - - loadProjectIndexMock.mockReturnValue( - Effect.succeed({ - projectsRoot: path.join(root, ".docker-git"), - configPaths: [existingConfigPath] - }) - ) - loadProjectStatusMock.mockImplementation((configPath: string) => - Effect.succeed( - makeStatus( - existingDir, - root, - configPath === existingConfigPath - ? {} - : { - containerName: "dg-test-other", - serviceName: "dg-test-other", - volumeName: "dg-test-other-home" - } - ) - ) - ) - - const error = yield* _(createProject(command).pipe(Effect.flip)) - - expect(error).toBeInstanceOf(DockerIdentityConflictError) - if (error instanceof DockerIdentityConflictError) { - expect(error.projectDir).toBe(outDir) - expect(error.conflicts).toEqual([ - { conflictingProjectDir: existingDir, kind: "containerName", name: "dg-test" }, - { conflictingProjectDir: existingDir, kind: "serviceName", name: "dg-test" }, - { conflictingProjectDir: existingDir, kind: "volumeName", name: "dg-test-home" }, - { conflictingProjectDir: existingDir, kind: "bootstrapVolumeName", name: "dg-test-home-bootstrap" } - ]) - } - expect(prepareProjectFilesMock).not.toHaveBeenCalled() - expect(deleteDockerGitProjectMock).not.toHaveBeenCalled() - expect(runDockerUpIfNeededMock).not.toHaveBeenCalled() - }) - ).pipe(Effect.provide(NodeContext.layer))) - - it.effect("force replaces the conflicting project before recreating", () => - withTempDir((root) => - Effect.gen(function*(_) { - const path = yield* _(Path.Path) - const outDir = path.join(root, "candidate") - const existingDir = path.join(root, "existing") - const existingConfigPath = path.join(existingDir, "docker-git.json") - const command = makeCommand(root, outDir, true) - - loadProjectIndexMock.mockReturnValue( - Effect.succeed({ - projectsRoot: path.join(root, ".docker-git"), - configPaths: [existingConfigPath] - }) - ) - loadProjectStatusMock.mockReturnValue( - Effect.succeed(makeStatus(existingDir, root)) - ) - - yield* _(createProject(command)) - - expect(deleteDockerGitProjectMock).toHaveBeenCalledTimes(1) - expect(deleteDockerGitProjectMock).toHaveBeenCalledWith({ - projectDir: existingDir, - repoUrl: "https://git.example.test/test-owner-a/repo.git", - containerName: "dg-test", - serviceName: "dg-test" - }) - expect(prepareProjectFilesMock).toHaveBeenCalledTimes(1) - expect(runDockerUpIfNeededMock).toHaveBeenCalledTimes(1) - }) - ).pipe(Effect.provide(NodeContext.layer))) - - it.effect("allows the same projectDir to be recreated with --force", () => - withTempDir((root) => - Effect.gen(function*(_) { - const path = yield* _(Path.Path) - const outDir = path.join(root, "candidate") - const configPath = path.join(outDir, "docker-git.json") - const command = makeCommand(root, outDir, true) - - loadProjectIndexMock.mockReturnValue( - Effect.succeed({ - projectsRoot: path.join(root, ".docker-git"), - configPaths: [configPath] - }) - ) - loadProjectStatusMock.mockReturnValue( - Effect.succeed(makeStatus(outDir, root)) - ) - - yield* _(createProject(command)) - - expect(prepareProjectFilesMock).toHaveBeenCalledTimes(1) - expect(deleteDockerGitProjectMock).not.toHaveBeenCalled() - expect(migrateProjectOrchLayoutMock).toHaveBeenCalledTimes(1) - expect(runDockerUpIfNeededMock).toHaveBeenCalledTimes(1) - }) - ).pipe(Effect.provide(NodeContext.layer))) -}) diff --git a/packages/app/tests/docker-git/docker-runtime-info.test.ts b/packages/app/tests/docker-git/docker-runtime-info.test.ts deleted file mode 100644 index 01aa4eec..00000000 --- a/packages/app/tests/docker-git/docker-runtime-info.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import * as Command from "@effect/platform/Command" -import * as CommandExecutor from "@effect/platform/CommandExecutor" -import { describe, expect, it } from "@effect/vitest" -import { Effect } from "effect" -import * as Inspectable from "effect/Inspectable" -import * as Sink from "effect/Sink" -import * as Stream from "effect/Stream" - -import { runDockerInspectContainerRuntimeInfo } from "../../src/lib/shell/docker.js" - -type RecordedCommand = { - readonly command: string - readonly args: ReadonlyArray -} - -const encode = (value: string): Uint8Array => new TextEncoder().encode(value) - -const isRuntimeInspect = (command: RecordedCommand): boolean => - command.command === "docker" && - command.args[0] === "inspect" && - command.args[1] === "-f" && - (command.args[2] ?? "").includes(".State.Status") - -const isIpInspect = (command: RecordedCommand): boolean => - command.command === "docker" && - command.args[0] === "inspect" && - command.args[1] === "-f" && - (command.args[2] ?? "").includes("NetworkSettings.Networks") - -const makeFakeExecutor = (outputs: { - readonly runtimeOutput: string - readonly ipOutput: string -}): CommandExecutor.CommandExecutor => { - const start = (command: Command.Command): Effect.Effect => - Effect.gen(function*(_) { - const flattened = Command.flatten(command) - const last = flattened[flattened.length - 1]! - const invocation: RecordedCommand = { - command: last.command, - args: last.args - } - - const stdoutText = isRuntimeInspect(invocation) - ? outputs.runtimeOutput - : isIpInspect(invocation) - ? outputs.ipOutput - : "" - - const stdout = stdoutText.length === 0 - ? Stream.empty - : Stream.succeed(encode(stdoutText)) - - const process: CommandExecutor.Process = { - [CommandExecutor.ProcessTypeId]: CommandExecutor.ProcessTypeId, - pid: CommandExecutor.ProcessId(1), - exitCode: Effect.succeed(CommandExecutor.ExitCode(0)), - isRunning: Effect.succeed(false), - kill: (_signal) => Effect.void, - stderr: Stream.empty, - stdin: Sink.drain, - stdout, - toJSON: () => ({ _tag: "DockerRuntimeInfoTestProcess", command: invocation.command, args: invocation.args }), - [Inspectable.NodeInspectSymbol]: () => ({ - _tag: "DockerRuntimeInfoTestProcess", - command: invocation.command, - args: invocation.args - }), - toString: () => `[DockerRuntimeInfoTestProcess ${invocation.command}]` - } - - return process - }) - - return CommandExecutor.makeExecutor(start) -} - -describe("runDockerInspectContainerRuntimeInfo", () => { - it.effect("parses running runtime ownership even when separators arrive as literal escapes", () => - Effect.gen(function*(_) { - const executor = makeFakeExecutor({ - runtimeOutput: "running\\t/home/dev/.docker-git/test-owner/repo\\tdg-repo\n", - ipOutput: "bridge=172.17.0.15\nproject=10.88.0.2\n" - }) - - const runtime = yield* _( - runDockerInspectContainerRuntimeInfo("/tmp", "dg-repo").pipe( - Effect.provideService(CommandExecutor.CommandExecutor, executor) - ) - ) - - expect(runtime).toEqual({ - containerName: "dg-repo", - running: true, - ipAddress: "172.17.0.15", - projectWorkingDir: "/home/dev/.docker-git/test-owner/repo", - composeService: "dg-repo" - }) - })) - - it.effect("keeps optional compose labels undefined when runtime is unlabeled", () => - Effect.gen(function*(_) { - const executor = makeFakeExecutor({ - runtimeOutput: "running\t\t\n", - ipOutput: "project=10.88.0.4\n" - }) - - const runtime = yield* _( - runDockerInspectContainerRuntimeInfo("/tmp", "dg-repo").pipe( - Effect.provideService(CommandExecutor.CommandExecutor, executor) - ) - ) - - expect(runtime).toEqual({ - containerName: "dg-repo", - running: true, - ipAddress: "10.88.0.4", - projectWorkingDir: undefined, - composeService: undefined - }) - })) -}) diff --git a/packages/app/tests/docker-git/entrypoint-auth.test.ts b/packages/app/tests/docker-git/entrypoint-auth.test.ts deleted file mode 100644 index 6554c358..00000000 --- a/packages/app/tests/docker-git/entrypoint-auth.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { describe, expect, it } from "@effect/vitest" -import { Effect } from "effect" - -import { defaultTemplateConfig } from "@lib/core/domain" -import { renderEntrypoint } from "@lib/core/templates-entrypoint" - -describe("renderEntrypoint auth bridge", () => { - it.effect("maps GH token fallback to git auth and sets git credential helper", () => - Effect.sync(() => { - const entrypoint = renderEntrypoint({ - ...defaultTemplateConfig, - repoUrl: "https://github.com/org/repo.git", - enableMcpPlaywright: false - }) - - expect(entrypoint).toContain( - "GIT_AUTH_TOKEN=\"${GIT_AUTH_TOKEN:-${GITHUB_TOKEN:-${GH_TOKEN:-}}}\"" - ) - expect(entrypoint).toContain("GITHUB_TOKEN=\"${GITHUB_TOKEN:-${GH_TOKEN:-}}\"") - expect(entrypoint).toContain("GITHUB_AUTH_SKIP=\"${GITHUB_AUTH_SKIP:-0}\"") - expect(entrypoint).toContain( - "if [[ \"$GITHUB_AUTH_SKIP\" != \"1\" && -z \"$AUTH_LABEL_RAW\" && \"$REPO_URL\" == https://github.com/* ]]; then" - ) - expect(entrypoint).toContain("if [[ \"${GITHUB_AUTH_SKIP:-0}\" == \"1\" ]]; then") - expect(entrypoint).toContain("AUTH_LABEL_RAW=\"${GIT_AUTH_LABEL:-${GITHUB_AUTH_LABEL:-}}\"") - expect(entrypoint).toContain("LABELED_GITHUB_TOKEN_KEY=\"GITHUB_TOKEN__$RESOLVED_AUTH_LABEL\"") - expect(entrypoint).toContain("LABELED_GIT_TOKEN_KEY=\"GIT_AUTH_TOKEN__$RESOLVED_AUTH_LABEL\"") - expect(entrypoint).toContain("if [[ -n \"$EFFECTIVE_GH_TOKEN\" ]]; then") - expect(entrypoint).toContain(String.raw`printf "export GITHUB_TOKEN=%q\n" "$EFFECTIVE_GITHUB_TOKEN"`) - expect(entrypoint).toContain(String.raw`printf "export GH_TOKEN=%q\n" "$EFFECTIVE_GH_TOKEN"`) - expect(entrypoint).toContain(String.raw`printf "export GIT_AUTH_TOKEN=%q\n" "$EFFECTIVE_GITHUB_TOKEN"`) - expect(entrypoint).toContain("docker_git_upsert_ssh_env \"GITHUB_TOKEN\" \"$EFFECTIVE_GITHUB_TOKEN\"") - expect(entrypoint).toContain("docker_git_upsert_ssh_env \"GH_TOKEN\" \"$EFFECTIVE_GH_TOKEN\"") - expect(entrypoint).toContain("docker_git_upsert_ssh_env \"GIT_AUTH_TOKEN\" \"$EFFECTIVE_GITHUB_TOKEN\"") - expect(entrypoint).toContain("GIT_CREDENTIAL_HELPER_PATH=\"/usr/local/bin/docker-git-credential-helper\"") - expect(entrypoint).toContain("CLAUDE_REAL_DIR=\"$(dirname \"$CURRENT_CLAUDE_BIN\")\"") - expect(entrypoint).toContain("CLAUDE_REAL_BIN=\"$CLAUDE_REAL_DIR/.docker-git-claude-real\"") - expect(entrypoint).toContain("CLAUDE_WRAPPER_BIN=\"/usr/local/bin/claude\"") - expect(entrypoint).toContain("cat <<'EOF' > \"$CLAUDE_WRAPPER_BIN\"") - expect(entrypoint).toContain("CLAUDE_REAL_BIN=\"__CLAUDE_REAL_BIN__\"") - expect(entrypoint).toContain( - "sed -i \"s#__CLAUDE_REAL_BIN__#$CLAUDE_REAL_BIN#g\" \"$CLAUDE_WRAPPER_BIN\" || true" - ) - expect(entrypoint).toContain("CLAUDE_CONFIG_DIR=\"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"") - expect(entrypoint).toContain("docker_git_ensure_claude_cli()") - expect(entrypoint).toContain("claude cli.js not found under npm global root; skip shim restore") - expect(entrypoint).toContain("CLAUDE_PERMISSION_SETTINGS_FILE=\"$CLAUDE_CONFIG_DIR/settings.json\"") - expect(entrypoint).toContain("docker_git_sync_claude_permissions()") - expect(entrypoint).toContain( - "const currentPermissions = isRecord(settings.permissions) ? settings.permissions : {}" - ) - expect(entrypoint).toContain("defaultMode: \"bypassPermissions\"") - expect(entrypoint).toContain("CLAUDE_TOKEN_FILE=\"$CLAUDE_CONFIG_DIR/.oauth-token\"") - expect(entrypoint).toContain("CLAUDE_CREDENTIALS_FILE=\"$CLAUDE_CONFIG_DIR/.credentials.json\"") - expect(entrypoint).toContain("CLAUDE_NESTED_CREDENTIALS_FILE=\"$CLAUDE_CONFIG_DIR/.claude/.credentials.json\"") - expect(entrypoint).toContain("docker_git_prepare_claude_auth_mode()") - expect(entrypoint).toContain( - "rm -f \"$CLAUDE_CREDENTIALS_FILE\" \"$CLAUDE_NESTED_CREDENTIALS_FILE\" \"$CLAUDE_HOME_DIR/.credentials.json\" || true" - ) - expect(entrypoint).toContain("if [[ ! -s \"$CLAUDE_TOKEN_FILE\" ]]; then") - expect(entrypoint).toContain("CLAUDE_SETTINGS_FILE=\"${CLAUDE_HOME_JSON:-$CLAUDE_CONFIG_DIR/.claude.json}\"") - expect(entrypoint).toContain("nextServers.playwright = {") - expect(entrypoint).toContain("command: \"docker-git-playwright-mcp\"") - expect(entrypoint).toContain("CLAUDE_ROOT_TOKEN_FILE=\"$CLAUDE_AUTH_ROOT/.oauth-token\"") - expect(entrypoint).toContain("CLAUDE_ROOT_CONFIG_FILE=\"$CLAUDE_AUTH_ROOT/.config.json\"") - expect(entrypoint).toContain("CLAUDE_HOME_DIR=\"/home/dev/.claude\"") - expect(entrypoint).toContain("CLAUDE_HOME_JSON=\"/home/dev/.claude.json\"") - expect(entrypoint).toContain("docker_git_link_claude_home_file()") - expect(entrypoint).toContain("docker_git_link_claude_home_file \".oauth-token\"") - expect(entrypoint).toContain("docker_git_link_claude_home_file \".config.json\"") - expect(entrypoint).toContain("docker_git_link_claude_home_file \".claude.json\"") - expect(entrypoint).toContain("docker_git_link_claude_home_file \".credentials.json\"") - expect(entrypoint).toContain( - "docker_git_link_claude_file \"$CLAUDE_CONFIG_DIR/.claude.json\" \"$CLAUDE_HOME_JSON\"" - ) - expect(entrypoint).toContain("su - dev -s /bin/bash -c \"bash -lc") - expect(entrypoint).toContain(". /etc/profile 2>/dev/null || true;") - expect(entrypoint).toContain(String.raw`. \"$AGENT_ENV_FILE\" 2>/dev/null || true;`) - expect(entrypoint).toContain( - String.raw`claude --dangerously-skip-permissions -p \"\$(cat \"$AGENT_PROMPT_FILE\")\"` - ) - expect(entrypoint).toContain(String.raw`codex exec \"\$(cat \"$AGENT_PROMPT_FILE\")\"`) - expect(entrypoint).not.toContain("codex --approval-mode full-auto") - expect(entrypoint).toContain("CLAUDE_GLOBAL_PROMPT_FILE=\"/home/dev/.claude/CLAUDE.md\"") - expect(entrypoint).toContain("CLAUDE_AUTO_SYSTEM_PROMPT=\"${CLAUDE_AUTO_SYSTEM_PROMPT:-1}\"") - expect(entrypoint).toContain("docker-git-managed:claude-md") - expect(entrypoint).toContain("docker_git_sync_project_codex_skills()") - expect(entrypoint).toContain("project_skills_root=\"$codex_home/skills/.docker-git-project\"") - expect(entrypoint).toContain("docker_git_prepare_active_agent_project_rules()") - expect(entrypoint).toContain("docker_git_detect_claude_project_rules()") - expect(entrypoint).toContain("docker_git_detect_gemini_project_rules()") - expect(entrypoint).toContain("\"codex\")") - expect(entrypoint).toContain("\"claude\")") - expect(entrypoint).toContain("\"gemini\")") - expect(entrypoint).toContain("\"20-agents-skills::.agents/skills\"") - expect(entrypoint).toContain("\"30-agents-dot-skills::.agents/.skills\"") - expect(entrypoint).toContain("\"80-codex-skills::.codex/skills\"") - expect(entrypoint).toContain("\"90-codex-dot-skills::.codex/.skills\"") - expect(entrypoint).not.toContain("\"40-claude-skills::.claude/skills\"") - expect(entrypoint).toContain("$project_dir/.claude/settings.json") - expect(entrypoint).toContain("$project_dir/.claude/agents") - expect(entrypoint).toContain("$project_dir/.gemini/settings.json") - expect(entrypoint).toContain("$project_dir/.gemini/commands") - expect(entrypoint).toContain("$project_dir/.gemini/skills") - expect(entrypoint).toContain( - "SUBAGENTS_LINE=\"Для решения задач обязательно используй subagents. Сам агент обязан выполнять финальную проверку, интеграцию и валидацию результата перед ответом пользователю.\"" - ) - expect(entrypoint.split("Для решения задач обязательно используй subagents.").length - 1).toBeGreaterThanOrEqual( - 2 - ) - expect(entrypoint).toContain("token=\"${GITHUB_TOKEN:-}\"") - expect(entrypoint).toContain("token=\"${GH_TOKEN:-}\"") - expect(entrypoint).toContain(String.raw`printf "%s\n" "password=$token"`) - expect(entrypoint).toContain("git config --global credential.helper") - })) -}) diff --git a/packages/app/tests/docker-git/fixtures/event-recorder.ts b/packages/app/tests/docker-git/fixtures/event-recorder.ts new file mode 100644 index 00000000..d524d6c2 --- /dev/null +++ b/packages/app/tests/docker-git/fixtures/event-recorder.ts @@ -0,0 +1,6 @@ +import { Effect } from "effect" + +export const recordEvent = (events: Array, entry: string): Effect.Effect => + Effect.sync(() => { + events.push(entry) + }) diff --git a/packages/app/tests/docker-git/fixtures/open-project-helpers.ts b/packages/app/tests/docker-git/fixtures/open-project-helpers.ts new file mode 100644 index 00000000..e5db2248 --- /dev/null +++ b/packages/app/tests/docker-git/fixtures/open-project-helpers.ts @@ -0,0 +1,49 @@ +import { Effect } from "effect" + +import type { ApiProjectDetails } from "../../../src/docker-git/api-project-codec.js" +import { selectOpenProject } from "../../../src/docker-git/open-project.js" + +const defaultProject = { + id: "/controller/org/repo", + displayName: "org/repo", + repoUrl: "https://github.com/org/repo.git", + repoRef: "main", + status: "stopped", + statusLabel: "stopped", + containerName: "dg-repo", + serviceName: "dg-repo", + sshUser: "dev", + sshPort: 2222, + targetDir: "/home/dev/workspaces/org/repo", + projectDir: "/controller/org/repo", + sshCommand: "ssh dev@127.0.0.1 -p 2222", + authorizedKeysPath: "/controller/org/repo/authorized_keys", + authorizedKeysExists: true, + envGlobalPath: "/controller/.orch/env/global.env", + envProjectPath: "/controller/org/repo/.orch/env/project.env", + codexAuthPath: "/controller/.orch/auth/codex", + codexHome: "/home/dev/.codex", + sshSessions: 0, + startedAtIso: null, + startedAtEpochMs: null +} satisfies Omit + +export const makeProject = (overrides: Partial = {}): ApiProjectDetails => ({ + ...defaultProject, + ...overrides +}) + +export const joinIp = (...octets: ReadonlyArray): string => octets.join(".") + +export const liveRuntimeIp = joinIp(172, 17, 0, 15) +export const liveFallbackIp = joinIp(172, 17, 0, 20) + +export const expectSelectedProject = ( + project: ApiProjectDetails, + selector: string | undefined, + assert: (resolved: ApiProjectDetails) => void +) => + Effect.gen(function*(_) { + const resolved = yield* _(selectOpenProject([project], selector)) + assert(resolved) + }) diff --git a/packages/app/tests/docker-git/fixtures/project-item.ts b/packages/app/tests/docker-git/fixtures/project-item.ts index 2d38c2af..fe867a9f 100644 --- a/packages/app/tests/docker-git/fixtures/project-item.ts +++ b/packages/app/tests/docker-git/fixtures/project-item.ts @@ -1,8 +1,9 @@ -import type { ProjectItem } from "@lib/usecases/projects" +import type { ProjectItem } from "../../../src/docker-git/project-item.js" export const makeProjectItem = ( overrides: Partial = {} ): ProjectItem => ({ + id: "/home/dev/.docker-git/org-repo", projectDir: "/home/dev/.docker-git/org-repo", displayName: "org/repo", repoUrl: "https://github.com/org/repo.git", @@ -13,12 +14,16 @@ export const makeProjectItem = ( sshPort: 2222, targetDir: "/home/dev/org/repo", sshCommand: "ssh -p 2222 dev@localhost", - sshKeyPath: null, authorizedKeysPath: "/home/dev/.docker-git/org-repo/authorized_keys", authorizedKeysExists: true, envGlobalPath: "/home/dev/.docker-git/org-repo/.orch/env/global.env", envProjectPath: "/home/dev/.docker-git/org-repo/.orch/env/project.env", codexAuthPath: "/home/dev/.docker-git/org-repo/.orch/auth/codex", codexHome: "/home/dev/.codex", + status: "stopped", + statusLabel: "Stopped", + sshSessions: 0, + startedAtIso: null, + startedAtEpochMs: null, ...overrides }) diff --git a/packages/app/tests/docker-git/host-ssh-material.test.ts b/packages/app/tests/docker-git/host-ssh-material.test.ts deleted file mode 100644 index 00c5460f..00000000 --- a/packages/app/tests/docker-git/host-ssh-material.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { NodeContext } from "@effect/platform-node" -/* jscpd:ignore-start */ -import type * as CommandExecutor from "@effect/platform/CommandExecutor" -import type { PlatformError } from "@effect/platform/Error" -import * as FileSystem from "@effect/platform/FileSystem" -import * as Path from "@effect/platform/Path" -import { describe, expect, it } from "@effect/vitest" -import { Effect, type Exit } from "effect" - -import type { HostSshMaterial } from "../../src/docker-git/host-ssh-material.js" -import { resolveHostSshMaterial, resolveManagedHostSshMaterial } from "../../src/docker-git/host-ssh-material.js" -import type { CreateCommand } from "../../src/lib/core/domain.js" -import type { CommandFailedError } from "../../src/lib/shell/errors.js" - -type HostSshMaterialError = PlatformError | CommandFailedError -type HostSshMaterialServices = - | CommandExecutor.CommandExecutor - | FileSystem.FileSystem - | Path.Path - -const withTempDir = ( - use: (tempDir: string) => Effect.Effect -): Effect.Effect => - Effect.scoped( - Effect.gen(function*(_) { - const fs = yield* _(FileSystem.FileSystem) - const tempDir = yield* _( - fs.makeTempDirectoryScoped({ - prefix: "docker-git-host-ssh-material-" - }) - ) - return yield* _(use(tempDir)) - }) - ) - -const withResource = ( - acquire: Effect.Effect, - use: (value: T) => Effect.Effect, - release: (value: T, exit: Exit.Exit) => Effect.Effect -) => Effect.acquireUseRelease(acquire, use, release) - -const withPatchedEnv = ( - patch: Readonly>, - effect: Effect.Effect -) => - withResource( - Effect.sync(() => { - const previous = new Map() - - for (const [key, value] of Object.entries(patch)) { - previous.set(key, process.env[key]) - - if (value === undefined) { - Reflect.deleteProperty(process.env, key) - continue - } - - process.env[key] = value - } - - return previous - }), - () => effect, - (previous) => - Effect.sync(() => { - for (const [key, value] of previous.entries()) { - if (value === undefined) { - Reflect.deleteProperty(process.env, key) - continue - } - - process.env[key] = value - } - }) - ) - -const withWorkingDirectory = ( - cwd: string, - effect: Effect.Effect -) => - withResource( - Effect.sync(() => { - const previous = process.cwd() - process.chdir(cwd) - return previous - }), - () => effect, - (previous) => - Effect.sync(() => { - process.chdir(previous) - }) - ) - -const runMaterialCase = ( - resolver: ( - workspaceDir: string, - path: Path.Path, - projectsRoot: string - ) => Effect.Effect, - assert: ( - material: HostSshMaterial, - fs: FileSystem.FileSystem, - path: Path.Path, - projectsRoot: string - ) => Effect.Effect -): Effect.Effect => - withTempDir((root) => - Effect.gen(function*(_) { - const fs = yield* _(FileSystem.FileSystem) - const path = yield* _(Path.Path) - const workspaceDir = path.join(root, "workspace") - const homeDir = path.join(root, "home") - const projectsRoot = path.join(root, ".docker-git") - - yield* _(fs.makeDirectory(workspaceDir, { recursive: true })) - yield* _(fs.makeDirectory(homeDir, { recursive: true })) - - const material = yield* _( - withPatchedEnv( - { - HOME: homeDir, - DOCKER_GIT_PROJECTS_ROOT: projectsRoot, - DOCKER_GIT_AUTHORIZED_KEYS: undefined, - DOCKER_GIT_SSH_KEY: undefined - }, - withWorkingDirectory(workspaceDir, resolver(workspaceDir, path, projectsRoot)) - ) - ) - - yield* _(assert(material, fs, path, projectsRoot)) - }) - ).pipe(Effect.provide(NodeContext.layer)) - -const assertManagedHostSshMaterial = ( - material: HostSshMaterial, - fs: FileSystem.FileSystem, - path: Path.Path, - projectsRoot: string, - checkPublicKey: boolean -) => { - expect(material.privateKeyPath).toBe(path.join(projectsRoot, "dev_ssh_key")) - expect(material.authorizedKeysContents).toContain("ssh-ed25519") - - return Effect.gen(function*(_) { - expect(yield* _(fs.exists(material.privateKeyPath))).toBe(true) - - if (checkPublicKey) { - expect(yield* _(fs.exists(`${material.privateKeyPath}.pub`))).toBe(true) - } - }) -} - -/* jscpd:ignore-start */ -const makeCommand = (outDir: string, path: Path.Path): CreateCommand => ({ - _tag: "Create", - config: { - containerName: "dg-test", - serviceName: "dg-test", - sshUser: "dev", - sshPort: 2222, - repoUrl: "https://github.com/org/repo.git", - repoRef: "main", - skipGithubAuth: false, - targetDir: "/home/dev/workspaces/org/repo", - volumeName: "dg-test-home", - dockerGitPath: path.join(outDir, ".docker-git"), - authorizedKeysPath: "./.docker-git/authorized_keys", - envGlobalPath: "./.orch/env/global.env", - envProjectPath: "./.orch/env/project.env", - codexAuthPath: "./.orch/auth/codex", - codexSharedAuthPath: "./.orch/auth/codex-shared", - codexHome: "/home/dev/.codex", - geminiAuthPath: "./.docker-git/.orch/auth/gemini", - geminiHome: "/home/dev/.gemini", - dockerNetworkMode: "shared", - dockerSharedNetworkName: "docker-git-shared", - enableMcpPlaywright: false, - pnpmVersion: "10.27.0" - }, - outDir, - runUp: true, - openSsh: true, - force: false, - forceEnv: false, - waitForClone: true -}) -/* jscpd:ignore-end */ - -describe("host ssh material", () => { - it.effect("creates a managed SSH keypair when no host key exists", () => - runMaterialCase( - (workspaceDir, path, _projectsRoot) => - resolveHostSshMaterial(makeCommand(path.join(workspaceDir, "project"), path)), - (material, fs, path, projectsRoot) => assertManagedHostSshMaterial(material, fs, path, projectsRoot, true) - )) - - it.effect("resolves managed SSH material for existing projects without create-command overrides", () => - runMaterialCase( - () => resolveManagedHostSshMaterial(), - (material, fs, path, projectsRoot) => assertManagedHostSshMaterial(material, fs, path, projectsRoot, false) - )) -}) diff --git a/packages/app/tests/docker-git/menu-create-shared.test.ts b/packages/app/tests/docker-git/menu-create-shared.test.ts new file mode 100644 index 00000000..ab60d0c0 --- /dev/null +++ b/packages/app/tests/docker-git/menu-create-shared.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vitest" + +import { advanceCreateFlow, createInitialFlowView } from "../../src/docker-git/menu-create-shared.js" + +const expectCompleteResult = ( + next: ReturnType +) => { + expect(next?._tag).toBe("Complete") + if (next === null || next._tag !== "Complete") { + throw new TypeError("expected complete create flow result") + } + return next.inputs +} + +describe("menu-create-shared", () => { + const cwd = process.cwd() + const defaultRoot = `${process.env["HOME"] ?? cwd}/.docker-git/org/repo` + + it("quick-creates from repo URL with derived defaults", () => { + const inputs = expectCompleteResult(advanceCreateFlow( + cwd, + createInitialFlowView("https://github.com/org/repo/tree/feature-x") + )) + + expect(inputs.repoUrl).toBe("https://github.com/org/repo/tree/feature-x") + expect(inputs.repoRef).toBe("feature-x") + expect(inputs.outDir).toBe(defaultRoot) + expect(inputs.runUp).toBe(true) + }) + + it("keeps the advanced wizard when quick-create is overridden", () => { + const next = advanceCreateFlow( + cwd, + createInitialFlowView("https://github.com/org/repo/tree/feature-x"), + { forceWizard: true } + ) + + expect(next?._tag).toBe("Continue") + if (next === null || next._tag !== "Continue") { + return + } + + expect(next.view.step).toBe(1) + expect(next.view.values.repoUrl).toBe("https://github.com/org/repo/tree/feature-x") + expect(next.view.values.outDir).toBe(defaultRoot) + }) + + it("uses server-provided projectsRoot in browser mode", () => { + const inputs = expectCompleteResult(advanceCreateFlow( + { + cwd: "/repo/packages/api", + projectsRoot: "/home/dev/.docker-git" + }, + createInitialFlowView("https://github.com/org/repo/tree/feature-x") + )) + + expect(inputs.outDir).toBe("/home/dev/.docker-git/org/repo") + }) +}) diff --git a/packages/app/tests/docker-git/menu-select-connect.test.ts b/packages/app/tests/docker-git/menu-select-connect.test.ts index 48ef3de1..2f378f25 100644 --- a/packages/app/tests/docker-git/menu-select-connect.test.ts +++ b/packages/app/tests/docker-git/menu-select-connect.test.ts @@ -1,20 +1,16 @@ import { Effect } from "effect" import { describe, expect, it } from "vitest" -import type { ProjectItem } from "@lib/usecases/projects" +import type { ProjectItem } from "../../src/docker-git/project-item.js" import { selectHint } from "../../src/docker-git/menu-render-select.js" import { buildConnectEffect, isConnectMcpToggleInput } from "../../src/docker-git/menu-select-connect.js" +import { recordEvent } from "./fixtures/event-recorder.js" import { makeProjectItem } from "./fixtures/project-item.js" -const record = (events: Array, entry: string): Effect.Effect => - Effect.sync(() => { - events.push(entry) - }) - const makeConnectDeps = (events: Array) => ({ - connectWithUp: (selected: ProjectItem) => record(events, `connect:${selected.projectDir}`), - enableMcpPlaywright: (projectDir: string) => record(events, `enable:${projectDir}`) + connectWithUp: (selected: ProjectItem) => recordEvent(events, `connect:${selected.projectDir}`), + enableMcpPlaywright: (projectDir: string) => recordEvent(events, `enable:${projectDir}`) }) const workspaceProject = () => diff --git a/packages/app/tests/docker-git/menu-startup.test.ts b/packages/app/tests/docker-git/menu-startup.test.ts index 64437cc6..8871c76c 100644 --- a/packages/app/tests/docker-git/menu-startup.test.ts +++ b/packages/app/tests/docker-git/menu-startup.test.ts @@ -5,7 +5,7 @@ import { makeProjectItem } from "./fixtures/project-item.js" describe("menu-startup", () => { it("returns empty snapshot when no docker-git containers are running", () => { - const snapshot = resolveMenuStartupSnapshot([makeProjectItem({})], ["postgres", "redis"]) + const snapshot = resolveMenuStartupSnapshot([makeProjectItem({ status: "stopped" })]) expect(snapshot).toEqual({ activeDir: null, @@ -15,8 +15,8 @@ describe("menu-startup", () => { }) it("auto-selects active project when exactly one known docker-git container is running", () => { - const item = makeProjectItem({}) - const snapshot = resolveMenuStartupSnapshot([item], [item.containerName]) + const item = makeProjectItem({ status: "running", statusLabel: "Up 1 minute" }) + const snapshot = resolveMenuStartupSnapshot([item]) expect(snapshot.activeDir).toBe(item.projectDir) expect(snapshot.runningDockerGitContainers).toBe(1) @@ -32,20 +32,27 @@ describe("menu-startup", () => { const second = makeProjectItem({ containerName: "dg-two", displayName: "org/two", - projectDir: "/home/dev/.docker-git/org-two" + projectDir: "/home/dev/.docker-git/org-two", + status: "running", + statusLabel: "Up 2 minutes" }) - const snapshot = resolveMenuStartupSnapshot([first, second], [first.containerName, second.containerName]) + const snapshot = resolveMenuStartupSnapshot([ + { ...first, status: "running", statusLabel: "Up 1 minute" }, + second + ]) expect(snapshot.activeDir).toBeNull() expect(snapshot.runningDockerGitContainers).toBe(2) expect(snapshot.message).toContain("Use Select project") }) - it("shows warning when running docker-git containers have no matching configs", () => { - const snapshot = resolveMenuStartupSnapshot([], ["dg-unknown", "dg-another"]) + it("keeps an empty snapshot when API reports no running projects", () => { + const snapshot = resolveMenuStartupSnapshot([]) - expect(snapshot.activeDir).toBeNull() - expect(snapshot.runningDockerGitContainers).toBe(2) - expect(snapshot.message).toContain("No matching project config found") + expect(snapshot).toEqual({ + activeDir: null, + runningDockerGitContainers: 0, + message: null + }) }) }) diff --git a/packages/app/tests/docker-git/open-project-ssh.test.ts b/packages/app/tests/docker-git/open-project-ssh.test.ts new file mode 100644 index 00000000..bba229a2 --- /dev/null +++ b/packages/app/tests/docker-git/open-project-ssh.test.ts @@ -0,0 +1,66 @@ +import { NodeContext } from "@effect/platform-node" +import { describe, expect, it } from "@effect/vitest" +import { Effect } from "effect" + +import type { ApiTerminalSession } from "../../src/docker-git/api-terminal-codec.js" +import type { HostError } from "../../src/docker-git/host-errors.js" +import { openResolvedProjectSshEffect } from "../../src/docker-git/open-project.js" +import { makeProjectItem } from "./fixtures/project-item.js" + +const makeSession = (): ApiTerminalSession => ({ + id: "session-1", + projectId: "/controller/org/repo", + sshCommand: "ssh -p 22 dev@127.0.0.1", + status: "ready", + createdAt: "2026-04-10T00:00:00Z" +}) + +describe("openResolvedProjectSshEffect", () => { + it.effect("attaches to a prepared terminal session", () => + Effect.gen(function*(_) { + const item = makeProjectItem({ projectDir: "/controller/org/repo/issue-7" }) + const events = yield* _(captureOpenResolvedProjectSshEvents(item)) + expect(events).toEqual(["create:/controller/org/repo/issue-7", "attach:session-1"]) + })) + + it.effect("fails when controller does not create a terminal session", () => + Effect.gen(function*(_) { + const item = makeProjectItem({ projectDir: "/controller/org/repo/issue-8" }) + const exit = yield* _( + openResolvedProjectSshEffect(item, { + createSession: (projectId) => + Effect.sync(() => { + expect(projectId).toBe("/controller/org/repo/issue-8") + return null + }), + attach: () => Effect.void + }).pipe(Effect.provide(NodeContext.layer), Effect.exit) + ) + + expect(exit._tag).toBe("Failure") + })) +}) + +const captureOpenResolvedProjectSshEvents = ( + item: ReturnType +): Effect.Effect, HostError> => + Effect.gen(function*(_) { + const events: Array = [] + yield* _( + openResolvedProjectSshEffect(item, { + createSession: (projectId) => + Effect.sync(() => { + events.push(`create:${projectId}`) + return { + project: {}, + session: makeSession() + } + }), + attach: (_project, session) => + Effect.sync(() => { + events.push(`attach:${session.id}`) + }) + }) + ) + return events + }).pipe(Effect.provide(NodeContext.layer)) diff --git a/packages/app/tests/docker-git/open-project.test.ts b/packages/app/tests/docker-git/open-project.test.ts index c4365cb2..91a3d5a6 100644 --- a/packages/app/tests/docker-git/open-project.test.ts +++ b/packages/app/tests/docker-git/open-project.test.ts @@ -2,190 +2,10 @@ import { describe, expect, it } from "@effect/vitest" import { Effect } from "effect" -import type { ApiProjectDetails } from "../../src/docker-git/api-project-codec.js" -import { openResolvedProjectSshEffect, resolveOpenProjectEffect, selectOpenProject } from "../../src/docker-git/open-project.js" -import { makeProjectItem } from "./fixtures/project-item.js" - -const defaultProject = { - id: "/controller/org/repo", - displayName: "org/repo", - repoUrl: "https://github.com/org/repo.git", - repoRef: "main", - status: "stopped", - statusLabel: "stopped", - containerName: "dg-repo", - serviceName: "dg-repo", - sshUser: "dev", - sshPort: 2222, - targetDir: "/home/dev/workspaces/org/repo", - projectDir: "/controller/org/repo", - sshCommand: "ssh dev@127.0.0.1 -p 2222", - envGlobalPath: "/controller/.orch/env/global.env", - envProjectPath: "/controller/org/repo/.orch/env/project.env", - codexAuthPath: "/controller/.orch/auth/codex", - codexHome: "/home/dev/.codex" -} satisfies Omit - -const makeProject = (overrides: Partial = {}): ApiProjectDetails => ({ - ...defaultProject, - ...overrides -}) - -const expectSelectedProject = ( - project: ApiProjectDetails, - selector: string | undefined, - assert: (resolved: ApiProjectDetails) => void -) => - Effect.gen(function*(_) { - const resolved = yield* _(selectOpenProject([project], selector)) - assert(resolved) - }) +import { resolveOpenProjectEffect, selectOpenProject } from "../../src/docker-git/open-project.js" +import { expectSelectedProject, liveRuntimeIp, makeProject } from "./fixtures/open-project-helpers.js" describe("selectOpenProject", () => { - it.effect("connects directly when SSH is already reachable", () => - Effect.gen(function*(_) { - const item = makeProjectItem({ - projectDir: "/controller/org/repo/issue-7", - sshCommand: "ssh -p 22 dev@172.17.0.20" - }) - const events: Array = [] - - yield* _( - openResolvedProjectSshEffect(item, { - log: (message) => - Effect.sync(() => { - events.push(`log:${message}`) - }), - resolvePreferredItem: () => Effect.succeed(null), - probeReady: () => Effect.succeed(true), - connect: (selected) => - Effect.sync(() => { - events.push(`connect:${selected.projectDir}`) - }), - connectWithUp: (selected) => - Effect.sync(() => { - events.push(`up:${selected.projectDir}`) - }) - }) - ) - - expect(events).toEqual([ - "log:Opening SSH: ssh -p 22 dev@172.17.0.20", - "connect:/controller/org/repo/issue-7" - ]) - })) - - it.effect("falls back to docker up when SSH is not yet reachable", () => - Effect.gen(function*(_) { - const item = makeProjectItem({ - projectDir: "/controller/org/repo/issue-8", - sshCommand: "ssh -p 2222 dev@localhost" - }) - const events: Array = [] - - yield* _( - openResolvedProjectSshEffect(item, { - log: (message) => - Effect.sync(() => { - events.push(`log:${message}`) - }), - resolvePreferredItem: () => Effect.succeed(null), - probeReady: () => Effect.succeed(false), - connect: (selected) => - Effect.sync(() => { - events.push(`connect:${selected.projectDir}`) - }), - connectWithUp: (selected) => - Effect.sync(() => { - events.push(`up:${selected.projectDir}`) - }) - }) - ) - - expect(events).toEqual([ - "log:Opening SSH: ssh -p 2222 dev@localhost", - "up:/controller/org/repo/issue-8" - ]) - })) - - it.effect("prefers a live runtime SSH target before falling back to docker up", () => - Effect.gen(function*(_) { - const item = makeProjectItem({ - projectDir: "/controller/org/repo/issue-9", - sshCommand: "ssh -p 2253 dev@localhost", - sshPort: 2253 - }) - const preferred = makeProjectItem({ - ...item, - ipAddress: "172.17.0.15", - sshCommand: "ssh -p 22 dev@172.17.0.15" - }) - const events: Array = [] - - yield* _( - openResolvedProjectSshEffect(item, { - log: (message) => - Effect.sync(() => { - events.push(`log:${message}`) - }), - resolvePreferredItem: () => Effect.succeed(preferred), - probeReady: (selected) => Effect.succeed(selected.ipAddress === "172.17.0.15"), - connect: (selected) => - Effect.sync(() => { - events.push(`connect:${selected.sshCommand}`) - }), - connectWithUp: (selected) => - Effect.sync(() => { - events.push(`up:${selected.projectDir}`) - }) - }) - ) - - expect(events).toEqual([ - "log:Opening SSH: ssh -p 22 dev@172.17.0.15", - "connect:ssh -p 22 dev@172.17.0.15" - ]) - })) - - it.effect("falls back to the original SSH target when live runtime probe fails", () => - Effect.gen(function*(_) { - const item = makeProjectItem({ - projectDir: "/controller/org/repo/issue-10", - sshCommand: "ssh -p 2237 dev@localhost", - sshPort: 2237 - }) - const preferred = makeProjectItem({ - ...item, - ipAddress: "172.17.0.20", - sshCommand: "ssh -p 22 dev@172.17.0.20" - }) - const events: Array = [] - - yield* _( - openResolvedProjectSshEffect(item, { - log: (message) => - Effect.sync(() => { - events.push(`log:${message}`) - }), - resolvePreferredItem: () => Effect.succeed(preferred), - probeReady: (selected) => Effect.succeed(selected.ipAddress !== "172.17.0.20"), - connect: (selected) => - Effect.sync(() => { - events.push(`connect:${selected.sshCommand}`) - }), - connectWithUp: (selected) => - Effect.sync(() => { - events.push(`up:${selected.projectDir}`) - }) - }) - ) - - expect(events).toEqual([ - "log:Opening SSH: ssh -p 2237 dev@localhost", - "connect:ssh -p 2237 dev@localhost" - ]) - })) - it.effect("prefers the single running project when selector is omitted", () => Effect.gen(function*(_) { const stopped = makeProject({ @@ -272,7 +92,7 @@ describe("selectOpenProject", () => { Effect.succeed({ containerName: "dg-openclaw_autodeployer", running: true, - ipAddress: "172.17.0.15", + ipAddress: liveRuntimeIp, projectWorkingDir: "/controller/telegramgpt/openclaw_autodeployer", composeService: "dg-openclaw_autodeployer" }) diff --git a/packages/app/tests/docker-git/parser-helpers.ts b/packages/app/tests/docker-git/parser-helpers.ts index 60077fb2..818716c5 100644 --- a/packages/app/tests/docker-git/parser-helpers.ts +++ b/packages/app/tests/docker-git/parser-helpers.ts @@ -1,8 +1,8 @@ import { expect } from "@effect/vitest" import { Effect, Either } from "effect" -import type { Command } from "@lib/core/domain" import { parseArgs } from "../../src/docker-git/cli/parser.js" +import type { Command } from "../../src/docker-git/frontend-lib/core/domain.js" export type CreateCommand = Extract export type OpenCommand = Extract diff --git a/packages/app/tests/docker-git/parser.test.ts b/packages/app/tests/docker-git/parser.test.ts index 09a36507..4633a85d 100644 --- a/packages/app/tests/docker-git/parser.test.ts +++ b/packages/app/tests/docker-git/parser.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "@effect/vitest" import { Effect } from "effect" -import { defaultTemplateConfig } from "@lib/core/domain" -import { expandContainerHome } from "@lib/usecases/scrap-path" +import { defaultTemplateConfig } from "../../src/docker-git/frontend-lib/core/domain.js" +import { expandContainerHome } from "../../src/docker-git/frontend-lib/usecases/scrap-path.js" import { type CreateCommand, expectAttachProjectDirCommand, diff --git a/packages/app/tests/docker-git/program.test.ts b/packages/app/tests/docker-git/program.test.ts index e2bb2ae1..28b3dcc4 100644 --- a/packages/app/tests/docker-git/program.test.ts +++ b/packages/app/tests/docker-git/program.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "@effect/vitest" import { Effect } from "effect" import { beforeEach, vi } from "vitest" -import type { Command } from "../../src/lib/core/domain.js" +import type { Command } from "../../src/docker-git/frontend-lib/core/domain.js" const ensureControllerReadyMock = vi.hoisted(() => vi.fn(() => Effect.void)) const runMenuCallMock = vi.hoisted(() => vi.fn(() => {})) diff --git a/packages/app/tests/docker-git/project-item.test.ts b/packages/app/tests/docker-git/project-item.test.ts index f5bed443..9e6d7dbf 100644 --- a/packages/app/tests/docker-git/project-item.test.ts +++ b/packages/app/tests/docker-git/project-item.test.ts @@ -3,8 +3,6 @@ import { describe, expect, it } from "vitest" import type { ApiProjectDetails } from "../../src/docker-git/api-project-codec.js" import { projectItemFromApiDetails } from "../../src/docker-git/project-item.js" -const joinIp = (...octets: ReadonlyArray): string => octets.join(".") - const makeProject = (): ApiProjectDetails => ({ id: "/home/dev/.docker-git/org/repo", displayName: "org/repo", @@ -12,13 +10,18 @@ const makeProject = (): ApiProjectDetails => ({ repoRef: "main", status: "running", statusLabel: "Up 10 seconds", + sshSessions: 2, + startedAtIso: "2026-04-10T00:00:00Z", + startedAtEpochMs: Date.parse("2026-04-10T00:00:00Z"), containerName: "dg-org-repo", serviceName: "workspace", sshUser: "dev", sshPort: 2222, targetDir: "~/workspaces/org/repo", projectDir: "/home/dev/.docker-git/org/repo", - sshCommand: "", + sshCommand: "ssh -p 2222 dev@127.0.0.1", + authorizedKeysPath: "/home/dev/.docker-git/org/repo/authorized_keys", + authorizedKeysExists: true, envGlobalPath: "/home/dev/.docker-git/org/repo/.orch/env/global.env", envProjectPath: "/home/dev/.docker-git/org/repo/.orch/env/project.env", codexAuthPath: "/home/dev/.docker-git/org/repo/.orch/auth/codex", @@ -27,19 +30,16 @@ const makeProject = (): ApiProjectDetails => ({ }) describe("project-itemFromApiDetails", () => { - it("builds a host-usable project item from API project details", () => { + it("maps API project details into the frontend project item", () => { const project = makeProject() - const sshKeyPath = `${project.projectDir}/dev_ssh_key` - const ipAddress = joinIp("172", "17", "0", "20") - const item = projectItemFromApiDetails(project, sshKeyPath, ipAddress) + const item = projectItemFromApiDetails(project) expect(item.projectDir).toBe(project.projectDir) expect(item.displayName).toBe(project.displayName) expect(item.containerName).toBe(project.containerName) - expect(item.authorizedKeysPath).toBe(`${project.projectDir}/authorized_keys`) - expect(item.sshKeyPath).toBe(sshKeyPath) - expect(item.ipAddress).toBe(ipAddress) + expect(item.authorizedKeysPath).toBe(project.authorizedKeysPath) + expect(item.sshCommand).toBe(project.sshCommand) + expect(item.sshSessions).toBe(2) expect(item.clonedOnHostname).toBe("builder-01") - expect(item.sshCommand).toContain(`dev@${ipAddress}`) }) }) diff --git a/packages/app/tests/docker-git/terminal.test.ts b/packages/app/tests/docker-git/terminal.test.ts new file mode 100644 index 00000000..c1d6814e --- /dev/null +++ b/packages/app/tests/docker-git/terminal.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "@effect/vitest" +import { afterEach, beforeEach, vi } from "vitest" + +import { parseTerminalServerMessage, resolveTerminalWebSocketUrl } from "../../src/web/terminal.js" +import type { TerminalServerMessage } from "../../src/web/terminal.js" + +const resolveApiBaseUrlMock = vi.hoisted(() => vi.fn<() => string>()) + +const readyMessagePayload: TerminalServerMessage = { + session: { + createdAt: "2026-04-08T10:00:00.000Z", + id: "session-1", + projectId: "project-1", + sshCommand: "ssh dev@127.0.0.1", + status: "attached" + }, + type: "ready" +} + +vi.mock("../../src/web/api-http.js", () => ({ + resolveApiBaseUrl: resolveApiBaseUrlMock +})) + +describe("browser terminal helpers", () => { + beforeEach(() => { + resolveApiBaseUrlMock.mockReset() + }) + + afterEach(() => { + vi.unstubAllGlobals() + }) + + it("builds websocket url from api base url", () => { + resolveApiBaseUrlMock.mockReturnValue("https://controller.example/api") + + expect(resolveTerminalWebSocketUrl("/projects/proj%201/terminal-sessions/sess%2F2/ws", 120, 40)).toBe( + "wss://controller.example/api/projects/proj%201/terminal-sessions/sess%2F2/ws?cols=120&rows=40" + ) + }) + + it("falls back to direct local api origin for relative browser api paths", () => { + const host = "terminal.example.local" + const httpProtocol = ["ht", "tp:"].join("") + const wsProtocol = ["ws", "://"].join("") + + resolveApiBaseUrlMock.mockReturnValue("/api") + vi.stubGlobal("location", { + hostname: host, + origin: `${httpProtocol}//${host}:4176`, + protocol: httpProtocol + }) + + expect(resolveTerminalWebSocketUrl("/projects/proj/terminal-sessions/sess/ws", 80, 24)).toBe([ + wsProtocol, + host, + ":3334/projects/proj/terminal-sessions/sess/ws?cols=80&rows=24" + ].join("")) + }) + + it("parses ready terminal messages", () => { + const parsed = parseTerminalServerMessage(JSON.stringify(readyMessagePayload)) + + expect(parsed).toEqual(readyMessagePayload) + }) + + it("rejects malformed terminal messages", () => { + expect(parseTerminalServerMessage("{\"type\":\"output\",\"data\":1}")).toBeNull() + }) +}) diff --git a/packages/app/tests/eslint/no-lib-imports.test.ts b/packages/app/tests/eslint/no-lib-imports.test.ts index df09b9ba..eb264f4d 100644 --- a/packages/app/tests/eslint/no-lib-imports.test.ts +++ b/packages/app/tests/eslint/no-lib-imports.test.ts @@ -4,7 +4,7 @@ import tseslint from "typescript-eslint" import { noLibImportsRule } from "../../eslint/no-lib-imports.mjs" -const defaultFilePath = "src/new-client.ts" +const defaultFilePath = "src/docker-git/new-client.ts" const verify = (source: string, filePath: string) => { const linter = new Linter({ configType: "flat" }) @@ -68,6 +68,11 @@ describe("noLibImportsRule", () => { line("import type { TemplateConfig } from \"@effect-template/lib/core/domain\""), [["@effect-template/lib/core/domain"]] ], + [ + "rejects direct imports from @lib aliases", + line("import type { Command } from \"@lib/core/domain\""), + [["@lib/core/domain"]] + ], [ "rejects type import expressions from lib", line("type Template = import(\"@effect-template/lib/core/domain\").TemplateConfig"), @@ -97,6 +102,17 @@ describe("noLibImportsRule", () => { ]), [["@effect-template/lib"], ["@effect-template/lib/core/domain"]] ], + [ + "rejects relative frontend imports into local src/lib", + line("import { resolveRepoInput } from \"../lib/core/domain.js\""), + [["../lib/core/domain.js"]] + ], + [ + "rejects relative app test imports into local src/lib", + line("import { renderEntrypoint } from \"../../src/lib/core/templates-entrypoint.js\""), + [["../../src/lib/core/templates-entrypoint.js"]], + "tests/docker-git/legacy-import.test.ts" + ], ["rejects migrated legacy paths too", line("import { listProjects } from \"@effect-template/lib\""), [[ "Direct import" ]], "src/docker-git/program.ts"] @@ -112,11 +128,20 @@ describe("noLibImportsRule", () => { const messages = verify( lines([ "import { request } from \"./api-client.js\"", - "import type { Command } from \"@lib/core/domain\"" + "import type { Command } from \"./frontend-lib/core/domain.js\"" ]), defaultFilePath ) expect(messages).toHaveLength(0) }) + + it("allows app test imports that stay within frontend-owned surfaces", () => { + const messages = verify( + line("import { parse } from \"../../src/docker-git/cli/parser.js\""), + "tests/docker-git/parser.test.ts" + ) + + expect(messages).toHaveLength(0) + }) }) diff --git a/packages/app/tsconfig.json b/packages/app/tsconfig.json index 961a61d2..58c0fd24 100644 --- a/packages/app/tsconfig.json +++ b/packages/app/tsconfig.json @@ -4,7 +4,8 @@ "allowJs": true, "rootDir": ".", "outDir": "dist", - "types": ["vitest"], + "types": ["vitest", "vite/client"], + "lib": ["ES2023", "DOM", "DOM.Iterable"], "jsx": "react-jsx", "baseUrl": ".", "paths": { @@ -18,6 +19,7 @@ "src/**/*", "tests/**/*", "vite.config.ts", + "vite.web.config.ts", "vitest.config.ts" ], "exclude": ["dist", "node_modules"] diff --git a/packages/app/vite.config.ts b/packages/app/vite.config.ts index cfd0bb55..3cea056b 100644 --- a/packages/app/vite.config.ts +++ b/packages/app/vite.config.ts @@ -1,18 +1,31 @@ import path from "node:path" import { fileURLToPath } from "node:url" import { defineConfig } from "vite" -import tsconfigPaths from "vite-tsconfig-paths" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) export default defineConfig({ - plugins: [tsconfigPaths()], publicDir: false, resolve: { - alias: { - "@": path.resolve(__dirname, "src") - } + alias: [ + { + find: /^@lib\/(.*)$/u, + replacement: path.resolve(__dirname, "src/lib") + "/$1.ts" + }, + { + find: "@lib", + replacement: path.resolve(__dirname, "src/lib/index.ts") + }, + { + find: /^@\/(.*)$/u, + replacement: path.resolve(__dirname, "src") + "/$1" + }, + { + find: "@", + replacement: path.resolve(__dirname, "src") + } + ] }, build: { target: "node20", diff --git a/packages/app/vite.docker-git.config.ts b/packages/app/vite.docker-git.config.ts index 7605ac45..52d0bca8 100644 --- a/packages/app/vite.docker-git.config.ts +++ b/packages/app/vite.docker-git.config.ts @@ -1,19 +1,35 @@ import path from "node:path" import { fileURLToPath } from "node:url" import { defineConfig } from "vite" -import tsconfigPaths from "vite-tsconfig-paths" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) export default defineConfig({ - plugins: [tsconfigPaths()], publicDir: false, resolve: { - alias: { - "@": path.resolve(__dirname, "src"), - "@effect-template/lib": path.resolve(__dirname, "../lib/src") - } + alias: [ + { + find: /^@lib\/(.*)$/u, + replacement: path.resolve(__dirname, "src/lib") + "/$1.ts" + }, + { + find: "@lib", + replacement: path.resolve(__dirname, "src/lib/index.ts") + }, + { + find: /^@\/(.*)$/u, + replacement: path.resolve(__dirname, "src") + "/$1" + }, + { + find: "@", + replacement: path.resolve(__dirname, "src") + }, + { + find: "@effect-template/lib", + replacement: path.resolve(__dirname, "../lib/src") + } + ] }, build: { target: "node20", @@ -21,12 +37,13 @@ export default defineConfig({ sourcemap: true, ssr: "src/docker-git/main.ts", rollupOptions: { + external: ["@gridland/bun"], output: { format: "es", - entryFileNames: "src/docker-git/main.js", - inlineDynamicImports: true + entryFileNames: "src/docker-git/main.js" } - } + }, + ssrEmitAssets: false }, ssr: { target: "node" diff --git a/packages/app/vite.web.config.ts b/packages/app/vite.web.config.ts new file mode 100644 index 00000000..88617076 --- /dev/null +++ b/packages/app/vite.web.config.ts @@ -0,0 +1,89 @@ +import path from "node:path" +import { fileURLToPath } from "node:url" + +import { gridlandWebPlugin } from "@gridland/web/vite-plugin" +import react from "@vitejs/plugin-react" +import { defineConfig, loadEnv } from "vite" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const defaultApiTarget = "http://127.0.0.1:3334" +const terminalWsProxyPath = "^/api/projects/.+/terminal-sessions/.+/ws$" +const noStoreHeaders = { + "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", + Pragma: "no-cache" +} + +const createProxy = (apiTarget: string, apiWsTarget: string) => ({ + [terminalWsProxyPath]: { + target: apiWsTarget, + changeOrigin: true, + ws: true, + rewrite: (requestPath: string) => requestPath.replace(/^\/api/u, "") + }, + "/api": { + target: apiTarget, + changeOrigin: true, + ws: true, + rewrite: (requestPath: string) => requestPath.replace(/^\/api/u, "") + } +}) + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, __dirname, "") + const apiTarget = env["DOCKER_GIT_API_URL"]?.trim() || defaultApiTarget + const apiWsTarget = apiTarget.replace(/^http/u, "ws") + + return { + plugins: [ + ...gridlandWebPlugin(), + react() + ], + publicDir: false, + resolve: { + alias: [ + { + find: /^@lib\/(.*)$/u, + replacement: path.resolve(__dirname, "src/lib") + "/$1.ts" + }, + { + find: "@lib", + replacement: path.resolve(__dirname, "src/lib/index.ts") + }, + { + find: /^@\/(.*)$/u, + replacement: path.resolve(__dirname, "src") + "/$1" + }, + { + find: "@", + replacement: path.resolve(__dirname, "src") + } + ] + }, + server: { + host: "127.0.0.1", + port: 4174, + allowedHosts: [".trycloudflare.com"], + headers: noStoreHeaders, + proxy: createProxy(apiTarget, apiWsTarget) + }, + preview: { + host: "127.0.0.1", + port: 4174, + headers: noStoreHeaders + }, + build: { + target: "esnext", + outDir: "dist-web", + sourcemap: true + }, + esbuild: { + target: "esnext" + }, + optimizeDeps: { + esbuildOptions: { + target: "esnext" + } + } + } +}) diff --git a/packages/app/vitest.config.ts b/packages/app/vitest.config.ts index 319bffbb..ec83f895 100644 --- a/packages/app/vitest.config.ts +++ b/packages/app/vitest.config.ts @@ -9,14 +9,12 @@ import path from "node:path" import { fileURLToPath } from "node:url" -import tsconfigPaths from "vite-tsconfig-paths" import { defineConfig } from "vitest/config" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) export default defineConfig({ - plugins: [tsconfigPaths()], // Resolves @/* paths from tsconfig test: { // CHANGE: Native ESM support without experimental flags // WHY: Vitest designed for ESM, no need for --experimental-vm-modules @@ -78,8 +76,23 @@ export default defineConfig({ // NOTE: Tests must import { describe, it, expect } from "vitest" }, resolve: { - alias: { - "@": path.resolve(__dirname, "src") - } + alias: [ + { + find: /^@lib\/(.*)$/u, + replacement: path.resolve(__dirname, "src/lib") + "/$1.ts" + }, + { + find: "@lib", + replacement: path.resolve(__dirname, "src/lib/index.ts") + }, + { + find: /^@\/(.*)$/u, + replacement: path.resolve(__dirname, "src") + "/$1" + }, + { + find: "@", + replacement: path.resolve(__dirname, "src") + } + ] } }) diff --git a/packages/lib/package.json b/packages/lib/package.json index 0f263c7c..22552263 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/ProverCoderAI/effect-template.git" + "url": "git+https://github.com/ProverCoderAI/docker-git.git" }, "keywords": [ "effect", @@ -29,10 +29,10 @@ "license": "ISC", "type": "module", "bugs": { - "url": "https://github.com/ProverCoderAI/effect-template/issues" + "url": "https://github.com/ProverCoderAI/docker-git/issues" }, - "homepage": "https://github.com/ProverCoderAI/effect-template#readme", - "packageManager": "pnpm@10.32.1", + "homepage": "https://github.com/ProverCoderAI/docker-git#readme", + "packageManager": "bun@1.3.11", "dependencies": { "@effect/cli": "^0.75.0", "@effect/cluster": "^0.58.0", @@ -78,7 +78,6 @@ "jscpd": "^4.0.8", "typescript": "^5.9.3", "vite": "^8.0.1", - "vite-tsconfig-paths": "^6.1.1", "vitest": "^4.1.0" }, "types": "dist/index.d.ts", diff --git a/packages/lib/src/core/clone.ts b/packages/lib/src/core/clone.ts index 073a108d..2801bf15 100644 --- a/packages/lib/src/core/clone.ts +++ b/packages/lib/src/core/clone.ts @@ -26,8 +26,8 @@ const resolveLifecycleArgs = ( return first === command ? rest : argv } -// CHANGE: resolve clone/open shortcut requests from argv + npm lifecycle metadata -// WHY: support pnpm run clone/open without requiring "--" +// CHANGE: resolve clone/open shortcut requests from argv + package lifecycle metadata +// WHY: support bun run clone/open without requiring "--" // QUOTE(ТЗ): "Добавить команду open. ... Просто открывает существующий по ссылке" // REF: user-request-2026-01-27 // SOURCE: n/a diff --git a/packages/lib/src/core/command-builders.ts b/packages/lib/src/core/command-builders.ts index 92484572..23eb2057 100644 --- a/packages/lib/src/core/command-builders.ts +++ b/packages/lib/src/core/command-builders.ts @@ -216,11 +216,11 @@ const buildTemplateConfig = ({ dockerSharedNetworkName, enableMcpPlaywright, gitTokenLabel, - skipGithubAuth, names, paths, ramLimit, - repo + repo, + skipGithubAuth }: BuildTemplateConfigInput): CreateCommand["config"] => ({ containerName: names.containerName, serviceName: names.serviceName, @@ -248,7 +248,7 @@ const buildTemplateConfig = ({ dockerNetworkMode, dockerSharedNetworkName, enableMcpPlaywright, - pnpmVersion: defaultTemplateConfig.pnpmVersion, + bunVersion: defaultTemplateConfig.bunVersion, agentMode, agentAuto, clonedOnHostname diff --git a/packages/lib/src/core/domain.ts b/packages/lib/src/core/domain.ts index cbf962f4..cd0acd49 100644 --- a/packages/lib/src/core/domain.ts +++ b/packages/lib/src/core/domain.ts @@ -6,8 +6,8 @@ export type { AuthClaudeLoginCommand, AuthClaudeLogoutCommand, AuthClaudeStatusCommand, - AuthCodexLoginCommand, AuthCodexImportCommand, + AuthCodexLoginCommand, AuthCodexLogoutCommand, AuthCodexStatusCommand, AuthCommand, @@ -80,7 +80,7 @@ export interface TemplateConfig { readonly dockerNetworkMode: DockerNetworkMode readonly dockerSharedNetworkName: string readonly enableMcpPlaywright: boolean - readonly pnpmVersion: string + readonly bunVersion: string readonly agentMode?: AgentMode | undefined readonly agentAuto?: boolean | undefined readonly clonedOnHostname?: string | undefined diff --git a/packages/lib/src/core/template-defaults.ts b/packages/lib/src/core/template-defaults.ts index b72bb767..c1370c2e 100644 --- a/packages/lib/src/core/template-defaults.ts +++ b/packages/lib/src/core/template-defaults.ts @@ -24,7 +24,7 @@ type DefaultTemplateConfig = Pick< | "dockerNetworkMode" | "dockerSharedNetworkName" | "enableMcpPlaywright" - | "pnpmVersion" + | "bunVersion" > export const defaultDockerNetworkMode: TemplateConfig["dockerNetworkMode"] = "shared" @@ -60,5 +60,5 @@ export const defaultTemplateConfig = { dockerNetworkMode: defaultDockerNetworkMode, dockerSharedNetworkName: defaultDockerSharedNetworkName, enableMcpPlaywright: false, - pnpmVersion: "10.27.0" + bunVersion: "1.3.11" } satisfies DefaultTemplateConfig diff --git a/packages/lib/src/core/templates-entrypoint/agents-notice.ts b/packages/lib/src/core/templates-entrypoint/agents-notice.ts index a4bd55e6..9038a3ee 100644 --- a/packages/lib/src/core/templates-entrypoint/agents-notice.ts +++ b/packages/lib/src/core/templates-entrypoint/agents-notice.ts @@ -55,7 +55,7 @@ $MANAGED_END EOF )" cat < "$AGENTS_PATH" -Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, codex, opencode, oh-my-opencode, sshpass, git, node, pnpm и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~ +Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, bun, codex, opencode, oh-my-opencode, sshpass, git, node и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~ $MANAGED_BLOCK Если ты видишь файлы AGENTS.md внутри проекта, ты обязан их читать и соблюдать инструкции. EOF diff --git a/packages/lib/src/core/templates-entrypoint/base.ts b/packages/lib/src/core/templates-entrypoint/base.ts index 32126342..2cc36ed5 100644 --- a/packages/lib/src/core/templates-entrypoint/base.ts +++ b/packages/lib/src/core/templates-entrypoint/base.ts @@ -54,24 +54,22 @@ docker_git_upsert_ssh_env() { export const renderEntrypointPackageCache = (config: TemplateConfig): string => `# Keep package manager caches inside the project home volume PACKAGE_CACHE_ROOT="/home/${config.sshUser}/.docker-git/.cache/packages" -PACKAGE_PNPM_STORE="\${npm_config_store_dir:-\${PNPM_STORE_DIR:-$PACKAGE_CACHE_ROOT/pnpm/store}}" +PACKAGE_BUN_CACHE="\${BUN_INSTALL_CACHE_DIR:-$PACKAGE_CACHE_ROOT/bun/install/cache}" PACKAGE_NPM_CACHE="\${npm_config_cache:-\${NPM_CONFIG_CACHE:-$PACKAGE_CACHE_ROOT/npm}}" PACKAGE_YARN_CACHE="\${YARN_CACHE_FOLDER:-$PACKAGE_CACHE_ROOT/yarn}" -mkdir -p "$PACKAGE_PNPM_STORE" "$PACKAGE_NPM_CACHE" "$PACKAGE_YARN_CACHE" +mkdir -p "$PACKAGE_BUN_CACHE" "$PACKAGE_NPM_CACHE" "$PACKAGE_YARN_CACHE" chown -R 1000:1000 "$PACKAGE_CACHE_ROOT" || true cat < /etc/profile.d/docker-git-package-cache.sh -export PNPM_STORE_DIR="$PACKAGE_PNPM_STORE" -export npm_config_store_dir="$PACKAGE_PNPM_STORE" +export BUN_INSTALL_CACHE_DIR="$PACKAGE_BUN_CACHE" export NPM_CONFIG_CACHE="$PACKAGE_NPM_CACHE" export npm_config_cache="$PACKAGE_NPM_CACHE" export YARN_CACHE_FOLDER="$PACKAGE_YARN_CACHE" EOF chmod 0644 /etc/profile.d/docker-git-package-cache.sh -docker_git_upsert_ssh_env "PNPM_STORE_DIR" "$PACKAGE_PNPM_STORE" -docker_git_upsert_ssh_env "npm_config_store_dir" "$PACKAGE_PNPM_STORE" +docker_git_upsert_ssh_env "BUN_INSTALL_CACHE_DIR" "$PACKAGE_BUN_CACHE" docker_git_upsert_ssh_env "NPM_CONFIG_CACHE" "$PACKAGE_NPM_CACHE" docker_git_upsert_ssh_env "npm_config_cache" "$PACKAGE_NPM_CACHE" docker_git_upsert_ssh_env "YARN_CACHE_FOLDER" "$PACKAGE_YARN_CACHE"` diff --git a/packages/lib/src/core/templates-entrypoint/claude-extra-config.ts b/packages/lib/src/core/templates-entrypoint/claude-extra-config.ts index e376aa66..c484accb 100644 --- a/packages/lib/src/core/templates-entrypoint/claude-extra-config.ts +++ b/packages/lib/src/core/templates-entrypoint/claude-extra-config.ts @@ -46,7 +46,7 @@ if [[ "$CLAUDE_AUTO_SYSTEM_PROMPT" == "1" ]]; then if [[ ! -f "$CLAUDE_GLOBAL_PROMPT_FILE" ]] || grep -q "^$" "$CLAUDE_GLOBAL_PROMPT_FILE"; then cat < "$CLAUDE_GLOBAL_PROMPT_FILE" -Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, codex, opencode, oh-my-opencode, sshpass, claude, git, node, pnpm и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~ +Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, bun, codex, opencode, oh-my-opencode, sshpass, claude, git, node и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~ Рабочая папка проекта (git clone): __TARGET_DIR__ Доступные workspace пути: __TARGET_DIR__ $CLAUDE_WORKSPACE_CONTEXT diff --git a/packages/lib/src/core/templates-entrypoint/gemini.ts b/packages/lib/src/core/templates-entrypoint/gemini.ts index b7a6f8a0..cb1092fb 100644 --- a/packages/lib/src/core/templates-entrypoint/gemini.ts +++ b/packages/lib/src/core/templates-entrypoint/gemini.ts @@ -266,7 +266,7 @@ fi cat < "$GEMINI_MD_PATH" -Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, codex, gemini, claude, opencode, oh-my-opencode, sshpass, git, node, pnpm и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~ +Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, bun, codex, gemini, claude, opencode, oh-my-opencode, sshpass, git, node и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~ Рабочая папка проекта (git clone): __TARGET_DIR__ Доступные workspace пути: __TARGET_DIR__ $GEMINI_WORKSPACE_CONTEXT diff --git a/packages/lib/src/core/templates-entrypoint/tasks.ts b/packages/lib/src/core/templates-entrypoint/tasks.ts index d439109d..09d2e067 100644 --- a/packages/lib/src/core/templates-entrypoint/tasks.ts +++ b/packages/lib/src/core/templates-entrypoint/tasks.ts @@ -193,7 +193,7 @@ const renderCloneBody = (config: TemplateConfig): string => ].join("\n") // CHANGE: provision docker-git scripts into workspace after successful clone -// WHY: git hooks reference scripts/ relative to repo root (e.g. "node scripts/session-backup-gist.js"); +// WHY: git hooks reference scripts/ relative to repo root (e.g. "bun scripts/session-backup-gist.js"); // symlinking embedded /opt/docker-git/scripts makes them available in any cloned repo // REF: issue-176 // PURITY: SHELL diff --git a/packages/lib/src/core/templates/docker-compose.ts b/packages/lib/src/core/templates/docker-compose.ts index 7a6b1b9c..f744d89d 100644 --- a/packages/lib/src/core/templates/docker-compose.ts +++ b/packages/lib/src/core/templates/docker-compose.ts @@ -1,8 +1,8 @@ import { - resolveComposeProjectName, dockerGitSharedCacheVolumeName, dockerGitSharedCodexVolumeName, resolveComposeNetworkName, + resolveComposeProjectName, resolveProjectBootstrapVolumeName, type TemplateConfig } from "../domain.js" diff --git a/packages/lib/src/core/templates/dockerfile.ts b/packages/lib/src/core/templates/dockerfile.ts index c27a49fa..7374f76d 100644 --- a/packages/lib/src/core/templates/dockerfile.ts +++ b/packages/lib/src/core/templates/dockerfile.ts @@ -31,13 +31,12 @@ RUN printf "export NVM_DIR=/usr/local/nvm\\n[ -s /usr/local/nvm/nvm.sh ] && . /u > /etc/profile.d/nvm.sh && chmod 0644 /etc/profile.d/nvm.sh` const renderDockerfileBunPrelude = (config: TemplateConfig): string => - `# Tooling: pnpm + Codex CLI (bun) + oh-my-opencode (npm + platform binary) + Claude Code CLI (npm) -RUN corepack enable && corepack prepare pnpm@${config.pnpmVersion} --activate + `# Tooling: Bun + Codex CLI (bun) + oh-my-opencode (npm + platform binary) + Claude Code CLI (npm) ENV TERM=xterm-256color RUN set -eu; \ for attempt in 1 2 3 4 5; do \ if curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 https://bun.sh/install -o /tmp/bun-install.sh \ - && BUN_INSTALL=/usr/local/bun bash /tmp/bun-install.sh; then \ + && BUN_INSTALL=/usr/local/bun BUN_VERSION=${config.bunVersion} bash /tmp/bun-install.sh; then \ rm -f /tmp/bun-install.sh; \ exit 0; \ fi; \ diff --git a/packages/lib/src/shell/clone.ts b/packages/lib/src/shell/clone.ts index e59df91a..56431e85 100644 --- a/packages/lib/src/shell/clone.ts +++ b/packages/lib/src/shell/clone.ts @@ -11,7 +11,7 @@ import { CommandFailedError } from "./errors.js" const successExitCode = Number(ExitCode(0)) // CHANGE: read shortcut requests from process argv and npm lifecycle metadata -// WHY: allow pnpm run clone/open to work without "--" +// WHY: allow bun run clone/open to work without "--" // QUOTE(ТЗ): "Добавить команду open. ... Просто открывает существующий по ссылке" // REF: user-request-2026-01-27 // SOURCE: n/a @@ -37,19 +37,19 @@ const runDockerGitCommand = ( const workspaceRoot = process.cwd() const appRoot = path.join(workspaceRoot, "packages", "app") const dockerGitCli = path.join(appRoot, "dist", "src", "docker-git", "main.js") - const buildLabel = `pnpm -C ${appRoot} build:docker-git` - const runLabel = `node ${dockerGitCli} ${commandName}` + const buildLabel = `bun run --cwd ${appRoot} build:docker-git` + const runLabel = `bun ${dockerGitCli} ${commandName}` yield* _( runCommandWithExitCodes( - { cwd: workspaceRoot, command: "pnpm", args: ["-C", appRoot, "build:docker-git"] }, + { cwd: workspaceRoot, command: "bun", args: ["run", "--cwd", appRoot, "build:docker-git"] }, [successExitCode], (exitCode) => new CommandFailedError({ command: buildLabel, exitCode }) ) ) yield* _( runCommandWithExitCodes( - { cwd: workspaceRoot, command: "node", args: [dockerGitCli, commandName, ...args] }, + { cwd: workspaceRoot, command: "bun", args: [dockerGitCli, commandName, ...args] }, [successExitCode], (exitCode) => new CommandFailedError({ command: runLabel, exitCode }) ) diff --git a/packages/lib/src/shell/command-runner.ts b/packages/lib/src/shell/command-runner.ts index a83635ad..1709f97c 100644 --- a/packages/lib/src/shell/command-runner.ts +++ b/packages/lib/src/shell/command-runner.ts @@ -128,20 +128,19 @@ export const runCommandWithCapturedOutput = ( Effect.gen(function*(_) { const executor = yield* _(CommandExecutor.CommandExecutor) const process = yield* _(executor.start(buildCommand(spec, "pipe", "pipe", "pipe"))) - const [stdout, stderr, exitCode] = yield* _( + const [stdout, stderr] = yield* _( Effect.all( [ collectStreamText(process.stdout), - collectStreamText(process.stderr), - Effect.map(process.exitCode, (value) => Number(value)) + collectStreamText(process.stderr) ], { concurrency: "unbounded" } ) ) + const exitCode = yield* _(process.exitCode) yield* _( ensureExitCode(exitCode, okExitCodes, (numericExitCode) => - onFailure(numericExitCode, combineCommandOutput(stdout, stderr)) - ) + onFailure(numericExitCode, combineCommandOutput(stdout, stderr))) ) }) ) diff --git a/packages/lib/src/shell/config.ts b/packages/lib/src/shell/config.ts index eb936c10..29289983 100644 --- a/packages/lib/src/shell/config.ts +++ b/packages/lib/src/shell/config.ts @@ -10,7 +10,7 @@ import { defaultTemplateConfig, type ProjectConfig } from "../core/domain.js" import { ConfigDecodeError, ConfigNotFoundError } from "./errors.js" import { resolveBaseDir } from "./paths.js" -const TemplateConfigSchema = Schema.Struct({ +const TemplateConfigInputSchema = Schema.Struct({ containerName: Schema.String, serviceName: Schema.String, sshUser: Schema.String, @@ -62,16 +62,32 @@ const TemplateConfigSchema = Schema.Struct({ enableMcpPlaywright: Schema.optionalWith(Schema.Boolean, { default: () => defaultTemplateConfig.enableMcpPlaywright }), - pnpmVersion: Schema.String, + bunVersion: Schema.optional(Schema.String), + pnpmVersion: Schema.optional(Schema.String), clonedOnHostname: Schema.optional(Schema.String) }) -const ProjectConfigSchema = Schema.Struct({ +type DecodedProjectConfigInput = Schema.Schema.Type + +const normalizeLegacyProjectConfig = ( + config: DecodedProjectConfigInput +): ProjectConfig => { + const { bunVersion, pnpmVersion, ...template } = config.template + return { + schemaVersion: config.schemaVersion, + template: { + ...template, + bunVersion: bunVersion ?? pnpmVersion ?? defaultTemplateConfig.bunVersion + } + } +} + +const ProjectConfigInputSchema = Schema.Struct({ schemaVersion: Schema.Literal(1), - template: TemplateConfigSchema + template: TemplateConfigInputSchema }) -const ProjectConfigJsonSchema = Schema.parseJson(ProjectConfigSchema) +const ProjectConfigJsonSchema = Schema.parseJson(ProjectConfigInputSchema) const decodeProjectConfig = ( path: string, @@ -85,7 +101,7 @@ const decodeProjectConfig = ( message: TreeFormatter.formatIssueSync(issue) }) ), - onRight: (value) => Effect.succeed(value) + onRight: (value) => Effect.succeed(normalizeLegacyProjectConfig(value)) }) // CHANGE: read and decode docker-git.json from disk diff --git a/packages/lib/src/shell/docker.ts b/packages/lib/src/shell/docker.ts index f6982b5e..543535d9 100644 --- a/packages/lib/src/shell/docker.ts +++ b/packages/lib/src/shell/docker.ts @@ -4,7 +4,12 @@ import { ExitCode } from "@effect/platform/CommandExecutor" import type { PlatformError } from "@effect/platform/Error" import { Duration, Effect, pipe, Schedule } from "effect" -import { runCommandCapture, runCommandExitCode, runCommandWithCapturedOutput, runCommandWithExitCodes } from "./command-runner.js" +import { + runCommandCapture, + runCommandExitCode, + runCommandWithCapturedOutput, + runCommandWithExitCodes +} from "./command-runner.js" import { composeSpec, resolveDockerComposeEnv } from "./docker-compose-env.js" import { parseInspectNetworkEntry } from "./docker-inspect-parse.js" import { CommandFailedError, DockerCommandError } from "./errors.js" diff --git a/packages/lib/src/shell/files.ts b/packages/lib/src/shell/files.ts index b4a30c9a..fdb3aa23 100644 --- a/packages/lib/src/shell/files.ts +++ b/packages/lib/src/shell/files.ts @@ -9,6 +9,7 @@ import { resolveComposeResourceLimits, withDefaultResourceLimitIntent } from ".. import { type FileSpec, planFiles } from "../core/templates.js" import { FileExistsError } from "./errors.js" import { resolveBaseDir } from "./paths.js" +import { resolveWorkspaceRoot } from "./workspace-root.js" const ensureParentDir = (path: Path.Path, fs: FileSystem.FileSystem, filePath: string) => fs.makeDirectory(path.dirname(filePath), { recursive: true }) @@ -114,9 +115,9 @@ const provisionDockerGitScripts = ( fs: FileSystem.FileSystem, path: Path.Path, baseDir: string -): Effect.Effect => +): Effect.Effect => Effect.gen(function*(_) { - const workspaceRoot = process.cwd() + const workspaceRoot = yield* _(resolveWorkspaceRoot(process.cwd())) const sourceScriptsDir = path.join(workspaceRoot, "scripts") const targetScriptsDir = path.join(baseDir, "scripts") @@ -132,7 +133,8 @@ const provisionDockerGitScripts = ( const targetPath = path.join(targetScriptsDir, scriptName) const exists = yield* _(fs.exists(sourcePath)) if (exists) { - yield* _(fs.copyFile(sourcePath, targetPath)) + const contents = yield* _(fs.readFileString(sourcePath)) + yield* _(fs.writeFileString(targetPath, contents)) } } }) diff --git a/packages/lib/src/shell/workspace-root.ts b/packages/lib/src/shell/workspace-root.ts new file mode 100644 index 00000000..d6f26082 --- /dev/null +++ b/packages/lib/src/shell/workspace-root.ts @@ -0,0 +1,49 @@ +import type { PlatformError } from "@effect/platform/Error" +import * as FileSystem from "@effect/platform/FileSystem" +import * as Path from "@effect/platform/Path" +import { Effect } from "effect" + +const workspaceMarkers: ReadonlyArray = ["bunfig.toml", ".git"] + +const hasWorkspaceMarker = ( + fs: FileSystem.FileSystem, + path: Path.Path, + directory: string +): Effect.Effect => + Effect.gen(function*(_) { + for (const marker of workspaceMarkers) { + if (yield* _(fs.exists(path.join(directory, marker)))) { + return true + } + } + return false + }) + +const resolveWorkspaceRootFrom = ( + fs: FileSystem.FileSystem, + path: Path.Path, + startDir: string, + currentDir: string +): Effect.Effect => + Effect.gen(function*(_) { + if (yield* _(hasWorkspaceMarker(fs, path, currentDir))) { + return currentDir + } + + const parent = path.dirname(currentDir) + if (parent === currentDir) { + return startDir + } + + return yield* _(resolveWorkspaceRootFrom(fs, path, startDir, parent)) + }) + +export const resolveWorkspaceRoot = ( + startDir: string +): Effect.Effect => + Effect.gen(function*(_) { + const fs = yield* _(FileSystem.FileSystem) + const path = yield* _(Path.Path) + const resolvedStartDir = path.resolve(startDir) + return yield* _(resolveWorkspaceRootFrom(fs, path, resolvedStartDir, resolvedStartDir)) + }) diff --git a/packages/lib/src/usecases/actions/create-project-conflicts.ts b/packages/lib/src/usecases/actions/create-project-conflicts.ts new file mode 100644 index 00000000..3d3c44c3 --- /dev/null +++ b/packages/lib/src/usecases/actions/create-project-conflicts.ts @@ -0,0 +1,179 @@ +/* jscpd:ignore-start */ +import type * as CommandExecutor from "@effect/platform/CommandExecutor" +import type { PlatformError } from "@effect/platform/Error" +import type * as FileSystem from "@effect/platform/FileSystem" +import type * as Path from "@effect/platform/Path" +import { Effect } from "effect" + +import type { TemplateConfig } from "../../core/domain.js" +import { resolveComposeProjectName, resolveProjectBootstrapVolumeName } from "../../core/domain.js" +import { type DockerCommandError, DockerIdentityConflictError } from "../../shell/errors.js" +import type { ProjectStatus } from "../projects-core.js" +import { loadProjectIndex, loadProjectStatus } from "../projects-core.js" +import { deleteDockerGitProject } from "../projects-delete.js" + +type CreateProjectRuntime = FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor + +type DockerIdentityOwner = Pick< + TemplateConfig, + "containerName" | "serviceName" | "volumeName" | "enableMcpPlaywright" +> + +type DockerIdentityNamespace = "container" | "composeProject" | "volume" + +type DockerIdentityClaim = Omit & { + readonly namespace: DockerIdentityNamespace +} + +type ConflictState = { + readonly conflicts: Array + readonly conflictingProjects: Map< + string, + { + readonly projectDir: string + readonly repoUrl: string + readonly containerName: string + readonly serviceName: string + } + > +} + +const resolveBrowserContainerClaims = ( + config: DockerIdentityOwner +): ReadonlyArray => + config.enableMcpPlaywright + ? [{ namespace: "container", kind: "browserContainerName", name: `${config.containerName}-browser` }] + : [] + +const resolveBrowserVolumeClaims = ( + config: DockerIdentityOwner +): ReadonlyArray => + config.enableMcpPlaywright + ? [{ namespace: "volume", kind: "browserVolumeName", name: `${config.volumeName}-browser` }] + : [] + +const resolveDockerIdentityClaims = ( + config: DockerIdentityOwner +): ReadonlyArray => [ + { namespace: "container", kind: "containerName", name: config.containerName }, + ...resolveBrowserContainerClaims(config), + { namespace: "composeProject", kind: "serviceName", name: resolveComposeProjectName(config) }, + { namespace: "volume", kind: "volumeName", name: config.volumeName }, + ...resolveBrowserVolumeClaims(config), + { namespace: "volume", kind: "bootstrapVolumeName", name: resolveProjectBootstrapVolumeName(config) } +] + +const loadProjectStatusOrNull = (configPath: string) => + loadProjectStatus(configPath).pipe( + Effect.match({ + onFailure: () => null, + onSuccess: (value) => value + }) + ) + +const collectSharedClaims = ( + candidateClaims: ReadonlyArray, + existingClaims: ReadonlyArray, + projectDir: string +): ReadonlyArray => + candidateClaims.flatMap((candidate) => + existingClaims.some( + (existing) => existing.namespace === candidate.namespace && existing.name === candidate.name + ) + ? [{ conflictingProjectDir: projectDir, kind: candidate.kind, name: candidate.name }] + : [] + ) + +const appendClaims = ( + conflicts: Array, + sharedClaims: ReadonlyArray +): void => { + for (const claim of sharedClaims) { + conflicts.push(claim) + } +} + +const rememberConflictingProject = ( + conflictingProjects: ConflictState["conflictingProjects"], + status: ProjectStatus +): void => { + conflictingProjects.set(status.projectDir, { + projectDir: status.projectDir, + repoUrl: status.config.template.repoUrl, + containerName: status.config.template.containerName, + serviceName: status.config.template.serviceName + }) +} + +const scanConflicts = ( + resolvedOutDir: string, + config: DockerIdentityOwner +): Effect.Effect => + Effect.gen(function*(_) { + const index = yield* _(loadProjectIndex()) + if (index === null) { + return null + } + + const candidateClaims = resolveDockerIdentityClaims(config) + const state: ConflictState = { + conflicts: [], + conflictingProjects: new Map() + } + + for (const configPath of index.configPaths) { + const status = yield* _(loadProjectStatusOrNull(configPath)) + if (status === null || status.projectDir === resolvedOutDir) { + continue + } + + const sharedClaims = collectSharedClaims( + candidateClaims, + resolveDockerIdentityClaims(status.config.template), + status.projectDir + ) + if (sharedClaims.length === 0) { + continue + } + + appendClaims(state.conflicts, sharedClaims) + rememberConflictingProject(state.conflictingProjects, status) + } + + return state + }) + +const deleteConflictingProjects = ( + conflictingProjects: ConflictState["conflictingProjects"] +): Effect.Effect => + Effect.gen(function*(_) { + for (const conflictingProject of conflictingProjects.values()) { + yield* _( + Effect.logWarning( + `Force enabled: replacing conflicting docker-git project ${conflictingProject.projectDir}` + ) + ) + yield* _(deleteDockerGitProject(conflictingProject)) + } + }) + +export const deleteConflictingProjectsIfNeeded = ( + resolvedOutDir: string, + config: DockerIdentityOwner, + force: boolean +): Effect.Effect => + Effect.gen(function*(_) { + const state = yield* _(scanConflicts(resolvedOutDir, config)) + if (state === null || state.conflicts.length === 0) { + return + } + + if (!force) { + return yield* _( + Effect.fail(new DockerIdentityConflictError({ projectDir: resolvedOutDir, conflicts: state.conflicts })) + ) + } + + yield* _(deleteConflictingProjects(state.conflictingProjects)) + }) +/* jscpd:ignore-end */ diff --git a/packages/lib/src/usecases/actions/create-project-open-ssh.ts b/packages/lib/src/usecases/actions/create-project-open-ssh.ts new file mode 100644 index 00000000..fb4cbc6d --- /dev/null +++ b/packages/lib/src/usecases/actions/create-project-open-ssh.ts @@ -0,0 +1,123 @@ +import type * as CommandExecutor from "@effect/platform/CommandExecutor" +import * as FileSystem from "@effect/platform/FileSystem" +import * as Path from "@effect/platform/Path" +import { Effect } from "effect" + +import type { CreateCommand } from "../../core/domain.js" +import { runCommandWithExitCodes } from "../../shell/command-runner.js" +import { CommandFailedError } from "../../shell/errors.js" +import { renderError } from "../errors.js" +import { findSshPrivateKey } from "../path-helpers.js" +import { buildSshCommand, getContainerIpIfInsideContainer } from "../projects-core.js" +import { ensureTerminalCursorVisible } from "../terminal-cursor.js" + +type CreateProjectOpenSshRuntime = + | FileSystem.FileSystem + | Path.Path + | CommandExecutor.CommandExecutor + +const isInteractiveTty = (): boolean => process.stdin.isTTY && process.stdout.isTTY + +const buildSshArgs = ( + config: CreateCommand["config"], + sshKeyPath: string | null, + remoteCommand?: string, + ipAddress?: string +): ReadonlyArray => { + const host = ipAddress ?? "localhost" + const port = ipAddress ? 22 : config.sshPort + const args: Array = [] + if (sshKeyPath !== null) { + args.push("-i", sshKeyPath) + } + args.push( + "-tt", + "-Y", + "-o", + "LogLevel=ERROR", + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + "-p", + String(port), + `${config.sshUser}@${host}` + ) + if (remoteCommand !== undefined) { + args.push(remoteCommand) + } + return args +} + +const openSshBestEffort = ( + template: CreateCommand["config"], + remoteCommand?: string +): Effect.Effect => + Effect.gen(function*(_) { + const fs = yield* _(FileSystem.FileSystem) + const path = yield* _(Path.Path) + + const ipAddress = yield* _( + getContainerIpIfInsideContainer(fs, process.cwd(), template.containerName).pipe( + Effect.orElse(() => Effect.succeed("")) + ) + ) + + const sshKey = yield* _(findSshPrivateKey(fs, path, process.cwd())) + const sshCommand = buildSshCommand(template, sshKey, ipAddress) + const remoteCommandLabel = remoteCommand === undefined ? "" : ` (${remoteCommand})` + + yield* _(Effect.log(`Opening SSH: ${sshCommand}${remoteCommandLabel}`)) + yield* _(ensureTerminalCursorVisible()) + yield* _( + runCommandWithExitCodes( + { + cwd: process.cwd(), + command: "ssh", + args: buildSshArgs(template, sshKey, remoteCommand, ipAddress) + }, + [0, 130], + (exitCode) => new CommandFailedError({ command: "ssh", exitCode }) + ).pipe(Effect.ensuring(ensureTerminalCursorVisible())) + ) + }).pipe( + Effect.asVoid, + Effect.matchEffect({ + onFailure: (error) => Effect.logWarning(`SSH auto-open failed: ${renderError(error)}`), + onSuccess: () => Effect.void + }) + ) + +const resolveInteractiveRemoteCommand = ( + projectConfig: CreateCommand["config"], + interactiveAgent: boolean +): string | undefined => + interactiveAgent && projectConfig.agentMode !== undefined + ? `cd '${projectConfig.targetDir}' && ${projectConfig.agentMode}` + : undefined + +export const maybeOpenSsh = ( + command: CreateCommand, + hasAgent: boolean, + waitForAgent: boolean, + projectConfig: CreateCommand["config"] +): Effect.Effect => + Effect.gen(function*(_) { + const interactiveAgent = hasAgent && !waitForAgent + if (!command.openSsh || (hasAgent && !interactiveAgent)) { + return + } + + if (!command.runUp) { + yield* _(Effect.logWarning("Skipping SSH auto-open: docker compose up disabled (--no-up).")) + return + } + + if (!isInteractiveTty()) { + yield* _(Effect.logWarning("Skipping SSH auto-open: not running in an interactive TTY.")) + return + } + + const remoteCommand = resolveInteractiveRemoteCommand(projectConfig, interactiveAgent) + yield* _(openSshBestEffort(projectConfig, remoteCommand)) + }).pipe(Effect.asVoid) diff --git a/packages/lib/src/usecases/actions/create-project.ts b/packages/lib/src/usecases/actions/create-project.ts index 842e5a84..55bd9e2f 100644 --- a/packages/lib/src/usecases/actions/create-project.ts +++ b/packages/lib/src/usecases/actions/create-project.ts @@ -1,36 +1,32 @@ +/* jscpd:ignore-start */ import type * as CommandExecutor from "@effect/platform/CommandExecutor" import type { PlatformError } from "@effect/platform/Error" -import * as FileSystem from "@effect/platform/FileSystem" +import type * as FileSystem from "@effect/platform/FileSystem" import * as Path from "@effect/platform/Path" import { Effect } from "effect" -import type { CreateCommand, ParseError, TemplateConfig } from "../../core/domain.js" -import { deriveRepoPathParts, resolveComposeProjectName, resolveProjectBootstrapVolumeName } from "../../core/domain.js" -import { runCommandWithExitCodes } from "../../shell/command-runner.js" +import type { CreateCommand, ParseError } from "../../core/domain.js" +import { deriveRepoPathParts } from "../../core/domain.js" import { ensureDockerDaemonAccess } from "../../shell/docker.js" -import { CommandFailedError, DockerIdentityConflictError } from "../../shell/errors.js" import type { AgentFailedError, AuthError, CloneFailedError, DockerAccessError, DockerCommandError, - DockerIdentityConflict, + DockerIdentityConflictError, FileExistsError, PortProbeError } from "../../shell/errors.js" import { logDockerAccessInfo } from "../access-log.js" import { resolveAutoAgentMode } from "../agent-auto-select.js" -import { renderError } from "../errors.js" import { applyGithubForkConfig } from "../github-fork.js" import { validateGithubCloneAuthTokenPreflight } from "../github-token-preflight.js" import { defaultProjectsRoot } from "../menu-helpers.js" -import { findSshPrivateKey } from "../path-helpers.js" -import { buildSshCommand, getContainerIpIfInsideContainer, loadProjectIndex, loadProjectStatus } from "../projects-core.js" -import { deleteDockerGitProject } from "../projects-delete.js" import { resolveTemplateResourceLimits } from "../resource-limits.js" import { autoSyncState } from "../state-repo.js" -import { ensureTerminalCursorVisible } from "../terminal-cursor.js" +import { deleteConflictingProjectsIfNeeded } from "./create-project-conflicts.js" +import { maybeOpenSsh } from "./create-project-open-ssh.js" import { runDockerDownCleanup, runDockerUpIfNeeded } from "./docker-up.js" import { buildProjectConfigs, resolveDockerGitRootRelativePath } from "./paths.js" import { resolveSshPort } from "./ports.js" @@ -55,6 +51,14 @@ type CreateContext = { readonly resolveRootPath: (value: string) => string } +type BuiltProjectConfigs = ReturnType +type PreparedProject = { + readonly resolvedOutDir: string + readonly finalConfig: CreateCommand["config"] + readonly globalConfig: BuiltProjectConfigs["globalConfig"] + readonly projectConfig: BuiltProjectConfigs["projectConfig"] +} + const makeCreateContext = (path: Path.Path, baseDir: string): CreateContext => { const projectsRoot = path.resolve(defaultProjectsRoot(baseDir)) const resolveRootPath = (value: string): string => resolveDockerGitRootRelativePath(path, projectsRoot, value) @@ -97,214 +101,6 @@ const formatStateSyncLabel = (repoUrl: string): string => { return repoPath.length > 0 ? repoPath : repoUrl } -type DockerIdentityOwner = Pick - -type DockerIdentityNamespace = "container" | "composeProject" | "volume" - -type DockerIdentityClaim = Omit & { - readonly namespace: DockerIdentityNamespace -} - -const resolveDockerIdentityClaims = ( - config: DockerIdentityOwner -): ReadonlyArray => [ - { namespace: "container", kind: "containerName", name: config.containerName }, - ...(config.enableMcpPlaywright - ? [{ namespace: "container" as const, kind: "browserContainerName" as const, name: `${config.containerName}-browser` }] - : []), - { namespace: "composeProject", kind: "serviceName", name: resolveComposeProjectName(config) }, - { namespace: "volume", kind: "volumeName", name: config.volumeName }, - ...(config.enableMcpPlaywright - ? [{ namespace: "volume" as const, kind: "browserVolumeName" as const, name: `${config.volumeName}-browser` }] - : []), - { namespace: "volume", kind: "bootstrapVolumeName", name: resolveProjectBootstrapVolumeName(config) } -] - -const deleteConflictingProjectsIfNeeded = ( - resolvedOutDir: string, - config: DockerIdentityOwner, - force: boolean -): Effect.Effect => - Effect.gen(function*(_) { - const index = yield* _(loadProjectIndex()) - if (index === null) { - return - } - - const candidateClaims = resolveDockerIdentityClaims(config) - const conflicts: Array = [] - const conflictingProjects = new Map() - - for (const configPath of index.configPaths) { - const status = yield* _( - loadProjectStatus(configPath).pipe( - Effect.match({ - onFailure: () => null, - onSuccess: (value) => value - }) - ) - ) - if (status === null || status.projectDir === resolvedOutDir) { - continue - } - - const existingClaims = resolveDockerIdentityClaims(status.config.template) - const sharedClaims = candidateClaims.flatMap((candidate) => - existingClaims.some( - (existing) => existing.namespace === candidate.namespace && existing.name === candidate.name - ) - ? [{ conflictingProjectDir: status.projectDir, kind: candidate.kind, name: candidate.name }] - : [] - ) - - if (sharedClaims.length === 0) { - continue - } - - conflicts.push(...sharedClaims) - conflictingProjects.set(status.projectDir, { - projectDir: status.projectDir, - repoUrl: status.config.template.repoUrl, - containerName: status.config.template.containerName, - serviceName: status.config.template.serviceName - }) - } - - if (conflicts.length === 0) { - return - } - - if (!force) { - return yield* _(Effect.fail(new DockerIdentityConflictError({ projectDir: resolvedOutDir, conflicts }))) - } - - for (const conflictingProject of conflictingProjects.values()) { - yield* _( - Effect.logWarning( - `Force enabled: replacing conflicting docker-git project ${conflictingProject.projectDir}` - ) - ) - yield* _(deleteDockerGitProject(conflictingProject)) - } - }) - -const isInteractiveTty = (): boolean => process.stdin.isTTY && process.stdout.isTTY - -const buildSshArgs = ( - config: CreateCommand["config"], - sshKeyPath: string | null, - remoteCommand?: string, - ipAddress?: string -): ReadonlyArray => { - const host = ipAddress ?? "localhost" - const port = ipAddress ? 22 : config.sshPort - const args: Array = [] - if (sshKeyPath !== null) { - args.push("-i", sshKeyPath) - } - args.push( - "-tt", - "-Y", - "-o", - "LogLevel=ERROR", - "-o", - "StrictHostKeyChecking=no", - "-o", - "UserKnownHostsFile=/dev/null", - "-p", - String(port), - `${config.sshUser}@${host}` - ) - if (remoteCommand !== undefined) { - args.push(remoteCommand) - } - return args -} - -// CHANGE: auto-open SSH after environment is created (best-effort) -// WHY: clone flow should drop the user into the container without manual copy/paste -// QUOTE(ТЗ): "Мне надо что бы он сразу открыл SSH" -// REF: issue-39 -// SOURCE: n/a -// FORMAT THEOREM: forall c: openSsh(c) -> ssh_session_started(c) || warning_logged(c) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: SSH failures do not fail the create/clone command -// COMPLEXITY: O(1) + ssh -const openSshBestEffort = ( - template: CreateCommand["config"], - remoteCommand?: string -): Effect.Effect => - Effect.gen(function*(_) { - const fs = yield* _(FileSystem.FileSystem) - const path = yield* _(Path.Path) - - const ipAddress = yield* _( - getContainerIpIfInsideContainer(fs, process.cwd(), template.containerName).pipe( - Effect.orElse(() => Effect.succeed("")) - ) - ) - - const sshKey = yield* _(findSshPrivateKey(fs, path, process.cwd())) - const sshCommand = buildSshCommand(template, sshKey, ipAddress) - - const remoteCommandLabel = remoteCommand === undefined ? "" : ` (${remoteCommand})` - - yield* _(Effect.log(`Opening SSH: ${sshCommand}${remoteCommandLabel}`)) - yield* _(ensureTerminalCursorVisible()) - yield* _( - runCommandWithExitCodes( - { - cwd: process.cwd(), - command: "ssh", - args: buildSshArgs(template, sshKey, remoteCommand, ipAddress) - }, - [0, 130], - (exitCode) => new CommandFailedError({ command: "ssh", exitCode }) - ).pipe(Effect.ensuring(ensureTerminalCursorVisible())) - ) - }).pipe( - Effect.asVoid, - Effect.matchEffect({ - onFailure: (error) => Effect.logWarning(`SSH auto-open failed: ${renderError(error)}`), - onSuccess: () => Effect.void - }) - ) - -const resolveInteractiveRemoteCommand = ( - projectConfig: CreateCommand["config"], - interactiveAgent: boolean -): string | undefined => - interactiveAgent && projectConfig.agentMode !== undefined - ? `cd '${projectConfig.targetDir}' && ${projectConfig.agentMode}` - : undefined - -const maybeOpenSsh = ( - command: CreateCommand, - hasAgent: boolean, - waitForAgent: boolean, - projectConfig: CreateCommand["config"] -): Effect.Effect => - Effect.gen(function*(_) { - const interactiveAgent = hasAgent && !waitForAgent - if (!command.openSsh || (hasAgent && !interactiveAgent)) { - return - } - - if (!command.runUp) { - yield* _(Effect.logWarning("Skipping SSH auto-open: docker compose up disabled (--no-up).")) - return - } - - if (!isInteractiveTty()) { - yield* _(Effect.logWarning("Skipping SSH auto-open: not running in an interactive TTY.")) - return - } - - const remoteCommand = resolveInteractiveRemoteCommand(projectConfig, interactiveAgent) - yield* _(openSshBestEffort(projectConfig, remoteCommand)) - }).pipe(Effect.asVoid) - const resolveFinalAgentConfig = ( resolvedConfig: CreateCommand["config"] ): Effect.Effect => @@ -330,10 +126,10 @@ const maybeCleanupAfterAgent = ( yield* _(runDockerDownCleanup(resolvedOutDir)) }) -const runCreateProject = ( +export const prepareProject = ( path: Path.Path, command: CreateCommand -): Effect.Effect => +): Effect.Effect => Effect.gen(function*(_) { if (command.runUp) { yield* _(ensureDockerDaemonAccess(process.cwd())) @@ -343,10 +139,7 @@ const runCreateProject = ( const resolvedOutDir = path.resolve(ctx.resolveRootPath(command.outDir)) const rootedConfig = resolveRootedConfig(command, ctx) - yield* _( - deleteConflictingProjectsIfNeeded(resolvedOutDir, rootedConfig, command.force) - ) - + yield* _(deleteConflictingProjectsIfNeeded(resolvedOutDir, rootedConfig, command.force)) yield* _(validateGithubCloneAuthTokenPreflight(rootedConfig)) const resolvedConfig = yield* _(resolveCreateConfig(rootedConfig, resolvedOutDir)) @@ -354,7 +147,6 @@ const runCreateProject = ( const { globalConfig, projectConfig } = buildProjectConfigs(path, ctx.baseDir, resolvedOutDir, finalConfig) yield* _(migrateProjectOrchLayout(ctx.baseDir, globalConfig, ctx.resolveRootPath)) - const createdFiles = yield* _( prepareProjectFiles(resolvedOutDir, ctx.baseDir, globalConfig, projectConfig, { force: command.force, @@ -362,25 +154,20 @@ const runCreateProject = ( }) ) yield* _(logCreatedProject(resolvedOutDir, createdFiles)) + return { resolvedOutDir, finalConfig, globalConfig, projectConfig } + }) - const hasAgent = finalConfig.agentMode !== undefined - const waitForAgent = hasAgent && (finalConfig.agentAuto ?? false) +export const runPreparedProject = ( + command: CreateCommand, + prepared: PreparedProject +): Effect.Effect => + Effect.gen(function*(_) { + const hasAgent = prepared.finalConfig.agentMode !== undefined + const waitForAgent = hasAgent && (prepared.finalConfig.agentAuto ?? false) - // CHANGE: run autoSyncState before docker compose up to prevent bind-mount inode invalidation - // WHY: git reset --hard in autoSyncState deletes and recreates .orch/auth/codex; if docker is - // already running with a bind-mount on that directory, the old inode becomes unreachable - // inside the container — codex fails with "No such file or directory" - // QUOTE(ТЗ): n/a - // REF: issue-158 - // SOURCE: n/a - // FORMAT THEOREM: ∀p: synced(p) ∧ stable_inode(.orch/auth/codex, p) → valid_mount(docker_up(p)) - // PURITY: SHELL - // EFFECT: Effect - // INVARIANT: .orch/auth/codex inode is stable when docker compose up runs - // COMPLEXITY: O(git_sync) before O(docker_up) - yield* _(autoSyncState(`chore(state): update ${formatStateSyncLabel(projectConfig.repoUrl)}`)) + yield* _(autoSyncState(`chore(state): update ${formatStateSyncLabel(prepared.projectConfig.repoUrl)}`)) yield* _( - runDockerUpIfNeeded(resolvedOutDir, projectConfig, { + runDockerUpIfNeeded(prepared.resolvedOutDir, prepared.projectConfig, { runUp: command.runUp, waitForClone: command.waitForClone, waitForAgent, @@ -389,13 +176,22 @@ const runCreateProject = ( }) ) if (command.runUp) { - yield* _(logDockerAccessInfo(resolvedOutDir, projectConfig)) + yield* _(logDockerAccessInfo(prepared.resolvedOutDir, prepared.projectConfig)) } - yield* _(maybeCleanupAfterAgent(waitForAgent, resolvedOutDir)) + yield* _(maybeCleanupAfterAgent(waitForAgent, prepared.resolvedOutDir)) + yield* _(maybeOpenSsh(command, hasAgent, waitForAgent, prepared.projectConfig)) + }).pipe(Effect.asVoid) - yield* _(maybeOpenSsh(command, hasAgent, waitForAgent, projectConfig)) +const runCreateProject = ( + path: Path.Path, + command: CreateCommand +): Effect.Effect => + Effect.gen(function*(_) { + const prepared = yield* _(prepareProject(path, command)) + yield* _(runPreparedProject(command, prepared)) }).pipe(Effect.asVoid) export const createProject = (command: CreateCommand): Effect.Effect => Path.Path.pipe(Effect.flatMap((path) => runCreateProject(path, command))) +/* jscpd:ignore-end */ diff --git a/packages/lib/src/usecases/actions/ports.ts b/packages/lib/src/usecases/actions/ports.ts index 1bcb1487..f96a17c2 100644 --- a/packages/lib/src/usecases/actions/ports.ts +++ b/packages/lib/src/usecases/actions/ports.ts @@ -8,7 +8,7 @@ import type { CreateCommand } from "../../core/domain.js" import type { PortProbeError } from "../../shell/errors.js" import { loadReservedPorts, selectAvailablePort } from "../ports-reserve.js" -const maxPortAttempts = 25 +const maxPortAttempts = 1024 export const resolveSshPort = ( config: CreateCommand["config"], diff --git a/packages/lib/src/usecases/actions/prepare-files.ts b/packages/lib/src/usecases/actions/prepare-files.ts index ceb3594d..81a8a983 100644 --- a/packages/lib/src/usecases/actions/prepare-files.ts +++ b/packages/lib/src/usecases/actions/prepare-files.ts @@ -275,7 +275,14 @@ export const prepareProjectFiles = ( const rewriteManagedFiles = options.force || options.forceEnv const envOnlyRefresh = options.forceEnv && !options.force const createdFiles = yield* _(writeProjectFiles(resolvedOutDir, projectConfig, rewriteManagedFiles)) - yield* _(ensureAuthorizedKeys(resolvedOutDir, projectConfig.authorizedKeysPath, globalConfig.authorizedKeysPath, options.force)) + yield* _( + ensureAuthorizedKeys( + resolvedOutDir, + projectConfig.authorizedKeysPath, + globalConfig.authorizedKeysPath, + options.force + ) + ) yield* _(ensureEnvFile(resolvedOutDir, projectConfig.envGlobalPath, defaultGlobalEnvContents)) yield* _(ensureEnvFile(resolvedOutDir, projectConfig.envProjectPath, defaultProjectEnvContents, envOnlyRefresh)) yield* _(ensureCodexConfigFile(baseDir, globalConfig.codexAuthPath)) diff --git a/packages/lib/src/usecases/auth-codex.ts b/packages/lib/src/usecases/auth-codex.ts index 1183b3c2..5f52b046 100644 --- a/packages/lib/src/usecases/auth-codex.ts +++ b/packages/lib/src/usecases/auth-codex.ts @@ -187,8 +187,8 @@ export const authCodexLogin = ( runCodexLogin(cwd, accountPath).pipe( Effect.flatMap((output) => (output.length === 0 ? Effect.void : Effect.log(output))) )).pipe( - Effect.zipRight(autoSyncState(`chore(state): auth codex ${normalizeAccountLabel(command.label, "default")}`)) - ) + Effect.zipRight(autoSyncState(`chore(state): auth codex ${normalizeAccountLabel(command.label, "default")}`)) + ) // CHANGE: show Codex auth status for a given label // WHY: make it obvious whether Codex is connected diff --git a/packages/lib/src/usecases/auth-sync.ts b/packages/lib/src/usecases/auth-sync.ts index 8f4d2ccf..236a7690 100644 --- a/packages/lib/src/usecases/auth-sync.ts +++ b/packages/lib/src/usecases/auth-sync.ts @@ -168,7 +168,14 @@ export const syncAuthArtifacts = ( yield* _(copyFileIfNeeded(sourceGlobal, targetGlobal)) yield* _(syncGithubTokenKeysInFile(sourceGlobal, targetGlobal)) yield* _(copyFileIfNeeded(sourceProject, targetProject)) - yield* _(copyCodexFile(fs, path, { sourceDir: sourceCodex, targetDir: targetCodex, fileName: "auth.json", label: "auth" })) + yield* _( + copyCodexFile(fs, path, { + sourceDir: sourceCodex, + targetDir: targetCodex, + fileName: "auth.json", + label: "auth" + }) + ) if (sourceCodex !== targetCodex) { yield* _( copyCodexFile(fs, path, { diff --git a/packages/lib/src/usecases/github-token-preflight.ts b/packages/lib/src/usecases/github-token-preflight.ts index c66f54e3..988cef29 100644 --- a/packages/lib/src/usecases/github-token-preflight.ts +++ b/packages/lib/src/usecases/github-token-preflight.ts @@ -16,13 +16,11 @@ import { export { githubInvalidTokenMessage } from "./github-token-validation.js" -export const githubMissingTokenMessage = - [ - "GitHub auth is missing: no GitHub token/key was found for this repository.", - "If the repository requires access, run: docker-git auth github login --web" - ].join("\n") -export const githubRepoAccessWarning = - "Unable to validate GitHub repository access before start; continuing." +export const githubMissingTokenMessage = [ + "GitHub auth is missing: no GitHub token/key was found for this repository.", + "If the repository requires access, run: docker-git auth github login --web" +].join("\n") +export const githubRepoAccessWarning = "Unable to validate GitHub repository access before start; continuing." export type GithubRepoAccessStatus = "accessible" | "notAccessible" | "unknown" @@ -167,16 +165,15 @@ export const validateGithubCloneAuthTokenPreflight = ( return } - const token = - config.skipGithubAuth === true - ? null - : yield* _( - Effect.gen(function*(__) { - const fs = yield* __(FileSystem.FileSystem) - const envText = yield* __(readEnvText(fs, config.envGlobalPath)) - return resolveGithubCloneAuthToken(envText, config) - }) - ) + const token = config.skipGithubAuth + ? null + : yield* _( + Effect.gen(function*(__) { + const fs = yield* __(FileSystem.FileSystem) + const envText = yield* __(readEnvText(fs, config.envGlobalPath)) + return resolveGithubCloneAuthToken(envText, config) + }) + ) if (token !== null) { const validation = yield* _(validateGithubToken(token)) @@ -196,7 +193,8 @@ export const validateGithubCloneAuthTokenPreflight = ( Match.when("accessible", () => Effect.void), Match.when("notAccessible", () => Effect.fail(new AuthError({ message: githubRepoAccessMessage(config.repoUrl, token !== null) }))), - Match.when("unknown", () => Effect.logWarning(githubRepoAccessWarning)), + Match.when("unknown", () => + Effect.logWarning(githubRepoAccessWarning)), Match.exhaustive ) ) diff --git a/packages/lib/src/usecases/github-token-validation.ts b/packages/lib/src/usecases/github-token-validation.ts index 24f10e4a..b24a47cf 100644 --- a/packages/lib/src/usecases/github-token-validation.ts +++ b/packages/lib/src/usecases/github-token-validation.ts @@ -6,11 +6,10 @@ import { Effect, Either } from "effect" const githubTokenValidationUrl = "https://api.github.com/user" export const githubTokenValidationWarning = "Unable to validate GitHub token before start; continuing." -export const githubInvalidTokenMessage = - [ - "GitHub auth is invalid: the stored token is dead, revoked, expired, or malformed.", - "To restore access, run: docker-git auth github login --web" - ].join("\n") +export const githubInvalidTokenMessage = [ + "GitHub auth is invalid: the stored token is dead, revoked, expired, or malformed.", + "To restore access, run: docker-git auth github login --web" +].join("\n") type GithubUser = { readonly login: string diff --git a/packages/lib/src/usecases/projects-ssh.ts b/packages/lib/src/usecases/projects-ssh.ts index 39b5d245..b1a4acda 100644 --- a/packages/lib/src/usecases/projects-ssh.ts +++ b/packages/lib/src/usecases/projects-ssh.ts @@ -29,6 +29,15 @@ import { runDockerComposeUpWithPortCheck } from "./projects-up.js" import { buildEditorSshAccess, formatEditorSshAccessSummary } from "./ssh-access.js" import { ensureTerminalCursorVisible } from "./terminal-cursor.js" +export type PreparedProjectSsh = { + readonly item: ProjectItem + readonly cwd: string + readonly command: "ssh" + readonly args: ReadonlyArray +} + +type ProjectSshUpRequirements = CommandExecutor.CommandExecutor | FileSystem.FileSystem | Path.Path + const buildSshArgs = (item: ProjectItem): ReadonlyArray => { const host = item.ipAddress ?? "localhost" const port = item.ipAddress ? 22 : item.sshPort @@ -90,7 +99,7 @@ export const probeProjectSshReady = ( args: buildSshProbeArgs(item) }).pipe(Effect.map((exitCode) => exitCode === 0)) -const waitForSshReady = ( +export const waitForProjectSshReady = ( item: ProjectItem ): Effect.Effect => { const host = item.ipAddress ?? "localhost" @@ -117,7 +126,31 @@ const waitForSshReady = ( ) } -export const waitForProjectSshReady = waitForSshReady +export const prepareProjectSsh = (item: ProjectItem): PreparedProjectSsh => ({ + item, + cwd: process.cwd(), + command: "ssh", + args: buildSshArgs(item) +}) + +const connectPreparedProjectSsh = ( + prepared: PreparedProjectSsh +): Effect.Effect => + pipe( + ensureTerminalCursorVisible(), + Effect.zipRight( + runCommandWithExitCodes( + { + cwd: prepared.cwd, + command: prepared.command, + args: prepared.args + }, + [0, 130], + (exitCode) => new CommandFailedError({ command: prepared.command, exitCode }) + ) + ), + Effect.ensuring(ensureTerminalCursorVisible()) + ) // CHANGE: connect to a project via SSH using its resolved settings // WHY: allow TUI to open a shell immediately after selection @@ -132,21 +165,7 @@ export const waitForProjectSshReady = waitForSshReady export const connectProjectSsh = ( item: ProjectItem ): Effect.Effect => - pipe( - ensureTerminalCursorVisible(), - Effect.zipRight( - runCommandWithExitCodes( - { - cwd: process.cwd(), - command: "ssh", - args: buildSshArgs(item) - }, - [0, 130], - (exitCode) => new CommandFailedError({ command: "ssh", exitCode }) - ) - ), - Effect.ensuring(ensureTerminalCursorVisible()) - ) + connectPreparedProjectSsh(prepareProjectSsh(item)) // CHANGE: ensure docker compose is up before SSH connection // WHY: selected project should auto-start when not running @@ -167,7 +186,24 @@ export const connectProjectSshWithUp = ( | PortProbeError | DockerCommandError | PlatformError, - CommandExecutor.CommandExecutor | FileSystem.FileSystem | Path.Path + ProjectSshUpRequirements +> => + prepareProjectSshWithUp(item).pipe( + Effect.flatMap((prepared) => connectPreparedProjectSsh(prepared)) + ) + +export const prepareProjectSshWithUp = ( + item: ProjectItem +): Effect.Effect< + PreparedProjectSsh, + | CommandFailedError + | ConfigNotFoundError + | ConfigDecodeError + | FileExistsError + | PortProbeError + | DockerCommandError + | PlatformError, + ProjectSshUpRequirements > => Effect.gen(function*(_) { const fs = yield* _(FileSystem.FileSystem) @@ -193,8 +229,8 @@ export const connectProjectSshWithUp = ( ipAddress } - yield* _(waitForSshReady(updated)) - yield* _(connectProjectSsh(updated)) + yield* _(waitForProjectSshReady(updated)) + return prepareProjectSsh(updated) }) // CHANGE: show docker compose status for all known docker-git projects diff --git a/packages/lib/src/usecases/projects.ts b/packages/lib/src/usecases/projects.ts index d63a49f8..9002827a 100644 --- a/packages/lib/src/usecases/projects.ts +++ b/packages/lib/src/usecases/projects.ts @@ -15,6 +15,9 @@ export { connectProjectSsh, connectProjectSshWithUp, listProjectStatus, + type PreparedProjectSsh, + prepareProjectSsh, + prepareProjectSshWithUp, probeProjectSshReady, waitForProjectSshReady } from "./projects-ssh.js" diff --git a/packages/lib/src/usecases/scrap-session-export.ts b/packages/lib/src/usecases/scrap-session-export.ts index 89238e81..41b60d84 100644 --- a/packages/lib/src/usecases/scrap-session-export.ts +++ b/packages/lib/src/usecases/scrap-session-export.ts @@ -125,7 +125,8 @@ const detectRebuildCommands = ( const script = [ "set -e", `cd ${targetDir}`, - // Priority: pnpm > npm > yarn. Keep commands deterministic and rebuildable. + // Priority: bun > pnpm > npm > yarn. Keep commands deterministic and rebuildable. + "if [ -f bun.lock ]; then echo 'bun install --frozen-lockfile'; exit 0; fi", "if [ -f pnpm-lock.yaml ]; then echo 'pnpm install --frozen-lockfile'; exit 0; fi", "if [ -f package-lock.json ]; then echo 'npm ci'; exit 0; fi", "if [ -f yarn.lock ]; then echo 'yarn install --frozen-lockfile'; exit 0; fi", diff --git a/packages/lib/src/usecases/shared-volume-seed.ts b/packages/lib/src/usecases/shared-volume-seed.ts index e7a37630..e43d8a39 100644 --- a/packages/lib/src/usecases/shared-volume-seed.ts +++ b/packages/lib/src/usecases/shared-volume-seed.ts @@ -203,7 +203,9 @@ const copyBootstrapSnapshotAuthDirs = ( yield* _(copyLabeledCodexFiles(fs, path, sources.codexAuthSource, targets.projectCodexTarget, "auth.json")) yield* _(copyLabeledCodexFiles(fs, path, sources.codexAuthSource, targets.projectCodexTarget, "config.toml")) yield* _(copyDirRecursive(fs, path, sources.claudeAuthSource, targets.projectClaudeTarget)) - yield* _(copyCodexAuthFileIfPresent(fs, path, sources.codexSharedAuthSource, targets.sharedCodexTarget, "auth.json")) + yield* _( + copyCodexAuthFileIfPresent(fs, path, sources.codexSharedAuthSource, targets.sharedCodexTarget, "auth.json") + ) yield* _(copyLabeledCodexFiles(fs, path, sources.codexSharedAuthSource, targets.sharedCodexTarget, "auth.json")) }) diff --git a/packages/lib/tests/core/templates.test.ts b/packages/lib/tests/core/templates.test.ts index 049c189c..0aee6714 100644 --- a/packages/lib/tests/core/templates.test.ts +++ b/packages/lib/tests/core/templates.test.ts @@ -24,6 +24,12 @@ const makeTemplateConfig = (overrides: Partial = {}): TemplateCo ...overrides }) +const expectContainsAll = (value: string, snippets: ReadonlyArray): void => { + for (const snippet of snippets) { + expect(value).toContain(snippet) + } +} + describe("renderEntrypointDnsRepair", () => { it("renders the fallback nameserver repair block", () => { const dnsRepair = renderEntrypointDnsRepair() @@ -79,6 +85,121 @@ describe("renderEntrypointGitHooks", () => { }) }) +describe("renderEntrypoint auth bridge", () => { + const renderAuthEntrypoint = (): string => + renderEntrypoint( + makeTemplateConfig({ + enableMcpPlaywright: false + }) + ) + + it("renders GitHub auth bridge and credential helper wiring", () => { + const entrypoint = renderAuthEntrypoint() + + expectContainsAll(entrypoint, [ + "GIT_AUTH_TOKEN=\"${GIT_AUTH_TOKEN:-${GITHUB_TOKEN:-${GH_TOKEN:-}}}\"", + "GITHUB_TOKEN=\"${GITHUB_TOKEN:-${GH_TOKEN:-}}\"", + "GITHUB_AUTH_SKIP=\"${GITHUB_AUTH_SKIP:-0}\"", + "AUTH_LABEL_RAW=\"${GIT_AUTH_LABEL:-${GITHUB_AUTH_LABEL:-}}\"", + "LABELED_GITHUB_TOKEN_KEY=\"GITHUB_TOKEN__$RESOLVED_AUTH_LABEL\"", + "LABELED_GIT_TOKEN_KEY=\"GIT_AUTH_TOKEN__$RESOLVED_AUTH_LABEL\"", + "if [[ -n \"$EFFECTIVE_GH_TOKEN\" ]]; then", + String.raw`printf "export GITHUB_TOKEN=%q\n" "$EFFECTIVE_GITHUB_TOKEN"`, + String.raw`printf "export GH_TOKEN=%q\n" "$EFFECTIVE_GH_TOKEN"`, + String.raw`printf "export GIT_AUTH_TOKEN=%q\n" "$EFFECTIVE_GITHUB_TOKEN"`, + "docker_git_upsert_ssh_env \"GITHUB_TOKEN\" \"$EFFECTIVE_GITHUB_TOKEN\"", + "docker_git_upsert_ssh_env \"GH_TOKEN\" \"$EFFECTIVE_GH_TOKEN\"", + "docker_git_upsert_ssh_env \"GIT_AUTH_TOKEN\" \"$EFFECTIVE_GITHUB_TOKEN\"", + "GIT_CREDENTIAL_HELPER_PATH=\"/usr/local/bin/docker-git-credential-helper\"", + "token=\"${GITHUB_TOKEN:-}\"", + "token=\"${GH_TOKEN:-}\"", + String.raw`printf "%s\n" "password=$token"`, + "git config --global credential.helper" + ]) + }) + + it("renders Claude auth and wrapper bootstrap wiring", () => { + const entrypoint = renderAuthEntrypoint() + + expectContainsAll(entrypoint, [ + "CLAUDE_REAL_DIR=\"$(dirname \"$CURRENT_CLAUDE_BIN\")\"", + "CLAUDE_REAL_BIN=\"$CLAUDE_REAL_DIR/.docker-git-claude-real\"", + "CLAUDE_WRAPPER_BIN=\"/usr/local/bin/claude\"", + "cat <<'EOF' > \"$CLAUDE_WRAPPER_BIN\"", + "CLAUDE_REAL_BIN=\"__CLAUDE_REAL_BIN__\"", + "sed -i \"s#__CLAUDE_REAL_BIN__#$CLAUDE_REAL_BIN#g\" \"$CLAUDE_WRAPPER_BIN\" || true", + "CLAUDE_CONFIG_DIR=\"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"", + "docker_git_ensure_claude_cli()", + "claude cli.js not found under npm global root; skip shim restore", + "CLAUDE_PERMISSION_SETTINGS_FILE=\"$CLAUDE_CONFIG_DIR/settings.json\"", + "docker_git_sync_claude_permissions()", + "const currentPermissions = isRecord(settings.permissions) ? settings.permissions : {}", + "defaultMode: \"bypassPermissions\"", + "CLAUDE_TOKEN_FILE=\"$CLAUDE_CONFIG_DIR/.oauth-token\"", + "CLAUDE_CREDENTIALS_FILE=\"$CLAUDE_CONFIG_DIR/.credentials.json\"", + "CLAUDE_NESTED_CREDENTIALS_FILE=\"$CLAUDE_CONFIG_DIR/.claude/.credentials.json\"", + "docker_git_prepare_claude_auth_mode()", + "if [[ ! -s \"$CLAUDE_TOKEN_FILE\" ]]; then", + "CLAUDE_SETTINGS_FILE=\"${CLAUDE_HOME_JSON:-$CLAUDE_CONFIG_DIR/.claude.json}\"", + "CLAUDE_ROOT_TOKEN_FILE=\"$CLAUDE_AUTH_ROOT/.oauth-token\"", + "CLAUDE_ROOT_CONFIG_FILE=\"$CLAUDE_AUTH_ROOT/.config.json\"", + "CLAUDE_HOME_DIR=\"/home/dev/.claude\"", + "CLAUDE_HOME_JSON=\"/home/dev/.claude.json\"", + "docker_git_link_claude_home_file()", + "docker_git_link_claude_home_file \".oauth-token\"", + "docker_git_link_claude_home_file \".config.json\"", + "docker_git_link_claude_home_file \".claude.json\"", + "docker_git_link_claude_home_file \".credentials.json\"" + ]) + }) + + it("renders Codex and Gemini project rules wiring", () => { + const entrypoint = renderAuthEntrypoint() + + expectContainsAll(entrypoint, [ + "nextServers.playwright = {", + "command: \"docker-git-playwright-mcp\"", + "docker_git_sync_project_codex_skills()", + "project_skills_root=\"$codex_home/skills/.docker-git-project\"", + "docker_git_prepare_active_agent_project_rules()", + "docker_git_detect_claude_project_rules()", + "docker_git_detect_gemini_project_rules()", + "\"codex\")", + "\"claude\")", + "\"gemini\")", + "\"20-agents-skills::.agents/skills\"", + "\"30-agents-dot-skills::.agents/.skills\"", + "\"80-codex-skills::.codex/skills\"", + "\"90-codex-dot-skills::.codex/.skills\"", + "$project_dir/.claude/settings.json", + "$project_dir/.claude/agents", + "$project_dir/.gemini/settings.json", + "$project_dir/.gemini/commands", + "$project_dir/.gemini/skills", + "codex exec" + ]) + expect(entrypoint).not.toContain("codex --approval-mode full-auto") + expect(entrypoint).not.toContain("\"40-claude-skills::.claude/skills\"") + }) + + it("renders agent prompt glue and repeated subagent notice", () => { + const entrypoint = renderAuthEntrypoint() + + expectContainsAll(entrypoint, [ + "su - dev -s /bin/bash -c \"bash -lc", + ". /etc/profile 2>/dev/null || true;", + String.raw`. \"$AGENT_ENV_FILE\" 2>/dev/null || true;`, + "AGENT_PROMPT_FILE=\"/run/docker-git/agent-prompt.txt\"", + "claude --dangerously-skip-permissions -p", + "CLAUDE_GLOBAL_PROMPT_FILE=\"/home/dev/.claude/CLAUDE.md\"", + "CLAUDE_AUTO_SYSTEM_PROMPT=\"${CLAUDE_AUTO_SYSTEM_PROMPT:-1}\"", + "docker-git-managed:claude-md", + "SUBAGENTS_LINE=\"Для решения задач обязательно используй subagents. Сам агент обязан выполнять финальную проверку, интеграцию и валидацию результата перед ответом пользователю.\"" + ]) + expect(entrypoint.split("Для решения задач обязательно используй subagents.").length - 1).toBeGreaterThanOrEqual(2) + }) +}) + describe("renderDockerCompose", () => { it("pins the compose project name to the managed service name", () => { const compose = renderDockerCompose( diff --git a/packages/lib/tests/usecases/agent-auto-select.test.ts b/packages/lib/tests/usecases/agent-auto-select.test.ts index 1d2ac420..edfe3e80 100644 --- a/packages/lib/tests/usecases/agent-auto-select.test.ts +++ b/packages/lib/tests/usecases/agent-auto-select.test.ts @@ -42,7 +42,7 @@ const makeConfig = (root: string, path: Path.Path): TemplateConfig => ({ dockerNetworkMode: "shared", dockerSharedNetworkName: "docker-git-shared", enableMcpPlaywright: false, - pnpmVersion: "10.27.0", + bunVersion: "1.3.11", agentAuto: true }) diff --git a/packages/lib/tests/usecases/apply.test.ts b/packages/lib/tests/usecases/apply.test.ts index b728af99..09320f7e 100644 --- a/packages/lib/tests/usecases/apply.test.ts +++ b/packages/lib/tests/usecases/apply.test.ts @@ -49,7 +49,7 @@ const makeTemplateConfig = ( dockerNetworkMode: "shared", dockerSharedNetworkName: "docker-git-shared", enableMcpPlaywright: false, - pnpmVersion: "10.27.0" + bunVersion: "1.3.11" }) const isRecord = (value: unknown): value is Record => @@ -68,6 +68,32 @@ const rewriteTargetDirInConfig = (source: string, targetDir: string): string => return `${JSON.stringify(next, null, 2)}\n` } +const rewriteLegacyVersionKeyInConfig = (source: string): string => { + const parsed: unknown = JSON.parse(source) + if (!isRecord(parsed)) { + throw new Error("invalid docker-git.json root") + } + const template = parsed["template"] + if (!isRecord(template)) { + throw new Error("invalid docker-git.json template") + } + + const bunVersion = template["bunVersion"] + if (typeof bunVersion !== "string") { + throw new Error("invalid docker-git.json bunVersion") + } + + const { bunVersion: _removed, ...legacyTemplate } = template + const next = { + ...parsed, + template: { + ...legacyTemplate, + pnpmVersion: bunVersion + } + } + return `${JSON.stringify(next, null, 2)}\n` +} + type ProcessPatch = { readonly prevCwd: string readonly prevProjectsRoot: string | undefined @@ -224,6 +250,36 @@ describe("applyProjectFiles", () => { expect(configAfter).toContain('"ramLimit": "4g"') }) ).pipe(Effect.provide(NodeContext.layer))) + + it.effect("reads legacy docker-git.json with pnpmVersion and rewrites it to bunVersion", () => + withTempDir((root) => + Effect.gen(function*(_) { + const fs = yield* _(FileSystem.FileSystem) + const path = yield* _(Path.Path) + const outDir = path.join(root, "project") + const targetDir = "/home/dev/workspaces/org/repo" + const globalConfig = makeTemplateConfig(root, outDir, path, targetDir) + const projectConfig = makeTemplateConfig(root, outDir, path, targetDir) + + yield* _( + prepareProjectFiles(outDir, root, globalConfig, projectConfig, { + force: false, + forceEnv: false + }) + ) + + const configPath = path.join(outDir, "docker-git.json") + const configBefore = yield* _(fs.readFileString(configPath)) + yield* _(fs.writeFileString(configPath, rewriteLegacyVersionKeyInConfig(configBefore))) + + const appliedTemplate = yield* _(applyProjectFiles(outDir)) + expect(appliedTemplate.bunVersion).toBe("1.3.11") + + const configAfter = yield* _(fs.readFileString(configPath)) + expect(configAfter).toContain('"bunVersion": "1.3.11"') + expect(configAfter).not.toContain('"pnpmVersion"') + }) + ).pipe(Effect.provide(NodeContext.layer))) }) describe("applyProjectConfig", () => { diff --git a/packages/lib/tests/usecases/create-project-docker-identities.test.ts b/packages/lib/tests/usecases/create-project-docker-identities.test.ts index 99f795f1..9dfcbfc6 100644 --- a/packages/lib/tests/usecases/create-project-docker-identities.test.ts +++ b/packages/lib/tests/usecases/create-project-docker-identities.test.ts @@ -21,6 +21,7 @@ vi.mock("../../src/usecases/actions/ports.js", () => ({ type RecordedCommand = { readonly command: string readonly args: ReadonlyArray + readonly cwd?: string | undefined } const withTempDir = ( @@ -81,7 +82,11 @@ const makeFakeExecutor = (recorded: Array): CommandExecutor.Com Effect.gen(function*(_) { const flattened = Command.flatten(command) for (const entry of flattened) { - recorded.push({ command: entry.command, args: entry.args }) + recorded.push({ + command: entry.command, + args: entry.args, + cwd: entry.cwd._tag === "Some" ? entry.cwd.value : undefined + }) } const invocation = flattened[flattened.length - 1]! @@ -117,7 +122,8 @@ const makeFakeExecutor = (recorded: Array): CommandExecutor.Com const makeTemplate = ( root: string, repoUrl: string, - path: Path.Path + path: Path.Path, + overrides: Partial = {} ): TemplateConfig => ({ containerName: "dg-openclaw_autodeployer", serviceName: "dg-openclaw_autodeployer", @@ -140,7 +146,8 @@ const makeTemplate = ( dockerNetworkMode: "shared", dockerSharedNetworkName: "docker-git-shared", enableMcpPlaywright: true, - pnpmVersion: "10.27.0" + pnpmVersion: "10.27.0", + ...overrides }) const makeCommand = ( @@ -148,10 +155,11 @@ const makeCommand = ( outDir: string, repoUrl: string, path: Path.Path, - force: boolean + force: boolean, + overrides: Partial = {} ): CreateCommand => ({ _tag: "Create", - config: makeTemplate(root, repoUrl, path), + config: makeTemplate(root, repoUrl, path, overrides), outDir, runUp: false, openSsh: false, @@ -160,6 +168,20 @@ const makeCommand = ( waitForClone: false }) +const expectedConflicts = (projectDir: string): ReadonlyArray<{ readonly conflictingProjectDir: string, readonly kind: string, readonly name: string }> => [{ conflictingProjectDir: projectDir, kind: "containerName", name: "dg-openclaw_autodeployer" }, { conflictingProjectDir: projectDir, kind: "serviceName", name: "dg-openclaw_autodeployer" }, { conflictingProjectDir: projectDir, kind: "volumeName", name: "dg-openclaw_autodeployer-home" }, { conflictingProjectDir: projectDir, kind: "bootstrapVolumeName", name: "dg-openclaw_autodeployer-home-bootstrap" }] + +const isComposeDownVolumes = (invocation: RecordedCommand, cwd: string): boolean => + invocation.command === "docker" && + invocation.cwd === cwd && + invocation.args[0] === "compose" && + invocation.args[1] === "--ansi" && + invocation.args[2] === "never" && + invocation.args[3] === "--progress" && + invocation.args[4] === "plain" && + invocation.args[5] === "down" && + invocation.args[6] === "-v" && + invocation.args[7] === "--remove-orphans" + const runCreate = ( cwd: string, projectsRoot: string, @@ -192,7 +214,14 @@ describe("createProject docker identity invariants", () => { runCreate( root, projectsRoot, - makeCommand(root, firstOutDir, "https://git.example.test/test-owner-a/openclaw_autodeployer.git", path, false), + makeCommand( + root, + firstOutDir, + "https://git.example.test/test-owner-a/openclaw_autodeployer.git", + path, + false, + { enableMcpPlaywright: false } + ), executor ) ) @@ -206,7 +235,8 @@ describe("createProject docker identity invariants", () => { secondOutDir, "https://git.example.test/test-owner-b/openclaw_autodeployer.git", path, - false + false, + { enableMcpPlaywright: false } ), executor ).pipe(Effect.flip) @@ -214,8 +244,8 @@ describe("createProject docker identity invariants", () => { expect(error).toBeInstanceOf(DockerIdentityConflictError) if (error instanceof DockerIdentityConflictError) { - expect(error.conflicts.map((conflict) => conflict.name)).toContain("dg-openclaw_autodeployer") - expect(error.conflicts.map((conflict) => conflict.conflictingProjectDir)).toContain(firstOutDir) + expect(error.projectDir).toBe(secondOutDir) + expect(error.conflicts).toEqual(expectedConflicts(firstOutDir)) } expect(yield* _(fs.exists(secondOutDir))).toBe(false) @@ -237,7 +267,14 @@ describe("createProject docker identity invariants", () => { runCreate( root, projectsRoot, - makeCommand(root, firstOutDir, "https://git.example.test/test-owner-a/openclaw_autodeployer.git", path, false), + makeCommand( + root, + firstOutDir, + "https://git.example.test/test-owner-a/openclaw_autodeployer.git", + path, + false, + { enableMcpPlaywright: false } + ), executor ) ) @@ -251,12 +288,14 @@ describe("createProject docker identity invariants", () => { secondOutDir, "https://git.example.test/test-owner-b/openclaw_autodeployer.git", path, - true + true, + { enableMcpPlaywright: false } ), executor ) ) + expect(recorded.some((invocation) => isComposeDownVolumes(invocation, firstOutDir))).toBe(true) expect(yield* _(fs.exists(firstOutDir))).toBe(false) expect(yield* _(fs.exists(secondOutDir))).toBe(true) }) @@ -276,7 +315,14 @@ describe("createProject docker identity invariants", () => { runCreate( root, projectsRoot, - makeCommand(root, outDir, "https://git.example.test/test-owner-a/openclaw_autodeployer.git", path, false), + makeCommand( + root, + outDir, + "https://git.example.test/test-owner-a/openclaw_autodeployer.git", + path, + false, + { enableMcpPlaywright: false } + ), executor ) ) @@ -285,11 +331,19 @@ describe("createProject docker identity invariants", () => { runCreate( root, projectsRoot, - makeCommand(root, outDir, "https://git.example.test/test-owner-a/openclaw_autodeployer.git", path, true), + makeCommand( + root, + outDir, + "https://git.example.test/test-owner-a/openclaw_autodeployer.git", + path, + true, + { enableMcpPlaywright: false } + ), executor ) ) + expect(recorded.some((invocation) => isComposeDownVolumes(invocation, outDir))).toBe(false) expect(yield* _(fs.exists(path.join(outDir, "docker-git.json")))).toBe(true) }) ).pipe(Effect.provide(NodeContext.layer))) diff --git a/packages/lib/tests/usecases/create-project-open-ssh.test.ts b/packages/lib/tests/usecases/create-project-open-ssh.test.ts index 8b47bc4c..e2d6b14a 100644 --- a/packages/lib/tests/usecases/create-project-open-ssh.test.ts +++ b/packages/lib/tests/usecases/create-project-open-ssh.test.ts @@ -190,7 +190,7 @@ const makeCommand = (root: string, outDir: string, path: Path.Path): CreateComma dockerNetworkMode: "shared", dockerSharedNetworkName: "docker-git-shared", enableMcpPlaywright: false, - pnpmVersion: "10.27.0" + bunVersion: "1.3.11" } return { diff --git a/packages/lib/tests/usecases/create-project-state-sync-order.test.ts b/packages/lib/tests/usecases/create-project-state-sync-order.test.ts index f1d121b5..2e8317f6 100644 --- a/packages/lib/tests/usecases/create-project-state-sync-order.test.ts +++ b/packages/lib/tests/usecases/create-project-state-sync-order.test.ts @@ -119,7 +119,7 @@ const makeCommand = (root: string, outDir: string, path: Path.Path): CreateComma dockerNetworkMode: "shared", dockerSharedNetworkName: "docker-git-shared", enableMcpPlaywright: false, - pnpmVersion: "10.27.0" + bunVersion: "1.3.11" } return { diff --git a/packages/lib/tests/usecases/docker-up-force.test.ts b/packages/lib/tests/usecases/docker-up-force.test.ts index f82e7fd1..46a8718c 100644 --- a/packages/lib/tests/usecases/docker-up-force.test.ts +++ b/packages/lib/tests/usecases/docker-up-force.test.ts @@ -115,7 +115,7 @@ describe("runDockerUpIfNeeded with force", () => { dockerNetworkMode: "project", dockerSharedNetworkName: "docker-git-shared", enableMcpPlaywright: true, - pnpmVersion: "10.27.0", + bunVersion: "1.3.11", agentMode: undefined, agentAuto: false, clonedOnHostname: undefined, diff --git a/packages/lib/tests/usecases/github-token-preflight.test.ts b/packages/lib/tests/usecases/github-token-preflight.test.ts index 27c41f3f..8eeb0db6 100644 --- a/packages/lib/tests/usecases/github-token-preflight.test.ts +++ b/packages/lib/tests/usecases/github-token-preflight.test.ts @@ -74,7 +74,7 @@ const makeCommand = (root: string, outDir: string, path: Path.Path): CreateComma dockerNetworkMode: "shared", dockerSharedNetworkName: "docker-git-shared", enableMcpPlaywright: false, - pnpmVersion: "10.27.0" + bunVersion: "1.3.11" } return { diff --git a/packages/lib/tests/usecases/mcp-playwright.test.ts b/packages/lib/tests/usecases/mcp-playwright.test.ts index a0d2f3a3..4e71b16b 100644 --- a/packages/lib/tests/usecases/mcp-playwright.test.ts +++ b/packages/lib/tests/usecases/mcp-playwright.test.ts @@ -43,7 +43,7 @@ const makeGlobalConfig = (root: string, path: Path.Path): TemplateConfig => ({ dockerNetworkMode: "shared", dockerSharedNetworkName: "docker-git-shared", enableMcpPlaywright: false, - pnpmVersion: "10.27.0" + bunVersion: "1.3.11" }) const makeProjectConfig = ( @@ -70,7 +70,7 @@ const makeProjectConfig = ( dockerNetworkMode: "shared", dockerSharedNetworkName: "docker-git-shared", enableMcpPlaywright, - pnpmVersion: "10.27.0" + bunVersion: "1.3.11" }) const isRecord = (value: unknown): value is Record => diff --git a/packages/lib/tests/usecases/prepare-files.test.ts b/packages/lib/tests/usecases/prepare-files.test.ts index c9d42d8c..c228f911 100644 --- a/packages/lib/tests/usecases/prepare-files.test.ts +++ b/packages/lib/tests/usecases/prepare-files.test.ts @@ -53,6 +53,23 @@ const withPatchedEnv = ( }) ) +const withWorkingDirectory = ( + cwd: string, + effect: Effect.Effect +): Effect.Effect => + Effect.acquireUseRelease( + Effect.sync(() => { + const previous = process.cwd() + process.chdir(cwd) + return previous + }), + () => effect, + (previous) => + Effect.sync(() => { + process.chdir(previous) + }) + ) + const failOnCopyFile = ( fs: FileSystem.FileSystem, label: string @@ -82,7 +99,7 @@ const makeGlobalConfig = (root: string, path: Path.Path): TemplateConfig => ({ dockerNetworkMode: "shared", dockerSharedNetworkName: "docker-git-shared", enableMcpPlaywright: false, - pnpmVersion: "10.27.0" + bunVersion: "1.3.11" }) const makeProjectConfig = ( @@ -115,7 +132,7 @@ const makeProjectConfig = ( dockerNetworkMode: "shared", dockerSharedNetworkName: "docker-git-shared", enableMcpPlaywright, - pnpmVersion: "10.27.0" + bunVersion: "1.3.11" }) const isRecord = (value: unknown): value is Record => @@ -302,6 +319,36 @@ describe("prepareProjectFiles", () => { }) ).pipe(Effect.provide(NodeContext.layer))) + it.effect("copies docker-git scripts from the workspace root when cwd is a nested package", () => + withTempDir((root) => + Effect.gen(function*(_) { + const fs = yield* _(FileSystem.FileSystem) + const path = yield* _(Path.Path) + const packageDir = path.join(root, "packages", "api") + const scriptsDir = path.join(root, "scripts") + const outDir = path.join(root, "project-with-scripts") + const globalConfig = makeGlobalConfig(root, path) + const projectConfig = makeProjectConfig(outDir, false, path) + + yield* _(fs.makeDirectory(packageDir, { recursive: true })) + yield* _(fs.makeDirectory(scriptsDir, { recursive: true })) + yield* _(fs.writeFileString(path.join(root, "bunfig.toml"), "[install]\nlinkWorkspacePackages = true\n")) + yield* _(fs.writeFileString(path.join(scriptsDir, "session-backup-gist.js"), "#!/usr/bin/env bun\n")) + + yield* _( + withWorkingDirectory( + packageDir, + prepareProjectFiles(outDir, packageDir, globalConfig, projectConfig, { + force: false, + forceEnv: false + }) + ) + ) + + expect(yield* _(fs.exists(path.join(outDir, "scripts", "session-backup-gist.js")))).toBe(true) + }) + ).pipe(Effect.provide(NodeContext.layer))) + it.effect("appends the active public key to the managed authorized_keys file", () => withTempDir((root) => Effect.gen(function*(_) { diff --git a/packages/lib/tests/usecases/projects-up.test.ts b/packages/lib/tests/usecases/projects-up.test.ts index 1a2cfb7b..470ce0cf 100644 --- a/packages/lib/tests/usecases/projects-up.test.ts +++ b/packages/lib/tests/usecases/projects-up.test.ts @@ -145,7 +145,7 @@ const makeTemplateConfig = ( dockerNetworkMode: "project", dockerSharedNetworkName: "docker-git-shared", enableMcpPlaywright: false, - pnpmVersion: "10.27.0" + bunVersion: "1.3.11" }) const isRecord = (value: unknown): value is Record => diff --git a/packages/lib/vite.config.ts b/packages/lib/vite.config.ts index cfd0bb55..091bc027 100644 --- a/packages/lib/vite.config.ts +++ b/packages/lib/vite.config.ts @@ -1,13 +1,11 @@ import path from "node:path" import { fileURLToPath } from "node:url" import { defineConfig } from "vite" -import tsconfigPaths from "vite-tsconfig-paths" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) export default defineConfig({ - plugins: [tsconfigPaths()], publicDir: false, resolve: { alias: { diff --git a/packages/lib/vitest.config.ts b/packages/lib/vitest.config.ts index 319bffbb..33f50647 100644 --- a/packages/lib/vitest.config.ts +++ b/packages/lib/vitest.config.ts @@ -9,14 +9,12 @@ import path from "node:path" import { fileURLToPath } from "node:url" -import tsconfigPaths from "vite-tsconfig-paths" import { defineConfig } from "vitest/config" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) export default defineConfig({ - plugins: [tsconfigPaths()], // Resolves @/* paths from tsconfig test: { // CHANGE: Native ESM support without experimental flags // WHY: Vitest designed for ESM, no need for --experimental-vm-modules diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index f8eeb226..00000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,7871 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - devDependencies: - '@changesets/changelog-github': - specifier: ^0.6.0 - version: 0.6.0 - '@changesets/cli': - specifier: ^2.30.0 - version: 2.30.0(@types/node@24.12.0) - - packages/api: - dependencies: - '@effect-template/lib': - specifier: workspace:* - version: link:../lib - '@effect/platform': - specifier: ^0.96.0 - version: 0.96.0(effect@3.21.0) - '@effect/platform-node': - specifier: ^0.106.0 - version: 0.106.0(@effect/cluster@0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - '@effect/schema': - specifier: ^0.75.5 - version: 0.75.5(effect@3.21.0) - effect: - specifier: ^3.21.0 - version: 3.21.0 - devDependencies: - '@effect/vitest': - specifier: ^0.29.0 - version: 0.29.0(effect@3.21.0)(vitest@4.1.0(@types/node@24.12.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))) - '@eslint/js': - specifier: 10.0.1 - version: 10.0.1(eslint@10.1.0(jiti@2.6.1)) - '@types/node': - specifier: ^24.12.0 - version: 24.12.0 - '@typescript-eslint/eslint-plugin': - specifier: ^8.57.1 - version: 8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^8.57.1 - version: 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint: - specifier: ^10.1.0 - version: 10.1.0(jiti@2.6.1) - globals: - specifier: ^17.4.0 - version: 17.4.0 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - vitest: - specifier: ^4.1.0 - version: 4.1.0(@types/node@24.12.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) - - packages/app: - dependencies: - '@effect/cli': - specifier: ^0.75.0 - version: 0.75.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/printer-ansi@0.49.0(@effect/typeclass@0.40.0(effect@3.21.0))(effect@3.21.0))(@effect/printer@0.49.0(@effect/typeclass@0.40.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - '@effect/cluster': - specifier: ^0.58.0 - version: 0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - '@effect/experimental': - specifier: ^0.60.0 - version: 0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0) - '@effect/platform': - specifier: ^0.96.0 - version: 0.96.0(effect@3.21.0) - '@effect/platform-node': - specifier: ^0.106.0 - version: 0.106.0(@effect/cluster@0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - '@effect/printer': - specifier: ^0.49.0 - version: 0.49.0(@effect/typeclass@0.40.0(effect@3.21.0))(effect@3.21.0) - '@effect/printer-ansi': - specifier: ^0.49.0 - version: 0.49.0(@effect/typeclass@0.40.0(effect@3.21.0))(effect@3.21.0) - '@effect/rpc': - specifier: ^0.75.0 - version: 0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0) - '@effect/schema': - specifier: ^0.75.5 - version: 0.75.5(effect@3.21.0) - '@effect/sql': - specifier: ^0.51.0 - version: 0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0) - '@effect/typeclass': - specifier: ^0.40.0 - version: 0.40.0(effect@3.21.0) - '@effect/workflow': - specifier: ^0.18.0 - version: 0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - effect: - specifier: ^3.21.0 - version: 3.21.0 - ink: - specifier: ^6.8.0 - version: 6.8.0(@types/react@19.2.14)(react@19.2.4) - react: - specifier: ^19.2.4 - version: 19.2.4 - react-reconciler: - specifier: ^0.33.0 - version: 0.33.0(react@19.2.4) - ts-morph: - specifier: ^27.0.2 - version: 27.0.2 - devDependencies: - '@biomejs/biome': - specifier: ^2.4.8 - version: 2.4.8 - '@effect-template/lib': - specifier: workspace:* - version: link:../lib - '@effect/eslint-plugin': - specifier: ^0.3.2 - version: 0.3.2 - '@effect/language-service': - specifier: latest - version: 0.81.0 - '@effect/vitest': - specifier: ^0.29.0 - version: 0.29.0(effect@3.21.0)(vitest@4.1.0(@types/node@24.12.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))) - '@eslint-community/eslint-plugin-eslint-comments': - specifier: ^4.7.1 - version: 4.7.1(eslint@10.1.0(jiti@2.6.1)) - '@eslint/compat': - specifier: 2.0.3 - version: 2.0.3(eslint@10.1.0(jiti@2.6.1)) - '@eslint/eslintrc': - specifier: 3.3.5 - version: 3.3.5 - '@eslint/js': - specifier: 10.0.1 - version: 10.0.1(eslint@10.1.0(jiti@2.6.1)) - '@prover-coder-ai/eslint-plugin-suggest-members': - specifier: ^0.0.25 - version: 0.0.25(@effect/cluster@0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@ton-ai-core/vibecode-linter': - specifier: ^1.0.11 - version: 1.0.11 - '@types/node': - specifier: ^24.12.0 - version: 24.12.0 - '@types/react': - specifier: ^19.2.14 - version: 19.2.14 - '@typescript-eslint/eslint-plugin': - specifier: ^8.57.1 - version: 8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^8.57.1 - version: 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@vitest/coverage-v8': - specifier: ^4.1.0 - version: 4.1.0(vitest@4.1.0(@types/node@24.12.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))) - '@vitest/eslint-plugin': - specifier: ^1.6.13 - version: 1.6.13(@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.0(@types/node@24.12.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))) - biome: - specifier: npm:@biomejs/biome@^2.4.8 - version: '@biomejs/biome@2.4.8' - eslint: - specifier: ^10.1.0 - version: 10.1.0(jiti@2.6.1) - eslint-import-resolver-typescript: - specifier: ^4.4.4 - version: 4.4.4(eslint-plugin-import@2.32.0)(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-codegen: - specifier: 0.34.1 - version: 0.34.1(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-import: - specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-simple-import-sort: - specifier: ^12.1.1 - version: 12.1.1(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-sonarjs: - specifier: ^4.0.2 - version: 4.0.2(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-sort-destructure-keys: - specifier: ^3.0.0 - version: 3.0.0(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-unicorn: - specifier: ^63.0.0 - version: 63.0.0(eslint@10.1.0(jiti@2.6.1)) - globals: - specifier: ^17.4.0 - version: 17.4.0 - jscpd: - specifier: ^4.0.8 - version: 4.0.8 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - typescript-eslint: - specifier: ^8.57.1 - version: 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - vite: - specifier: ^8.0.1 - version: 8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) - vite-tsconfig-paths: - specifier: ^6.1.1 - version: 6.1.1(typescript@5.9.3)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) - vitest: - specifier: ^4.1.0 - version: 4.1.0(@types/node@24.12.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) - - packages/lib: - dependencies: - '@effect/cli': - specifier: ^0.75.0 - version: 0.75.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/printer-ansi@0.49.0(@effect/typeclass@0.40.0(effect@3.21.0))(effect@3.21.0))(@effect/printer@0.49.0(@effect/typeclass@0.40.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - '@effect/cluster': - specifier: ^0.58.0 - version: 0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - '@effect/experimental': - specifier: ^0.60.0 - version: 0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0) - '@effect/platform': - specifier: ^0.96.0 - version: 0.96.0(effect@3.21.0) - '@effect/platform-node': - specifier: ^0.106.0 - version: 0.106.0(@effect/cluster@0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - '@effect/printer': - specifier: ^0.49.0 - version: 0.49.0(@effect/typeclass@0.40.0(effect@3.21.0))(effect@3.21.0) - '@effect/printer-ansi': - specifier: ^0.49.0 - version: 0.49.0(@effect/typeclass@0.40.0(effect@3.21.0))(effect@3.21.0) - '@effect/rpc': - specifier: ^0.75.0 - version: 0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0) - '@effect/schema': - specifier: ^0.75.5 - version: 0.75.5(effect@3.21.0) - '@effect/sql': - specifier: ^0.51.0 - version: 0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0) - '@effect/typeclass': - specifier: ^0.40.0 - version: 0.40.0(effect@3.21.0) - '@effect/workflow': - specifier: ^0.18.0 - version: 0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - effect: - specifier: ^3.21.0 - version: 3.21.0 - ts-morph: - specifier: ^27.0.2 - version: 27.0.2 - devDependencies: - '@biomejs/biome': - specifier: ^2.4.8 - version: 2.4.8 - '@effect/eslint-plugin': - specifier: ^0.3.2 - version: 0.3.2 - '@effect/language-service': - specifier: latest - version: 0.81.0 - '@effect/vitest': - specifier: ^0.29.0 - version: 0.29.0(effect@3.21.0)(vitest@4.1.0(@types/node@24.12.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))) - '@eslint-community/eslint-plugin-eslint-comments': - specifier: ^4.7.1 - version: 4.7.1(eslint@10.1.0(jiti@2.6.1)) - '@eslint/compat': - specifier: 2.0.3 - version: 2.0.3(eslint@10.1.0(jiti@2.6.1)) - '@eslint/eslintrc': - specifier: 3.3.5 - version: 3.3.5 - '@eslint/js': - specifier: 10.0.1 - version: 10.0.1(eslint@10.1.0(jiti@2.6.1)) - '@prover-coder-ai/eslint-plugin-suggest-members': - specifier: ^0.0.25 - version: 0.0.25(@effect/cluster@0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@ton-ai-core/vibecode-linter': - specifier: ^1.0.11 - version: 1.0.11 - '@types/node': - specifier: ^24.12.0 - version: 24.12.0 - '@typescript-eslint/eslint-plugin': - specifier: ^8.57.1 - version: 8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^8.57.1 - version: 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@vitest/coverage-v8': - specifier: ^4.1.0 - version: 4.1.0(vitest@4.1.0(@types/node@24.12.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))) - '@vitest/eslint-plugin': - specifier: ^1.6.13 - version: 1.6.13(@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.0(@types/node@24.12.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))) - eslint: - specifier: ^10.1.0 - version: 10.1.0(jiti@2.6.1) - eslint-import-resolver-typescript: - specifier: ^4.4.4 - version: 4.4.4(eslint-plugin-import@2.32.0)(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-codegen: - specifier: 0.34.1 - version: 0.34.1(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-import: - specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-simple-import-sort: - specifier: ^12.1.1 - version: 12.1.1(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-sonarjs: - specifier: ^4.0.2 - version: 4.0.2(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-sort-destructure-keys: - specifier: ^3.0.0 - version: 3.0.0(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-unicorn: - specifier: ^63.0.0 - version: 63.0.0(eslint@10.1.0(jiti@2.6.1)) - globals: - specifier: ^17.4.0 - version: 17.4.0 - jscpd: - specifier: ^4.0.8 - version: 4.0.8 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - typescript-eslint: - specifier: ^8.57.1 - version: 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - vite: - specifier: ^8.0.1 - version: 8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) - vite-tsconfig-paths: - specifier: ^6.1.1 - version: 6.1.1(typescript@5.9.3)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) - vitest: - specifier: ^4.1.0 - version: 4.1.0(@types/node@24.12.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) - -packages: - - '@alcalzone/ansi-tokenize@0.2.5': - resolution: {integrity: sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==} - engines: {node: '>=18'} - - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.28.5': - resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.28.5': - resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.28.5': - resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.27.2': - resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.27.1': - resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.28.3': - resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.28.4': - resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.28.5': - resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/parser@7.29.0': - resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/runtime@7.28.4': - resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} - engines: {node: '>=6.9.0'} - - '@babel/template@7.27.2': - resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.28.5': - resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.28.5': - resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} - engines: {node: '>=6.9.0'} - - '@bcoe/v8-coverage@1.0.2': - resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} - engines: {node: '>=18'} - - '@biomejs/biome@2.4.8': - resolution: {integrity: sha512-ponn0oKOky1oRXBV+rlSaUlixUxf1aZvWC19Z41zBfUOUesthrQqL3OtiAlSB1EjFjyWpn98Q64DHelhA6jNlA==} - engines: {node: '>=14.21.3'} - hasBin: true - - '@biomejs/cli-darwin-arm64@2.4.8': - resolution: {integrity: sha512-ARx0tECE8I7S2C2yjnWYLNbBdDoPdq3oyNLhMglmuctThwUsuzFWRKrHmIGwIRWKz0Mat9DuzLEDp52hGnrxGQ==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [darwin] - - '@biomejs/cli-darwin-x64@2.4.8': - resolution: {integrity: sha512-Jg9/PsB9vDCJlANE8uhG7qDhb5w0Ix69D7XIIc8IfZPUoiPrbLm33k2Ig3NOJ/7nb3UbesFz3D1aDKm9DvzjhQ==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [darwin] - - '@biomejs/cli-linux-arm64-musl@2.4.8': - resolution: {integrity: sha512-Zo9OhBQDJ3IBGPlqHiTISloo5H0+FBIpemqIJdW/0edJ+gEcLR+MZeZozcUyz3o1nXkVA7++DdRKQT0599j9jA==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@biomejs/cli-linux-arm64@2.4.8': - resolution: {integrity: sha512-5CdrsJct76XG2hpKFwXnEtlT1p+4g4yV+XvvwBpzKsTNLO9c6iLlAxwcae2BJ7ekPGWjNGw9j09T5KGPKKxQig==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@biomejs/cli-linux-x64-musl@2.4.8': - resolution: {integrity: sha512-Gi8quv8MEuDdKaPFtS2XjEnMqODPsRg6POT6KhoP+VrkNb+T2ywunVB+TvOU0LX1jAZzfBr+3V1mIbBhzAMKvw==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@biomejs/cli-linux-x64@2.4.8': - resolution: {integrity: sha512-PdKXspVEaMCQLjtZCn6vfSck/li4KX9KGwSDbZdgIqlrizJ2MnMcE3TvHa2tVfXNmbjMikzcfJpuPWH695yJrw==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@biomejs/cli-win32-arm64@2.4.8': - resolution: {integrity: sha512-LoFatS0tnHv6KkCVpIy3qZCih+MxUMvdYiPWLHRri7mhi2vyOOs8OrbZBcLTUEWCS+ktO72nZMy4F96oMhkOHQ==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [win32] - - '@biomejs/cli-win32-x64@2.4.8': - resolution: {integrity: sha512-vAn7iXDoUbqFXqVocuq1sMYAd33p8+mmurqJkWl6CtIhobd/O6moe4rY5AJvzbunn/qZCdiDVcveqtkFh1e7Hg==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [win32] - - '@changesets/apply-release-plan@7.1.0': - resolution: {integrity: sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ==} - - '@changesets/assemble-release-plan@6.0.9': - resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} - - '@changesets/changelog-git@0.2.1': - resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} - - '@changesets/changelog-github@0.6.0': - resolution: {integrity: sha512-wA2/y4hR/A1K411cCT75rz0d46Iezxp1WYRFoFJDIUpkQ6oDBAIUiU7BZkDCmYgz0NBl94X1lgcZO+mHoiHnFg==} - - '@changesets/cli@2.30.0': - resolution: {integrity: sha512-5D3Nk2JPqMI1wK25pEymeWRSlSMdo5QOGlyfrKg0AOufrUcjEE3RQgaCpHoBiM31CSNrtSgdJ0U6zL1rLDDfBA==} - hasBin: true - - '@changesets/config@3.1.3': - resolution: {integrity: sha512-vnXjcey8YgBn2L1OPWd3ORs0bGC4LoYcK/ubpgvzNVr53JXV5GiTVj7fWdMRsoKUH7hhhMAQnsJUqLr21EncNw==} - - '@changesets/errors@0.2.0': - resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} - - '@changesets/get-dependents-graph@2.1.3': - resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} - - '@changesets/get-github-info@0.8.0': - resolution: {integrity: sha512-cRnC+xdF0JIik7coko3iUP9qbnfi1iJQ3sAa6dE+Tx3+ET8bjFEm63PA4WEohgjYcmsOikPHWzPsMWWiZmntOQ==} - - '@changesets/get-release-plan@4.0.15': - resolution: {integrity: sha512-Q04ZaRPuEVZtA+auOYgFaVQQSA98dXiVe/yFaZfY7hoSmQICHGvP0TF4u3EDNHWmmCS4ekA/XSpKlSM2PyTS2g==} - - '@changesets/get-version-range-type@0.4.0': - resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} - - '@changesets/git@3.0.4': - resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} - - '@changesets/logger@0.1.1': - resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} - - '@changesets/parse@0.4.3': - resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==} - - '@changesets/pre@2.0.2': - resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} - - '@changesets/read@0.6.7': - resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==} - - '@changesets/should-skip-package@0.1.2': - resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} - - '@changesets/types@4.1.0': - resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} - - '@changesets/types@6.1.0': - resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} - - '@changesets/write@0.4.0': - resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - - '@colors/colors@1.5.0': - resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} - engines: {node: '>=0.1.90'} - - '@dprint/formatter@0.4.1': - resolution: {integrity: sha512-IB/GXdlMOvi0UhQQ9mcY15Fxcrc2JPadmo6tqefCNV0bptFq7YBpggzpqYXldBXDa04CbKJ+rDwO2eNRPE2+/g==} - - '@dprint/typescript@0.91.8': - resolution: {integrity: sha512-tuKn4leCPItox1O4uunHcQF0QllDCvPWklnNQIh2PiWWVtRAGltJJnM4Cwj5AciplosD1Hiz7vAY3ew3crLb3A==} - - '@effect/cli@0.75.0': - resolution: {integrity: sha512-SAJj1a1kb5yoSUz4yORmwjyOBv89y2wf2Q08KC/RwskUCZunj29eNZgl8Pkbv6nDFTGlre6EW/Kl2S/aOtQWwQ==} - peerDependencies: - '@effect/platform': ^0.96.0 - '@effect/printer': ^0.49.0 - '@effect/printer-ansi': ^0.49.0 - effect: ^3.21.0 - - '@effect/cluster@0.58.0': - resolution: {integrity: sha512-0Zog7s7XdntWcTqdqWPoj6nc7hPaWIzp0k0DsFUWyCynXNPK9dAtgFrSce04NhddNqqbhtZck/lhuqJwNBrprQ==} - peerDependencies: - '@effect/platform': ^0.96.0 - '@effect/rpc': ^0.75.0 - '@effect/sql': ^0.51.0 - '@effect/workflow': ^0.18.0 - effect: ^3.21.0 - - '@effect/eslint-plugin@0.3.2': - resolution: {integrity: sha512-c4Vs9t3r54A4Zpl+wo8+PGzZz3JWYsip41H+UrebRLjQ2Hk/ap63IeCgN/HWcYtxtyhRopjp7gW9nOQ2Snbl+g==} - - '@effect/experimental@0.60.0': - resolution: {integrity: sha512-i5zIg7Xup2KgHyqHlYtkgqSE1bNzCL0GbbTQxrpIzKF0q/ebknOk/ox8B/gIq2vImjoEE81h/oxU+6i1NH210g==} - peerDependencies: - '@effect/platform': ^0.96.0 - effect: ^3.21.0 - ioredis: ^5 - lmdb: ^3 - peerDependenciesMeta: - ioredis: - optional: true - lmdb: - optional: true - - '@effect/language-service@0.81.0': - resolution: {integrity: sha512-7MYFsq9w9l2MkUw5/33fiG3YAkgnT6U1mwV0QvhokhnLhPW9cIetwAHNtXwsgr5omPQheLuflTIAFvPaZLQcPw==} - hasBin: true - - '@effect/platform-node-shared@0.57.1': - resolution: {integrity: sha512-oX/bApMdoKsyrDiNdJxo7U9Rz1RXsjRv+ecfAPp1qGlSdGIo32wVRvJ2XCHqYj0sqaYJS0pU0/GCulRfVGuJag==} - peerDependencies: - '@effect/cluster': ^0.56.1 - '@effect/platform': ^0.94.2 - '@effect/rpc': ^0.73.0 - '@effect/sql': ^0.49.0 - effect: ^3.19.15 - - '@effect/platform-node-shared@0.59.0': - resolution: {integrity: sha512-3bq2YKKfLY7UFauZSxqZUneCXoA3SMSls82V+0RKunvRlfPuPQW0hVn6t1RkvEdh0PDoygWG2mZXYQa6Iqgp9A==} - peerDependencies: - '@effect/cluster': ^0.58.0 - '@effect/platform': ^0.96.0 - '@effect/rpc': ^0.75.0 - '@effect/sql': ^0.51.0 - effect: ^3.21.0 - - '@effect/platform-node@0.104.1': - resolution: {integrity: sha512-jT1a/z98niK6fnEU8pWHPPCdJMVDRCIdB65lolcOjse5rsTwVbczMjvKkhVQpF63mNWoOnol7OTRNkw5L54llg==} - peerDependencies: - '@effect/cluster': ^0.56.1 - '@effect/platform': ^0.94.2 - '@effect/rpc': ^0.73.0 - '@effect/sql': ^0.49.0 - effect: ^3.19.15 - - '@effect/platform-node@0.106.0': - resolution: {integrity: sha512-mpsJK2jNLVd0jQAjHKBo8j3wdKWznSGvfnKBcAuG/9Rr4mb8bMRZFLXHHT9wUP7EvnZ0tDZJgEDxkC+j+ByRag==} - peerDependencies: - '@effect/cluster': ^0.58.0 - '@effect/platform': ^0.96.0 - '@effect/rpc': ^0.75.0 - '@effect/sql': ^0.51.0 - effect: ^3.21.0 - - '@effect/platform@0.94.5': - resolution: {integrity: sha512-z05APUiDDPbodhTkH/RJqOLoCU11bU2IZLfcwLFrld03+ob1VeqRnELQlmueLIYm6NZifHAtjl32V+GRt34y4A==} - peerDependencies: - effect: ^3.19.17 - - '@effect/platform@0.96.0': - resolution: {integrity: sha512-U7PLhkVzg7zzrgFvyWATOzD6reL87KG/fcdOxgLWBQ/J5CCU6qdPAVG+0o6o+IxcsLoqGwxs+rFxaFzrdtDV1A==} - peerDependencies: - effect: ^3.21.0 - - '@effect/printer-ansi@0.49.0': - resolution: {integrity: sha512-N2OyqDTqcGLKeUy2URowThoU5issZQwG/Ihv5qOYWJD0neq9qBIgC57/9BkFpTRPNSMtPHyCOk1TFj297HGLLQ==} - peerDependencies: - '@effect/typeclass': ^0.40.0 - effect: ^3.21.0 - - '@effect/printer@0.49.0': - resolution: {integrity: sha512-hrjTuExF87wuWjOnnND1c2fKcCWhleQBVaoA7JlrU3rC7s+RYPETDOXtpgAK3/uuMCRnDhfVFQMevtKT8MBdKg==} - peerDependencies: - '@effect/typeclass': ^0.40.0 - effect: ^3.21.0 - - '@effect/rpc@0.75.0': - resolution: {integrity: sha512-VFeJ16cZUXqiIzG9UHOVKGuiBPJ7fV+0lEbJU6xi12JnnxXe/19BQPpOwiRawCUbPOR3/xIURDUgGxU+Ft0pvQ==} - peerDependencies: - '@effect/platform': ^0.96.0 - effect: ^3.21.0 - - '@effect/schema@0.75.5': - resolution: {integrity: sha512-TQInulTVCuF+9EIbJpyLP6dvxbQJMphrnRqgexm/Ze39rSjfhJuufF7XvU3SxTgg3HnL7B/kpORTJbHhlE6thw==} - deprecated: this package has been merged into the main effect package - peerDependencies: - effect: ^3.9.2 - - '@effect/sql@0.51.0': - resolution: {integrity: sha512-e7hWe46QD15eMCr4kNBMVdItIVK/WLHJG+d8DLL1FjVf5Ra82k2mwUYIXplJewVbHjt3my6GSKPPd1ZrQjVd5A==} - peerDependencies: - '@effect/experimental': ^0.60.0 - '@effect/platform': ^0.96.0 - effect: ^3.21.0 - - '@effect/typeclass@0.40.0': - resolution: {integrity: sha512-L/2o2ImeqbemFlqH0b3y2PqQTFc+E0/DUnffCU8bkJUGh0yUZmh2RXuXhR8QOpfNCe718JQjI+mLnpVF2MMmaQ==} - peerDependencies: - effect: ^3.21.0 - - '@effect/vitest@0.29.0': - resolution: {integrity: sha512-DvWr1aeEcaZ8mtu8hNVb4e3rEYvGEwQSr7wsNrW53t6nKYjkmjRICcvVEsXUhjoCblRHSxRsRV0TOt0+UmcvaQ==} - peerDependencies: - effect: ^3.21.0 - vitest: ^3.2.0 - - '@effect/workflow@0.18.0': - resolution: {integrity: sha512-9Zp+x9ADtR0H6CRhU6wLyPcIRjO1PXjvSpUlFlBQ8piw7ldjPmnUWEY8YQuH6eExV2dalQ4z2LMiZ5Bd7XAJbA==} - peerDependencies: - '@effect/experimental': ^0.60.0 - '@effect/platform': ^0.96.0 - '@effect/rpc': ^0.75.0 - effect: ^3.21.0 - - '@emnapi/core@1.7.1': - resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} - - '@emnapi/runtime@1.7.1': - resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} - - '@emnapi/wasi-threads@1.1.0': - resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} - - '@esbuild/aix-ppc64@0.27.2': - resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.27.2': - resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.27.2': - resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.27.2': - resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.27.2': - resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.2': - resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.27.2': - resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.2': - resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.27.2': - resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.27.2': - resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.27.2': - resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.27.2': - resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.27.2': - resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.27.2': - resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.2': - resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.27.2': - resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.27.2': - resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.27.2': - resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.2': - resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.27.2': - resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.2': - resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.27.2': - resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.27.2': - resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.27.2': - resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.27.2': - resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.27.2': - resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@eslint-community/eslint-plugin-eslint-comments@4.7.1': - resolution: {integrity: sha512-Ql2nJFwA8wUGpILYGOQaT1glPsmvEwE0d+a+l7AALLzQvInqdbXJdx7aSu0DpUX9dB1wMVBMhm99/++S3MdEtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 - - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/compat@2.0.3': - resolution: {integrity: sha512-SjIJhGigp8hmd1YGIBwh7Ovri7Kisl42GYFjrOyHhtfYGGoLW6teYi/5p8W50KSsawUPpuLOSmsq1bD0NGQLBw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - peerDependencies: - eslint: ^8.40 || 9 || 10 - peerDependenciesMeta: - eslint: - optional: true - - '@eslint/config-array@0.23.3': - resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/config-helpers@0.5.3': - resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/core@1.1.1': - resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/eslintrc@3.3.5': - resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@10.0.1': - resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - peerDependencies: - eslint: ^10.0.0 - peerDependenciesMeta: - eslint: - optional: true - - '@eslint/object-schema@3.0.3': - resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/plugin-kit@0.6.1': - resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} - engines: {node: '>=18.18.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} - - '@inquirer/external-editor@1.0.3': - resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - - '@jest/expect-utils@29.7.0': - resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/schemas@29.6.3': - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/types@29.6.3': - resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@jscpd/badge-reporter@4.0.4': - resolution: {integrity: sha512-I9b4MmLXPM2vo0SxSUWnNGKcA4PjQlD3GzXvFK60z43cN/EIdLbOq3FVwCL+dg2obUqGXKIzAm7EsDFTg0D+mQ==} - - '@jscpd/core@4.0.4': - resolution: {integrity: sha512-QGMT3iXEX1fI6lgjPH+x8eyJwhwr2KkpSF5uBpjC0Z5Xloj0yFTFLtwJT+RhxP/Ob4WYrtx2jvpKB269oIwgMQ==} - - '@jscpd/finder@4.0.4': - resolution: {integrity: sha512-qVUWY7Nzuvfd5OIk+n7/5CM98LmFroLqblRXAI2gDABwZrc7qS+WH2SNr0qoUq0f4OqwM+piiwKvwL/VDNn/Cg==} - - '@jscpd/html-reporter@4.0.4': - resolution: {integrity: sha512-YiepyeYkeH74Kx59PJRdUdonznct0wHPFkf6FLQN+mCBoy6leAWCcOfHtcexnp+UsBFDlItG5nRdKrDSxSH+Kg==} - - '@jscpd/tokenizer@4.0.4': - resolution: {integrity: sha512-xxYYY/qaLah/FlwogEbGIxx9CjDO+G9E6qawcy26WwrflzJb6wsnhjwdneN6Wb0RNCDsqvzY+bzG453jsin4UQ==} - - '@manypkg/find-root@1.1.0': - resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} - - '@manypkg/get-packages@1.1.3': - resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} - - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': - resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} - cpu: [arm64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': - resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} - cpu: [x64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': - resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} - cpu: [arm64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': - resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} - cpu: [arm] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': - resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} - cpu: [x64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': - resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} - cpu: [x64] - os: [win32] - - '@napi-rs/wasm-runtime@0.2.12': - resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - - '@napi-rs/wasm-runtime@1.1.1': - resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@oxc-project/types@0.120.0': - resolution: {integrity: sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==} - - '@parcel/watcher-android-arm64@2.5.1': - resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [android] - - '@parcel/watcher-darwin-arm64@2.5.1': - resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [darwin] - - '@parcel/watcher-darwin-x64@2.5.1': - resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [darwin] - - '@parcel/watcher-freebsd-x64@2.5.1': - resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [freebsd] - - '@parcel/watcher-linux-arm-glibc@2.5.1': - resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-arm-musl@2.5.1': - resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - libc: [musl] - - '@parcel/watcher-linux-arm64-glibc@2.5.1': - resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-arm64-musl@2.5.1': - resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@parcel/watcher-linux-x64-glibc@2.5.1': - resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-x64-musl@2.5.1': - resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@parcel/watcher-win32-arm64@2.5.1': - resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [win32] - - '@parcel/watcher-win32-ia32@2.5.1': - resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} - engines: {node: '>= 10.0.0'} - cpu: [ia32] - os: [win32] - - '@parcel/watcher-win32-x64@2.5.1': - resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [win32] - - '@parcel/watcher@2.5.1': - resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} - engines: {node: '>= 10.0.0'} - - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - - '@pnpm/deps.graph-sequencer@1.0.0': - resolution: {integrity: sha512-vWWVbYYBBN/kweokmURicokyg7crzcDZo9/naziv8B8RSWrLWFpq5Xl0ro6QCQKgRmb6O78Qy9uQT+Fp79RxsA==} - engines: {node: '>=16.14'} - - '@prover-coder-ai/eslint-plugin-suggest-members@0.0.25': - resolution: {integrity: sha512-J0oZtIz6IYeXWBgNLXaX2HyzSOcqTsjE+vzs/MQr7SKASvBYsyA7F34dQsh/8GM/kWBuSltkUsfv2RIcM6+t5Q==} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@rolldown/binding-android-arm64@1.0.0-rc.10': - resolution: {integrity: sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.0.0-rc.10': - resolution: {integrity: sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.0.0-rc.10': - resolution: {integrity: sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.0.0-rc.10': - resolution: {integrity: sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': - resolution: {integrity: sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': - resolution: {integrity: sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': - resolution: {integrity: sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': - resolution: {integrity: sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': - resolution: {integrity: sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': - resolution: {integrity: sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': - resolution: {integrity: sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': - resolution: {integrity: sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': - resolution: {integrity: sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': - resolution: {integrity: sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': - resolution: {integrity: sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/pluginutils@1.0.0-rc.10': - resolution: {integrity: sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==} - - '@rtsao/scc@1.1.0': - resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - - '@sinclair/typebox@0.27.8': - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - - '@ton-ai-core/vibecode-linter@1.0.11': - resolution: {integrity: sha512-CSert5rYENM7MMvY3AcKdtBTYBnqeb2ti4CS4lNMWoDbyGqA6PmOH7/WK8+fcl6VyGJiPBTzq5Hp+1LYHUUuJA==} - engines: {node: '>=18.0.0'} - hasBin: true - - '@ts-morph/common@0.28.1': - resolution: {integrity: sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==} - - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.28.0': - resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - - '@types/dedent@0.7.0': - resolution: {integrity: sha512-EGlKlgMhnLt/cM4DbUSafFdrkeJoC9Mvnj0PUCU7tFmTjMjNRT957kXCx0wYm3JuEq4o4ZsS5vG+NlkM2DMd2A==} - - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - - '@types/eslint@8.56.12': - resolution: {integrity: sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==} - - '@types/esrecurse@4.3.1': - resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/glob@7.1.3': - resolution: {integrity: sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==} - - '@types/istanbul-lib-coverage@2.0.6': - resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} - - '@types/istanbul-lib-report@3.0.3': - resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} - - '@types/istanbul-reports@3.0.4': - resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - - '@types/js-yaml@3.12.5': - resolution: {integrity: sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/json5@0.0.29': - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - - '@types/lodash@4.17.21': - resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==} - - '@types/mdast@3.0.15': - resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} - - '@types/minimatch@6.0.0': - resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==} - deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed. - - '@types/node@12.20.55': - resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - - '@types/node@24.12.0': - resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} - - '@types/normalize-package-data@2.4.4': - resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - - '@types/react@19.2.14': - resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} - - '@types/sarif@2.1.7': - resolution: {integrity: sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==} - - '@types/stack-utils@2.0.3': - resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - - '@types/unist@2.0.11': - resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} - - '@types/yargs-parser@21.0.3': - resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} - - '@types/yargs@17.0.35': - resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - - '@typescript-eslint/eslint-plugin@8.57.1': - resolution: {integrity: sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.57.1 - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/parser@8.57.1': - resolution: {integrity: sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/project-service@8.55.0': - resolution: {integrity: sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/project-service@8.57.1': - resolution: {integrity: sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/scope-manager@8.55.0': - resolution: {integrity: sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/scope-manager@8.57.1': - resolution: {integrity: sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.55.0': - resolution: {integrity: sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/tsconfig-utils@8.57.0': - resolution: {integrity: sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/tsconfig-utils@8.57.1': - resolution: {integrity: sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/type-utils@8.57.1': - resolution: {integrity: sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/types@8.55.0': - resolution: {integrity: sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/types@8.57.1': - resolution: {integrity: sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.55.0': - resolution: {integrity: sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/typescript-estree@8.57.1': - resolution: {integrity: sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.55.0': - resolution: {integrity: sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.57.1': - resolution: {integrity: sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/visitor-keys@8.55.0': - resolution: {integrity: sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/visitor-keys@8.57.1': - resolution: {integrity: sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@unrs/resolver-binding-android-arm-eabi@1.11.1': - resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} - cpu: [arm] - os: [android] - - '@unrs/resolver-binding-android-arm64@1.11.1': - resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} - cpu: [arm64] - os: [android] - - '@unrs/resolver-binding-darwin-arm64@1.11.1': - resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} - cpu: [arm64] - os: [darwin] - - '@unrs/resolver-binding-darwin-x64@1.11.1': - resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} - cpu: [x64] - os: [darwin] - - '@unrs/resolver-binding-freebsd-x64@1.11.1': - resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} - cpu: [x64] - os: [freebsd] - - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} - cpu: [arm] - os: [linux] - - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} - cpu: [arm] - os: [linux] - - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@unrs/resolver-binding-wasm32-wasi@1.11.1': - resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': - resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} - cpu: [arm64] - os: [win32] - - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': - resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} - cpu: [ia32] - os: [win32] - - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': - resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} - cpu: [x64] - os: [win32] - - '@vitest/coverage-v8@4.1.0': - resolution: {integrity: sha512-nDWulKeik2bL2Va/Wl4x7DLuTKAXa906iRFooIRPR+huHkcvp9QDkPQ2RJdmjOFrqOqvNfoSQLF68deE3xC3CQ==} - peerDependencies: - '@vitest/browser': 4.1.0 - vitest: 4.1.0 - peerDependenciesMeta: - '@vitest/browser': - optional: true - - '@vitest/eslint-plugin@1.6.13': - resolution: {integrity: sha512-ui7JGWBoQpS5NKKW0FDb1eTuFEZ5EupEv2Psemuyfba7DfA5K52SeDLelt6P4pQJJ/4UGkker/BgMk/KrjH3WQ==} - engines: {node: '>=18'} - peerDependencies: - '@typescript-eslint/eslint-plugin': '*' - eslint: '>=8.57.0' - typescript: '>=5.0.0' - vitest: '*' - peerDependenciesMeta: - '@typescript-eslint/eslint-plugin': - optional: true - typescript: - optional: true - vitest: - optional: true - - '@vitest/expect@4.1.0': - resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} - - '@vitest/mocker@4.1.0': - resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==} - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@4.1.0': - resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} - - '@vitest/runner@4.1.0': - resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} - - '@vitest/snapshot@4.1.0': - resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==} - - '@vitest/spy@4.1.0': - resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} - - '@vitest/utils@4.1.0': - resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@7.4.1: - resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} - engines: {node: '>=0.4.0'} - hasBin: true - - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true - - ajv@6.14.0: - resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} - - ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - - ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} - - ansi-escapes@7.3.0: - resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} - engines: {node: '>=18'} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} - engines: {node: '>=12'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} - - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - array-buffer-byte-length@1.0.2: - resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} - engines: {node: '>= 0.4'} - - array-includes@3.1.9: - resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} - engines: {node: '>= 0.4'} - - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - - array.prototype.findlastindex@1.2.6: - resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} - engines: {node: '>= 0.4'} - - array.prototype.flat@1.3.3: - resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} - engines: {node: '>= 0.4'} - - array.prototype.flatmap@1.3.3: - resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} - engines: {node: '>= 0.4'} - - arraybuffer.prototype.slice@1.0.4: - resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} - engines: {node: '>= 0.4'} - - asap@2.0.6: - resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - - assert-never@1.4.0: - resolution: {integrity: sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - ast-types@0.16.1: - resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} - engines: {node: '>=4'} - - ast-v8-to-istanbul@1.0.0: - resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} - - async-function@1.0.0: - resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} - engines: {node: '>= 0.4'} - - auto-bind@5.0.1: - resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - - babel-walk@3.0.0-canary-5: - resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} - engines: {node: '>= 10.0.0'} - - badgen@3.2.3: - resolution: {integrity: sha512-svDuwkc63E/z0ky3drpUppB83s/nlgDciH9m+STwwQoWyq7yCgew1qEfJ+9axkKdNq7MskByptWUN9j1PGMwFA==} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} - - baseline-browser-mapping@2.8.32: - resolution: {integrity: sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==} - hasBin: true - - better-path-resolve@1.0.0: - resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} - engines: {node: '>=4'} - - blamer@1.0.7: - resolution: {integrity: sha512-GbBStl/EVlSWkiJQBZps3H1iARBrC7vt++Jb/TTmCNu/jZ04VW7tSN1nScbFXBUy1AN+jzeL7Zep9sbQxLhXKA==} - engines: {node: '>=8.9'} - - boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - - brace-expansion@5.0.4: - resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} - engines: {node: 18 || 20 || >=22} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - browserslist@4.28.0: - resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - builtin-modules@3.3.0: - resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} - engines: {node: '>=6'} - - builtin-modules@5.0.0: - resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} - engines: {node: '>=18.20'} - - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bind@1.0.8: - resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - caniuse-lite@1.0.30001759: - resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==} - - chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} - engines: {node: '>=18'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - - change-case@5.4.4: - resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} - - character-entities-legacy@1.1.4: - resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} - - character-entities@1.2.4: - resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} - - character-parser@2.2.0: - resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} - - character-reference-invalid@1.1.4: - resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} - - chardet@2.1.1: - resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} - - cheerio-select@2.1.0: - resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} - - cheerio@1.1.2: - resolution: {integrity: sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==} - engines: {node: '>=20.18.1'} - - ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} - - ci-info@4.3.1: - resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} - engines: {node: '>=8'} - - clean-regexp@1.0.0: - resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} - engines: {node: '>=4'} - - cli-boxes@3.0.0: - resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} - engines: {node: '>=10'} - - cli-cursor@4.0.0: - resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - cli-table3@0.6.5: - resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} - engines: {node: 10.* || >= 12.*} - - cli-truncate@5.2.0: - resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} - engines: {node: '>=20'} - - code-block-writer@13.0.3: - resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} - - code-excerpt@4.0.0: - resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - colors@1.4.0: - resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} - engines: {node: '>=0.1.90'} - - commander@5.1.0: - resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} - engines: {node: '>= 6'} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - constantinople@4.0.1: - resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - convert-to-spaces@2.0.1: - resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - core-js-compat@3.47.0: - resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - css-select@5.2.2: - resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} - - css-what@6.2.2: - resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} - engines: {node: '>= 6'} - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - data-view-buffer@1.0.2: - resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} - engines: {node: '>= 0.4'} - - data-view-byte-length@1.0.2: - resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} - engines: {node: '>= 0.4'} - - data-view-byte-offset@1.0.1: - resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} - engines: {node: '>= 0.4'} - - dataloader@1.4.0: - resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} - - debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - dedent@1.7.0: - resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==} - peerDependencies: - babel-plugin-macros: ^3.1.0 - peerDependenciesMeta: - babel-plugin-macros: - optional: true - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - - define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - - detect-indent@6.1.0: - resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} - engines: {node: '>=8'} - - detect-libc@1.0.3: - resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} - engines: {node: '>=0.10'} - hasBin: true - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - - doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - - doctypes@1.1.0: - resolution: {integrity: sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==} - - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - - domutils@3.2.2: - resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - - dotenv@8.6.0: - resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} - engines: {node: '>=10'} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - effect@3.21.0: - resolution: {integrity: sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==} - - electron-to-chromium@1.5.263: - resolution: {integrity: sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==} - - emoji-regex@10.6.0: - resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - - encoding-sniffer@0.2.1: - resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} - - end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - - enquirer@2.4.1: - resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} - engines: {node: '>=8.6'} - - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - - entities@6.0.1: - resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} - engines: {node: '>=0.12'} - - environment@1.1.0: - resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} - engines: {node: '>=18'} - - error-ex@1.3.4: - resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - - es-abstract@1.24.0: - resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} - engines: {node: '>= 0.4'} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-module-lexer@2.0.0: - resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} - - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - - es-shim-unscopables@1.1.0: - resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} - engines: {node: '>= 0.4'} - - es-to-primitive@1.3.0: - resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} - engines: {node: '>= 0.4'} - - es-toolkit@1.44.0: - resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==} - - esbuild@0.27.2: - resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} - engines: {node: '>=18'} - hasBin: true - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - - escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-import-context@0.1.9: - resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - peerDependencies: - unrs-resolver: ^1.0.0 - peerDependenciesMeta: - unrs-resolver: - optional: true - - eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - - eslint-import-resolver-typescript@4.4.4: - resolution: {integrity: sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==} - engines: {node: ^16.17.0 || >=18.6.0} - peerDependencies: - eslint: '*' - eslint-plugin-import: '*' - eslint-plugin-import-x: '*' - peerDependenciesMeta: - eslint-plugin-import: - optional: true - eslint-plugin-import-x: - optional: true - - eslint-module-utils@2.12.1: - resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - - eslint-plugin-codegen@0.34.1: - resolution: {integrity: sha512-Z9N+8eIP5G61Ta+kYf87h9fN8RkxtT6Kjy9goHVGeSgAPryPhcU2SrS4265z2qtKhrNlpSU6gYIcETMbUySfXg==} - engines: {node: '>=18.0.0'} - - eslint-plugin-import@2.32.0: - resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - - eslint-plugin-markdown@4.0.1: - resolution: {integrity: sha512-5/MnGvYU0i8MbHH5cg8S+Vl3DL+bqRNYshk1xUO86DilNBaxtTkhH+5FD0/yO03AmlI6+lfNFdk2yOw72EPzpA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - deprecated: Please use @eslint/markdown instead - peerDependencies: - eslint: '>=8' - - eslint-plugin-simple-import-sort@12.1.1: - resolution: {integrity: sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==} - peerDependencies: - eslint: '>=5.0.0' - - eslint-plugin-sonarjs@4.0.2: - resolution: {integrity: sha512-BTcT1zr1iTbmJtVlcesISwnXzh+9uhf9LEOr+RRNf4kR8xA0HQTPft4oiyOCzCOGKkpSJxjR8ZYF6H7VPyplyw==} - peerDependencies: - eslint: ^8.0.0 || ^9.0.0 || ^10.0.0 - - eslint-plugin-sort-destructure-keys@3.0.0: - resolution: {integrity: sha512-ian2KEdGi8xZW50SVz9HIP9PDQN4XWeo3Hax3LsDk0ojL+wrwk40az8bKCnt3q2J7I3q5xF2ncZ0arj2q8Ou+A==} - engines: {node: '>=18'} - peerDependencies: - eslint: 5 - 10 - - eslint-plugin-unicorn@63.0.0: - resolution: {integrity: sha512-Iqecl9118uQEXYh7adylgEmGfkn5es3/mlQTLLkd4pXkIk9CTGrAbeUux+YljSa2ohXCBmQQ0+Ej1kZaFgcfkA==} - engines: {node: ^20.10.0 || >=21.0.0} - peerDependencies: - eslint: '>=9.38.0' - - eslint-scope@9.1.2: - resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@4.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint-visitor-keys@5.0.1: - resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - eslint@10.1.0: - resolution: {integrity: sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - espree@10.4.0: - resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - espree@11.2.0: - resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - - execa@4.1.0: - resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} - engines: {node: '>=10'} - - expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} - engines: {node: '>=12.0.0'} - - expect@29.7.0: - resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - extendable-error@0.1.7: - resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} - - fast-check@3.23.2: - resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} - engines: {node: '>=8.0.0'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - find-my-way-ts@0.1.6: - resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} - - find-up-simple@1.0.1: - resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} - engines: {node: '>=18'} - - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - - for-each@0.3.5: - resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} - engines: {node: '>= 0.4'} - - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - - fp-ts@2.16.11: - resolution: {integrity: sha512-LaI+KaX2NFkfn1ZGHoKCmcfv7yrZsC3b8NtWsTVQeHkq4F27vI5igUuO53sxqDEa2gNQMHFPmpojDw/1zmUK7w==} - - fs-extra@11.3.2: - resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} - engines: {node: '>=14.14'} - - fs-extra@7.0.1: - resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} - engines: {node: '>=6 <7 || >=8'} - - fs-extra@8.1.0: - resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} - engines: {node: '>=6 <7 || >=8'} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - function.prototype.name@1.1.8: - resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} - engines: {node: '>= 0.4'} - - functional-red-black-tree@1.0.1: - resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} - - functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - - generator-function@2.0.1: - resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} - engines: {node: '>= 0.4'} - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - get-east-asian-width@1.4.0: - resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} - engines: {node: '>=18'} - - get-east-asian-width@1.5.0: - resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} - engines: {node: '>=18'} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - - get-symbol-description@1.1.0: - resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} - engines: {node: '>= 0.4'} - - get-tsconfig@4.13.0: - resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} - - gitignore-to-glob@0.3.0: - resolution: {integrity: sha512-mk74BdnK7lIwDHnotHddx1wsjMOFIThpLY3cPNniJ/2fA/tlLzHnFxIdR+4sLOu5KGgQJdij4kjJ2RoUNnCNMA==} - engines: {node: '>=4.4 <5 || >=6.9'} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - glob@10.5.0: - resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true - - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - - globals@16.5.0: - resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} - engines: {node: '>=18'} - - globals@17.4.0: - resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} - engines: {node: '>=18'} - - globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - - globrex@0.1.2: - resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - has-bigints@1.1.0: - resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} - engines: {node: '>= 0.4'} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - - has-proto@1.2.0: - resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} - engines: {node: '>= 0.4'} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - hosted-git-info@2.8.9: - resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} - - html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - - htmlparser2@10.0.0: - resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} - - human-id@4.1.3: - resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} - hasBin: true - - human-signals@1.1.1: - resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} - engines: {node: '>=8.12.0'} - - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - - iconv-lite@0.7.0: - resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} - engines: {node: '>=0.10.0'} - - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} - engines: {node: '>= 4'} - - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - indent-string@5.0.0: - resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} - engines: {node: '>=12'} - - ini@4.1.3: - resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - ink@6.8.0: - resolution: {integrity: sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA==} - engines: {node: '>=20'} - peerDependencies: - '@types/react': '>=19.0.0' - react: '>=19.0.0' - react-devtools-core: '>=6.1.2' - peerDependenciesMeta: - '@types/react': - optional: true - react-devtools-core: - optional: true - - internal-slot@1.1.0: - resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} - engines: {node: '>= 0.4'} - - io-ts-extra@0.11.6: - resolution: {integrity: sha512-rTsvx3W5B2nx7p/eGf+OsEaBTmjSjLzxBDEiweCjwqIL9ZN6CZjG7hFK8zyGJyM0I2uCsRU4uYUhaTgg2SKHkQ==} - - io-ts@2.2.22: - resolution: {integrity: sha512-FHCCztTkHoV9mdBsHpocLpdTAfh956ZQcIkWQxxS0U5HT53vtrcuYdQneEJKH6xILaLNzXVl2Cvwtoy8XNN0AA==} - peerDependencies: - fp-ts: ^2.5.0 - - is-alphabetical@1.0.4: - resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} - - is-alphanumerical@1.0.4: - resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} - - is-array-buffer@3.0.5: - resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} - engines: {node: '>= 0.4'} - - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - - is-async-function@2.1.1: - resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} - engines: {node: '>= 0.4'} - - is-bigint@1.1.0: - resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} - engines: {node: '>= 0.4'} - - is-boolean-object@1.2.2: - resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} - engines: {node: '>= 0.4'} - - is-builtin-module@5.0.0: - resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} - engines: {node: '>=18.20'} - - is-bun-module@2.0.0: - resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} - - is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - - is-data-view@1.0.2: - resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} - engines: {node: '>= 0.4'} - - is-date-object@1.1.0: - resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} - engines: {node: '>= 0.4'} - - is-decimal@1.0.4: - resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} - - is-expression@4.0.0: - resolution: {integrity: sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-finalizationregistry@1.1.1: - resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} - engines: {node: '>= 0.4'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-fullwidth-code-point@5.1.0: - resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} - engines: {node: '>=18'} - - is-generator-function@1.1.2: - resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} - engines: {node: '>= 0.4'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-hexadecimal@1.0.4: - resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} - - is-in-ci@2.0.0: - resolution: {integrity: sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==} - engines: {node: '>=20'} - hasBin: true - - is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - engines: {node: '>= 0.4'} - - is-negative-zero@2.0.3: - resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} - engines: {node: '>= 0.4'} - - is-number-object@1.1.1: - resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} - engines: {node: '>= 0.4'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-promise@2.2.2: - resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} - - is-regex@1.2.1: - resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} - engines: {node: '>= 0.4'} - - is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} - - is-shared-array-buffer@1.0.4: - resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} - engines: {node: '>= 0.4'} - - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - - is-string@1.1.1: - resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} - engines: {node: '>= 0.4'} - - is-subdir@1.2.0: - resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} - engines: {node: '>=4'} - - is-symbol@1.1.1: - resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} - engines: {node: '>= 0.4'} - - is-typed-array@1.1.15: - resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} - engines: {node: '>= 0.4'} - - is-weakmap@2.0.2: - resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} - engines: {node: '>= 0.4'} - - is-weakref@1.1.1: - resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} - engines: {node: '>= 0.4'} - - is-weakset@2.0.4: - resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} - engines: {node: '>= 0.4'} - - is-windows@1.0.2: - resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} - engines: {node: '>=0.10.0'} - - isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} - - istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} - - istanbul-reports@3.2.0: - resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} - engines: {node: '>=8'} - - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - - jest-diff@29.7.0: - resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-get-type@29.6.3: - resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-matcher-utils@29.7.0: - resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-message-util@29.7.0: - resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-util@29.7.0: - resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jiti@2.6.1: - resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} - hasBin: true - - js-stringify@1.0.2: - resolution: {integrity: sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==} - - js-tokens@10.0.0: - resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@3.14.2: - resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} - hasBin: true - - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true - - jscpd-sarif-reporter@4.0.5: - resolution: {integrity: sha512-cD1MtUdpomUPM5C0YD0vKZmdj+Gyr0KD5Bk47yGMrPCtwtgsK+7v59OzBIUjYOL8AuxNAt6hvPFo0PH+PYJh0Q==} - - jscpd-sarif-reporter@4.0.6: - resolution: {integrity: sha512-b9Sm3IPZ3+m8Lwa4gZa+4/LhDhlc/ZLEsLXKSOy1DANQ6kx0ueqZT+fUHWEdQ6m0o3+RIVIa7DmvLSojQD05ng==} - - jscpd@4.0.8: - resolution: {integrity: sha512-d2VNT/2Hv4dxT2/59He8Lyda4DYOxPRyRG9zBaOpTZAqJCVf2xLrBlZkT8Va6Lo9u3X2qz8Bpq4HrDi4JsrQhA==} - hasBin: true - - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - json5@1.0.2: - resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} - hasBin: true - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - - jsonfile@6.2.0: - resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} - - jstransformer@1.0.0: - resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==} - - jsx-ast-utils-x@0.1.0: - resolution: {integrity: sha512-eQQBjBnsVtGacsG9uJNB8qOr3yA8rga4wAaGG1qRcBzSIvfhERLrWxMAM1hp5fcS6Abo8M4+bUBTekYR0qTPQw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - kubernetes-types@1.30.0: - resolution: {integrity: sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} - engines: {node: '>= 12.0.0'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - lodash.startcase@4.4.0: - resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} - - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - - loop-controls@1.1.0: - resolution: {integrity: sha512-otnxF3ngIuLecg99p7On7nJF6ws1mT2kNOiGOPFykEHQfhJtdsjcQMxM4LEHsUi3LeMrm2Ic0hFdykJcG0N1YQ==} - engines: {node: '>=18'} - - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - magicast@0.5.2: - resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} - - make-dir@4.0.0: - resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} - engines: {node: '>=10'} - - markdown-table@2.0.0: - resolution: {integrity: sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==} - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - mdast-util-from-markdown@0.8.5: - resolution: {integrity: sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==} - - mdast-util-to-string@2.0.0: - resolution: {integrity: sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==} - - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromark@2.11.4: - resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - mime@3.0.0: - resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} - engines: {node: '>=10.0.0'} - hasBin: true - - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - - minimatch@10.2.4: - resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} - engines: {node: 18 || 20 || >=22} - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@3.1.5: - resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - - mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - msgpackr-extract@3.0.3: - resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} - hasBin: true - - msgpackr@1.11.5: - resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} - - multipasta@0.2.7: - resolution: {integrity: sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==} - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - napi-postinstall@0.3.4: - resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - hasBin: true - - natural-compare-lite@1.4.0: - resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - node-addon-api@7.1.1: - resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - - node-gyp-build-optional-packages@5.2.2: - resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} - hasBin: true - - node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} - - node-sarif-builder@3.4.0: - resolution: {integrity: sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==} - engines: {node: '>=20'} - - normalize-package-data@2.5.0: - resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} - - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - - nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - - object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - - object.assign@4.1.7: - resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} - engines: {node: '>= 0.4'} - - object.fromentries@2.0.8: - resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} - engines: {node: '>= 0.4'} - - object.groupby@1.0.3: - resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} - engines: {node: '>= 0.4'} - - object.values@1.2.1: - resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} - engines: {node: '>= 0.4'} - - obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - outdent@0.5.0: - resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} - - own-keys@1.0.1: - resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} - engines: {node: '>= 0.4'} - - p-filter@2.1.0: - resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} - engines: {node: '>=8'} - - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - p-map@2.1.0: - resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} - engines: {node: '>=6'} - - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - - package-manager-detector@0.2.11: - resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - parse-entities@2.0.0: - resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} - - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - - parse5-htmlparser2-tree-adapter@7.1.0: - resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} - - parse5-parser-stream@7.1.2: - resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} - - parse5@7.3.0: - resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - - patch-console@2.0.0: - resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} - - pify@4.0.1: - resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} - engines: {node: '>=6'} - - pluralize@8.0.0: - resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} - engines: {node: '>=4'} - - possible-typed-array-names@1.1.0: - resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} - engines: {node: '>= 0.4'} - - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} - engines: {node: ^10 || ^12 || >=14} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} - engines: {node: '>=6.0.0'} - - prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} - hasBin: true - - pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - promise@7.3.1: - resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} - - pug-attrs@3.0.0: - resolution: {integrity: sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==} - - pug-code-gen@3.0.3: - resolution: {integrity: sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==} - - pug-error@2.1.0: - resolution: {integrity: sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==} - - pug-filters@4.0.0: - resolution: {integrity: sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==} - - pug-lexer@5.0.1: - resolution: {integrity: sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==} - - pug-linker@4.0.0: - resolution: {integrity: sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==} - - pug-load@3.0.0: - resolution: {integrity: sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==} - - pug-parser@6.0.0: - resolution: {integrity: sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==} - - pug-runtime@3.0.1: - resolution: {integrity: sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==} - - pug-strip-comments@2.0.0: - resolution: {integrity: sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==} - - pug-walk@2.0.0: - resolution: {integrity: sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==} - - pug@3.0.3: - resolution: {integrity: sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==} - - pump@3.0.3: - resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - pure-rand@6.1.0: - resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - - quansync@0.2.11: - resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - - react-reconciler@0.33.0: - resolution: {integrity: sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==} - engines: {node: '>=0.10.0'} - peerDependencies: - react: ^19.2.0 - - react@19.2.4: - resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} - engines: {node: '>=0.10.0'} - - read-pkg-up@7.0.1: - resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} - engines: {node: '>=8'} - - read-pkg@5.2.0: - resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} - engines: {node: '>=8'} - - read-yaml-file@1.1.0: - resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} - engines: {node: '>=6'} - - recast@0.23.11: - resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} - engines: {node: '>= 4'} - - refa@0.12.1: - resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - reflect.getprototypeof@1.0.10: - resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} - engines: {node: '>= 0.4'} - - regexp-ast-analysis@0.7.1: - resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - regexp-tree@0.1.27: - resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} - hasBin: true - - regexp.prototype.flags@1.5.4: - resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} - engines: {node: '>= 0.4'} - - regjsparser@0.13.0: - resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} - hasBin: true - - repeat-string@1.6.1: - resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} - engines: {node: '>=0.10'} - - reprism@0.0.11: - resolution: {integrity: sha512-VsxDR5QxZo08M/3nRypNlScw5r3rKeSOPdU/QhDmu3Ai3BJxHn/qgfXGWQp/tAxUtzwYNo9W6997JZR0tPLZsA==} - - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - - resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - - resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} - engines: {node: '>= 0.4'} - hasBin: true - - restore-cursor@4.0.0: - resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rolldown@1.0.0-rc.10: - resolution: {integrity: sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - safe-array-concat@1.1.3: - resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} - engines: {node: '>=0.4'} - - safe-push-apply@1.0.0: - resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} - engines: {node: '>= 0.4'} - - safe-regex-test@1.1.0: - resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} - engines: {node: '>= 0.4'} - - safe-stringify@1.2.0: - resolution: {integrity: sha512-C+LbapLbyGhP/WeMTrnYhIPjUoNTXZ/A3Znli8D5iF+IZXrDlgvfruykOq/bZ/5ncGy/K6RsavHlkirgWDFNdA==} - engines: {node: '>=16'} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - - scslre@0.3.0: - resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} - engines: {node: ^14.0.0 || >=16.0.0} - - semver@5.7.2: - resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} - hasBin: true - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} - engines: {node: '>=10'} - hasBin: true - - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} - engines: {node: '>=10'} - hasBin: true - - set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} - - set-function-name@2.0.2: - resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} - engines: {node: '>= 0.4'} - - set-proto@1.0.0: - resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} - engines: {node: '>= 0.4'} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - - slice-ansi@8.0.0: - resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} - engines: {node: '>=20'} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - spark-md5@3.0.2: - resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==} - - spawndamnit@3.0.1: - resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} - - spdx-correct@3.2.0: - resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} - - spdx-exceptions@2.5.0: - resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} - - spdx-expression-parse@3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - - spdx-license-ids@3.0.22: - resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==} - - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - - stable-hash-x@0.2.0: - resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} - engines: {node: '>=12.0.0'} - - stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - std-env@4.0.0: - resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} - - stop-iteration-iterator@1.1.0: - resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} - engines: {node: '>= 0.4'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - - string-width@7.2.0: - resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} - engines: {node: '>=18'} - - string-width@8.2.0: - resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} - engines: {node: '>=20'} - - string.prototype.trim@1.2.10: - resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} - engines: {node: '>= 0.4'} - - string.prototype.trimend@1.0.9: - resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} - engines: {node: '>= 0.4'} - - string.prototype.trimstart@1.0.8: - resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} - engines: {node: '>= 0.4'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-ansi@7.1.2: - resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} - engines: {node: '>=12'} - - strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - - strip-indent@4.1.1: - resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} - engines: {node: '>=12'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - tagged-tag@1.0.0: - resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} - engines: {node: '>=20'} - - term-size@2.2.1: - resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} - engines: {node: '>=8'} - - terminal-size@4.0.1: - resolution: {integrity: sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==} - engines: {node: '>=18'} - - tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} - engines: {node: '>=18'} - - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} - - tinyrainbow@3.0.3: - resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} - engines: {node: '>=14.0.0'} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - token-stream@1.0.0: - resolution: {integrity: sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==} - - toml@3.0.0: - resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} - - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - - ts-api-utils@2.4.0: - resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - - ts-morph@27.0.2: - resolution: {integrity: sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==} - - ts-pattern@5.9.0: - resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==} - - tsconfck@3.1.6: - resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} - engines: {node: ^18 || >=20} - hasBin: true - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - tsconfig-paths@3.15.0: - resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - tsx@4.21.0: - resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} - engines: {node: '>=18.0.0'} - hasBin: true - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - type-fest@0.6.0: - resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} - engines: {node: '>=8'} - - type-fest@0.8.1: - resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} - engines: {node: '>=8'} - - type-fest@5.4.4: - resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==} - engines: {node: '>=20'} - - typed-array-buffer@1.0.3: - resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} - engines: {node: '>= 0.4'} - - typed-array-byte-length@1.0.3: - resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} - engines: {node: '>= 0.4'} - - typed-array-byte-offset@1.0.4: - resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} - engines: {node: '>= 0.4'} - - typed-array-length@1.0.7: - resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} - engines: {node: '>= 0.4'} - - typescript-eslint@8.57.1: - resolution: {integrity: sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - unbox-primitive@1.1.0: - resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} - engines: {node: '>= 0.4'} - - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - - undici@7.16.0: - resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==} - engines: {node: '>=20.18.1'} - - unist-util-stringify-position@2.0.3: - resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} - - universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} - - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - - unrs-resolver@1.11.1: - resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - - update-browserslist-db@1.1.4: - resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - uuid@11.1.0: - resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} - hasBin: true - - validate-npm-package-license@3.0.4: - resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - - vite-tsconfig-paths@6.1.1: - resolution: {integrity: sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==} - peerDependencies: - vite: '*' - - vite@8.0.1: - resolution: {integrity: sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.0 - esbuild: ^0.27.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitest@4.1.0: - resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.0 - '@vitest/browser-preview': 4.1.0 - '@vitest/browser-webdriverio': 4.1.0 - '@vitest/ui': 4.1.0 - happy-dom: '*' - jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@opentelemetry/api': - optional: true - '@types/node': - optional: true - '@vitest/browser-playwright': - optional: true - '@vitest/browser-preview': - optional: true - '@vitest/browser-webdriverio': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - void-elements@3.1.0: - resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} - engines: {node: '>=0.10.0'} - - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - - whatwg-encoding@3.1.1: - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} - engines: {node: '>=18'} - deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation - - whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} - - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - - which-boxed-primitive@1.1.1: - resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} - engines: {node: '>= 0.4'} - - which-builtin-type@1.2.1: - resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} - engines: {node: '>= 0.4'} - - which-collection@1.0.2: - resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} - engines: {node: '>= 0.4'} - - which-typed-array@1.1.19: - resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} - engines: {node: '>= 0.4'} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - - widest-line@6.0.0: - resolution: {integrity: sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==} - engines: {node: '>=20'} - - with@7.0.2: - resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} - engines: {node: '>= 10.0.0'} - - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - - wrap-ansi@9.0.2: - resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} - engines: {node: '>=18'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - ws@8.18.3: - resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - - yaml@2.8.2: - resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} - engines: {node: '>= 14.6'} - hasBin: true - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - - yoga-layout@3.2.1: - resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} - - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - - zx@8.8.5: - resolution: {integrity: sha512-SNgDF5L0gfN7FwVOdEFguY3orU5AkfFZm9B5YSHog/UDHv+lvmd82ZAsOenOkQixigwH2+yyH198AwNdKhj+RA==} - engines: {node: '>= 12.17.0'} - hasBin: true - -snapshots: - - '@alcalzone/ansi-tokenize@0.2.5': - dependencies: - ansi-styles: 6.2.3 - is-fullwidth-code-point: 5.1.0 - - '@babel/code-frame@7.27.1': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.28.5': {} - - '@babel/core@7.28.5': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.5 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) - '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.5 - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.28.5': - dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/helper-compilation-targets@7.27.2': - dependencies: - '@babel/compat-data': 7.28.5 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.0 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-globals@7.28.0': {} - - '@babel/helper-module-imports@7.27.1': - dependencies: - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.28.5 - transitivePeerDependencies: - - supports-color - - '@babel/helper-string-parser@7.27.1': {} - - '@babel/helper-validator-identifier@7.28.5': {} - - '@babel/helper-validator-option@7.27.1': {} - - '@babel/helpers@7.28.4': - dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.5 - - '@babel/parser@7.28.5': - dependencies: - '@babel/types': 7.28.5 - - '@babel/parser@7.29.0': - dependencies: - '@babel/types': 7.29.0 - - '@babel/runtime@7.28.4': {} - - '@babel/template@7.27.2': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 - - '@babel/traverse@7.28.5': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.5 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.5 - '@babel/template': 7.27.2 - '@babel/types': 7.28.5 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.28.5': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - - '@babel/types@7.29.0': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - - '@bcoe/v8-coverage@1.0.2': {} - - '@biomejs/biome@2.4.8': - optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.4.8 - '@biomejs/cli-darwin-x64': 2.4.8 - '@biomejs/cli-linux-arm64': 2.4.8 - '@biomejs/cli-linux-arm64-musl': 2.4.8 - '@biomejs/cli-linux-x64': 2.4.8 - '@biomejs/cli-linux-x64-musl': 2.4.8 - '@biomejs/cli-win32-arm64': 2.4.8 - '@biomejs/cli-win32-x64': 2.4.8 - - '@biomejs/cli-darwin-arm64@2.4.8': - optional: true - - '@biomejs/cli-darwin-x64@2.4.8': - optional: true - - '@biomejs/cli-linux-arm64-musl@2.4.8': - optional: true - - '@biomejs/cli-linux-arm64@2.4.8': - optional: true - - '@biomejs/cli-linux-x64-musl@2.4.8': - optional: true - - '@biomejs/cli-linux-x64@2.4.8': - optional: true - - '@biomejs/cli-win32-arm64@2.4.8': - optional: true - - '@biomejs/cli-win32-x64@2.4.8': - optional: true - - '@changesets/apply-release-plan@7.1.0': - dependencies: - '@changesets/config': 3.1.3 - '@changesets/get-version-range-type': 0.4.0 - '@changesets/git': 3.0.4 - '@changesets/should-skip-package': 0.1.2 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - detect-indent: 6.1.0 - fs-extra: 7.0.1 - lodash.startcase: 4.4.0 - outdent: 0.5.0 - prettier: 2.8.8 - resolve-from: 5.0.0 - semver: 7.7.4 - - '@changesets/assemble-release-plan@6.0.9': - dependencies: - '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.3 - '@changesets/should-skip-package': 0.1.2 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - semver: 7.7.4 - - '@changesets/changelog-git@0.2.1': - dependencies: - '@changesets/types': 6.1.0 - - '@changesets/changelog-github@0.6.0': - dependencies: - '@changesets/get-github-info': 0.8.0 - '@changesets/types': 6.1.0 - dotenv: 8.6.0 - transitivePeerDependencies: - - encoding - - '@changesets/cli@2.30.0(@types/node@24.12.0)': - dependencies: - '@changesets/apply-release-plan': 7.1.0 - '@changesets/assemble-release-plan': 6.0.9 - '@changesets/changelog-git': 0.2.1 - '@changesets/config': 3.1.3 - '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.3 - '@changesets/get-release-plan': 4.0.15 - '@changesets/git': 3.0.4 - '@changesets/logger': 0.1.1 - '@changesets/pre': 2.0.2 - '@changesets/read': 0.6.7 - '@changesets/should-skip-package': 0.1.2 - '@changesets/types': 6.1.0 - '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.3(@types/node@24.12.0) - '@manypkg/get-packages': 1.1.3 - ansi-colors: 4.1.3 - enquirer: 2.4.1 - fs-extra: 7.0.1 - mri: 1.2.0 - package-manager-detector: 0.2.11 - picocolors: 1.1.1 - resolve-from: 5.0.0 - semver: 7.7.3 - spawndamnit: 3.0.1 - term-size: 2.2.1 - transitivePeerDependencies: - - '@types/node' - - '@changesets/config@3.1.3': - dependencies: - '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.3 - '@changesets/logger': 0.1.1 - '@changesets/should-skip-package': 0.1.2 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - fs-extra: 7.0.1 - micromatch: 4.0.8 - - '@changesets/errors@0.2.0': - dependencies: - extendable-error: 0.1.7 - - '@changesets/get-dependents-graph@2.1.3': - dependencies: - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - picocolors: 1.1.1 - semver: 7.7.4 - - '@changesets/get-github-info@0.8.0': - dependencies: - dataloader: 1.4.0 - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - - '@changesets/get-release-plan@4.0.15': - dependencies: - '@changesets/assemble-release-plan': 6.0.9 - '@changesets/config': 3.1.3 - '@changesets/pre': 2.0.2 - '@changesets/read': 0.6.7 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - - '@changesets/get-version-range-type@0.4.0': {} - - '@changesets/git@3.0.4': - dependencies: - '@changesets/errors': 0.2.0 - '@manypkg/get-packages': 1.1.3 - is-subdir: 1.2.0 - micromatch: 4.0.8 - spawndamnit: 3.0.1 - - '@changesets/logger@0.1.1': - dependencies: - picocolors: 1.1.1 - - '@changesets/parse@0.4.3': - dependencies: - '@changesets/types': 6.1.0 - js-yaml: 4.1.1 - - '@changesets/pre@2.0.2': - dependencies: - '@changesets/errors': 0.2.0 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - fs-extra: 7.0.1 - - '@changesets/read@0.6.7': - dependencies: - '@changesets/git': 3.0.4 - '@changesets/logger': 0.1.1 - '@changesets/parse': 0.4.3 - '@changesets/types': 6.1.0 - fs-extra: 7.0.1 - p-filter: 2.1.0 - picocolors: 1.1.1 - - '@changesets/should-skip-package@0.1.2': - dependencies: - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - - '@changesets/types@4.1.0': {} - - '@changesets/types@6.1.0': {} - - '@changesets/write@0.4.0': - dependencies: - '@changesets/types': 6.1.0 - fs-extra: 7.0.1 - human-id: 4.1.3 - prettier: 2.8.8 - - '@colors/colors@1.5.0': - optional: true - - '@dprint/formatter@0.4.1': {} - - '@dprint/typescript@0.91.8': {} - - '@effect/cli@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/printer-ansi@0.49.0(@effect/typeclass@0.40.0(effect@3.21.0))(effect@3.21.0))(@effect/printer@0.49.0(@effect/typeclass@0.40.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': - dependencies: - '@effect/platform': 0.96.0(effect@3.21.0) - '@effect/printer': 0.49.0(@effect/typeclass@0.40.0(effect@3.21.0))(effect@3.21.0) - '@effect/printer-ansi': 0.49.0(@effect/typeclass@0.40.0(effect@3.21.0))(effect@3.21.0) - effect: 3.21.0 - ini: 4.1.3 - toml: 3.0.0 - yaml: 2.8.2 - - '@effect/cluster@0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': - dependencies: - '@effect/platform': 0.96.0(effect@3.21.0) - '@effect/rpc': 0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0) - '@effect/sql': 0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0) - '@effect/workflow': 0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - effect: 3.21.0 - kubernetes-types: 1.30.0 - - '@effect/eslint-plugin@0.3.2': - dependencies: - '@dprint/formatter': 0.4.1 - '@dprint/typescript': 0.91.8 - prettier-linter-helpers: 1.0.0 - - '@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0)': - dependencies: - '@effect/platform': 0.96.0(effect@3.21.0) - effect: 3.21.0 - uuid: 11.1.0 - - '@effect/language-service@0.81.0': {} - - '@effect/platform-node-shared@0.57.1(@effect/cluster@0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.94.5(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': - dependencies: - '@effect/cluster': 0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - '@effect/platform': 0.94.5(effect@3.21.0) - '@effect/rpc': 0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0) - '@effect/sql': 0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0) - '@parcel/watcher': 2.5.1 - effect: 3.21.0 - multipasta: 0.2.7 - ws: 8.18.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@effect/platform-node-shared@0.59.0(@effect/cluster@0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': - dependencies: - '@effect/cluster': 0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - '@effect/platform': 0.96.0(effect@3.21.0) - '@effect/rpc': 0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0) - '@effect/sql': 0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0) - '@parcel/watcher': 2.5.1 - effect: 3.21.0 - multipasta: 0.2.7 - ws: 8.18.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@effect/platform-node@0.104.1(@effect/cluster@0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.94.5(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': - dependencies: - '@effect/cluster': 0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - '@effect/platform': 0.94.5(effect@3.21.0) - '@effect/platform-node-shared': 0.57.1(@effect/cluster@0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.94.5(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - '@effect/rpc': 0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0) - '@effect/sql': 0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0) - effect: 3.21.0 - mime: 3.0.0 - undici: 7.16.0 - ws: 8.18.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@effect/platform-node@0.106.0(@effect/cluster@0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': - dependencies: - '@effect/cluster': 0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - '@effect/platform': 0.96.0(effect@3.21.0) - '@effect/platform-node-shared': 0.59.0(@effect/cluster@0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - '@effect/rpc': 0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0) - '@effect/sql': 0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0) - effect: 3.21.0 - mime: 3.0.0 - undici: 7.16.0 - ws: 8.18.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@effect/platform@0.94.5(effect@3.21.0)': - dependencies: - effect: 3.21.0 - find-my-way-ts: 0.1.6 - msgpackr: 1.11.5 - multipasta: 0.2.7 - - '@effect/platform@0.96.0(effect@3.21.0)': - dependencies: - effect: 3.21.0 - find-my-way-ts: 0.1.6 - msgpackr: 1.11.5 - multipasta: 0.2.7 - - '@effect/printer-ansi@0.49.0(@effect/typeclass@0.40.0(effect@3.21.0))(effect@3.21.0)': - dependencies: - '@effect/printer': 0.49.0(@effect/typeclass@0.40.0(effect@3.21.0))(effect@3.21.0) - '@effect/typeclass': 0.40.0(effect@3.21.0) - effect: 3.21.0 - - '@effect/printer@0.49.0(@effect/typeclass@0.40.0(effect@3.21.0))(effect@3.21.0)': - dependencies: - '@effect/typeclass': 0.40.0(effect@3.21.0) - effect: 3.21.0 - - '@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0)': - dependencies: - '@effect/platform': 0.96.0(effect@3.21.0) - effect: 3.21.0 - msgpackr: 1.11.5 - - '@effect/schema@0.75.5(effect@3.21.0)': - dependencies: - effect: 3.21.0 - fast-check: 3.23.2 - - '@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0)': - dependencies: - '@effect/experimental': 0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0) - '@effect/platform': 0.96.0(effect@3.21.0) - effect: 3.21.0 - uuid: 11.1.0 - - '@effect/typeclass@0.40.0(effect@3.21.0)': - dependencies: - effect: 3.21.0 - - '@effect/vitest@0.29.0(effect@3.21.0)(vitest@4.1.0(@types/node@24.12.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)))': - dependencies: - effect: 3.21.0 - vitest: 4.1.0(@types/node@24.12.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) - - '@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': - dependencies: - '@effect/experimental': 0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0) - '@effect/platform': 0.96.0(effect@3.21.0) - '@effect/rpc': 0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0) - effect: 3.21.0 - - '@emnapi/core@1.7.1': - dependencies: - '@emnapi/wasi-threads': 1.1.0 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.7.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.1.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@esbuild/aix-ppc64@0.27.2': - optional: true - - '@esbuild/android-arm64@0.27.2': - optional: true - - '@esbuild/android-arm@0.27.2': - optional: true - - '@esbuild/android-x64@0.27.2': - optional: true - - '@esbuild/darwin-arm64@0.27.2': - optional: true - - '@esbuild/darwin-x64@0.27.2': - optional: true - - '@esbuild/freebsd-arm64@0.27.2': - optional: true - - '@esbuild/freebsd-x64@0.27.2': - optional: true - - '@esbuild/linux-arm64@0.27.2': - optional: true - - '@esbuild/linux-arm@0.27.2': - optional: true - - '@esbuild/linux-ia32@0.27.2': - optional: true - - '@esbuild/linux-loong64@0.27.2': - optional: true - - '@esbuild/linux-mips64el@0.27.2': - optional: true - - '@esbuild/linux-ppc64@0.27.2': - optional: true - - '@esbuild/linux-riscv64@0.27.2': - optional: true - - '@esbuild/linux-s390x@0.27.2': - optional: true - - '@esbuild/linux-x64@0.27.2': - optional: true - - '@esbuild/netbsd-arm64@0.27.2': - optional: true - - '@esbuild/netbsd-x64@0.27.2': - optional: true - - '@esbuild/openbsd-arm64@0.27.2': - optional: true - - '@esbuild/openbsd-x64@0.27.2': - optional: true - - '@esbuild/openharmony-arm64@0.27.2': - optional: true - - '@esbuild/sunos-x64@0.27.2': - optional: true - - '@esbuild/win32-arm64@0.27.2': - optional: true - - '@esbuild/win32-ia32@0.27.2': - optional: true - - '@esbuild/win32-x64@0.27.2': - optional: true - - '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.1.0(jiti@2.6.1))': - dependencies: - escape-string-regexp: 4.0.0 - eslint: 10.1.0(jiti@2.6.1) - ignore: 7.0.5 - - '@eslint-community/eslint-utils@4.9.1(eslint@10.1.0(jiti@2.6.1))': - dependencies: - eslint: 10.1.0(jiti@2.6.1) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} - - '@eslint/compat@2.0.3(eslint@10.1.0(jiti@2.6.1))': - dependencies: - '@eslint/core': 1.1.1 - optionalDependencies: - eslint: 10.1.0(jiti@2.6.1) - - '@eslint/config-array@0.23.3': - dependencies: - '@eslint/object-schema': 3.0.3 - debug: 4.4.3 - minimatch: 10.2.4 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.5.3': - dependencies: - '@eslint/core': 1.1.1 - - '@eslint/core@1.1.1': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/eslintrc@3.3.5': - dependencies: - ajv: 6.14.0 - debug: 4.4.3 - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.5 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@10.0.1(eslint@10.1.0(jiti@2.6.1))': - optionalDependencies: - eslint: 10.1.0(jiti@2.6.1) - - '@eslint/object-schema@3.0.3': {} - - '@eslint/plugin-kit@0.6.1': - dependencies: - '@eslint/core': 1.1.1 - levn: 0.4.1 - - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.7': - dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.4.3 - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.4.3': {} - - '@inquirer/external-editor@1.0.3(@types/node@24.12.0)': - dependencies: - chardet: 2.1.1 - iconv-lite: 0.7.0 - optionalDependencies: - '@types/node': 24.12.0 - - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.2 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - - '@jest/expect-utils@29.7.0': - dependencies: - jest-get-type: 29.6.3 - - '@jest/schemas@29.6.3': - dependencies: - '@sinclair/typebox': 0.27.8 - - '@jest/types@29.6.3': - dependencies: - '@jest/schemas': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 3.0.4 - '@types/node': 24.12.0 - '@types/yargs': 17.0.35 - chalk: 4.1.2 - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@jscpd/badge-reporter@4.0.4': - dependencies: - badgen: 3.2.3 - colors: 1.4.0 - fs-extra: 11.3.2 - - '@jscpd/core@4.0.4': - dependencies: - eventemitter3: 5.0.1 - - '@jscpd/finder@4.0.4': - dependencies: - '@jscpd/core': 4.0.4 - '@jscpd/tokenizer': 4.0.4 - blamer: 1.0.7 - bytes: 3.1.2 - cli-table3: 0.6.5 - colors: 1.4.0 - fast-glob: 3.3.3 - fs-extra: 11.3.2 - markdown-table: 2.0.0 - pug: 3.0.3 - - '@jscpd/html-reporter@4.0.4': - dependencies: - colors: 1.4.0 - fs-extra: 11.3.2 - pug: 3.0.3 - - '@jscpd/tokenizer@4.0.4': - dependencies: - '@jscpd/core': 4.0.4 - reprism: 0.0.11 - spark-md5: 3.0.2 - - '@manypkg/find-root@1.1.0': - dependencies: - '@babel/runtime': 7.28.4 - '@types/node': 12.20.55 - find-up: 4.1.0 - fs-extra: 8.1.0 - - '@manypkg/get-packages@1.1.3': - dependencies: - '@babel/runtime': 7.28.4 - '@changesets/types': 4.1.0 - '@manypkg/find-root': 1.1.0 - fs-extra: 8.1.0 - globby: 11.1.0 - read-yaml-file: 1.1.0 - - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': - optional: true - - '@napi-rs/wasm-runtime@0.2.12': - dependencies: - '@emnapi/core': 1.7.1 - '@emnapi/runtime': 1.7.1 - '@tybys/wasm-util': 0.10.1 - optional: true - - '@napi-rs/wasm-runtime@1.1.1': - dependencies: - '@emnapi/core': 1.7.1 - '@emnapi/runtime': 1.7.1 - '@tybys/wasm-util': 0.10.1 - optional: true - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 - - '@oxc-project/types@0.120.0': {} - - '@parcel/watcher-android-arm64@2.5.1': - optional: true - - '@parcel/watcher-darwin-arm64@2.5.1': - optional: true - - '@parcel/watcher-darwin-x64@2.5.1': - optional: true - - '@parcel/watcher-freebsd-x64@2.5.1': - optional: true - - '@parcel/watcher-linux-arm-glibc@2.5.1': - optional: true - - '@parcel/watcher-linux-arm-musl@2.5.1': - optional: true - - '@parcel/watcher-linux-arm64-glibc@2.5.1': - optional: true - - '@parcel/watcher-linux-arm64-musl@2.5.1': - optional: true - - '@parcel/watcher-linux-x64-glibc@2.5.1': - optional: true - - '@parcel/watcher-linux-x64-musl@2.5.1': - optional: true - - '@parcel/watcher-win32-arm64@2.5.1': - optional: true - - '@parcel/watcher-win32-ia32@2.5.1': - optional: true - - '@parcel/watcher-win32-x64@2.5.1': - optional: true - - '@parcel/watcher@2.5.1': - dependencies: - detect-libc: 1.0.3 - is-glob: 4.0.3 - micromatch: 4.0.8 - node-addon-api: 7.1.1 - optionalDependencies: - '@parcel/watcher-android-arm64': 2.5.1 - '@parcel/watcher-darwin-arm64': 2.5.1 - '@parcel/watcher-darwin-x64': 2.5.1 - '@parcel/watcher-freebsd-x64': 2.5.1 - '@parcel/watcher-linux-arm-glibc': 2.5.1 - '@parcel/watcher-linux-arm-musl': 2.5.1 - '@parcel/watcher-linux-arm64-glibc': 2.5.1 - '@parcel/watcher-linux-arm64-musl': 2.5.1 - '@parcel/watcher-linux-x64-glibc': 2.5.1 - '@parcel/watcher-linux-x64-musl': 2.5.1 - '@parcel/watcher-win32-arm64': 2.5.1 - '@parcel/watcher-win32-ia32': 2.5.1 - '@parcel/watcher-win32-x64': 2.5.1 - - '@pkgjs/parseargs@0.11.0': - optional: true - - '@pnpm/deps.graph-sequencer@1.0.0': {} - - '@prover-coder-ai/eslint-plugin-suggest-members@0.0.25(@effect/cluster@0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@effect/platform': 0.94.5(effect@3.21.0) - '@effect/platform-node': 0.104.1(@effect/cluster@0.58.0(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.94.5(effect@3.21.0))(@effect/rpc@0.75.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.51.0(@effect/experimental@0.60.0(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.96.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) - '@effect/schema': 0.75.5(effect@3.21.0) - '@typescript-eslint/utils': 8.55.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - effect: 3.21.0 - eslint: 10.1.0(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - '@effect/cluster' - - '@effect/rpc' - - '@effect/sql' - - bufferutil - - supports-color - - utf-8-validate - - '@rolldown/binding-android-arm64@1.0.0-rc.10': - optional: true - - '@rolldown/binding-darwin-arm64@1.0.0-rc.10': - optional: true - - '@rolldown/binding-darwin-x64@1.0.0-rc.10': - optional: true - - '@rolldown/binding-freebsd-x64@1.0.0-rc.10': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': - optional: true - - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': - optional: true - - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': - optional: true - - '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': - optional: true - - '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': - optional: true - - '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': - dependencies: - '@napi-rs/wasm-runtime': 1.1.1 - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': - optional: true - - '@rolldown/pluginutils@1.0.0-rc.10': {} - - '@rtsao/scc@1.1.0': {} - - '@sinclair/typebox@0.27.8': {} - - '@standard-schema/spec@1.1.0': {} - - '@ton-ai-core/vibecode-linter@1.0.11': - dependencies: - ajv: 8.17.1 - effect: 3.21.0 - jiti: 2.6.1 - jscpd: 4.0.8 - jscpd-sarif-reporter: 4.0.5 - loop-controls: 1.1.0 - ts-pattern: 5.9.0 - - '@ts-morph/common@0.28.1': - dependencies: - minimatch: 10.2.4 - path-browserify: 1.0.1 - tinyglobby: 0.2.15 - - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 - - '@types/babel__generator@7.27.0': - dependencies: - '@babel/types': 7.28.5 - - '@types/babel__template@7.4.4': - dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 - - '@types/babel__traverse@7.28.0': - dependencies: - '@babel/types': 7.28.5 - - '@types/chai@5.2.3': - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 - - '@types/dedent@0.7.0': {} - - '@types/deep-eql@4.0.2': {} - - '@types/eslint@8.56.12': - dependencies: - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - - '@types/esrecurse@4.3.1': {} - - '@types/estree@1.0.8': {} - - '@types/glob@7.1.3': - dependencies: - '@types/minimatch': 6.0.0 - '@types/node': 24.12.0 - - '@types/istanbul-lib-coverage@2.0.6': {} - - '@types/istanbul-lib-report@3.0.3': - dependencies: - '@types/istanbul-lib-coverage': 2.0.6 - - '@types/istanbul-reports@3.0.4': - dependencies: - '@types/istanbul-lib-report': 3.0.3 - - '@types/js-yaml@3.12.5': {} - - '@types/json-schema@7.0.15': {} - - '@types/json5@0.0.29': {} - - '@types/lodash@4.17.21': {} - - '@types/mdast@3.0.15': - dependencies: - '@types/unist': 2.0.11 - - '@types/minimatch@6.0.0': - dependencies: - minimatch: 10.2.4 - - '@types/node@12.20.55': {} - - '@types/node@24.12.0': - dependencies: - undici-types: 7.16.0 - - '@types/normalize-package-data@2.4.4': {} - - '@types/react@19.2.14': - dependencies: - csstype: 3.2.3 - - '@types/sarif@2.1.7': {} - - '@types/stack-utils@2.0.3': {} - - '@types/unist@2.0.11': {} - - '@types/yargs-parser@21.0.3': {} - - '@types/yargs@17.0.35': - dependencies: - '@types/yargs-parser': 21.0.3 - - '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/type-utils': 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.1 - eslint: 10.1.0(jiti@2.6.1) - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.1 - debug: 4.4.3 - eslint: 10.1.0(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.55.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.57.0(typescript@5.9.3) - '@typescript-eslint/types': 8.57.1 - debug: 4.4.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.57.1(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) - '@typescript-eslint/types': 8.57.1 - debug: 4.4.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@8.55.0': - dependencies: - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/visitor-keys': 8.55.0 - - '@typescript-eslint/scope-manager@8.57.1': - dependencies: - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/visitor-keys': 8.57.1 - - '@typescript-eslint/tsconfig-utils@8.55.0(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@typescript-eslint/tsconfig-utils@8.57.0(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@typescript-eslint/tsconfig-utils@8.57.1(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@typescript-eslint/type-utils@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - debug: 4.4.3 - eslint: 10.1.0(jiti@2.6.1) - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@8.55.0': {} - - '@typescript-eslint/types@8.57.1': {} - - '@typescript-eslint/typescript-estree@8.55.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.55.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/visitor-keys': 8.55.0 - debug: 4.4.3 - minimatch: 9.0.5 - semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.57.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/visitor-keys': 8.57.1 - debug: 4.4.3 - minimatch: 10.2.4 - semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.55.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.55.0 - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - eslint: 10.1.0(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - eslint: 10.1.0(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.55.0': - dependencies: - '@typescript-eslint/types': 8.55.0 - eslint-visitor-keys: 4.2.1 - - '@typescript-eslint/visitor-keys@8.57.1': - dependencies: - '@typescript-eslint/types': 8.57.1 - eslint-visitor-keys: 5.0.1 - - '@unrs/resolver-binding-android-arm-eabi@1.11.1': - optional: true - - '@unrs/resolver-binding-android-arm64@1.11.1': - optional: true - - '@unrs/resolver-binding-darwin-arm64@1.11.1': - optional: true - - '@unrs/resolver-binding-darwin-x64@1.11.1': - optional: true - - '@unrs/resolver-binding-freebsd-x64@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-wasm32-wasi@1.11.1': - dependencies: - '@napi-rs/wasm-runtime': 0.2.12 - optional: true - - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': - optional: true - - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': - optional: true - - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': - optional: true - - '@vitest/coverage-v8@4.1.0(vitest@4.1.0(@types/node@24.12.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)))': - dependencies: - '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.0 - ast-v8-to-istanbul: 1.0.0 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-reports: 3.2.0 - magicast: 0.5.2 - obug: 2.1.1 - std-env: 4.0.0 - tinyrainbow: 3.0.3 - vitest: 4.1.0(@types/node@24.12.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) - - '@vitest/eslint-plugin@1.6.13(@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.0(@types/node@24.12.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)))': - dependencies: - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.1.0(jiti@2.6.1) - optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - typescript: 5.9.3 - vitest: 4.1.0(@types/node@24.12.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) - transitivePeerDependencies: - - supports-color - - '@vitest/expect@4.1.0': - dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.0 - '@vitest/utils': 4.1.0 - chai: 6.2.2 - tinyrainbow: 3.0.3 - - '@vitest/mocker@4.1.0(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': - dependencies: - '@vitest/spy': 4.1.0 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) - - '@vitest/pretty-format@4.1.0': - dependencies: - tinyrainbow: 3.0.3 - - '@vitest/runner@4.1.0': - dependencies: - '@vitest/utils': 4.1.0 - pathe: 2.0.3 - - '@vitest/snapshot@4.1.0': - dependencies: - '@vitest/pretty-format': 4.1.0 - '@vitest/utils': 4.1.0 - magic-string: 0.30.21 - pathe: 2.0.3 - - '@vitest/spy@4.1.0': {} - - '@vitest/utils@4.1.0': - dependencies: - '@vitest/pretty-format': 4.1.0 - convert-source-map: 2.0.0 - tinyrainbow: 3.0.3 - - acorn-jsx@5.3.2(acorn@8.16.0): - dependencies: - acorn: 8.16.0 - - acorn@7.4.1: {} - - acorn@8.16.0: {} - - ajv@6.14.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ajv@8.17.1: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - - ansi-colors@4.1.3: {} - - ansi-escapes@7.3.0: - dependencies: - environment: 1.1.0 - - ansi-regex@5.0.1: {} - - ansi-regex@6.2.2: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@5.2.0: {} - - ansi-styles@6.2.3: {} - - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - - argparse@2.0.1: {} - - array-buffer-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - is-array-buffer: 3.0.5 - - array-includes@3.1.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - is-string: 1.1.1 - math-intrinsics: 1.1.0 - - array-union@2.1.0: {} - - array.prototype.findlastindex@1.2.6: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.1.0 - - array.prototype.flat@1.3.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-shim-unscopables: 1.1.0 - - array.prototype.flatmap@1.3.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-shim-unscopables: 1.1.0 - - arraybuffer.prototype.slice@1.0.4: - dependencies: - array-buffer-byte-length: 1.0.2 - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.5 - - asap@2.0.6: {} - - assert-never@1.4.0: {} - - assertion-error@2.0.1: {} - - ast-types@0.16.1: - dependencies: - tslib: 2.8.1 - - ast-v8-to-istanbul@1.0.0: - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - estree-walker: 3.0.3 - js-tokens: 10.0.0 - - async-function@1.0.0: {} - - auto-bind@5.0.1: {} - - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.1.0 - - babel-walk@3.0.0-canary-5: - dependencies: - '@babel/types': 7.28.5 - - badgen@3.2.3: {} - - balanced-match@1.0.2: {} - - balanced-match@4.0.4: {} - - baseline-browser-mapping@2.8.32: {} - - better-path-resolve@1.0.0: - dependencies: - is-windows: 1.0.2 - - blamer@1.0.7: - dependencies: - execa: 4.1.0 - which: 2.0.2 - - boolbase@1.0.0: {} - - brace-expansion@1.1.12: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.0.2: - dependencies: - balanced-match: 1.0.2 - - brace-expansion@5.0.4: - dependencies: - balanced-match: 4.0.4 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.28.0: - dependencies: - baseline-browser-mapping: 2.8.32 - caniuse-lite: 1.0.30001759 - electron-to-chromium: 1.5.263 - node-releases: 2.0.27 - update-browserslist-db: 1.1.4(browserslist@4.28.0) - - builtin-modules@3.3.0: {} - - builtin-modules@5.0.0: {} - - bytes@3.1.2: {} - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bind@1.0.8: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - - callsites@3.1.0: {} - - caniuse-lite@1.0.30001759: {} - - chai@6.2.2: {} - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - chalk@5.6.2: {} - - change-case@5.4.4: {} - - character-entities-legacy@1.1.4: {} - - character-entities@1.2.4: {} - - character-parser@2.2.0: - dependencies: - is-regex: 1.2.1 - - character-reference-invalid@1.1.4: {} - - chardet@2.1.1: {} - - cheerio-select@2.1.0: - dependencies: - boolbase: 1.0.0 - css-select: 5.2.2 - css-what: 6.2.2 - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - - cheerio@1.1.2: - dependencies: - cheerio-select: 2.1.0 - dom-serializer: 2.0.0 - domhandler: 5.0.3 - domutils: 3.2.2 - encoding-sniffer: 0.2.1 - htmlparser2: 10.0.0 - parse5: 7.3.0 - parse5-htmlparser2-tree-adapter: 7.1.0 - parse5-parser-stream: 7.1.2 - undici: 7.16.0 - whatwg-mimetype: 4.0.0 - - ci-info@3.9.0: {} - - ci-info@4.3.1: {} - - clean-regexp@1.0.0: - dependencies: - escape-string-regexp: 1.0.5 - - cli-boxes@3.0.0: {} - - cli-cursor@4.0.0: - dependencies: - restore-cursor: 4.0.0 - - cli-table3@0.6.5: - dependencies: - string-width: 4.2.3 - optionalDependencies: - '@colors/colors': 1.5.0 - - cli-truncate@5.2.0: - dependencies: - slice-ansi: 8.0.0 - string-width: 8.2.0 - - code-block-writer@13.0.3: {} - - code-excerpt@4.0.0: - dependencies: - convert-to-spaces: 2.0.1 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - colors@1.4.0: {} - - commander@5.1.0: {} - - concat-map@0.0.1: {} - - constantinople@4.0.1: - dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 - - convert-source-map@2.0.0: {} - - convert-to-spaces@2.0.1: {} - - core-js-compat@3.47.0: - dependencies: - browserslist: 4.28.0 - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - css-select@5.2.2: - dependencies: - boolbase: 1.0.0 - css-what: 6.2.2 - domhandler: 5.0.3 - domutils: 3.2.2 - nth-check: 2.1.1 - - css-what@6.2.2: {} - - csstype@3.2.3: {} - - data-view-buffer@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-offset@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - dataloader@1.4.0: {} - - debug@3.2.7: - dependencies: - ms: 2.1.3 - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - dedent@1.7.0: {} - - deep-is@0.1.4: {} - - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - - define-properties@1.2.1: - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - - detect-indent@6.1.0: {} - - detect-libc@1.0.3: {} - - detect-libc@2.1.2: {} - - diff-sequences@29.6.3: {} - - dir-glob@3.0.1: - dependencies: - path-type: 4.0.0 - - doctrine@2.1.0: - dependencies: - esutils: 2.0.3 - - doctypes@1.1.0: {} - - dom-serializer@2.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - - domelementtype@2.3.0: {} - - domhandler@5.0.3: - dependencies: - domelementtype: 2.3.0 - - domutils@3.2.2: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - - dotenv@8.6.0: {} - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - eastasianwidth@0.2.0: {} - - effect@3.21.0: - dependencies: - '@standard-schema/spec': 1.1.0 - fast-check: 3.23.2 - - electron-to-chromium@1.5.263: {} - - emoji-regex@10.6.0: {} - - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} - - encoding-sniffer@0.2.1: - dependencies: - iconv-lite: 0.6.3 - whatwg-encoding: 3.1.1 - - end-of-stream@1.4.5: - dependencies: - once: 1.4.0 - - enquirer@2.4.1: - dependencies: - ansi-colors: 4.1.3 - strip-ansi: 6.0.1 - - entities@4.5.0: {} - - entities@6.0.1: {} - - environment@1.1.0: {} - - error-ex@1.3.4: - dependencies: - is-arrayish: 0.2.1 - - es-abstract@1.24.0: - dependencies: - array-buffer-byte-length: 1.0.2 - arraybuffer.prototype.slice: 1.0.4 - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - data-view-buffer: 1.0.2 - data-view-byte-length: 1.0.2 - data-view-byte-offset: 1.0.1 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 - function.prototype.name: 1.1.8 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - get-symbol-description: 1.1.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - internal-slot: 1.1.0 - is-array-buffer: 3.0.5 - is-callable: 1.2.7 - is-data-view: 1.0.2 - is-negative-zero: 2.0.3 - is-regex: 1.2.1 - is-set: 2.0.3 - is-shared-array-buffer: 1.0.4 - is-string: 1.1.1 - is-typed-array: 1.1.15 - is-weakref: 1.1.1 - math-intrinsics: 1.1.0 - object-inspect: 1.13.4 - object-keys: 1.1.1 - object.assign: 4.1.7 - own-keys: 1.0.1 - regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.3 - safe-push-apply: 1.0.0 - safe-regex-test: 1.1.0 - set-proto: 1.0.0 - stop-iteration-iterator: 1.1.0 - string.prototype.trim: 1.2.10 - string.prototype.trimend: 1.0.9 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.3 - typed-array-byte-length: 1.0.3 - typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.7 - unbox-primitive: 1.1.0 - which-typed-array: 1.1.19 - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-module-lexer@2.0.0: {} - - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - es-shim-unscopables@1.1.0: - dependencies: - hasown: 2.0.2 - - es-to-primitive@1.3.0: - dependencies: - is-callable: 1.2.7 - is-date-object: 1.1.0 - is-symbol: 1.1.1 - - es-toolkit@1.44.0: {} - - esbuild@0.27.2: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.2 - '@esbuild/android-arm': 0.27.2 - '@esbuild/android-arm64': 0.27.2 - '@esbuild/android-x64': 0.27.2 - '@esbuild/darwin-arm64': 0.27.2 - '@esbuild/darwin-x64': 0.27.2 - '@esbuild/freebsd-arm64': 0.27.2 - '@esbuild/freebsd-x64': 0.27.2 - '@esbuild/linux-arm': 0.27.2 - '@esbuild/linux-arm64': 0.27.2 - '@esbuild/linux-ia32': 0.27.2 - '@esbuild/linux-loong64': 0.27.2 - '@esbuild/linux-mips64el': 0.27.2 - '@esbuild/linux-ppc64': 0.27.2 - '@esbuild/linux-riscv64': 0.27.2 - '@esbuild/linux-s390x': 0.27.2 - '@esbuild/linux-x64': 0.27.2 - '@esbuild/netbsd-arm64': 0.27.2 - '@esbuild/netbsd-x64': 0.27.2 - '@esbuild/openbsd-arm64': 0.27.2 - '@esbuild/openbsd-x64': 0.27.2 - '@esbuild/openharmony-arm64': 0.27.2 - '@esbuild/sunos-x64': 0.27.2 - '@esbuild/win32-arm64': 0.27.2 - '@esbuild/win32-ia32': 0.27.2 - '@esbuild/win32-x64': 0.27.2 - optional: true - - escalade@3.2.0: {} - - escape-string-regexp@1.0.5: {} - - escape-string-regexp@2.0.0: {} - - escape-string-regexp@4.0.0: {} - - eslint-import-context@0.1.9(unrs-resolver@1.11.1): - dependencies: - get-tsconfig: 4.13.0 - stable-hash-x: 0.2.0 - optionalDependencies: - unrs-resolver: 1.11.1 - - eslint-import-resolver-node@0.3.9: - dependencies: - debug: 3.2.7 - is-core-module: 2.16.1 - resolve: 1.22.11 - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@10.1.0(jiti@2.6.1)): - dependencies: - debug: 4.4.3 - eslint: 10.1.0(jiti@2.6.1) - eslint-import-context: 0.1.9(unrs-resolver@1.11.1) - get-tsconfig: 4.13.0 - is-bun-module: 2.0.0 - stable-hash-x: 0.2.0 - tinyglobby: 0.2.15 - unrs-resolver: 1.11.1 - optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.1.0(jiti@2.6.1) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@10.1.0(jiti@2.6.1)) - transitivePeerDependencies: - - supports-color - - eslint-plugin-codegen@0.34.1(eslint@10.1.0(jiti@2.6.1)): - dependencies: - '@babel/core': 7.28.5 - '@babel/generator': 7.28.5 - '@babel/parser': 7.28.5 - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 - '@pnpm/deps.graph-sequencer': 1.0.0 - '@types/babel__core': 7.20.5 - '@types/babel__generator': 7.27.0 - '@types/dedent': 0.7.0 - '@types/eslint': 8.56.12 - '@types/glob': 7.1.3 - '@types/js-yaml': 3.12.5 - '@types/lodash': 4.17.21 - cheerio: 1.1.2 - dedent: 1.7.0 - eslint-plugin-markdown: 4.0.1(eslint@10.1.0(jiti@2.6.1)) - expect: 29.7.0 - fp-ts: 2.16.11 - glob: 10.5.0 - io-ts: 2.2.22(fp-ts@2.16.11) - io-ts-extra: 0.11.6 - js-yaml: 3.14.2 - lodash: 4.17.21 - ms: 2.1.3 - read-pkg-up: 7.0.1 - recast: 0.23.11 - safe-stringify: 1.2.0 - strip-ansi: 6.0.1 - zod: 3.25.76 - zx: 8.8.5 - transitivePeerDependencies: - - babel-plugin-macros - - eslint - - supports-color - - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 10.1.0(jiti@2.6.1) - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)) - hasown: 2.0.2 - is-core-module: 2.16.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-markdown@4.0.1(eslint@10.1.0(jiti@2.6.1)): - dependencies: - eslint: 10.1.0(jiti@2.6.1) - mdast-util-from-markdown: 0.8.5 - transitivePeerDependencies: - - supports-color - - eslint-plugin-simple-import-sort@12.1.1(eslint@10.1.0(jiti@2.6.1)): - dependencies: - eslint: 10.1.0(jiti@2.6.1) - - eslint-plugin-sonarjs@4.0.2(eslint@10.1.0(jiti@2.6.1)): - dependencies: - '@eslint-community/regexpp': 4.12.2 - builtin-modules: 3.3.0 - bytes: 3.1.2 - eslint: 10.1.0(jiti@2.6.1) - functional-red-black-tree: 1.0.1 - globals: 17.4.0 - jsx-ast-utils-x: 0.1.0 - lodash.merge: 4.6.2 - minimatch: 10.2.4 - scslre: 0.3.0 - semver: 7.7.4 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - - eslint-plugin-sort-destructure-keys@3.0.0(eslint@10.1.0(jiti@2.6.1)): - dependencies: - eslint: 10.1.0(jiti@2.6.1) - natural-compare-lite: 1.4.0 - - eslint-plugin-unicorn@63.0.0(eslint@10.1.0(jiti@2.6.1)): - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) - change-case: 5.4.4 - ci-info: 4.3.1 - clean-regexp: 1.0.0 - core-js-compat: 3.47.0 - eslint: 10.1.0(jiti@2.6.1) - find-up-simple: 1.0.1 - globals: 16.5.0 - indent-string: 5.0.0 - is-builtin-module: 5.0.0 - jsesc: 3.1.0 - pluralize: 8.0.0 - regexp-tree: 0.1.27 - regjsparser: 0.13.0 - semver: 7.7.3 - strip-indent: 4.1.1 - - eslint-scope@9.1.2: - dependencies: - '@types/esrecurse': 4.3.1 - '@types/estree': 1.0.8 - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.2.1: {} - - eslint-visitor-keys@5.0.1: {} - - eslint@10.1.0(jiti@2.6.1): - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.3 - '@eslint/config-helpers': 0.5.3 - '@eslint/core': 1.1.1 - '@eslint/plugin-kit': 0.6.1 - '@humanfs/node': 0.16.7 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - ajv: 6.14.0 - cross-spawn: 7.0.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint-scope: 9.1.2 - eslint-visitor-keys: 5.0.1 - espree: 11.2.0 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.4 - natural-compare: 1.4.0 - optionator: 0.9.4 - optionalDependencies: - jiti: 2.6.1 - transitivePeerDependencies: - - supports-color - - espree@10.4.0: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 4.2.1 - - espree@11.2.0: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 5.0.1 - - esprima@4.0.1: {} - - esquery@1.7.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.8 - - esutils@2.0.3: {} - - eventemitter3@5.0.1: {} - - execa@4.1.0: - dependencies: - cross-spawn: 7.0.6 - get-stream: 5.2.0 - human-signals: 1.1.1 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - - expect-type@1.3.0: {} - - expect@29.7.0: - dependencies: - '@jest/expect-utils': 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - - extendable-error@0.1.7: {} - - fast-check@3.23.2: - dependencies: - pure-rand: 6.1.0 - - fast-deep-equal@3.1.3: {} - - fast-diff@1.3.0: {} - - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fast-uri@3.1.0: {} - - fastq@1.19.1: - dependencies: - reusify: 1.1.0 - - fdir@6.5.0(picomatch@4.0.3): - optionalDependencies: - picomatch: 4.0.3 - - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - find-my-way-ts@0.1.6: {} - - find-up-simple@1.0.1: {} - - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@4.0.1: - dependencies: - flatted: 3.3.3 - keyv: 4.5.4 - - flatted@3.3.3: {} - - for-each@0.3.5: - dependencies: - is-callable: 1.2.7 - - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - - fp-ts@2.16.11: {} - - fs-extra@11.3.2: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.2.0 - universalify: 2.0.1 - - fs-extra@7.0.1: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 - - fs-extra@8.1.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - function.prototype.name@1.1.8: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - functions-have-names: 1.2.3 - hasown: 2.0.2 - is-callable: 1.2.7 - - functional-red-black-tree@1.0.1: {} - - functions-have-names@1.2.3: {} - - generator-function@2.0.1: {} - - gensync@1.0.0-beta.2: {} - - get-east-asian-width@1.4.0: {} - - get-east-asian-width@1.5.0: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - - get-stream@5.2.0: - dependencies: - pump: 3.0.3 - - get-symbol-description@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - - get-tsconfig@4.13.0: - dependencies: - resolve-pkg-maps: 1.0.0 - - gitignore-to-glob@0.3.0: {} - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - glob@10.5.0: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - - globals@14.0.0: {} - - globals@16.5.0: {} - - globals@17.4.0: {} - - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 - - globby@11.1.0: - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.3 - ignore: 5.3.2 - merge2: 1.4.1 - slash: 3.0.0 - - globrex@0.1.2: {} - - gopd@1.2.0: {} - - graceful-fs@4.2.11: {} - - has-bigints@1.1.0: {} - - has-flag@4.0.0: {} - - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - - has-proto@1.2.0: - dependencies: - dunder-proto: 1.0.1 - - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - hosted-git-info@2.8.9: {} - - html-escaper@2.0.2: {} - - htmlparser2@10.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - entities: 6.0.1 - - human-id@4.1.3: {} - - human-signals@1.1.1: {} - - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - - iconv-lite@0.7.0: - dependencies: - safer-buffer: 2.1.2 - - ignore@5.3.2: {} - - ignore@7.0.5: {} - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - imurmurhash@0.1.4: {} - - indent-string@5.0.0: {} - - ini@4.1.3: {} - - ink@6.8.0(@types/react@19.2.14)(react@19.2.4): - dependencies: - '@alcalzone/ansi-tokenize': 0.2.5 - ansi-escapes: 7.3.0 - ansi-styles: 6.2.3 - auto-bind: 5.0.1 - chalk: 5.6.2 - cli-boxes: 3.0.0 - cli-cursor: 4.0.0 - cli-truncate: 5.2.0 - code-excerpt: 4.0.0 - es-toolkit: 1.44.0 - indent-string: 5.0.0 - is-in-ci: 2.0.0 - patch-console: 2.0.0 - react: 19.2.4 - react-reconciler: 0.33.0(react@19.2.4) - scheduler: 0.27.0 - signal-exit: 3.0.7 - slice-ansi: 8.0.0 - stack-utils: 2.0.6 - string-width: 8.2.0 - terminal-size: 4.0.1 - type-fest: 5.4.4 - widest-line: 6.0.0 - wrap-ansi: 9.0.2 - ws: 8.18.3 - yoga-layout: 3.2.1 - optionalDependencies: - '@types/react': 19.2.14 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - internal-slot@1.1.0: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.1.0 - - io-ts-extra@0.11.6: - dependencies: - fp-ts: 2.16.11 - io-ts: 2.2.22(fp-ts@2.16.11) - - io-ts@2.2.22(fp-ts@2.16.11): - dependencies: - fp-ts: 2.16.11 - - is-alphabetical@1.0.4: {} - - is-alphanumerical@1.0.4: - dependencies: - is-alphabetical: 1.0.4 - is-decimal: 1.0.4 - - is-array-buffer@3.0.5: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - is-arrayish@0.2.1: {} - - is-async-function@2.1.1: - dependencies: - async-function: 1.0.0 - call-bound: 1.0.4 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-bigint@1.1.0: - dependencies: - has-bigints: 1.1.0 - - is-boolean-object@1.2.2: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-builtin-module@5.0.0: - dependencies: - builtin-modules: 5.0.0 - - is-bun-module@2.0.0: - dependencies: - semver: 7.7.4 - - is-callable@1.2.7: {} - - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 - - is-data-view@1.0.2: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-typed-array: 1.1.15 - - is-date-object@1.1.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-decimal@1.0.4: {} - - is-expression@4.0.0: - dependencies: - acorn: 7.4.1 - object-assign: 4.1.1 - - is-extglob@2.1.1: {} - - is-finalizationregistry@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-fullwidth-code-point@3.0.0: {} - - is-fullwidth-code-point@5.1.0: - dependencies: - get-east-asian-width: 1.4.0 - - is-generator-function@1.1.2: - dependencies: - call-bound: 1.0.4 - generator-function: 2.0.1 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-hexadecimal@1.0.4: {} - - is-in-ci@2.0.0: {} - - is-map@2.0.3: {} - - is-negative-zero@2.0.3: {} - - is-number-object@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-number@7.0.0: {} - - is-promise@2.2.2: {} - - is-regex@1.2.1: - dependencies: - call-bound: 1.0.4 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - is-set@2.0.3: {} - - is-shared-array-buffer@1.0.4: - dependencies: - call-bound: 1.0.4 - - is-stream@2.0.1: {} - - is-string@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-subdir@1.2.0: - dependencies: - better-path-resolve: 1.0.0 - - is-symbol@1.1.1: - dependencies: - call-bound: 1.0.4 - has-symbols: 1.1.0 - safe-regex-test: 1.1.0 - - is-typed-array@1.1.15: - dependencies: - which-typed-array: 1.1.19 - - is-weakmap@2.0.2: {} - - is-weakref@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-weakset@2.0.4: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - is-windows@1.0.2: {} - - isarray@2.0.5: {} - - isexe@2.0.0: {} - - istanbul-lib-coverage@3.2.2: {} - - istanbul-lib-report@3.0.1: - dependencies: - istanbul-lib-coverage: 3.2.2 - make-dir: 4.0.0 - supports-color: 7.2.0 - - istanbul-reports@3.2.0: - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.1 - - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - - jest-diff@29.7.0: - dependencies: - chalk: 4.1.2 - diff-sequences: 29.6.3 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-get-type@29.6.3: {} - - jest-matcher-utils@29.7.0: - dependencies: - chalk: 4.1.2 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-message-util@29.7.0: - dependencies: - '@babel/code-frame': 7.27.1 - '@jest/types': 29.6.3 - '@types/stack-utils': 2.0.3 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - stack-utils: 2.0.6 - - jest-util@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/node': 24.12.0 - chalk: 4.1.2 - ci-info: 3.9.0 - graceful-fs: 4.2.11 - picomatch: 2.3.1 - - jiti@2.6.1: {} - - js-stringify@1.0.2: {} - - js-tokens@10.0.0: {} - - js-tokens@4.0.0: {} - - js-yaml@3.14.2: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 - - jscpd-sarif-reporter@4.0.5: - dependencies: - colors: 1.4.0 - fs-extra: 11.3.2 - node-sarif-builder: 3.4.0 - - jscpd-sarif-reporter@4.0.6: - dependencies: - colors: 1.4.0 - fs-extra: 11.3.2 - node-sarif-builder: 3.4.0 - - jscpd@4.0.8: - dependencies: - '@jscpd/badge-reporter': 4.0.4 - '@jscpd/core': 4.0.4 - '@jscpd/finder': 4.0.4 - '@jscpd/html-reporter': 4.0.4 - '@jscpd/tokenizer': 4.0.4 - colors: 1.4.0 - commander: 5.1.0 - fs-extra: 11.3.2 - gitignore-to-glob: 0.3.0 - jscpd-sarif-reporter: 4.0.6 - - jsesc@3.1.0: {} - - json-buffer@3.0.1: {} - - json-parse-even-better-errors@2.3.1: {} - - json-schema-traverse@0.4.1: {} - - json-schema-traverse@1.0.0: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - json5@1.0.2: - dependencies: - minimist: 1.2.8 - - json5@2.2.3: {} - - jsonfile@4.0.0: - optionalDependencies: - graceful-fs: 4.2.11 - - jsonfile@6.2.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - - jstransformer@1.0.0: - dependencies: - is-promise: 2.2.2 - promise: 7.3.1 - - jsx-ast-utils-x@0.1.0: {} - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - kubernetes-types@1.30.0: {} - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - lightningcss-android-arm64@1.32.0: - optional: true - - lightningcss-darwin-arm64@1.32.0: - optional: true - - lightningcss-darwin-x64@1.32.0: - optional: true - - lightningcss-freebsd-x64@1.32.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.32.0: - optional: true - - lightningcss-linux-arm64-gnu@1.32.0: - optional: true - - lightningcss-linux-arm64-musl@1.32.0: - optional: true - - lightningcss-linux-x64-gnu@1.32.0: - optional: true - - lightningcss-linux-x64-musl@1.32.0: - optional: true - - lightningcss-win32-arm64-msvc@1.32.0: - optional: true - - lightningcss-win32-x64-msvc@1.32.0: - optional: true - - lightningcss@1.32.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.32.0 - lightningcss-darwin-arm64: 1.32.0 - lightningcss-darwin-x64: 1.32.0 - lightningcss-freebsd-x64: 1.32.0 - lightningcss-linux-arm-gnueabihf: 1.32.0 - lightningcss-linux-arm64-gnu: 1.32.0 - lightningcss-linux-arm64-musl: 1.32.0 - lightningcss-linux-x64-gnu: 1.32.0 - lightningcss-linux-x64-musl: 1.32.0 - lightningcss-win32-arm64-msvc: 1.32.0 - lightningcss-win32-x64-msvc: 1.32.0 - - lines-and-columns@1.2.4: {} - - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash.merge@4.6.2: {} - - lodash.startcase@4.4.0: {} - - lodash@4.17.21: {} - - loop-controls@1.1.0: {} - - lru-cache@10.4.3: {} - - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - magicast@0.5.2: - dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - source-map-js: 1.2.1 - - make-dir@4.0.0: - dependencies: - semver: 7.7.4 - - markdown-table@2.0.0: - dependencies: - repeat-string: 1.6.1 - - math-intrinsics@1.1.0: {} - - mdast-util-from-markdown@0.8.5: - dependencies: - '@types/mdast': 3.0.15 - mdast-util-to-string: 2.0.0 - micromark: 2.11.4 - parse-entities: 2.0.0 - unist-util-stringify-position: 2.0.3 - transitivePeerDependencies: - - supports-color - - mdast-util-to-string@2.0.0: {} - - merge-stream@2.0.0: {} - - merge2@1.4.1: {} - - micromark@2.11.4: - dependencies: - debug: 4.4.3 - parse-entities: 2.0.0 - transitivePeerDependencies: - - supports-color - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - mime@3.0.0: {} - - mimic-fn@2.1.0: {} - - minimatch@10.2.4: - dependencies: - brace-expansion: 5.0.4 - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.12 - - minimatch@3.1.5: - dependencies: - brace-expansion: 1.1.12 - - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.2 - - minimist@1.2.8: {} - - minipass@7.1.2: {} - - mri@1.2.0: {} - - ms@2.1.3: {} - - msgpackr-extract@3.0.3: - dependencies: - node-gyp-build-optional-packages: 5.2.2 - optionalDependencies: - '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 - optional: true - - msgpackr@1.11.5: - optionalDependencies: - msgpackr-extract: 3.0.3 - - multipasta@0.2.7: {} - - nanoid@3.3.11: {} - - napi-postinstall@0.3.4: {} - - natural-compare-lite@1.4.0: {} - - natural-compare@1.4.0: {} - - node-addon-api@7.1.1: {} - - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - - node-gyp-build-optional-packages@5.2.2: - dependencies: - detect-libc: 2.1.2 - optional: true - - node-releases@2.0.27: {} - - node-sarif-builder@3.4.0: - dependencies: - '@types/sarif': 2.1.7 - fs-extra: 11.3.2 - - normalize-package-data@2.5.0: - dependencies: - hosted-git-info: 2.8.9 - resolve: 1.22.11 - semver: 5.7.2 - validate-npm-package-license: 3.0.4 - - npm-run-path@4.0.1: - dependencies: - path-key: 3.1.1 - - nth-check@2.1.1: - dependencies: - boolbase: 1.0.0 - - object-assign@4.1.1: {} - - object-inspect@1.13.4: {} - - object-keys@1.1.1: {} - - object.assign@4.1.7: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - has-symbols: 1.1.0 - object-keys: 1.1.1 - - object.fromentries@2.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 - - object.groupby@1.0.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - - object.values@1.2.1: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - obug@2.1.1: {} - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - outdent@0.5.0: {} - - own-keys@1.0.1: - dependencies: - get-intrinsic: 1.3.0 - object-keys: 1.1.1 - safe-push-apply: 1.0.0 - - p-filter@2.1.0: - dependencies: - p-map: 2.1.0 - - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - p-map@2.1.0: {} - - p-try@2.2.0: {} - - package-json-from-dist@1.0.1: {} - - package-manager-detector@0.2.11: - dependencies: - quansync: 0.2.11 - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - parse-entities@2.0.0: - dependencies: - character-entities: 1.2.4 - character-entities-legacy: 1.1.4 - character-reference-invalid: 1.1.4 - is-alphanumerical: 1.0.4 - is-decimal: 1.0.4 - is-hexadecimal: 1.0.4 - - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.27.1 - error-ex: 1.3.4 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - - parse5-htmlparser2-tree-adapter@7.1.0: - dependencies: - domhandler: 5.0.3 - parse5: 7.3.0 - - parse5-parser-stream@7.1.2: - dependencies: - parse5: 7.3.0 - - parse5@7.3.0: - dependencies: - entities: 6.0.1 - - patch-console@2.0.0: {} - - path-browserify@1.0.1: {} - - path-exists@4.0.0: {} - - path-key@3.1.1: {} - - path-parse@1.0.7: {} - - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 - - path-type@4.0.0: {} - - pathe@2.0.3: {} - - picocolors@1.1.1: {} - - picomatch@2.3.1: {} - - picomatch@4.0.3: {} - - pify@4.0.1: {} - - pluralize@8.0.0: {} - - possible-typed-array-names@1.1.0: {} - - postcss@8.5.8: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prelude-ls@1.2.1: {} - - prettier-linter-helpers@1.0.0: - dependencies: - fast-diff: 1.3.0 - - prettier@2.8.8: {} - - pretty-format@29.7.0: - dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.3.1 - - promise@7.3.1: - dependencies: - asap: 2.0.6 - - pug-attrs@3.0.0: - dependencies: - constantinople: 4.0.1 - js-stringify: 1.0.2 - pug-runtime: 3.0.1 - - pug-code-gen@3.0.3: - dependencies: - constantinople: 4.0.1 - doctypes: 1.1.0 - js-stringify: 1.0.2 - pug-attrs: 3.0.0 - pug-error: 2.1.0 - pug-runtime: 3.0.1 - void-elements: 3.1.0 - with: 7.0.2 - - pug-error@2.1.0: {} - - pug-filters@4.0.0: - dependencies: - constantinople: 4.0.1 - jstransformer: 1.0.0 - pug-error: 2.1.0 - pug-walk: 2.0.0 - resolve: 1.22.11 - - pug-lexer@5.0.1: - dependencies: - character-parser: 2.2.0 - is-expression: 4.0.0 - pug-error: 2.1.0 - - pug-linker@4.0.0: - dependencies: - pug-error: 2.1.0 - pug-walk: 2.0.0 - - pug-load@3.0.0: - dependencies: - object-assign: 4.1.1 - pug-walk: 2.0.0 - - pug-parser@6.0.0: - dependencies: - pug-error: 2.1.0 - token-stream: 1.0.0 - - pug-runtime@3.0.1: {} - - pug-strip-comments@2.0.0: - dependencies: - pug-error: 2.1.0 - - pug-walk@2.0.0: {} - - pug@3.0.3: - dependencies: - pug-code-gen: 3.0.3 - pug-filters: 4.0.0 - pug-lexer: 5.0.1 - pug-linker: 4.0.0 - pug-load: 3.0.0 - pug-parser: 6.0.0 - pug-runtime: 3.0.1 - pug-strip-comments: 2.0.0 - - pump@3.0.3: - dependencies: - end-of-stream: 1.4.5 - once: 1.4.0 - - punycode@2.3.1: {} - - pure-rand@6.1.0: {} - - quansync@0.2.11: {} - - queue-microtask@1.2.3: {} - - react-is@18.3.1: {} - - react-reconciler@0.33.0(react@19.2.4): - dependencies: - react: 19.2.4 - scheduler: 0.27.0 - - react@19.2.4: {} - - read-pkg-up@7.0.1: - dependencies: - find-up: 4.1.0 - read-pkg: 5.2.0 - type-fest: 0.8.1 - - read-pkg@5.2.0: - dependencies: - '@types/normalize-package-data': 2.4.4 - normalize-package-data: 2.5.0 - parse-json: 5.2.0 - type-fest: 0.6.0 - - read-yaml-file@1.1.0: - dependencies: - graceful-fs: 4.2.11 - js-yaml: 3.14.2 - pify: 4.0.1 - strip-bom: 3.0.0 - - recast@0.23.11: - dependencies: - ast-types: 0.16.1 - esprima: 4.0.1 - source-map: 0.6.1 - tiny-invariant: 1.3.3 - tslib: 2.8.1 - - refa@0.12.1: - dependencies: - '@eslint-community/regexpp': 4.12.2 - - reflect.getprototypeof@1.0.10: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - which-builtin-type: 1.2.1 - - regexp-ast-analysis@0.7.1: - dependencies: - '@eslint-community/regexpp': 4.12.2 - refa: 0.12.1 - - regexp-tree@0.1.27: {} - - regexp.prototype.flags@1.5.4: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-errors: 1.3.0 - get-proto: 1.0.1 - gopd: 1.2.0 - set-function-name: 2.0.2 - - regjsparser@0.13.0: - dependencies: - jsesc: 3.1.0 - - repeat-string@1.6.1: {} - - reprism@0.0.11: {} - - require-from-string@2.0.2: {} - - resolve-from@4.0.0: {} - - resolve-from@5.0.0: {} - - resolve-pkg-maps@1.0.0: {} - - resolve@1.22.11: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - restore-cursor@4.0.0: - dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - - reusify@1.1.0: {} - - rolldown@1.0.0-rc.10: - dependencies: - '@oxc-project/types': 0.120.0 - '@rolldown/pluginutils': 1.0.0-rc.10 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.10 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.10 - '@rolldown/binding-darwin-x64': 1.0.0-rc.10 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.10 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.10 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.10 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.10 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.10 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.10 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.10 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.10 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.10 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.10 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.10 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.10 - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - safe-array-concat@1.1.3: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - isarray: 2.0.5 - - safe-push-apply@1.0.0: - dependencies: - es-errors: 1.3.0 - isarray: 2.0.5 - - safe-regex-test@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-regex: 1.2.1 - - safe-stringify@1.2.0: {} - - safer-buffer@2.1.2: {} - - scheduler@0.27.0: {} - - scslre@0.3.0: - dependencies: - '@eslint-community/regexpp': 4.12.2 - refa: 0.12.1 - regexp-ast-analysis: 0.7.1 - - semver@5.7.2: {} - - semver@6.3.1: {} - - semver@7.7.3: {} - - semver@7.7.4: {} - - set-function-length@1.2.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - - set-function-name@2.0.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - - set-proto@1.0.0: - dependencies: - dunder-proto: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - side-channel-list@1.0.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.0 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - - siginfo@2.0.0: {} - - signal-exit@3.0.7: {} - - signal-exit@4.1.0: {} - - slash@3.0.0: {} - - slice-ansi@8.0.0: - dependencies: - ansi-styles: 6.2.3 - is-fullwidth-code-point: 5.1.0 - - source-map-js@1.2.1: {} - - source-map@0.6.1: {} - - spark-md5@3.0.2: {} - - spawndamnit@3.0.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - - spdx-correct@3.2.0: - dependencies: - spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.22 - - spdx-exceptions@2.5.0: {} - - spdx-expression-parse@3.0.1: - dependencies: - spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.22 - - spdx-license-ids@3.0.22: {} - - sprintf-js@1.0.3: {} - - stable-hash-x@0.2.0: {} - - stack-utils@2.0.6: - dependencies: - escape-string-regexp: 2.0.0 - - stackback@0.0.2: {} - - std-env@4.0.0: {} - - stop-iteration-iterator@1.1.0: - dependencies: - es-errors: 1.3.0 - internal-slot: 1.1.0 - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.2 - - string-width@7.2.0: - dependencies: - emoji-regex: 10.6.0 - get-east-asian-width: 1.4.0 - strip-ansi: 7.1.2 - - string-width@8.2.0: - dependencies: - get-east-asian-width: 1.5.0 - strip-ansi: 7.1.2 - - string.prototype.trim@1.2.10: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-data-property: 1.1.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 - has-property-descriptors: 1.0.2 - - string.prototype.trimend@1.0.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - string.prototype.trimstart@1.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-ansi@7.1.2: - dependencies: - ansi-regex: 6.2.2 - - strip-bom@3.0.0: {} - - strip-final-newline@2.0.0: {} - - strip-indent@4.1.1: {} - - strip-json-comments@3.1.1: {} - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - - tagged-tag@1.0.0: {} - - term-size@2.2.1: {} - - terminal-size@4.0.1: {} - - tiny-invariant@1.3.3: {} - - tinybench@2.9.0: {} - - tinyexec@1.0.2: {} - - tinyglobby@0.2.15: - dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - - tinyrainbow@3.0.3: {} - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - token-stream@1.0.0: {} - - toml@3.0.0: {} - - tr46@0.0.3: {} - - ts-api-utils@2.4.0(typescript@5.9.3): - dependencies: - typescript: 5.9.3 - - ts-morph@27.0.2: - dependencies: - '@ts-morph/common': 0.28.1 - code-block-writer: 13.0.3 - - ts-pattern@5.9.0: {} - - tsconfck@3.1.6(typescript@5.9.3): - optionalDependencies: - typescript: 5.9.3 - - tsconfig-paths@3.15.0: - dependencies: - '@types/json5': 0.0.29 - json5: 1.0.2 - minimist: 1.2.8 - strip-bom: 3.0.0 - - tslib@2.8.1: {} - - tsx@4.21.0: - dependencies: - esbuild: 0.27.2 - get-tsconfig: 4.13.0 - optionalDependencies: - fsevents: 2.3.3 - optional: true - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - type-fest@0.6.0: {} - - type-fest@0.8.1: {} - - type-fest@5.4.4: - dependencies: - tagged-tag: 1.0.0 - - typed-array-buffer@1.0.3: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-typed-array: 1.1.15 - - typed-array-byte-length@1.0.3: - dependencies: - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - - typed-array-byte-offset@1.0.4: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - reflect.getprototypeof: 1.0.10 - - typed-array-length@1.0.7: - dependencies: - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - is-typed-array: 1.1.15 - possible-typed-array-names: 1.1.0 - reflect.getprototypeof: 1.0.10 - - typescript-eslint@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): - dependencies: - '@typescript-eslint/eslint-plugin': 8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.1.0(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - typescript@5.9.3: {} - - unbox-primitive@1.1.0: - dependencies: - call-bound: 1.0.4 - has-bigints: 1.1.0 - has-symbols: 1.1.0 - which-boxed-primitive: 1.1.1 - - undici-types@7.16.0: {} - - undici@7.16.0: {} - - unist-util-stringify-position@2.0.3: - dependencies: - '@types/unist': 2.0.11 - - universalify@0.1.2: {} - - universalify@2.0.1: {} - - unrs-resolver@1.11.1: - dependencies: - napi-postinstall: 0.3.4 - optionalDependencies: - '@unrs/resolver-binding-android-arm-eabi': 1.11.1 - '@unrs/resolver-binding-android-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-x64': 1.11.1 - '@unrs/resolver-binding-freebsd-x64': 1.11.1 - '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 - '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 - '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-musl': 1.11.1 - '@unrs/resolver-binding-wasm32-wasi': 1.11.1 - '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 - '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 - '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - - update-browserslist-db@1.1.4(browserslist@4.28.0): - dependencies: - browserslist: 4.28.0 - escalade: 3.2.0 - picocolors: 1.1.1 - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - uuid@11.1.0: {} - - validate-npm-package-license@3.0.4: - dependencies: - spdx-correct: 3.2.0 - spdx-expression-parse: 3.0.1 - - vite-tsconfig-paths@6.1.1(typescript@5.9.3)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)): - dependencies: - debug: 4.4.3 - globrex: 0.1.2 - tsconfck: 3.1.6(typescript@5.9.3) - vite: 8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) - transitivePeerDependencies: - - supports-color - - typescript - - vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2): - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.3 - postcss: 8.5.8 - rolldown: 1.0.0-rc.10 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 24.12.0 - esbuild: 0.27.2 - fsevents: 2.3.3 - jiti: 2.6.1 - tsx: 4.21.0 - yaml: 2.8.2 - - vitest@4.1.0(@types/node@24.12.0)(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)): - dependencies: - '@vitest/expect': 4.1.0 - '@vitest/mocker': 4.1.0(vite@8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) - '@vitest/pretty-format': 4.1.0 - '@vitest/runner': 4.1.0 - '@vitest/snapshot': 4.1.0 - '@vitest/spy': 4.1.0 - '@vitest/utils': 4.1.0 - es-module-lexer: 2.0.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 4.0.0 - tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 8.0.1(@types/node@24.12.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 24.12.0 - transitivePeerDependencies: - - msw - - void-elements@3.1.0: {} - - webidl-conversions@3.0.1: {} - - whatwg-encoding@3.1.1: - dependencies: - iconv-lite: 0.6.3 - - whatwg-mimetype@4.0.0: {} - - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - - which-boxed-primitive@1.1.1: - dependencies: - is-bigint: 1.1.0 - is-boolean-object: 1.2.2 - is-number-object: 1.1.1 - is-string: 1.1.1 - is-symbol: 1.1.1 - - which-builtin-type@1.2.1: - dependencies: - call-bound: 1.0.4 - function.prototype.name: 1.1.8 - has-tostringtag: 1.0.2 - is-async-function: 2.1.1 - is-date-object: 1.1.0 - is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.2 - is-regex: 1.2.1 - is-weakref: 1.1.1 - isarray: 2.0.5 - which-boxed-primitive: 1.1.1 - which-collection: 1.0.2 - which-typed-array: 1.1.19 - - which-collection@1.0.2: - dependencies: - is-map: 2.0.3 - is-set: 2.0.3 - is-weakmap: 2.0.2 - is-weakset: 2.0.4 - - which-typed-array@1.1.19: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - for-each: 0.3.5 - get-proto: 1.0.1 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - - widest-line@6.0.0: - dependencies: - string-width: 8.2.0 - - with@7.0.2: - dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 - assert-never: 1.4.0 - babel-walk: 3.0.0-canary-5 - - word-wrap@1.2.5: {} - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 5.1.2 - strip-ansi: 7.1.2 - - wrap-ansi@9.0.2: - dependencies: - ansi-styles: 6.2.3 - string-width: 7.2.0 - strip-ansi: 7.1.2 - - wrappy@1.0.2: {} - - ws@8.18.3: {} - - yallist@3.1.1: {} - - yaml@2.8.2: {} - - yocto-queue@0.1.0: {} - - yoga-layout@3.2.1: {} - - zod@3.25.76: {} - - zx@8.8.5: {} diff --git a/scripts/e2e/_lib.sh b/scripts/e2e/_lib.sh index b419b2c6..a2862be7 100644 --- a/scripts/e2e/_lib.sh +++ b/scripts/e2e/_lib.sh @@ -82,3 +82,70 @@ dg_ensure_docker() { echo "e2e: docker is not accessible (docker ps failed; sudo -n docker ps also failed)" >&2 return 1 } + +dg_ensure_bun() { + if command -v bun >/dev/null 2>&1; then + return 0 + fi + + echo "e2e: bun is not installed or not in PATH" >&2 + return 1 +} + +dg_ensure_node_gyp() { + local bin_dir="$1" + + if command -v node-gyp >/dev/null 2>&1; then + return 0 + fi + + local prefix="$bin_dir/node-gyp" + local node_gyp_bin="$prefix/node_modules/.bin" + + if [[ ! -x "$node_gyp_bin/node-gyp" ]]; then + mkdir -p "$prefix" + npm install --prefix "$prefix" node-gyp >/dev/null + fi + + export PATH="$node_gyp_bin:$PATH" +} + +dg_prepare_bun_workspace() { + local repo_root="$1" + local bin_dir="$2" + + dg_ensure_bun + dg_ensure_node_gyp "$bin_dir" + + ( + cd "$repo_root" + bun install --no-save --silent + ) +} + +dg_build_docker_git_cli() { + local repo_root="$1" + + ( + cd "$repo_root" + bun run --cwd packages/app build:docker-git + ) +} + +dg_prepare_docker_git_cli() { + local repo_root="$1" + local bin_dir="$2" + + dg_prepare_bun_workspace "$repo_root" "$bin_dir" + dg_build_docker_git_cli "$repo_root" +} + +dg_run_docker_git() { + local repo_root="$1" + shift + + ( + cd "$repo_root" + bun packages/app/dist/src/docker-git/main.js "$@" + ) +} diff --git a/scripts/e2e/clone-cache.sh b/scripts/e2e/clone-cache.sh index e82956a2..12989337 100755 --- a/scripts/e2e/clone-cache.sh +++ b/scripts/e2e/clone-cache.sh @@ -16,6 +16,7 @@ chmod 0777 "$ROOT/e2e" KEEP="${KEEP:-0}" dg_ensure_docker "$ROOT/.e2e-bin" +dg_prepare_docker_git_cli "$REPO_ROOT" "$ROOT/.e2e-bin" export DOCKER_GIT_PROJECTS_ROOT="$ROOT" export DOCKER_GIT_STATE_AUTO_PULL=0 @@ -122,7 +123,7 @@ EOF_ENV ( cd "$REPO_ROOT" - pnpm run docker-git clone "$REPO_URL" \ + dg_run_docker_git "$REPO_ROOT" clone "$REPO_URL" \ --force \ --gh-skip \ --no-ssh \ diff --git a/scripts/e2e/issue-61-auth-labels.sh b/scripts/e2e/issue-61-auth-labels.sh index 17f42e27..227c5153 100755 --- a/scripts/e2e/issue-61-auth-labels.sh +++ b/scripts/e2e/issue-61-auth-labels.sh @@ -18,6 +18,7 @@ chmod 0777 "$ROOT" KEEP="${KEEP:-0}" export DOCKER_GIT_PROJECTS_ROOT="$ROOT" +dg_prepare_docker_git_cli "$REPO_ROOT" "$ROOT/.e2e-bin" # Keep the bare origin remote outside the state repo root so auto-sync commits # don't accidentally include its objects/refs. @@ -75,11 +76,11 @@ git_token="git_token_$RUN_ID" # 1) Store multiple GitHub tokens by label (non-interactive / CI path). ( cd "$REPO_ROOT" - pnpm run docker-git auth gh login --token "$default_token" + dg_run_docker_git "$REPO_ROOT" auth gh login --token "$default_token" ) ( cd "$REPO_ROOT" - pnpm run docker-git auth gh login --token "$agiens_token" --label agiens + dg_run_docker_git "$REPO_ROOT" auth gh login --token "$agiens_token" --label agiens ) grep -Fq -- "GITHUB_TOKEN=$default_token" "$ROOT/.orch/env/global.env" \ diff --git a/scripts/e2e/local-package-cli.sh b/scripts/e2e/local-package-cli.sh index 8b80bd01..7f627d18 100755 --- a/scripts/e2e/local-package-cli.sh +++ b/scripts/e2e/local-package-cli.sh @@ -4,16 +4,18 @@ set -euo pipefail RUN_ID="$(date +%s)-$RANDOM" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +source "$REPO_ROOT/scripts/e2e/_lib.sh" ROOT_BASE="${DOCKER_GIT_E2E_ROOT_BASE:-/tmp/docker-git-e2e-root}" mkdir -p "$ROOT_BASE" ROOT="$(mktemp -d "$ROOT_BASE/local-package-cli.XXXXXX")" KEEP="${KEEP:-0}" -PACK_LOG="$ROOT/npm-pack.log" -HELP_LOG_PNPM="$ROOT/docker-git-help-pnpm.log" -HELP_LOG_NPM="$ROOT/docker-git-help-npm.log" +PACK_LOG="$ROOT/bun-pack.log" +HELP_LOG_BUN="$ROOT/docker-git-help-bun.log" TAR_LIST="$ROOT/tar-list.txt" PACKED_TARBALL="" +PACKAGE_JSON="$REPO_ROOT/packages/app/package.json" +PACKAGE_JSON_BACKUP="$ROOT/package.json.backup" fail() { echo "e2e/local-package-cli: $*" >&2 @@ -24,20 +26,19 @@ on_error() { local line="$1" echo "e2e/local-package-cli: failed at line $line" >&2 if [[ -f "$PACK_LOG" ]]; then - echo "--- npm pack log ---" >&2 + echo "--- bun pack log ---" >&2 cat "$PACK_LOG" >&2 || true fi - if [[ -f "$HELP_LOG_PNPM" ]]; then - echo "--- pnpm docker-git --help log ---" >&2 - cat "$HELP_LOG_PNPM" >&2 || true - fi - if [[ -f "$HELP_LOG_NPM" ]]; then - echo "--- npm exec docker-git --help log ---" >&2 - cat "$HELP_LOG_NPM" >&2 || true + if [[ -f "$HELP_LOG_BUN" ]]; then + echo "--- bun run docker-git --help log ---" >&2 + cat "$HELP_LOG_BUN" >&2 || true fi } cleanup() { + if [[ -f "$PACKAGE_JSON_BACKUP" ]]; then + cp "$PACKAGE_JSON_BACKUP" "$PACKAGE_JSON" >/dev/null 2>&1 || true + fi if [[ "$KEEP" == "1" ]]; then echo "e2e/local-package-cli: KEEP=1 set; preserving temp dir: $ROOT" >&2 return @@ -51,13 +52,16 @@ cleanup() { trap 'on_error $LINENO' ERR trap cleanup EXIT -cd "$REPO_ROOT/packages/app" -npm pack --silent >"$PACK_LOG" -tarball_name="$(tail -n 1 "$PACK_LOG" | tr -d '\r')" -[[ -n "$tarball_name" ]] || fail "npm pack did not return tarball name" +dg_prepare_docker_git_cli "$REPO_ROOT" "$ROOT/.e2e-bin" + +cp "$PACKAGE_JSON" "$PACKAGE_JSON_BACKUP" +bun -e 'import { readFileSync, writeFileSync } from "node:fs"; const path = process.argv[1]; const pkg = JSON.parse(readFileSync(path, "utf8")); delete pkg.devDependencies; writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n");' "$PACKAGE_JSON" -PACKED_TARBALL="$REPO_ROOT/packages/app/$tarball_name" +cd "$REPO_ROOT/packages/app" +PACKED_TARBALL="$(bun pm pack --quiet --ignore-scripts --destination "$ROOT" | tee "$PACK_LOG" | tail -n 1 | tr -d '\r')" +[[ -n "$PACKED_TARBALL" ]] || fail "bun pm pack did not return tarball path" [[ -f "$PACKED_TARBALL" ]] || fail "packed tarball not found: $PACKED_TARBALL" +cp "$PACKAGE_JSON_BACKUP" "$PACKAGE_JSON" tar -tf "$PACKED_TARBALL" >"$TAR_LIST" while IFS= read -r entry; do @@ -70,34 +74,25 @@ while IFS= read -r entry; do esac done <"$TAR_LIST" -grep -Fxq "package/dist/src/docker-git/main.js" "$TAR_LIST" \ +grep -Fq -- "package/dist/src/docker-git/main.js" "$TAR_LIST" \ || fail "packed tarball does not include dist/src/docker-git/main.js" main_entry_tmp="$ROOT/main-entry.js" tar -xOf "$PACKED_TARBALL" package/dist/src/docker-git/main.js >"$main_entry_tmp" main_first_line="$(head -n 1 "$main_entry_tmp" | tr -d '\r')" -[[ "$main_first_line" == "#!/usr/bin/env node" ]] \ - || fail "packed CLI entrypoint missing shebang: expected '#!/usr/bin/env node', got '$main_first_line'" +[[ "$main_first_line" == "#!/usr/bin/env bun" ]] \ + || fail "packed CLI entrypoint missing shebang: expected '#!/usr/bin/env bun', got '$main_first_line'" -dep_keys="$(tar -xOf "$PACKED_TARBALL" package/package.json | node -e 'let s="";process.stdin.on("data",(c)=>{s+=c});process.stdin.on("end",()=>{const pkg=JSON.parse(s);const deps=Object.keys(pkg.dependencies ?? {});if (deps.includes("@effect-template/lib")) {console.error("@effect-template/lib must not be a runtime dependency in packed package");process.exit(1)}process.stdout.write(deps.join(","));});')" +dep_keys="$(tar -xOf "$PACKED_TARBALL" package/package.json | bun -e 'const s = await Bun.stdin.text(); const pkg = JSON.parse(s); const deps = Object.keys(pkg.dependencies ?? {}); if (deps.includes("@effect-template/lib")) { console.error("@effect-template/lib must not be a runtime dependency in packed package"); process.exit(1) } process.stdout.write(deps.join(","));')" [[ "$dep_keys" == *"effect"* ]] || fail "packed dependency set looks invalid: $dep_keys" mkdir -p "$ROOT/project" cd "$ROOT/project" -npm init -y >/dev/null -pnpm add "$PACKED_TARBALL" --silent --lockfile=false -pnpm docker-git --help >"$HELP_LOG_PNPM" 2>&1 - -grep -Fq -- "docker-git clone [options]" "$HELP_LOG_PNPM" \ - || fail "expected docker-git help output from local packed package" - -mkdir -p "$ROOT/project-npm" -cd "$ROOT/project-npm" -npm init -y >/dev/null -npm install "$PACKED_TARBALL" --silent --no-audit --fund=false -npm exec -- docker-git --help >"$HELP_LOG_NPM" 2>&1 +bun init -y >/dev/null 2>&1 +bun add "$PACKED_TARBALL" --silent +bun run docker-git --help >"$HELP_LOG_BUN" 2>&1 -grep -Fq -- "docker-git clone [options]" "$HELP_LOG_NPM" \ - || fail "expected docker-git help output via npm exec from local packed package" +grep -Fq -- "docker-git clone [options]" "$HELP_LOG_BUN" \ + || fail "expected docker-git help output via Bun from local packed package" -echo "e2e/local-package-cli: local tarball install + pnpm/npm CLI execution OK" >&2 +echo "e2e/local-package-cli: local tarball install + Bun CLI execution OK" >&2 diff --git a/scripts/e2e/login-context.sh b/scripts/e2e/login-context.sh index 57210a61..343e5d80 100755 --- a/scripts/e2e/login-context.sh +++ b/scripts/e2e/login-context.sh @@ -17,6 +17,7 @@ chmod 0777 "$ROOT/e2e" KEEP="${KEEP:-0}" dg_ensure_docker "$ROOT/.e2e-bin" +dg_prepare_docker_git_cli "$REPO_ROOT" "$ROOT/.e2e-bin" export DOCKER_GIT_PROJECTS_ROOT="$ROOT" export DOCKER_GIT_STATE_AUTO_SYNC=0 @@ -99,7 +100,7 @@ EOF_ENV ( cd "$REPO_ROOT" - pnpm run docker-git clone "$repo_url" \ + dg_run_docker_git "$REPO_ROOT" clone "$repo_url" \ --force \ --gh-skip \ --no-ssh \ diff --git a/scripts/e2e/opencode-autoconnect.sh b/scripts/e2e/opencode-autoconnect.sh index a959d175..7b5741a5 100755 --- a/scripts/e2e/opencode-autoconnect.sh +++ b/scripts/e2e/opencode-autoconnect.sh @@ -32,6 +32,7 @@ REPO_URL="https://github.com/octocat/Hello-World/issues/1" TARGET_DIR="/home/dev/workspaces/octocat/hello-world/issue-1" E2E_BIN="$ROOT/.e2e-bin" dg_ensure_docker "$E2E_BIN" +dg_prepare_docker_git_cli "$REPO_ROOT" "$E2E_BIN" fail() { echo "e2e/opencode-autoconnect: $*" >&2 @@ -84,7 +85,7 @@ mkdir -p "$ROOT/.orch/auth/codex" # Seed a fake (but structurally valid) Codex auth.json so the entrypoint can # auto-connect OpenCode without manual /connect. -node <<'NODE' > "$ROOT/.orch/auth/codex/auth.json" +bun - <<'BUN' > "$ROOT/.orch/auth/codex/auth.json" const now = Math.floor(Date.now() / 1000) const b64 = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64url") const jwt = (payload) => `${b64({ alg: "none", typ: "JWT" })}.${b64(payload)}.sig` @@ -105,7 +106,7 @@ const auth = { } process.stdout.write(JSON.stringify(auth, null, 2)) -NODE +BUN # Keep the container startup deterministic and fast for CI. mkdir -p "$OUT_DIR/.orch/env" @@ -120,8 +121,8 @@ EOF_ENV AUTH_LOG="$ROOT/codex-auth.log" ( cd "$REPO_ROOT" - pnpm run docker-git auth codex import --codex-auth "$ROOT/.orch/auth/codex" - pnpm run docker-git auth codex status --codex-auth "$ROOT/.orch/auth/codex" + dg_run_docker_git "$REPO_ROOT" auth codex import --codex-auth "$ROOT/.orch/auth/codex" + dg_run_docker_git "$REPO_ROOT" auth codex status --codex-auth "$ROOT/.orch/auth/codex" ) >"$AUTH_LOG" 2>&1 auth_confirmation_count="$(grep -Fc -- "Codex auth imported into controller state (account: ci@example.com)." "$AUTH_LOG" || true)" @@ -135,16 +136,16 @@ while [[ "$clone_attempt" -le "$clone_attempts" ]]; do set +e ( cd "$REPO_ROOT" - pnpm run docker-git clone "$REPO_URL" \ - --force \ - --gh-skip \ - --no-ssh \ - --repo-ref master \ - --env-project "$OUT_DIR/.orch/env/project.env" \ - --authorized-keys "$ROOT/authorized_keys" \ - --ssh-port "$SSH_PORT" \ - --out-dir "$OUT_DIR_REL" \ - --container-name "$CONTAINER_NAME" \ + dg_run_docker_git "$REPO_ROOT" clone "$REPO_URL" \ + --force \ + --gh-skip \ + --no-ssh \ + --repo-ref master \ + --env-project "$OUT_DIR/.orch/env/project.env" \ + --authorized-keys "$ROOT/authorized_keys" \ + --ssh-port "$SSH_PORT" \ + --out-dir "$OUT_DIR_REL" \ + --container-name "$CONTAINER_NAME" \ --service-name "$SERVICE_NAME" \ --volume-name "$VOLUME_NAME" ) @@ -176,11 +177,11 @@ docker exec -u dev "$CONTAINER_NAME" bash -lc \ docker exec -u dev "$CONTAINER_NAME" bash -lc 'test -f ~/.codex-shared/auth.json' docker exec -u dev "$CONTAINER_NAME" bash -lc \ - 'node - <<'\''NODE'\'' -const fs = require("fs") + 'bun - <<'\''BUN'\'' +import { readFileSync } from "node:fs" const p = process.env.HOME + "/.local/share/opencode/auth.json" -const auth = JSON.parse(fs.readFileSync(p, "utf8")) +const auth = JSON.parse(readFileSync(p, "utf8")) const openai = auth && auth.openai if (!openai) process.exit(1) if (openai.type === "oauth") { @@ -194,7 +195,7 @@ if (openai.type === "api") { process.exit(0) } process.exit(1) -NODE' +BUN' # Exercises Bun-based plugin install path (regression test for BUN_INSTALL env). docker exec -u dev "$CONTAINER_NAME" bash -lc \ diff --git a/scripts/e2e/runtime-volumes-ssh.sh b/scripts/e2e/runtime-volumes-ssh.sh index e0610090..76c98fb4 100755 --- a/scripts/e2e/runtime-volumes-ssh.sh +++ b/scripts/e2e/runtime-volumes-ssh.sh @@ -16,6 +16,7 @@ chmod 0777 "$ROOT/e2e" KEEP="${KEEP:-0}" dg_ensure_docker "$ROOT/.e2e-bin" +dg_prepare_docker_git_cli "$REPO_ROOT" "$ROOT/.e2e-bin" export DOCKER_GIT_PROJECTS_ROOT="$ROOT" export DOCKER_GIT_STATE_AUTO_SYNC=0 @@ -91,7 +92,7 @@ dg_write_docker_host_file "$ROOT/authorized_keys" 644 < "$SSH_PUB_KEY" # Seed a structurally valid auth.json so the shared Codex volume must be created # and wired into the container runtime. -node <<'NODE' | dg_write_docker_host_file "$ROOT/.orch/auth/codex/auth.json" 600 +bun - <<'BUN' | dg_write_docker_host_file "$ROOT/.orch/auth/codex/auth.json" 600 const now = Math.floor(Date.now() / 1000) const b64 = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64url") const jwt = (payload) => `${b64({ alg: "none", typ: "JWT" })}.${b64(payload)}.sig` @@ -112,7 +113,7 @@ const auth = { } process.stdout.write(JSON.stringify(auth, null, 2)) -NODE +BUN mkdir -p "$OUT_DIR/.orch/env" chmod 0777 "$OUT_DIR" "$OUT_DIR/.orch" "$OUT_DIR/.orch/env" @@ -125,7 +126,7 @@ EOF_ENV ( cd "$REPO_ROOT" - pnpm run docker-git clone "$REPO_URL" \ + dg_run_docker_git "$REPO_ROOT" clone "$REPO_URL" \ --force \ --gh-skip \ --no-ssh \ @@ -150,7 +151,7 @@ docker exec -u dev "$CONTAINER_NAME" bash -lc "test -d '$TARGET_DIR/.git'" \ || fail "expected cloned repo at: $TARGET_DIR" MOUNTS_JSON="$(docker inspect --format '{{json .Mounts}}' "$CONTAINER_NAME")" -MOUNTS_JSON="$MOUNTS_JSON" HOME_VOLUME_NAME="$VOLUME_NAME" node <<'NODE' +MOUNTS_JSON="$MOUNTS_JSON" HOME_VOLUME_NAME="$VOLUME_NAME" bun - <<'BUN' const mounts = JSON.parse(process.env.MOUNTS_JSON) const byDestination = new Map(mounts.map((mount) => [mount.Destination, mount])) @@ -180,7 +181,7 @@ expect(codexSharedMount.Name === "docker-git-shared-codex", `unexpected Codex sh expect(!byDestination.has("/home/dev/.docker-git"), "did not expect a direct bind mount for /home/dev/.docker-git") expect(!byDestination.has("/home/dev/.codex"), "did not expect a direct bind mount for /home/dev/.codex") expect(!byDestination.has("/home/dev/.ssh/authorized_keys"), "did not expect a direct bind mount for authorized_keys") -NODE +BUN docker exec -u dev "$CONTAINER_NAME" bash -lc 'test -f ~/.docker-git/authorized_keys' \ || fail "expected authorized_keys to be mirrored into the home volume" diff --git a/scripts/npx b/scripts/npx index ebc16dbb..3fb65412 100755 --- a/scripts/npx +++ b/scripts/npx @@ -1,12 +1,12 @@ #!/usr/bin/env sh set -eu -# CHANGE: provide a minimal npx shim for pnpm-managed workspaces -# WHY: some tools (e.g. vibecode-linter) call `npx tsc` which may accidentally run the wrong `tsc` package instead of the local TypeScript compiler +# CHANGE: provide a minimal npx shim for Bun-managed workspaces +# WHY: some tools (e.g. vibecode-linter) call `npx tsc` and should resolve the local workspace binary through Bun instead of downloading a similarly named package # QUOTE(ТЗ): n/a # REF: issue-27 (CI/test harness) # SOURCE: n/a -# FORMAT THEOREM: ∀cmd,args: npx(cmd,args) -> pnpm_exec(cmd,args) +# FORMAT THEOREM: ∀cmd,args: npx(cmd,args) -> local_bin(cmd,args) ∨ bun_x(cmd,args) # PURITY: SHELL # EFFECT: Effect # INVARIANT: prefers local workspace binaries over downloading similarly-named packages @@ -33,4 +33,16 @@ if [ "${1-}" = "" ]; then exit 2 fi -exec pnpm exec "$@" +command_name="$1" +shift + +search_dir="$PWD" +while [ "$search_dir" != "/" ]; do + candidate="$search_dir/node_modules/.bin/$command_name" + if [ -x "$candidate" ]; then + exec "$candidate" "$@" + fi + search_dir=$(dirname "$search_dir") +done + +exec bun x --bun "$command_name" "$@" diff --git a/scripts/pre-push-knowledge-guard.js b/scripts/pre-push-knowledge-guard.js index 54b227c5..cdd55955 100755 --- a/scripts/pre-push-knowledge-guard.js +++ b/scripts/pre-push-knowledge-guard.js @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env bun // CHANGE: Prevent pushing commits that contain oversized blobs or secret-like data under any .knowledge/.knowlenge path. // WHY: keep repository history safe (size + credentials) before refs leave local machine. @@ -227,9 +227,9 @@ console.error(""); console.error("Fix options:"); console.error(" - For new changes: commit again (pre-commit will split + redact knowledge files)."); console.error(" - For already committed changes in upstream..HEAD:"); -console.error(" 1) node scripts/split-knowledge-large-files.js"); +console.error(" 1) bun scripts/split-knowledge-large-files.js"); console.error(" 2) bash scripts/pre-commit-secret-guard.sh"); -console.error(" 3) node scripts/repair-knowledge-history.js"); +console.error(" 3) bun scripts/repair-knowledge-history.js"); console.error(" 4) git push"); console.error(""); console.error("To bypass this guard (not recommended): set DOCKER_GIT_SKIP_KNOWLEDGE_GUARD=1"); diff --git a/scripts/repair-knowledge-history.js b/scripts/repair-knowledge-history.js index 07777f06..4ffe354f 100755 --- a/scripts/repair-knowledge-history.js +++ b/scripts/repair-knowledge-history.js @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env bun // CHANGE: Rewrite unpushed commits so oversized .knowledge/.knowlenge files are split inside history. // WHY: Splitting in the working tree is not enough once a >100MB blob is committed; the blob must become unreachable. @@ -53,7 +53,7 @@ if (!Number.isFinite(count) || count <= 0) { // Run splitter + secret redaction after each commit is replayed, then amend if needed. const execCmd = [ - `node scripts/split-knowledge-large-files.js`, + `bun scripts/split-knowledge-large-files.js`, `while IFS= read -r -d '' knowledge_dir; do git add -A -- "$knowledge_dir"; done < <(find . \\( -name ".git" -o -name "tmp" \\) -type d -prune -o \\( -type d \\( -name ".knowledge" -o -name ".knowlenge" \\) -print0 \\))`, `bash scripts/pre-commit-secret-guard.sh`, `if ! git diff --cached --quiet; then git commit --amend --no-edit --no-verify; fi`, diff --git a/scripts/session-backup-gist.js b/scripts/session-backup-gist.js index cd9a34f7..a87c08de 100644 --- a/scripts/session-backup-gist.js +++ b/scripts/session-backup-gist.js @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env bun /** * Session Backup to a private GitHub repository @@ -8,7 +8,7 @@ * associated PR with direct links to the uploaded files. * * Usage: - * node scripts/session-backup-gist.js [options] + * bun scripts/session-backup-gist.js [options] * * Options: * --session-dir Path to session directory under $HOME (default: auto-detect ~/.codex, ~/.claude, ~/.qwen, or ~/.gemini) diff --git a/scripts/session-backup-repo.js b/scripts/session-backup-repo.js index 7f7c12ba..bd0845f1 100644 --- a/scripts/session-backup-repo.js +++ b/scripts/session-backup-repo.js @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env bun const fs = require("node:fs"); const os = require("node:os"); diff --git a/scripts/session-list-gists.js b/scripts/session-list-gists.js index 8487fae6..87e637e9 100644 --- a/scripts/session-list-gists.js +++ b/scripts/session-list-gists.js @@ -1,10 +1,10 @@ -#!/usr/bin/env node +#!/usr/bin/env bun /** * List AI Session Backups from the private session backup repository * * Usage: - * node scripts/session-list-gists.js [command] [options] + * bun scripts/session-list-gists.js [command] [options] * * Commands: * list List session snapshots (default) diff --git a/scripts/setup-pre-commit-hook.js b/scripts/setup-pre-commit-hook.js index 4b1e348c..6fe4ccf9 100644 --- a/scripts/setup-pre-commit-hook.js +++ b/scripts/setup-pre-commit-hook.js @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env bun // CHANGE: Add repeatable pre-commit hook setup for secret auto-redaction and AI session directory staging // WHY: Keep secret scanning on every commit without one-time manual hook wiring, @@ -25,7 +25,7 @@ HOOK_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)" cd "$REPO_ROOT" -node scripts/split-knowledge-large-files.js +bun scripts/split-knowledge-large-files.js while IFS= read -r -d '' knowledge_dir; do git add -A -- "$knowledge_dir" done < <( diff --git a/scripts/split-knowledge-large-files.js b/scripts/split-knowledge-large-files.js index b225e0f9..62a5cd5e 100755 --- a/scripts/split-knowledge-large-files.js +++ b/scripts/split-knowledge-large-files.js @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env bun const fs = require("node:fs"); const path = require("node:path");