From 7bbe950515d957849bbbd39eb26acf6eba9b6996 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:30:11 +0000 Subject: [PATCH 01/26] feat(runtime): migrate docker-git to bun and gridland --- .githooks/pre-commit | 2 +- .githooks/pre-push | 4 +- .github/actions/setup/action.yml | 22 +- .github/workflows/check.yml | 29 +- .github/workflows/checking-dependencies.yml | 10 +- .github/workflows/release.yml | 271 +- .github/workflows/snapshot.yml | 27 +- AGENTS.md | 2 +- CLAUDE.md | 2 +- Dockerfile | 11 +- bun.lock | 1928 ++++ ctl | 4 +- flake.nix | 2 +- package.json | 74 +- packages/api/Dockerfile | 17 +- packages/api/README.md | 6 +- packages/api/package.json | 26 +- packages/api/src/api/contracts.ts | 91 +- packages/api/src/api/schema.ts | 61 +- packages/api/src/auth-terminal-runner.ts | 35 + packages/api/src/http.ts | 164 +- packages/api/src/program.ts | 4 + packages/api/src/services/auth-menu.ts | 227 + .../src/services/auth-terminal-sessions.ts | 313 + packages/api/src/services/project-auth.ts | 439 + .../src/services/project-authorized-keys.ts | 133 + packages/api/src/services/project-runtime.ts | 138 + packages/api/src/services/projects.ts | 99 +- .../api/src/services/terminal-sessions.ts | 363 + packages/api/tests/projects.test.ts | 60 +- packages/api/tests/schema.test.ts | 27 + packages/app/eslint.config.mts | 2 +- packages/app/index.html | 49 + packages/app/package.json | 57 +- packages/app/src/app/program.ts | 10 +- .../app/src/docker-git/controller-revision.ts | 4 +- packages/app/src/docker-git/gridland-bun.d.ts | 93 + packages/app/src/docker-git/main.ts | 2 +- packages/app/src/docker-git/menu-actions.ts | 48 +- packages/app/src/docker-git/menu-auth-data.ts | 118 +- .../app/src/docker-git/menu-auth-shared.ts | 105 + .../app/src/docker-git/menu-create-shared.ts | 267 + packages/app/src/docker-git/menu-create.ts | 173 +- .../src/docker-git/menu-gridland-runtime.tsx | 183 + packages/app/src/docker-git/menu-input.ts | 3 +- .../src/docker-git/menu-project-auth-data.ts | 65 +- .../docker-git/menu-project-auth-shared.ts | 76 + .../app/src/docker-git/menu-project-auth.ts | 25 +- .../app/src/docker-git/menu-render-auth.ts | 16 +- .../app/src/docker-git/menu-render-common.ts | 44 +- .../app/src/docker-git/menu-render-layout.ts | 27 +- .../docker-git/menu-render-project-auth.ts | 28 +- .../app/src/docker-git/menu-render-select.ts | 265 +- packages/app/src/docker-git/menu-render.ts | 85 +- .../app/src/docker-git/menu-select-actions.ts | 5 +- .../src/docker-git/menu-select-presenter.ts | 247 + packages/app/src/docker-git/menu-types.ts | 1 + packages/app/src/docker-git/menu.ts | 92 +- packages/app/src/lib/core/clone.ts | 4 +- packages/app/src/lib/core/command-builders.ts | 2 +- packages/app/src/lib/core/domain.ts | 2 +- .../app/src/lib/core/template-defaults.ts | 4 +- .../templates-entrypoint/agents-notice.ts | 2 +- .../src/lib/core/templates-entrypoint/base.ts | 10 +- .../claude-extra-config.ts | 2 +- .../lib/core/templates-entrypoint/gemini.ts | 2 +- .../lib/core/templates-entrypoint/tasks.ts | 2 +- .../src/lib/core/templates/docker-compose.ts | 2 +- .../app/src/lib/core/templates/dockerfile.ts | 5 +- packages/app/src/lib/shell/clone.ts | 10 +- packages/app/src/lib/shell/config.ts | 28 +- packages/app/src/lib/shell/files.ts | 8 +- packages/app/src/lib/shell/workspace-root.ts | 51 + .../app/src/lib/usecases/actions/ports.ts | 2 +- packages/app/src/lib/usecases/auth-sync.ts | 9 +- packages/app/src/lib/usecases/projects-ssh.ts | 72 +- packages/app/src/lib/usecases/projects.ts | 3 + .../src/lib/usecases/scrap-session-export.ts | 3 +- packages/app/src/ui/primitives-gridland.tsx | 68 + packages/app/src/ui/primitives-web.tsx | 138 + packages/app/src/ui/primitives.tsx | 104 + packages/app/src/ui/shared.tsx | 195 + packages/app/src/web/action-prompt.ts | 100 + packages/app/src/web/actions-auth.ts | 285 + packages/app/src/web/actions-projects.ts | 215 + packages/app/src/web/actions-shared.ts | 76 + packages/app/src/web/actions.ts | 31 + packages/app/src/web/api-http.ts | 100 + packages/app/src/web/api-schema.ts | 192 + packages/app/src/web/api.ts | 174 + packages/app/src/web/app-ready-actions.ts | 60 + packages/app/src/web/app-ready-controller.ts | 158 + packages/app/src/web/app-ready-create.ts | 113 + packages/app/src/web/app-ready-hooks.ts | 236 + packages/app/src/web/app-ready-layout.tsx | 78 + .../app/src/web/app-ready-main-panels.tsx | 258 + .../app/src/web/app-ready-shortcut-runtime.ts | 176 + packages/app/src/web/app-ready-shortcuts.ts | 245 + packages/app/src/web/app-ready.tsx | 114 + packages/app/src/web/app.tsx | 169 + packages/app/src/web/elements.tsx | 97 + packages/app/src/web/main.tsx | 9 + packages/app/src/web/menu.ts | 33 + packages/app/src/web/panel-auth-shared.tsx | 10 + packages/app/src/web/panel-auth.tsx | 97 + packages/app/src/web/panel-content.tsx | 259 + packages/app/src/web/panel-create-select.tsx | 153 + packages/app/src/web/panel-layout.tsx | 230 + packages/app/src/web/panel-project-auth.tsx | 93 + .../app/src/web/panel-project-details.tsx | 155 + packages/app/src/web/panel-terminal.tsx | 119 + packages/app/src/web/panels.tsx | 10 + .../app/src/web/terminal-panel-runtime.ts | 280 + packages/app/src/web/terminal.ts | 86 + packages/app/src/web/vite-env.d.ts | 11 + packages/app/tests/app/main.test.ts | 2 +- .../docker-git/app-ready-shortcuts.test.ts | 150 + .../docker-git/host-ssh-material.test.ts | 2 +- .../docker-git/menu-create-shared.test.ts | 59 + .../app/tests/docker-git/terminal.test.ts | 69 + packages/app/tsconfig.json | 4 +- packages/app/vite.config.ts | 23 +- packages/app/vite.docker-git.config.ts | 35 +- packages/app/vite.web.config.ts | 89 + packages/app/vitest.config.ts | 23 +- packages/lib/package.json | 9 +- packages/lib/src/core/clone.ts | 4 +- packages/lib/src/core/command-builders.ts | 6 +- packages/lib/src/core/domain.ts | 4 +- packages/lib/src/core/template-defaults.ts | 4 +- .../templates-entrypoint/agents-notice.ts | 2 +- .../lib/src/core/templates-entrypoint/base.ts | 10 +- .../claude-extra-config.ts | 2 +- .../src/core/templates-entrypoint/gemini.ts | 2 +- .../src/core/templates-entrypoint/tasks.ts | 2 +- .../lib/src/core/templates/docker-compose.ts | 2 +- packages/lib/src/core/templates/dockerfile.ts | 5 +- packages/lib/src/shell/clone.ts | 10 +- packages/lib/src/shell/config.ts | 28 +- packages/lib/src/shell/files.ts | 8 +- packages/lib/src/shell/workspace-root.ts | 49 + packages/lib/src/usecases/actions/ports.ts | 2 +- .../lib/src/usecases/actions/prepare-files.ts | 9 +- packages/lib/src/usecases/auth-codex.ts | 4 +- packages/lib/src/usecases/auth-sync.ts | 9 +- .../src/usecases/github-token-preflight.ts | 34 +- .../src/usecases/github-token-validation.ts | 9 +- packages/lib/src/usecases/projects-ssh.ts | 76 +- packages/lib/src/usecases/projects.ts | 10 +- .../lib/src/usecases/scrap-session-export.ts | 3 +- .../lib/src/usecases/shared-volume-seed.ts | 4 +- .../tests/usecases/agent-auto-select.test.ts | 2 +- packages/lib/tests/usecases/apply.test.ts | 58 +- .../usecases/create-project-open-ssh.test.ts | 2 +- .../create-project-state-sync-order.test.ts | 2 +- .../tests/usecases/docker-up-force.test.ts | 2 +- .../usecases/github-token-preflight.test.ts | 2 +- .../lib/tests/usecases/mcp-playwright.test.ts | 4 +- .../lib/tests/usecases/prepare-files.test.ts | 51 +- .../lib/tests/usecases/projects-up.test.ts | 2 +- packages/lib/vite.config.ts | 2 - packages/lib/vitest.config.ts | 2 - pnpm-lock.yaml | 7871 ----------------- scripts/e2e/_lib.sh | 67 + scripts/e2e/clone-cache.sh | 3 +- scripts/e2e/issue-61-auth-labels.sh | 5 +- scripts/e2e/local-package-cli.sh | 65 +- scripts/e2e/login-context.sh | 3 +- scripts/e2e/opencode-autoconnect.sh | 37 +- scripts/e2e/runtime-volumes-ssh.sh | 11 +- scripts/npx | 20 +- scripts/pre-push-knowledge-guard.js | 6 +- scripts/repair-knowledge-history.js | 4 +- scripts/session-backup-gist.js | 4 +- scripts/session-backup-repo.js | 2 +- scripts/session-list-gists.js | 4 +- scripts/setup-pre-commit-hook.js | 4 +- scripts/split-knowledge-large-files.js | 2 +- 178 files changed, 11727 insertions(+), 9104 deletions(-) create mode 100644 bun.lock create mode 100644 packages/api/src/auth-terminal-runner.ts create mode 100644 packages/api/src/services/auth-menu.ts create mode 100644 packages/api/src/services/auth-terminal-sessions.ts create mode 100644 packages/api/src/services/project-auth.ts create mode 100644 packages/api/src/services/project-authorized-keys.ts create mode 100644 packages/api/src/services/project-runtime.ts create mode 100644 packages/api/src/services/terminal-sessions.ts create mode 100644 packages/app/index.html create mode 100644 packages/app/src/docker-git/gridland-bun.d.ts create mode 100644 packages/app/src/docker-git/menu-auth-shared.ts create mode 100644 packages/app/src/docker-git/menu-create-shared.ts create mode 100644 packages/app/src/docker-git/menu-gridland-runtime.tsx create mode 100644 packages/app/src/docker-git/menu-project-auth-shared.ts create mode 100644 packages/app/src/docker-git/menu-select-presenter.ts create mode 100644 packages/app/src/lib/shell/workspace-root.ts create mode 100644 packages/app/src/ui/primitives-gridland.tsx create mode 100644 packages/app/src/ui/primitives-web.tsx create mode 100644 packages/app/src/ui/primitives.tsx create mode 100644 packages/app/src/ui/shared.tsx create mode 100644 packages/app/src/web/action-prompt.ts create mode 100644 packages/app/src/web/actions-auth.ts create mode 100644 packages/app/src/web/actions-projects.ts create mode 100644 packages/app/src/web/actions-shared.ts create mode 100644 packages/app/src/web/actions.ts create mode 100644 packages/app/src/web/api-http.ts create mode 100644 packages/app/src/web/api-schema.ts create mode 100644 packages/app/src/web/api.ts create mode 100644 packages/app/src/web/app-ready-actions.ts create mode 100644 packages/app/src/web/app-ready-controller.ts create mode 100644 packages/app/src/web/app-ready-create.ts create mode 100644 packages/app/src/web/app-ready-hooks.ts create mode 100644 packages/app/src/web/app-ready-layout.tsx create mode 100644 packages/app/src/web/app-ready-main-panels.tsx create mode 100644 packages/app/src/web/app-ready-shortcut-runtime.ts create mode 100644 packages/app/src/web/app-ready-shortcuts.ts create mode 100644 packages/app/src/web/app-ready.tsx create mode 100644 packages/app/src/web/app.tsx create mode 100644 packages/app/src/web/elements.tsx create mode 100644 packages/app/src/web/main.tsx create mode 100644 packages/app/src/web/menu.ts create mode 100644 packages/app/src/web/panel-auth-shared.tsx create mode 100644 packages/app/src/web/panel-auth.tsx create mode 100644 packages/app/src/web/panel-content.tsx create mode 100644 packages/app/src/web/panel-create-select.tsx create mode 100644 packages/app/src/web/panel-layout.tsx create mode 100644 packages/app/src/web/panel-project-auth.tsx create mode 100644 packages/app/src/web/panel-project-details.tsx create mode 100644 packages/app/src/web/panel-terminal.tsx create mode 100644 packages/app/src/web/panels.tsx create mode 100644 packages/app/src/web/terminal-panel-runtime.ts create mode 100644 packages/app/src/web/terminal.ts create mode 100644 packages/app/src/web/vite-env.d.ts create mode 100644 packages/app/tests/docker-git/app-ready-shortcuts.test.ts create mode 100644 packages/app/tests/docker-git/menu-create-shared.test.ts create mode 100644 packages/app/tests/docker-git/terminal.test.ts create mode 100644 packages/app/vite.web.config.ts create mode 100644 packages/lib/src/shell/workspace-root.ts delete mode 100644 pnpm-lock.yaml 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..417dd19c 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -1,21 +1,29 @@ 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 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..f814bbc9 100644 --- a/.github/workflows/checking-dependencies.yml +++ b/.github/workflows/checking-dependencies.yml @@ -12,18 +12,14 @@ 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 \ + bun x @prover-coder-ai/dist-deps-prune scan \ --package ./packages/app/package.json \ --prune-dev true \ --silent 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/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..01a26beb --- /dev/null +++ b/bun.lock @@ -0,0 +1,1928 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "effect-template-workspace", + "devDependencies": { + "@changesets/changelog-github": "^0.6.0", + "@changesets/cli": "^2.30.0", + }, + }, + "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.76", + "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.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-crQwaeLmpmUfKaH2K42STNFMD5cywR6kBGKZI0FkvCbDyG3xM7CikJKucMIXOBvnUviO8loq0afT1ZSCtZiaaw=="], + + "@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/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/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/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/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..6fba95a3 100644 --- a/package.json +++ b/package.json @@ -1,65 +1,63 @@ { - "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", "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", + "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" }, + "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..91119ec5 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 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 ad1cc612..afc4a0b3 100644 --- a/packages/api/src/http.ts +++ b/packages/api/src/http.ts @@ -11,6 +11,8 @@ import * as Schema from "effect/Schema" import { ApiAuthRequiredError, ApiBadRequestError, ApiConflictError, ApiInternalError, ApiNotFoundError, describeUnknown } from "./api/errors.js" import { + AuthMenuRequestSchema, + AuthTerminalSessionRequestSchema, ApplyAllRequestSchema, CodexAuthImportRequestSchema, CodexAuthLoginRequestSchema, @@ -20,20 +22,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" @@ -62,6 +70,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, @@ -81,6 +91,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 @@ -92,13 +111,21 @@ type ApiError = | HttpServerError.RequestError | PlatformError +const noStoreHeaders = { + "cache-control": "no-store, no-cache, must-revalidate, max-age=0", + pragma: "no-cache" +} + 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 ) ) @@ -167,21 +194,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) @@ -236,8 +269,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) @@ -247,7 +299,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*(_) { @@ -255,6 +317,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*(_) { @@ -263,6 +332,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*(_) { @@ -309,7 +406,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( @@ -478,6 +578,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( @@ -491,8 +615,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) ) @@ -505,6 +631,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( 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/api/src/services/project-authorized-keys.ts b/packages/api/src/services/project-authorized-keys.ts new file mode 100644 index 00000000..2d5dba3b --- /dev/null +++ b/packages/api/src/services/project-authorized-keys.ts @@ -0,0 +1,133 @@ +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" + +const normalizeAuthorizedKeys = (value: string): ReadonlyArray => + value + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter((line) => line.length > 0) + +const mergeAuthorizedKeys = ( + base: ReadonlyArray, + required: ReadonlyArray +): string => { + const merged = [...base] + for (const line of required) { + if (!merged.includes(line)) { + merged.push(line) + } + } + return merged.length === 0 ? "" : `${merged.join("\n")}\n` +} + +const resolvePublicKeyFromPrivate = (privateKeyPath: string) => + withFsPathContext(({ fs }) => + Effect.gen(function*(_) { + const publicKeyPath = `${privateKeyPath}.pub` + const publicKeyExists = yield* _(fs.exists(publicKeyPath)) + if (publicKeyExists) { + return yield* _(fs.readFileString(publicKeyPath)) + } + + return yield* _( + runCommandCapture( + { + cwd: process.cwd(), + command: "ssh-keygen", + args: ["-y", "-f", privateKeyPath] + }, + [0], + (exitCode) => new CommandFailedError({ command: "ssh-keygen -y", exitCode }) + ).pipe(Effect.map((value) => `${value.trim()}\n`)) + ) + }) + ) + +const resolveHostPrivateKeyPath = () => + withFsPathContext(({ fs, path }) => + Effect.gen(function*(_) { + const existing = yield* _(findSshPrivateKey(fs, path, process.cwd())) + if (existing !== null) { + return existing + } + + const projectsRoot = defaultProjectsRoot(process.cwd()) + const managedKeyPath = path.join(projectsRoot, "dev_ssh_key") + const managedPublicKeyPath = `${managedKeyPath}.pub` + + yield* _(fs.makeDirectory(path.dirname(managedKeyPath), { recursive: true })) + + const stalePublicKeyExists = yield* _(fs.exists(managedPublicKeyPath)) + if (stalePublicKeyExists) { + yield* _(fs.remove(managedPublicKeyPath)) + } + + yield* _( + runCommandWithExitCodes( + { + cwd: process.cwd(), + command: "ssh-keygen", + args: ["-q", "-t", "ed25519", "-N", "", "-C", "docker-git", "-f", managedKeyPath] + }, + [0], + (exitCode) => new CommandFailedError({ command: "ssh-keygen", exitCode }) + ) + ) + + return managedKeyPath + }) + ) + +const resolveManagedHostPublicKey = () => + Effect.gen(function*(_) { + const privateKeyPath = yield* _(resolveHostPrivateKeyPath()) + const publicKey = yield* _(resolvePublicKeyFromPrivate(privateKeyPath)) + + return { + privateKeyPath, + publicKey + } + }) + +const readLocalAuthorizedKeysOverride = ( + projectDir: string, + authorizedKeysPath: string +) => + withFsPathContext(({ fs, path }) => + Effect.gen(function*(_) { + if (authorizedKeysPath === defaultTemplateConfig.authorizedKeysPath) { + return "" + } + + const resolved = resolvePathFromCwd(path, projectDir, authorizedKeysPath) + const exists = yield* _(fs.exists(resolved)) + if (!exists) { + return "" + } + + return yield* _(fs.readFileString(resolved)) + }) + ) + +export const resolveManagedAuthorizedKeysContents = () => + Effect.gen(function*(_) { + const { publicKey } = yield* _(resolveManagedHostPublicKey()) + return mergeAuthorizedKeys([], normalizeAuthorizedKeys(publicKey)) + }) + +export const resolveCreateAuthorizedKeysContents = ( + projectDir: string, + authorizedKeysPath: string +) => + Effect.gen(function*(_) { + 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 ba5abaf7..2b115497 100644 --- a/packages/api/src/services/projects.ts +++ b/packages/api/src/services/projects.ts @@ -1,4 +1,5 @@ import { + type AppError, buildCreateCommand, createProject, formatParseError, @@ -6,6 +7,7 @@ import { downAllDockerGitProjects, listProjectItems, readProjectConfig, + renderError, runDockerComposeUpWithPortCheck } from "@effect-template/lib" import * as FileSystem from "@effect/platform/FileSystem" @@ -19,9 +21,11 @@ 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 { ApiInternalError, ApiNotFoundError, ApiBadRequestError } from "../api/errors.js" +import { ApiAuthRequiredError, 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( @@ -99,7 +103,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) => ({ @@ -108,7 +115,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 })) ) @@ -124,11 +135,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) => @@ -141,6 +153,8 @@ const findProjectById = (projectId: string) => }) ) +export const getProjectItemById = (projectId: string) => findProjectById(projectId) + const resolveCreatedProject = ( containerName: string, repoUrl: string, @@ -168,6 +182,26 @@ const normalizeAuthorizedKeys = (value: string): ReadonlyArray => .map((line) => line.trim()) .filter((line) => line.length > 0) +type ProjectApiError = + | AppError + | ApiAuthRequiredError + | ApiBadRequestError + | ApiInternalError + | ApiNotFoundError + +const toProjectApiError = ( + error: ProjectApiError +): ApiAuthRequiredError | ApiBadRequestError | ApiInternalError | ApiNotFoundError => + error instanceof ApiAuthRequiredError || + error instanceof ApiBadRequestError || + error instanceof ApiInternalError || + error instanceof ApiNotFoundError + ? error + : new ApiInternalError({ + message: renderError(error), + cause: error + }) + const mergeAuthorizedKeys = ( current: ReadonlyArray, next: ReadonlyArray @@ -210,7 +244,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)) ) @@ -227,7 +271,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) }) @@ -295,7 +340,13 @@ export const createProjectFromRequest = ( waitForClone: request.waitForClone ?? parsed.right.waitForClone } - yield* _(seedAuthorizedKeysForCreate(command.outDir, request.authorizedKeysContents)) + const resolvedAuthorizedKeysContents = request.authorizedKeysContents ?? ( + request.useManagedAuthorizedKeys === true + ? yield* _(resolveCreateAuthorizedKeysContents(command.outDir, command.config.authorizedKeysPath)) + : undefined + ) + + yield* _(seedAuthorizedKeysForCreate(command.outDir, resolvedAuthorizedKeysContents)) yield* _(ensureGithubAuthForCreate(command.config)) @@ -317,7 +368,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(() => { @@ -329,7 +381,7 @@ export const createProjectFromRequest = ( ) return toProjectDetails(project, summary) - }) + }).pipe(Effect.mapError(toProjectApiError)) export const deleteProjectById = ( projectId: string @@ -342,7 +394,7 @@ export const deleteProjectById = ( emitProjectEvent(projectId, "project.deleted", { projectId }) }) ) - }) + }).pipe(Effect.mapError(toProjectApiError)) const markDeployment = (projectId: string, phase: string, message: string) => Effect.sync(() => { @@ -422,18 +474,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 @@ -443,7 +504,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 @@ -470,7 +531,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 @@ -478,7 +539,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 @@ -486,6 +547,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..371f5e68 --- /dev/null +++ b/packages/api/src/services/terminal-sessions.ts @@ -0,0 +1,363 @@ +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*(_) { + const project = yield* _(upProject(projectId, undefined, true)) + const projectItem = yield* _(getProjectItemById(projectId)) + yield* _(waitForProjectSshReady(projectItem).pipe(Effect.mapError(toApiInternalError))) + 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/projects.test.ts b/packages/api/tests/projects.test.ts index 8ba366ea..8d1a2eb7 100644 --- a/packages/api/tests/projects.test.ts +++ b/packages/api/tests/projects.test.ts @@ -4,7 +4,8 @@ import { NodeContext } from "@effect/platform-node" import { describe, expect, it } from "@effect/vitest" import { Effect } from "effect" -import { seedAuthorizedKeysForCreate } from "../src/services/projects.js" +import { ApiInternalError } from "../src/api/errors.js" +import { createProjectFromRequest, seedAuthorizedKeysForCreate } from "../src/services/projects.js" const withTempDir = ( use: (tempDir: string) => Effect.Effect @@ -59,6 +60,32 @@ const withProjectsRoot = ( }) ) +const withEnvVar = ( + key: string, + value: string | undefined, + effect: Effect.Effect +) => + Effect.acquireUseRelease( + Effect.sync(() => { + const previous = process.env[key] + if (value === undefined) { + delete process.env[key] + } else { + process.env[key] = value + } + return previous + }), + () => effect, + (previous) => + Effect.sync(() => { + if (previous === undefined) { + delete process.env[key] + } else { + process.env[key] = previous + } + }) + ) + describe("projects service", () => { it.effect("seeds host SSH keys into the controller managed authorized_keys file", () => withTempDir((root) => @@ -86,4 +113,35 @@ describe("projects service", () => { expect(projectContents).toBe(`${hostKey}\n`) }) ).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))) }) 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/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 93c2b233..f6568257 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,7 +1,7 @@ { "name": "@prover-coder-ai/docker-git", "version": "1.0.76", - "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,29 @@ "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", + "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 +58,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 +72,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 +95,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 +111,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/src/app/program.ts b/packages/app/src/app/program.ts index 2b098677..ee939ed0 100644 --- a/packages/app/src/app/program.ts +++ b/packages/app/src/app/program.ts @@ -26,10 +26,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 +42,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 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/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/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..85b5650f 100644 --- a/packages/app/src/docker-git/menu-actions.ts +++ b/packages/app/src/docker-git/menu-actions.ts @@ -5,14 +5,15 @@ 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" @@ -98,23 +99,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-auth-data.ts b/packages/app/src/docker-git/menu-auth-data.ts index a12ff738..a8be4bc2 100644 --- a/packages/app/src/docker-git/menu-auth-data.ts +++ b/packages/app/src/docker-git/menu-auth-data.ts @@ -6,101 +6,20 @@ import { ensureEnvFile, parseEnvEntries, readEnvText, upsertEnvKey } from "@lib/ import { type AppError } from "@lib/usecases/errors" import { defaultProjectsRoot } from "@lib/usecases/menu-helpers" import { autoSyncState } from "@lib/usecases/state-repo" - +import type { AuthEnvFlow } from "./menu-auth-shared.js" 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 - ) +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 buildGlobalEnvPath = (cwd: string): string => `${defaultProjectsRoot(cwd)}/.orch/env/global.env` const buildClaudeAuthPath = (cwd: string): string => `${defaultProjectsRoot(cwd)}/.orch/auth/claude` @@ -192,16 +111,3 @@ export const writeAuthFlow = ( }), 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-shared.ts b/packages/app/src/docker-git/menu-auth-shared.ts new file mode 100644 index 00000000..5fc6f01a --- /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 = Extract + +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-create-shared.ts b/packages/app/src/docker-git/menu-create-shared.ts new file mode 100644 index 00000000..aa2b4f6c --- /dev/null +++ b/packages/app/src/docker-git/menu-create-shared.ts @@ -0,0 +1,267 @@ +import { deriveRepoPathParts, resolveRepoInput } from "@lib/core/domain" +import { defaultProjectsRoot, isRepoUrlInput } from "@lib/usecases/menu-helpers" +import { Match } from "effect" + +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..2390b506 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 { type CreateCommand } from "@lib/core/domain" +import { Effect, Either, pipe } from "effect" 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-gridland-runtime.tsx b/packages/app/src/docker-git/menu-gridland-runtime.tsx new file mode 100644 index 00000000..4575a858 --- /dev/null +++ b/packages/app/src/docker-git/menu-gridland-runtime.tsx @@ -0,0 +1,183 @@ +import { InputReadError } from "@lib/shell/errors" +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" + +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 => + new 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-project-auth-data.ts b/packages/app/src/docker-git/menu-project-auth-data.ts index 8e40590c..0bfb7c08 100644 --- a/packages/app/src/docker-git/menu-project-auth-data.ts +++ b/packages/app/src/docker-git/menu-project-auth-data.ts @@ -7,57 +7,19 @@ 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 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: [] -} +export { + projectAuthMenuActionByIndex, + projectAuthMenuLabels, + projectAuthMenuSize, + projectAuthSuccessMessage, + projectAuthViewSteps +} from "./menu-project-auth-shared.js" +export type { ProjectAuthMenuAction, ProjectAuthPromptStep } from "./menu-project-auth-shared.js" const resolveCanonicalLabel = (value: string): string => { const normalized = normalizeLabel(value) @@ -192,14 +154,3 @@ export const writeProjectAuthFlow = ( ), 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-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..a17224b3 100644 --- a/packages/app/src/docker-git/menu-project-auth.ts +++ b/packages/app/src/docker-git/menu-project-auth.ts @@ -1,4 +1,4 @@ -import { Effect, Match, pipe } from "effect" +import { Effect, pipe } from "effect" import type { AppError } from "@lib/usecases/errors" import type { ProjectItem } from "@lib/usecases/projects" @@ -9,6 +9,7 @@ import { type ProjectAuthMenuAction, projectAuthMenuActionByIndex, projectAuthMenuSize, + projectAuthSuccessMessage, projectAuthViewSteps, readProjectAuthSnapshot, writeProjectAuthFlow @@ -73,19 +74,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 +88,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 +250,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..d9da4f8a 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" -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..53d0943f 100644 --- a/packages/app/src/docker-git/menu-render.ts +++ b/packages/app/src/docker-git/menu-render.ts @@ -1,18 +1,20 @@ -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" // CHANGE: render menu views with Ink without JSX @@ -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..82cf7f6f 100644 --- a/packages/app/src/docker-git/menu-select-actions.ts +++ b/packages/app/src/docker-git/menu-select-actions.ts @@ -3,6 +3,7 @@ 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" @@ -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-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-types.ts b/packages/app/src/docker-git/menu-types.ts index 16886f9d..a5d0081a 100644 --- a/packages/app/src/docker-git/menu-types.ts +++ b/packages/app/src/docker-git/menu-types.ts @@ -40,6 +40,7 @@ export type MenuKeyInput = { readonly downArrow?: boolean readonly return?: boolean readonly escape?: boolean + readonly shift?: boolean readonly backspace?: boolean readonly delete?: boolean } diff --git a/packages/app/src/docker-git/menu.ts b/packages/app/src/docker-git/menu.ts index 9d9a14c3..2848e6d3 100644 --- a/packages/app/src/docker-git/menu.ts +++ b/packages/app/src/docker-git/menu.ts @@ -1,15 +1,16 @@ 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,10 +18,9 @@ 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" @@ -87,7 +87,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) } @@ -224,56 +224,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 +261,16 @@ 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.ensuring( Effect.sync(() => { leaveTui() @@ -316,7 +280,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/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/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/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/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/projects-ssh.ts b/packages/app/src/lib/usecases/projects-ssh.ts index 456d813b..5daa7f32 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 @@ -115,6 +124,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" @@ -128,21 +163,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 @@ -163,7 +184,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) @@ -190,7 +228,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 840363c3..5451e216 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, waitForProjectSshReady } from "./projects-ssh.js" export { runDockerComposeUpWithPortCheck } from "./projects-up.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/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/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..bfeab433 --- /dev/null +++ b/packages/app/src/web/terminal.ts @@ -0,0 +1,86 @@ +import * as ParseResult from "@effect/schema/ParseResult" +import * as Schema from "@effect/schema/Schema" +import { Either } from "effect" + +import { resolveApiBaseUrl, trimTrailingSlash } from "./api-http.js" +import { type TerminalSession, TerminalSessionSchema } 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 +} + +export 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 } + +const TerminalServerMessageSchema = Schema.parseJson( + 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 + }) + ) +) + +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): TerminalServerMessage | null => + Either.getOrNull(ParseResult.decodeUnknownEither(TerminalServerMessageSchema)(value)) 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/host-ssh-material.test.ts b/packages/app/tests/docker-git/host-ssh-material.test.ts index 00c5460f..c9767708 100644 --- a/packages/app/tests/docker-git/host-ssh-material.test.ts +++ b/packages/app/tests/docker-git/host-ssh-material.test.ts @@ -175,7 +175,7 @@ const makeCommand = (outDir: string, path: Path.Path): CreateCommand => ({ dockerNetworkMode: "shared", dockerSharedNetworkName: "docker-git-shared", enableMcpPlaywright: false, - pnpmVersion: "10.27.0" + bunVersion: "1.3.11" }, outDir, runUp: true, 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/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/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/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/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/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 cecdc8d6..6cccbf63 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 @@ -81,7 +90,7 @@ const buildSshProbeArgs = (item: ProjectItem): ReadonlyArray => { return args } -const waitForSshReady = ( +export const waitForProjectSshReady = ( item: ProjectItem ): Effect.Effect => { const host = item.ipAddress ?? "localhost" @@ -114,6 +123,32 @@ const 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 // QUOTE(ТЗ): "выбор проекта сразу подключает по SSH" @@ -127,21 +162,7 @@ const 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 @@ -162,7 +183,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) @@ -188,8 +226,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 3a3e8b75..f5d810e2 100644 --- a/packages/lib/src/usecases/projects.ts +++ b/packages/lib/src/usecases/projects.ts @@ -11,5 +11,13 @@ export { export { deleteDockerGitProject } from "./projects-delete.js" export { downAllDockerGitProjects } from "./projects-down.js" export { listProjectItems, listProjects, listProjectSummaries, listRunningProjectItems } from "./projects-list.js" -export { connectProjectSsh, connectProjectSshWithUp, listProjectStatus } from "./projects-ssh.js" +export { + connectProjectSsh, + connectProjectSshWithUp, + listProjectStatus, + type PreparedProjectSsh, + prepareProjectSsh, + prepareProjectSshWithUp, + waitForProjectSshReady +} from "./projects-ssh.js" export { runDockerComposeUpWithPortCheck } from "./projects-up.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/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-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..aa098811 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..0d64e115 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"); From 6f28d0b080dccb638ff716995b126bc98fee1eb1 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:55:22 +0000 Subject: [PATCH 02/26] fix(ci): stabilize bun dependency and e2e flows --- .github/workflows/checking-dependencies.yml | 6 +----- bun.lock | 9 +++++++++ bunfig.toml | 2 ++ package.json | 4 +++- scripts/e2e/opencode-autoconnect.sh | 2 +- scripts/e2e/runtime-volumes-ssh.sh | 4 ++-- 6 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 bunfig.toml diff --git a/.github/workflows/checking-dependencies.yml b/.github/workflows/checking-dependencies.yml index f814bbc9..6f3ba1e0 100644 --- a/.github/workflows/checking-dependencies.yml +++ b/.github/workflows/checking-dependencies.yml @@ -18,8 +18,4 @@ jobs: - run: bun run --cwd packages/app build - name: Dist deps prune (lint) - run: | - bun x @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/bun.lock b/bun.lock index 01a26beb..2d9de357 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "devDependencies": { "@changesets/changelog-github": "^0.6.0", "@changesets/cli": "^2.30.0", + "@prover-coder-ai/dist-deps-prune": "^1.0.17", }, }, "packages/api": { @@ -483,6 +484,8 @@ "@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=="], @@ -1729,6 +1732,10 @@ "@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=="], @@ -1851,6 +1858,8 @@ "@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=="], 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/package.json b/package.json index 6fba95a3..a8562d2c 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "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": "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", @@ -43,7 +44,8 @@ }, "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", diff --git a/scripts/e2e/opencode-autoconnect.sh b/scripts/e2e/opencode-autoconnect.sh index aa098811..7b5741a5 100755 --- a/scripts/e2e/opencode-autoconnect.sh +++ b/scripts/e2e/opencode-autoconnect.sh @@ -85,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. -bun <<'BUN' > "$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` diff --git a/scripts/e2e/runtime-volumes-ssh.sh b/scripts/e2e/runtime-volumes-ssh.sh index 0d64e115..76c98fb4 100755 --- a/scripts/e2e/runtime-volumes-ssh.sh +++ b/scripts/e2e/runtime-volumes-ssh.sh @@ -92,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. -bun <<'BUN' | 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` @@ -151,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" bun <<'BUN' +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])) From 1b546a61ef575f5c305b443b8ffa2b38355a4bdd Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:16:27 +0000 Subject: [PATCH 03/26] fix(web): stabilize tunnel preview and terminal flow --- .gitignore | 1 + package.json | 1 + packages/api/src/http.ts | 47 ++++- .../api/src/services/terminal-sessions.ts | 24 +++ packages/app/package.json | 1 + packages/app/scripts/serve-dist-web.ts | 153 ++++++++++++++++ packages/app/src/docker-git/api-json.ts | 20 ++- packages/app/src/web/actions-projects.ts | 34 +++- packages/app/src/web/actions-shared.ts | 6 +- packages/app/src/web/api-http.ts | 8 +- packages/app/src/web/api-schema.ts | 40 +++++ packages/app/src/web/api.ts | 14 ++ packages/app/src/web/app-ready-controller.ts | 1 + packages/app/src/web/app-ready-hooks.ts | 6 + .../app/src/web/app-ready-shortcut-runtime.ts | 7 +- packages/app/src/web/app-ready-shortcuts.ts | 6 +- packages/app/src/web/elements.tsx | 7 +- packages/app/src/web/project-events.ts | 166 ++++++++++++++++++ 18 files changed, 523 insertions(+), 19 deletions(-) create mode 100644 packages/app/scripts/serve-dist-web.ts create mode 100644 packages/app/src/web/project-events.ts 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/package.json b/package.json index a8562d2c..1b2837f9 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "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", diff --git a/packages/api/src/http.ts b/packages/api/src/http.ts index afc4a0b3..f4664c7d 100644 --- a/packages/api/src/http.ts +++ b/packages/api/src/http.ts @@ -138,6 +138,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( @@ -742,6 +745,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( @@ -757,6 +785,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)) @@ -765,20 +795,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 @@ -789,9 +821,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/services/terminal-sessions.ts b/packages/api/src/services/terminal-sessions.ts index 371f5e68..b0ddd007 100644 --- a/packages/api/src/services/terminal-sessions.ts +++ b/packages/api/src/services/terminal-sessions.ts @@ -216,9 +216,33 @@ 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* _( diff --git a/packages/app/package.json b/packages/app/package.json index f6568257..55138d43 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -20,6 +20,7 @@ "prepack": "bun run build:docker-git", "dev": "vite build --watch --ssr src/app/main.ts", "dev:web": "vite --config vite.web.config.ts", + "serve:web": "bun scripts/serve-dist-web.ts", "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/", diff --git a/packages/app/scripts/serve-dist-web.ts b/packages/app/scripts/serve-dist-web.ts new file mode 100644 index 00000000..ab58e373 --- /dev/null +++ b/packages/app/scripts/serve-dist-web.ts @@ -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: Readonly> = { + ".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: string): string => { + const normalized = normalize(pathname) + return normalized.startsWith(staticRoot) + ? normalized + : join(staticRoot, normalized) +} + +const serveFile = ( + pathname: string, + response: import("node:http").ServerResponse +): void => { + 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: string): string => { + const parsed = new URL(url, "http://localhost") + const pathname = parsed.pathname.replace(/^\/api/u, "") || "/" + return `${pathname}${parsed.search}` +} + +const proxyHttp = ( + request: import("node:http").IncomingMessage, + response: import("node:http").ServerResponse +): void => { + 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/docker-git/api-json.ts b/packages/app/src/docker-git/api-json.ts index af8f8004..f6d20da8 100644 --- a/packages/app/src/docker-git/api-json.ts +++ b/packages/app/src/docker-git/api-json.ts @@ -88,6 +88,17 @@ 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"]) +} + export const renderJsonPayload = (payload: JsonValue): string => { if (typeof payload === "string") { return payload @@ -114,11 +125,12 @@ export const renderJsonPayload = (payload: JsonValue): string => { if (renderedNestedStatus !== null) { return renderedNestedStatus } + return readNestedMessage(object, "status") ?? JSON.stringify(payload, null, 2) + } - 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/web/actions-projects.ts b/packages/app/src/web/actions-projects.ts index f033ee50..21362174 100644 --- a/packages/app/src/web/actions-projects.ts +++ b/packages/app/src/web/actions-projects.ts @@ -18,6 +18,22 @@ import { loadProjectPs } from "./api.js" import type { BrowserMenuTag } from "./menu.js" +import { openProjectEventStream } from "./project-events.js" + +const appendOutputLine = ( + context: BrowserActionContext, + line: string +) => { + context.setOutput((current) => { + const trimmed = line.trim() + if (trimmed.length === 0) { + return current + } + const next = current.trim().length === 0 ? trimmed : `${current}\n${trimmed}` + const lines = next.split("\n") + return lines.length <= 120 ? next : lines.slice(-120).join("\n") + }) +} export const loadSelectedProjectInfo = ( context: BrowserActionContext, @@ -68,14 +84,30 @@ export const connectSelectedProject = (context: BrowserActionContext) => { if (projectId === null) { return } + context.setOutput("") + appendOutputLine(context, "[ssh.prepare] Preparing SSH session") + const stream = openProjectEventStream(projectId, { + onLine: (line) => { + appendOutputLine(context, line) + }, + onRateLimit: () => { + context.setMessage("HTTP 429: tunnel or proxy rate limited the live stream. Retry or request a fresh tunnel URL.") + } + }) withBusy({ context, effect: createProjectTerminalSession(projectId), label: "Opening SSH terminal", + onFailure: (error) => { + appendOutputLine(context, `[error] ${error}`) + }, + onFinally: () => { + stream.close() + }, onSuccess: ({ project, session }) => { context.reloadDashboard() context.setSelectedProject(project) - context.setOutput(session.sshCommand) + appendOutputLine(context, `SSH command: ${session.sshCommand}`) const encodedProjectId = encodeURIComponent(project.id) const encodedSessionId = encodeURIComponent(session.id) context.setTerminalSession({ diff --git a/packages/app/src/web/actions-shared.ts b/packages/app/src/web/actions-shared.ts index 0d90ecba..bf836096 100644 --- a/packages/app/src/web/actions-shared.ts +++ b/packages/app/src/web/actions-shared.ts @@ -11,6 +11,8 @@ type BusyAction
= { readonly context: BrowserActionContext readonly effect: Effect.Effect readonly label: string + readonly onFailure?: (error: string) => void + readonly onFinally?: () => void readonly onSuccess: (value: A) => void } @@ -46,19 +48,21 @@ export const nullableValue = (value: string | undefined): string | null => { return trimmed.length === 0 ? null : trimmed } -export const withBusy = ({ context, effect, label, onSuccess }: BusyAction) => { +export const withBusy = ({ context, effect, label, onFailure, onFinally, onSuccess }: BusyAction) => { context.setBusyLabel(label) void Effect.runPromise( effect.pipe( Effect.match({ onFailure: (error) => { context.setMessage(error) + onFailure?.(error) }, onSuccess }) ) ).finally(() => { context.setBusyLabel(null) + onFinally?.() }) } diff --git a/packages/app/src/web/api-http.ts b/packages/app/src/web/api-http.ts index 74d264f1..462e493d 100644 --- a/packages/app/src/web/api-http.ts +++ b/packages/app/src/web/api-http.ts @@ -58,9 +58,11 @@ const decodeSchema = (schema: Schema.Schema, value: string): Effect. }) const readErrorMessage = (status: number, text: string): Effect.Effect => - parseResponseBody(text).pipe( - Effect.flatMap((payload) => Effect.fail(payload === null ? `HTTP ${status}` : renderJsonPayload(payload))) - ) + status === 429 + ? Effect.fail("HTTP 429: tunnel or proxy rate limited the request. Retry or request a fresh tunnel URL.") + : parseResponseBody(text).pipe( + Effect.flatMap((payload) => Effect.fail(payload === null ? `HTTP ${status}` : renderJsonPayload(payload))) + ) const toRequestBody = (body: JsonRequest | undefined) => body === undefined ? HttpBody.empty : HttpBody.unsafeJson(body) diff --git a/packages/app/src/web/api-schema.ts b/packages/app/src/web/api-schema.ts index f3065822..cc4af48e 100644 --- a/packages/app/src/web/api-schema.ts +++ b/packages/app/src/web/api-schema.ts @@ -1,6 +1,20 @@ import * as Schema from "@effect/schema/Schema" +type JsonPrimitive = boolean | number | string | null +type JsonValue = JsonPrimitive | JsonObject | ReadonlyArray +type JsonObject = Readonly<{ [key: string]: JsonValue }> + const NullableString = Schema.NullOr(Schema.String) +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 }) + ) +) export const ProjectStatusSchema = Schema.Union( Schema.Literal("running"), @@ -148,12 +162,38 @@ export const AuthTerminalSessionResponseSchema = Schema.Struct({ session: TerminalSessionSchema }) +export const ApiEventSchema = Schema.Struct({ + seq: Schema.Number, + projectId: Schema.String, + type: Schema.Union( + Schema.Literal("snapshot"), + Schema.Literal("project.created"), + Schema.Literal("project.deleted"), + Schema.Literal("project.deployment.status"), + Schema.Literal("project.deployment.log"), + Schema.Literal("project.ssh.session"), + Schema.Literal("agent.started"), + Schema.Literal("agent.output"), + Schema.Literal("agent.exited"), + Schema.Literal("agent.stopped"), + Schema.Literal("agent.error") + ), + at: Schema.String, + payload: JsonValueSchema +}) + +export const ProjectEventsPollResponseSchema = Schema.Struct({ + cursor: Schema.Number, + events: Schema.Array(ApiEventSchema) +}) + export type ProjectSummary = Schema.Schema.Type export type ProjectDetails = Schema.Schema.Type export type GithubAuthStatus = Schema.Schema.Type export type AuthSnapshot = Schema.Schema.Type export type ProjectAuthSnapshot = Schema.Schema.Type export type TerminalSession = Schema.Schema.Type +export type ApiEvent = Schema.Schema.Type export type DashboardData = { readonly apiBaseUrl: string diff --git a/packages/app/src/web/api.ts b/packages/app/src/web/api.ts index dafe2627..211576dc 100644 --- a/packages/app/src/web/api.ts +++ b/packages/app/src/web/api.ts @@ -8,6 +8,7 @@ import { HealthResponseSchema, OutputResponseSchema, ProjectAuthSnapshotResponseSchema, + ProjectEventsPollResponseSchema, ProjectResponseSchema, ProjectsResponseSchema, TerminalSessionResponseSchema @@ -15,6 +16,7 @@ import { import type { AuthMenuFlow, CreateProjectDraft, DashboardData, ProjectAuthFlow } from "./api-schema.js" export type { + ApiEvent, AuthMenuFlow, AuthSnapshot, CreateProjectDraft, @@ -128,6 +130,18 @@ export const loginGithub = (label: string | null) => Effect.map((response) => response.status) ) +export const loadProjectEvents = ( + projectId: string, + cursor?: number +) => + requestJson( + "GET", + cursor === undefined + ? `/projects/${encodeURIComponent(projectId)}/events-poll` + : `/projects/${encodeURIComponent(projectId)}/events-poll?cursor=${cursor}`, + ProjectEventsPollResponseSchema + ) + export const loadAuthSnapshot = () => requestJson("GET", "/auth/menu", AuthSnapshotResponseSchema).pipe( Effect.map((response) => response.snapshot) diff --git a/packages/app/src/web/app-ready-controller.ts b/packages/app/src/web/app-ready-controller.ts index 621845e3..33a2abfb 100644 --- a/packages/app/src/web/app-ready-controller.ts +++ b/packages/app/src/web/app-ready-controller.ts @@ -48,6 +48,7 @@ const useReadySideEffects = ( useProjectDetailsReset(args.state.selectedProjectId, args.state.setSelectedProject) usePanelAutoload({ authSnapshot: args.state.authSnapshot, + busyLabel: args.state.busyLabel, context: args.actionContext, currentMenu: args.currentMenu, dashboardRefreshTick: args.dashboardRefreshTick, diff --git a/packages/app/src/web/app-ready-hooks.ts b/packages/app/src/web/app-ready-hooks.ts index bf295a8a..78107412 100644 --- a/packages/app/src/web/app-ready-hooks.ts +++ b/packages/app/src/web/app-ready-hooks.ts @@ -32,6 +32,7 @@ type SelectionSyncArgs = { type PanelAutoloadArgs = { readonly authSnapshot: AuthSnapshot | null + readonly busyLabel: string | null readonly context: BrowserActionContext readonly currentMenu: BrowserMenuTag readonly dashboardRefreshTick: number @@ -183,6 +184,7 @@ export const useProjectNavigationReset = ( export const usePanelAutoload = ({ authSnapshot, + busyLabel, context, currentMenu, dashboardRefreshTick, @@ -192,6 +194,9 @@ export const usePanelAutoload = ({ selectedProjectId }: PanelAutoloadArgs) => { const loadCurrentPanel = useEffectEvent(() => { + if (busyLabel !== null) { + return + } if (shouldRefreshAuthPanel(currentMenu, authSnapshot)) { refreshAuthPanel(context) } @@ -207,6 +212,7 @@ export const usePanelAutoload = ({ loadCurrentPanel() }, [ authSnapshot, + busyLabel, currentMenu, dashboardRefreshTick, project?.id, diff --git a/packages/app/src/web/app-ready-shortcut-runtime.ts b/packages/app/src/web/app-ready-shortcut-runtime.ts index 6d1ff246..a69b2498 100644 --- a/packages/app/src/web/app-ready-shortcut-runtime.ts +++ b/packages/app/src/web/app-ready-shortcut-runtime.ts @@ -132,7 +132,12 @@ const handleReadyShortcut = ( ) { return true } - return handleActionKey(event, args.currentMenu, args.context) + return handleActionKey( + event, + args.currentMenu, + args.projectNavigationArmed, + args.context + ) } export const dispatchBrowserShortcut = ( diff --git a/packages/app/src/web/app-ready-shortcuts.ts b/packages/app/src/web/app-ready-shortcuts.ts index 29208667..0fe42495 100644 --- a/packages/app/src/web/app-ready-shortcuts.ts +++ b/packages/app/src/web/app-ready-shortcuts.ts @@ -185,10 +185,14 @@ export const handleProjectNavigationKey = ( export const handleActionKey = ( event: ShortcutKeyboardEvent, currentMenu: BrowserMenuTag, + projectNavigationArmed: boolean, context: BrowserActionContext ): boolean => { if (event.key === "Enter") { - if (isNativeActionTarget(event.target)) { + if ( + isNativeActionTarget(event.target) && + !(usesProjectPrimaryNavigation(currentMenu) && projectNavigationArmed) + ) { return false } event.preventDefault() diff --git a/packages/app/src/web/elements.tsx b/packages/app/src/web/elements.tsx index 6c5ed8c0..1e2724e7 100644 --- a/packages/app/src/web/elements.tsx +++ b/packages/app/src/web/elements.tsx @@ -82,7 +82,12 @@ const textStyle = (props: GridElementProps): CSSProperties => ({ export const Box = ({ children, onClick, ...props }: GridElementProps): JSX.Element => createElement(onClick === undefined ? "div" : "button", { children, - onClick, + onClick: onClick === undefined + ? undefined + : ((event: Parameters>[0]) => { + onClick(event) + event.currentTarget.blur() + }) satisfies MouseEventHandler, style: { ...baseStyle(props), ...interactiveStyle(onClick, props.width) diff --git a/packages/app/src/web/project-events.ts b/packages/app/src/web/project-events.ts new file mode 100644 index 00000000..b302f10c --- /dev/null +++ b/packages/app/src/web/project-events.ts @@ -0,0 +1,166 @@ +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 formatStatusLine = (payload: JsonValue | undefined): string | null => { + const object = asObject(payload) + if (object === null) { + return null + } + const phase = asString(object["phase"]) + const message = asString(object["message"]) + if (message === null) { + return null + } + return phase === null ? message : `[${phase}] ${message}` +} + +const formatLogLine = (payload: JsonValue | undefined): string | null => { + const object = asObject(payload) + if (object === null) { + return null + } + const line = asString(object["line"]) + return line +} + +const formatSshLine = (payload: JsonValue | undefined): string | null => { + const object = asObject(payload) + if (object === null) { + return null + } + const phase = asString(object["phase"]) + const sessionId = asString(object["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) + } + } + const delayMs = isInitialPoll ? 100 : response.events.length === 0 ? 500 : 150 + 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) + } + } + } +} From 0b03ecd15d189e14b662b93980174f880e1e3bf4 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:35:31 +0000 Subject: [PATCH 04/26] fix(ci): restore app effect lint after merge --- packages/app/package.json | 2 +- packages/app/scripts/serve-dist-web.mjs | 153 ++++++++++++++++++ packages/app/src/lib/shell/docker.ts | 2 +- .../lib/usecases/actions/create-project.ts | 22 ++- 4 files changed, 171 insertions(+), 8 deletions(-) create mode 100644 packages/app/scripts/serve-dist-web.mjs diff --git a/packages/app/package.json b/packages/app/package.json index 467b82fd..8a29144d 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -20,7 +20,7 @@ "prepack": "bun run build:docker-git", "dev": "vite build --watch --ssr src/app/main.ts", "dev:web": "vite --config vite.web.config.ts", - "serve:web": "bun scripts/serve-dist-web.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/", 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/lib/shell/docker.ts b/packages/app/src/lib/shell/docker.ts index fd4df315..26b54224 100644 --- a/packages/app/src/lib/shell/docker.ts +++ b/packages/app/src/lib/shell/docker.ts @@ -359,7 +359,7 @@ export const runDockerInspectContainerRuntimeInfo = ( })) ) }), - Effect.catchAll(() => Effect.succeed(null)) + Effect.catchTag("DockerCommandError", () => Effect.succeed(null)) ) // CHANGE: inspect the container IP address on the default `bridge` network diff --git a/packages/app/src/lib/usecases/actions/create-project.ts b/packages/app/src/lib/usecases/actions/create-project.ts index cf6d19a0..f25ae4ae 100644 --- a/packages/app/src/lib/usecases/actions/create-project.ts +++ b/packages/app/src/lib/usecases/actions/create-project.ts @@ -263,18 +263,28 @@ type DockerIdentityClaim = Omit readonly namespace: DockerIdentityNamespace } +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 }, - ...(config.enableMcpPlaywright - ? [{ namespace: "container" as const, kind: "browserContainerName" as const, name: `${config.containerName}-browser` }] - : []), + ...resolveBrowserContainerClaims(config), { 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` }] - : []), + ...resolveBrowserVolumeClaims(config), { namespace: "volume", kind: "bootstrapVolumeName", name: resolveProjectBootstrapVolumeName(config) } ] From 040b1234e2b557db93fd778a1a9584139afbd1e0 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:35:47 +0000 Subject: [PATCH 05/26] fix(ci): remove legacy typed web serve script --- packages/app/scripts/serve-dist-web.ts | 153 ------------------------- 1 file changed, 153 deletions(-) delete mode 100644 packages/app/scripts/serve-dist-web.ts diff --git a/packages/app/scripts/serve-dist-web.ts b/packages/app/scripts/serve-dist-web.ts deleted file mode 100644 index ab58e373..00000000 --- a/packages/app/scripts/serve-dist-web.ts +++ /dev/null @@ -1,153 +0,0 @@ -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: Readonly> = { - ".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: string): string => { - const normalized = normalize(pathname) - return normalized.startsWith(staticRoot) - ? normalized - : join(staticRoot, normalized) -} - -const serveFile = ( - pathname: string, - response: import("node:http").ServerResponse -): void => { - 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: string): string => { - const parsed = new URL(url, "http://localhost") - const pathname = parsed.pathname.replace(/^\/api/u, "") || "/" - return `${pathname}${parsed.search}` -} - -const proxyHttp = ( - request: import("node:http").IncomingMessage, - response: import("node:http").ServerResponse -): void => { - 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}`) -}) From 32144e9b1bfc83f555478b0233a9548c81c07b05 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:44:09 +0000 Subject: [PATCH 06/26] fix(ci): stabilize merged app and lib checks --- .../create-project-identity-conflict.test.ts | 10 +-- .../docker-git/docker-runtime-info.test.ts | 40 ++++++++---- .../fixtures/open-project-helpers.ts | 44 +++++++++++++ .../app/tests/docker-git/open-project.test.ts | 65 +++++-------------- .../src/usecases/actions/create-project.ts | 22 +++++-- 5 files changed, 111 insertions(+), 70 deletions(-) create mode 100644 packages/app/tests/docker-git/fixtures/open-project-helpers.ts 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 index c9cf3643..47abd0e0 100644 --- a/packages/app/tests/docker-git/create-project-identity-conflict.test.ts +++ b/packages/app/tests/docker-git/create-project-identity-conflict.test.ts @@ -1,12 +1,12 @@ -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 * as FileSystem from "@effect/platform/FileSystem" +import * as Path from "@effect/platform/Path" 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 { type CreateCommand, defaultTemplateConfig } from "@lib/core/domain" import { DockerIdentityConflictError } from "../../src/lib/shell/errors.js" import { createProject } from "../../src/lib/usecases/actions/create-project.js" @@ -14,7 +14,9 @@ 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 getContainerIpIfInsideContainerMock = vi.hoisted(() => + vi.fn(() => Effect.sync((): string | undefined => undefined)) +) const loadProjectIndexMock = vi.hoisted(() => vi.fn()) const loadProjectStatusMock = vi.hoisted(() => vi.fn()) const migrateProjectOrchLayoutMock = vi.hoisted(() => vi.fn(() => Effect.void)) diff --git a/packages/app/tests/docker-git/docker-runtime-info.test.ts b/packages/app/tests/docker-git/docker-runtime-info.test.ts index 01aa4eec..cd257ea0 100644 --- a/packages/app/tests/docker-git/docker-runtime-info.test.ts +++ b/packages/app/tests/docker-git/docker-runtime-info.test.ts @@ -14,6 +14,7 @@ type RecordedCommand = { } const encode = (value: string): Uint8Array => new TextEncoder().encode(value) +const joinIp = (...octets: ReadonlyArray): string => octets.join(".") const isRuntimeInspect = (command: RecordedCommand): boolean => command.command === "docker" && @@ -27,24 +28,36 @@ const isIpInspect = (command: RecordedCommand): boolean => command.args[1] === "-f" && (command.args[2] ?? "").includes("NetworkSettings.Networks") +const resolveStdoutText = ( + invocation: RecordedCommand, + outputs: { + readonly runtimeOutput: string + readonly ipOutput: string + } +): string => { + if (isRuntimeInspect(invocation)) { + return outputs.runtimeOutput + } + if (isIpInspect(invocation)) { + return outputs.ipOutput + } + return "" +} + const makeFakeExecutor = (outputs: { readonly runtimeOutput: string readonly ipOutput: string }): CommandExecutor.CommandExecutor => { - const start = (command: Command.Command): Effect.Effect => - Effect.gen(function*(_) { + const start = (command: Command.Command): Effect.Effect => + Effect.sync(() => { const flattened = Command.flatten(command) - const last = flattened[flattened.length - 1]! + const last = flattened.at(-1)! const invocation: RecordedCommand = { command: last.command, args: last.args } - const stdoutText = isRuntimeInspect(invocation) - ? outputs.runtimeOutput - : isIpInspect(invocation) - ? outputs.ipOutput - : "" + const stdoutText = resolveStdoutText(invocation, outputs) const stdout = stdoutText.length === 0 ? Stream.empty @@ -77,9 +90,11 @@ const makeFakeExecutor = (outputs: { describe("runDockerInspectContainerRuntimeInfo", () => { it.effect("parses running runtime ownership even when separators arrive as literal escapes", () => Effect.gen(function*(_) { + const bridgeIp = joinIp(172, 17, 0, 15) + const projectIp = joinIp(10, 88, 0, 2) 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" + ipOutput: `bridge=${bridgeIp}\nproject=${projectIp}\n` }) const runtime = yield* _( @@ -91,7 +106,7 @@ describe("runDockerInspectContainerRuntimeInfo", () => { expect(runtime).toEqual({ containerName: "dg-repo", running: true, - ipAddress: "172.17.0.15", + ipAddress: bridgeIp, projectWorkingDir: "/home/dev/.docker-git/test-owner/repo", composeService: "dg-repo" }) @@ -99,9 +114,10 @@ describe("runDockerInspectContainerRuntimeInfo", () => { it.effect("keeps optional compose labels undefined when runtime is unlabeled", () => Effect.gen(function*(_) { + const projectIp = joinIp(10, 88, 0, 4) const executor = makeFakeExecutor({ runtimeOutput: "running\t\t\n", - ipOutput: "project=10.88.0.4\n" + ipOutput: `project=${projectIp}\n` }) const runtime = yield* _( @@ -113,7 +129,7 @@ describe("runDockerInspectContainerRuntimeInfo", () => { expect(runtime).toEqual({ containerName: "dg-repo", running: true, - ipAddress: "10.88.0.4", + ipAddress: projectIp, projectWorkingDir: undefined, composeService: undefined }) 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..546ad2b7 --- /dev/null +++ b/packages/app/tests/docker-git/fixtures/open-project-helpers.ts @@ -0,0 +1,44 @@ +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", + 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 + +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/open-project.test.ts b/packages/app/tests/docker-git/open-project.test.ts index c4365cb2..29c6c80e 100644 --- a/packages/app/tests/docker-git/open-project.test.ts +++ b/packages/app/tests/docker-git/open-project.test.ts @@ -2,51 +2,20 @@ 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 { + openResolvedProjectSshEffect, + resolveOpenProjectEffect, + selectOpenProject +} from "../../src/docker-git/open-project.js" +import { expectSelectedProject, liveFallbackIp, liveRuntimeIp, makeProject } from "./fixtures/open-project-helpers.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) - }) - 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" + sshCommand: `ssh -p 22 dev@${liveFallbackIp}` }) const events: Array = [] @@ -70,7 +39,7 @@ describe("selectOpenProject", () => { ) expect(events).toEqual([ - "log:Opening SSH: ssh -p 22 dev@172.17.0.20", + `log:Opening SSH: ssh -p 22 dev@${liveFallbackIp}`, "connect:/controller/org/repo/issue-7" ]) })) @@ -117,8 +86,8 @@ describe("selectOpenProject", () => { }) const preferred = makeProjectItem({ ...item, - ipAddress: "172.17.0.15", - sshCommand: "ssh -p 22 dev@172.17.0.15" + ipAddress: liveRuntimeIp, + sshCommand: `ssh -p 22 dev@${liveRuntimeIp}` }) const events: Array = [] @@ -129,7 +98,7 @@ describe("selectOpenProject", () => { events.push(`log:${message}`) }), resolvePreferredItem: () => Effect.succeed(preferred), - probeReady: (selected) => Effect.succeed(selected.ipAddress === "172.17.0.15"), + probeReady: (selected) => Effect.succeed(selected.ipAddress === liveRuntimeIp), connect: (selected) => Effect.sync(() => { events.push(`connect:${selected.sshCommand}`) @@ -142,8 +111,8 @@ describe("selectOpenProject", () => { ) expect(events).toEqual([ - "log:Opening SSH: ssh -p 22 dev@172.17.0.15", - "connect:ssh -p 22 dev@172.17.0.15" + `log:Opening SSH: ssh -p 22 dev@${liveRuntimeIp}`, + `connect:ssh -p 22 dev@${liveRuntimeIp}` ]) })) @@ -156,8 +125,8 @@ describe("selectOpenProject", () => { }) const preferred = makeProjectItem({ ...item, - ipAddress: "172.17.0.20", - sshCommand: "ssh -p 22 dev@172.17.0.20" + ipAddress: liveFallbackIp, + sshCommand: `ssh -p 22 dev@${liveFallbackIp}` }) const events: Array = [] @@ -168,7 +137,7 @@ describe("selectOpenProject", () => { events.push(`log:${message}`) }), resolvePreferredItem: () => Effect.succeed(preferred), - probeReady: (selected) => Effect.succeed(selected.ipAddress !== "172.17.0.20"), + probeReady: (selected) => Effect.succeed(selected.ipAddress !== liveFallbackIp), connect: (selected) => Effect.sync(() => { events.push(`connect:${selected.sshCommand}`) @@ -272,7 +241,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/lib/src/usecases/actions/create-project.ts b/packages/lib/src/usecases/actions/create-project.ts index 842e5a84..9748f920 100644 --- a/packages/lib/src/usecases/actions/create-project.ts +++ b/packages/lib/src/usecases/actions/create-project.ts @@ -105,18 +105,28 @@ type DockerIdentityClaim = Omit readonly namespace: DockerIdentityNamespace } +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 }, - ...(config.enableMcpPlaywright - ? [{ namespace: "container" as const, kind: "browserContainerName" as const, name: `${config.containerName}-browser` }] - : []), + ...resolveBrowserContainerClaims(config), { 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` }] - : []), + ...resolveBrowserVolumeClaims(config), { namespace: "volume", kind: "bootstrapVolumeName", name: resolveProjectBootstrapVolumeName(config) } ] From 060bd16c2f4f44e3d4c3a0003fe611561433cd8f Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:46:54 +0000 Subject: [PATCH 07/26] test(app): split open-project SSH cases --- .../tests/docker-git/open-project-ssh.test.ts | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 packages/app/tests/docker-git/open-project-ssh.test.ts 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..f632af85 --- /dev/null +++ b/packages/app/tests/docker-git/open-project-ssh.test.ts @@ -0,0 +1,152 @@ +import { describe, expect, it } from "@effect/vitest" +import { Effect } from "effect" + +import { openResolvedProjectSshEffect } from "../../src/docker-git/open-project.js" +import { liveFallbackIp, liveRuntimeIp } from "./fixtures/open-project-helpers.js" +import { makeProjectItem } from "./fixtures/project-item.js" + +describe("openResolvedProjectSshEffect", () => { + 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@${liveFallbackIp}` + }) + 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@${liveFallbackIp}`, + "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: liveRuntimeIp, + sshCommand: `ssh -p 22 dev@${liveRuntimeIp}` + }) + const events: Array = [] + + yield* _( + openResolvedProjectSshEffect(item, { + log: (message) => + Effect.sync(() => { + events.push(`log:${message}`) + }), + resolvePreferredItem: () => Effect.succeed(preferred), + probeReady: (selected) => Effect.succeed(selected.ipAddress === liveRuntimeIp), + 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@${liveRuntimeIp}`, + `connect:ssh -p 22 dev@${liveRuntimeIp}` + ]) + })) + + 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: liveFallbackIp, + sshCommand: `ssh -p 22 dev@${liveFallbackIp}` + }) + const events: Array = [] + + yield* _( + openResolvedProjectSshEffect(item, { + log: (message) => + Effect.sync(() => { + events.push(`log:${message}`) + }), + resolvePreferredItem: () => Effect.succeed(preferred), + probeReady: (selected) => Effect.succeed(selected.ipAddress !== liveFallbackIp), + 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" + ]) + })) +}) From f800c05aa58f21cc93e6daf9c405ef61172df4b5 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:47:22 +0000 Subject: [PATCH 08/26] test(app): extract docker runtime fixture loader --- .../docker-git/docker-runtime-info.test.ts | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/app/tests/docker-git/docker-runtime-info.test.ts b/packages/app/tests/docker-git/docker-runtime-info.test.ts index cd257ea0..ea599350 100644 --- a/packages/app/tests/docker-git/docker-runtime-info.test.ts +++ b/packages/app/tests/docker-git/docker-runtime-info.test.ts @@ -87,21 +87,25 @@ const makeFakeExecutor = (outputs: { return CommandExecutor.makeExecutor(start) } +const loadRuntimeInfo = ( + outputs: { + readonly runtimeOutput: string + readonly ipOutput: string + } +) => + runDockerInspectContainerRuntimeInfo("/tmp", "dg-repo").pipe( + Effect.provideService(CommandExecutor.CommandExecutor, makeFakeExecutor(outputs)) + ) + describe("runDockerInspectContainerRuntimeInfo", () => { it.effect("parses running runtime ownership even when separators arrive as literal escapes", () => Effect.gen(function*(_) { const bridgeIp = joinIp(172, 17, 0, 15) const projectIp = joinIp(10, 88, 0, 2) - const executor = makeFakeExecutor({ + const runtime = yield* _(loadRuntimeInfo({ runtimeOutput: "running\\t/home/dev/.docker-git/test-owner/repo\\tdg-repo\n", ipOutput: `bridge=${bridgeIp}\nproject=${projectIp}\n` - }) - - const runtime = yield* _( - runDockerInspectContainerRuntimeInfo("/tmp", "dg-repo").pipe( - Effect.provideService(CommandExecutor.CommandExecutor, executor) - ) - ) + })) expect(runtime).toEqual({ containerName: "dg-repo", @@ -115,16 +119,10 @@ describe("runDockerInspectContainerRuntimeInfo", () => { it.effect("keeps optional compose labels undefined when runtime is unlabeled", () => Effect.gen(function*(_) { const projectIp = joinIp(10, 88, 0, 4) - const executor = makeFakeExecutor({ + const runtime = yield* _(loadRuntimeInfo({ runtimeOutput: "running\t\t\n", ipOutput: `project=${projectIp}\n` - }) - - const runtime = yield* _( - runDockerInspectContainerRuntimeInfo("/tmp", "dg-repo").pipe( - Effect.provideService(CommandExecutor.CommandExecutor, executor) - ) - ) + })) expect(runtime).toEqual({ containerName: "dg-repo", From eeeea4550951f8200317b958e51830e5270a4643 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:47:58 +0000 Subject: [PATCH 09/26] test(app): deduplicate identity conflict setup --- .../create-project-identity-conflict.test.ts | 61 +++++++++++-------- 1 file changed, 34 insertions(+), 27 deletions(-) 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 index 47abd0e0..4d22cbdf 100644 --- a/packages/app/tests/docker-git/create-project-identity-conflict.test.ts +++ b/packages/app/tests/docker-git/create-project-identity-conflict.test.ts @@ -121,6 +121,34 @@ const makeCommand = ( waitForClone: false }) +const makeConflictContext = ( + root: string, + path: Path.Path, + force: boolean +) => { + const outDir = path.join(root, "candidate") + const existingDir = path.join(root, "existing") + return { + outDir, + existingDir, + existingConfigPath: path.join(existingDir, "docker-git.json"), + command: makeCommand(root, outDir, force) + } +} + +const mockProjectIndex = ( + root: string, + path: Path.Path, + configPaths: ReadonlyArray +): void => { + loadProjectIndexMock.mockReturnValue( + Effect.succeed({ + projectsRoot: path.join(root, ".docker-git"), + configPaths + }) + ) +} + describe("createProject docker identity guard", () => { beforeEach(() => { loadProjectIndexMock.mockReset() @@ -138,17 +166,9 @@ describe("createProject docker identity guard", () => { 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] - }) - ) + const { command, existingConfigPath, existingDir, outDir } = makeConflictContext(root, path, false) + + mockProjectIndex(root, path, [existingConfigPath]) loadProjectStatusMock.mockImplementation((configPath: string) => Effect.succeed( makeStatus( @@ -187,17 +207,9 @@ describe("createProject docker identity guard", () => { 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) + const { command, existingConfigPath, existingDir } = makeConflictContext(root, path, true) - loadProjectIndexMock.mockReturnValue( - Effect.succeed({ - projectsRoot: path.join(root, ".docker-git"), - configPaths: [existingConfigPath] - }) - ) + mockProjectIndex(root, path, [existingConfigPath]) loadProjectStatusMock.mockReturnValue( Effect.succeed(makeStatus(existingDir, root)) ) @@ -224,12 +236,7 @@ describe("createProject docker identity guard", () => { 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] - }) - ) + mockProjectIndex(root, path, [configPath]) loadProjectStatusMock.mockReturnValue( Effect.succeed(makeStatus(outDir, root)) ) From ea3d0f32edf06790374ebd7e3da7307ae4ff34f2 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 9 Apr 2026 20:13:00 +0000 Subject: [PATCH 10/26] fix(ci): restore app lint and test checks --- packages/app/src/lib/shell/command-runner.ts | 19 +- .../actions/create-project-conflicts.ts | 179 +++++++++++ .../actions/create-project-open-ssh.ts | 119 +++++++ .../lib/usecases/actions/create-project.ts | 295 +++--------------- packages/app/src/web/project-events.ts | 7 +- .../fixtures/open-project-ssh-helpers.ts | 52 +++ .../tests/docker-git/open-project-ssh.test.ts | 86 +---- 7 files changed, 413 insertions(+), 344 deletions(-) create mode 100644 packages/app/src/lib/usecases/actions/create-project-conflicts.ts create mode 100644 packages/app/src/lib/usecases/actions/create-project-open-ssh.ts create mode 100644 packages/app/tests/docker-git/fixtures/open-project-ssh-helpers.ts 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/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..2fdbecfa --- /dev/null +++ b/packages/app/src/lib/usecases/actions/create-project-open-ssh.ts @@ -0,0 +1,119 @@ +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 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 + 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/app/src/lib/usecases/actions/create-project.ts b/packages/app/src/lib/usecases/actions/create-project.ts index f25ae4ae..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,107 +136,6 @@ const resolveRuntimeConfig = ( : { ...finalAgentConfig, clonedOnHostname } }) -type DockerIdentityOwner = Pick - -type DockerIdentityNamespace = "container" | "composeProject" | "volume" - -type DockerIdentityClaim = Omit & { - readonly namespace: DockerIdentityNamespace -} - -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 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 @@ -368,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())) @@ -384,7 +164,6 @@ const runCreateProject = ( yield* _( deleteConflictingProjectsIfNeeded(resolvedOutDir, rootedConfig, command.force) ) - yield* _(validateGithubCloneAuthTokenPreflight(rootedConfig)) const resolvedConfig = yield* _(resolveCreateConfig(rootedConfig, resolvedOutDir)) @@ -392,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, @@ -400,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, @@ -427,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/web/project-events.ts b/packages/app/src/web/project-events.ts index b302f10c..0df3a7cf 100644 --- a/packages/app/src/web/project-events.ts +++ b/packages/app/src/web/project-events.ts @@ -122,7 +122,12 @@ const handlePollSuccess = ( onLine(line) } } - const delayMs = isInitialPoll ? 100 : response.events.length === 0 ? 500 : 150 + let delayMs = 150 + if (isInitialPoll) { + delayMs = 100 + } else if (response.events.length === 0) { + delayMs = 500 + } schedulePoll(state, runPoll, delayMs) } diff --git a/packages/app/tests/docker-git/fixtures/open-project-ssh-helpers.ts b/packages/app/tests/docker-git/fixtures/open-project-ssh-helpers.ts new file mode 100644 index 00000000..59c81e74 --- /dev/null +++ b/packages/app/tests/docker-git/fixtures/open-project-ssh-helpers.ts @@ -0,0 +1,52 @@ +import { Effect } from "effect" + +import type { ProjectItem } from "@lib/usecases/projects" + +import { openResolvedProjectSshEffect } from "../../../src/docker-git/open-project.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 +} + +type OpenResolvedProjectSshOptions = + & Partial< + Omit + > + & { + readonly connectEntry?: (selected: ProjectItem) => string + readonly upEntry?: (selected: ProjectItem) => string + } + +const record = (events: Array, entry: string): Effect.Effect => + Effect.sync(() => { + events.push(entry) + }) + +export const makeOpenResolvedProjectSshDeps = ( + events: Array, + options: OpenResolvedProjectSshOptions = {} +): OpenResolvedProjectSshDeps => { + const { connectEntry, upEntry, ...overrides } = options + return { + log: (message) => record(events, `log:${message}`), + resolvePreferredItem: () => Effect.succeed(null), + probeReady: () => Effect.succeed(true), + connect: (selected) => record(events, connectEntry?.(selected) ?? `connect:${selected.projectDir}`), + connectWithUp: (selected) => record(events, upEntry?.(selected) ?? `up:${selected.projectDir}`), + ...overrides + } +} + +export const captureOpenResolvedProjectSshEvents = ( + item: ProjectItem, + options: OpenResolvedProjectSshOptions = {} +): Effect.Effect> => + Effect.gen(function*(_) { + const events: Array = [] + yield* _(openResolvedProjectSshEffect(item, makeOpenResolvedProjectSshDeps(events, options))) + return events + }) diff --git a/packages/app/tests/docker-git/open-project-ssh.test.ts b/packages/app/tests/docker-git/open-project-ssh.test.ts index f632af85..4776f108 100644 --- a/packages/app/tests/docker-git/open-project-ssh.test.ts +++ b/packages/app/tests/docker-git/open-project-ssh.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "@effect/vitest" import { Effect } from "effect" -import { openResolvedProjectSshEffect } from "../../src/docker-git/open-project.js" import { liveFallbackIp, liveRuntimeIp } from "./fixtures/open-project-helpers.js" +import { captureOpenResolvedProjectSshEvents } from "./fixtures/open-project-ssh-helpers.js" import { makeProjectItem } from "./fixtures/project-item.js" describe("openResolvedProjectSshEffect", () => { @@ -12,27 +12,7 @@ describe("openResolvedProjectSshEffect", () => { projectDir: "/controller/org/repo/issue-7", sshCommand: `ssh -p 22 dev@${liveFallbackIp}` }) - 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}`) - }) - }) - ) - + const events = yield* _(captureOpenResolvedProjectSshEvents(item)) expect(events).toEqual([ `log:Opening SSH: ssh -p 22 dev@${liveFallbackIp}`, "connect:/controller/org/repo/issue-7" @@ -45,27 +25,11 @@ describe("openResolvedProjectSshEffect", () => { 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}`) - }) + const events = yield* _( + captureOpenResolvedProjectSshEvents(item, { + probeReady: () => Effect.succeed(false) }) ) - expect(events).toEqual([ "log:Opening SSH: ssh -p 2222 dev@localhost", "up:/controller/org/repo/issue-8" @@ -84,27 +48,13 @@ describe("openResolvedProjectSshEffect", () => { ipAddress: liveRuntimeIp, sshCommand: `ssh -p 22 dev@${liveRuntimeIp}` }) - const events: Array = [] - - yield* _( - openResolvedProjectSshEffect(item, { - log: (message) => - Effect.sync(() => { - events.push(`log:${message}`) - }), + const events = yield* _( + captureOpenResolvedProjectSshEvents(item, { resolvePreferredItem: () => Effect.succeed(preferred), probeReady: (selected) => Effect.succeed(selected.ipAddress === liveRuntimeIp), - connect: (selected) => - Effect.sync(() => { - events.push(`connect:${selected.sshCommand}`) - }), - connectWithUp: (selected) => - Effect.sync(() => { - events.push(`up:${selected.projectDir}`) - }) + connectEntry: (selected) => `connect:${selected.sshCommand}` }) ) - expect(events).toEqual([ `log:Opening SSH: ssh -p 22 dev@${liveRuntimeIp}`, `connect:ssh -p 22 dev@${liveRuntimeIp}` @@ -123,27 +73,13 @@ describe("openResolvedProjectSshEffect", () => { ipAddress: liveFallbackIp, sshCommand: `ssh -p 22 dev@${liveFallbackIp}` }) - const events: Array = [] - - yield* _( - openResolvedProjectSshEffect(item, { - log: (message) => - Effect.sync(() => { - events.push(`log:${message}`) - }), + const events = yield* _( + captureOpenResolvedProjectSshEvents(item, { resolvePreferredItem: () => Effect.succeed(preferred), probeReady: (selected) => Effect.succeed(selected.ipAddress !== liveFallbackIp), - connect: (selected) => - Effect.sync(() => { - events.push(`connect:${selected.sshCommand}`) - }), - connectWithUp: (selected) => - Effect.sync(() => { - events.push(`up:${selected.projectDir}`) - }) + connectEntry: (selected) => `connect:${selected.sshCommand}` }) ) - expect(events).toEqual([ "log:Opening SSH: ssh -p 2237 dev@localhost", "connect:ssh -p 2237 dev@localhost" From 5c2a9777f39ef01b797e439ca27803a2a19abf73 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 9 Apr 2026 20:25:53 +0000 Subject: [PATCH 11/26] fix(ci): split app lint and test helpers --- .../app/src/docker-git/open-project-ssh.ts | 105 ++++ packages/app/src/docker-git/open-project.ts | 122 +--- packages/app/src/lib/shell/docker-compose.ts | 120 ++++ packages/app/src/lib/shell/docker-network.ts | 93 +++ packages/app/src/lib/shell/docker-runtime.ts | 155 +++++ packages/app/src/lib/shell/docker.ts | 577 +----------------- .../docker-git/fixtures/event-recorder.ts | 6 + .../fixtures/open-project-ssh-helpers.ts | 12 +- .../docker-git/menu-select-connect.test.ts | 10 +- .../tests/docker-git/open-project-ssh.test.ts | 110 ++-- 10 files changed, 566 insertions(+), 744 deletions(-) create mode 100644 packages/app/src/docker-git/open-project-ssh.ts create mode 100644 packages/app/src/lib/shell/docker-compose.ts create mode 100644 packages/app/src/lib/shell/docker-network.ts create mode 100644 packages/app/src/lib/shell/docker-runtime.ts create mode 100644 packages/app/tests/docker-git/fixtures/event-recorder.ts 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..0619aae1 --- /dev/null +++ b/packages/app/src/docker-git/open-project-ssh.ts @@ -0,0 +1,105 @@ +import { defaultTemplateConfig } from "@lib/core/domain" +import { runDockerInspectContainerRuntimeInfo } from "@lib/shell/docker" +import { buildSshCommand, connectProjectSsh, probeProjectSshReady, type ProjectItem } from "@lib/usecases/projects" +import { Effect, pipe } from "effect" + +import { connectMenuProjectSshWithUp } from "./menu-api.js" + +export 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 +} + +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) + }) diff --git a/packages/app/src/docker-git/open-project.ts b/packages/app/src/docker-git/open-project.ts index 0ec6b433..e68169de 100644 --- a/packages/app/src/docker-git/open-project.ts +++ b/packages/app/src/docker-git/open-project.ts @@ -1,7 +1,5 @@ -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 { type DockerContainerRuntimeInfo, runDockerInspectContainerRuntimeInfo } from "@lib/shell/docker" +import { Effect } from "effect" import type { OpenCommand } from "@lib/core/domain" import { parseGithubRepoUrl, resolveRepoInput } from "@lib/core/repo" @@ -9,16 +7,14 @@ import { parseGithubRepoUrl, resolveRepoInput } from "@lib/core/repo" 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 { + openResolvedProjectSsh, + type OpenResolvedProjectSshDeps, + openResolvedProjectSshEffect +} from "./open-project-ssh.js" type ResolveOpenProjectDeps = { readonly inspectRuntime: (containerName: string) => Effect.Effect @@ -221,8 +217,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 +254,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,99 +272,6 @@ 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 ) => 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..0e8f4afc --- /dev/null +++ b/packages/app/src/lib/shell/docker-compose.ts @@ -0,0 +1,120 @@ +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 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) + ) + +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..e6dc0c58 --- /dev/null +++ b/packages/app/src/lib/shell/docker-network.ts @@ -0,0 +1,93 @@ +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" + +export const runDockerNetworkConnectBridge = ( + cwd: string, + containerName: string +): Effect.Effect => + runCommandCapture( + { + cwd, + command: "docker", + args: ["network", "connect", "bridge", containerName] + }, + [Number(ExitCode(0))], + (exitCode) => new DockerCommandError({ exitCode }) + ).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 => + runCommandWithExitCodes( + { + cwd, + command: "docker", + args: ["network", "create", "--driver", "bridge", networkName] + }, + [Number(ExitCode(0))], + (exitCode) => new DockerCommandError({ exitCode }) + ) + +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 }) + ) + +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 + }) + ) + +export const runDockerNetworkRemove = ( + cwd: string, + networkName: string +): Effect.Effect => + runCommandWithExitCodes( + { + cwd, + command: "docker", + args: ["network", "rm", networkName] + }, + [Number(ExitCode(0))], + (exitCode) => new DockerCommandError({ exitCode }) + ) 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..824834b7 --- /dev/null +++ b/packages/app/src/lib/shell/docker-runtime.ts @@ -0,0 +1,155 @@ +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" + +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 +} + +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 = ( + cwd: string, + containerName: string +): Effect.Effect => + pipe( + runCommandCapture( + { + cwd, + command: "docker", + args: [ + "inspect", + "-f", + 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 entryMap = new Map(entries) + return entryMap.get("bridge") ?? entries[0]![1] + }) + ) + +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(String.raw`\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.catchTag("DockerCommandError", () => Effect.succeed(null)) + ) + +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()) + ) + +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 26b54224..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.catchTag("DockerCommandError", () => 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/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-ssh-helpers.ts b/packages/app/tests/docker-git/fixtures/open-project-ssh-helpers.ts index 59c81e74..817a0a09 100644 --- a/packages/app/tests/docker-git/fixtures/open-project-ssh-helpers.ts +++ b/packages/app/tests/docker-git/fixtures/open-project-ssh-helpers.ts @@ -3,6 +3,7 @@ import { Effect } from "effect" import type { ProjectItem } from "@lib/usecases/projects" import { openResolvedProjectSshEffect } from "../../../src/docker-git/open-project.js" +import { recordEvent } from "./event-recorder.js" type OpenResolvedProjectSshDeps = { readonly log: (message: string) => Effect.Effect @@ -21,22 +22,17 @@ type OpenResolvedProjectSshOptions = readonly upEntry?: (selected: ProjectItem) => string } -const record = (events: Array, entry: string): Effect.Effect => - Effect.sync(() => { - events.push(entry) - }) - export const makeOpenResolvedProjectSshDeps = ( events: Array, options: OpenResolvedProjectSshOptions = {} ): OpenResolvedProjectSshDeps => { const { connectEntry, upEntry, ...overrides } = options return { - log: (message) => record(events, `log:${message}`), + log: (message) => recordEvent(events, `log:${message}`), resolvePreferredItem: () => Effect.succeed(null), probeReady: () => Effect.succeed(true), - connect: (selected) => record(events, connectEntry?.(selected) ?? `connect:${selected.projectDir}`), - connectWithUp: (selected) => record(events, upEntry?.(selected) ?? `up:${selected.projectDir}`), + connect: (selected) => recordEvent(events, connectEntry?.(selected) ?? `connect:${selected.projectDir}`), + connectWithUp: (selected) => recordEvent(events, upEntry?.(selected) ?? `up:${selected.projectDir}`), ...overrides } } 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..bce4a18c 100644 --- a/packages/app/tests/docker-git/menu-select-connect.test.ts +++ b/packages/app/tests/docker-git/menu-select-connect.test.ts @@ -5,16 +5,12 @@ import type { ProjectItem } from "@lib/usecases/projects" 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/open-project-ssh.test.ts b/packages/app/tests/docker-git/open-project-ssh.test.ts index 4776f108..619976f6 100644 --- a/packages/app/tests/docker-git/open-project-ssh.test.ts +++ b/packages/app/tests/docker-git/open-project-ssh.test.ts @@ -1,10 +1,52 @@ import { describe, expect, it } from "@effect/vitest" import { Effect } from "effect" +import type { ProjectItem } from "@lib/usecases/projects" + import { liveFallbackIp, liveRuntimeIp } from "./fixtures/open-project-helpers.js" import { captureOpenResolvedProjectSshEvents } from "./fixtures/open-project-ssh-helpers.js" import { makeProjectItem } from "./fixtures/project-item.js" +type RuntimePreferenceCase = { + readonly name: string + readonly projectDir: string + readonly localSshCommand: string + readonly sshPort: number + readonly preferredIp: string + readonly preferredSshCommand: string + readonly probeReady: (selected: ProjectItem) => boolean + readonly expected: ReadonlyArray +} + +const runtimePreferenceCases: ReadonlyArray = [ + { + name: "prefers a live runtime SSH target before falling back to docker up", + projectDir: "/controller/org/repo/issue-9", + localSshCommand: "ssh -p 2253 dev@localhost", + sshPort: 2253, + preferredIp: liveRuntimeIp, + preferredSshCommand: `ssh -p 22 dev@${liveRuntimeIp}`, + probeReady: (selected: ProjectItem) => selected.ipAddress === liveRuntimeIp, + expected: [ + `log:Opening SSH: ssh -p 22 dev@${liveRuntimeIp}`, + `connect:ssh -p 22 dev@${liveRuntimeIp}` + ] + }, + { + name: "falls back to the original SSH target when live runtime probe fails", + projectDir: "/controller/org/repo/issue-10", + localSshCommand: "ssh -p 2237 dev@localhost", + sshPort: 2237, + preferredIp: liveFallbackIp, + preferredSshCommand: `ssh -p 22 dev@${liveFallbackIp}`, + probeReady: (selected: ProjectItem) => selected.ipAddress !== liveFallbackIp, + expected: [ + "log:Opening SSH: ssh -p 2237 dev@localhost", + "connect:ssh -p 2237 dev@localhost" + ] + } +] + describe("openResolvedProjectSshEffect", () => { it.effect("connects directly when SSH is already reachable", () => Effect.gen(function*(_) { @@ -36,53 +78,27 @@ describe("openResolvedProjectSshEffect", () => { ]) })) - 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: liveRuntimeIp, - sshCommand: `ssh -p 22 dev@${liveRuntimeIp}` - }) - const events = yield* _( - captureOpenResolvedProjectSshEvents(item, { - resolvePreferredItem: () => Effect.succeed(preferred), - probeReady: (selected) => Effect.succeed(selected.ipAddress === liveRuntimeIp), - connectEntry: (selected) => `connect:${selected.sshCommand}` + for (const testCase of runtimePreferenceCases) { + it.effect(testCase.name, () => + Effect.gen(function*(_) { + const item = makeProjectItem({ + projectDir: testCase.projectDir, + sshCommand: testCase.localSshCommand, + sshPort: testCase.sshPort }) - ) - expect(events).toEqual([ - `log:Opening SSH: ssh -p 22 dev@${liveRuntimeIp}`, - `connect:ssh -p 22 dev@${liveRuntimeIp}` - ]) - })) - - 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: liveFallbackIp, - sshCommand: `ssh -p 22 dev@${liveFallbackIp}` - }) - const events = yield* _( - captureOpenResolvedProjectSshEvents(item, { - resolvePreferredItem: () => Effect.succeed(preferred), - probeReady: (selected) => Effect.succeed(selected.ipAddress !== liveFallbackIp), - connectEntry: (selected) => `connect:${selected.sshCommand}` + const preferred = makeProjectItem({ + ...item, + ipAddress: testCase.preferredIp, + sshCommand: testCase.preferredSshCommand }) - ) - expect(events).toEqual([ - "log:Opening SSH: ssh -p 2237 dev@localhost", - "connect:ssh -p 2237 dev@localhost" - ]) - })) + const events = yield* _( + captureOpenResolvedProjectSshEvents(item, { + resolvePreferredItem: () => Effect.succeed(preferred), + probeReady: (selected) => Effect.succeed(testCase.probeReady(selected)), + connectEntry: (selected) => `connect:${selected.sshCommand}` + }) + ) + expect(events).toEqual(testCase.expected) + })) + } }) From 8e721d3ffe9fa51554dedf4646fd5cb7ec9134f4 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 9 Apr 2026 20:29:32 +0000 Subject: [PATCH 12/26] fix(ci): reduce api json render complexity --- packages/app/src/docker-git/api-json.ts | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/app/src/docker-git/api-json.ts b/packages/app/src/docker-git/api-json.ts index f6d20da8..9e5aaaed 100644 --- a/packages/app/src/docker-git/api-json.ts +++ b/packages/app/src/docker-git/api-json.ts @@ -99,6 +99,23 @@ const readNestedMessage = ( 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) +} + export const renderJsonPayload = (payload: JsonValue): string => { if (typeof payload === "string") { return payload @@ -119,13 +136,9 @@ export const renderJsonPayload = (payload: JsonValue): string => { return message } - const nestedStatus = asObject(object["status"]) + const nestedStatus = renderNestedStatusPayload(payload, object) if (nestedStatus !== null) { - const renderedNestedStatus = renderGithubStatusLike(nestedStatus) - if (renderedNestedStatus !== null) { - return renderedNestedStatus - } - return readNestedMessage(object, "status") ?? JSON.stringify(payload, null, 2) + return nestedStatus } const nestedErrorMessage = readNestedMessage(object, "error") From 69bd6dcd6a17ba0110d790b86072728795a70610 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 9 Apr 2026 20:30:06 +0000 Subject: [PATCH 13/26] fix(ci): reduce api payload renderer complexity --- packages/app/src/docker-git/api-json.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/app/src/docker-git/api-json.ts b/packages/app/src/docker-git/api-json.ts index 9e5aaaed..ea1342af 100644 --- a/packages/app/src/docker-git/api-json.ts +++ b/packages/app/src/docker-git/api-json.ts @@ -116,6 +116,15 @@ const renderNestedStatusPayload = ( 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 @@ -126,14 +135,9 @@ export const renderJsonPayload = (payload: JsonValue): string => { return JSON.stringify(payload, null, 2) } - const directStatus = renderGithubStatusLike(object) - if (directStatus !== null) { - return directStatus - } - - const message = asString(object["message"]) - if (message !== null) { - return message + const directPayload = renderDirectObjectPayload(object) + if (directPayload !== null) { + return directPayload } const nestedStatus = renderNestedStatusPayload(payload, object) From 91333c32c16044d0aa646fc991e161bf1133a6e0 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 9 Apr 2026 20:36:40 +0000 Subject: [PATCH 14/26] fix(ci): deduplicate app lint paths --- packages/app/src/docker-git/api-json.ts | 25 ++++++---------- packages/app/src/docker-git/host-ssh.ts | 21 +++++-------- .../actions/create-project-open-ssh.ts | 19 +++++------- .../app/src/lib/usecases/auto-open-ssh.ts | 27 +++++++++++++++++ packages/app/src/shared/json-schema.ts | 16 ++++++++++ packages/app/src/web/api-schema.ts | 14 +-------- packages/app/src/web/project-events.ts | 30 ++++++++----------- 7 files changed, 82 insertions(+), 70 deletions(-) create mode 100644 packages/app/src/lib/usecases/auto-open-ssh.ts create mode 100644 packages/app/src/shared/json-schema.ts diff --git a/packages/app/src/docker-git/api-json.ts b/packages/app/src/docker-git/api-json.ts index ea1342af..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), { diff --git a/packages/app/src/docker-git/host-ssh.ts b/packages/app/src/docker-git/host-ssh.ts index 960df6b2..3b91c802 100644 --- a/packages/app/src/docker-git/host-ssh.ts +++ b/packages/app/src/docker-git/host-ssh.ts @@ -1,14 +1,13 @@ import { Effect } from "effect" import type { CreateCommand } from "@lib/core/domain" +import { shouldAutoOpenSsh } from "@lib/usecases/auto-open-ssh" import { connectProjectSsh, waitForProjectSshReady } from "@lib/usecases/projects" import type { ApiProjectDetails } from "./api-project-codec.js" import { resolveHostSshMaterial } from "./host-ssh-material.js" import { resolveApiProjectItemWithSshKeyPath } from "./project-item.js" -const isInteractiveTty = (): boolean => process.stdin.isTTY && process.stdout.isTTY - type RenderableError = Error | { readonly message: string } const renderKnownError = (error: RenderableError): string => error.message @@ -29,17 +28,13 @@ export const autoOpenProjectSsh = ( 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 } 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 index 2fdbecfa..a632f3c9 100644 --- a/packages/app/src/lib/usecases/actions/create-project-open-ssh.ts +++ b/packages/app/src/lib/usecases/actions/create-project-open-ssh.ts @@ -6,6 +6,7 @@ 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" @@ -16,8 +17,6 @@ type CreateProjectOpenSshRuntime = | Path.Path | CommandExecutor.CommandExecutor -const isInteractiveTty = (): boolean => process.stdin.isTTY && process.stdout.isTTY - const buildSshArgs = ( config: CreateCommand["config"], sshKeyPath: string | null, @@ -102,15 +101,13 @@ export const maybeOpenSsh = ( ): 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.")) + const autoOpenSsh = yield* _( + shouldAutoOpenSsh({ + shouldOpen: command.openSsh && (!hasAgent || interactiveAgent), + runUp: command.runUp + }) + ) + if (!autoOpenSsh) { return } 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..77d6174f --- /dev/null +++ b/packages/app/src/lib/usecases/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/web/api-schema.ts b/packages/app/src/web/api-schema.ts index cc4af48e..5bcdfeba 100644 --- a/packages/app/src/web/api-schema.ts +++ b/packages/app/src/web/api-schema.ts @@ -1,20 +1,8 @@ import * as Schema from "@effect/schema/Schema" -type JsonPrimitive = boolean | number | string | null -type JsonValue = JsonPrimitive | JsonObject | ReadonlyArray -type JsonObject = Readonly<{ [key: string]: JsonValue }> +import { JsonValueSchema } from "../shared/json-schema.js" const NullableString = Schema.NullOr(Schema.String) -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 }) - ) -) export const ProjectStatusSchema = Schema.Union( Schema.Literal("running"), diff --git a/packages/app/src/web/project-events.ts b/packages/app/src/web/project-events.ts index 0df3a7cf..ccb852e2 100644 --- a/packages/app/src/web/project-events.ts +++ b/packages/app/src/web/project-events.ts @@ -14,35 +14,31 @@ type EventStreamHandlers = { readonly onRateLimit: () => void } -const formatStatusLine = (payload: JsonValue | undefined): string | null => { +const readPayloadString = ( + payload: JsonValue | undefined, + key: string +): string | null => { const object = asObject(payload) if (object === null) { return null } - const phase = asString(object["phase"]) - const message = asString(object["message"]) + 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 => { - const object = asObject(payload) - if (object === null) { - return null - } - const line = asString(object["line"]) - return line -} +const formatLogLine = (payload: JsonValue | undefined): string | null => readPayloadString(payload, "line") const formatSshLine = (payload: JsonValue | undefined): string | null => { - const object = asObject(payload) - if (object === null) { - return null - } - const phase = asString(object["phase"]) - const sessionId = asString(object["sessionId"]) + const phase = readPayloadString(payload, "phase") + const sessionId = readPayloadString(payload, "sessionId") if (phase === null) { return null } From d889d7e557bb4a65ebcdba16995c436d97491fc7 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 9 Apr 2026 20:57:38 +0000 Subject: [PATCH 15/26] fix(ci): deduplicate docker shell helpers --- .../app/src/docker-git/cli/parser-open.ts | 14 ++-- packages/app/src/lib/shell/docker-compose.ts | 19 +++-- packages/app/src/lib/shell/docker-network.ts | 68 +++++++---------- packages/app/src/lib/shell/docker-runtime.ts | 74 ++++++++----------- packages/app/src/shared/trimmed-text.ts | 4 + 5 files changed, 77 insertions(+), 102 deletions(-) create mode 100644 packages/app/src/shared/trimmed-text.ts diff --git a/packages/app/src/docker-git/cli/parser-open.ts b/packages/app/src/docker-git/cli/parser-open.ts index ee605079..9d217b99 100644 --- a/packages/app/src/docker-git/cli/parser-open.ts +++ b/packages/app/src/docker-git/cli/parser-open.ts @@ -2,6 +2,7 @@ import { Either } from "effect" import { type OpenCommand, type ParseError } from "@lib/core/domain" +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/lib/shell/docker-compose.ts b/packages/app/src/lib/shell/docker-compose.ts index 0e8f4afc..d4ad1927 100644 --- a/packages/app/src/lib/shell/docker-compose.ts +++ b/packages/app/src/lib/shell/docker-compose.ts @@ -7,6 +7,15 @@ import { runCommandCapture, runCommandWithCapturedOutput } from "./command-runne 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, @@ -16,10 +25,7 @@ const runCompose = ( const env = yield* _(resolveDockerComposeEnv(cwd)) yield* _( runCommandWithCapturedOutput( - { - ...composeSpec(cwd, args), - ...(Object.keys(env).length > 0 ? { env } : {}) - }, + buildComposeCommand(cwd, args, env), okExitCodes, (exitCode, output) => new DockerCommandError({ exitCode, ...(output.length > 0 ? { details: output } : {}) }) ) @@ -35,10 +41,7 @@ const runComposeCapture = ( const env = yield* _(resolveDockerComposeEnv(cwd)) return yield* _( runCommandCapture( - { - ...composeSpec(cwd, args), - ...(Object.keys(env).length > 0 ? { env } : {}) - }, + buildComposeCommand(cwd, args, env), okExitCodes, (exitCode) => new DockerCommandError({ exitCode }) ) diff --git a/packages/app/src/lib/shell/docker-network.ts b/packages/app/src/lib/shell/docker-network.ts index e6dc0c58..9e0780b5 100644 --- a/packages/app/src/lib/shell/docker-network.ts +++ b/packages/app/src/lib/shell/docker-network.ts @@ -6,19 +6,39 @@ import { Effect } from "effect" import { runCommandCapture, runCommandExitCode, runCommandWithExitCodes } from "./command-runner.js" import { DockerCommandError } from "./errors.js" -export const runDockerNetworkConnectBridge = ( +const runDockerNetworkCommand = ( cwd: string, - containerName: string + args: ReadonlyArray ): Effect.Effect => + runCommandWithExitCodes( + { + cwd, + command: "docker", + args + }, + [Number(ExitCode(0))], + (exitCode) => new DockerCommandError({ exitCode }) + ) + +const runDockerNetworkCapture = ( + cwd: string, + args: ReadonlyArray +): Effect.Effect => runCommandCapture( { cwd, command: "docker", - args: ["network", "connect", "bridge", containerName] + args }, [Number(ExitCode(0))], (exitCode) => new DockerCommandError({ exitCode }) - ).pipe(Effect.asVoid) + ) + +export const runDockerNetworkConnectBridge = ( + cwd: string, + containerName: string +): Effect.Effect => + runDockerNetworkCapture(cwd, ["network", "connect", "bridge", containerName]).pipe(Effect.asVoid) export const runDockerNetworkExists = ( cwd: string, @@ -34,44 +54,20 @@ 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 }) - ) + runDockerNetworkCommand(cwd, ["network", "create", "--driver", "bridge", networkName]) 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 }) - ) + runDockerNetworkCommand(cwd, ["network", "create", "--driver", "bridge", "--subnet", subnet, networkName]) 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( + 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 @@ -82,12 +78,4 @@ export const runDockerNetworkRemove = ( cwd: string, networkName: string ): Effect.Effect => - runCommandWithExitCodes( - { - cwd, - command: "docker", - args: ["network", "rm", networkName] - }, - [Number(ExitCode(0))], - (exitCode) => new DockerCommandError({ exitCode }) - ) + 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 index 824834b7..46e5a50c 100644 --- a/packages/app/src/lib/shell/docker-runtime.ts +++ b/packages/app/src/lib/shell/docker-runtime.ts @@ -4,6 +4,7 @@ import type * as CommandExecutor from "@effect/platform/CommandExecutor" import type { PlatformError } from "@effect/platform/Error" import { Effect, pipe } from "effect" +import { trimToUndefined } from "../../shared/trimmed-text.js" import { runCommandCapture } from "./command-runner.js" import { parseInspectNetworkEntry } from "./docker-inspect-parse.js" import { CommandFailedError, DockerCommandError } from "./errors.js" @@ -16,10 +17,20 @@ export type DockerContainerRuntimeInfo = { readonly composeService?: string | undefined } -const parseOptionalInspectField = (value: string | undefined): string | undefined => { - const trimmed = value?.trim() ?? "" - return trimmed.length > 0 ? trimmed : undefined -} +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, @@ -42,19 +53,10 @@ export const runDockerInspectContainerIp = ( containerName: string ): Effect.Effect => pipe( - runCommandCapture( - { - cwd, - command: "docker", - args: [ - "inspect", - "-f", - String.raw`{{range $k,$v := .NetworkSettings.Networks}}{{printf "%s=%s\n" $k $v.IPAddress}}{{end}}`, - containerName - ] - }, - [Number(ExitCode(0))], - (exitCode) => new DockerCommandError({ exitCode }) + runDockerInspectValue( + cwd, + containerName, + String.raw`{{range $k,$v := .NetworkSettings.Networks}}{{printf "%s=%s\n" $k $v.IPAddress}}{{end}}` ), Effect.map((output) => { const lines = output @@ -78,19 +80,10 @@ export const runDockerInspectContainerRuntimeInfo = ( 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 }) + runDockerInspectValue( + cwd, + containerName, + `{{.State.Status}}\t{{with index .Config.Labels "com.docker.compose.project.working_dir"}}{{.}}{{end}}\t{{with index .Config.Labels "com.docker.compose.service"}}{{.}}{{end}}` ), Effect.flatMap((output) => { const [status, projectWorkingDir, composeService] = output.trim().replaceAll(String.raw`\t`, "\t").split("\t") @@ -103,8 +96,8 @@ export const runDockerInspectContainerRuntimeInfo = ( containerName, running: true, ipAddress, - projectWorkingDir: parseOptionalInspectField(projectWorkingDir), - composeService: parseOptionalInspectField(composeService) + projectWorkingDir: trimToUndefined(projectWorkingDir), + composeService: trimToUndefined(composeService) })) ) }), @@ -116,19 +109,10 @@ export const runDockerInspectContainerBridgeIp = ( 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 }) + runDockerInspectValue( + cwd, + containerName, + "{{with (index .NetworkSettings.Networks \"bridge\")}}{{.IPAddress}}{{end}}" ), Effect.map((output) => output.trim()) ) 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 +} From 7095672440766e9ab842f89204a8522f35664b33 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 9 Apr 2026 21:02:30 +0000 Subject: [PATCH 16/26] fix(ci): remove remaining docker shell clones --- packages/app/src/lib/shell/docker-network.ts | 33 +++++---- packages/app/src/lib/shell/docker-runtime.ts | 71 ++++++++++---------- 2 files changed, 54 insertions(+), 50 deletions(-) diff --git a/packages/app/src/lib/shell/docker-network.ts b/packages/app/src/lib/shell/docker-network.ts index 9e0780b5..bf370004 100644 --- a/packages/app/src/lib/shell/docker-network.ts +++ b/packages/app/src/lib/shell/docker-network.ts @@ -6,18 +6,27 @@ 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( - { - cwd, - command: "docker", - args - }, - [Number(ExitCode(0))], - (exitCode) => new DockerCommandError({ exitCode }) + buildDockerNetworkCommand(cwd, args), + dockerSuccessExitCodes, + createDockerNetworkError ) const runDockerNetworkCapture = ( @@ -25,13 +34,9 @@ const runDockerNetworkCapture = ( args: ReadonlyArray ): Effect.Effect => runCommandCapture( - { - cwd, - command: "docker", - args - }, - [Number(ExitCode(0))], - (exitCode) => new DockerCommandError({ exitCode }) + buildDockerNetworkCommand(cwd, args), + dockerSuccessExitCodes, + createDockerNetworkError ) export const runDockerNetworkConnectBridge = ( diff --git a/packages/app/src/lib/shell/docker-runtime.ts b/packages/app/src/lib/shell/docker-runtime.ts index 46e5a50c..f154ee09 100644 --- a/packages/app/src/lib/shell/docker-runtime.ts +++ b/packages/app/src/lib/shell/docker-runtime.ts @@ -17,6 +17,21 @@ export type DockerContainerRuntimeInfo = { readonly composeService?: string | undefined } +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, @@ -48,32 +63,24 @@ export const runDockerExecExitCode = ( return Number(exitCode) }) -export const runDockerInspectContainerIp = ( - cwd: string, - containerName: string -): Effect.Effect => - pipe( - runDockerInspectValue( - cwd, - containerName, - String.raw`{{range $k,$v := .NetworkSettings.Networks}}{{printf "%s=%s\n" $k $v.IPAddress}}{{end}}` - ), - Effect.map((output) => { - const lines = output - .trim() - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => line.length > 0) +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 entries = lines.flatMap((line) => parseInspectNetworkEntry(line)) + if (entries.length === 0) { + return "" + } - const entryMap = new Map(entries) - return entryMap.get("bridge") ?? entries[0]![1] - }) - ) + const entryMap = new Map(entries) + return entryMap.get("bridge") ?? entries[0]![1] + } +) export const runDockerInspectContainerRuntimeInfo = ( cwd: string, @@ -104,18 +111,10 @@ export const runDockerInspectContainerRuntimeInfo = ( Effect.catchTag("DockerCommandError", () => Effect.succeed(null)) ) -export const runDockerInspectContainerBridgeIp = ( - cwd: string, - containerName: string -): Effect.Effect => - pipe( - runDockerInspectValue( - cwd, - containerName, - "{{with (index .NetworkSettings.Networks \"bridge\")}}{{.IPAddress}}{{end}}" - ), - Effect.map((output) => output.trim()) - ) +export const runDockerInspectContainerBridgeIp = createDockerInspectReader( + "{{with (index .NetworkSettings.Networks \"bridge\")}}{{.IPAddress}}{{end}}", + (output) => output.trim() +) export const runDockerPsNames = ( cwd: string From 50ceb3f305e9ca43bb5c1928fcd73ff5004298f6 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 9 Apr 2026 21:20:20 +0000 Subject: [PATCH 17/26] fix(ci): split lib create-project lint paths --- packages/lib/src/shell/command-runner.ts | 9 +- packages/lib/src/shell/docker.ts | 7 +- .../actions/create-project-conflicts.ts | 179 +++++++++++ .../actions/create-project-open-ssh.ts | 123 ++++++++ .../src/usecases/actions/create-project.ts | 296 +++--------------- 5 files changed, 353 insertions(+), 261 deletions(-) create mode 100644 packages/lib/src/usecases/actions/create-project-conflicts.ts create mode 100644 packages/lib/src/usecases/actions/create-project-open-ssh.ts 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/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/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 9748f920..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,224 +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 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 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 => @@ -340,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())) @@ -353,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)) @@ -364,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, @@ -372,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, @@ -399,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 */ From 82ee937a177daf854e1a0dc0862baad7121ad23f Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:47:21 +0000 Subject: [PATCH 18/26] refactor(app): finalize frontend-only cleanup --- packages/app/eslint/no-lib-imports.mjs | 56 +++- packages/app/src/app/program.ts | 12 +- packages/app/src/docker-git/api-auth-codec.ts | 100 ++++++ packages/app/src/docker-git/api-client.ts | 92 +++-- .../app/src/docker-git/api-project-codec.ts | 58 ++-- .../app/src/docker-git/api-terminal-codec.ts | 60 ++++ packages/app/src/docker-git/cli/input.ts | 2 +- .../app/src/docker-git/cli/parser-apply.ts | 4 +- .../app/src/docker-git/cli/parser-attach.ts | 2 +- .../app/src/docker-git/cli/parser-auth.ts | 4 +- .../app/src/docker-git/cli/parser-clone.ts | 6 +- .../app/src/docker-git/cli/parser-create.ts | 6 +- .../docker-git/cli/parser-mcp-playwright.ts | 2 +- .../app/src/docker-git/cli/parser-open.ts | 2 +- .../app/src/docker-git/cli/parser-options.ts | 6 +- .../app/src/docker-git/cli/parser-panes.ts | 2 +- .../app/src/docker-git/cli/parser-scrap.ts | 2 +- .../docker-git/cli/parser-session-gists.ts | 2 +- .../app/src/docker-git/cli/parser-sessions.ts | 2 +- .../app/src/docker-git/cli/parser-shared.ts | 2 +- .../app/src/docker-git/cli/parser-state.ts | 2 +- packages/app/src/docker-git/cli/parser.ts | 2 +- .../app/src/docker-git/cli/read-command.ts | 2 +- packages/app/src/docker-git/cli/usage.ts | 2 +- .../app/src/docker-git/controller-docker.ts | 2 +- .../frontend-lib/core/auth-domain.ts | 106 ++++++ .../frontend-lib/core/auto-agent-flags.ts | 26 ++ .../src/docker-git/frontend-lib/core/clone.ts | 62 ++++ .../core/command-builders-shared.ts | 55 +++ .../frontend-lib/core/command-builders.ts | 312 +++++++++++++++++ .../frontend-lib/core/command-options.ts | 75 +++++ .../docker-git/frontend-lib/core/domain.ts | 286 ++++++++++++++++ .../src/docker-git/frontend-lib/core/menu.ts | 113 +++++++ .../frontend-lib/core/parse-errors.ts | 26 ++ .../src/docker-git/frontend-lib/core/repo.ts | 317 ++++++++++++++++++ .../frontend-lib/core/resource-limits.ts | 145 ++++++++ .../frontend-lib/core/session-gist-domain.ts | 38 +++ .../frontend-lib/core/sessions-domain.ts | 28 ++ .../frontend-lib/core/state-domain.ts | 42 +++ .../docker-git/frontend-lib/core/strings.ts | 17 + .../frontend-lib/core/template-defaults.ts | 66 ++++ .../frontend-lib/core/token-labels.ts | 53 +++ .../docker-git/frontend-lib/shell/clone.ts | 95 ++++++ .../frontend-lib/shell/command-runner.ts | 146 ++++++++ .../docker-git/frontend-lib/shell/errors.ts | 101 ++++++ .../frontend-lib/usecases/menu-helpers.ts | 52 +++ .../frontend-lib/usecases/path-helpers.ts | 216 ++++++++++++ .../frontend-lib/usecases/scrap-path.ts | 72 ++++ packages/app/src/docker-git/host-errors.ts | 51 ++- .../app/src/docker-git/host-ssh-material.ts | 151 --------- packages/app/src/docker-git/host-ssh.ts | 64 +++- packages/app/src/docker-git/menu-actions.ts | 3 +- packages/app/src/docker-git/menu-api.ts | 32 +- packages/app/src/docker-git/menu-auth-data.ts | 117 ++----- .../app/src/docker-git/menu-auth-effects.ts | 88 +++-- .../app/src/docker-git/menu-auth-helpers.ts | 5 +- .../app/src/docker-git/menu-auth-shared.ts | 2 +- .../docker-git/menu-auth-snapshot-builder.ts | 4 +- packages/app/src/docker-git/menu-auth.ts | 5 +- .../app/src/docker-git/menu-create-shared.ts | 4 +- packages/app/src/docker-git/menu-create.ts | 2 +- packages/app/src/docker-git/menu-errors.ts | 15 +- .../src/docker-git/menu-gridland-runtime.tsx | 12 +- .../app/src/docker-git/menu-labeled-env.ts | 27 +- packages/app/src/docker-git/menu-menu.ts | 78 ++++- .../src/docker-git/menu-project-auth-data.ts | 159 ++------- .../src/docker-git/menu-project-auth-flows.ts | 150 --------- .../app/src/docker-git/menu-project-auth.ts | 7 +- .../app/src/docker-git/menu-render-select.ts | 2 +- packages/app/src/docker-git/menu-render.ts | 2 +- .../app/src/docker-git/menu-select-actions.ts | 2 +- .../app/src/docker-git/menu-select-connect.ts | 2 +- .../app/src/docker-git/menu-select-load.ts | 2 +- .../app/src/docker-git/menu-select-order.ts | 2 +- .../app/src/docker-git/menu-select-runtime.ts | 126 +------ .../app/src/docker-git/menu-select-view.ts | 2 +- packages/app/src/docker-git/menu-startup.ts | 11 +- packages/app/src/docker-git/menu-types.ts | 18 +- packages/app/src/docker-git/menu.ts | 11 +- .../app/src/docker-git/open-project-ssh.ts | 148 ++++---- packages/app/src/docker-git/open-project.ts | 18 +- packages/app/src/docker-git/program.ts | 2 +- packages/app/src/docker-git/project-item.ts | 97 ++---- .../src/docker-git/terminal-session-client.ts | 205 +++++++++++ packages/app/src/docker-git/tmux.ts | 288 ---------------- packages/app/src/lib/shell/docker-runtime.ts | 38 --- .../create-project-identity-conflict.test.ts | 252 -------------- .../docker-git/docker-runtime-info.test.ts | 135 -------- .../tests/docker-git/entrypoint-auth.test.ts | 116 ------- .../fixtures/open-project-helpers.ts | 7 +- .../fixtures/open-project-ssh-helpers.ts | 48 --- .../tests/docker-git/fixtures/project-item.ts | 8 +- .../docker-git/host-ssh-material.test.ts | 202 ----------- .../docker-git/menu-select-connect.test.ts | 2 +- .../app/tests/docker-git/menu-startup.test.ts | 27 +- .../tests/docker-git/open-project-ssh.test.ts | 137 +++----- .../app/tests/docker-git/open-project.test.ts | 153 +-------- .../app/tests/docker-git/parser-helpers.ts | 2 +- packages/app/tests/docker-git/parser.test.ts | 4 +- packages/app/tests/docker-git/program.test.ts | 2 +- .../app/tests/docker-git/project-item.test.ts | 22 +- .../app/tests/eslint/no-lib-imports.test.ts | 29 +- packages/lib/tests/core/templates.test.ts | 121 +++++++ .../create-project-docker-identities.test.ts | 80 ++++- 104 files changed, 3768 insertions(+), 2412 deletions(-) create mode 100644 packages/app/src/docker-git/api-auth-codec.ts create mode 100644 packages/app/src/docker-git/api-terminal-codec.ts create mode 100644 packages/app/src/docker-git/frontend-lib/core/auth-domain.ts create mode 100644 packages/app/src/docker-git/frontend-lib/core/auto-agent-flags.ts create mode 100644 packages/app/src/docker-git/frontend-lib/core/clone.ts create mode 100644 packages/app/src/docker-git/frontend-lib/core/command-builders-shared.ts create mode 100644 packages/app/src/docker-git/frontend-lib/core/command-builders.ts create mode 100644 packages/app/src/docker-git/frontend-lib/core/command-options.ts create mode 100644 packages/app/src/docker-git/frontend-lib/core/domain.ts create mode 100644 packages/app/src/docker-git/frontend-lib/core/menu.ts create mode 100644 packages/app/src/docker-git/frontend-lib/core/parse-errors.ts create mode 100644 packages/app/src/docker-git/frontend-lib/core/repo.ts create mode 100644 packages/app/src/docker-git/frontend-lib/core/resource-limits.ts create mode 100644 packages/app/src/docker-git/frontend-lib/core/session-gist-domain.ts create mode 100644 packages/app/src/docker-git/frontend-lib/core/sessions-domain.ts create mode 100644 packages/app/src/docker-git/frontend-lib/core/state-domain.ts create mode 100644 packages/app/src/docker-git/frontend-lib/core/strings.ts create mode 100644 packages/app/src/docker-git/frontend-lib/core/template-defaults.ts create mode 100644 packages/app/src/docker-git/frontend-lib/core/token-labels.ts create mode 100644 packages/app/src/docker-git/frontend-lib/shell/clone.ts create mode 100644 packages/app/src/docker-git/frontend-lib/shell/command-runner.ts create mode 100644 packages/app/src/docker-git/frontend-lib/shell/errors.ts create mode 100644 packages/app/src/docker-git/frontend-lib/usecases/menu-helpers.ts create mode 100644 packages/app/src/docker-git/frontend-lib/usecases/path-helpers.ts create mode 100644 packages/app/src/docker-git/frontend-lib/usecases/scrap-path.ts delete mode 100644 packages/app/src/docker-git/host-ssh-material.ts delete mode 100644 packages/app/src/docker-git/menu-project-auth-flows.ts create mode 100644 packages/app/src/docker-git/terminal-session-client.ts delete mode 100644 packages/app/src/docker-git/tmux.ts delete mode 100644 packages/app/tests/docker-git/create-project-identity-conflict.test.ts delete mode 100644 packages/app/tests/docker-git/docker-runtime-info.test.ts delete mode 100644 packages/app/tests/docker-git/entrypoint-auth.test.ts delete mode 100644 packages/app/tests/docker-git/fixtures/open-project-ssh-helpers.ts delete mode 100644 packages/app/tests/docker-git/host-ssh-material.test.ts 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/src/app/program.ts b/packages/app/src/app/program.ts index ee939ed0..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. @@ -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..b9508e0e --- /dev/null +++ b/packages/app/src/docker-git/api-auth-codec.ts @@ -0,0 +1,100 @@ +import { asObject, asString, type JsonValue } from "./api-json.js" +import type { AuthSnapshot, ProjectAuthSnapshot } from "./menu-types.js" + +const readNumber = (value: JsonValue | undefined): number | null => + typeof value === "number" ? value : null + +const resolveSnapshotObject = (payload: JsonValue) => { + const object = asObject(payload) + return asObject(object?.["snapshot"] ?? payload) +} + +export const decodeAuthSnapshot = (payload: JsonValue): AuthSnapshot | null => { + const snapshot = resolveSnapshotObject(payload) + if (snapshot === null) { + return null + } + + const decoded = { + 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"]) + } + + return Object.values(decoded).includes(null) + ? null + : { + globalEnvPath: decoded.globalEnvPath ?? "", + claudeAuthPath: decoded.claudeAuthPath ?? "", + geminiAuthPath: decoded.geminiAuthPath ?? "", + totalEntries: decoded.totalEntries ?? 0, + githubTokenEntries: decoded.githubTokenEntries ?? 0, + gitTokenEntries: decoded.gitTokenEntries ?? 0, + gitUserEntries: decoded.gitUserEntries ?? 0, + claudeAuthEntries: decoded.claudeAuthEntries ?? 0, + geminiAuthEntries: decoded.geminiAuthEntries ?? 0 + } +} + +export const decodeProjectAuthSnapshot = (payload: JsonValue): ProjectAuthSnapshot | null => { + const snapshot = resolveSnapshotObject(payload) + if (snapshot === null) { + return null + } + + const decoded = { + 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 requiredValues = [ + decoded.projectDir, + decoded.projectName, + decoded.envGlobalPath, + decoded.envProjectPath, + decoded.claudeAuthPath, + decoded.geminiAuthPath, + decoded.githubTokenEntries, + decoded.gitTokenEntries, + decoded.claudeAuthEntries, + decoded.geminiAuthEntries + ] + if (requiredValues.includes(null)) { + return null + } + + return { + projectDir: decoded.projectDir ?? "", + projectName: decoded.projectName ?? "", + envGlobalPath: decoded.envGlobalPath ?? "", + envProjectPath: decoded.envProjectPath ?? "", + claudeAuthPath: decoded.claudeAuthPath ?? "", + geminiAuthPath: decoded.geminiAuthPath ?? "", + githubTokenEntries: decoded.githubTokenEntries ?? 0, + gitTokenEntries: decoded.gitTokenEntries ?? 0, + claudeAuthEntries: decoded.claudeAuthEntries ?? 0, + geminiAuthEntries: decoded.geminiAuthEntries ?? 0, + activeGithubLabel: decoded.activeGithubLabel, + activeGitLabel: decoded.activeGitLabel, + activeClaudeLabel: decoded.activeClaudeLabel, + activeGeminiLabel: decoded.activeGeminiLabel + } +} diff --git a/packages/app/src/docker-git/api-client.ts b/packages/app/src/docker-git/api-client.ts index 72ddf4d3..c9703d2d 100644 --- a/packages/app/src/docker-git/api-client.ts +++ b/packages/app/src/docker-git/api-client.ts @@ -2,6 +2,11 @@ import * as FileSystem from "@effect/platform/FileSystem" import * as Path from "@effect/platform/Path" import { Effect } from "effect" +import { decodeAuthSnapshot, decodeProjectAuthSnapshot } from "./api-auth-codec.js" +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" +import { decodeTerminalSession } from "./api-terminal-codec.js" import type { AuthCodexImportCommand, AuthCodexLoginCommand, @@ -14,14 +19,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,6 +31,7 @@ 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" @@ -92,7 +93,6 @@ export const getProject = (projectId: string) => 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, @@ -110,9 +110,7 @@ export const createProject = (command: CreateCommand) => outDir: command.outDir, gitTokenLabel: config.gitTokenLabel, skipGithubAuth: config.skipGithubAuth, - authorizedKeysContents: sshMaterial.authorizedKeysContents.length > 0 - ? sshMaterial.authorizedKeysContents - : undefined, + useManagedAuthorizedKeys: true, codexTokenLabel: config.codexAuthLabel, claudeTokenLabel: config.claudeAuthLabel, agentAutoMode: config.agentAuto ? (config.agentMode ?? "auto") : undefined, @@ -131,16 +129,7 @@ export const createProject = (command: CreateCommand) => 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 +143,67 @@ 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 loadAuthSnapshot = () => + request("GET", "/auth/menu").pipe( + Effect.map((payload) => decodeAuthSnapshot(payload)) + ) + +export const runAuthMenuFlow = (requestBody: { + readonly flow: string + readonly label?: string | null + readonly token?: string | null + readonly user?: string | null + readonly apiKey?: string | null +}) => + 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: { readonly flow: string; readonly label?: string | null } +) => + request("POST", projectPath(projectId, "/auth/menu"), { + flow: requestBody.flow, + label: requestBody.label ?? undefined + }).pipe( + Effect.map((payload) => decodeProjectAuthSnapshot(payload)) + ) + export const applyAllProjects = (activeOnly: boolean) => requestVoid("POST", "/projects/apply-all", { activeOnly }) export const downAllProjects = () => requestVoid("POST", "/projects/down-all") diff --git a/packages/app/src/docker-git/api-project-codec.ts b/packages/app/src/docker-git/api-project-codec.ts index 36a00b18..8679c374 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,15 @@ 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 const isProjectStatus = ( value: string @@ -34,6 +38,8 @@ 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 : value === null ? null : null const readSummaryBaseFields = ( object: ReturnType @@ -48,7 +54,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,13 +69,16 @@ 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 => { +): ProjectDetailFields | null => { if (object === null) { return null } @@ -78,23 +90,15 @@ const readRequiredProjectDetails = ( const targetDir = asString(object["targetDir"]) const projectDir = asString(object["projectDir"]) const sshCommand = asString(object["sshCommand"]) + const authorizedKeysPath = asString(object["authorizedKeysPath"]) + const authorizedKeysExists = typeof object["authorizedKeysExists"] === "boolean" + ? object["authorizedKeysExists"] + : null 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 - ] + const values = [containerName, serviceName, sshUser, sshPort, targetDir, projectDir, sshCommand, authorizedKeysPath, authorizedKeysExists, envGlobalPath, envProjectPath, codexAuthPath, codexHome] if (values.includes(null)) { return null @@ -108,6 +112,8 @@ const readRequiredProjectDetails = ( targetDir: stringOrEmpty(targetDir), projectDir: stringOrEmpty(projectDir), sshCommand: stringOrEmpty(sshCommand), + authorizedKeysPath: stringOrEmpty(authorizedKeysPath), + authorizedKeysExists: authorizedKeysExists === true, envGlobalPath: stringOrEmpty(envGlobalPath), envProjectPath: stringOrEmpty(envProjectPath), codexAuthPath: stringOrEmpty(codexAuthPath), @@ -127,20 +133,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 => + readRequiredProjectDetails(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..ca960a3c --- /dev/null +++ b/packages/app/src/docker-git/api-terminal-codec.ts @@ -0,0 +1,60 @@ +import { asObject, asString, type JsonValue } from "./api-json.js" + +export type ApiTerminalSession = { + readonly id: string + readonly projectId: string + readonly sshCommand: string + readonly status: "ready" | "attached" | "exited" | "failed" + readonly createdAt: string + 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 + +export const decodeTerminalSession = (payload: JsonValue): ApiTerminalSession | null => { + const object = asObject(payload) + if (object === null) { + return null + } + + const session = { + 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"]) + } + + 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..dd25c299 100644 --- a/packages/app/src/docker-git/cli/parser-auth.ts +++ b/packages/app/src/docker-git/cli/parser-auth.ts @@ -1,7 +1,7 @@ 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 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" 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 9d217b99..19bb6610 100644 --- a/packages/app/src/docker-git/cli/parser-open.ts +++ b/packages/app/src/docker-git/cli/parser-open.ts @@ -1,6 +1,6 @@ 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" 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..c9bf683e 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 { 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/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-material.ts b/packages/app/src/docker-git/host-ssh-material.ts deleted file mode 100644 index 652c88f8..00000000 --- a/packages/app/src/docker-git/host-ssh-material.ts +++ /dev/null @@ -1,151 +0,0 @@ -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) - .map((line) => line.trim()) - .filter((line) => line.length > 0) - -const mergeAuthorizedKeys = ( - base: ReadonlyArray, - required: ReadonlyArray -): string => { - const merged = [...base] - for (const line of required) { - if (!merged.includes(line)) { - merged.push(line) - } - } - return merged.length === 0 ? "" : `${merged.join("\n")}\n` -} - -const resolvePublicKeyFromPrivate = ( - privateKeyPath: string -) => - withFsPathContext(({ fs }) => - Effect.gen(function*(_) { - const publicKeyPath = `${privateKeyPath}.pub` - const publicKeyExists = yield* _(fs.exists(publicKeyPath)) - if (publicKeyExists) { - return yield* _(fs.readFileString(publicKeyPath)) - } - - return yield* _( - runCommandCapture( - { - cwd: process.cwd(), - command: "ssh-keygen", - args: ["-y", "-f", privateKeyPath] - }, - [0], - (exitCode) => new CommandFailedError({ command: "ssh-keygen -y", exitCode }) - ).pipe(Effect.map((value) => `${value.trim()}\n`)) - ) - }) - ) - -export const resolveHostPrivateKeyPath = () => - withFsPathContext(({ fs, path }) => - Effect.gen(function*(_) { - const existing = yield* _(findSshPrivateKey(fs, path, process.cwd())) - if (existing !== null) { - return existing - } - - const projectsRoot = defaultProjectsRoot(process.cwd()) - const managedKeyPath = path.join(projectsRoot, "dev_ssh_key") - const managedPublicKeyPath = `${managedKeyPath}.pub` - - yield* _(fs.makeDirectory(path.dirname(managedKeyPath), { recursive: true })) - - const stalePublicKeyExists = yield* _(fs.exists(managedPublicKeyPath)) - if (stalePublicKeyExists) { - yield* _(fs.remove(managedPublicKeyPath)) - } - - yield* _( - runCommandWithExitCodes( - { - cwd: process.cwd(), - command: "ssh-keygen", - args: ["-q", "-t", "ed25519", "-N", "", "-C", "docker-git", "-f", managedKeyPath] - }, - [0], - (exitCode) => new CommandFailedError({ command: "ssh-keygen", exitCode }) - ) - ) - - return managedKeyPath - }) - ) - -const readLocalAuthorizedKeysOverride = ( - command: CreateCommand -) => - withFsPathContext(({ fs, path }) => - Effect.gen(function*(_) { - if (command.config.authorizedKeysPath === defaultTemplateConfig.authorizedKeysPath) { - return "" - } - - const resolved = resolvePathFromCwd(path, process.cwd(), command.config.authorizedKeysPath) - const exists = yield* _(fs.exists(resolved)) - if (!exists) { - return "" - } - - return yield* _(fs.readFileString(resolved)) - }) - ) - -const resolveManagedHostPublicKey = () => - Effect.gen(function*(_) { - const privateKeyPath = yield* _(resolveHostPrivateKeyPath()) - const publicKey = yield* _(resolvePublicKeyFromPrivate(privateKeyPath)) - - return { - privateKeyPath, - publicKey - } - }) - -export const resolveHostSshMaterial = ( - command: CreateCommand -) => - 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) - ) - } - }) diff --git a/packages/app/src/docker-git/host-ssh.ts b/packages/app/src/docker-git/host-ssh.ts index 3b91c802..ff033dd3 100644 --- a/packages/app/src/docker-git/host-ssh.ts +++ b/packages/app/src/docker-git/host-ssh.ts @@ -1,30 +1,47 @@ import { Effect } from "effect" -import type { CreateCommand } from "@lib/core/domain" -import { shouldAutoOpenSsh } from "@lib/usecases/auto-open-ssh" -import { connectProjectSsh, waitForProjectSshReady } from "@lib/usecases/projects" - +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" + +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 shouldOpenSsh = (command: AutoOpenSshCommand): boolean => command.openSsh -const resolveProjectItem = ( - command: CreateCommand, - project: ApiProjectDetails -) => +const isInteractiveTty = (): boolean => process.stdin.isTTY && process.stdout.isTTY + +const shouldAutoOpenSsh = ({ + runUp, + shouldOpen +}: { + readonly shouldOpen: boolean + readonly runUp: boolean +}): Effect.Effect => Effect.gen(function*(_) { - const sshMaterial = yield* _(resolveHostSshMaterial(command)) - return yield* _(resolveApiProjectItemWithSshKeyPath(project, sshMaterial.privateKeyPath)) + 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 }) export const autoOpenProjectSsh = ( - command: CreateCommand, + command: AutoOpenSshCommand, project: ApiProjectDetails | null ) => Effect.gen(function*(_) { @@ -43,10 +60,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/menu-actions.ts b/packages/app/src/docker-git/menu-actions.ts index 85b5650f..efd79da2 100644 --- a/packages/app/src/docker-git/menu-actions.ts +++ b/packages/app/src/docker-git/menu-actions.ts @@ -1,4 +1,3 @@ -import type { MenuAction } from "@lib/core/domain" import { Effect, Match, pipe } from "effect" import { downAllProjects, downProject, upProject } from "./api-client.js" @@ -16,7 +15,7 @@ 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 diff --git a/packages/app/src/docker-git/menu-api.ts b/packages/app/src/docker-git/menu-api.ts index 5f3fa889..d1503f04 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,17 @@ import { listProjects, readProjectLogs, readProjectPs, - renderProjectSummaryLine, - upProject + renderProjectSummaryLine } from "./api-client.js" import { asObject, asString, type JsonValue } from "./api-json.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 = { _tag: "AuthGithubStatus", envGlobalPath: "" -} satisfies AuthGithubStatusCommand +} as const const compact = (values: ReadonlyArray): ReadonlyArray => values.filter((value): value is A => value !== null) @@ -41,7 +37,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 +85,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 a8be4bc2..865144c7 100644 --- a/packages/app/src/docker-git/menu-auth-data.ts +++ b/packages/app/src/docker-git/menu-auth-data.ts @@ -1,14 +1,8 @@ -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, 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 { loadAuthSnapshot, runAuthMenuFlow as submitAuthMenuFlow } from "./api-client.js" import type { AuthEnvFlow } from "./menu-auth-shared.js" -import { countAuthAccountEntries } from "./menu-auth-snapshot-builder.js" -import { buildLabeledEnvKey, countKeyEntries, normalizeLabel } from "./menu-labeled-env.js" +import type { MenuError } from "./menu-errors.js" import type { AuthSnapshot, MenuEnv } from "./menu-types.js" export { @@ -21,93 +15,38 @@ export { } from "./menu-auth-shared.js" export type { AuthEnvFlow, AuthMenuAction, AuthPromptStep } from "./menu-auth-shared.js" -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 defaultValue = (value: string | undefined): string | null => { + const trimmed = value?.trim() ?? "" + return trimmed.length === 0 ? null : trimmed } -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 } - }) +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: defaultValue(values["label"]), + token: defaultValue(values["token"]), + user: defaultValue(values["user"]), + apiKey: defaultValue(values["apiKey"]) + }).pipe( + Effect.flatMap((snapshot) => decodeSnapshot(snapshot)), Effect.asVoid ) 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 index 5fc6f01a..7e185224 100644 --- a/packages/app/src/docker-git/menu-auth-shared.ts +++ b/packages/app/src/docker-git/menu-auth-shared.ts @@ -4,7 +4,7 @@ import type { AuthFlow } from "./menu-types.js" export type AuthMenuAction = AuthFlow | "Refresh" | "Back" -export type AuthEnvFlow = Extract +export type AuthEnvFlow = Exclude export type AuthPromptStep = { readonly key: "label" | "token" | "user" | "apiKey" 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 index aa2b4f6c..41e82863 100644 --- a/packages/app/src/docker-git/menu-create-shared.ts +++ b/packages/app/src/docker-git/menu-create-shared.ts @@ -1,6 +1,6 @@ -import { deriveRepoPathParts, resolveRepoInput } from "@lib/core/domain" -import { defaultProjectsRoot, isRepoUrlInput } from "@lib/usecases/menu-helpers" 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" diff --git a/packages/app/src/docker-git/menu-create.ts b/packages/app/src/docker-git/menu-create.ts index 2390b506..51be89bf 100644 --- a/packages/app/src/docker-git/menu-create.ts +++ b/packages/app/src/docker-git/menu-create.ts @@ -1,5 +1,5 @@ -import { type CreateCommand } from "@lib/core/domain" 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" 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 index 4575a858..b9dcd79f 100644 --- a/packages/app/src/docker-git/menu-gridland-runtime.tsx +++ b/packages/app/src/docker-git/menu-gridland-runtime.tsx @@ -1,4 +1,3 @@ -import { InputReadError } from "@lib/shell/errors" import { Effect } from "effect" import React, { useMemo } from "react" @@ -8,6 +7,11 @@ 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", @@ -58,8 +62,10 @@ const toMenuKeyInput = (event: GridlandKeyEvent) => { } as const } -const toInputReadError = (error: Error | string): InputReadError => - new InputReadError({ message: error instanceof Error ? error.message : error }) +const toInputReadError = (error: Error | string): InputReadError => ({ + _tag: "InputReadError", + message: error instanceof Error ? error.message : error +}) const waitForRendererDestroy = (renderer: GridlandRenderer): Effect.Effect => Effect.async((resume) => { 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 0bfb7c08..7952c5f5 100644 --- a/packages/app/src/docker-git/menu-project-auth-data.ts +++ b/packages/app/src/docker-git/menu-project-auth-data.ts @@ -1,16 +1,9 @@ -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 { loadProjectAuthSnapshot, runProjectAuthFlow as submitProjectAuthFlow } from "./api-client.js" +import type { MenuError } from "./menu-errors.js" import type { MenuEnv, ProjectAuthFlow, ProjectAuthSnapshot } from "./menu-types.js" +import type { ProjectItem } from "./project-item.js" export { projectAuthMenuActionByIndex, @@ -21,136 +14,40 @@ export { } from "./menu-project-auth-shared.js" export type { ProjectAuthMenuAction, ProjectAuthPromptStep } from "./menu-project-auth-shared.js" -const resolveCanonicalLabel = (value: string): string => { - const normalized = normalizeLabel(value) - return normalized.length === 0 || normalized === "DEFAULT" ? "default" : normalized +const defaultValue = (value: string | undefined): string | null => { + const trimmed = value?.trim() ?? "" + return trimmed.length === 0 ? null : trimmed } -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 - } - }) +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: defaultValue(values["label"]) + }).pipe( + Effect.flatMap((snapshot) => decodeSnapshot(project.projectDir, snapshot)), Effect.asVoid ) 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.ts b/packages/app/src/docker-git/menu-project-auth.ts index a17224b3..d355e64a 100644 --- a/packages/app/src/docker-git/menu-project-auth.ts +++ b/packages/app/src/docker-git/menu-project-auth.ts @@ -1,9 +1,7 @@ import { Effect, pipe } from "effect" -import type { AppError } from "@lib/usecases/errors" -import type { ProjectItem } from "@lib/usecases/projects" - 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, @@ -24,6 +22,7 @@ import type { ProjectAuthSnapshot, ViewState } from "./menu-types.js" +import type { ProjectItem } from "./project-item.js" type ProjectAuthContext = Pick & { readonly runner: MenuRunner @@ -63,7 +62,7 @@ const startProjectAuthPrompt = ( const loadProjectAuthMenuView = ( project: ProjectItem, context: Pick -): Effect.Effect => +): Effect.Effect => pipe( readProjectAuthSnapshot(project), Effect.tap((snapshot) => diff --git a/packages/app/src/docker-git/menu-render-select.ts b/packages/app/src/docker-git/menu-render-select.ts index d9da4f8a..fb17755b 100644 --- a/packages/app/src/docker-git/menu-render-select.ts +++ b/packages/app/src/docker-git/menu-render-select.ts @@ -1,9 +1,9 @@ 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" const computeListWidth = (labels: ReadonlyArray): number => { const maxLabelWidth = labels.length > 0 ? Math.max(...labels.map((label) => label.length)) : 24 diff --git a/packages/app/src/docker-git/menu-render.ts b/packages/app/src/docker-git/menu-render.ts index 53d0943f..0ae72a6e 100644 --- a/packages/app/src/docker-git/menu-render.ts +++ b/packages/app/src/docker-git/menu-render.ts @@ -1,6 +1,5 @@ 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" @@ -16,6 +15,7 @@ import { } from "./menu-render-select.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 diff --git a/packages/app/src/docker-git/menu-select-actions.ts b/packages/app/src/docker-git/menu-select-actions.ts index 82cf7f6f..29303daa 100644 --- a/packages/app/src/docker-git/menu-select-actions.ts +++ b/packages/app/src/docker-git/menu-select-actions.ts @@ -1,4 +1,3 @@ -import type { ProjectItem } from "@lib/usecases/projects" import { Effect, pipe } from "effect" import { deleteMenuProject, downMenuProject, listMenuRunningProjectItems } from "./menu-api.js" @@ -16,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 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-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 a5d0081a..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 @@ -172,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 2848e6d3..3817b4e5 100644 --- a/packages/app/src/docker-git/menu.ts +++ b/packages/app/src/docker-git/menu.ts @@ -1,5 +1,4 @@ import { NodeContext } from "@effect/platform-node" -import { runDockerPsNames } from "@lib/shell/docker" import { Effect, pipe } from "effect" import React, { useEffect, useMemo, useState } from "react" @@ -24,6 +23,11 @@ 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? Красивый, удобный" @@ -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(), @@ -271,6 +275,7 @@ const GridlandTuiApp = ({ exit, gridland }: { readonly exit: () => void; readonl const runInteractiveMenu = (): Effect.Effect => pipe( runGridlandMenu((args) => React.createElement(GridlandTuiApp, args)), + Effect.mapError((error) => gridlandBootstrapError(error.message)), Effect.ensuring( Effect.sync(() => { leaveTui() diff --git a/packages/app/src/docker-git/open-project-ssh.ts b/packages/app/src/docker-git/open-project-ssh.ts index 0619aae1..f9888d3b 100644 --- a/packages/app/src/docker-git/open-project-ssh.ts +++ b/packages/app/src/docker-git/open-project-ssh.ts @@ -1,105 +1,75 @@ -import { defaultTemplateConfig } from "@lib/core/domain" -import { runDockerInspectContainerRuntimeInfo } from "@lib/shell/docker" -import { buildSshCommand, connectProjectSsh, probeProjectSshReady, type ProjectItem } from "@lib/usecases/projects" -import { Effect, pipe } from "effect" +import { Effect } from "effect" -import { connectMenuProjectSshWithUp } from "./menu-api.js" +import { createProjectTerminalSession } from "./api-client.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 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 OpenResolvedProjectSshDeps = { + readonly createSession: ( + projectId: string + ) => Effect.Effect< + { + readonly project: Readonly> + readonly session: { + readonly id: string + readonly projectId: string + readonly sshCommand: string + readonly status: "ready" | "attached" | "exited" | "failed" + readonly createdAt: string + readonly startedAt?: string | undefined + readonly closedAt?: string | undefined + readonly exitCode?: number | undefined + readonly signal?: number | undefined + } + } | null, + HostError, + ControllerRuntime + > + readonly attach: ( + project: ProjectItem, + session: { + readonly id: string + readonly projectId: string + readonly sshCommand: string + readonly status: "ready" | "attached" | "exited" | "failed" + readonly createdAt: string + readonly startedAt?: string | undefined + readonly closedAt?: string | undefined + readonly exitCode?: number | undefined + readonly signal?: number | undefined + } + ) => Effect.Effect } -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 missingTerminalSessionError = (item: ProjectItem): HostError => ({ + _tag: "TerminalSessionClientError", + message: `Terminal session was not created for ${item.displayName}.` }) -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 = ( +export const openResolvedProjectSshEffect = ( 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 + 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 - } + const prepared = yield* _(deps.createSession(item.projectDir)) + if (prepared === null) { + return yield* _(Effect.fail(missingTerminalSessionError(item))) } - yield* _(deps.log(`Opening SSH: ${item.sshCommand}`)) - yield* _(deps.connectWithUp(item)) + yield* _(deps.attach(item, prepared.session)) }) 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) + 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 e68169de..53362e93 100644 --- a/packages/app/src/docker-git/open-project.ts +++ b/packages/app/src/docker-git/open-project.ts @@ -1,8 +1,7 @@ -import { type DockerContainerRuntimeInfo, runDockerInspectContainerRuntimeInfo } from "@lib/shell/docker" 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" @@ -10,6 +9,11 @@ import type { ProjectResolutionError } from "./host-errors.js" import { openResolvedProjectSsh } from "./open-project-ssh.js" import { resolveApiProjectItem } from "./project-item.js" +export type DockerContainerRuntimeInfo = { + readonly ipAddress: string + readonly projectWorkingDir?: string | undefined +} + export { openResolvedProjectSsh, type OpenResolvedProjectSshDeps, @@ -278,11 +282,7 @@ export const openExistingProjectSsh = ( 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..e2eeadc5 100644 --- a/packages/app/src/docker-git/project-item.ts +++ b/packages/app/src/docker-git/project-item.ts @@ -1,19 +1,31 @@ -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 type ProjectItem = { + readonly projectDir: string + readonly displayName: string + readonly repoUrl: string + readonly repoRef: string + readonly containerName: string + readonly serviceName: string + readonly sshUser: string + readonly sshPort: number + readonly targetDir: string + readonly sshCommand: string + readonly authorizedKeysPath: string + readonly authorizedKeysExists: boolean + readonly envGlobalPath: string + readonly envProjectPath: string + readonly codexAuthPath: string + readonly codexHome: 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 const projectItemFromApiDetails = ( - project: ApiProjectDetails, - sshKeyPath: string | null, - ipAddress?: string -): ProjectItem => ({ +export const projectItemFromApiDetails = (project: ApiProjectDetails): ProjectItem => ({ projectDir: project.projectDir, displayName: project.displayName, repoUrl: project.repoUrl, @@ -23,60 +35,19 @@ export const projectItemFromApiDetails = ( 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, + sshCommand: project.sshCommand, + authorizedKeysPath: project.authorizedKeysPath, + authorizedKeysExists: project.authorizedKeysExists, envGlobalPath: project.envGlobalPath, envProjectPath: project.envProjectPath, codexAuthPath: project.codexAuthPath, codexHome: project.codexHome, + status: project.status, + statusLabel: project.statusLabel, + sshSessions: project.sshSessions, + startedAtIso: project.startedAtIso, + startedAtEpochMs: project.startedAtEpochMs, clonedOnHostname: project.clonedOnHostname }) -export const resolveApiProjectItem = ( - project: ApiProjectDetails -) => - Effect.gen(function*(_) { - const sshKeyPath = yield* _(resolveHostPrivateKeyPath()) - return yield* _(resolveApiProjectItemWithSshKeyPath(project, sshKeyPath)) - }) - -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 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..fcd4d17e --- /dev/null +++ b/packages/app/src/docker-git/terminal-session-client.ts @@ -0,0 +1,205 @@ +import * as ParseResult from "@effect/schema/ParseResult" +import * as Schema from "@effect/schema/Schema" +import { Effect, Either } from "effect" + +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 TerminalServerMessage = + | { readonly type: "ready"; readonly session: ApiTerminalSession } + | { readonly type: "output"; readonly data: string } + | { readonly type: "exit"; readonly exitCode: number | null; readonly signal: number | null } + | { readonly type: "error"; readonly message: string } + +type TerminalAttachment = { + readonly header: string + readonly session: ApiTerminalSession + readonly websocketPath: string +} + +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 TerminalServerMessageSchema = Schema.parseJson( + 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 + }) + ) +) + +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 resolveTerminalWebSocketUrl = (websocketPath: string): string => { + const apiBaseUrl = new URL(resolveApiBaseUrl()) + apiBaseUrl.protocol = apiBaseUrl.protocol === "https:" ? "wss:" : "ws:" + apiBaseUrl.pathname = `${apiBaseUrl.pathname.replace(/\/$/u, "")}${websocketPath}` + apiBaseUrl.searchParams.set("cols", String(process.stdout.columns ?? 120)) + apiBaseUrl.searchParams.set("rows", String(process.stdout.rows ?? 32)) + return apiBaseUrl.toString() +} + +const sendResize = (socket: WebSocket): void => { + socket.send(encodeClientMessage({ + type: "resize", + cols: process.stdout.columns ?? 120, + rows: process.stdout.rows ?? 32 + })) +} + +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`) +} + +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 inputHandler = (chunk: Buffer): void => { + if (socket.readyState !== WebSocket.OPEN) { + return + } + socket.send(encodeClientMessage({ type: "input", data: chunk.toString("utf8") })) + } + + const resizeHandler = (): void => { + if (socket.readyState !== WebSocket.OPEN) { + return + } + sendResize(socket) + } + + socket.onopen = () => { + writeHeader(attachment) + process.stdin.resume() + setRawMode(true) + process.stdin.on("data", inputHandler) + process.stdout.on("resize", resizeHandler) + sendResize(socket) + } + + socket.onmessage = (event) => { + 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 + } + + if (message.type === "ready") { + return + } + + if (message.type === "output") { + writeToTerminal(message.data) + return + } + + if (message.type === "error") { + finish(Effect.fail(terminalSessionError(message.message))) + return + } + + sawExit = true + const suffix = message.exitCode === null ? "" : ` (exit ${message.exitCode})` + writeToTerminal(`\n[docker-git] terminal finished${suffix}\n`) + finish(Effect.void) + } + + socket.onerror = () => { + finish(Effect.fail(terminalSessionError("Terminal websocket error."))) + } + + socket.onclose = () => { + cleanupTerminalHandlers(socket, inputHandler, resizeHandler) + if (!sawExit) { + finish(Effect.fail(terminalSessionError("Terminal websocket closed before exit."))) + } + } + + return Effect.sync(() => { + cleanupTerminalHandlers(socket, inputHandler, 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/shell/docker-runtime.ts b/packages/app/src/lib/shell/docker-runtime.ts index f154ee09..b644ebca 100644 --- a/packages/app/src/lib/shell/docker-runtime.ts +++ b/packages/app/src/lib/shell/docker-runtime.ts @@ -4,19 +4,10 @@ import type * as CommandExecutor from "@effect/platform/CommandExecutor" import type { PlatformError } from "@effect/platform/Error" import { Effect, pipe } from "effect" -import { trimToUndefined } from "../../shared/trimmed-text.js" import { runCommandCapture } from "./command-runner.js" import { parseInspectNetworkEntry } from "./docker-inspect-parse.js" import { CommandFailedError, DockerCommandError } from "./errors.js" -export type DockerContainerRuntimeInfo = { - readonly containerName: string - readonly running: boolean - readonly ipAddress: string - readonly projectWorkingDir?: string | undefined - readonly composeService?: string | undefined -} - type DockerInspectReader = ( cwd: string, containerName: string @@ -82,35 +73,6 @@ export const runDockerInspectContainerIp = createDockerInspectReader( } ) -export const runDockerInspectContainerRuntimeInfo = ( - cwd: string, - containerName: string -): Effect.Effect => - pipe( - runDockerInspectValue( - cwd, - containerName, - `{{.State.Status}}\t{{with index .Config.Labels "com.docker.compose.project.working_dir"}}{{.}}{{end}}\t{{with index .Config.Labels "com.docker.compose.service"}}{{.}}{{end}}` - ), - Effect.flatMap((output) => { - const [status, projectWorkingDir, composeService] = output.trim().replaceAll(String.raw`\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: trimToUndefined(projectWorkingDir), - composeService: trimToUndefined(composeService) - })) - ) - }), - Effect.catchTag("DockerCommandError", () => Effect.succeed(null)) - ) - export const runDockerInspectContainerBridgeIp = createDockerInspectReader( "{{with (index .NetworkSettings.Networks \"bridge\")}}{{.IPAddress}}{{end}}", (output) => output.trim() 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 4d22cbdf..00000000 --- a/packages/app/tests/docker-git/create-project-identity-conflict.test.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { NodeContext } from "@effect/platform-node" -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 } from "effect" -import { beforeEach, vi } from "vitest" - -import { type CreateCommand, defaultTemplateConfig } 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.sync((): string | undefined => 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 -}) - -const makeConflictContext = ( - root: string, - path: Path.Path, - force: boolean -) => { - const outDir = path.join(root, "candidate") - const existingDir = path.join(root, "existing") - return { - outDir, - existingDir, - existingConfigPath: path.join(existingDir, "docker-git.json"), - command: makeCommand(root, outDir, force) - } -} - -const mockProjectIndex = ( - root: string, - path: Path.Path, - configPaths: ReadonlyArray -): void => { - loadProjectIndexMock.mockReturnValue( - Effect.succeed({ - projectsRoot: path.join(root, ".docker-git"), - configPaths - }) - ) -} - -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 { command, existingConfigPath, existingDir, outDir } = makeConflictContext(root, path, false) - - mockProjectIndex(root, path, [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 { command, existingConfigPath, existingDir } = makeConflictContext(root, path, true) - - mockProjectIndex(root, path, [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) - - mockProjectIndex(root, path, [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 ea599350..00000000 --- a/packages/app/tests/docker-git/docker-runtime-info.test.ts +++ /dev/null @@ -1,135 +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 joinIp = (...octets: ReadonlyArray): string => octets.join(".") - -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 resolveStdoutText = ( - invocation: RecordedCommand, - outputs: { - readonly runtimeOutput: string - readonly ipOutput: string - } -): string => { - if (isRuntimeInspect(invocation)) { - return outputs.runtimeOutput - } - if (isIpInspect(invocation)) { - return outputs.ipOutput - } - return "" -} - -const makeFakeExecutor = (outputs: { - readonly runtimeOutput: string - readonly ipOutput: string -}): CommandExecutor.CommandExecutor => { - const start = (command: Command.Command): Effect.Effect => - Effect.sync(() => { - const flattened = Command.flatten(command) - const last = flattened.at(-1)! - const invocation: RecordedCommand = { - command: last.command, - args: last.args - } - - const stdoutText = resolveStdoutText(invocation, outputs) - - 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) -} - -const loadRuntimeInfo = ( - outputs: { - readonly runtimeOutput: string - readonly ipOutput: string - } -) => - runDockerInspectContainerRuntimeInfo("/tmp", "dg-repo").pipe( - Effect.provideService(CommandExecutor.CommandExecutor, makeFakeExecutor(outputs)) - ) - -describe("runDockerInspectContainerRuntimeInfo", () => { - it.effect("parses running runtime ownership even when separators arrive as literal escapes", () => - Effect.gen(function*(_) { - const bridgeIp = joinIp(172, 17, 0, 15) - const projectIp = joinIp(10, 88, 0, 2) - const runtime = yield* _(loadRuntimeInfo({ - runtimeOutput: "running\\t/home/dev/.docker-git/test-owner/repo\\tdg-repo\n", - ipOutput: `bridge=${bridgeIp}\nproject=${projectIp}\n` - })) - - expect(runtime).toEqual({ - containerName: "dg-repo", - running: true, - ipAddress: bridgeIp, - 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 projectIp = joinIp(10, 88, 0, 4) - const runtime = yield* _(loadRuntimeInfo({ - runtimeOutput: "running\t\t\n", - ipOutput: `project=${projectIp}\n` - })) - - expect(runtime).toEqual({ - containerName: "dg-repo", - running: true, - ipAddress: projectIp, - 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/open-project-helpers.ts b/packages/app/tests/docker-git/fixtures/open-project-helpers.ts index 546ad2b7..e5db2248 100644 --- a/packages/app/tests/docker-git/fixtures/open-project-helpers.ts +++ b/packages/app/tests/docker-git/fixtures/open-project-helpers.ts @@ -17,10 +17,15 @@ const defaultProject = { 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" + codexHome: "/home/dev/.codex", + sshSessions: 0, + startedAtIso: null, + startedAtEpochMs: null } satisfies Omit export const makeProject = (overrides: Partial = {}): ApiProjectDetails => ({ diff --git a/packages/app/tests/docker-git/fixtures/open-project-ssh-helpers.ts b/packages/app/tests/docker-git/fixtures/open-project-ssh-helpers.ts deleted file mode 100644 index 817a0a09..00000000 --- a/packages/app/tests/docker-git/fixtures/open-project-ssh-helpers.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Effect } from "effect" - -import type { ProjectItem } from "@lib/usecases/projects" - -import { openResolvedProjectSshEffect } from "../../../src/docker-git/open-project.js" -import { recordEvent } from "./event-recorder.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 -} - -type OpenResolvedProjectSshOptions = - & Partial< - Omit - > - & { - readonly connectEntry?: (selected: ProjectItem) => string - readonly upEntry?: (selected: ProjectItem) => string - } - -export const makeOpenResolvedProjectSshDeps = ( - events: Array, - options: OpenResolvedProjectSshOptions = {} -): OpenResolvedProjectSshDeps => { - const { connectEntry, upEntry, ...overrides } = options - return { - log: (message) => recordEvent(events, `log:${message}`), - resolvePreferredItem: () => Effect.succeed(null), - probeReady: () => Effect.succeed(true), - connect: (selected) => recordEvent(events, connectEntry?.(selected) ?? `connect:${selected.projectDir}`), - connectWithUp: (selected) => recordEvent(events, upEntry?.(selected) ?? `up:${selected.projectDir}`), - ...overrides - } -} - -export const captureOpenResolvedProjectSshEvents = ( - item: ProjectItem, - options: OpenResolvedProjectSshOptions = {} -): Effect.Effect> => - Effect.gen(function*(_) { - const events: Array = [] - yield* _(openResolvedProjectSshEffect(item, makeOpenResolvedProjectSshDeps(events, options))) - return events - }) diff --git a/packages/app/tests/docker-git/fixtures/project-item.ts b/packages/app/tests/docker-git/fixtures/project-item.ts index 2d38c2af..0f0cf457 100644 --- a/packages/app/tests/docker-git/fixtures/project-item.ts +++ b/packages/app/tests/docker-git/fixtures/project-item.ts @@ -1,4 +1,4 @@ -import type { ProjectItem } from "@lib/usecases/projects" +import type { ProjectItem } from "../../../src/docker-git/project-item.js" export const makeProjectItem = ( overrides: Partial = {} @@ -13,12 +13,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 c9767708..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, - bunVersion: "1.3.11" - }, - 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-select-connect.test.ts b/packages/app/tests/docker-git/menu-select-connect.test.ts index bce4a18c..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,7 +1,7 @@ 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" 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 index 619976f6..b2c9ede7 100644 --- a/packages/app/tests/docker-git/open-project-ssh.test.ts +++ b/packages/app/tests/docker-git/open-project-ssh.test.ts @@ -1,104 +1,65 @@ +import { NodeContext } from "@effect/platform-node" import { describe, expect, it } from "@effect/vitest" import { Effect } from "effect" -import type { ProjectItem } from "@lib/usecases/projects" - -import { liveFallbackIp, liveRuntimeIp } from "./fixtures/open-project-helpers.js" -import { captureOpenResolvedProjectSshEvents } from "./fixtures/open-project-ssh-helpers.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" -type RuntimePreferenceCase = { - readonly name: string - readonly projectDir: string - readonly localSshCommand: string - readonly sshPort: number - readonly preferredIp: string - readonly preferredSshCommand: string - readonly probeReady: (selected: ProjectItem) => boolean - readonly expected: ReadonlyArray -} - -const runtimePreferenceCases: ReadonlyArray = [ - { - name: "prefers a live runtime SSH target before falling back to docker up", - projectDir: "/controller/org/repo/issue-9", - localSshCommand: "ssh -p 2253 dev@localhost", - sshPort: 2253, - preferredIp: liveRuntimeIp, - preferredSshCommand: `ssh -p 22 dev@${liveRuntimeIp}`, - probeReady: (selected: ProjectItem) => selected.ipAddress === liveRuntimeIp, - expected: [ - `log:Opening SSH: ssh -p 22 dev@${liveRuntimeIp}`, - `connect:ssh -p 22 dev@${liveRuntimeIp}` - ] - }, - { - name: "falls back to the original SSH target when live runtime probe fails", - projectDir: "/controller/org/repo/issue-10", - localSshCommand: "ssh -p 2237 dev@localhost", - sshPort: 2237, - preferredIp: liveFallbackIp, - preferredSshCommand: `ssh -p 22 dev@${liveFallbackIp}`, - probeReady: (selected: ProjectItem) => selected.ipAddress !== liveFallbackIp, - expected: [ - "log:Opening SSH: ssh -p 2237 dev@localhost", - "connect:ssh -p 2237 dev@localhost" - ] - } -] +const makeSession = () => ({ + id: "session-1", + projectId: "/controller/org/repo", + sshCommand: "ssh -p 22 dev@127.0.0.1", + status: "ready" as const, + createdAt: "2026-04-10T00:00:00Z" +}) describe("openResolvedProjectSshEffect", () => { - it.effect("connects directly when SSH is already reachable", () => + it.effect("attaches to a prepared terminal session", () => Effect.gen(function*(_) { - const item = makeProjectItem({ - projectDir: "/controller/org/repo/issue-7", - sshCommand: `ssh -p 22 dev@${liveFallbackIp}` - }) + const item = makeProjectItem({ projectDir: "/controller/org/repo/issue-7" }) const events = yield* _(captureOpenResolvedProjectSshEvents(item)) - expect(events).toEqual([ - `log:Opening SSH: ssh -p 22 dev@${liveFallbackIp}`, - "connect:/controller/org/repo/issue-7" - ]) + expect(events).toEqual(["create:/controller/org/repo/issue-7", "attach:session-1"]) })) - it.effect("falls back to docker up when SSH is not yet reachable", () => + it.effect("fails when controller does not create a terminal session", () => Effect.gen(function*(_) { - const item = makeProjectItem({ - projectDir: "/controller/org/repo/issue-8", - sshCommand: "ssh -p 2222 dev@localhost" - }) - const events = yield* _( - captureOpenResolvedProjectSshEvents(item, { - probeReady: () => Effect.succeed(false) - }) + 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(events).toEqual([ - "log:Opening SSH: ssh -p 2222 dev@localhost", - "up:/controller/org/repo/issue-8" - ]) + + expect(exit._tag).toBe("Failure") })) +}) - for (const testCase of runtimePreferenceCases) { - it.effect(testCase.name, () => - Effect.gen(function*(_) { - const item = makeProjectItem({ - projectDir: testCase.projectDir, - sshCommand: testCase.localSshCommand, - sshPort: testCase.sshPort - }) - const preferred = makeProjectItem({ - ...item, - ipAddress: testCase.preferredIp, - sshCommand: testCase.preferredSshCommand - }) - const events = yield* _( - captureOpenResolvedProjectSshEvents(item, { - resolvePreferredItem: () => Effect.succeed(preferred), - probeReady: (selected) => Effect.succeed(testCase.probeReady(selected)), - connectEntry: (selected) => `connect:${selected.sshCommand}` +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}`) }) - ) - expect(events).toEqual(testCase.expected) - })) - } -}) + }) + ) + 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 29c6c80e..91a3d5a6 100644 --- a/packages/app/tests/docker-git/open-project.test.ts +++ b/packages/app/tests/docker-git/open-project.test.ts @@ -2,159 +2,10 @@ import { describe, expect, it } from "@effect/vitest" import { Effect } from "effect" -import { - openResolvedProjectSshEffect, - resolveOpenProjectEffect, - selectOpenProject -} from "../../src/docker-git/open-project.js" -import { expectSelectedProject, liveFallbackIp, liveRuntimeIp, makeProject } from "./fixtures/open-project-helpers.js" -import { makeProjectItem } from "./fixtures/project-item.js" +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@${liveFallbackIp}` - }) - 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@${liveFallbackIp}`, - "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: liveRuntimeIp, - sshCommand: `ssh -p 22 dev@${liveRuntimeIp}` - }) - const events: Array = [] - - yield* _( - openResolvedProjectSshEffect(item, { - log: (message) => - Effect.sync(() => { - events.push(`log:${message}`) - }), - resolvePreferredItem: () => Effect.succeed(preferred), - probeReady: (selected) => Effect.succeed(selected.ipAddress === liveRuntimeIp), - 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@${liveRuntimeIp}`, - `connect:ssh -p 22 dev@${liveRuntimeIp}` - ]) - })) - - 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: liveFallbackIp, - sshCommand: `ssh -p 22 dev@${liveFallbackIp}` - }) - const events: Array = [] - - yield* _( - openResolvedProjectSshEffect(item, { - log: (message) => - Effect.sync(() => { - events.push(`log:${message}`) - }), - resolvePreferredItem: () => Effect.succeed(preferred), - probeReady: (selected) => Effect.succeed(selected.ipAddress !== liveFallbackIp), - 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({ 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/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/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/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))) From 8e7f1917e5ef4becc94e364be6d2e970f35a9b11 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:48:33 +0000 Subject: [PATCH 19/26] fix(app): remove remaining lint-effect casts --- packages/app/src/docker-git/menu-api.ts | 5 +++-- packages/app/tests/docker-git/open-project-ssh.test.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/app/src/docker-git/menu-api.ts b/packages/app/src/docker-git/menu-api.ts index d1503f04..d818dd25 100644 --- a/packages/app/src/docker-git/menu-api.ts +++ b/packages/app/src/docker-git/menu-api.ts @@ -11,14 +11,15 @@ import { 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 { type ProjectItem, resolveApiProjectItem } from "./project-item.js" -const menuGithubStatusCommand = { +const menuGithubStatusCommand: AuthGithubStatusCommand = { _tag: "AuthGithubStatus", envGlobalPath: "" -} as const +} const compact = (values: ReadonlyArray): ReadonlyArray => values.filter((value): value is A => value !== 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 index b2c9ede7..bba229a2 100644 --- a/packages/app/tests/docker-git/open-project-ssh.test.ts +++ b/packages/app/tests/docker-git/open-project-ssh.test.ts @@ -2,15 +2,16 @@ 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 = () => ({ +const makeSession = (): ApiTerminalSession => ({ id: "session-1", projectId: "/controller/org/repo", sshCommand: "ssh -p 22 dev@127.0.0.1", - status: "ready" as const, + status: "ready", createdAt: "2026-04-10T00:00:00Z" }) From 024ecea1c512523e7a2f61ec6ead5272074640c2 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:07:50 +0000 Subject: [PATCH 20/26] fix(ci): restore check workflow stability --- .github/actions/setup/action.yml | 5 + bun.lock | 8 +- packages/api/Dockerfile | 2 +- packages/app/package.json | 4 +- .../app/src/docker-git/api-project-codec.ts | 89 +++++--- .../src/docker-git/terminal-session-client.ts | 200 ++++++++++++------ 6 files changed, 203 insertions(+), 105 deletions(-) diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 417dd19c..3dcf1303 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -21,6 +21,11 @@ runs: uses: actions/setup-node@v6 with: 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 diff --git a/bun.lock b/bun.lock index 2d9de357..6741a267 100644 --- a/bun.lock +++ b/bun.lock @@ -37,7 +37,7 @@ }, "packages/app": { "name": "@prover-coder-ai/docker-git", - "version": "1.0.76", + "version": "1.0.77", "bin": { "docker-git": "dist/src/docker-git/main.js", }, @@ -54,8 +54,8 @@ "@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", + "@gridland/bun": "0.2.53", + "@gridland/web": "0.2.53", "effect": "^3.21.0", "react": "^19.2.4", "react-dom": "^19.2.4", @@ -270,7 +270,7 @@ "@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.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-crQwaeLmpmUfKaH2K42STNFMD5cywR6kBGKZI0FkvCbDyG3xM7CikJKucMIXOBvnUviO8loq0afT1ZSCtZiaaw=="], + "@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=="], diff --git a/packages/api/Dockerfile b/packages/api/Dockerfile index 91119ec5..c616778d 100644 --- a/packages/api/Dockerfile +++ b/packages/api/Dockerfile @@ -10,7 +10,7 @@ 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 python3 make g++ unzip \ + 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 - \ diff --git a/packages/app/package.json b/packages/app/package.json index 8a29144d..78955ad4 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -73,8 +73,8 @@ "@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", + "@gridland/bun": "0.2.53", + "@gridland/web": "0.2.53", "effect": "^3.21.0", "react": "^19.2.4", "react-dom": "^19.2.4", diff --git a/packages/app/src/docker-git/api-project-codec.ts b/packages/app/src/docker-git/api-project-codec.ts index 8679c374..3d7ac836 100644 --- a/packages/app/src/docker-git/api-project-codec.ts +++ b/packages/app/src/docker-git/api-project-codec.ts @@ -30,6 +30,21 @@ export type ApiProjectDetails = ApiProjectSummary & { } type ProjectDetailFields = 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 @@ -38,8 +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 : value === null ? null : null +const readNullableNumber = (value: JsonValue | undefined): number | null => typeof value === "number" ? value : null const readSummaryBaseFields = ( object: ReturnType @@ -78,46 +92,53 @@ const readSummaryBaseFields = ( const readRequiredProjectDetails = ( object: ReturnType -): ProjectDetailFields | 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 authorizedKeysPath = asString(object["authorizedKeysPath"]) - const authorizedKeysExists = typeof object["authorizedKeysExists"] === "boolean" - ? object["authorizedKeysExists"] - : null - 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, authorizedKeysPath, authorizedKeysExists, 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), - authorizedKeysPath: stringOrEmpty(authorizedKeysPath), - authorizedKeysExists: authorizedKeysExists === true, - 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) } } @@ -134,7 +155,7 @@ const readProjectSummaryFields = (value: JsonValue): ApiProjectSummary | null => } const readProjectDetailFields = (value: JsonValue): ProjectDetailFields | null => - readRequiredProjectDetails(asObject(value)) + decodeRequiredProjectDetails(asObject(value)) export const decodeProjectSummary = (value: JsonValue): ApiProjectSummary | null => readProjectSummaryFields(value) diff --git a/packages/app/src/docker-git/terminal-session-client.ts b/packages/app/src/docker-git/terminal-session-client.ts index fcd4d17e..4d2d1bda 100644 --- a/packages/app/src/docker-git/terminal-session-client.ts +++ b/packages/app/src/docker-git/terminal-session-client.ts @@ -28,6 +28,15 @@ type TerminalAttachment = { 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 TerminalSessionSchema = Schema.Struct({ id: Schema.String, projectId: Schema.String, @@ -77,20 +86,25 @@ const encodeClientMessage = (message: TerminalClientMessage): string => JSON.str 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(process.stdout.columns ?? 120)) - apiBaseUrl.searchParams.set("rows", String(process.stdout.rows ?? 32)) + 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: process.stdout.columns ?? 120, - rows: process.stdout.rows ?? 32 + cols, + rows })) } @@ -118,6 +132,111 @@ const writeHeader = (attachment: TerminalAttachment): void => { 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 => @@ -134,70 +253,23 @@ export const attachTerminalSession = ( resume(effect) } - const inputHandler = (chunk: Buffer): void => { - if (socket.readyState !== WebSocket.OPEN) { - return - } - socket.send(encodeClientMessage({ type: "input", data: chunk.toString("utf8") })) - } - - const resizeHandler = (): void => { - if (socket.readyState !== WebSocket.OPEN) { - return - } - sendResize(socket) - } - - socket.onopen = () => { - writeHeader(attachment) - process.stdin.resume() - setRawMode(true) - process.stdin.on("data", inputHandler) - process.stdout.on("resize", resizeHandler) - sendResize(socket) - } - - socket.onmessage = (event) => { - 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 + const handlers = createTerminalHandlers( + attachment, + socket, + finish, + () => sawExit, + () => { + sawExit = true } + ) - if (message.type === "ready") { - return - } - - if (message.type === "output") { - writeToTerminal(message.data) - return - } - - if (message.type === "error") { - finish(Effect.fail(terminalSessionError(message.message))) - return - } - - sawExit = true - const suffix = message.exitCode === null ? "" : ` (exit ${message.exitCode})` - writeToTerminal(`\n[docker-git] terminal finished${suffix}\n`) - finish(Effect.void) - } - - socket.onerror = () => { - finish(Effect.fail(terminalSessionError("Terminal websocket error."))) - } - - socket.onclose = () => { - cleanupTerminalHandlers(socket, inputHandler, resizeHandler) - if (!sawExit) { - finish(Effect.fail(terminalSessionError("Terminal websocket closed before exit."))) - } - } + 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, inputHandler, resizeHandler) + cleanupTerminalHandlers(socket, handlers.inputHandler, handlers.resizeHandler) if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) { socket.close() } From dc26d75576b324b6f2f0b58206171e2af91d0a9d Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:13:07 +0000 Subject: [PATCH 21/26] fix(ci): reduce auth codec lint complexity --- packages/app/src/docker-git/api-auth-codec.ts | 148 ++++++++++++------ 1 file changed, 102 insertions(+), 46 deletions(-) diff --git a/packages/app/src/docker-git/api-auth-codec.ts b/packages/app/src/docker-git/api-auth-codec.ts index b9508e0e..c8ceeb86 100644 --- a/packages/app/src/docker-git/api-auth-codec.ts +++ b/packages/app/src/docker-git/api-auth-codec.ts @@ -1,21 +1,55 @@ import { asObject, asString, type JsonValue } from "./api-json.js" import type { AuthSnapshot, ProjectAuthSnapshot } from "./menu-types.js" -const readNumber = (value: JsonValue | undefined): number | null => - typeof value === "number" ? value : null +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) } -export const decodeAuthSnapshot = (payload: JsonValue): AuthSnapshot | null => { - const snapshot = resolveSnapshotObject(payload) +const readAuthSnapshot = ( + snapshot: ReturnType +): RawAuthSnapshot | null => { if (snapshot === null) { return null } - const decoded = { + return { globalEnvPath: asString(snapshot["globalEnvPath"]), claudeAuthPath: asString(snapshot["claudeAuthPath"]), geminiAuthPath: asString(snapshot["geminiAuthPath"]), @@ -26,29 +60,34 @@ export const decodeAuthSnapshot = (payload: JsonValue): AuthSnapshot | null => { claudeAuthEntries: readNumber(snapshot["claudeAuthEntries"]), geminiAuthEntries: readNumber(snapshot["geminiAuthEntries"]) } +} + +const decodeRequiredAuthSnapshot = (snapshot: RawAuthSnapshot): AuthSnapshot | null => { + if (hasNullValue(Object.values(snapshot))) { + return null + } - return Object.values(decoded).includes(null) - ? null - : { - globalEnvPath: decoded.globalEnvPath ?? "", - claudeAuthPath: decoded.claudeAuthPath ?? "", - geminiAuthPath: decoded.geminiAuthPath ?? "", - totalEntries: decoded.totalEntries ?? 0, - githubTokenEntries: decoded.githubTokenEntries ?? 0, - gitTokenEntries: decoded.gitTokenEntries ?? 0, - gitUserEntries: decoded.gitUserEntries ?? 0, - claudeAuthEntries: decoded.claudeAuthEntries ?? 0, - geminiAuthEntries: decoded.geminiAuthEntries ?? 0 - } + 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) + } } -export const decodeProjectAuthSnapshot = (payload: JsonValue): ProjectAuthSnapshot | null => { - const snapshot = resolveSnapshotObject(payload) +const readProjectAuthSnapshot = ( + snapshot: ReturnType +): RawProjectAuthSnapshot | null => { if (snapshot === null) { return null } - const decoded = { + return { projectDir: asString(snapshot["projectDir"]), projectName: asString(snapshot["projectName"]), envGlobalPath: asString(snapshot["envGlobalPath"]), @@ -64,37 +103,54 @@ export const decodeProjectAuthSnapshot = (payload: JsonValue): ProjectAuthSnapsh activeClaudeLabel: asString(snapshot["activeClaudeLabel"]), activeGeminiLabel: asString(snapshot["activeGeminiLabel"]) } +} +const decodeRequiredProjectAuthSnapshot = ( + snapshot: RawProjectAuthSnapshot +): ProjectAuthSnapshot | null => { const requiredValues = [ - decoded.projectDir, - decoded.projectName, - decoded.envGlobalPath, - decoded.envProjectPath, - decoded.claudeAuthPath, - decoded.geminiAuthPath, - decoded.githubTokenEntries, - decoded.gitTokenEntries, - decoded.claudeAuthEntries, - decoded.geminiAuthEntries + snapshot.projectDir, + snapshot.projectName, + snapshot.envGlobalPath, + snapshot.envProjectPath, + snapshot.claudeAuthPath, + snapshot.geminiAuthPath, + snapshot.githubTokenEntries, + snapshot.gitTokenEntries, + snapshot.claudeAuthEntries, + snapshot.geminiAuthEntries ] - if (requiredValues.includes(null)) { + + if (hasNullValue(requiredValues)) { return null } return { - projectDir: decoded.projectDir ?? "", - projectName: decoded.projectName ?? "", - envGlobalPath: decoded.envGlobalPath ?? "", - envProjectPath: decoded.envProjectPath ?? "", - claudeAuthPath: decoded.claudeAuthPath ?? "", - geminiAuthPath: decoded.geminiAuthPath ?? "", - githubTokenEntries: decoded.githubTokenEntries ?? 0, - gitTokenEntries: decoded.gitTokenEntries ?? 0, - claudeAuthEntries: decoded.claudeAuthEntries ?? 0, - geminiAuthEntries: decoded.geminiAuthEntries ?? 0, - activeGithubLabel: decoded.activeGithubLabel, - activeGitLabel: decoded.activeGitLabel, - activeClaudeLabel: decoded.activeClaudeLabel, - activeGeminiLabel: decoded.activeGeminiLabel + 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) +} From 50e82956a7b42110a3d6e403a0bb57b2dda63ba6 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:17:04 +0000 Subject: [PATCH 22/26] fix(ci): simplify terminal codec decoding --- .../app/src/docker-git/api-terminal-codec.ts | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/app/src/docker-git/api-terminal-codec.ts b/packages/app/src/docker-git/api-terminal-codec.ts index ca960a3c..815e6a26 100644 --- a/packages/app/src/docker-git/api-terminal-codec.ts +++ b/packages/app/src/docker-git/api-terminal-codec.ts @@ -12,6 +12,18 @@ export type ApiTerminalSession = { readonly signal?: number | undefined } +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"] => @@ -20,13 +32,13 @@ const isTerminalSessionStatus = ( const readOptionalNumber = (value: JsonValue | undefined): number | undefined => typeof value === "number" ? value : undefined -export const decodeTerminalSession = (payload: JsonValue): ApiTerminalSession | null => { +const readTerminalSession = (payload: JsonValue): RawTerminalSession | null => { const object = asObject(payload) if (object === null) { return null } - const session = { + return { id: asString(object["id"]), projectId: asString(object["projectId"]), sshCommand: asString(object["sshCommand"]), @@ -37,6 +49,13 @@ export const decodeTerminalSession = (payload: JsonValue): ApiTerminalSession | 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 || From c0b31a02d4b4fef1c9cb7005bad4a3f5fa6692ad Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:57:29 +0000 Subject: [PATCH 23/26] fix(ci): sync controller env and app dedupe --- .../src/docker-git/api-auth-menu-client.ts | 39 ++++++++++++++ .../app/src/docker-git/api-client-helpers.ts | 47 ++++++++++++++++ packages/app/src/docker-git/api-client.ts | 53 +++---------------- .../app/src/docker-git/api-terminal-codec.ts | 13 +---- .../app/src/docker-git/cli/parser-auth.ts | 12 ++--- .../app/src/docker-git/controller-docker.ts | 18 ++++++- packages/app/src/docker-git/host-ssh.ts | 25 +-------- packages/app/src/docker-git/menu-auth-data.ts | 16 +++--- .../src/docker-git/menu-project-auth-data.ts | 10 ++-- .../app/src/docker-git/open-project-ssh.ts | 25 ++------- packages/app/src/docker-git/project-item.ts | 50 +---------------- .../src/docker-git/terminal-session-client.ts | 47 +--------------- .../app/src/lib/usecases/auto-open-ssh.ts | 28 +--------- packages/app/src/shared/auth-menu-request.ts | 12 +++++ packages/app/src/shared/auto-open-ssh.ts | 27 ++++++++++ packages/app/src/shared/optional-text.ts | 4 ++ .../app/src/shared/terminal-session-schema.ts | 43 +++++++++++++++ packages/app/src/web/api-schema.ts | 25 +++------ packages/app/src/web/api.ts | 16 ++---- packages/app/src/web/terminal.ts | 37 +++---------- .../tests/docker-git/fixtures/project-item.ts | 1 + 21 files changed, 237 insertions(+), 311 deletions(-) create mode 100644 packages/app/src/docker-git/api-auth-menu-client.ts create mode 100644 packages/app/src/docker-git/api-client-helpers.ts create mode 100644 packages/app/src/shared/auth-menu-request.ts create mode 100644 packages/app/src/shared/auto-open-ssh.ts create mode 100644 packages/app/src/shared/optional-text.ts create mode 100644 packages/app/src/shared/terminal-session-schema.ts 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-helpers.ts b/packages/app/src/docker-git/api-client-helpers.ts new file mode 100644 index 00000000..16e56bd5 --- /dev/null +++ b/packages/app/src/docker-git/api-client-helpers.ts @@ -0,0 +1,47 @@ +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) + +export const resolveCreateRequestPaths = (command: CreateCommand) => + Effect.gen(function*(_) { + const path = yield* _(Path.Path) + const cwd = process.cwd() + + return { + authorizedKeysPath: command.config.authorizedKeysPath === defaultTemplateConfig.authorizedKeysPath + ? command.config.authorizedKeysPath + : resolveClientCreatePath(path, cwd, command.config.authorizedKeysPath) + } + }) diff --git a/packages/app/src/docker-git/api-client.ts b/packages/app/src/docker-git/api-client.ts index c9703d2d..ef430642 100644 --- a/packages/app/src/docker-git/api-client.ts +++ b/packages/app/src/docker-git/api-client.ts @@ -2,9 +2,9 @@ import * as FileSystem from "@effect/platform/FileSystem" import * as Path from "@effect/platform/Path" import { Effect } from "effect" -import { decodeAuthSnapshot, decodeProjectAuthSnapshot } from "./api-auth-codec.js" +import { readProjectOutput, resolveCreateRequestPaths } from "./api-client-helpers.js" import { request, requestTextStream, requestVoid } from "./api-http.js" -import { asArray, asObject, asString, type JsonRequest, type JsonValue } from "./api-json.js" +import { asArray, asObject, type JsonRequest } from "./api-json.js" import { decodeProjectDetails, decodeProjectSummary } from "./api-project-codec.js" import { decodeTerminalSession } from "./api-terminal-codec.js" import type { @@ -66,11 +66,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) => { @@ -93,6 +88,7 @@ export const getProject = (projectId: string) => export const createProject = (command: CreateCommand) => Effect.gen(function*(_) { const config = command.config + const resolvedPaths = yield* _(resolveCreateRequestPaths(command)) const body = { repoUrl: config.repoUrl, repoRef: config.repoRef, @@ -102,6 +98,11 @@ export const createProject = (command: CreateCommand) => containerName: config.containerName, serviceName: config.serviceName, volumeName: config.volumeName, + authorizedKeysPath: resolvedPaths.authorizedKeysPath, + envGlobalPath: config.envGlobalPath, + envProjectPath: config.envProjectPath, + codexAuthPath: config.codexAuthPath, + codexHome: config.codexHome, cpuLimit: config.cpuLimit, ramLimit: config.ramLimit, dockerNetworkMode: config.dockerNetworkMode, @@ -166,44 +167,6 @@ export const createAuthTerminalSession = ( export const deleteTerminalSessionByPath = (path: string) => requestVoid("DELETE", path) -export const loadAuthSnapshot = () => - request("GET", "/auth/menu").pipe( - Effect.map((payload) => decodeAuthSnapshot(payload)) - ) - -export const runAuthMenuFlow = (requestBody: { - readonly flow: string - readonly label?: string | null - readonly token?: string | null - readonly user?: string | null - readonly apiKey?: string | null -}) => - 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: { readonly flow: string; readonly label?: string | null } -) => - request("POST", projectPath(projectId, "/auth/menu"), { - flow: requestBody.flow, - label: requestBody.label ?? undefined - }).pipe( - Effect.map((payload) => decodeProjectAuthSnapshot(payload)) - ) - export const applyAllProjects = (activeOnly: boolean) => requestVoid("POST", "/projects/apply-all", { activeOnly }) export const downAllProjects = () => requestVoid("POST", "/projects/down-all") diff --git a/packages/app/src/docker-git/api-terminal-codec.ts b/packages/app/src/docker-git/api-terminal-codec.ts index 815e6a26..c14aaab7 100644 --- a/packages/app/src/docker-git/api-terminal-codec.ts +++ b/packages/app/src/docker-git/api-terminal-codec.ts @@ -1,16 +1,7 @@ +import type { TerminalSession } from "../shared/terminal-session-schema.js" import { asObject, asString, type JsonValue } from "./api-json.js" -export type ApiTerminalSession = { - readonly id: string - readonly projectId: string - readonly sshCommand: string - readonly status: "ready" | "attached" | "exited" | "failed" - readonly createdAt: string - readonly startedAt?: string | undefined - readonly closedAt?: string | undefined - readonly exitCode?: number | undefined - readonly signal?: number | undefined -} +export type ApiTerminalSession = TerminalSession type RawTerminalSession = { readonly id: string | null diff --git a/packages/app/src/docker-git/cli/parser-auth.ts b/packages/app/src/docker-git/cli/parser-auth.ts index dd25c299..abf1a049 100644 --- a/packages/app/src/docker-git/cli/parser-auth.ts +++ b/packages/app/src/docker-git/cli/parser-auth.ts @@ -1,5 +1,6 @@ import { Either, Match } from "effect" +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" @@ -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/controller-docker.ts b/packages/app/src/docker-git/controller-docker.ts index c9bf683e..b97aaacf 100644 --- a/packages/app/src/docker-git/controller-docker.ts +++ b/packages/app/src/docker-git/controller-docker.ts @@ -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/host-ssh.ts b/packages/app/src/docker-git/host-ssh.ts index ff033dd3..367793b7 100644 --- a/packages/app/src/docker-git/host-ssh.ts +++ b/packages/app/src/docker-git/host-ssh.ts @@ -1,5 +1,6 @@ import { Effect } from "effect" +import { shouldAutoOpenSsh } from "../shared/auto-open-ssh.js" import { createProjectTerminalSession } from "./api-client.js" import type { ApiProjectDetails } from "./api-project-codec.js" import { projectItemFromApiDetails } from "./project-item.js" @@ -16,30 +17,6 @@ const renderKnownError = (error: RenderableError): string => error.message const shouldOpenSsh = (command: AutoOpenSshCommand): boolean => command.openSsh -const isInteractiveTty = (): boolean => process.stdin.isTTY && process.stdout.isTTY - -const shouldAutoOpenSsh = ({ - runUp, - shouldOpen -}: { - readonly shouldOpen: boolean - readonly runUp: boolean -}): 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 - }) - export const autoOpenProjectSsh = ( command: AutoOpenSshCommand, project: ApiProjectDetails | null diff --git a/packages/app/src/docker-git/menu-auth-data.ts b/packages/app/src/docker-git/menu-auth-data.ts index 865144c7..6c5e05b0 100644 --- a/packages/app/src/docker-git/menu-auth-data.ts +++ b/packages/app/src/docker-git/menu-auth-data.ts @@ -1,6 +1,7 @@ import { Effect } from "effect" -import { loadAuthSnapshot, runAuthMenuFlow as submitAuthMenuFlow } from "./api-client.js" +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" @@ -15,11 +16,6 @@ export { } from "./menu-auth-shared.js" export type { AuthEnvFlow, AuthMenuAction, AuthPromptStep } from "./menu-auth-shared.js" -const defaultValue = (value: string | undefined): string | null => { - const trimmed = value?.trim() ?? "" - return trimmed.length === 0 ? null : trimmed -} - const decodeSnapshot = (snapshot: AuthSnapshot | null): Effect.Effect => snapshot === null ? Effect.fail({ @@ -42,10 +38,10 @@ export const writeAuthFlow = ( ): Effect.Effect => submitAuthMenuFlow({ flow, - label: defaultValue(values["label"]), - token: defaultValue(values["token"]), - user: defaultValue(values["user"]), - apiKey: defaultValue(values["apiKey"]) + label: normalizeOptionalText(values["label"]), + token: normalizeOptionalText(values["token"]), + user: normalizeOptionalText(values["user"]), + apiKey: normalizeOptionalText(values["apiKey"]) }).pipe( Effect.flatMap((snapshot) => decodeSnapshot(snapshot)), Effect.asVoid 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 7952c5f5..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,6 +1,7 @@ import { Effect } from "effect" -import { loadProjectAuthSnapshot, runProjectAuthFlow as submitProjectAuthFlow } from "./api-client.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" import type { ProjectItem } from "./project-item.js" @@ -14,11 +15,6 @@ export { } from "./menu-project-auth-shared.js" export type { ProjectAuthMenuAction, ProjectAuthPromptStep } from "./menu-project-auth-shared.js" -const defaultValue = (value: string | undefined): string | null => { - const trimmed = value?.trim() ?? "" - return trimmed.length === 0 ? null : trimmed -} - const decodeSnapshot = ( projectId: string, snapshot: ProjectAuthSnapshot | null @@ -46,7 +42,7 @@ export const writeProjectAuthFlow = ( ): Effect.Effect => submitProjectAuthFlow(project.projectDir, { flow, - label: defaultValue(values["label"]) + label: normalizeOptionalText(values["label"]) }).pipe( Effect.flatMap((snapshot) => decodeSnapshot(project.projectDir, snapshot)), Effect.asVoid diff --git a/packages/app/src/docker-git/open-project-ssh.ts b/packages/app/src/docker-git/open-project-ssh.ts index f9888d3b..dc511cd7 100644 --- a/packages/app/src/docker-git/open-project-ssh.ts +++ b/packages/app/src/docker-git/open-project-ssh.ts @@ -1,6 +1,7 @@ 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" @@ -12,34 +13,14 @@ export type OpenResolvedProjectSshDeps = { ) => Effect.Effect< { readonly project: Readonly> - readonly session: { - readonly id: string - readonly projectId: string - readonly sshCommand: string - readonly status: "ready" | "attached" | "exited" | "failed" - readonly createdAt: string - readonly startedAt?: string | undefined - readonly closedAt?: string | undefined - readonly exitCode?: number | undefined - readonly signal?: number | undefined - } + readonly session: ApiTerminalSession } | null, HostError, ControllerRuntime > readonly attach: ( project: ProjectItem, - session: { - readonly id: string - readonly projectId: string - readonly sshCommand: string - readonly status: "ready" | "attached" | "exited" | "failed" - readonly createdAt: string - readonly startedAt?: string | undefined - readonly closedAt?: string | undefined - readonly exitCode?: number | undefined - readonly signal?: number | undefined - } + session: ApiTerminalSession ) => Effect.Effect } diff --git a/packages/app/src/docker-git/project-item.ts b/packages/app/src/docker-git/project-item.ts index e2eeadc5..efdd19ef 100644 --- a/packages/app/src/docker-git/project-item.ts +++ b/packages/app/src/docker-git/project-item.ts @@ -1,53 +1,7 @@ import type { ApiProjectDetails } from "./api-project-codec.js" -export type ProjectItem = { - readonly projectDir: string - readonly displayName: string - readonly repoUrl: string - readonly repoRef: string - readonly containerName: string - readonly serviceName: string - readonly sshUser: string - readonly sshPort: number - readonly targetDir: string - readonly sshCommand: string - readonly authorizedKeysPath: string - readonly authorizedKeysExists: boolean - readonly envGlobalPath: string - readonly envProjectPath: string - readonly codexAuthPath: string - readonly codexHome: 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 ProjectItem = ApiProjectDetails -export const projectItemFromApiDetails = (project: ApiProjectDetails): 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: project.sshCommand, - authorizedKeysPath: project.authorizedKeysPath, - authorizedKeysExists: project.authorizedKeysExists, - envGlobalPath: project.envGlobalPath, - envProjectPath: project.envProjectPath, - codexAuthPath: project.codexAuthPath, - codexHome: project.codexHome, - status: project.status, - statusLabel: project.statusLabel, - sshSessions: project.sshSessions, - startedAtIso: project.startedAtIso, - startedAtEpochMs: project.startedAtEpochMs, - clonedOnHostname: project.clonedOnHostname -}) +export const projectItemFromApiDetails = (project: ApiProjectDetails): ProjectItem => project 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 index 4d2d1bda..6311ff8b 100644 --- a/packages/app/src/docker-git/terminal-session-client.ts +++ b/packages/app/src/docker-git/terminal-session-client.ts @@ -1,7 +1,7 @@ import * as ParseResult from "@effect/schema/ParseResult" -import * as Schema from "@effect/schema/Schema" 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" @@ -16,12 +16,6 @@ type TerminalClientMessage = | { readonly type: "resize"; readonly cols: number; readonly rows: number } | { readonly type: "close" } -type TerminalServerMessage = - | { readonly type: "ready"; readonly session: ApiTerminalSession } - | { readonly type: "output"; readonly data: string } - | { readonly type: "exit"; readonly exitCode: number | null; readonly signal: number | null } - | { readonly type: "error"; readonly message: string } - type TerminalAttachment = { readonly header: string readonly session: ApiTerminalSession @@ -37,45 +31,6 @@ type TerminalHandlers = { readonly resizeHandler: () => void } -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 TerminalServerMessageSchema = Schema.parseJson( - 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 - }) - ) -) - const terminalSessionError = (message: string): TerminalSessionClientError => ({ _tag: "TerminalSessionClientError", message diff --git a/packages/app/src/lib/usecases/auto-open-ssh.ts b/packages/app/src/lib/usecases/auto-open-ssh.ts index 77d6174f..eb823d43 100644 --- a/packages/app/src/lib/usecases/auto-open-ssh.ts +++ b/packages/app/src/lib/usecases/auto-open-ssh.ts @@ -1,27 +1 @@ -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 - }) +export { shouldAutoOpenSsh } from "../../shared/auto-open-ssh.js" 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/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/web/api-schema.ts b/packages/app/src/web/api-schema.ts index 5bcdfeba..65654534 100644 --- a/packages/app/src/web/api-schema.ts +++ b/packages/app/src/web/api-schema.ts @@ -1,6 +1,7 @@ import * as Schema from "@effect/schema/Schema" import { JsonValueSchema } from "../shared/json-schema.js" +import { TerminalSessionSchema } from "../shared/terminal-session-schema.js" const NullableString = Schema.NullOr(Schema.String) @@ -122,23 +123,6 @@ export const ProjectAuthSnapshotResponseSchema = Schema.Struct({ snapshot: ProjectAuthSnapshotSchema }) -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) -}) - export const TerminalSessionResponseSchema = Schema.Struct({ ok: Schema.optional(Schema.Boolean), project: ProjectDetailsSchema, @@ -180,7 +164,6 @@ export type ProjectDetails = Schema.Schema.Type export type GithubAuthStatus = Schema.Schema.Type export type AuthSnapshot = Schema.Schema.Type export type ProjectAuthSnapshot = Schema.Schema.Type -export type TerminalSession = Schema.Schema.Type export type ApiEvent = Schema.Schema.Type export type DashboardData = { @@ -218,3 +201,9 @@ export type ProjectAuthFlow = | "ProjectClaudeDisconnect" | "ProjectGeminiConnect" | "ProjectGeminiDisconnect" + +export { + type TerminalServerMessage, + TerminalServerMessageSchema, + type TerminalSession +} from "../shared/terminal-session-schema.js" diff --git a/packages/app/src/web/api.ts b/packages/app/src/web/api.ts index 211576dc..5b9e50b4 100644 --- a/packages/app/src/web/api.ts +++ b/packages/app/src/web/api.ts @@ -1,5 +1,6 @@ import { Effect } from "effect" +import type { AuthMenuRequestBody, ProjectAuthMenuRequestBody } from "../shared/auth-menu-request.js" import { requestJson, requestText, resolveApiBaseUrl } from "./api-http.js" import { AuthSnapshotResponseSchema, @@ -147,15 +148,7 @@ export const loadAuthSnapshot = () => Effect.map((response) => response.snapshot) ) -export const runAuthMenuFlow = ( - request: { - readonly flow: AuthMenuFlow - readonly label?: string | null - readonly token?: string | null - readonly user?: string | null - readonly apiKey?: string | null - } -) => +export const runAuthMenuFlow = (request: AuthMenuRequestBody & { readonly flow: AuthMenuFlow }) => requestJson("POST", "/auth/menu", AuthSnapshotResponseSchema, request).pipe( Effect.map((response) => response.snapshot) ) @@ -171,10 +164,7 @@ export const loadProjectAuthSnapshot = (projectId: string) => export const runProjectAuthFlow = ( projectId: string, - request: { - readonly flow: ProjectAuthFlow - readonly label?: string | null - } + request: ProjectAuthMenuRequestBody & { readonly flow: ProjectAuthFlow } ) => requestJson( "POST", diff --git a/packages/app/src/web/terminal.ts b/packages/app/src/web/terminal.ts index bfeab433..2f9fb8f5 100644 --- a/packages/app/src/web/terminal.ts +++ b/packages/app/src/web/terminal.ts @@ -1,9 +1,10 @@ import * as ParseResult from "@effect/schema/ParseResult" -import * as Schema from "@effect/schema/Schema" 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, TerminalSessionSchema } from "./api-schema.js" +import type { TerminalSession } from "./api-schema.js" export type ActiveTerminalSession = { readonly closePath: string @@ -18,34 +19,6 @@ export type ActiveTerminalSession = { readonly websocketPath: string } -export 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 } - -const TerminalServerMessageSchema = Schema.parseJson( - 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 - }) - ) -) - const resolveTerminalApiBaseUrl = (): string => { const configured = import.meta.env.VITE_DOCKER_GIT_TERMINAL_API_BASE_URL if (configured !== undefined && configured.trim().length > 0) { @@ -82,5 +55,7 @@ export const resolveTerminalWebSocketUrl = (websocketPath: string, cols: number, return apiUrl.toString() } -export const parseTerminalServerMessage = (value: string): TerminalServerMessage | null => +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/tests/docker-git/fixtures/project-item.ts b/packages/app/tests/docker-git/fixtures/project-item.ts index 0f0cf457..fe867a9f 100644 --- a/packages/app/tests/docker-git/fixtures/project-item.ts +++ b/packages/app/tests/docker-git/fixtures/project-item.ts @@ -3,6 +3,7 @@ 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", From 4544598c0e05f273f7c8e8f1f16fa2f9f4c5f9bc Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:23:34 +0000 Subject: [PATCH 24/26] fix(ci): preserve custom SSH keys in API create flow --- .../app/src/docker-git/api-client-create.ts | 82 ++++++++++++++++++ .../app/src/docker-git/api-client-helpers.ts | 21 ++++- packages/app/src/docker-git/api-client.ts | 83 +++++++++---------- 3 files changed, 141 insertions(+), 45 deletions(-) create mode 100644 packages/app/src/docker-git/api-client-create.ts 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..98633189 --- /dev/null +++ b/packages/app/src/docker-git/api-client-create.ts @@ -0,0 +1,82 @@ +import { Effect } from "effect" + +import { request } from "./api-http.js" +import { asObject, type JsonRequest, type JsonValue } from "./api-json.js" +import { decodeProjectDetails } from "./api-project-codec.js" +import type { CreateCommand } from "./frontend-lib/core/domain.js" + +type ResolvedCreateRequestPaths = { + readonly authorizedKeysPath: string + readonly authorizedKeysContents?: string | undefined +} + +const projectPath = (projectId: string, suffix = ""): string => `/projects/${encodeURIComponent(projectId)}${suffix}` + +export const decodeProjectResponse = (payload: JsonValue) => { + const object = asObject(payload) + return object === null + ? decodeProjectDetails(payload) + : decodeProjectDetails(object["project"] ?? payload) +} + +export const createProjectRequestNeedsFollowUpUp = ( + command: CreateCommand, + resolvedPaths: ResolvedCreateRequestPaths +): boolean => command.runUp && resolvedPaths.authorizedKeysContents !== undefined + +export const createProjectRequestAllowsImmediateUp = ( + command: CreateCommand, + resolvedPaths: ResolvedCreateRequestPaths +): boolean => command.runUp && resolvedPaths.authorizedKeysContents === undefined + +export const buildCreateProjectRequest = ( + command: CreateCommand, + resolvedPaths: ResolvedCreateRequestPaths, + shouldRunUpInCreateRequest: boolean +) => { + 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.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: shouldRunUpInCreateRequest, + openSsh: false, + force: command.force, + forceEnv: command.forceEnv, + waitForClone: command.waitForClone + } satisfies JsonRequest +} + +export const upCreatedProjectWithAuthorizedKeys = ( + projectId: string, + authorizedKeysContents: string +) => + request("POST", projectPath(projectId, "/up"), { + authorizedKeysContents, + useManagedAuthorizedKeys: true + }).pipe( + Effect.map((payload) => decodeProjectResponse(payload)) + ) diff --git a/packages/app/src/docker-git/api-client-helpers.ts b/packages/app/src/docker-git/api-client-helpers.ts index 16e56bd5..0b0bd62a 100644 --- a/packages/app/src/docker-git/api-client-helpers.ts +++ b/packages/app/src/docker-git/api-client-helpers.ts @@ -1,3 +1,4 @@ +import * as FileSystem from "@effect/platform/FileSystem" import * as Path from "@effect/platform/Path" import { Effect } from "effect" @@ -34,14 +35,28 @@ const resolveClientCreatePath = ( ? 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: command.config.authorizedKeysPath === defaultTemplateConfig.authorizedKeysPath - ? command.config.authorizedKeysPath - : resolveClientCreatePath(path, cwd, command.config.authorizedKeysPath) + authorizedKeysPath, + authorizedKeysContents } }) diff --git a/packages/app/src/docker-git/api-client.ts b/packages/app/src/docker-git/api-client.ts index ef430642..82ddbdf4 100644 --- a/packages/app/src/docker-git/api-client.ts +++ b/packages/app/src/docker-git/api-client.ts @@ -1,10 +1,17 @@ -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, + createProjectRequestAllowsImmediateUp, + createProjectRequestNeedsFollowUpUp, + decodeProjectResponse, + upCreatedProjectWithAuthorizedKeys +} 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 JsonRequest } from "./api-json.js" +import { asArray, asObject } from "./api-json.js" import { decodeProjectDetails, decodeProjectSummary } from "./api-project-codec.js" import { decodeTerminalSession } from "./api-terminal-codec.js" import type { @@ -85,46 +92,38 @@ export const getProject = (projectId: string) => }) ) +const createProjectWithResolvedPaths = ( + command: CreateCommand, + resolvedPaths: { + readonly authorizedKeysPath: string + readonly authorizedKeysContents?: string | undefined + } +) => + Effect.gen(function*(_) { + const createRequest = buildCreateProjectRequest( + command, + resolvedPaths, + createProjectRequestAllowsImmediateUp(command, resolvedPaths) + ) + const payload = yield* _(request("POST", "/projects", createRequest)) + const createdProject = decodeProjectResponse(payload) + if ( + createdProject === null || + resolvedPaths.authorizedKeysContents === undefined || + !createProjectRequestNeedsFollowUpUp(command, resolvedPaths) + ) { + return createdProject + } + + return yield* _( + upCreatedProjectWithAuthorizedKeys(createdProject.projectDir, resolvedPaths.authorizedKeysContents) + ) + }) + export const createProject = (command: CreateCommand) => Effect.gen(function*(_) { - const config = command.config const resolvedPaths = yield* _(resolveCreateRequestPaths(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, - authorizedKeysPath: resolvedPaths.authorizedKeysPath, - 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 - - const payload = yield* _(request("POST", "/projects", body)) - const object = asObject(payload) - return object === null ? decodeProjectDetails(payload) : decodeProjectDetails(object["project"] ?? payload) + return yield* _(createProjectWithResolvedPaths(command, resolvedPaths)) }) export const deleteProject = (projectId: string) => requestVoid("DELETE", projectPath(projectId)) @@ -286,8 +285,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)) From 92b79358ba1f6f06a7c14e55a4343719775d1e73 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:30:05 +0000 Subject: [PATCH 25/26] fix(ci): use project ids for follow-up up requests --- packages/app/src/docker-git/api-client.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/app/src/docker-git/api-client.ts b/packages/app/src/docker-git/api-client.ts index 82ddbdf4..15eeae25 100644 --- a/packages/app/src/docker-git/api-client.ts +++ b/packages/app/src/docker-git/api-client.ts @@ -86,10 +86,7 @@ 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 = ( @@ -116,7 +113,7 @@ const createProjectWithResolvedPaths = ( } return yield* _( - upCreatedProjectWithAuthorizedKeys(createdProject.projectDir, resolvedPaths.authorizedKeysContents) + upCreatedProjectWithAuthorizedKeys(createdProject.id, resolvedPaths.authorizedKeysContents) ) }) From 8db6918267ab4f62617104706746f6f98afe57f4 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:39:50 +0000 Subject: [PATCH 26/26] fix(ci): inline API create SSH key seeding --- packages/api/src/services/projects.ts | 22 ++++++++- .../app/src/docker-git/api-client-create.ts | 47 +++---------------- packages/app/src/docker-git/api-client.ts | 36 +++++--------- 3 files changed, 38 insertions(+), 67 deletions(-) diff --git a/packages/api/src/services/projects.ts b/packages/api/src/services/projects.ts index de3a4151..6a61ea72 100644 --- a/packages/api/src/services/projects.ts +++ b/packages/api/src/services/projects.ts @@ -1,6 +1,7 @@ import { type AppError, buildCreateCommand, + defaultTemplateConfig, createProject, formatParseError, applyAllDockerGitProjects, @@ -17,6 +18,7 @@ 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" @@ -217,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 @@ -336,7 +352,7 @@ export const createProjectFromRequest = ( ) } - const command = { + const parsedCommand = { ...parsed.right, openSsh: false, waitForClone: request.waitForClone ?? parsed.right.waitForClone @@ -344,10 +360,12 @@ export const createProjectFromRequest = ( const resolvedAuthorizedKeysContents = request.authorizedKeysContents ?? ( request.useManagedAuthorizedKeys === true - ? yield* _(resolveCreateAuthorizedKeysContents(command.outDir, command.config.authorizedKeysPath)) + ? yield* _(resolveCreateAuthorizedKeysContents(parsedCommand.outDir, parsedCommand.config.authorizedKeysPath)) : undefined ) + const command = withManagedAuthorizedKeysForCreate(parsedCommand, resolvedAuthorizedKeysContents) + yield* _(seedAuthorizedKeysForCreate(command.outDir, resolvedAuthorizedKeysContents)) yield* _(ensureGithubAuthForCreate(command.config)) diff --git a/packages/app/src/docker-git/api-client-create.ts b/packages/app/src/docker-git/api-client-create.ts index 98633189..cdb4d466 100644 --- a/packages/app/src/docker-git/api-client-create.ts +++ b/packages/app/src/docker-git/api-client-create.ts @@ -1,38 +1,14 @@ -import { Effect } from "effect" - -import { request } from "./api-http.js" -import { asObject, type JsonRequest, type JsonValue } from "./api-json.js" -import { decodeProjectDetails } from "./api-project-codec.js" -import type { CreateCommand } from "./frontend-lib/core/domain.js" +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 } -const projectPath = (projectId: string, suffix = ""): string => `/projects/${encodeURIComponent(projectId)}${suffix}` - -export const decodeProjectResponse = (payload: JsonValue) => { - const object = asObject(payload) - return object === null - ? decodeProjectDetails(payload) - : decodeProjectDetails(object["project"] ?? payload) -} - -export const createProjectRequestNeedsFollowUpUp = ( - command: CreateCommand, - resolvedPaths: ResolvedCreateRequestPaths -): boolean => command.runUp && resolvedPaths.authorizedKeysContents !== undefined - -export const createProjectRequestAllowsImmediateUp = ( - command: CreateCommand, - resolvedPaths: ResolvedCreateRequestPaths -): boolean => command.runUp && resolvedPaths.authorizedKeysContents === undefined - export const buildCreateProjectRequest = ( command: CreateCommand, - resolvedPaths: ResolvedCreateRequestPaths, - shouldRunUpInCreateRequest: boolean + resolvedPaths: ResolvedCreateRequestPaths ) => { const config = command.config return { @@ -44,7 +20,9 @@ export const buildCreateProjectRequest = ( containerName: config.containerName, serviceName: config.serviceName, volumeName: config.volumeName, - authorizedKeysPath: resolvedPaths.authorizedKeysPath, + authorizedKeysPath: resolvedPaths.authorizedKeysContents === undefined + ? resolvedPaths.authorizedKeysPath + : defaultTemplateConfig.authorizedKeysPath, authorizedKeysContents: resolvedPaths.authorizedKeysContents, envGlobalPath: config.envGlobalPath, envProjectPath: config.envProjectPath, @@ -62,21 +40,10 @@ export const buildCreateProjectRequest = ( codexTokenLabel: config.codexAuthLabel, claudeTokenLabel: config.claudeAuthLabel, agentAutoMode: config.agentAuto ? (config.agentMode ?? "auto") : undefined, - up: shouldRunUpInCreateRequest, + up: command.runUp, openSsh: false, force: command.force, forceEnv: command.forceEnv, waitForClone: command.waitForClone } satisfies JsonRequest } - -export const upCreatedProjectWithAuthorizedKeys = ( - projectId: string, - authorizedKeysContents: string -) => - request("POST", projectPath(projectId, "/up"), { - authorizedKeysContents, - useManagedAuthorizedKeys: true - }).pipe( - Effect.map((payload) => decodeProjectResponse(payload)) - ) diff --git a/packages/app/src/docker-git/api-client.ts b/packages/app/src/docker-git/api-client.ts index 15eeae25..67eb1e63 100644 --- a/packages/app/src/docker-git/api-client.ts +++ b/packages/app/src/docker-git/api-client.ts @@ -2,16 +2,10 @@ import * as FsPlatform from "@effect/platform/FileSystem" import * as PathPlatform from "@effect/platform/Path" import { Effect } from "effect" -import { - buildCreateProjectRequest, - createProjectRequestAllowsImmediateUp, - createProjectRequestNeedsFollowUpUp, - decodeProjectResponse, - upCreatedProjectWithAuthorizedKeys -} from "./api-client-create.js" +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 } from "./api-json.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 { @@ -44,6 +38,13 @@ const projectPath = (projectId: string, suffix = ""): string => `/projects/${enc 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." @@ -97,24 +98,9 @@ const createProjectWithResolvedPaths = ( } ) => Effect.gen(function*(_) { - const createRequest = buildCreateProjectRequest( - command, - resolvedPaths, - createProjectRequestAllowsImmediateUp(command, resolvedPaths) - ) + const createRequest = buildCreateProjectRequest(command, resolvedPaths) const payload = yield* _(request("POST", "/projects", createRequest)) - const createdProject = decodeProjectResponse(payload) - if ( - createdProject === null || - resolvedPaths.authorizedKeysContents === undefined || - !createProjectRequestNeedsFollowUpUp(command, resolvedPaths) - ) { - return createdProject - } - - return yield* _( - upCreatedProjectWithAuthorizedKeys(createdProject.id, resolvedPaths.authorizedKeysContents) - ) + return decodeProjectResponse(payload) }) export const createProject = (command: CreateCommand) =>