From 10011618fc63a89c15599e67977e048d5b859a37 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Sat, 21 Feb 2026 08:39:06 +0000 Subject: [PATCH 1/2] fix(repo): keep only app and lib packages --- packages/docker-git/eslint.config.mjs | 30 - packages/docker-git/package.json | 45 - packages/docker-git/src/core/domain.ts | 1 - packages/docker-git/src/core/templates.ts | 1 - packages/docker-git/src/server/claude.ts | 111 - packages/docker-git/src/server/codex.ts | 630 --- packages/docker-git/src/server/compose.ts | 189 - packages/docker-git/src/server/core/domain.ts | 157 - packages/docker-git/src/server/core/env.ts | 158 - packages/docker-git/src/server/core/ports.ts | 42 - packages/docker-git/src/server/core/schema.ts | 21 - packages/docker-git/src/server/deployments.ts | 192 - packages/docker-git/src/server/docker.ts | 81 - packages/docker-git/src/server/env.ts | 67 - packages/docker-git/src/server/errors.ts | 10 - .../docker-git/src/server/git-credentials.ts | 131 - packages/docker-git/src/server/github.ts | 226 - packages/docker-git/src/server/http.ts | 1735 ------- packages/docker-git/src/server/labeled-env.ts | 121 - packages/docker-git/src/server/main.ts | 16 - packages/docker-git/src/server/ports.ts | 178 - packages/docker-git/src/server/program.ts | 117 - packages/docker-git/src/server/projects.ts | 293 -- packages/docker-git/src/server/terminal.ts | 414 -- packages/docker-git/src/server/view.ts | 895 ---- packages/docker-git/src/shell/config.ts | 1 - packages/docker-git/src/shell/docker.ts | 1 - packages/docker-git/src/shell/errors.ts | 1 - packages/docker-git/src/shell/files.ts | 1 - packages/docker-git/tests/core/domain.test.ts | 68 - .../docker-git/tests/core/templates.test.ts | 229 - .../docker-git/tests/server/claude.test.ts | 56 - .../docker-git/tests/server/domain.test.ts | 69 - .../tests/server/git-credentials.test.ts | 63 - .../docker-git/tests/server/ports.test.ts | 24 - packages/docker-git/tsconfig.json | 10 - packages/docker-git/vitest.config.ts | 9 - packages/docker-git/web/deploy-logs.js | 111 - packages/docker-git/web/deploy.js | 134 - packages/docker-git/web/index.html | 23 - packages/docker-git/web/styles.css | 381 -- packages/docker-git/web/terminal.js | 97 - packages/web/.gitignore | 41 - packages/web/README.md | 36 - packages/web/eslint.config.mjs | 18 - packages/web/next.config.mjs | 20 - packages/web/package.json | 36 - packages/web/pnpm-lock.yaml | 4008 ----------------- packages/web/pnpm-workspace.yaml | 3 - packages/web/postcss.config.mjs | 7 - packages/web/public/file.svg | 1 - packages/web/public/globe.svg | 1 - packages/web/public/next.svg | 1 - packages/web/public/vercel.svg | 1 - packages/web/public/window.svg | 1 - packages/web/scripts/terminal-ws.mjs | 583 --- .../api/projects/[projectId]/down/route.ts | 22 - .../api/projects/[projectId]/exec/route.ts | 47 - .../projects/[projectId]/processes/route.ts | 139 - .../projects/[projectId]/recreate/route.ts | 22 - .../[projectId]/recreate/status/route.ts | 52 - .../src/app/api/projects/[projectId]/route.ts | 22 - .../app/api/projects/[projectId]/up/route.ts | 22 - packages/web/src/app/api/projects/route.ts | 13 - .../web/src/app/api/terminal-config/route.ts | 17 - .../src/app/api/terminal-sessions/route.ts | 17 - packages/web/src/app/favicon.ico | Bin 25931 -> 0 bytes packages/web/src/app/globals.css | 705 --- packages/web/src/app/layout.tsx | 34 - packages/web/src/app/page.tsx | 884 ---- packages/web/src/lib/api-schema.ts | 133 - packages/web/src/lib/api-types.ts | 104 - packages/web/src/server/dev-server.ts | 39 - packages/web/src/server/docker-git.ts | 351 -- packages/web/src/server/projects-root.ts | 27 - packages/web/src/server/recreate-state.ts | 68 - packages/web/src/server/runtime.ts | 15 - packages/web/src/server/terminal-ws.ts | 268 -- .../tests/api/terminal-sessions-route.test.ts | 69 - .../lib/api-schema-terminal-sessions.test.ts | 58 - packages/web/tsconfig.json | 34 - pnpm-lock.yaml | 1843 +------- pnpm-workspace.yaml | 3 +- 83 files changed, 55 insertions(+), 16849 deletions(-) delete mode 100644 packages/docker-git/eslint.config.mjs delete mode 100644 packages/docker-git/package.json delete mode 100644 packages/docker-git/src/core/domain.ts delete mode 100644 packages/docker-git/src/core/templates.ts delete mode 100644 packages/docker-git/src/server/claude.ts delete mode 100644 packages/docker-git/src/server/codex.ts delete mode 100644 packages/docker-git/src/server/compose.ts delete mode 100644 packages/docker-git/src/server/core/domain.ts delete mode 100644 packages/docker-git/src/server/core/env.ts delete mode 100644 packages/docker-git/src/server/core/ports.ts delete mode 100644 packages/docker-git/src/server/core/schema.ts delete mode 100644 packages/docker-git/src/server/deployments.ts delete mode 100644 packages/docker-git/src/server/docker.ts delete mode 100644 packages/docker-git/src/server/env.ts delete mode 100644 packages/docker-git/src/server/errors.ts delete mode 100644 packages/docker-git/src/server/git-credentials.ts delete mode 100644 packages/docker-git/src/server/github.ts delete mode 100644 packages/docker-git/src/server/http.ts delete mode 100644 packages/docker-git/src/server/labeled-env.ts delete mode 100644 packages/docker-git/src/server/main.ts delete mode 100644 packages/docker-git/src/server/ports.ts delete mode 100644 packages/docker-git/src/server/program.ts delete mode 100644 packages/docker-git/src/server/projects.ts delete mode 100644 packages/docker-git/src/server/terminal.ts delete mode 100644 packages/docker-git/src/server/view.ts delete mode 100644 packages/docker-git/src/shell/config.ts delete mode 100644 packages/docker-git/src/shell/docker.ts delete mode 100644 packages/docker-git/src/shell/errors.ts delete mode 100644 packages/docker-git/src/shell/files.ts delete mode 100644 packages/docker-git/tests/core/domain.test.ts delete mode 100644 packages/docker-git/tests/core/templates.test.ts delete mode 100644 packages/docker-git/tests/server/claude.test.ts delete mode 100644 packages/docker-git/tests/server/domain.test.ts delete mode 100644 packages/docker-git/tests/server/git-credentials.test.ts delete mode 100644 packages/docker-git/tests/server/ports.test.ts delete mode 100644 packages/docker-git/tsconfig.json delete mode 100644 packages/docker-git/vitest.config.ts delete mode 100644 packages/docker-git/web/deploy-logs.js delete mode 100644 packages/docker-git/web/deploy.js delete mode 100644 packages/docker-git/web/index.html delete mode 100644 packages/docker-git/web/styles.css delete mode 100644 packages/docker-git/web/terminal.js delete mode 100644 packages/web/.gitignore delete mode 100644 packages/web/README.md delete mode 100644 packages/web/eslint.config.mjs delete mode 100644 packages/web/next.config.mjs delete mode 100644 packages/web/package.json delete mode 100644 packages/web/pnpm-lock.yaml delete mode 100644 packages/web/pnpm-workspace.yaml delete mode 100644 packages/web/postcss.config.mjs delete mode 100644 packages/web/public/file.svg delete mode 100644 packages/web/public/globe.svg delete mode 100644 packages/web/public/next.svg delete mode 100644 packages/web/public/vercel.svg delete mode 100644 packages/web/public/window.svg delete mode 100644 packages/web/scripts/terminal-ws.mjs delete mode 100644 packages/web/src/app/api/projects/[projectId]/down/route.ts delete mode 100644 packages/web/src/app/api/projects/[projectId]/exec/route.ts delete mode 100644 packages/web/src/app/api/projects/[projectId]/processes/route.ts delete mode 100644 packages/web/src/app/api/projects/[projectId]/recreate/route.ts delete mode 100644 packages/web/src/app/api/projects/[projectId]/recreate/status/route.ts delete mode 100644 packages/web/src/app/api/projects/[projectId]/route.ts delete mode 100644 packages/web/src/app/api/projects/[projectId]/up/route.ts delete mode 100644 packages/web/src/app/api/projects/route.ts delete mode 100644 packages/web/src/app/api/terminal-config/route.ts delete mode 100644 packages/web/src/app/api/terminal-sessions/route.ts delete mode 100644 packages/web/src/app/favicon.ico delete mode 100644 packages/web/src/app/globals.css delete mode 100644 packages/web/src/app/layout.tsx delete mode 100644 packages/web/src/app/page.tsx delete mode 100644 packages/web/src/lib/api-schema.ts delete mode 100644 packages/web/src/lib/api-types.ts delete mode 100644 packages/web/src/server/dev-server.ts delete mode 100644 packages/web/src/server/docker-git.ts delete mode 100644 packages/web/src/server/projects-root.ts delete mode 100644 packages/web/src/server/recreate-state.ts delete mode 100644 packages/web/src/server/runtime.ts delete mode 100644 packages/web/src/server/terminal-ws.ts delete mode 100644 packages/web/tests/api/terminal-sessions-route.test.ts delete mode 100644 packages/web/tests/lib/api-schema-terminal-sessions.test.ts delete mode 100644 packages/web/tsconfig.json diff --git a/packages/docker-git/eslint.config.mjs b/packages/docker-git/eslint.config.mjs deleted file mode 100644 index 29781123..00000000 --- a/packages/docker-git/eslint.config.mjs +++ /dev/null @@ -1,30 +0,0 @@ -import js from "@eslint/js" -import globals from "globals" -import tsPlugin from "@typescript-eslint/eslint-plugin" -import tsParser from "@typescript-eslint/parser" - -export default [ - { - // Generated output + browser bundles are not part of the source lint surface. - ignores: ["dist/**", "web/**", ".docker-git/**"] - }, - js.configs.recommended, - { - files: ["**/*.ts"], - languageOptions: { - parser: tsParser, - parserOptions: { - sourceType: "module" - }, - globals: { - ...globals.node - } - }, - plugins: { - "@typescript-eslint": tsPlugin - }, - rules: { - ...tsPlugin.configs.recommended.rules - } - } -] diff --git a/packages/docker-git/package.json b/packages/docker-git/package.json deleted file mode 100644 index de191d90..00000000 --- a/packages/docker-git/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "@effect-template/docker-git", - "version": "0.1.0", - "private": true, - "description": "docker-git orchestrator (CLI + server)", - "main": "dist/src/server/main.js", - "type": "module", - "scripts": { - "prebuild": "pnpm -C ../lib build", - "build": "tsc -p tsconfig.json", - "dev": "tsc -p tsconfig.json --watch", - "prestart": "pnpm run build", - "start": "node dist/src/server/main.js", - "prestart:server": "pnpm run build", - "start:server": "node dist/src/server/main.js", - "pretypecheck": "pnpm -C ../lib build", - "typecheck": "tsc --noEmit -p tsconfig.json", - "lint": "eslint .", - "pretest": "pnpm -C ../lib build", - "test": "vitest run" - }, - "dependencies": { - "@effect-template/lib": "workspace:*", - "@effect/platform": "^0.94.1", - "@effect/platform-node": "^0.104.0", - "@effect/schema": "^0.75.5", - "effect": "^3.19.14", - "node-pty": "^1.1.0", - "ws": "^8.18.1", - "xterm": "^5.3.0" - }, - "devDependencies": { - "@biomejs/biome": "^2.3.8", - "@effect/vitest": "^0.27.0", - "@eslint/js": "9.39.1", - "@types/node": "^24.10.1", - "@types/ws": "^8.18.0", - "@typescript-eslint/eslint-plugin": "^8.48.1", - "@typescript-eslint/parser": "^8.48.1", - "eslint": "^9.39.1", - "globals": "^16.5.0", - "typescript": "^5.9.3", - "vitest": "^3.2.4" - } -} diff --git a/packages/docker-git/src/core/domain.ts b/packages/docker-git/src/core/domain.ts deleted file mode 100644 index 0e8c89ae..00000000 --- a/packages/docker-git/src/core/domain.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@effect-template/lib/core/domain" diff --git a/packages/docker-git/src/core/templates.ts b/packages/docker-git/src/core/templates.ts deleted file mode 100644 index 7e42c0dd..00000000 --- a/packages/docker-git/src/core/templates.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@effect-template/lib/core/templates" diff --git a/packages/docker-git/src/server/claude.ts b/packages/docker-git/src/server/claude.ts deleted file mode 100644 index 9dd335b2..00000000 --- a/packages/docker-git/src/server/claude.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { findEnvValue } from "./core/env.js" -import { - findLabeledEnvEntryByLabel, - listLabeledEnvEntries, - resolveLabeledEnvLabelForValue -} from "./labeled-env.js" - -export interface ClaudeApiKeyEntry { - readonly label: string - readonly apiKey: string -} - -const claudeApiKeyBase = "ANTHROPIC_API_KEY" -const claudeLabelKey = "CLAUDE_AUTH_LABEL" - -// CHANGE: list labeled Claude API keys from env text -// WHY: support multiple Claude Code profiles in integrations -// QUOTE(ТЗ): "N множества ключей ... Claude Code" -// REF: issue-61 -// SOURCE: n/a -// FORMAT THEOREM: forall env: list(env) -> keys(env) -// PURITY: CORE -// EFFECT: Effect, never, never> -// INVARIANT: only non-empty keys are returned -// COMPLEXITY: O(n) where n = |entries| -export const listClaudeApiKeys = (envText: string): ReadonlyArray => - listLabeledEnvEntries(envText, claudeApiKeyBase).map((entry) => ({ - label: entry.label, - apiKey: entry.value - })) - -// CHANGE: find Claude API key by label -// WHY: map selected profile label to stored API key -// QUOTE(ТЗ): "возможность выбора" -// REF: issue-61 -// SOURCE: n/a -// FORMAT THEOREM: forall env,l: find(env,l) -> key(l) | null -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: label normalization is deterministic -// COMPLEXITY: O(n) where n = |entries| -export const findClaudeApiKeyByLabel = ( - envText: string, - label: string -): ClaudeApiKeyEntry | null => { - const entries = listLabeledEnvEntries(envText, claudeApiKeyBase) - const matched = findLabeledEnvEntryByLabel(entries, claudeApiKeyBase, label) - if (matched === null) { - return null - } - return { - label: matched.label, - apiKey: matched.value - } -} - -// CHANGE: resolve active Claude API key from project env -// WHY: show whether project is attached to a Claude profile -// QUOTE(ТЗ): "возможность выбора" -// REF: issue-61 -// SOURCE: n/a -// FORMAT THEOREM: forall env: key(env) -> string | null -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: empty values map to null -// COMPLEXITY: O(n) where n = |entries| -export const resolveProjectClaudeApiKey = (envText: string): string | null => - findEnvValue(envText, claudeApiKeyBase) - -// CHANGE: resolve active Claude label override from project env -// WHY: preserve explicit label selection across equivalent keys -// QUOTE(ТЗ): "возможность выбора" -// REF: issue-61 -// SOURCE: n/a -// FORMAT THEOREM: forall env: label(env) -> string | null -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: empty values map to null -// COMPLEXITY: O(n) where n = |entries| -export const resolveProjectClaudeLabel = (envText: string): string | null => - findEnvValue(envText, claudeLabelKey) - -// CHANGE: resolve Claude label for a raw API key value -// WHY: infer current profile when explicit project label is missing -// QUOTE(ТЗ): "возможность выбора" -// REF: issue-61 -// SOURCE: n/a -// FORMAT THEOREM: forall env,v: label(env,v) -> string | null -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: exact value match -// COMPLEXITY: O(n) where n = |entries| -export const resolveClaudeLabelForApiKey = ( - envText: string, - apiKey: string -): string | null => { - const entries = listLabeledEnvEntries(envText, claudeApiKeyBase) - return resolveLabeledEnvLabelForValue(entries, apiKey) -} - -// CHANGE: expose env key used to persist project Claude label -// WHY: keep handlers aligned on a single key -// QUOTE(ТЗ): "реализовать систему выбора" -// REF: issue-61 -// SOURCE: n/a -// FORMAT THEOREM: forall _: key = constant -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: constant key -// COMPLEXITY: O(1) -export const projectClaudeLabelKey = claudeLabelKey diff --git a/packages/docker-git/src/server/codex.ts b/packages/docker-git/src/server/codex.ts deleted file mode 100644 index f2db0427..00000000 --- a/packages/docker-git/src/server/codex.ts +++ /dev/null @@ -1,630 +0,0 @@ -import { Effect } from "effect" -import * as Data from "effect/Data" -import type { PlatformError } from "@effect/platform/Error" -import * as FileSystem from "@effect/platform/FileSystem" -import * as Path from "@effect/platform/Path" - -import { parseEnvEntries } from "./core/env.js" -import { resolveCodexAuthPath } from "./core/domain.js" - -export interface CodexAuthStatus { - readonly path: string - readonly connected: boolean - readonly entries: number -} - -export interface CodexAccountEntry { - readonly label: string - readonly path: string - readonly connected: boolean - readonly entries: number - readonly legacy: boolean -} - -export class CodexAuthError extends Data.TaggedError("CodexAuthError")<{ - readonly message: string -}> {} - -const normalizeInput = (input: string | undefined): string => input?.trim() ?? "" - -const expandHome = (input: string, home: string | undefined): string => { - if (input === "~") { - return home ?? input - } - if (input.startsWith("~/")) { - return home ? `${home}/${input.slice(2)}` : input - } - return input -} - -const normalizeLabel = (value: string): string => { - const trimmed = value.trim() - if (trimmed.length === 0) { - return "" - } - const normalized = trimmed - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+/, "") - .replace(/-+$/, "") - return normalized.length > 0 ? normalized : "" -} - -const normalizeCodexLabel = (value: string | undefined): string => { - const normalized = normalizeLabel(value ?? "") - return normalized.length === 0 ? "default" : normalized -} - -const authMarker = "auth.json" -const codexConfigYamlName = "config.yml" -const authFiles: ReadonlyArray = [ - "auth.json", - "internal_storage.json", - "version.json", - codexConfigYamlName -] -const codexConfigKey = "cli_auth_credentials_store" -const codexConfigLine = `${codexConfigKey} = "file"` -const codexApprovalPolicyKey = "approval_policy" -const codexApprovalPolicyLine = `${codexApprovalPolicyKey} = "never"` -const codexSandboxModeKey = "sandbox_mode" -const codexSandboxModeLine = `${codexSandboxModeKey} = "danger-full-access"` -const codexConfigTomlContents = `model = "gpt-5.3-codex" -model_reasoning_effort = "xhigh" -personality = "pragmatic" - -${codexApprovalPolicyLine} -${codexSandboxModeLine} -web_search = "live" -${codexConfigLine} - -[features] -shell_snapshot = true -multi_agent = true -apps = true - -[projects."/home/dev"] -trust_level = "trusted" - -[projects."/home/dev/app"] -trust_level = "trusted" - -[projects."/home/dev/.codex"] -trust_level = "trusted" -` -const codexConfigYamlContents = `model = "gpt-5.3-codex" -model_reasoning_effort = "xhigh" -personality = "pragmatic" -approval_policy = "never" -sandbox_mode = "danger-full-access" -web_search = "live" - -[features] -shell_snapshot = true -multi_agent = true -apps = true -` - -const trimTrailingSlash = (value: string): string => value.replace(/\/+$/, "") - -// CHANGE: derive per-project Codex auth path under a root -// WHY: avoid shared root collisions between multiple projects -// QUOTE(ТЗ): "Почему он не видит авторизацию тогда?" -// REF: user-request-2026-01-14 -// SOURCE: n/a -// FORMAT THEOREM: forall r,p: path(r,p) = r + "/" + p -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: no trailing slash in root -// COMPLEXITY: O(1) -export const resolveProjectCodexAuthPath = (root: string, projectId: string): string => - `${trimTrailingSlash(root)}/${projectId}` - -const hasAuthMarker = ( - fs: FileSystem.FileSystem, - path: Path.Path, - dirPath: string -): Effect.Effect => - fs.exists(path.join(dirPath, authMarker)) - -// CHANGE: resolve Codex auth source path from input and environment -// WHY: provide a deterministic import path for Codex credentials -// QUOTE(ТЗ): "Добавь подключение Codex в интеграции" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall s: resolve(s) -> path | null -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: empty input falls back to CODEX_HOME or HOME/.codex -// COMPLEXITY: O(1) -export const resolveCodexSourcePath = ( - input: string | undefined, - home: string | undefined, - codexHome: string | undefined -): string | null => { - const trimmed = normalizeInput(input) - if (trimmed.length > 0) { - return expandHome(trimmed, home) - } - - const fallback = normalizeInput(codexHome) - if (fallback.length > 0) { - return expandHome(fallback, home) - } - - return home && home.length > 0 ? `${home}/.codex` : null -} - -// CHANGE: read Codex auth status from a directory -// WHY: show whether Codex credentials are available in integrations -// QUOTE(ТЗ): "Добавь подключение Codex в интеграции" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall p: status(p) -> connected(p) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: connected iff directory exists and has entries -// COMPLEXITY: O(n) where n = |entries| -export const readCodexAuthStatus = ( - authPath: string -): Effect.Effect => - Effect.gen(function* (_) { - const fs = yield* _(FileSystem.FileSystem) - const exists = yield* _(fs.exists(authPath)) - if (!exists) { - return { path: authPath, connected: false, entries: 0 } - } - - const info = yield* _(fs.stat(authPath)) - if (info.type !== "Directory") { - return { path: authPath, connected: false, entries: 0 } - } - - const entries = yield* _(fs.readDirectory(authPath)) - return { - path: authPath, - connected: entries.length > 0, - entries: entries.length - } - }) - -// CHANGE: list Codex auth accounts in the shared secrets root -// WHY: allow multiple Codex integrations to coexist -// QUOTE(ТЗ): "Добавь подключение Codex в интеграции" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall r: list(r) = accounts(r) -// PURITY: SHELL -// EFFECT: Effect, PlatformError, FileSystem | Path> -// INVARIANT: legacy root files map to label "default" -// COMPLEXITY: O(n) where n = |entries| -export const listCodexAccounts = ( - rootPath: string -): Effect.Effect, PlatformError, FileSystem.FileSystem | Path.Path> => - Effect.gen(function* (_) { - const fs = yield* _(FileSystem.FileSystem) - const path = yield* _(Path.Path) - const exists = yield* _(fs.exists(rootPath)) - if (!exists) { - return [] - } - - const info = yield* _(fs.stat(rootPath)) - if (info.type !== "Directory") { - return [] - } - - const entries = yield* _(fs.readDirectory(rootPath)) - let hasDefaultDir = false - const legacyAuth = yield* _(hasAuthMarker(fs, path, rootPath)) - const accounts: Array = [] - - for (const entry of entries) { - const entryPath = path.join(rootPath, entry) - const entryInfo = yield* _(fs.stat(entryPath)) - if (entryInfo.type === "Directory") { - if (entry === "default") { - hasDefaultDir = true - } - const isAccount = yield* _(hasAuthMarker(fs, path, entryPath)) - if (isAccount) { - const status = yield* _(readCodexAuthStatus(entryPath)) - accounts.push({ - label: entry, - path: entryPath, - connected: status.connected, - entries: status.entries, - legacy: false - }) - } - } - } - - if (legacyAuth && !hasDefaultDir) { - const status = yield* _(readCodexAuthStatus(rootPath)) - accounts.unshift({ - label: "default", - path: rootPath, - connected: status.connected, - entries: status.entries, - legacy: true - }) - } - - return accounts - }) - -const resolveAccountPath = (rootPath: string, label: string): string => { - const normalized = normalizeCodexLabel(label) - return `${rootPath}/${normalized}` -} - -const ensureCodexConfig = ( - fs: FileSystem.FileSystem, - path: Path.Path, - accountPath: string -): Effect.Effect => - Effect.gen(function* (_) { - const configPath = path.join(accountPath, "config.toml") - // Always overwrite to avoid copying host project trust entries. - yield* _(fs.writeFileString(configPath, codexConfigTomlContents)) - }) - -// CHANGE: ensure Codex config.yml exists with required settings -// WHY: propagate model settings into the mounted Codex auth directory -// QUOTE(ТЗ): "Добавь конфиг config.yml" -// REF: user-request-2026-01-15 -// SOURCE: n/a -// FORMAT THEOREM: forall p: ensure(p) -> exists(config.yml, p) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: file is overwritten with requested contents -// COMPLEXITY: O(1) -const ensureCodexConfigYaml = ( - fs: FileSystem.FileSystem, - path: Path.Path, - accountPath: string -): Effect.Effect => - Effect.gen(function* (_) { - const configPath = path.join(accountPath, codexConfigYamlName) - yield* _(fs.writeFileString(configPath, codexConfigYamlContents)) - }) - -const ensureWritableDirectory = ( - fs: FileSystem.FileSystem, - path: Path.Path, - dirPath: string -): Effect.Effect => - Effect.gen(function* (_) { - const ensureDir = yield* _(Effect.either(fs.makeDirectory(dirPath, { recursive: true }))) - if (ensureDir._tag === "Left") { - return false - } - const probePath = path.join(dirPath, ".dg-write-test") - const writeResult = yield* _(Effect.either(fs.writeFileString(probePath, "ok"))) - if (writeResult._tag === "Left") { - return false - } - yield* _(fs.remove(probePath, { force: true })) - return true - }) - -// CHANGE: resolve a writable Codex auth root directory -// WHY: allow Codex auth to be persisted by the host orchestrator -// QUOTE(ТЗ): "Я не могу нормально пока прокинуть авторизацию Codex" -// REF: user-request-2026-01-14 -// SOURCE: n/a -// FORMAT THEOREM: forall root: writable(root) -> select(root) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: returns a directory writable by the orchestrator -// COMPLEXITY: O(1) -export const resolveWritableCodexRoot = ( - projectsRoot: string -): Effect.Effect => - Effect.gen(function* (_) { - const fs = yield* _(FileSystem.FileSystem) - const path = yield* _(Path.Path) - const baseRoot = path.resolve(resolveCodexAuthPath(projectsRoot)) - const baseWritable = yield* _(ensureWritableDirectory(fs, path, baseRoot)) - if (baseWritable) { - return baseRoot - } - - const fallbackRoot = `${baseRoot}-host` - const fallbackWritable = yield* _(ensureWritableDirectory(fs, path, fallbackRoot)) - if (fallbackWritable) { - return fallbackRoot - } - - const envRoot = resolveCodexSourcePath( - undefined, - process.env["HOME"], - process.env["CODEX_HOME"] - ) - if (envRoot) { - const resolvedEnvRoot = path.resolve(envRoot) - const envWritable = yield* _(ensureWritableDirectory(fs, path, resolvedEnvRoot)) - if (envWritable) { - return resolvedEnvRoot - } - } - - return yield* _( - Effect.fail( - new CodexAuthError({ - message: `Codex auth root is not writable: ${baseRoot}` - }) - ) - ) - }) - -// CHANGE: prepare a Codex CLI auth directory for a labeled account -// WHY: ensure device auth writes credentials into a dedicated CODEX_HOME -// QUOTE(ТЗ): "Мне нужна прямо нативная интеграция с Codex" -// REF: user-request-2026-01-10 -// SOURCE: n/a -// FORMAT THEOREM: forall r,l: prepare(r,l) -> exists(dir(r,l)) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: config.toml contains cli_auth_credentials_store = "file" -// COMPLEXITY: O(n) where n = |config| -export const prepareCodexAccountDir = ( - rootPath: string, - label: string -): Effect.Effect => - Effect.gen(function* (_) { - const fs = yield* _(FileSystem.FileSystem) - const path = yield* _(Path.Path) - const accountPath = resolveAccountPath(rootPath, label) - yield* _(fs.makeDirectory(accountPath, { recursive: true })) - yield* _(ensureCodexConfig(fs, path, accountPath)) - yield* _(ensureCodexConfigYaml(fs, path, accountPath)) - return accountPath - }) - -// CHANGE: import Codex auth cache into a labeled integration directory -// WHY: allow multiple Codex integrations in the orchestrator -// QUOTE(ТЗ): "Добавь подключение Codex в интеграции" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall s,l: import(s,l) -> exists(account(l)) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: target directory is replaced -// COMPLEXITY: O(n) where n = |files| -export const importCodexAuthDir = ( - sourcePath: string, - rootPath: string, - label: string -): Effect.Effect => - Effect.gen(function* (_) { - const fs = yield* _(FileSystem.FileSystem) - const path = yield* _(Path.Path) - const exists = yield* _(fs.exists(sourcePath)) - if (!exists) { - yield* _( - Effect.fail( - new CodexAuthError({ message: `Source not found: ${sourcePath}` }) - ) - ) - return - } - - const info = yield* _(fs.stat(sourcePath)) - if (info.type !== "Directory") { - yield* _( - Effect.fail( - new CodexAuthError({ message: `Source is not a directory: ${sourcePath}` }) - ) - ) - return - } - - const target = resolveAccountPath(rootPath, label) - yield* _(fs.makeDirectory(path.dirname(target), { recursive: true })) - yield* _(fs.remove(target, { recursive: true, force: true })) - yield* _(fs.makeDirectory(target, { recursive: true })) - for (const file of authFiles) { - const src = path.join(sourcePath, file) - const dst = path.join(target, file) - const exists = yield* _(fs.exists(src)) - if (exists) { - yield* _(fs.copyFile(src, dst)) - } - } - yield* _(ensureCodexConfig(fs, path, target)) - yield* _(ensureCodexConfigYaml(fs, path, target)) - }) - -// CHANGE: copy Codex auth cache to a destination directory -// WHY: attach a selected Codex integration to a project -// QUOTE(ТЗ): "хочу подключить 10 Codex интеграций" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall s,d: copy(s,d) -> exists(d) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: destination directory is replaced -// COMPLEXITY: O(n) where n = |files| -export const copyCodexAuthDir = ( - sourcePath: string, - destPath: string -): Effect.Effect => - Effect.gen(function* (_) { - const fs = yield* _(FileSystem.FileSystem) - const path = yield* _(Path.Path) - const exists = yield* _(fs.exists(sourcePath)) - if (!exists) { - yield* _( - Effect.fail( - new CodexAuthError({ message: `Source not found: ${sourcePath}` }) - ) - ) - return - } - - const info = yield* _(fs.stat(sourcePath)) - if (info.type !== "Directory") { - yield* _( - Effect.fail( - new CodexAuthError({ message: `Source is not a directory: ${sourcePath}` }) - ) - ) - return - } - - yield* _(fs.remove(destPath, { recursive: true, force: true })) - yield* _(fs.makeDirectory(path.dirname(destPath), { recursive: true })) - yield* _(fs.makeDirectory(destPath, { recursive: true })) - for (const file of authFiles) { - const src = path.join(sourcePath, file) - const dst = path.join(destPath, file) - const exists = yield* _(fs.exists(src)) - if (exists) { - yield* _(fs.copyFile(src, dst)) - } - } - yield* _(ensureCodexConfig(fs, path, destPath)) - yield* _(ensureCodexConfigYaml(fs, path, destPath)) - }) - -// CHANGE: remove a Codex integration by label -// WHY: allow deleting an individual Codex account -// QUOTE(ТЗ): "хочу подключить 10 Codex интеграций" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall r,l: remove(r,l) -> not exists(account(l)) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: only the labeled account is removed -// COMPLEXITY: O(n) where n = |entries| -export const removeCodexAccount = ( - rootPath: string, - label: string -): Effect.Effect => - Effect.gen(function* (_) { - const fs = yield* _(FileSystem.FileSystem) - const path = yield* _(Path.Path) - const normalized = normalizeCodexLabel(label) - const target = resolveAccountPath(rootPath, normalized) - const targetExists = yield* _(fs.exists(target)) - if (targetExists) { - yield* _(fs.remove(target, { recursive: true, force: true })) - return - } - - if (normalized !== "default") { - return - } - - const rootExists = yield* _(fs.exists(rootPath)) - if (!rootExists) { - return - } - - const rootInfo = yield* _(fs.stat(rootPath)) - if (rootInfo.type !== "Directory") { - return - } - - const hasLegacy = yield* _(hasAuthMarker(fs, path, rootPath)) - if (!hasLegacy) { - return - } - - const entries = yield* _(fs.readDirectory(rootPath)) - for (const entry of entries) { - const entryPath = path.join(rootPath, entry) - const entryInfo = yield* _(fs.stat(entryPath)) - if (entryInfo.type === "Directory") { - const isAccount = yield* _(hasAuthMarker(fs, path, entryPath)) - if (!isAccount) { - yield* _(fs.remove(entryPath, { recursive: true, force: true })) - } - } else { - yield* _(fs.remove(entryPath, { recursive: true, force: true })) - } - } - }) - -// CHANGE: clear Codex auth cache for a path -// WHY: allow disconnecting Codex from a project -// QUOTE(ТЗ): "хочу подключить 10 Codex интеграций" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall p: clear(p) -> empty(p) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: directory exists after clear -// COMPLEXITY: O(n) where n = |files| -export const clearCodexAuthDir = ( - destPath: string -): Effect.Effect => - Effect.gen(function* (_) { - const fs = yield* _(FileSystem.FileSystem) - yield* _(fs.remove(destPath, { recursive: true, force: true })) - yield* _(fs.makeDirectory(destPath, { recursive: true })) - }) - -// CHANGE: locate a Codex integration path by label -// WHY: map project selection to a concrete auth directory -// QUOTE(ТЗ): "хочу подключить 10 Codex интеграций" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall r,l: find(r,l) -> path | null -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: legacy default is resolved to root when present -// COMPLEXITY: O(n) where n = |entries| -export const findCodexAccountPath = ( - rootPath: string, - label: string -): Effect.Effect => - Effect.gen(function* (_) { - const fs = yield* _(FileSystem.FileSystem) - const path = yield* _(Path.Path) - const normalized = normalizeCodexLabel(label) - const target = resolveAccountPath(rootPath, normalized) - const targetExists = yield* _(fs.exists(target)) - if (targetExists) { - const isAccount = yield* _(hasAuthMarker(fs, path, target)) - return isAccount ? target : null - } - - if (normalized !== "default") { - return null - } - - const rootExists = yield* _(fs.exists(rootPath)) - if (!rootExists) { - return null - } - - const info = yield* _(fs.stat(rootPath)) - if (info.type !== "Directory") { - return null - } - - const legacy = yield* _(hasAuthMarker(fs, path, rootPath)) - return legacy ? rootPath : null - }) - -// CHANGE: resolve Codex label stored in a project env file -// WHY: show which Codex account is active for a project -// QUOTE(ТЗ): "хочу подключить 10 Codex интеграций" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall env: label(env) -> string | null -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: empty values map to null -// COMPLEXITY: O(n) where n = |lines| -export const resolveProjectCodexLabel = (envText: string): string | null => { - const entry = parseEnvEntries(envText).find((item) => item.key === "CODEX_AUTH_LABEL") - if (!entry) { - return null - } - const trimmed = entry.value.trim() - return trimmed.length > 0 ? trimmed : null -} diff --git a/packages/docker-git/src/server/compose.ts b/packages/docker-git/src/server/compose.ts deleted file mode 100644 index 03ec43af..00000000 --- a/packages/docker-git/src/server/compose.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { Effect, pipe, Duration } from "effect" -import * as Stream from "effect/Stream" -import type { PlatformError } from "@effect/platform/Error" -import * as Command from "@effect/platform/Command" -import * as CommandExecutor from "@effect/platform/CommandExecutor" - -import { DockerCommandError } from "../shell/errors.js" -import { appendDeploymentLog, setDeploymentStatus, type DeploymentPhase } from "./deployments.js" - -const decoder = new TextDecoder("utf-8") - -const decodeChunk = (chunk: Uint8Array): string => decoder.decode(chunk) - -interface BuildStepInfo { - readonly step: number - readonly total: number - readonly command: string -} - -const heartbeatMs = 5000 - -const collectLines = ( - buffer: string, - nextChunk: string -): { readonly lines: ReadonlyArray; readonly rest: string } => { - const combined = buffer + nextChunk - const parts = combined.split(/\r\n|\n|\r/) - if (parts.length === 0) { - return { lines: [], rest: "" } - } - const rest = parts.pop() ?? "" - return { lines: parts, rest } -} - -// CHANGE: parse build step info from docker output -// WHY: surface deterministic stage metadata for long-running builds -// QUOTE(ТЗ): "не понимаю на каком этапе" -// REF: user-request-2026-01-15 -// SOURCE: n/a -// FORMAT THEOREM: forall line: parse(line) -> stepInfo | null -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: returns null when no step is detected -// COMPLEXITY: O(1) -const parseBuildStep = (line: string): BuildStepInfo | null => { - const stepMatch = line.match(/Step\s+(\d+)\/(\d+)\s*:\s*(.*)$/) - if (stepMatch) { - const step = Number(stepMatch[1]) - const total = Number(stepMatch[2]) - const command = stepMatch[3]?.trim() ?? "" - if (Number.isFinite(step) && Number.isFinite(total)) { - return { step, total, command } - } - } - - const buildkitMatch = line.match(/\[(\d+)\/(\d+)\]\s+RUN\s+(.*)$/) - if (buildkitMatch) { - const step = Number(buildkitMatch[1]) - const total = Number(buildkitMatch[2]) - const command = buildkitMatch[3]?.trim() ?? "" - if (Number.isFinite(step) && Number.isFinite(total)) { - return { step, total, command } - } - } - - return null -} - -// CHANGE: format a step message for UI/heartbeat -// WHY: keep the current build step visible even with sparse logs -// QUOTE(ТЗ): "я не могу понять на каком этапе" -// REF: user-request-2026-01-15 -// SOURCE: n/a -// FORMAT THEOREM: forall s: format(s) -> string -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: message always includes step/total -// COMPLEXITY: O(1) -const formatStepMessage = (step: BuildStepInfo): string => { - const command = step.command.length > 0 ? step.command : "running" - return `Step ${step.step}/${step.total}: ${command}` -} - -const makeComposeCommand = (cwd: string, args: ReadonlyArray) => - pipe( - Command.make("docker", "compose", ...args), - Command.workingDirectory(cwd), - Command.stdout("pipe"), - Command.stderr("pipe") - ) - -// CHANGE: run docker compose while streaming output into deployment status -// WHY: surface deploy progress, step metadata, and heartbeat during long builds -// QUOTE(ТЗ): "процесс деплоя отображать" -// REF: user-request-2026-01-13 -// SOURCE: n/a -// FORMAT THEOREM: forall line: stream(line) -> status(line) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: status updated in order of output lines -// COMPLEXITY: O(n) where n = |output| -export const runComposeWithStatus = ( - cwd: string, - args: ReadonlyArray, - okExitCodes: ReadonlyArray, - projectId: string, - phase: DeploymentPhase -): Effect.Effect => - Effect.scoped( - Effect.gen(function* (_) { - const executor = yield* _(CommandExecutor.CommandExecutor) - const process = yield* _(executor.start(makeComposeCommand(cwd, args))) - const merged = Stream.merge(process.stdout, process.stderr) - let buffer = "" - let lastOutputAt = Date.now() - let currentStep: BuildStepInfo | null = null - - if (phase === "build") { - yield* _( - Effect.forkScoped( - Effect.gen(function* (_) { - while (true) { - yield* _(Effect.sleep(Duration.millis(heartbeatMs))) - const now = Date.now() - if (now - lastOutputAt >= heartbeatMs) { - const stepLabel = currentStep - ? formatStepMessage(currentStep) - : "build still running" - const heartbeatLine = `[heartbeat] ${stepLabel}` - lastOutputAt = now - yield* _(appendDeploymentLog(projectId, heartbeatLine)) - yield* _(setDeploymentStatus(projectId, phase, heartbeatLine)) - } - } - }) - ) - ) - } - - yield* _( - Stream.runForEach(merged, (chunk) => - Effect.gen(function* (_) { - const text = decodeChunk(chunk) - const output = collectLines(buffer, text) - buffer = output.rest - for (const line of output.lines) { - const trimmed = line.trim() - if (trimmed.length > 0) { - const stepInfo = parseBuildStep(trimmed) - if (stepInfo) { - currentStep = stepInfo - } - const message = stepInfo - ? formatStepMessage(stepInfo) - : currentStep - ? `${formatStepMessage(currentStep)} • ${trimmed}` - : trimmed - lastOutputAt = Date.now() - yield* _(appendDeploymentLog(projectId, trimmed)) - yield* _(setDeploymentStatus(projectId, phase, message)) - } - } - }) - ) - ) - - if (buffer.trim().length > 0) { - const trimmed = buffer.trim() - const stepInfo = parseBuildStep(trimmed) - if (stepInfo) { - currentStep = stepInfo - } - const message = stepInfo - ? formatStepMessage(stepInfo) - : currentStep - ? `${formatStepMessage(currentStep)} • ${trimmed}` - : trimmed - lastOutputAt = Date.now() - yield* _(appendDeploymentLog(projectId, trimmed)) - yield* _(setDeploymentStatus(projectId, phase, message)) - } - - const exitCode = yield* _(process.exitCode) - const numericExitCode = Number(exitCode) - if (!okExitCodes.includes(numericExitCode)) { - return yield* _(Effect.fail(new DockerCommandError({ exitCode: numericExitCode }))) - } - }) - ) diff --git a/packages/docker-git/src/server/core/domain.ts b/packages/docker-git/src/server/core/domain.ts deleted file mode 100644 index 52d5071a..00000000 --- a/packages/docker-git/src/server/core/domain.ts +++ /dev/null @@ -1,157 +0,0 @@ -export interface ProjectSummary { - readonly id: string - readonly directory: string - readonly repoUrl: string - readonly repoRef: string - readonly sshUser: string - readonly sshPort: number - readonly sshHost: string - readonly sshCommand: string - readonly sshKeyPath: string | null - readonly containerName: string - readonly serviceName: string - readonly targetDir: string - readonly volumeName: string - readonly authorizedKeysPath: string - readonly authorizedKeysExists: boolean - readonly envGlobalPath: string - readonly envGlobalExists: boolean - readonly envProjectPath: string - readonly envProjectExists: boolean - readonly codexAuthPath: string - readonly codexHome: string -} - -export type ProjectIssue = - | { readonly _tag: "ConfigNotFound"; readonly id: string; readonly path: string } - | { readonly _tag: "ConfigDecode"; readonly id: string; readonly path: string; readonly message: string } - -export interface ProjectsIndex { - readonly root: string - readonly exists: boolean - readonly projects: ReadonlyArray - readonly issues: ReadonlyArray -} - -export interface SshCommandInput { - readonly sshUser: string - readonly sshPort: number - readonly sshHost: string - readonly sshKeyPath: string | null -} - -const trimTrailingSlash = (value: string): string => value.replace(/\/+$/, "") -const expandHome = (value: string, env: Record): string => { - const home = env["HOME"] ?? env["USERPROFILE"] - if (!home || home.trim().length === 0) { - return value - } - const trimmedHome = trimTrailingSlash(home.trim()) - if (value === "~") { - return trimmedHome - } - if (value.startsWith("~/") || value.startsWith("~\\")) { - return `${trimmedHome}${value.slice(1)}` - } - return value -} - -// CHANGE: resolve the projects root from environment or cwd -// WHY: keep root selection pure and consistent across CLI and API -// QUOTE(ТЗ): "оркестратор ... управляем всеми докер образами проектов" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall cwd, env: root(env, cwd) != "" -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: returns a non-empty absolute-ish path segment -// COMPLEXITY: O(1) -export const resolveProjectsRoot = (cwd: string, env: Record): string => { - const explicit = env["DOCKER_GIT_PROJECTS_ROOT"]?.trim() - if (explicit && explicit.length > 0) { - return expandHome(explicit, env) - } - - const home = env["HOME"] ?? env["USERPROFILE"] - if (home && home.trim().length > 0) { - return `${trimTrailingSlash(home.trim())}/.docker-git` - } - - return `${cwd}/.docker-git` -} - -// CHANGE: derive the shared `.orch` root from the projects root -// WHY: keep shared credentials and auth caches in the existing `.orch` layout -// QUOTE(ТЗ): "ОСТАВЬ ВСЁ В .orch" -// REF: issue-61 -// SOURCE: n/a -// FORMAT THEOREM: forall root: orch(root) = root + "/.orch" -// PURITY: CORE -// EFFECT: n/a -// INVARIANT: returned path is non-empty -// COMPLEXITY: O(1) -export const resolveOrchRoot = (projectsRoot: string): string => - `${trimTrailingSlash(projectsRoot)}/.orch` - -// Backward-compatible alias (older code/tests referenced "secrets" root). -export const resolveSecretsRoot = (projectsRoot: string): string => resolveOrchRoot(projectsRoot) - -// CHANGE: derive the shared global env file path -// WHY: allow orchestrator-level integrations without entering containers -// QUOTE(ТЗ): "у меня должна быть возможность подключать гитхаб" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall root: globalEnv(root) = orch(root) + "/env/global.env" -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: returned path is non-empty -// COMPLEXITY: O(1) -export const resolveGlobalEnvPath = (projectsRoot: string): string => - `${resolveOrchRoot(projectsRoot)}/env/global.env` - -// CHANGE: derive the shared Codex auth directory path -// WHY: allow Codex credentials to be managed globally in the orchestrator -// QUOTE(ТЗ): "Добавь подключение Codex в интеграции" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall root: codex(root) = orch(root) + "/auth/codex" -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: returned path is non-empty -// COMPLEXITY: O(1) -export const resolveCodexAuthPath = (projectsRoot: string): string => - `${resolveOrchRoot(projectsRoot)}/auth/codex` - -// CHANGE: resolve the SSH host for connection commands -// WHY: allow users to override the hostname when SSH is bound to a specific interface -// QUOTE(ТЗ): "одной командой мог подключиться по SSH" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall env: host(env) != "" -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: non-empty hostname string -// COMPLEXITY: O(1) -export const resolveSshHost = (env: Record): string => { - const explicit = env["DOCKER_GIT_SSH_HOST"]?.trim() - if (explicit && explicit.length > 0) { - return explicit - } - - return "localhost" -} - -// CHANGE: build a deterministic SSH command for a project -// WHY: provide a single copy-paste command in the UI/API -// QUOTE(ТЗ): "Что бы я одной командой мог подключиться по SSH" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall input: command(input) is deterministic -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: command includes ssh user, host, and port -// COMPLEXITY: O(1) -export const buildSshCommand = (input: SshCommandInput): string => - input.sshKeyPath === null - ? `ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p ${input.sshPort} ${input.sshUser}@${input.sshHost}` - : `ssh -i ${input.sshKeyPath} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p ${input.sshPort} ${input.sshUser}@${input.sshHost}` diff --git a/packages/docker-git/src/server/core/env.ts b/packages/docker-git/src/server/core/env.ts deleted file mode 100644 index c7384578..00000000 --- a/packages/docker-git/src/server/core/env.ts +++ /dev/null @@ -1,158 +0,0 @@ -// CHANGE: normalize env file contents for deterministic writes -// WHY: keep env persistence stable across platforms and edits -// QUOTE(ТЗ): "удобную настройку ENV" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall s: normalize(s) endsWith("\n") -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: output uses LF line endings -// COMPLEXITY: O(n) where n = |s| -const envAssignmentPattern = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/ - -const splitLines = (input: string): ReadonlyArray => - input.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n") - -const joinLines = (lines: ReadonlyArray): string => lines.join("\n") - -const stripQuotes = (value: string): string => { - const trimmed = value.trim() - if (trimmed.length < 2) { - return trimmed - } - const first = trimmed[0] - const last = trimmed[trimmed.length - 1] - if ((first === '"' || first === "'") && first === last) { - return trimmed.slice(1, -1) - } - return trimmed -} - -// CHANGE: normalize env file contents for deterministic writes -// WHY: keep env persistence stable across platforms and edits -// QUOTE(ТЗ): "удобную настройку ENV" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall s: normalize(s) endsWith("\n") -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: output uses LF line endings -// COMPLEXITY: O(n) where n = |s| -export const normalizeEnvText = (input: string): string => { - const normalized = joinLines(splitLines(input)) - return normalized.endsWith("\n") ? normalized : `${normalized}\n` -} - -const extractEnvAssignment = (line: string): { readonly key: string; readonly value: string } | null => { - const match = envAssignmentPattern.exec(line) - if (!match || !match[1]) { - return null - } - return { key: match[1], value: match[2] ?? "" } -} - -export interface EnvEntry { - readonly key: string - readonly value: string -} - -// CHANGE: parse env file into key/value entries -// WHY: enable orchestration of multiple credentials -// QUOTE(ТЗ): "я могу несколько аккаунтов подключать" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall s: parse(s) -> entries(s) -// PURITY: CORE -// EFFECT: Effect, never, never> -// INVARIANT: ignores empty and commented lines -// COMPLEXITY: O(n) where n = |lines| -export const parseEnvEntries = (input: string): ReadonlyArray => { - const entries: Array = [] - for (const line of splitLines(input)) { - const trimmed = line.trim() - if (trimmed.length === 0 || trimmed.startsWith("#")) { - continue - } - const assignment = extractEnvAssignment(line) - if (assignment) { - entries.push({ - key: assignment.key, - value: stripQuotes(assignment.value) - }) - } - } - return entries -} - -// CHANGE: detect whether an env file sets a given key -// WHY: show integration connection status without leaking secrets -// QUOTE(ТЗ): "подключать гитхаб" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall s,k: has(s,k) -> exists assignment in s -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: ignores commented lines -// COMPLEXITY: O(n) where n = |lines| -export const hasEnvKey = (input: string, key: string): boolean => { - const lines = splitLines(input) - for (let i = lines.length - 1; i >= 0; i -= 1) { - const assignment = extractEnvAssignment(lines[i] ?? "") - if (assignment && assignment.key === key) { - return assignment.value.trim().length > 0 - } - } - return false -} - -// CHANGE: resolve the latest value for an env key -// WHY: display current git identity in the UI -// QUOTE(ТЗ): "гит конфиг автоматически?" -// REF: user-request-2026-01-14 -// SOURCE: n/a -// FORMAT THEOREM: forall s,k: value(s,k) = last_assignment(s,k) | null -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: ignores commented lines and empty assignments -// COMPLEXITY: O(n) where n = |lines| -export const findEnvValue = (input: string, key: string): string | null => { - const lines = splitLines(input) - for (let i = lines.length - 1; i >= 0; i -= 1) { - const assignment = extractEnvAssignment(lines[i] ?? "") - if (assignment && assignment.key === key) { - const value = stripQuotes(assignment.value).trim() - return value.length > 0 ? value : null - } - } - return null -} - -// CHANGE: upsert or remove an env key in a text file -// WHY: persist integration credentials deterministically -// QUOTE(ТЗ): "подключать гитхаб" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall s,k,v: upsert(s,k,v) contains k iff v != "" -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: output ends with newline -// COMPLEXITY: O(n) where n = |lines| -export const upsertEnvKey = ( - input: string, - key: string, - value: string -): string => { - const sanitized = normalizeEnvText(input) - const lines = splitLines(sanitized) - const trimmedKey = key.trim() - const cleaned = trimmedKey.length === 0 ? lines : lines.filter((line) => { - const assignment = extractEnvAssignment(line) - return assignment ? assignment.key !== trimmedKey : true - }) - - if (trimmedKey.length === 0 || value.trim().length === 0) { - return normalizeEnvText(joinLines(cleaned)) - } - - return normalizeEnvText(joinLines([...cleaned, `${trimmedKey}=${value}`])) -} diff --git a/packages/docker-git/src/server/core/ports.ts b/packages/docker-git/src/server/core/ports.ts deleted file mode 100644 index 67d3b01a..00000000 --- a/packages/docker-git/src/server/core/ports.ts +++ /dev/null @@ -1,42 +0,0 @@ -export interface PortRange { - readonly min: number - readonly max: number -} - -// CHANGE: select a free port from a range -// WHY: avoid SSH port collisions across docker-git projects -// QUOTE(ТЗ): "Что не так?" -// REF: user-request-2026-01-14 -// SOURCE: n/a -// FORMAT THEOREM: forall p in range: p not in used -> select(p) = p -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: result is within [min, max] or null if exhausted -// COMPLEXITY: O(n) where n = |range| -export const findAvailablePort = ( - preferred: number, - used: ReadonlyArray, - range: PortRange -): number | null => { - const min = Math.min(range.min, range.max) - const max = Math.max(range.min, range.max) - const usedSet = new Set( - used.filter((port) => Number.isFinite(port)).map((port) => Math.trunc(port)) - ) - const normalized = Number.isFinite(preferred) ? Math.trunc(preferred) : min - const start = normalized >= min && normalized <= max ? normalized : min - - for (let port = start; port <= max; port += 1) { - if (!usedSet.has(port)) { - return port - } - } - - for (let port = min; port < start; port += 1) { - if (!usedSet.has(port)) { - return port - } - } - - return null -} diff --git a/packages/docker-git/src/server/core/schema.ts b/packages/docker-git/src/server/core/schema.ts deleted file mode 100644 index 7fafb10f..00000000 --- a/packages/docker-git/src/server/core/schema.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as Schema from "effect/Schema" - -export const ProjectIdPattern = /^(?!.*\.{2})[a-zA-Z0-9._-]+$/ - -// CHANGE: define a safe project id schema for URL params -// WHY: prevent path traversal and keep IDs deterministic -// QUOTE(ТЗ): "оркестратор ... управляем всеми докер образами проектов" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall id: valid(id) -> id matches /^[a-zA-Z0-9._-]+$/ -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: non-empty, no slashes -// COMPLEXITY: O(1) -export const ProjectIdSchema = Schema.NonEmptyString.pipe( - Schema.pattern(ProjectIdPattern) -) - -export const ProjectParamsSchema = Schema.Struct({ - projectId: ProjectIdSchema -}) diff --git a/packages/docker-git/src/server/deployments.ts b/packages/docker-git/src/server/deployments.ts deleted file mode 100644 index e58dc18d..00000000 --- a/packages/docker-git/src/server/deployments.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { Effect } from "effect" - -export type DeploymentPhase = "idle" | "down" | "build" | "up" | "running" | "error" - -export interface DeploymentStatus { - readonly projectId: string - readonly phase: DeploymentPhase - readonly message: string - readonly updatedAt: string -} - -export interface DeploymentLogEntry { - readonly projectId: string - readonly line: string - readonly timestamp: string -} - -const nowIso = (): string => new Date().toISOString() - -const makeDefaultStatus = (projectId: string): DeploymentStatus => ({ - projectId, - phase: "idle", - message: "", - updatedAt: nowIso() -}) - -const state = new Map() -const logs = new Map>() -const active = new Set() -const maxLogEntries = 400 - -// CHANGE: read deployment status for a project -// WHY: expose current deploy phase to the UI -// QUOTE(ТЗ): "процесс деплоя отображать" -// REF: user-request-2026-01-13 -// SOURCE: n/a -// FORMAT THEOREM: forall id: status(id) -> DeploymentStatus(id) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: returns a status even if none exists -// COMPLEXITY: O(1) -export const getDeploymentStatus = ( - projectId: string -): Effect.Effect => - Effect.sync(() => state.get(projectId) ?? makeDefaultStatus(projectId)) - -// CHANGE: list all deployment statuses -// WHY: enable dashboard polling without per-project calls -// QUOTE(ТЗ): "на фронте показывать что конейтер ещё не запущен" -// REF: user-request-2026-01-13 -// SOURCE: n/a -// FORMAT THEOREM: forall _: list() -> statuses -// PURITY: SHELL -// EFFECT: Effect, never, never> -// INVARIANT: order follows insertion order -// COMPLEXITY: O(n) where n = |statuses| -export const listDeploymentStatuses = (): Effect.Effect, never, never> => - Effect.sync(() => Array.from(state.values())) - -// CHANGE: append a deployment log line -// WHY: persist build output for troubleshooting long installs -// QUOTE(ТЗ): "лог установки зависимостей выводить" -// REF: user-request-2026-01-15 -// SOURCE: n/a -// FORMAT THEOREM: forall id: append(id) -> log_length(id) <= max -// PURITY: SHELL -// EFFECT: Effect, never, never> -// INVARIANT: log lines are capped -// COMPLEXITY: O(1) -export const appendDeploymentLog = ( - projectId: string, - line: string -): Effect.Effect, never, never> => - Effect.sync(() => { - const entry: DeploymentLogEntry = { - projectId, - line, - timestamp: nowIso() - } - const existing = logs.get(projectId) ?? [] - const next = [...existing, entry] - const trimmed = next.length > maxLogEntries ? next.slice(next.length - maxLogEntries) : next - logs.set(projectId, trimmed) - return trimmed - }) - -// CHANGE: clear deployment logs for a project -// WHY: separate runs for clarity -// QUOTE(ТЗ): "лог установки зависимостей выводить" -// REF: user-request-2026-01-15 -// SOURCE: n/a -// FORMAT THEOREM: forall id: clear(id) -> log_length(id) = 0 -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: log array exists after clear -// COMPLEXITY: O(1) -export const clearDeploymentLogs = ( - projectId: string -): Effect.Effect => - Effect.sync(() => { - logs.set(projectId, []) - }) - -// CHANGE: list deployment logs for a project -// WHY: show build output in the UI -// QUOTE(ТЗ): "лог установки зависимостей выводить" -// REF: user-request-2026-01-15 -// SOURCE: n/a -// FORMAT THEOREM: forall id: list(id) -> logs(id) -// PURITY: SHELL -// EFFECT: Effect, never, never> -// INVARIANT: returns an empty list if none -// COMPLEXITY: O(1) -export const listDeploymentLogs = ( - projectId: string -): Effect.Effect, never, never> => - Effect.sync(() => logs.get(projectId) ?? []) - -// CHANGE: mark deployment as active -// WHY: avoid orphaned in-progress status when HTTP requests are cancelled -// QUOTE(ТЗ): "Он вообще виснет как я понимаю" -// REF: user-request-2026-01-15 -// SOURCE: n/a -// FORMAT THEOREM: forall id: active(id) -> activeSet contains id -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: active set contains projectId -// COMPLEXITY: O(1) -export const markDeploymentActive = ( - projectId: string -): Effect.Effect => - Effect.sync(() => { - active.add(projectId) - }) - -// CHANGE: mark deployment as inactive -// WHY: ensure status can recover after completion or interruption -// QUOTE(ТЗ): "Он вообще виснет как я понимаю" -// REF: user-request-2026-01-15 -// SOURCE: n/a -// FORMAT THEOREM: forall id: inactive(id) -> activeSet excludes id -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: active set excludes projectId -// COMPLEXITY: O(1) -export const markDeploymentInactive = ( - projectId: string -): Effect.Effect => - Effect.sync(() => { - active.delete(projectId) - }) - -// CHANGE: check whether a deployment is active -// WHY: avoid overlapping docker compose actions -// QUOTE(ТЗ): "Он вообще виснет как я понимаю" -// REF: user-request-2026-01-15 -// SOURCE: n/a -// FORMAT THEOREM: forall id: isActive(id) -> boolean -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: reflects active set membership -// COMPLEXITY: O(1) -export const isDeploymentActive = ( - projectId: string -): Effect.Effect => - Effect.sync(() => active.has(projectId)) - -// CHANGE: upsert deployment status for a project -// WHY: track deploy phases across docker compose operations -// QUOTE(ТЗ): "Он запускается ТАкая-то стадия" -// REF: user-request-2026-01-13 -// SOURCE: n/a -// FORMAT THEOREM: forall id,phase: set(id, phase) -> status(id).phase = phase -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: updatedAt is ISO timestamp -// COMPLEXITY: O(1) -export const setDeploymentStatus = ( - projectId: string, - phase: DeploymentPhase, - message: string -): Effect.Effect => - Effect.sync(() => { - const next: DeploymentStatus = { - projectId, - phase, - message, - updatedAt: nowIso() - } - state.set(projectId, next) - return next - }) diff --git a/packages/docker-git/src/server/docker.ts b/packages/docker-git/src/server/docker.ts deleted file mode 100644 index 49403d9b..00000000 --- a/packages/docker-git/src/server/docker.ts +++ /dev/null @@ -1,81 +0,0 @@ -import * as Chunk from "effect/Chunk" -import { Effect, pipe } from "effect" -import * as Stream from "effect/Stream" -import type { PlatformError } from "@effect/platform/Error" -import * as Command from "@effect/platform/Command" -import * as CommandExecutor from "@effect/platform/CommandExecutor" - -import { DockerCommandError } from "../shell/errors.js" - -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 runCommandCapture = ( - command: Command.Command, - okExitCodes: ReadonlyArray -): Effect.Effect => - Effect.scoped( - Effect.gen(function* (_) { - const executor = yield* _(CommandExecutor.CommandExecutor) - const process = yield* _(executor.start(command)) - const bytes = yield* _(pipe(process.stdout, Stream.runCollect, Effect.map(collectUint8Array))) - const exitCode = yield* _(process.exitCode) - const numericExitCode = Number(exitCode) - - if (!okExitCodes.includes(numericExitCode)) { - return yield* _(Effect.fail(new DockerCommandError({ exitCode: numericExitCode }))) - } - - return new TextDecoder("utf-8").decode(bytes) - }) - ) - -const runComposeCapture = ( - cwd: string, - args: ReadonlyArray, - okExitCodes: ReadonlyArray -): Effect.Effect => { - const command = pipe( - Command.make("docker", "compose", ...args), - Command.workingDirectory(cwd), - Command.stdout("pipe"), - Command.stderr("pipe") - ) - - return runCommandCapture(command, okExitCodes) -} - -// CHANGE: capture docker compose ps output for UI display -// WHY: show container status in the web UI without shell access -// QUOTE(ТЗ): "видеть всю инфу по ним" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall dir: exitCode(cmd(dir)) = 0 -> ps_output(dir) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: stdout captured for response -// COMPLEXITY: O(command) -export const readDockerComposePs = ( - cwd: string -): Effect.Effect => - runComposeCapture(cwd, ["ps"], [0]) - -// CHANGE: capture docker compose logs output for UI display -// WHY: allow log inspection from the web UI -// QUOTE(ТЗ): "вижу всю инфу по ним" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall dir: exitCode(cmd(dir)) in {0,130} -> logs_output(dir) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: stdout captured for response -// COMPLEXITY: O(command) -export const readDockerComposeLogs = ( - cwd: string -): Effect.Effect => - runComposeCapture(cwd, ["logs", "--tail", "200"], [0, 130]) diff --git a/packages/docker-git/src/server/env.ts b/packages/docker-git/src/server/env.ts deleted file mode 100644 index 2286f4f0..00000000 --- a/packages/docker-git/src/server/env.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Effect } from "effect" -import type { PlatformError } from "@effect/platform/Error" -import * as FileSystem from "@effect/platform/FileSystem" -import * as Path from "@effect/platform/Path" - -import { normalizeEnvText } from "./core/env.js" - -const ensureEnvFilePath = ( - fs: FileSystem.FileSystem, - resolved: string -): Effect.Effect => - Effect.gen(function* (_) { - const exists = yield* _(fs.exists(resolved)) - if (!exists) { - return - } - - const info = yield* _(fs.stat(resolved)) - if (info.type === "Directory") { - const backupPath = `${resolved}.bak-${Date.now()}` - yield* _(fs.rename(resolved, backupPath)) - } - }) - -// CHANGE: read an env file from disk -// WHY: supply the env editor with persisted secrets -// QUOTE(ТЗ): "удобную настройку ENV" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall p: exists(p) -> read(p) = text(p) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: missing file yields empty string -// COMPLEXITY: O(n) where n = |file| -export const readEnvFile = ( - filePath: string -): Effect.Effect => - Effect.gen(function* (_) { - const fs = yield* _(FileSystem.FileSystem) - const exists = yield* _(fs.exists(filePath)) - if (!exists) { - return "" - } - return yield* _(fs.readFileString(filePath)) - }) - -// CHANGE: persist env contents to disk -// WHY: allow UI edits to flow into docker compose env_file -// QUOTE(ТЗ): "удобную настройку ENV" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall s: write(s) -> file(s) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: file ends with newline -// COMPLEXITY: O(n) where n = |env| -export const writeEnvFile = ( - filePath: string, - contents: string -): Effect.Effect => - Effect.gen(function* (_) { - const fs = yield* _(FileSystem.FileSystem) - const path = yield* _(Path.Path) - yield* _(ensureEnvFilePath(fs, filePath)) - yield* _(fs.makeDirectory(path.dirname(filePath), { recursive: true })) - yield* _(fs.writeFileString(filePath, normalizeEnvText(contents))) - }) diff --git a/packages/docker-git/src/server/errors.ts b/packages/docker-git/src/server/errors.ts deleted file mode 100644 index bacbf67b..00000000 --- a/packages/docker-git/src/server/errors.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Data } from "effect" - -export class ProjectNotFoundError extends Data.TaggedError("ProjectNotFoundError")<{ - readonly id: string - readonly root: string -}> {} - -export class StaticAssetNotFoundError extends Data.TaggedError("StaticAssetNotFoundError")<{ - readonly path: string -}> {} diff --git a/packages/docker-git/src/server/git-credentials.ts b/packages/docker-git/src/server/git-credentials.ts deleted file mode 100644 index affc4e92..00000000 --- a/packages/docker-git/src/server/git-credentials.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { findEnvValue } from "./core/env.js" -import { - type LabeledEnvEntry, - findLabeledEnvEntryByLabel, - listLabeledEnvEntries, - resolveLabeledEnvLabelForValue -} from "./labeled-env.js" - -export interface GitCredentialEntry { - readonly label: string - readonly token: string - readonly user: string -} - -const gitTokenBaseKey = "GIT_AUTH_TOKEN" -const gitUserBaseKey = "GIT_AUTH_USER" -const gitLabelKey = "GIT_AUTH_LABEL" -const defaultGitUser = "x-access-token" - -const buildGitUserMap = (envText: string): ReadonlyMap => - new Map( - listLabeledEnvEntries(envText, gitUserBaseKey).map((entry) => [entry.label, entry.value] as const) - ) - -const toGitCredential = ( - tokenEntry: LabeledEnvEntry, - users: ReadonlyMap -): GitCredentialEntry => { - const defaultUser = users.get("default") ?? defaultGitUser - const user = users.get(tokenEntry.label) ?? defaultUser - return { - label: tokenEntry.label, - token: tokenEntry.value, - user - } -} - -// CHANGE: list labeled Git credentials from env text -// WHY: enable selecting one of many Git credential sets per project -// QUOTE(ТЗ): "возможность выбора нескольки GH, GIT ключей" -// REF: issue-61 -// SOURCE: n/a -// FORMAT THEOREM: forall env: list(env) -> credentials(env) -// PURITY: CORE -// EFFECT: Effect, never, never> -// INVARIANT: token values are non-empty -// COMPLEXITY: O(n) where n = |entries| -export const listGitCredentials = (envText: string): ReadonlyArray => { - const tokens = listLabeledEnvEntries(envText, gitTokenBaseKey) - const users = buildGitUserMap(envText) - return tokens.map((entry) => toGitCredential(entry, users)) -} - -// CHANGE: find Git credential by label -// WHY: map selected label from UI to token+user pair -// QUOTE(ТЗ): "реализовать систему где я могу задавать N множества ключей" -// REF: issue-61 -// SOURCE: n/a -// FORMAT THEOREM: forall c,l: find(c,l) -> credential(l) | null -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: label normalization matches key normalization -// COMPLEXITY: O(n) where n = |credentials| -export const findGitCredentialByLabel = ( - envText: string, - label: string -): GitCredentialEntry | null => { - const tokenEntries = listLabeledEnvEntries(envText, gitTokenBaseKey) - const token = findLabeledEnvEntryByLabel(tokenEntries, gitTokenBaseKey, label) - if (token === null) { - return null - } - const users = buildGitUserMap(envText) - return toGitCredential(token, users) -} - -// CHANGE: resolve active Git token from project env -// WHY: show currently connected Git credential in project settings -// QUOTE(ТЗ): "возможность выбора" -// REF: issue-61 -// SOURCE: n/a -// FORMAT THEOREM: forall env: token(env) -> string | null -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: empty values map to null -// COMPLEXITY: O(n) where n = |entries| -export const resolveProjectGitToken = (envText: string): string | null => - findEnvValue(envText, gitTokenBaseKey) - -// CHANGE: resolve active Git label override from project env -// WHY: preserve explicit label choice when token value can overlap -// QUOTE(ТЗ): "возможность выбора" -// REF: issue-61 -// SOURCE: n/a -// FORMAT THEOREM: forall env: label(env) -> string | null -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: empty values map to null -// COMPLEXITY: O(n) where n = |entries| -export const resolveProjectGitLabel = (envText: string): string | null => - findEnvValue(envText, gitLabelKey) - -// CHANGE: resolve Git label by token value -// WHY: show inferred active label when project label key is absent -// QUOTE(ТЗ): "возможность выбора" -// REF: issue-61 -// SOURCE: n/a -// FORMAT THEOREM: forall c,t: label(c,t) -> string | null -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: exact token match -// COMPLEXITY: O(n) where n = |credentials| -export const resolveGitLabelForToken = ( - envText: string, - token: string -): string | null => { - const entries = listLabeledEnvEntries(envText, gitTokenBaseKey) - return resolveLabeledEnvLabelForValue(entries, token) -} - -// CHANGE: expose env key used to persist project Git label -// WHY: keep route handlers and helpers consistent -// QUOTE(ТЗ): "реализовать систему выбора" -// REF: issue-61 -// SOURCE: n/a -// FORMAT THEOREM: forall _: key = constant -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: constant key -// COMPLEXITY: O(1) -export const projectGitLabelKey = gitLabelKey diff --git a/packages/docker-git/src/server/github.ts b/packages/docker-git/src/server/github.ts deleted file mode 100644 index 27a009aa..00000000 --- a/packages/docker-git/src/server/github.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { Effect, Either } from "effect" -import * as Data from "effect/Data" -import * as ParseResult from "effect/ParseResult" -import * as Schema from "effect/Schema" - -import { findEnvValue, parseEnvEntries } from "./core/env.js" - -export interface GithubTokenEntry { - readonly key: string - readonly label: string - readonly token: string -} - -export interface GithubAccount { - readonly label: string - readonly login: string -} - -export class GithubAccountError extends Data.TaggedError("GithubAccountError")<{ - readonly label: string - readonly message: string -}> {} - -const GithubUserSchema = Schema.Struct({ - login: Schema.String -}) - -const decodeGithubUser = ( - label: string, - input: unknown -): Effect.Effect<{ readonly login: string }, GithubAccountError> => - Either.match(ParseResult.decodeUnknownEither(GithubUserSchema)(input), { - onLeft: (issue) => - Effect.fail( - new GithubAccountError({ - label, - message: ParseResult.TreeFormatter.formatIssueSync(issue) - }) - ), - onRight: (value) => Effect.succeed(value) - }) - -const tokenKey = "GITHUB_TOKEN" -const tokenPrefix = "GITHUB_TOKEN__" -const projectTokenKeys: ReadonlyArray = ["GIT_AUTH_TOKEN", "GITHUB_TOKEN"] -const projectLabelKey = "GITHUB_AUTH_LABEL" - -const normalizeLabel = (value: string): string => { - const trimmed = value.trim() - if (trimmed.length === 0) { - return "" - } - const normalized = trimmed - .toUpperCase() - .replace(/[^A-Z0-9]+/g, "_") - .replace(/^_+/, "") - .replace(/_+$/, "") - return normalized.length > 0 ? normalized : "" -} - -// CHANGE: treat "default" label as the base GitHub token key -// WHY: keep UI label stable while resolving to GITHUB_TOKEN -// QUOTE(ТЗ): "выбираю гитхаб аккаунт для определённого докера" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall l: l = "default" -> key(l) = GITHUB_TOKEN -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: "default" and empty labels map to the same key -// COMPLEXITY: O(1) -export const buildGithubTokenKey = (label: string): string => { - const normalized = normalizeLabel(label) - if (normalized === "DEFAULT") { - return tokenKey - } - return normalized.length === 0 ? tokenKey : `${tokenPrefix}${normalized}` -} - -const labelFromKey = (key: string): string => - key.startsWith(tokenPrefix) ? key.slice(tokenPrefix.length) : "default" - -// CHANGE: find a GitHub token entry by label -// WHY: allow selecting a specific account for a project -// QUOTE(ТЗ): "я могу несколько аккаунтов подключать" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall label: find(label) -> entry(label) | null -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: label normalization matches buildGithubTokenKey -// COMPLEXITY: O(n) where n = |tokens| -export const findGithubTokenByLabel = ( - tokens: ReadonlyArray, - label: string -): GithubTokenEntry | null => { - const key = buildGithubTokenKey(label) - return tokens.find((entry) => entry.key === key) ?? null -} - -// CHANGE: resolve active GitHub token from a project env file -// WHY: display which account is wired to a project -// QUOTE(ТЗ): "должен быть отображён что за аккаунт подключён" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall env: resolve(env) = token(env) | null -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: prefers GIT_AUTH_TOKEN over GITHUB_TOKEN -// COMPLEXITY: O(n) where n = |entries| -export const resolveProjectGithubToken = (envText: string): string | null => { - const entries = parseEnvEntries(envText) - for (const key of projectTokenKeys) { - const entry = entries.find((item) => item.key === key) - if (entry && entry.value.trim().length > 0) { - return entry.value - } - } - return null -} - -// CHANGE: resolve active GitHub label override from project env -// WHY: avoid scanning all stored tokens when a project explicitly picks a label -// QUOTE(ТЗ): "выбор нескольки GH ключей" -// REF: issue-61 -// SOURCE: n/a -// FORMAT THEOREM: forall env: label(env) -> string | null -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: empty values map to null -// COMPLEXITY: O(n) where n = |lines| -export const resolveProjectGithubLabel = (envText: string): string | null => - findEnvValue(envText, projectLabelKey) - -// CHANGE: expose env key used to persist project GitHub label -// WHY: keep route handlers aligned on a single key -// QUOTE(ТЗ): "выбор нескольки GH ключей" -// REF: issue-61 -// SOURCE: n/a -// FORMAT THEOREM: forall _: key = constant -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: constant key -// COMPLEXITY: O(1) -export const projectGithubLabelKey = projectLabelKey - -// CHANGE: match a token value to a known label -// WHY: show connected account label without leaking secrets -// QUOTE(ТЗ): "должен быть отображён что за аккаунт подключён" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall t: match(t) -> label(t) | null -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: exact token match -// COMPLEXITY: O(n) where n = |tokens| -export const resolveGithubLabelForToken = ( - tokens: ReadonlyArray, - token: string -): string | null => { - const match = tokens.find((entry) => entry.token === token) - return match ? match.label : null -} - -// CHANGE: list GitHub tokens from env text -// WHY: support multiple accounts in the orchestrator -// QUOTE(ТЗ): "я могу несколько аккаунтов подключать" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall s: tokens(s) = subset(env(s)) -// PURITY: CORE -// EFFECT: Effect, never, never> -// INVARIANT: returns only non-empty tokens -// COMPLEXITY: O(n) where n = |entries| -export const listGithubTokens = (envText: string): ReadonlyArray => - parseEnvEntries(envText) - .filter((entry) => - entry.key === tokenKey || entry.key.startsWith(tokenPrefix) - ) - .map((entry) => ({ - key: entry.key, - label: labelFromKey(entry.key), - token: entry.value - })) - .filter((entry) => entry.token.trim().length > 0) - -// CHANGE: resolve GitHub account identity for a token -// WHY: show which account is connected in the UI -// QUOTE(ТЗ): "должен быть отображён что за аккаунт подключён" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall t: fetch(t) -> login(t) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: no tokens are logged -// COMPLEXITY: O(1) -export const fetchGithubAccount = ( - entry: GithubTokenEntry -): Effect.Effect => - Effect.tryPromise({ - try: async () => { - const response = await fetch("https://api.github.com/user", { - method: "GET", - headers: { - "Accept": "application/vnd.github+json", - "Authorization": `Bearer ${entry.token}`, - "User-Agent": "docker-git" - } - }) - if (!response.ok) { - const body = await response.text() - throw new Error(`GitHub response ${response.status}: ${body}`) - } - return response.json() - }, - catch: (error) => - new GithubAccountError({ - label: entry.label, - message: error instanceof Error ? error.message : String(error) - }) - }).pipe( - Effect.flatMap((json) => decodeGithubUser(entry.label, json)), - Effect.map((user) => ({ - label: entry.label, - login: user.login - })) - ) diff --git a/packages/docker-git/src/server/http.ts b/packages/docker-git/src/server/http.ts deleted file mode 100644 index cb9a3670..00000000 --- a/packages/docker-git/src/server/http.ts +++ /dev/null @@ -1,1735 +0,0 @@ -import { Console, Effect, pipe, Either, Chunk, Ref, Duration } from "effect" -import * as Stream from "effect/Stream" -import type { PlatformError } from "@effect/platform/Error" -import type * as HttpBody from "@effect/platform/HttpBody" -import * as HttpRouter from "@effect/platform/HttpRouter" -import * as HttpServerRequest from "@effect/platform/HttpServerRequest" -import * as HttpServerResponse from "@effect/platform/HttpServerResponse" -import * as HttpServerError from "@effect/platform/HttpServerError" -import * as FileSystem from "@effect/platform/FileSystem" -import * as ParseResult from "effect/ParseResult" -import * as Schema from "effect/Schema" -import * as Path from "@effect/platform/Path" - -import { - type ConfigDecodeError, - type ConfigNotFoundError, - CloneFailedError, - FileExistsError, - PortProbeError -} from "../shell/errors.js" -import { DockerCommandError } from "../shell/errors.js" -import { readProjectConfig } from "../shell/config.js" -import { ProjectNotFoundError, StaticAssetNotFoundError } from "./errors.js" -import { ProjectParamsSchema } from "./core/schema.js" -import { resolveCodexAuthPath, resolveGlobalEnvPath } from "./core/domain.js" -import { parseEnvEntries, upsertEnvKey } from "./core/env.js" -import { loadProject, scanProjects } from "./projects.js" -import { readDockerComposeLogs, readDockerComposePs } from "./docker.js" -import { readEnvFile, writeEnvFile } from "./env.js" -import { - appendDeploymentLog, - clearDeploymentLogs, - getDeploymentStatus, - isDeploymentActive, - listDeploymentLogs, - listDeploymentStatuses, - markDeploymentActive, - markDeploymentInactive, - setDeploymentStatus -} from "./deployments.js" -import { runComposeWithStatus } from "./compose.js" -import { - renderCodexLoginPage, - renderClonePage, - renderDashboard, - renderEnvPage, - renderGithubTokenHelpPage, - renderIntegrationsPage, - renderDeployLogsPage, - renderOutputPage, - renderTerminalPage -} from "./view.js" -import { - buildGithubTokenKey, - findGithubTokenByLabel, - listGithubTokens, - projectGithubLabelKey, - resolveProjectGithubLabel, - resolveProjectGithubToken -} from "./github.js" -import { - findGitCredentialByLabel, - projectGitLabelKey, - resolveProjectGitLabel, - resolveProjectGitToken -} from "./git-credentials.js" -import { - findClaudeApiKeyByLabel, - projectClaudeLabelKey, - resolveProjectClaudeApiKey, - resolveProjectClaudeLabel -} from "./claude.js" -import { buildLabeledEnvKey } from "./labeled-env.js" -import { createProject } from "@effect-template/lib/usecases/actions" -import { buildCreateCommand } from "@effect-template/lib/core/command-builders" -import type { RawOptions } from "@effect-template/lib/core/command-options" -import type { DockerAccessError as LibDockerAccessError } from "@effect-template/lib/shell/errors" -import { defaultTemplateConfig, deriveRepoSlug } from "../core/domain.js" -import { formatParseError } from "@effect-template/lib/core/parse-errors" -import { - CodexAuthError, - clearCodexAuthDir, - copyCodexAuthDir, - findCodexAccountPath, - importCodexAuthDir, - listCodexAccounts, - readCodexAuthStatus, - removeCodexAccount, - resolveCodexSourcePath, - resolveProjectCodexAuthPath, - resolveWritableCodexRoot, - resolveProjectCodexLabel -} from "./codex.js" -import { findAvailablePort, type PortRange } from "./core/ports.js" - -export interface ServerPaths { - readonly cwd: string - readonly projectsRoot: string - readonly webRoot: string - readonly vendorRoot: string - readonly terminalPort: number -} - -type ApiError = - | ProjectNotFoundError - | StaticAssetNotFoundError - | ConfigNotFoundError - | ConfigDecodeError - | FileExistsError - | CloneFailedError - | PortProbeError - | DockerCommandError - | LibDockerAccessError - | CodexAuthError - | ParseResult.ParseError - | HttpBody.HttpBodyError - | HttpServerError.RequestError - | PlatformError - -const EnvFormSchema = Schema.Struct({ - globalEnv: Schema.String, - projectEnv: Schema.String -}) - -const GithubConnectSchema = Schema.Struct({ - githubToken: Schema.String, - githubLabel: Schema.optional(Schema.String) -}) - -const GithubDisconnectSchema = Schema.Struct({ - githubLabel: Schema.optional(Schema.String) -}) - -const GithubProjectConnectSchema = Schema.Struct({ - githubLabel: Schema.String -}) - -const GithubProjectDisconnectSchema = Schema.Struct({}) - -const GitConnectSchema = Schema.Struct({ - gitToken: Schema.String, - gitUser: Schema.optional(Schema.String), - gitLabel: Schema.optional(Schema.String) -}) - -const GitDisconnectSchema = Schema.Struct({ - gitLabel: Schema.optional(Schema.String) -}) - -const GitProjectConnectSchema = Schema.Struct({ - gitLabel: Schema.String -}) - -const GitProjectDisconnectSchema = Schema.Struct({}) - -const ClaudeConnectSchema = Schema.Struct({ - claudeApiKey: Schema.String, - claudeLabel: Schema.optional(Schema.String) -}) - -const ClaudeDisconnectSchema = Schema.Struct({ - claudeLabel: Schema.optional(Schema.String) -}) - -const ClaudeProjectConnectSchema = Schema.Struct({ - claudeLabel: Schema.String -}) - -const ClaudeProjectDisconnectSchema = Schema.Struct({}) - -const CodexConnectSchema = Schema.Struct({ - codexSource: Schema.optional(Schema.String), - codexLabel: Schema.optional(Schema.String) -}) -const CodexCliLoginSchema = Schema.Struct({ - codexLabel: Schema.optional(Schema.String) -}) - -const CodexProjectConnectSchema = Schema.Struct({ - codexLabel: Schema.String -}) - -const CodexProjectDisconnectSchema = Schema.Struct({}) - -const CloneFormSchema = Schema.Struct({ - repoUrl: Schema.String, - repoRef: Schema.optional(Schema.String), - githubLabel: Schema.optional(Schema.String) -}) - -const GitIdentitySchema = Schema.Struct({ - gitUserName: Schema.String, - gitUserEmail: Schema.String -}) - -const sshPortRange: PortRange = { min: 2222, max: 2299 } - -const chooseSshPort = ( - preferred: number, - usedPorts: ReadonlyArray -): number => - findAvailablePort(preferred, usedPorts, sshPortRange) ?? preferred - -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 jsonResponse = (data: unknown, status: number) => - pipe( - HttpServerResponse.json(data), - Effect.map(HttpServerResponse.setStatus(status)) - ) - -const htmlResponse = (data: string, status: number) => - Effect.succeed(HttpServerResponse.setStatus(HttpServerResponse.html(data), status)) - -const errorResponse = (error: ApiError) => { - if (ParseResult.isParseError(error)) { - const message = ParseResult.TreeFormatter.formatIssueSync(error.issue) - return jsonResponse({ - error: { - type: "ParseError", - message - } - }, 400) - } - - if (error._tag === "ProjectNotFoundError") { - return jsonResponse({ - error: { - type: error._tag, - message: `Project not found: ${error.id}`, - root: error.root - } - }, 404) - } - - if (error._tag === "ConfigNotFoundError") { - return jsonResponse({ - error: { - type: error._tag, - message: `Config not found: ${error.path}` - } - }, 404) - } - - if (error._tag === "ConfigDecodeError") { - return jsonResponse({ - error: { - type: error._tag, - message: `Config decode failed at ${error.path}`, - reason: error.message - } - }, 400) - } - - if (error._tag === "FileExistsError") { - return jsonResponse({ - error: { - type: error._tag, - message: `File exists: ${error.path}` - } - }, 409) - } - - if (error._tag === "DockerCommandError") { - return jsonResponse({ - error: { - type: error._tag, - message: `docker compose failed: exit ${error.exitCode}` - } - }, 502) - } - - if (error._tag === "CodexAuthError") { - return jsonResponse({ - error: { - type: error._tag, - message: error.message - } - }, 400) - } - - if (error._tag === "StaticAssetNotFoundError") { - return jsonResponse({ - error: { - type: error._tag, - message: `Asset not found: ${error.path}` - } - }, 404) - } - - if (error._tag === "RequestError") { - return jsonResponse({ - error: { - type: error._tag, - message: error.message - } - }, 400) - } - - return jsonResponse({ - error: { - type: "UnknownError", - message: String(error) - } - }, 500) -} - -const htmlErrorResponse = (error: ApiError) => - htmlResponse( - renderOutputPage("Error", error instanceof Error ? error.message : String(error)), - 500 - ) - -const serveFile = (path: string) => - Effect.gen(function* (_) { - const fs = yield* _(FileSystem.FileSystem) - const exists = yield* _(fs.exists(path)) - if (!exists) { - return yield* _(Effect.fail(new StaticAssetNotFoundError({ path }))) - } - return yield* _(HttpServerResponse.file(path)) - }) - -const resolveProjectCodexTarget = ( - projectsRoot: string, - project: { readonly id: string } -): Effect.Effect => - Effect.gen(function* (_) { - const path = yield* _(Path.Path) - const root = yield* _(resolveWritableCodexRoot(projectsRoot)) - return path.resolve(resolveProjectCodexAuthPath(root, project.id)) - }) - -const isWritableDirectory = ( - dirPath: string -): Effect.Effect => - Effect.gen(function* (_) { - const fs = yield* _(FileSystem.FileSystem) - const path = yield* _(Path.Path) - const exists = yield* _(fs.exists(dirPath)) - if (!exists) { - return false - } - const info = yield* _(fs.stat(dirPath)) - if (info.type !== "Directory") { - return false - } - const testPath = path.join(dirPath, ".dg-write-test") - const write = yield* _(Effect.either(fs.writeFileString(testPath, "ok"))) - if (write._tag === "Left") { - return false - } - yield* _(fs.remove(testPath, { force: true })) - return true - }) - -const syncProjectCodexAuth = ( - projectsRoot: string, - project: { readonly id: string; readonly codexAuthPath: string } -): Effect.Effect => - Effect.gen(function* (_) { - const path = yield* _(Path.Path) - const resolvedRoot = path.resolve(projectsRoot) - const resolvedTarget = path.resolve(project.codexAuthPath) - const targetWritable = yield* _(isWritableDirectory(resolvedTarget)) - if (!targetWritable) { - yield* _( - Effect.fail( - new CodexAuthError({ - message: `Codex target path not writable: ${resolvedTarget}` - }) - ) - ) - return - } - - const projectEnv = yield* _(readEnvFile(project.codexAuthPath)) - const codexLabel = resolveProjectCodexLabel(projectEnv) ?? "default" - - const codexRootResult = yield* _(Effect.either(resolveWritableCodexRoot(resolvedRoot))) - const codexRootPath = codexRootResult._tag === "Right" ? codexRootResult.right : null - const expectedRoot = resolveCodexAuthPath(resolvedRoot) - if (codexRootResult._tag === "Right") { - const resolvedCodexRoot = path.resolve(codexRootResult.right) - if (resolvedCodexRoot !== path.resolve(expectedRoot)) { - yield* _(Console.log(`codex auth root fallback: ${expectedRoot} -> ${resolvedCodexRoot}`)) - } - } - - let accountPath: string | null = null - if (codexRootPath !== null) { - const accountResult = yield* _(Effect.either(findCodexAccountPath(codexRootPath, codexLabel))) - if (accountResult._tag === "Right") { - accountPath = accountResult.right - } - } - - const fallbackSource = resolveCodexSourcePath( - undefined, - process.env["HOME"], - process.env["CODEX_HOME"] - ) - const resolvedSource = accountPath - ? path.resolve(accountPath) - : fallbackSource - ? path.resolve(fallbackSource) - : null - if (resolvedSource === null) { - yield* _( - Console.warn( - `codex sync skipped project=${project.id} reason=source-not-connected` - ) - ) - return - } - - const status = yield* _(readCodexAuthStatus(resolvedSource)) - if (!status.connected) { - yield* _( - Console.warn( - `codex sync skipped project=${project.id} reason=source-not-connected source=${resolvedSource}` - ) - ) - return - } - - const targetPath = yield* _(resolveProjectCodexTarget(projectsRoot, project)) - if (resolvedSource === targetPath) { - return - } - - yield* _( - Console.log( - `codex sync project=${project.id} label=${codexLabel} source=${resolvedSource} target=${targetPath}` - ) - ) - yield* _(copyCodexAuthDir(resolvedSource, targetPath)) - }) - -// CHANGE: build the HTTP router for docker-git orchestration -// WHY: expose a typed API and static UI for managing containers -// QUOTE(ТЗ): "Просто сделай сайт и бекенд приложение" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall request: route(request) -> response(request) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: all errors mapped to HTTP responses -// COMPLEXITY: O(1) per route -export const makeRouter = ({ cwd, projectsRoot, webRoot, vendorRoot, terminalPort }: ServerPaths) => { - const projectParams = HttpRouter.schemaPathParams(ProjectParamsSchema) - - const withDeploymentGuard = ( - project: { readonly id: string }, - effect: Effect.Effect - ): Effect.Effect => - Effect.gen(function* (_) { - const running = yield* _(isDeploymentActive(project.id)) - if (running) { - yield* _(appendDeploymentLog(project.id, "[skip] deployment already running")) - yield* _(setDeploymentStatus(project.id, "build", "deployment already running")) - return - } - // CHANGE: clear logs before starting a new deployment - // WHY: ensure the UI reflects the fresh run immediately - // QUOTE(ТЗ): "Да чисти логи деплоя" - // REF: user-request-2026-01-15 - // SOURCE: n/a - // FORMAT THEOREM: forall run: start(run) -> logs(run) = empty - // PURITY: SHELL - // EFFECT: Effect - // INVARIANT: logs are empty before the new run emits output - // COMPLEXITY: O(1) - yield* _(clearDeploymentLogs(project.id)) - yield* _(markDeploymentActive(project.id)) - yield* _( - Effect.forkDaemon( - effect.pipe( - Effect.onInterrupt(() => - pipe( - appendDeploymentLog(project.id, "[interrupt] deployment interrupted"), - Effect.zipRight( - setDeploymentStatus(project.id, "error", "deployment interrupted") - ) - ) - ), - Effect.catchAll((error) => - pipe( - appendDeploymentLog(project.id, `[error] ${String(error)}`), - Effect.zipRight( - setDeploymentStatus(project.id, "error", "deployment failed") - ) - ) - ), - Effect.ensuring(markDeploymentInactive(project.id)) - ) - ) - ) - }) - - const uiRouterBase = HttpRouter.empty.pipe( - HttpRouter.get( - "/", - pipe( - scanProjects(projectsRoot, cwd), - Effect.map((index) => renderDashboard(index)), - Effect.map((html) => HttpServerResponse.html(html)), - Effect.catchAll(htmlErrorResponse) - ) - ), - HttpRouter.get( - "/clone", - Effect.gen(function* (_) { - const path = yield* _(Path.Path) - const resolvedRoot = path.resolve(projectsRoot) - const globalEnvPath = resolveGlobalEnvPath(resolvedRoot) - const envText = yield* _(readEnvFile(globalEnvPath)) - const githubTokenEntries = countKeyEntries(envText, "GITHUB_TOKEN") - const html = renderClonePage(globalEnvPath, githubTokenEntries) - return HttpServerResponse.html(html) - }).pipe( - Effect.tapError((error) => Console.error(`clone failed: ${String(error)}`)), - Effect.catchAll(htmlErrorResponse) - ) - ), - HttpRouter.get( - "/integrations", - Effect.gen(function* (_) { - const path = yield* _(Path.Path) - const resolvedRoot = path.resolve(projectsRoot) - const globalEnvPath = resolveGlobalEnvPath(resolvedRoot) - const codexRootPath = yield* _(resolveWritableCodexRoot(resolvedRoot)) - const envText = yield* _(readEnvFile(globalEnvPath)) - const githubTokenEntries = countKeyEntries(envText, "GITHUB_TOKEN") - const gitTokenEntries = countKeyEntries(envText, "GIT_AUTH_TOKEN") - const claudeKeyEntries = countKeyEntries(envText, "ANTHROPIC_API_KEY") - const codexAccounts = yield* _(listCodexAccounts(codexRootPath)) - const html = renderIntegrationsPage( - globalEnvPath, - githubTokenEntries, - gitTokenEntries, - claudeKeyEntries, - codexRootPath, - codexAccounts - ) - return HttpServerResponse.html(html) - }).pipe(Effect.catchAll(htmlErrorResponse)) - ), - HttpRouter.get( - "/integrations/github/token", - Effect.succeed(renderGithubTokenHelpPage()).pipe( - Effect.map((html) => HttpServerResponse.html(html)), - Effect.catchAll(htmlErrorResponse) - ) - ), - HttpRouter.post( - "/integrations/github/connect", - Effect.gen(function* (_) { - const { githubToken, githubLabel } = yield* _(HttpServerRequest.schemaBodyUrlParams(GithubConnectSchema)) - const path = yield* _(Path.Path) - const resolvedRoot = path.resolve(projectsRoot) - const globalEnvPath = resolveGlobalEnvPath(resolvedRoot) - const envText = yield* _(readEnvFile(globalEnvPath)) - const key = buildGithubTokenKey(githubLabel?.trim() ?? "") - const nextText = upsertEnvKey(envText, key, githubToken) - yield* _(writeEnvFile(globalEnvPath, nextText)) - return HttpServerResponse.redirect("/integrations") - }).pipe(Effect.catchAll(htmlErrorResponse)) - ), - HttpRouter.post( - "/integrations/github/disconnect", - Effect.gen(function* (_) { - const { githubLabel } = yield* _(HttpServerRequest.schemaBodyUrlParams(GithubDisconnectSchema)) - const path = yield* _(Path.Path) - const resolvedRoot = path.resolve(projectsRoot) - const globalEnvPath = resolveGlobalEnvPath(resolvedRoot) - const envText = yield* _(readEnvFile(globalEnvPath)) - const key = buildGithubTokenKey(githubLabel?.trim() ?? "") - const nextText = upsertEnvKey(envText, key, "") - yield* _(writeEnvFile(globalEnvPath, nextText)) - return HttpServerResponse.redirect("/integrations") - }).pipe(Effect.catchAll(htmlErrorResponse)) - ), - HttpRouter.post( - "/integrations/git/connect", - Effect.gen(function* (_) { - const { gitLabel, gitToken, gitUser } = yield* _(HttpServerRequest.schemaBodyUrlParams(GitConnectSchema)) - const path = yield* _(Path.Path) - const resolvedRoot = path.resolve(projectsRoot) - const globalEnvPath = resolveGlobalEnvPath(resolvedRoot) - const envText = yield* _(readEnvFile(globalEnvPath)) - const label = gitLabel?.trim() ?? "" - const tokenKey = buildLabeledEnvKey("GIT_AUTH_TOKEN", label) - const userKey = buildLabeledEnvKey("GIT_AUTH_USER", label) - const trimmedGitUser = gitUser?.trim() ?? "" - const user = trimmedGitUser.length > 0 ? trimmedGitUser : "x-access-token" - const withToken = upsertEnvKey(envText, tokenKey, gitToken) - const nextText = upsertEnvKey(withToken, userKey, user) - yield* _(writeEnvFile(globalEnvPath, nextText)) - return HttpServerResponse.redirect("/integrations") - }).pipe(Effect.catchAll(htmlErrorResponse)) - ), - HttpRouter.post( - "/integrations/git/disconnect", - Effect.gen(function* (_) { - const { gitLabel } = yield* _(HttpServerRequest.schemaBodyUrlParams(GitDisconnectSchema)) - const path = yield* _(Path.Path) - const resolvedRoot = path.resolve(projectsRoot) - const globalEnvPath = resolveGlobalEnvPath(resolvedRoot) - const envText = yield* _(readEnvFile(globalEnvPath)) - const label = gitLabel?.trim() ?? "" - const tokenKey = buildLabeledEnvKey("GIT_AUTH_TOKEN", label) - const userKey = buildLabeledEnvKey("GIT_AUTH_USER", label) - const withoutToken = upsertEnvKey(envText, tokenKey, "") - const nextText = upsertEnvKey(withoutToken, userKey, "") - yield* _(writeEnvFile(globalEnvPath, nextText)) - return HttpServerResponse.redirect("/integrations") - }).pipe(Effect.catchAll(htmlErrorResponse)) - ), - HttpRouter.post( - "/integrations/claude/connect", - Effect.gen(function* (_) { - const { claudeApiKey, claudeLabel } = yield* _( - HttpServerRequest.schemaBodyUrlParams(ClaudeConnectSchema) - ) - const path = yield* _(Path.Path) - const resolvedRoot = path.resolve(projectsRoot) - const globalEnvPath = resolveGlobalEnvPath(resolvedRoot) - const envText = yield* _(readEnvFile(globalEnvPath)) - const key = buildLabeledEnvKey("ANTHROPIC_API_KEY", claudeLabel?.trim() ?? "") - const nextText = upsertEnvKey(envText, key, claudeApiKey) - yield* _(writeEnvFile(globalEnvPath, nextText)) - return HttpServerResponse.redirect("/integrations") - }).pipe(Effect.catchAll(htmlErrorResponse)) - ), - HttpRouter.post( - "/integrations/claude/disconnect", - Effect.gen(function* (_) { - const { claudeLabel } = yield* _( - HttpServerRequest.schemaBodyUrlParams(ClaudeDisconnectSchema) - ) - const path = yield* _(Path.Path) - const resolvedRoot = path.resolve(projectsRoot) - const globalEnvPath = resolveGlobalEnvPath(resolvedRoot) - const envText = yield* _(readEnvFile(globalEnvPath)) - const key = buildLabeledEnvKey("ANTHROPIC_API_KEY", claudeLabel?.trim() ?? "") - const nextText = upsertEnvKey(envText, key, "") - yield* _(writeEnvFile(globalEnvPath, nextText)) - return HttpServerResponse.redirect("/integrations") - }).pipe(Effect.catchAll(htmlErrorResponse)) - ), - HttpRouter.post( - "/integrations/codex/connect", - Effect.gen(function* (_) { - const { codexSource, codexLabel } = yield* _(HttpServerRequest.schemaBodyUrlParams(CodexConnectSchema)) - const path = yield* _(Path.Path) - const resolvedRoot = path.resolve(projectsRoot) - const codexRootPath = yield* _(resolveWritableCodexRoot(resolvedRoot)) - const expectedRoot = resolveCodexAuthPath(resolvedRoot) - const source = resolveCodexSourcePath( - codexSource, - process.env["HOME"], - process.env["CODEX_HOME"] - ) - if (source === null) { - yield* _( - Effect.fail( - new CodexAuthError({ - message: "Codex source path not provided and HOME is not set" - }) - ) - ) - return HttpServerResponse.redirect("/integrations") - } - const resolvedSource = path.isAbsolute(source) ? source : path.resolve(cwd, source) - const resolvedRootPath = path.resolve(codexRootPath) - if (resolvedRootPath !== path.resolve(expectedRoot)) { - yield* _(Console.log(`codex auth root fallback: ${expectedRoot} -> ${resolvedRootPath}`)) - } - yield* _( - Console.log( - `codex import start label=${codexLabel?.trim() ?? "default"} source=${resolvedSource}` - ) - ) - yield* _(importCodexAuthDir(resolvedSource, resolvedRootPath, codexLabel ?? "")) - yield* _( - Console.log(`codex import done label=${codexLabel?.trim() ?? "default"}`) - ) - return HttpServerResponse.redirect("/integrations") - }).pipe(Effect.catchAll(htmlErrorResponse)) - ), - HttpRouter.post( - "/integrations/codex/login", - Effect.gen(function* (_) { - const { codexLabel } = yield* _(HttpServerRequest.schemaBodyUrlParams(CodexCliLoginSchema)) - yield* _( - Console.log(`codex login requested label=${codexLabel?.trim() ?? "default"}`) - ) - const html = renderCodexLoginPage(codexLabel ?? null, terminalPort) - return HttpServerResponse.html(html) - }).pipe(Effect.catchAll(htmlErrorResponse)) - ), - HttpRouter.post( - "/integrations/codex/disconnect", - Effect.gen(function* (_) { - const { codexLabel } = yield* _(HttpServerRequest.schemaBodyUrlParams(CodexConnectSchema)) - const path = yield* _(Path.Path) - const resolvedRoot = path.resolve(projectsRoot) - const codexRootPath = yield* _(resolveWritableCodexRoot(resolvedRoot)) - const expectedRoot = resolveCodexAuthPath(resolvedRoot) - if (path.resolve(codexRootPath) !== path.resolve(expectedRoot)) { - yield* _(Console.log(`codex auth root fallback: ${expectedRoot} -> ${codexRootPath}`)) - } - yield* _( - Console.log(`codex disconnect label=${codexLabel?.trim() ?? "default"}`) - ) - yield* _(removeCodexAccount(codexRootPath, codexLabel ?? "default")) - return HttpServerResponse.redirect("/integrations") - }).pipe(Effect.catchAll(htmlErrorResponse)) - ), - HttpRouter.post( - "/clone", - Effect.gen(function* (_) { - const { repoUrl, repoRef, githubLabel } = yield* _( - HttpServerRequest.schemaBodyUrlParams(CloneFormSchema) - ) - const trimmedRepoUrl = repoUrl.trim() - if (trimmedRepoUrl.length === 0) { - return HttpServerResponse.html( - renderOutputPage("Clone error", "Repo URL is required.") - ) - } - - yield* _( - Console.log( - `clone request repo=${trimmedRepoUrl} ref=${repoRef?.trim() ?? ""} label=${githubLabel?.trim() ?? ""}` - ) - ) - - const path = yield* _(Path.Path) - const resolvedRoot = path.resolve(projectsRoot) - const codexRootPath = yield* _(resolveWritableCodexRoot(resolvedRoot)) - const expectedCodexRoot = resolveCodexAuthPath(resolvedRoot) - const resolvedCodexRoot = path.resolve(codexRootPath) - if (resolvedCodexRoot !== path.resolve(expectedCodexRoot)) { - yield* _(Console.log(`codex auth root fallback: ${expectedCodexRoot} -> ${resolvedCodexRoot}`)) - } - const repoSlug = deriveRepoSlug(trimmedRepoUrl) - const projectCodexPath = resolveProjectCodexAuthPath(resolvedCodexRoot, repoSlug) - const outDir = path.join(resolvedRoot, repoSlug) - const trimmedRepoRef = repoRef?.trim() ?? "" - const index = yield* _(scanProjects(projectsRoot, cwd)) - const usedPorts = index.projects.map((project) => project.sshPort) - const selectedPort = chooseSshPort(defaultTemplateConfig.sshPort, usedPorts) - const raw: RawOptions = { - repoUrl: trimmedRepoUrl, - codexAuthPath: projectCodexPath, - outDir, - up: false, - ...(selectedPort !== defaultTemplateConfig.sshPort - ? { sshPort: String(selectedPort) } - : {}), - ...(trimmedRepoRef.length > 0 ? { repoRef: trimmedRepoRef } : {}) - } - - const parsed = buildCreateCommand(raw) - - return yield* _( - Either.match(parsed, { - onLeft: (error) => - Effect.succeed( - HttpServerResponse.html( - renderOutputPage("Invalid clone request", formatParseError(error)) - ) - ), - onRight: (create) => - Effect.gen(function* (_) { - yield* _(createProject(create)) - const project = yield* _(loadProject(projectsRoot, repoSlug, cwd)) - const selectedLabel = githubLabel?.trim() ?? "" - if (selectedLabel.length > 0) { - const globalEnvPath = resolveGlobalEnvPath(resolvedRoot) - const envText = yield* _(readEnvFile(globalEnvPath)) - const tokens = listGithubTokens(envText) - const selected = findGithubTokenByLabel(tokens, selectedLabel) - if (selected === null) { - return HttpServerResponse.html( - renderOutputPage( - "GitHub token not found", - `Label not found: ${selectedLabel}` - ) - ) - } - const projectEnv = yield* _(readEnvFile(project.envProjectPath)) - const withGitToken = upsertEnvKey( - projectEnv, - "GIT_AUTH_TOKEN", - selected.token - ) - const withGhToken = upsertEnvKey(withGitToken, "GH_TOKEN", selected.token) - const withGitLabel = upsertEnvKey(withGhToken, projectGitLabelKey, "") - const nextProjectEnv = upsertEnvKey(withGitLabel, projectGithubLabelKey, selected.label) - yield* _(writeEnvFile(project.envProjectPath, nextProjectEnv)) - } - yield* _(syncProjectCodexAuth(projectsRoot, project)) - yield* _(clearDeploymentLogs(project.id)) - yield* _(setDeploymentStatus(project.id, "build", "docker compose --progress=plain build")) - yield* _(appendDeploymentLog(project.id, "$ docker compose --progress=plain build")) - yield* _( - runComposeWithStatus( - project.directory, - ["--progress", "plain", "build"], - [0], - project.id, - "build" - ).pipe( - Effect.tapError(() => - setDeploymentStatus(project.id, "error", "docker compose build failed") - ) - ) - ) - yield* _(setDeploymentStatus(project.id, "up", "docker compose up -d")) - yield* _(appendDeploymentLog(project.id, "$ docker compose up -d")) - yield* _( - runComposeWithStatus( - project.directory, - ["up", "-d"], - [0], - project.id, - "up" - ).pipe( - Effect.tapError(() => - setDeploymentStatus(project.id, "error", "docker compose up failed") - ) - ) - ) - yield* _(setDeploymentStatus(project.id, "running", "Container running")) - const index = yield* _(scanProjects(projectsRoot, cwd)) - const html = renderDashboard( - index, - `Clone completed for ${repoSlug}.` - ) - return HttpServerResponse.html(html) - }) - }) - ) - }).pipe(Effect.catchAll(htmlErrorResponse)) - ) - ) - - const uiRouter = uiRouterBase.pipe( - HttpRouter.get( - "/terminal/:projectId", - pipe( - projectParams, - Effect.flatMap(({ projectId }) => loadProject(projectsRoot, projectId, cwd)), - Effect.map((project) => renderTerminalPage(project, terminalPort)), - Effect.map((html) => HttpServerResponse.html(html)), - Effect.catchAll(htmlErrorResponse) - ) - ), - HttpRouter.get( - "/env/:projectId", - pipe( - projectParams, - Effect.flatMap(({ projectId }) => loadProject(projectsRoot, projectId, cwd)), - Effect.flatMap((project) => - Effect.gen(function* (_) { - const path = yield* _(Path.Path) - const resolvedRoot = path.resolve(projectsRoot) - const integrationsEnvPath = resolveGlobalEnvPath(resolvedRoot) - const codexRootPath = yield* _(resolveWritableCodexRoot(resolvedRoot)) - const data = yield* _( - Effect.all({ - project: Effect.succeed(project), - globalEnv: readEnvFile(project.envGlobalPath), - projectEnv: readEnvFile(project.envProjectPath), - integrationsEnv: readEnvFile(integrationsEnvPath), - codexAccounts: listCodexAccounts(codexRootPath), - codexProject: readCodexAuthStatus(project.codexAuthPath) - }) - ) - return data - }) - ), - Effect.flatMap(({ project, globalEnv, projectEnv, integrationsEnv, codexAccounts, codexProject }) => - Effect.sync(() => { - const githubTokenEntries = countKeyEntries(integrationsEnv, "GITHUB_TOKEN") - const gitTokenEntries = countKeyEntries(integrationsEnv, "GIT_AUTH_TOKEN") - const claudeKeyEntries = countKeyEntries(integrationsEnv, "ANTHROPIC_API_KEY") - - const projectGithubLabel = resolveProjectGithubLabel(projectEnv) - const projectGithubToken = resolveProjectGithubToken(projectEnv) - const activeGithubLabel = projectGithubLabel ?? (projectGithubToken === null ? null : "custom") - - const activeProjectGitLabel = resolveProjectGitLabel(projectEnv) - const activeProjectGitToken = resolveProjectGitToken(projectEnv) - const activeGitLabel = activeProjectGitLabel ?? (activeProjectGitToken === null ? null : "custom") - - const activeProjectClaudeLabel = resolveProjectClaudeLabel(projectEnv) - const activeProjectClaudeApiKey = resolveProjectClaudeApiKey(projectEnv) - const activeClaudeLabel = activeProjectClaudeLabel ?? (activeProjectClaudeApiKey === null ? null : "custom") - - const codexLabel = resolveProjectCodexLabel(projectEnv) - const activeCodexLabel = codexLabel ?? (codexProject.connected ? "custom" : null) - return renderEnvPage( - project, - globalEnv, - projectEnv, - githubTokenEntries, - activeGithubLabel, - gitTokenEntries, - activeGitLabel, - claudeKeyEntries, - activeClaudeLabel, - codexAccounts, - activeCodexLabel - ) - }) - ), - Effect.map((html) => HttpServerResponse.html(html)), - Effect.catchAll(htmlErrorResponse) - ) - ), - HttpRouter.post( - "/env/:projectId", - Effect.gen(function* (_) { - const { projectId } = yield* _(projectParams) - const { globalEnv, projectEnv } = yield* _(HttpServerRequest.schemaBodyUrlParams(EnvFormSchema)) - const project = yield* _(loadProject(projectsRoot, projectId, cwd)) - yield* _(writeEnvFile(project.envGlobalPath, globalEnv)) - yield* _(writeEnvFile(project.envProjectPath, projectEnv)) - return HttpServerResponse.redirect(`/env/${encodeURIComponent(project.id)}`) - }).pipe(Effect.catchAll(htmlErrorResponse)) - ), - HttpRouter.post( - "/env/:projectId/git/identity", - Effect.gen(function* (_) { - const { projectId } = yield* _(projectParams) - const { gitUserName, gitUserEmail } = yield* _( - HttpServerRequest.schemaBodyUrlParams(GitIdentitySchema) - ) - const project = yield* _(loadProject(projectsRoot, projectId, cwd)) - const projectEnv = yield* _(readEnvFile(project.envProjectPath)) - const withName = upsertEnvKey(projectEnv, "GIT_USER_NAME", gitUserName) - const nextProjectEnv = upsertEnvKey(withName, "GIT_USER_EMAIL", gitUserEmail) - yield* _(writeEnvFile(project.envProjectPath, nextProjectEnv)) - return HttpServerResponse.redirect(`/env/${encodeURIComponent(project.id)}`) - }).pipe(Effect.catchAll(htmlErrorResponse)) - ), - HttpRouter.post( - "/env/:projectId/connect/github", - Effect.gen(function* (_) { - const { projectId } = yield* _(projectParams) - const { githubLabel } = yield* _( - HttpServerRequest.schemaBodyUrlParams(GithubProjectConnectSchema) - ) - const project = yield* _(loadProject(projectsRoot, projectId, cwd)) - const path = yield* _(Path.Path) - const resolvedRoot = path.resolve(projectsRoot) - const globalEnvPath = resolveGlobalEnvPath(resolvedRoot) - const globalEnv = yield* _(readEnvFile(globalEnvPath)) - const tokens = listGithubTokens(globalEnv) - const selected = findGithubTokenByLabel(tokens, githubLabel) - if (selected === null) { - return HttpServerResponse.html( - renderOutputPage("GitHub token not found", `Label not found: ${githubLabel}`) - ) - } - const projectEnv = yield* _(readEnvFile(project.envProjectPath)) - const withGitToken = upsertEnvKey(projectEnv, "GIT_AUTH_TOKEN", selected.token) - const withGhToken = upsertEnvKey(withGitToken, "GH_TOKEN", selected.token) - const withGitLabel = upsertEnvKey(withGhToken, projectGitLabelKey, "") - const nextProjectEnv = upsertEnvKey(withGitLabel, projectGithubLabelKey, selected.label) - yield* _(writeEnvFile(project.envProjectPath, nextProjectEnv)) - return HttpServerResponse.redirect(`/env/${encodeURIComponent(project.id)}`) - }).pipe(Effect.catchAll(htmlErrorResponse)) - ), - HttpRouter.post( - "/env/:projectId/disconnect/github", - Effect.gen(function* (_) { - const { projectId } = yield* _(projectParams) - yield* _(HttpServerRequest.schemaBodyUrlParams(GithubProjectDisconnectSchema)) - const project = yield* _(loadProject(projectsRoot, projectId, cwd)) - const projectEnv = yield* _(readEnvFile(project.envProjectPath)) - const withoutGitToken = upsertEnvKey(projectEnv, "GIT_AUTH_TOKEN", "") - const withGhToken = upsertEnvKey(withoutGitToken, "GH_TOKEN", "") - const withGitLabel = upsertEnvKey(withGhToken, projectGitLabelKey, "") - const nextProjectEnv = upsertEnvKey(withGitLabel, projectGithubLabelKey, "") - yield* _(writeEnvFile(project.envProjectPath, nextProjectEnv)) - return HttpServerResponse.redirect(`/env/${encodeURIComponent(project.id)}`) - }).pipe(Effect.catchAll(htmlErrorResponse)) - ), - HttpRouter.post( - "/env/:projectId/connect/git", - Effect.gen(function* (_) { - const { projectId } = yield* _(projectParams) - const { gitLabel } = yield* _(HttpServerRequest.schemaBodyUrlParams(GitProjectConnectSchema)) - const project = yield* _(loadProject(projectsRoot, projectId, cwd)) - const path = yield* _(Path.Path) - const resolvedRoot = path.resolve(projectsRoot) - const globalEnvPath = resolveGlobalEnvPath(resolvedRoot) - const globalEnv = yield* _(readEnvFile(globalEnvPath)) - const selected = findGitCredentialByLabel(globalEnv, gitLabel) - if (selected === null) { - return HttpServerResponse.html( - renderOutputPage("Git credentials not found", `Label not found: ${gitLabel}`) - ) - } - const projectEnv = yield* _(readEnvFile(project.envProjectPath)) - const withToken = upsertEnvKey(projectEnv, "GIT_AUTH_TOKEN", selected.token) - const withUser = upsertEnvKey(withToken, "GIT_AUTH_USER", selected.user) - const withGhToken = upsertEnvKey(withUser, "GH_TOKEN", selected.token) - const withGitLabel = upsertEnvKey(withGhToken, projectGitLabelKey, selected.label) - const nextProjectEnv = upsertEnvKey(withGitLabel, projectGithubLabelKey, selected.label) - yield* _(writeEnvFile(project.envProjectPath, nextProjectEnv)) - return HttpServerResponse.redirect(`/env/${encodeURIComponent(project.id)}`) - }).pipe(Effect.catchAll(htmlErrorResponse)) - ), - HttpRouter.post( - "/env/:projectId/disconnect/git", - Effect.gen(function* (_) { - const { projectId } = yield* _(projectParams) - yield* _(HttpServerRequest.schemaBodyUrlParams(GitProjectDisconnectSchema)) - const project = yield* _(loadProject(projectsRoot, projectId, cwd)) - const projectEnv = yield* _(readEnvFile(project.envProjectPath)) - const withoutToken = upsertEnvKey(projectEnv, "GIT_AUTH_TOKEN", "") - const withoutUser = upsertEnvKey(withoutToken, "GIT_AUTH_USER", "") - const withoutGhToken = upsertEnvKey(withoutUser, "GH_TOKEN", "") - const withGitLabel = upsertEnvKey(withoutGhToken, projectGitLabelKey, "") - const nextProjectEnv = upsertEnvKey(withGitLabel, projectGithubLabelKey, "") - yield* _(writeEnvFile(project.envProjectPath, nextProjectEnv)) - return HttpServerResponse.redirect(`/env/${encodeURIComponent(project.id)}`) - }).pipe(Effect.catchAll(htmlErrorResponse)) - ), - HttpRouter.post( - "/env/:projectId/connect/claude", - Effect.gen(function* (_) { - const { projectId } = yield* _(projectParams) - const { claudeLabel } = yield* _( - HttpServerRequest.schemaBodyUrlParams(ClaudeProjectConnectSchema) - ) - const project = yield* _(loadProject(projectsRoot, projectId, cwd)) - const path = yield* _(Path.Path) - const resolvedRoot = path.resolve(projectsRoot) - const globalEnvPath = resolveGlobalEnvPath(resolvedRoot) - const globalEnv = yield* _(readEnvFile(globalEnvPath)) - const selected = findClaudeApiKeyByLabel(globalEnv, claudeLabel) - if (selected === null) { - return HttpServerResponse.html( - renderOutputPage("Claude key not found", `Label not found: ${claudeLabel}`) - ) - } - const projectEnv = yield* _(readEnvFile(project.envProjectPath)) - const withApiKey = upsertEnvKey(projectEnv, "ANTHROPIC_API_KEY", selected.apiKey) - const nextProjectEnv = upsertEnvKey(withApiKey, projectClaudeLabelKey, selected.label) - yield* _(writeEnvFile(project.envProjectPath, nextProjectEnv)) - return HttpServerResponse.redirect(`/env/${encodeURIComponent(project.id)}`) - }).pipe(Effect.catchAll(htmlErrorResponse)) - ), - HttpRouter.post( - "/env/:projectId/disconnect/claude", - Effect.gen(function* (_) { - const { projectId } = yield* _(projectParams) - yield* _(HttpServerRequest.schemaBodyUrlParams(ClaudeProjectDisconnectSchema)) - const project = yield* _(loadProject(projectsRoot, projectId, cwd)) - const projectEnv = yield* _(readEnvFile(project.envProjectPath)) - const withoutApiKey = upsertEnvKey(projectEnv, "ANTHROPIC_API_KEY", "") - const nextProjectEnv = upsertEnvKey(withoutApiKey, projectClaudeLabelKey, "") - yield* _(writeEnvFile(project.envProjectPath, nextProjectEnv)) - return HttpServerResponse.redirect(`/env/${encodeURIComponent(project.id)}`) - }).pipe(Effect.catchAll(htmlErrorResponse)) - ), - HttpRouter.post( - "/env/:projectId/connect/codex", - Effect.gen(function* (_) { - const { projectId } = yield* _(projectParams) - const { codexLabel } = yield* _(HttpServerRequest.schemaBodyUrlParams(CodexProjectConnectSchema)) - const project = yield* _(loadProject(projectsRoot, projectId, cwd)) - const path = yield* _(Path.Path) - const resolvedRoot = path.resolve(projectsRoot) - const resolvedTargetPath = yield* _(resolveProjectCodexTarget(projectsRoot, project)) - const targetWritable = yield* _(isWritableDirectory(resolvedTargetPath)) - if (!targetWritable) { - return HttpServerResponse.html( - renderOutputPage( - "Codex auth error", - `Target path not writable: ${resolvedTargetPath}` - ) - ) - } - const codexRootResult = yield* _(Effect.either(resolveWritableCodexRoot(resolvedRoot))) - const expectedCodexRoot = resolveCodexAuthPath(resolvedRoot) - const codexRootPath = codexRootResult._tag === "Right" ? codexRootResult.right : null - if (codexRootResult._tag === "Right") { - const resolvedCodexRoot = path.resolve(codexRootResult.right) - if (resolvedCodexRoot !== path.resolve(expectedCodexRoot)) { - yield* _(Console.log(`codex auth root fallback: ${expectedCodexRoot} -> ${resolvedCodexRoot}`)) - } - } else { - yield* _(Console.warn(`codex auth root not writable: ${expectedCodexRoot}`)) - } - - let accountPath: string | null = null - if (codexRootPath !== null) { - const accountResult = yield* _(Effect.either(findCodexAccountPath(codexRootPath, codexLabel))) - if (accountResult._tag === "Right") { - accountPath = accountResult.right - } - } - - const fallbackSource = resolveCodexSourcePath( - undefined, - process.env["HOME"], - process.env["CODEX_HOME"] - ) - const resolvedSource = accountPath - ? path.resolve(accountPath) - : fallbackSource - ? path.resolve(fallbackSource) - : null - if (resolvedSource === null) { - return HttpServerResponse.html( - renderOutputPage("Codex account not found", `Label not found: ${codexLabel}`) - ) - } - if (accountPath === null && fallbackSource) { - yield* _(Console.log(`codex attach fallback source=${resolvedSource}`)) - } - if (resolvedSource === resolvedTargetPath) { - return HttpServerResponse.redirect(`/env/${encodeURIComponent(project.id)}`) - } - const status = yield* _(readCodexAuthStatus(resolvedSource)) - if (!status.connected) { - return HttpServerResponse.html( - renderOutputPage("Codex not connected", "Connect Codex in Integrations first.") - ) - } - yield* _( - Console.log( - `codex attach label=${codexLabel} project=${project.id} source=${resolvedSource} target=${resolvedTargetPath}` - ) - ) - yield* _(copyCodexAuthDir(resolvedSource, resolvedTargetPath)) - const projectEnv = yield* _(readEnvFile(project.envProjectPath)) - const nextProjectEnv = upsertEnvKey(projectEnv, "CODEX_AUTH_LABEL", codexLabel) - yield* _(writeEnvFile(project.envProjectPath, nextProjectEnv)) - return HttpServerResponse.redirect(`/env/${encodeURIComponent(project.id)}`) - }).pipe(Effect.catchAll(htmlErrorResponse)) - ), - HttpRouter.post( - "/env/:projectId/disconnect/codex", - Effect.gen(function* (_) { - const { projectId } = yield* _(projectParams) - yield* _(HttpServerRequest.schemaBodyUrlParams(CodexProjectDisconnectSchema)) - const project = yield* _(loadProject(projectsRoot, projectId, cwd)) - const resolvedTargetPath = yield* _(resolveProjectCodexTarget(projectsRoot, project)) - const targetWritable = yield* _(isWritableDirectory(resolvedTargetPath)) - if (!targetWritable) { - return HttpServerResponse.html( - renderOutputPage( - "Codex auth error", - `Target path not writable: ${resolvedTargetPath}` - ) - ) - } - yield* _(Console.log(`codex detach project=${project.id}`)) - yield* _(clearCodexAuthDir(resolvedTargetPath)) - const projectEnv = yield* _(readEnvFile(project.envProjectPath)) - const nextProjectEnv = upsertEnvKey(projectEnv, "CODEX_AUTH_LABEL", "") - yield* _(writeEnvFile(project.envProjectPath, nextProjectEnv)) - return HttpServerResponse.redirect(`/env/${encodeURIComponent(project.id)}`) - }).pipe(Effect.catchAll(htmlErrorResponse)) - ) - ) - - const uiRouterWithActions = uiRouter.pipe( - HttpRouter.post( - "/actions/:projectId/up", - pipe( - projectParams, - Effect.flatMap(({ projectId }) => loadProject(projectsRoot, projectId, cwd)), - Effect.tap((project) => Console.log(`deploy up: ${project.id}`)), - Effect.flatMap((project) => - Effect.gen(function* (_) { - const run = Effect.gen(function* (_) { - yield* _(clearDeploymentLogs(project.id)) - yield* _(syncProjectCodexAuth(projectsRoot, project)) - yield* _(setDeploymentStatus(project.id, "build", "docker compose --progress=plain build")) - yield* _(appendDeploymentLog(project.id, "$ docker compose --progress=plain build")) - yield* _( - runComposeWithStatus( - project.directory, - ["--progress", "plain", "build"], - [0], - project.id, - "build" - ).pipe( - Effect.tapError(() => - setDeploymentStatus(project.id, "error", "docker compose build failed") - ) - ) - ) - yield* _(setDeploymentStatus(project.id, "up", "docker compose up -d")) - yield* _(appendDeploymentLog(project.id, "$ docker compose up -d")) - yield* _( - runComposeWithStatus( - project.directory, - ["up", "-d"], - [0], - project.id, - "up" - ).pipe( - Effect.tapError(() => - setDeploymentStatus(project.id, "error", "docker compose up failed") - ) - ) - ) - yield* _(setDeploymentStatus(project.id, "running", "Container running")) - }) - yield* _(withDeploymentGuard(project, run)) - const index = yield* _(scanProjects(projectsRoot, cwd)) - return renderDashboard(index, "Docker compose up started.") - }) - ), - Effect.map((html) => HttpServerResponse.html(html)), - Effect.tapError((error) => Console.error(`deploy up failed: ${String(error)}`)), - Effect.catchAll(htmlErrorResponse) - ) - ), - HttpRouter.post( - "/actions/:projectId/down", - pipe( - projectParams, - Effect.flatMap(({ projectId }) => loadProject(projectsRoot, projectId, cwd)), - Effect.tap((project) => Console.log(`deploy down: ${project.id}`)), - Effect.flatMap((project) => - Effect.gen(function* (_) { - const run = Effect.gen(function* (_) { - yield* _(clearDeploymentLogs(project.id)) - yield* _(appendDeploymentLog(project.id, "$ docker compose down")) - yield* _(setDeploymentStatus(project.id, "down", "docker compose down")) - yield* _( - runComposeWithStatus( - project.directory, - ["down"], - [0], - project.id, - "down" - ).pipe( - Effect.tapError(() => - setDeploymentStatus(project.id, "error", "docker compose down failed") - ) - ) - ) - yield* _(setDeploymentStatus(project.id, "idle", "Container stopped")) - }) - yield* _(withDeploymentGuard(project, run)) - const index = yield* _(scanProjects(projectsRoot, cwd)) - return renderDashboard(index, "Docker compose down started.") - }) - ), - Effect.map((html) => HttpServerResponse.html(html)), - Effect.tapError((error) => Console.error(`deploy down failed: ${String(error)}`)), - Effect.catchAll(htmlErrorResponse) - ) - ), - HttpRouter.post( - "/actions/:projectId/recreate", - pipe( - projectParams, - Effect.flatMap(({ projectId }) => loadProject(projectsRoot, projectId, cwd)), - Effect.tap((project) => Console.log(`deploy recreate: ${project.id}`)), - Effect.flatMap((project) => - Effect.gen(function* (_) { - const run = Effect.gen(function* (_) { - yield* _(clearDeploymentLogs(project.id)) - const config = yield* _(readProjectConfig(project.directory)) - const index = yield* _(scanProjects(projectsRoot, cwd)) - const usedPorts = index.projects - .filter((entry) => entry.id !== project.id) - .map((entry) => entry.sshPort) - const selectedPort = chooseSshPort(config.template.sshPort, usedPorts) - const nextTemplate = selectedPort === config.template.sshPort - ? config.template - : { ...config.template, sshPort: selectedPort } - if (selectedPort !== config.template.sshPort) { - yield* _( - Console.log( - `ssh port reassigned for ${project.id}: ${config.template.sshPort} -> ${selectedPort}` - ) - ) - } - yield* _(createProject({ - _tag: "Create", - config: nextTemplate, - outDir: project.directory, - runUp: false, - openSsh: false, - force: true, - forceEnv: false, - waitForClone: false - })) - yield* _(syncProjectCodexAuth(projectsRoot, project)) - yield* _(setDeploymentStatus(project.id, "down", "docker compose down")) - yield* _(appendDeploymentLog(project.id, "$ docker compose down")) - yield* _( - runComposeWithStatus(project.directory, ["down"], [0], project.id, "down").pipe( - Effect.tapError(() => - setDeploymentStatus(project.id, "error", "docker compose down failed") - ) - ) - ) - yield* _(setDeploymentStatus(project.id, "build", "docker compose --progress=plain build")) - yield* _(appendDeploymentLog(project.id, "$ docker compose --progress=plain build")) - yield* _( - runComposeWithStatus( - project.directory, - ["--progress", "plain", "build"], - [0], - project.id, - "build" - ).pipe( - Effect.tapError(() => - setDeploymentStatus(project.id, "error", "docker compose build failed") - ) - ) - ) - yield* _(setDeploymentStatus(project.id, "up", "docker compose up -d")) - yield* _(appendDeploymentLog(project.id, "$ docker compose up -d")) - yield* _( - runComposeWithStatus(project.directory, ["up", "-d"], [0], project.id, "up").pipe( - Effect.tapError(() => - setDeploymentStatus(project.id, "error", "docker compose up failed") - ) - ) - ) - yield* _(setDeploymentStatus(project.id, "running", "Container running")) - }) - yield* _(withDeploymentGuard(project, run)) - const index = yield* _(scanProjects(projectsRoot, cwd)) - return renderDashboard(index, "Docker compose recreate started.") - }).pipe( - Effect.tapError(() => - setDeploymentStatus(project.id, "error", "docker compose recreate failed") - ) - ) - ), - Effect.map((html) => HttpServerResponse.html(html)), - Effect.tapError((error) => Console.error(`deploy recreate failed: ${String(error)}`)), - Effect.catchAll(htmlErrorResponse) - ) - ), - HttpRouter.get( - "/projects/:projectId/ps", - pipe( - projectParams, - Effect.flatMap(({ projectId }) => loadProject(projectsRoot, projectId, cwd)), - Effect.flatMap((project) => readDockerComposePs(project.directory)), - Effect.map((output) => renderOutputPage("docker compose ps", output)), - Effect.map((html) => HttpServerResponse.html(html)), - Effect.catchAll(htmlErrorResponse) - ) - ), - HttpRouter.get( - "/projects/:projectId/logs", - pipe( - projectParams, - Effect.flatMap(({ projectId }) => loadProject(projectsRoot, projectId, cwd)), - Effect.flatMap((project) => readDockerComposeLogs(project.directory)), - Effect.map((output) => renderOutputPage("docker compose logs", output)), - Effect.map((html) => HttpServerResponse.html(html)), - Effect.catchAll(htmlErrorResponse) - ) - ), - HttpRouter.get( - "/deployments/:projectId/logs", - pipe( - projectParams, - Effect.flatMap(({ projectId }) => - Effect.gen(function* (_) { - const entries = yield* _(listDeploymentLogs(projectId)) - const status = yield* _(getDeploymentStatus(projectId)) - const output = entries - .map((entry) => `${entry.timestamp} ${entry.line}`) - .join("\n") - return renderDeployLogsPage( - projectId, - output, - status.phase, - status.message, - status.updatedAt - ) - }) - ), - Effect.map((html) => HttpServerResponse.html(html)), - Effect.catchAll(htmlErrorResponse) - ) - ), - HttpRouter.get("/api/health", jsonResponse({ ok: true }, 200)), - HttpRouter.get( - "/api/deployments", - pipe( - listDeploymentStatuses(), - Effect.flatMap((deployments) => jsonResponse({ deployments }, 200)), - Effect.catchAll(errorResponse) - ) - ), - HttpRouter.get( - "/api/deployments/:projectId/logs", - pipe( - projectParams, - Effect.flatMap(({ projectId }) => - Effect.gen(function* (_) { - const entries = yield* _(listDeploymentLogs(projectId)) - const status = yield* _(getDeploymentStatus(projectId)) - return { entries, status } - }) - ), - Effect.flatMap((payload) => jsonResponse(payload, 200)), - Effect.catchAll(errorResponse) - ) - ), - HttpRouter.get( - "/api/deployments/:projectId/stream", - pipe( - projectParams, - Effect.flatMap(({ projectId }) => - Effect.gen(function* (_) { - const encoder = new TextEncoder() - const ref = yield* _(Ref.make({ index: 0, statusAt: "", snapshotSent: false })) - - const encodeEvent = (event: string, data: unknown): Uint8Array => - encoder.encode(`event: ${event} -data: ${JSON.stringify(data)} - -`) - - const poll = Effect.gen(function* (_) { - const state = yield* _(Ref.get(ref)) - const entries = yield* _(listDeploymentLogs(projectId)) - const status = yield* _(getDeploymentStatus(projectId)) - - // CHANGE: re-emit snapshot when logs reset - // WHY: keep the UI aligned with cleared deployment output - // QUOTE(ТЗ): "Да чисти логи деплоя" - // REF: user-request-2026-01-15 - // SOURCE: n/a - // FORMAT THEOREM: forall reset: snapshot(reset) -> ui(logs) = empty - // PURITY: SHELL - // EFFECT: Effect, never, never> - // INVARIANT: snapshot is sent when log length drops - // COMPLEXITY: O(1) - const resetDetected = state.snapshotSent && entries.length < state.index - - if (!state.snapshotSent || resetDetected) { - yield* _(Ref.set(ref, { - index: entries.length, - statusAt: status.updatedAt, - snapshotSent: true - })) - return Chunk.of(encodeEvent("snapshot", { entries, status })) - } - - const nextEntries = entries.slice(state.index) - const events: Array = [] - if (nextEntries.length > 0) { - for (const entry of nextEntries) { - events.push(encodeEvent("log", entry)) - } - } - - if (status.updatedAt !== state.statusAt) { - events.push(encodeEvent("status", status)) - } - - yield* _(Ref.set(ref, { - index: entries.length, - statusAt: status.updatedAt, - snapshotSent: true - })) - - if (events.length === 0) { - yield* _(Effect.sleep(Duration.millis(500))) - return Chunk.empty() - } - - yield* _(Effect.sleep(Duration.millis(200))) - return Chunk.fromIterable(events) - }) - - const stream = Stream.repeatEffectChunk(poll) - - return HttpServerResponse.stream(stream, { - headers: { - "content-type": "text/event-stream", - "cache-control": "no-cache", - "connection": "keep-alive" - } - }) - }) - ), - Effect.catchAll(errorResponse) - ) - ), - HttpRouter.get( - "/api/projects", - pipe( - scanProjects(projectsRoot, cwd), - Effect.flatMap((index) => jsonResponse(index, 200)), - Effect.catchAll(errorResponse) - ) - ), - HttpRouter.get( - "/api/projects/:projectId", - pipe( - projectParams, - Effect.flatMap(({ projectId }) => loadProject(projectsRoot, projectId, cwd)), - Effect.flatMap((project) => jsonResponse({ project }, 200)), - Effect.catchAll(errorResponse) - ) - ) - ) - - return uiRouterWithActions.pipe( - HttpRouter.post( - "/api/projects/:projectId/up", - pipe( - projectParams, - Effect.flatMap(({ projectId }) => loadProject(projectsRoot, projectId, cwd)), - Effect.flatMap((project) => - Effect.gen(function* (_) { - const run = Effect.gen(function* (_) { - yield* _(clearDeploymentLogs(project.id)) - yield* _(syncProjectCodexAuth(projectsRoot, project)) - yield* _(setDeploymentStatus(project.id, "build", "docker compose --progress=plain build")) - yield* _(appendDeploymentLog(project.id, "$ docker compose --progress=plain build")) - yield* _( - runComposeWithStatus( - project.directory, - ["--progress", "plain", "build"], - [0], - project.id, - "build" - ).pipe( - Effect.tapError(() => - setDeploymentStatus(project.id, "error", "docker compose build failed") - ) - ) - ) - yield* _(setDeploymentStatus(project.id, "up", "docker compose up -d")) - yield* _(appendDeploymentLog(project.id, "$ docker compose up -d")) - yield* _( - runComposeWithStatus( - project.directory, - ["up", "-d"], - [0], - project.id, - "up" - ).pipe( - Effect.tapError(() => - setDeploymentStatus(project.id, "error", "docker compose up failed") - ) - ) - ) - yield* _(setDeploymentStatus(project.id, "running", "Container running")) - }) - yield* _(withDeploymentGuard(project, run)) - return { ok: true, started: true } - }) - ), - Effect.flatMap((payload) => jsonResponse(payload, 200)), - Effect.catchAll(errorResponse) - ) - ), - HttpRouter.post( - "/api/projects/:projectId/down", - pipe( - projectParams, - Effect.flatMap(({ projectId }) => loadProject(projectsRoot, projectId, cwd)), - Effect.flatMap((project) => - Effect.gen(function* (_) { - const run = Effect.gen(function* (_) { - yield* _(clearDeploymentLogs(project.id)) - yield* _(setDeploymentStatus(project.id, "down", "docker compose down")) - yield* _(appendDeploymentLog(project.id, "$ docker compose down")) - yield* _( - runComposeWithStatus( - project.directory, - ["down"], - [0], - project.id, - "down" - ).pipe( - Effect.tapError(() => - setDeploymentStatus(project.id, "error", "docker compose down failed") - ) - ) - ) - yield* _(setDeploymentStatus(project.id, "idle", "Container stopped")) - }) - yield* _(withDeploymentGuard(project, run)) - return { ok: true, started: true } - }) - ), - Effect.flatMap((payload) => jsonResponse(payload, 200)), - Effect.catchAll(errorResponse) - ) - ), - HttpRouter.post( - "/api/projects/:projectId/recreate", - pipe( - projectParams, - Effect.flatMap(({ projectId }) => loadProject(projectsRoot, projectId, cwd)), - Effect.flatMap((project) => - Effect.gen(function* (_) { - const run = Effect.gen(function* (_) { - yield* _(clearDeploymentLogs(project.id)) - const config = yield* _(readProjectConfig(project.directory)) - const index = yield* _(scanProjects(projectsRoot, cwd)) - const usedPorts = index.projects - .filter((entry) => entry.id !== project.id) - .map((entry) => entry.sshPort) - const selectedPort = chooseSshPort(config.template.sshPort, usedPorts) - const nextTemplate = selectedPort === config.template.sshPort - ? config.template - : { ...config.template, sshPort: selectedPort } - if (selectedPort !== config.template.sshPort) { - yield* _( - Console.log( - `ssh port reassigned for ${project.id}: ${config.template.sshPort} -> ${selectedPort}` - ) - ) - } - yield* _(createProject({ - _tag: "Create", - config: nextTemplate, - outDir: project.directory, - runUp: false, - openSsh: false, - force: true, - forceEnv: false, - waitForClone: false - })) - yield* _(syncProjectCodexAuth(projectsRoot, project)) - yield* _(setDeploymentStatus(project.id, "down", "docker compose down")) - yield* _(appendDeploymentLog(project.id, "$ docker compose down")) - yield* _( - runComposeWithStatus(project.directory, ["down"], [0], project.id, "down").pipe( - Effect.tapError(() => - setDeploymentStatus(project.id, "error", "docker compose down failed") - ) - ) - ) - yield* _(setDeploymentStatus(project.id, "build", "docker compose --progress=plain build")) - yield* _(appendDeploymentLog(project.id, "$ docker compose --progress=plain build")) - yield* _( - runComposeWithStatus( - project.directory, - ["--progress", "plain", "build"], - [0], - project.id, - "build" - ).pipe( - Effect.tapError(() => - setDeploymentStatus(project.id, "error", "docker compose build failed") - ) - ) - ) - yield* _(setDeploymentStatus(project.id, "up", "docker compose up -d")) - yield* _(appendDeploymentLog(project.id, "$ docker compose up -d")) - yield* _( - runComposeWithStatus(project.directory, ["up", "-d"], [0], project.id, "up").pipe( - Effect.tapError(() => - setDeploymentStatus(project.id, "error", "docker compose up failed") - ) - ) - ) - yield* _(setDeploymentStatus(project.id, "running", "Container running")) - }).pipe( - Effect.tapError(() => - setDeploymentStatus(project.id, "error", "docker compose recreate failed") - ) - ) - yield* _(withDeploymentGuard(project, run)) - return { ok: true, started: true } - }) - ), - Effect.flatMap((payload) => jsonResponse(payload, 200)), - Effect.catchAll(errorResponse) - ) - ), - HttpRouter.get( - "/api/projects/:projectId/ps", - pipe( - projectParams, - Effect.flatMap(({ projectId }) => loadProject(projectsRoot, projectId, cwd)), - Effect.flatMap((project) => readDockerComposePs(project.directory)), - Effect.flatMap((output) => jsonResponse({ output }, 200)), - Effect.catchAll(errorResponse) - ) - ), - HttpRouter.get( - "/api/projects/:projectId/logs", - pipe( - projectParams, - Effect.flatMap(({ projectId }) => loadProject(projectsRoot, projectId, cwd)), - Effect.flatMap((project) => readDockerComposeLogs(project.directory)), - Effect.flatMap((output) => jsonResponse({ output }, 200)), - Effect.catchAll(errorResponse) - ) - ), - HttpRouter.get("/styles.css", pipe(serveFile(`${webRoot}/styles.css`), Effect.catchAll(errorResponse))), - HttpRouter.get("/terminal.js", pipe(serveFile(`${webRoot}/terminal.js`), Effect.catchAll(errorResponse))), - HttpRouter.get("/deploy.js", pipe(serveFile(`${webRoot}/deploy.js`), Effect.catchAll(errorResponse))), - HttpRouter.get( - "/deploy-logs.js", - pipe(serveFile(`${webRoot}/deploy-logs.js`), Effect.catchAll(errorResponse)) - ), - HttpRouter.get( - "/vendor/xterm.js", - pipe(serveFile(`${vendorRoot}/xterm/lib/xterm.js`), Effect.catchAll(errorResponse)) - ), - HttpRouter.get( - "/vendor/xterm.css", - pipe(serveFile(`${vendorRoot}/xterm/css/xterm.css`), Effect.catchAll(errorResponse)) - ) - ) -} diff --git a/packages/docker-git/src/server/labeled-env.ts b/packages/docker-git/src/server/labeled-env.ts deleted file mode 100644 index fd3c0536..00000000 --- a/packages/docker-git/src/server/labeled-env.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { parseEnvEntries } from "./core/env.js" - -export interface LabeledEnvEntry { - readonly key: string - readonly label: string - readonly value: string -} - -const normalizeLabel = (value: string): string => { - const trimmed = value.trim() - if (trimmed.length === 0) { - return "" - } - const normalized = trimmed - .toUpperCase() - .replace(/[^A-Z0-9]+/g, "_") - .replace(/^_+/, "") - .replace(/_+$/, "") - return normalized.length > 0 ? normalized : "" -} - -const buildLabelPrefix = (baseKey: string): string => `${baseKey}__` - -const resolveLabelFromKey = ( - baseKey: string, - key: string -): string | null => { - if (key === baseKey) { - return "default" - } - const prefix = buildLabelPrefix(baseKey) - if (!key.startsWith(prefix)) { - return null - } - const rawLabel = key.slice(prefix.length) - const normalized = normalizeLabel(rawLabel) - return normalized.length > 0 ? normalized : null -} - -// CHANGE: build a normalized env key for labeled secrets -// WHY: keep key resolution deterministic across services (Git / Claude / etc) -// QUOTE(ТЗ): "реализовать систему где я могу задавать N множества ключей" -// REF: issue-61 -// SOURCE: n/a -// FORMAT THEOREM: forall b,l: key(b,l) = b | b__LABEL -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: empty/default labels resolve to the base key -// COMPLEXITY: O(|l|) -export const buildLabeledEnvKey = (baseKey: string, label: string): string => { - const normalized = normalizeLabel(label) - if (normalized.length === 0 || normalized === "DEFAULT") { - return baseKey - } - return `${buildLabelPrefix(baseKey)}${normalized}` -} - -// CHANGE: list labeled env entries for a base key -// WHY: support multiple credential sets under one service namespace -// QUOTE(ТЗ): "задавать N множества ключей" -// REF: issue-61 -// SOURCE: n/a -// FORMAT THEOREM: forall e,b: list(e,b) subset assignments(e) -// PURITY: CORE -// EFFECT: Effect, never, never> -// INVARIANT: only non-empty values are returned -// COMPLEXITY: O(n) where n = |entries| -export const listLabeledEnvEntries = ( - envText: string, - baseKey: string -): ReadonlyArray => - parseEnvEntries(envText) - .flatMap((entry) => { - const label = resolveLabelFromKey(baseKey, entry.key) - if (label === null) { - return [] - } - return [{ - key: entry.key, - label, - value: entry.value - }] - }) - .filter((entry) => entry.value.trim().length > 0) - -// CHANGE: find a labeled entry by label -// WHY: map UI-selected labels to stored env values -// QUOTE(ТЗ): "возможность выбора" -// REF: issue-61 -// SOURCE: n/a -// FORMAT THEOREM: forall s,l: find(s,l) -> entry(l) | null -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: label normalization matches buildLabeledEnvKey -// COMPLEXITY: O(n) where n = |entries| -export const findLabeledEnvEntryByLabel = ( - entries: ReadonlyArray, - baseKey: string, - label: string -): LabeledEnvEntry | null => { - const key = buildLabeledEnvKey(baseKey, label) - return entries.find((entry) => entry.key === key) ?? null -} - -// CHANGE: resolve label for a concrete secret value -// WHY: show active labeled profile in project settings -// QUOTE(ТЗ): "возможность выбора" -// REF: issue-61 -// SOURCE: n/a -// FORMAT THEOREM: forall s,v: resolve(s,v) -> label(v) | null -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: exact value match -// COMPLEXITY: O(n) where n = |entries| -export const resolveLabeledEnvLabelForValue = ( - entries: ReadonlyArray, - value: string -): string | null => { - const match = entries.find((entry) => entry.value === value) - return match ? match.label : null -} diff --git a/packages/docker-git/src/server/main.ts b/packages/docker-git/src/server/main.ts deleted file mode 100644 index 3b08c408..00000000 --- a/packages/docker-git/src/server/main.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NodeContext, NodeRuntime } from "@effect/platform-node" -import { Effect } from "effect" - -import { program } from "./program.js" - -// CHANGE: launch the docker-git web server via NodeRuntime -// WHY: run the HTTP program with platform services and proper shutdown -// QUOTE(ТЗ): "Просто сделай сайт и бекенд приложение" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall env: runMain(program, env) -> server running -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: program executed exactly once -// COMPLEXITY: O(1) -NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer))) diff --git a/packages/docker-git/src/server/ports.ts b/packages/docker-git/src/server/ports.ts deleted file mode 100644 index 847abc17..00000000 --- a/packages/docker-git/src/server/ports.ts +++ /dev/null @@ -1,178 +0,0 @@ -import * as Chunk from "effect/Chunk" -import * as Data from "effect/Data" -import { Effect, pipe } from "effect" -import type { PlatformError } from "@effect/platform/Error" -import * as Command from "@effect/platform/Command" -import * as CommandExecutor from "@effect/platform/CommandExecutor" -import * as Stream from "effect/Stream" - -export class PortCommandError extends Data.TaggedError("PortCommandError")<{ - readonly command: string - readonly exitCode: number -}> {} - -export class PortInUseError extends Data.TaggedError("PortInUseError")<{ - readonly port: number - readonly pids: ReadonlyArray -}> {} - -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 runCommandCapture = ( - command: Command.Command, - okExitCodes: ReadonlyArray, - label: string -): Effect.Effect => - Effect.scoped( - Effect.gen(function* (_) { - const executor = yield* _(CommandExecutor.CommandExecutor) - const process = yield* _(executor.start(command)) - const bytes = yield* _(pipe(process.stdout, Stream.runCollect, Effect.map(collectUint8Array))) - const exitCode = yield* _(process.exitCode) - const numericExitCode = Number(exitCode) - if (!okExitCodes.includes(numericExitCode)) { - return yield* _( - Effect.fail(new PortCommandError({ command: label, exitCode: numericExitCode })) - ) - } - return new TextDecoder("utf-8").decode(bytes) - }) - ) - -const parsePidsForPort = (output: string, port: number): ReadonlyArray => { - const matches = output - .split("\n") - .filter((line) => line.includes(`:${port}`)) - .flatMap((line) => [...line.matchAll(/pid=(\d+)/g)].map((match) => Number(match[1]))) - .filter((pid) => Number.isFinite(pid)) - return Array.from(new Set(matches)) -} - -const readListeningPids = ( - port: number -): Effect.Effect< - ReadonlyArray, - PortCommandError | PlatformError, - CommandExecutor.CommandExecutor -> => { - const command = pipe(Command.make("ss", "-ltnp"), Command.stdout("pipe"), Command.stderr("pipe")) - return runCommandCapture(command, [0], "ss -ltnp").pipe( - Effect.map((output) => parsePidsForPort(output, port)) - ) -} - -const readProcessCommand = ( - pid: number -): Effect.Effect => { - const command = pipe( - Command.make("ps", "-p", String(pid), "-o", "cmd="), - Command.stdout("pipe"), - Command.stderr("pipe") - ) - return runCommandCapture(command, [0, 1], "ps") -} - -const readProcessCwd = ( - pid: number -): Effect.Effect => { - const command = pipe( - Command.make("pwdx", String(pid)), - Command.stdout("pipe"), - Command.stderr("pipe") - ) - return runCommandCapture(command, [0, 1], "pwdx").pipe( - Effect.map((output) => { - const trimmed = output.trim() - if (trimmed.length === 0) { - return null - } - const parts = trimmed.split(":") - return parts.length > 1 ? parts.slice(1).join(":").trim() : null - }) - ) -} - -const killProcess = ( - pid: number -): Effect.Effect => { - const command = pipe(Command.make("kill", String(pid)), Command.stdout("pipe"), Command.stderr("pipe")) - return runCommandCapture(command, [0, 1], "kill").pipe(Effect.asVoid) -} - -const isDockerGitServerProcess = ( - command: string, - serverCwd: string, - processCwd: string | null -): boolean => { - const normalized = command.trim().toLowerCase() - if (normalized.length === 0) { - return false - } - if ( - normalized.includes("docker-git") && - (normalized.includes("dist/server/main.js") || - normalized.includes("start:server") || - normalized.includes("docker-git start:server")) - ) { - return true - } - if (!normalized.includes("dist/server/main.js")) { - return false - } - if (!processCwd || processCwd.trim().length === 0) { - return false - } - return processCwd === serverCwd -} - -const releasePort = ( - port: number, - serverCwd: string -): Effect.Effect => - Effect.gen(function* (_) { - const pids = yield* _(readListeningPids(port)) - if (pids.length === 0) { - return - } - - for (const pid of pids) { - const command = yield* _(readProcessCommand(pid)) - const processCwd = yield* _(readProcessCwd(pid)) - if (isDockerGitServerProcess(command, serverCwd, processCwd)) { - yield* _(killProcess(pid)) - } - } - - const remaining = yield* _(readListeningPids(port)) - if (remaining.length > 0) { - yield* _(Effect.fail(new PortInUseError({ port, pids: remaining }))) - } - }) - -// CHANGE: ensure docker-git ports are free before binding the HTTP/WS servers -// WHY: prevent EADDRINUSE when a previous server instance is still running -// QUOTE(ТЗ): "Почему процессы сами по себе не убиваются?" -// REF: user-request-2026-01-10 -// SOURCE: n/a -// FORMAT THEOREM: forall p in ports: free(p) -> bind(p) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: only docker-git server processes are terminated -// COMPLEXITY: O(n) where n = |ports| -export const ensurePortsFree = ( - ports: ReadonlyArray, - serverCwd: string -): Effect.Effect< - void, - PortInUseError | PortCommandError | PlatformError, - CommandExecutor.CommandExecutor -> => - Effect.forEach(ports, (port) => releasePort(port, serverCwd), { concurrency: 1 }).pipe( - Effect.asVoid - ) diff --git a/packages/docker-git/src/server/program.ts b/packages/docker-git/src/server/program.ts deleted file mode 100644 index 2979e320..00000000 --- a/packages/docker-git/src/server/program.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { HttpMiddleware, HttpServer, HttpServerRequest } from "@effect/platform" -import { NodeContext, NodeHttpServer } from "@effect/platform-node" -import { Console, Effect, Layer } from "effect" -import * as Option from "effect/Option" -import { createServer } from "node:http" - -import { resolveProjectsRoot } from "./core/domain.js" -import { makeRouter } from "./http.js" -import { ensurePortsFree } from "./ports.js" -import { attachTerminalBridge } from "./terminal.js" - -const resolvePort = (env: Record): number => { - const raw = env["DOCKER_GIT_PORT"] ?? env["PORT"] - const parsed = raw === undefined ? Number.NaN : Number(raw) - return Number.isFinite(parsed) && parsed > 0 ? parsed : 3333 -} - -const resolveTerminalPort = (env: Record, httpPort: number): number => { - const raw = env["DOCKER_GIT_TERM_PORT"] ?? env["DOCKER_GIT_WS_PORT"] - const parsed = raw === undefined ? Number.NaN : Number(raw) - const candidate = Number.isFinite(parsed) && parsed > 0 ? parsed : httpPort + 1 - return candidate === httpPort ? httpPort + 1 : candidate -} - -// CHANGE: compose the HTTP server layer for docker-git -// WHY: provide a backend runtime for the orchestration UI -// QUOTE(ТЗ): "Просто сделай сайт и бекенд приложение" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall port: launch(port) -> server_listening(port) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: router is mounted before listening -// COMPLEXITY: O(1) -export const program = (() => { - const cwd = process.cwd() - const projectsRoot = resolveProjectsRoot(cwd, process.env) - const webRoot = `${cwd}/web` - const vendorRoot = `${cwd}/node_modules` - const port = resolvePort(process.env) - const terminalPort = resolveTerminalPort(process.env, port) - - // CHANGE: add per-request logging middleware - // WHY: trace every API call to understand server behavior - // QUOTE(ТЗ): "Добавь логи на вызов каждого апи ендпоинта" - // REF: user-request-2026-01-15 - // SOURCE: n/a - // FORMAT THEOREM: forall req: log(req) -> log(res) - // PURITY: SHELL - // EFFECT: Effect - // INVARIANT: does not alter response semantics - // COMPLEXITY: O(1) - const requestLogger = HttpMiddleware.make((httpApp) => - Effect.gen(function* (_) { - const request = yield* _(HttpServerRequest.HttpServerRequest) - const start = Date.now() - const id = `${start}-${Math.floor(Math.random() * 1e6)}` - const remote = Option.getOrElse(request.remoteAddress, () => "unknown") - const userAgent = request.headers["user-agent"] ?? "" - const contentType = request.headers["content-type"] ?? "" - const contentLength = request.headers["content-length"] ?? "" - yield* _( - Console.log( - `[req ${id}] ${request.method} ${request.url} remote=${remote} ct="${contentType}" len="${contentLength}" ua="${userAgent}"` - ) - ) - return yield* _( - httpApp.pipe( - Effect.tap((response) => - Console.log( - `[res ${id}] ${request.method} ${request.url} status=${response.status} ms=${Date.now() - start}` - ) - ), - Effect.tapError((error) => - Console.error( - `[err ${id}] ${request.method} ${request.url} ${String(error)}` - ) - ) - ) - ) - }) - ) - - const router = makeRouter({ cwd, projectsRoot, webRoot, vendorRoot, terminalPort }) - const app = router.pipe(HttpServer.serve(requestLogger), HttpServer.withLogAddress) - const server = createServer() - const serverLayer = NodeHttpServer.layer(() => server, { port }) - const preflight = ensurePortsFree([port, terminalPort], cwd).pipe( - Effect.provide(NodeContext.layer) - ) - - const boot = Effect.scoped( - preflight.pipe( - Effect.tap(() => - Console.log(`docker-git ports ready http=${port} ws=${terminalPort}`) - ), - Effect.zipRight( - attachTerminalBridge(terminalPort, projectsRoot, cwd).pipe( - Effect.tap(() => Console.log(`Terminal bridge listening on ws://0.0.0.0:${terminalPort}`)), - Effect.zipRight(Layer.launch(Layer.provide(app, serverLayer))) - ) - ) - ) - ) - - const startup = Console.log( - `docker-git boot cwd=${cwd} root=${projectsRoot} web=${webRoot} vendor=${vendorRoot} http=${port} ws=${terminalPort}` - ) - - const formatError = (error: unknown): string => - error instanceof Error ? error.stack ?? error.message : String(error) - - return startup.pipe( - Effect.zipRight(boot), - Effect.tapError((error) => Console.error(`docker-git fatal: ${formatError(error)}`)) - ) -})() diff --git a/packages/docker-git/src/server/projects.ts b/packages/docker-git/src/server/projects.ts deleted file mode 100644 index 18c23745..00000000 --- a/packages/docker-git/src/server/projects.ts +++ /dev/null @@ -1,293 +0,0 @@ -import { Effect, pipe } from "effect" -import type { PlatformError } from "@effect/platform/Error" -import * as FileSystem from "@effect/platform/FileSystem" -import * as Path from "@effect/platform/Path" - -import type { ProjectConfig } from "../core/domain.js" -import { readProjectConfig } from "../shell/config.js" -import type { ConfigDecodeError, ConfigNotFoundError } from "../shell/errors.js" -import type { ProjectIssue, ProjectSummary, ProjectsIndex } from "./core/domain.js" -import { buildSshCommand, resolveSshHost } from "./core/domain.js" -import { ProjectNotFoundError } from "./errors.js" - -type ScanResult = - | { readonly _tag: "Project"; readonly summary: ProjectSummary } - | { readonly _tag: "Issue"; readonly issue: ProjectIssue } - | { readonly _tag: "Skip" } - -const makeSkip = (): ScanResult => ({ _tag: "Skip" }) - -const makeIssue = (issue: ProjectIssue): ScanResult => ({ _tag: "Issue", issue }) - -const makeProject = (summary: ProjectSummary): ScanResult => ({ _tag: "Project", summary }) - -const resolvePath = (path: Path.Path, baseDir: string, inputPath: string): string => - path.isAbsolute(inputPath) ? inputPath : path.resolve(baseDir, inputPath) - -const findExistingUpwards = ( - fs: FileSystem.FileSystem, - path: Path.Path, - startDir: string, - fileName: string, - maxDepth: number -): Effect.Effect => - Effect.gen(function* (_) { - let current = startDir - let depth = 0 - - while (true) { - const candidate = path.join(current, fileName) - const exists = yield* _(fs.exists(candidate)) - if (exists) { - return candidate - } - - const parent = path.dirname(current) - if (parent === current || depth >= maxDepth) { - return null - } - - current = parent - depth += 1 - } - }) - -const findSshPrivateKey = ( - fs: FileSystem.FileSystem, - path: Path.Path, - cwd: string -): Effect.Effect => - Effect.gen(function* (_) { - const envPath = process.env["DOCKER_GIT_SSH_KEY"]?.trim() - if (envPath && envPath.length > 0) { - const exists = yield* _(fs.exists(envPath)) - if (exists) { - return envPath - } - } - - const devKey = yield* _(findExistingUpwards(fs, path, cwd, "dev_ssh_key", 6)) - if (devKey !== null) { - return devKey - } - - const home = process.env["HOME"]?.trim() - if (home && home.length > 0) { - const ed25519 = path.join(home, ".ssh", "id_ed25519") - const edExists = yield* _(fs.exists(ed25519)) - if (edExists) { - return ed25519 - } - - const rsa = path.join(home, ".ssh", "id_rsa") - const rsaExists = yield* _(fs.exists(rsa)) - if (rsaExists) { - return rsa - } - } - - return null - }) - -const toProjectSummary = ( - dir: string, - id: string, - config: ProjectConfig, - sshHost: string, - sshKeyPath: string | null -): Effect.Effect => - Effect.gen(function* (_) { - const fs = yield* _(FileSystem.FileSystem) - const path = yield* _(Path.Path) - const authorizedKeysPath = resolvePath(path, dir, config.template.authorizedKeysPath) - const codexAuthPath = resolvePath(path, dir, config.template.codexAuthPath) - const envGlobalPath = resolvePath(path, dir, config.template.envGlobalPath) - const envProjectPath = resolvePath(path, dir, config.template.envProjectPath) - const authorizedKeysExists = yield* _(fs.exists(authorizedKeysPath)) - const envGlobalExists = yield* _(fs.exists(envGlobalPath)) - const envProjectExists = yield* _(fs.exists(envProjectPath)) - const sshCommand = buildSshCommand({ - sshUser: config.template.sshUser, - sshPort: config.template.sshPort, - sshHost, - sshKeyPath - }) - - return { - id, - directory: dir, - repoUrl: config.template.repoUrl, - repoRef: config.template.repoRef, - sshUser: config.template.sshUser, - sshPort: config.template.sshPort, - sshHost, - sshCommand, - sshKeyPath, - containerName: config.template.containerName, - serviceName: config.template.serviceName, - targetDir: config.template.targetDir, - volumeName: config.template.volumeName, - authorizedKeysPath, - authorizedKeysExists, - envGlobalPath, - envGlobalExists, - envProjectPath, - envProjectExists, - codexAuthPath, - codexHome: config.template.codexHome - } - }) - -const toIssue = ( - id: string, - error: { readonly _tag: string; readonly path: string; readonly message?: string } -): ProjectIssue => - error._tag === "ConfigNotFoundError" - ? { - _tag: "ConfigNotFound", - id, - path: error.path - } - : { - _tag: "ConfigDecode", - id, - path: error.path, - message: error.message ?? "Invalid config" - } - -const scanEntry = ( - root: string, - id: string, - sshHost: string, - sshKeyPath: string | null -): Effect.Effect< - ScanResult, - PlatformError, - FileSystem.FileSystem | Path.Path -> => - Effect.gen(function* (_) { - const fs = yield* _(FileSystem.FileSystem) - const path = yield* _(Path.Path) - const dir = path.join(root, id) - const info = yield* _(fs.stat(dir)) - if (info.type !== "Directory") { - return makeSkip() - } - - return yield* _( - pipe( - readProjectConfig(dir), - Effect.flatMap((config) => - pipe( - toProjectSummary(dir, id, config, sshHost, sshKeyPath), - Effect.map((summary) => makeProject(summary)) - ) - ), - Effect.catchAll((error) => - error._tag === "ConfigNotFoundError" || error._tag === "ConfigDecodeError" - ? Effect.succeed(makeIssue(toIssue(id, error))) - : Effect.fail(error) - ) - ) - ) - }) - -// CHANGE: scan project folders for docker-git configs -// WHY: build the web dashboard data from generated docker-git projects -// QUOTE(ТЗ): "Будет список докер образом" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall root: scan(root) -> projects(root) + issues(root) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: returns absolute root path -// COMPLEXITY: O(n) where n = |entries| -export const scanProjects = ( - projectsRoot: string, - cwd: string -): Effect.Effect => - Effect.gen(function* (_) { - const fs = yield* _(FileSystem.FileSystem) - const path = yield* _(Path.Path) - const resolvedRoot = path.resolve(projectsRoot) - const rootExists = yield* _(fs.exists(resolvedRoot)) - - if (!rootExists) { - return { - root: resolvedRoot, - exists: false, - projects: [], - issues: [] - } - } - - const entries = yield* _(fs.readDirectory(resolvedRoot)) - const sshHost = resolveSshHost(process.env) - const sshKeyPath = yield* _(findSshPrivateKey(fs, path, cwd)) - - const results = yield* _( - Effect.forEach(entries, (entry) => - scanEntry(resolvedRoot, entry, sshHost, sshKeyPath) - ) - ) - - const projects: Array = [] - const issues: Array = [] - - for (const result of results) { - if (result._tag === "Project") { - projects.push(result.summary) - } else if (result._tag === "Issue") { - issues.push(result.issue) - } - } - - return { - root: resolvedRoot, - exists: true, - projects, - issues - } - }) - -// CHANGE: load a single docker-git project summary -// WHY: provide a typed API for per-project actions -// QUOTE(ТЗ): "вижу всю инфу по ним" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall id: exists(id) -> summary(id) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: returns absolute directory paths -// COMPLEXITY: O(1) -export const loadProject = ( - projectsRoot: string, - id: string, - cwd: string -): Effect.Effect< - ProjectSummary, - ProjectNotFoundError | ConfigNotFoundError | ConfigDecodeError | PlatformError, - FileSystem.FileSystem | Path.Path -> => - Effect.gen(function* (_) { - const fs = yield* _(FileSystem.FileSystem) - const path = yield* _(Path.Path) - const resolvedRoot = path.resolve(projectsRoot) - const dir = path.join(resolvedRoot, id) - const exists = yield* _(fs.exists(dir)) - - if (!exists) { - return yield* _(Effect.fail(new ProjectNotFoundError({ id, root: resolvedRoot }))) - } - - const info = yield* _(fs.stat(dir)) - if (info.type !== "Directory") { - return yield* _(Effect.fail(new ProjectNotFoundError({ id, root: resolvedRoot }))) - } - - const sshHost = resolveSshHost(process.env) - const sshKeyPath = yield* _(findSshPrivateKey(fs, path, cwd)) - const config = yield* _(readProjectConfig(dir)) - - return yield* _(toProjectSummary(dir, id, config, sshHost, sshKeyPath)) - }) diff --git a/packages/docker-git/src/server/terminal.ts b/packages/docker-git/src/server/terminal.ts deleted file mode 100644 index 36d77b49..00000000 --- a/packages/docker-git/src/server/terminal.ts +++ /dev/null @@ -1,414 +0,0 @@ -import * as Cause from "effect/Cause" -import * as Exit from "effect/Exit" -import { Effect } from "effect" -import * as Layer from "effect/Layer" -import * as Runtime from "effect/Runtime" -import * as Scope from "effect/Scope" -import { NodeContext } from "@effect/platform-node" -import type { PlatformError } from "@effect/platform/Error" -import type { IncomingMessage } from "node:http" -import { WebSocketServer } from "ws" -import type { RawData, WebSocket } from "ws" -import * as Pty from "node-pty" - -import type { ConfigDecodeError, ConfigNotFoundError } from "../shell/errors.js" -import { ProjectNotFoundError } from "./errors.js" -import { loadProject } from "./projects.js" -import { resolveWritableCodexRoot } from "./codex.js" -import type { ProjectSummary } from "./core/domain.js" -import { ProjectIdPattern } from "./core/schema.js" -import { CodexAuthError, prepareCodexAccountDir } from "./codex.js" - -type TerminalError = - | ProjectNotFoundError - | ConfigNotFoundError - | ConfigDecodeError - | CodexAuthError - | PlatformError - -interface TerminalBase { - readonly cols: number - readonly rows: number -} - -interface TerminalProjectTarget extends TerminalBase { - readonly type: "ssh" - readonly projectId: string -} - -interface TerminalCodexTarget extends TerminalBase { - readonly type: "codex" - readonly label: string -} - -type TerminalTarget = TerminalProjectTarget | TerminalCodexTarget - -interface ClientInputMessage { - readonly type: "input" - readonly data: string -} - -interface ClientResizeMessage { - readonly type: "resize" - readonly cols: number - readonly rows: number -} - -type ClientMessage = ClientInputMessage | ClientResizeMessage - -interface ServerMessage { - readonly type: "output" | "error" - readonly data?: string - readonly message?: string -} - -const decodePayload = (data: RawData): string => { - if (typeof data === "string") { - return data - } - - if (Buffer.isBuffer(data)) { - return data.toString("utf-8") - } - - if (data instanceof ArrayBuffer) { - return Buffer.from(data).toString("utf-8") - } - - return Buffer.concat(data).toString("utf-8") -} - -const decodeClientMessage = (data: RawData): ClientMessage | null => { - const payload = decodePayload(data) - - try { - const parsed = JSON.parse(payload) - if (parsed && parsed.type === "input" && typeof parsed.data === "string") { - return { type: "input", data: parsed.data } - } - if ( - parsed && - parsed.type === "resize" && - Number.isFinite(parsed.cols) && - Number.isFinite(parsed.rows) - ) { - return { type: "resize", cols: Number(parsed.cols), rows: Number(parsed.rows) } - } - } catch { - return { type: "input", data: payload } - } - - return null -} - -const sendMessage = (socket: WebSocket, message: ServerMessage) => { - if (socket.readyState === socket.OPEN) { - socket.send(JSON.stringify(message)) - } -} - -const parseTerminalTarget = (url: string | undefined): TerminalTarget | null => { - if (!url) { - return null - } - - const parsed = new URL(url, "http://localhost") - const colsRaw = parsed.searchParams.get("cols") - const rowsRaw = parsed.searchParams.get("rows") - const cols = colsRaw ? Number(colsRaw) : 120 - const rows = rowsRaw ? Number(rowsRaw) : 30 - const base: TerminalBase = { - cols: Number.isFinite(cols) && cols > 0 ? cols : 120, - rows: Number.isFinite(rows) && rows > 0 ? rows : 30 - } - - const termMatch = /^\/term\/([^/]+)\/?$/.exec(parsed.pathname) - if (termMatch && termMatch[1]) { - const projectId = decodeURIComponent(termMatch[1]) - if (!ProjectIdPattern.test(projectId)) { - return null - } - return { type: "ssh", projectId, ...base } - } - - const codexMatch = /^\/codex\/([^/]+)\/?$/.exec(parsed.pathname) - if (codexMatch && codexMatch[1]) { - return { type: "codex", label: decodeURIComponent(codexMatch[1]), ...base } - } - - return null -} - -const buildSshArgs = (project: ProjectSummary): Array => { - const args: Array = [ - "-tt", - "-p", - String(project.sshPort), - "-o", - "LogLevel=ERROR", - "-o", - "StrictHostKeyChecking=no", - "-o", - "UserKnownHostsFile=/dev/null", - "-o", - "BatchMode=yes" - ] - - if (project.sshKeyPath) { - args.push("-i", project.sshKeyPath) - } - - args.push(`${project.sshUser}@${project.sshHost}`) - - return args -} - -const startTerminalSession = ( - socket: WebSocket, - target: TerminalProjectTarget, - project: ProjectSummary -) => { - const home = process.env["HOME"] - const baseOptions = { - name: "xterm-256color", - cols: target.cols, - rows: target.rows - } - const options = home && home.length > 0 ? { ...baseOptions, cwd: home } : baseOptions - - const term = Pty.spawn("ssh", buildSshArgs(project), options) - let closed = false - - const safeWrite = (data: string) => { - if (closed) { - return - } - try { - term.write(data) - } catch (error) { - closed = true - sendMessage(socket, { - type: "error", - message: error instanceof Error ? error.message : String(error) - }) - } - } - - const safeResize = (cols: number, rows: number) => { - if (closed) { - return - } - try { - term.resize(cols, rows) - } catch (error) { - closed = true - sendMessage(socket, { - type: "error", - message: error instanceof Error ? error.message : String(error) - }) - } - } - - term.onData((data) => { - sendMessage(socket, { type: "output", data }) - }) - - term.onExit(() => { - closed = true - sendMessage(socket, { type: "output", data: "\r\n[session closed]\r\n" }) - socket.close() - }) - - socket.on("message", (raw: RawData) => { - const message = decodeClientMessage(raw) - if (!message) { - return - } - - if (message.type === "input") { - safeWrite(message.data) - } else { - safeResize(message.cols, message.rows) - } - }) - - socket.on("close", () => { - term.kill() - }) - - socket.on("error", () => { - term.kill() - }) -} - -const startCodexSession = ( - socket: WebSocket, - target: TerminalCodexTarget, - codexHome: string -) => { - const home = process.env["HOME"] - const baseOptions = { - name: "xterm-256color", - cols: target.cols, - rows: target.rows, - env: { ...process.env, CODEX_HOME: codexHome } - } - const options = home && home.length > 0 ? { ...baseOptions, cwd: home } : baseOptions - - const term = Pty.spawn("codex", ["login"], options) - let closed = false - - const safeWrite = (data: string) => { - if (closed) { - return - } - try { - term.write(data) - } catch (error) { - closed = true - sendMessage(socket, { - type: "error", - message: error instanceof Error ? error.message : String(error) - }) - } - } - - const safeResize = (cols: number, rows: number) => { - if (closed) { - return - } - try { - term.resize(cols, rows) - } catch (error) { - closed = true - sendMessage(socket, { - type: "error", - message: error instanceof Error ? error.message : String(error) - }) - } - } - - term.onData((data) => { - sendMessage(socket, { type: "output", data }) - }) - - term.onExit(() => { - closed = true - sendMessage(socket, { type: "output", data: "\r\n[session closed]\r\n" }) - socket.close() - }) - - socket.on("message", (raw: RawData) => { - const message = decodeClientMessage(raw) - if (!message) { - return - } - - if (message.type === "input") { - safeWrite(message.data) - } else { - safeResize(message.cols, message.rows) - } - }) - - socket.on("close", () => { - term.kill() - }) - - socket.on("error", () => { - term.kill() - }) -} - -const handleTerminalExit = ( - socket: WebSocket, - exit: Exit.Exit, - target: TerminalProjectTarget -) => - Exit.match(exit, { - onFailure: (cause) => { - const message = Cause.pretty(cause) - sendMessage(socket, { type: "error", message }) - socket.close() - }, - onSuccess: (project) => { - startTerminalSession(socket, target, project) - } - }) - -const handleCodexExit = ( - socket: WebSocket, - exit: Exit.Exit, - target: TerminalCodexTarget -) => - Exit.match(exit, { - onFailure: (cause) => { - const message = Cause.pretty(cause) - sendMessage(socket, { type: "error", message }) - socket.close() - }, - onSuccess: (codexHome) => { - startCodexSession(socket, target, codexHome) - } - }) - -// CHANGE: attach a WebSocket terminal bridge on a dedicated port -// WHY: allow in-browser SSH sessions without conflicting with the HTTP server upgrade handler -// QUOTE(ТЗ): "Сделай что бы я сразу от сюда мог подключаться к терминалу" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall ws: connect(ws) -> ssh_session(ws) | error(ws) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: terminal sessions are closed when websocket closes -// COMPLEXITY: O(1) per connection -export const attachTerminalBridge = ( - terminalPort: number, - projectsRoot: string, - cwd: string -): Effect.Effect => - Layer.toRuntime(NodeContext.layer).pipe( - Effect.flatMap((runtime) => - Effect.acquireRelease( - Effect.sync(() => { - const wss = new WebSocketServer({ port: terminalPort }) - - const handleConnection = (ws: WebSocket, request: IncomingMessage) => { - const target = parseTerminalTarget(request.url) - if (!target) { - sendMessage(ws, { type: "error", message: "Invalid terminal target" }) - ws.close() - return - } - - if (target.type === "ssh") { - Runtime.runCallback(runtime, loadProject(projectsRoot, target.projectId, cwd), { - onExit: (exit) => handleTerminalExit(ws, exit, target) - }) - return - } - - Runtime.runCallback( - runtime, - Effect.gen(function* (_) { - const codexRootPath = yield* _(resolveWritableCodexRoot(projectsRoot)) - return yield* _(prepareCodexAccountDir(codexRootPath, target.label)) - }), - { - onExit: (exit) => handleCodexExit(ws, exit, target) - } - ) - } - - wss.on("connection", handleConnection) - - return { wss, handleConnection } - }), - ({ wss, handleConnection }) => - Effect.sync(() => { - wss.off("connection", handleConnection) - wss.close() - }) - ).pipe(Effect.asVoid) - ) - ) diff --git a/packages/docker-git/src/server/view.ts b/packages/docker-git/src/server/view.ts deleted file mode 100644 index ab41e5f7..00000000 --- a/packages/docker-git/src/server/view.ts +++ /dev/null @@ -1,895 +0,0 @@ -import { Match } from "effect" - -import type { ProjectIssue, ProjectSummary, ProjectsIndex } from "./core/domain.js" -import { findEnvValue } from "./core/env.js" - -const escapeHtml = (value: string): string => - value - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'") - -const renderIssue = (issue: ProjectIssue): string => - Match.value(issue).pipe( - Match.when({ _tag: "ConfigNotFound" }, ({ id, path }) => - `
${escapeHtml(id)} — missing config at ${escapeHtml(path)}
` - ), - Match.when({ _tag: "ConfigDecode" }, ({ id, message }) => - `
${escapeHtml(id)} — ${escapeHtml(message)}
` - ), - Match.exhaustive - ) - -const formatPathStatus = (value: string, exists: boolean): string => - exists ? value : `${value} (missing)` - -const renderProject = (project: ProjectSummary): string => ` -
-
- ${escapeHtml(project.serviceName)} -
-

${escapeHtml(project.id)}

-
-
Repo: ${escapeHtml(project.repoUrl)}
-
Ref: ${escapeHtml(project.repoRef)}
-
Container: ${escapeHtml(project.containerName)}
-
Workspace: ${escapeHtml(project.targetDir)}
-
SSH: ${escapeHtml(project.sshUser)}@${escapeHtml(project.sshHost)}:${project.sshPort}
-
Env: ${escapeHtml(formatPathStatus(project.envGlobalPath, project.envGlobalExists))}
-
Env (project): ${escapeHtml(formatPathStatus(project.envProjectPath, project.envProjectExists))}
-
- Deploy: - idle - -
-
-
${escapeHtml(project.sshCommand)}
-
-
- -
-
- -
-
- -
- Env - Terminal - Deploy logs - PS - Logs -
-
-` - -// CHANGE: render the dashboard HTML from the projects index -// WHY: keep the UI deterministic without client-side scripting -// QUOTE(ТЗ): "Просто сделай сайт и бекенд приложение" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall index: render(index) -> html(index) -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: escaped user-provided strings -// COMPLEXITY: O(n) where n = |projects| -export const renderDashboard = (index: ProjectsIndex, notice?: string): string => { - const issues = index.issues.length > 0 - ? ` -
-

Config issues

-
${index.issues.map(renderIssue).join("\n") -}
-
- ` - : "" - - const noticeBlock = notice - ? `
${escapeHtml(notice)}
` - : "" - - const projects = index.projects.length > 0 - ? index.projects.map(renderProject).join("\n") - : `

No projects yet

Create a docker-git project to populate this list.

` - - return ` - - - - - docker-git orchestrator - - - -
-
-
- -
-

docker-git

-

Orchestrator

-

Manage dev containers, SSH access, and repo lifecycle.

-
-
-
- Clone repo - Refresh - Integrations -
Root ${escapeHtml(index.root)}
-
-
- -
-
- Projects root - ${escapeHtml(index.root)} -
-
- Projects - ${index.projects.length} -
-
- Issues - ${index.issues.length} -
-
- - ${noticeBlock} - ${issues} - -
-
-

Projects

-

Each card maps to one docker-git environment.

-
-
${projects}
-
-
- - -` -} - -// CHANGE: render command output pages for logs/ps -// WHY: provide human-readable command output in the browser -// QUOTE(ТЗ): "видеть всю инфу по ним" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall output: render(output) -> html(output) -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: output is escaped -// COMPLEXITY: O(n) where n = |output| -export const renderOutputPage = (title: string, output: string): string => ` - - - - - ${escapeHtml(title)} - - - -
-
-
- -
-

docker-git

-

${escapeHtml(title)}

-

Command output snapshot.

-
-
-
- Back -
-
-
-
${escapeHtml(output.trim().length === 0 ? "(no output)" : output)}
-
-
- -` - -// CHANGE: render deployment logs page with live refresh hooks -// WHY: show progress for long-running builds without manual reloads -// QUOTE(ТЗ): "ОН как показывал так и показывает" -// REF: user-request-2026-01-15 -// SOURCE: n/a -// FORMAT THEOREM: forall output: render(output) -> html(output) -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: output is escaped -// COMPLEXITY: O(n) where n = |output| -export const renderDeployLogsPage = ( - projectId: string, - output: string, - phase: string, - message: string, - updatedAt: string -): string => { - const phaseClass = Match.value(phase).pipe( - Match.when("down", () => "deploy-badge--down"), - Match.when("build", () => "deploy-badge--build"), - Match.when("up", () => "deploy-badge--up"), - Match.when("running", () => "deploy-badge--running"), - Match.when("error", () => "deploy-badge--error"), - Match.orElse(() => "deploy-badge--idle") - ) - - return ` - - - - - deploy logs - - - -
-
-
- -
-

docker-git

-

deploy logs

-

Command output snapshot.

-
-
-
- Back -
-
-
-
- ${escapeHtml(projectId)} - ${escapeHtml(phase.length === 0 ? "idle" : phase)} - ${escapeHtml(message)} - ${escapeHtml(updatedAt)} -
-
${escapeHtml(
-          output.trim().length === 0 ? "(no output)" : output
-        )}
-
-
- - -` -} - -// CHANGE: render the terminal connection page for a project -// WHY: enable in-browser terminal access to the container via SSH -// QUOTE(ТЗ): "Сделай что бы я сразу от сюда мог подключаться к терминалу" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall project: render(project) -> html(project) -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: escaped user-provided strings -// COMPLEXITY: O(1) -export const renderTerminalPage = (project: ProjectSummary, terminalPort: number): string => ` - - - - - Terminal — ${escapeHtml(project.id)} - - - - -
-
-
- -
-

docker-git

-

Terminal

-

${escapeHtml(project.id)} · ${escapeHtml(project.repoUrl)}

-
-
-
- Back -
SSH ${escapeHtml(project.sshUser)}@${escapeHtml(project.sshHost)}:${project.sshPort}
-
-
-
-
-
-
- - - -` - -// CHANGE: render Codex CLI login session page -// WHY: allow device auth to run inside the orchestrator UI -// QUOTE(ТЗ): "Мне нужна прямо нативная интеграция с Codex" -// REF: user-request-2026-01-10 -// SOURCE: n/a -// FORMAT THEOREM: forall l: render(l) -> html(l) -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: label is normalized to a non-empty value -// COMPLEXITY: O(1) -export const renderCodexLoginPage = (label: string | null, terminalPort: number): string => { - const safeLabel = label && label.trim().length > 0 ? label.trim() : "default" - return ` - - - - - Codex Login — ${escapeHtml(safeLabel)} - - - - -
-
-
- -
-

docker-git

-

Codex Login

-

Account · ${escapeHtml(safeLabel)}

-
-
-
- Back -
Device auth
-
-
-
-
-
-
- - - -` -} - -// CHANGE: render integrations page for shared credentials -// WHY: allow GitHub access configuration without entering containers -// QUOTE(ТЗ): "у меня должна быть возможность подключать гитхаб" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall s: render(s) -> html(s) -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: escaped user-provided strings -// COMPLEXITY: O(1) -export interface CodexAccountView { - readonly label: string - readonly path: string - readonly connected: boolean - readonly entries: number - readonly legacy: boolean -} - -const renderActiveLabelLine = (activeLabel: string | null): string => - activeLabel === null - ? `
Active: none
` - : `
Active: ${escapeHtml(activeLabel)}
` - -const renderCodexAccounts = (accounts: ReadonlyArray): string => { - if (accounts.length === 0) { - return `
Accounts: none
` - } - - return accounts - .map((account) => { - const legacyTag = account.legacy ? " · legacy" : "" - const status = account.connected ? "connected" : "empty" - return `
Account: ${escapeHtml(account.label)}${legacyTag} · ${status}
` - }) - .join("\n") -} - -const renderCodexDisconnectForms = (accounts: ReadonlyArray): string => { - if (accounts.length === 0) { - return "" - } - - return accounts - .map((account) => - `
- - -
` - ) - .join("\n") -} - -const renderCodexAccountOptions = (accounts: ReadonlyArray): string => { - if (accounts.length === 0) { - return `` - } - - return accounts - .map((account) => { - const legacyTag = account.legacy ? " · legacy" : "" - return account.connected - ? `` - : `` - }) - .join("\n") -} - -const renderCodexActiveLine = ( - accounts: ReadonlyArray, - activeLabel: string | null -): string => { - if (activeLabel === null) { - return `
Active: none
` - } - const match = accounts.find((account) => account.label === activeLabel) - const legacyTag = match?.legacy ? " · legacy" : "" - return `
Active: ${escapeHtml(activeLabel)}${legacyTag}
` -} - -export const renderIntegrationsPage = ( - globalEnvPath: string, - githubTokenEntries: number, - gitTokenEntries: number, - claudeKeyEntries: number, - codexRootPath: string, - codexAccounts: ReadonlyArray -): string => ` - - - - - Integrations - - - -
-
-
- -
-

docker-git

-

Integrations

-

Connect shared services for private repos and CLIs.

-
-
-
- Back -
Global env ${escapeHtml(globalEnvPath)}
-
-
- -
-
-
-
- github -
-

GitHub

-
-
Status: ${githubTokenEntries > 0 ? "Connected" : "Not connected"}
-
Tokens: ${githubTokenEntries}
-
- -
- - - -
-
- - -
-
-
-
- git -
-

Git credentials

-
-
Status: ${gitTokenEntries > 0 ? "Connected" : "Not connected"}
-
Tokens: ${gitTokenEntries}
-
-
- - - - -
-
- - -
-
-
-
- claude -
-

Claude Code

-
-
Status: ${claudeKeyEntries > 0 ? "Connected" : "Not connected"}
-
Keys: ${claudeKeyEntries}
-
-
- - - -
-
- - -
-
-
-
- codex -
-

Codex

-
-
Status: ${codexAccounts.length > 0 ? "Connected" : "Not connected"}
-
Auth root: ${escapeHtml(codexRootPath)}
- ${renderCodexAccounts(codexAccounts)} -
- -
- - -
-
- - - -
- ${renderCodexDisconnectForms(codexAccounts)} -
-
-
-
- -` - -// CHANGE: render clone form for creating new docker-git projects -// WHY: allow cloning repositories from the UI with a selected GitHub session -// QUOTE(ТЗ): "Добавь на нашу платформу кнопку склонировать репозиторий" -// REF: user-request-2026-01-13 -// SOURCE: n/a -// FORMAT THEOREM: forall accounts: render(accounts) -> html(accounts) -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: escaped user-provided strings -// COMPLEXITY: O(n) where n = |accounts| -export const renderClonePage = ( - globalEnvPath: string, - githubTokenEntries: number -): string => ` - - - - - Clone repository - - - -
-
-
- -
-

docker-git

-

Clone repository

-

Spin up a container and clone the repo into it.

-
-
-
- Back -
Global env ${escapeHtml(globalEnvPath)}
-
-
- -
-
-
-
- clone -
-

New docker-git project

-
-
Git session: enter a GitHub label for private repos (${githubTokenEntries} tokens)
-
-
- - - - -
-
-

For private repos, enter a GitHub label from Integrations (use default for the base token).

-

Public repos can be cloned without a token.

-
-
-
-
-
- -` - -// CHANGE: render GitHub token instructions page -// WHY: provide a single link that embeds the required token guidance -// QUOTE(ТЗ): "генерировать такую ссылку которая автоматически подствит всё что необходимо" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall _: render() -> html() -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: escaped user-provided strings -// COMPLEXITY: O(1) -export const renderGithubTokenHelpPage = (): string => ` - - - - - GitHub token - - - -
-
-
- -
-

docker-git

-

GitHub token

-

Create a PAT for clone + push.

-
-
-
- Back -
-
- -
-
-

To clone + push, the token must allow write access to repository contents.

-

Classic PAT: select repo. Fine-grained PAT: Repository permissions → Contents: Read & write.

-
- -

Direct link: github.com/settings/tokens/new?description=gitingest&scopes=repo,read:audit_log,write:discussion,read:project

-
-
- -` - -interface GitIdentityView { - readonly projectName: string | null - readonly projectEmail: string | null - readonly effectiveName: string | null - readonly effectiveEmail: string | null -} - -// CHANGE: resolve git identity from env files -// WHY: surface git config status and allow editing in the UI -// QUOTE(ТЗ): "почему у нас не задаётся гит конфиг автоматически?" -// REF: user-request-2026-01-14 -// SOURCE: n/a -// FORMAT THEOREM: forall env: effective = project ?? global -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: project env overrides global env -// COMPLEXITY: O(n) where n = |lines| -const resolveGitIdentity = (globalEnv: string, projectEnv: string): GitIdentityView => { - const projectName = findEnvValue(projectEnv, "GIT_USER_NAME") - const projectEmail = findEnvValue(projectEnv, "GIT_USER_EMAIL") - const globalName = findEnvValue(globalEnv, "GIT_USER_NAME") - const globalEmail = findEnvValue(globalEnv, "GIT_USER_EMAIL") - return { - projectName, - projectEmail, - effectiveName: projectName ?? globalName, - effectiveEmail: projectEmail ?? globalEmail - } -} - -const renderOptionalValue = (value: string | null): string => - value === null ? `not set` : `${escapeHtml(value)}` - -// CHANGE: render env editor page for a project -// WHY: allow shared secrets and service tokens to be managed in one place -// QUOTE(ТЗ): "удобную настройку ENV" -// REF: user-request-2026-01-09 -// SOURCE: n/a -// FORMAT THEOREM: forall env: render(env) -> html(env) -// PURITY: CORE -// EFFECT: Effect -// INVARIANT: escaped user-provided strings -// COMPLEXITY: O(n) where n = |env| -export const renderEnvPage = ( - project: ProjectSummary, - globalEnv: string, - projectEnv: string, - githubTokenEntries: number, - activeGithubLabel: string | null, - gitTokenEntries: number, - activeGitLabel: string | null, - claudeKeyEntries: number, - activeClaudeLabel: string | null, - codexAccounts: ReadonlyArray, - activeCodexLabel: string | null -): string => { - const hasGithubAccounts = githubTokenEntries > 0 - const githubActiveLine = renderActiveLabelLine(activeGithubLabel) - const hasGitCredentials = gitTokenEntries > 0 - const gitCredentialActiveLine = renderActiveLabelLine(activeGitLabel) - const hasClaudeAccounts = claudeKeyEntries > 0 - const claudeActiveLine = renderActiveLabelLine(activeClaudeLabel) - const codexHasAccounts = codexAccounts.some((account) => account.connected) - const codexSelect = renderCodexAccountOptions(codexAccounts) - const codexActiveLine = renderCodexActiveLine(codexAccounts, activeCodexLabel) - const gitIdentity = resolveGitIdentity(globalEnv, projectEnv) - const gitUserName = gitIdentity.projectName ?? "" - const gitUserEmail = gitIdentity.projectEmail ?? "" - const gitEffectiveName = renderOptionalValue(gitIdentity.effectiveName) - const gitEffectiveEmail = renderOptionalValue(gitIdentity.effectiveEmail) - - return ` - - - - - Env — ${escapeHtml(project.id)} - - - -
-
-
- -
-

docker-git

-

Env

-

${escapeHtml(project.id)} · ${escapeHtml(project.repoUrl)}

-
-
-
- Back -
SSH ${escapeHtml(project.sshUser)}@${escapeHtml(project.sshHost)}:${project.sshPort}
-
-
- -
-
-
Services
-
-
- GitHub - ${githubActiveLine} -
-
- - -
-
- -
-

Available tokens: ${githubTokenEntries}

-
-
-
- Git credentials - ${gitCredentialActiveLine} -
-
- - -
-
- -
-

Available tokens: ${gitTokenEntries}

-
-
-
- Claude Code - ${claudeActiveLine} -
-
- - -
-
- -
-

Available keys: ${claudeKeyEntries}

-
-
-
- Git identity -
Effective name: ${gitEffectiveName}
-
Effective email: ${gitEffectiveEmail}
-
-
- - - -
-

Stored in project env. Leave empty to fall back to global env.

-
-
-
- Codex - ${codexActiveLine} -
-
- - -
-
- -
-
-
-
-
-
- -

Shared across all containers. Path: ${escapeHtml(project.envGlobalPath)}

- -
-
- -

Only for this project. Path: ${escapeHtml(project.envProjectPath)}

- -
-
-
- -
-
-
-

Examples: GITHUB_TOKEN, GIT_AUTH_TOKEN, ANTHROPIC_API_KEY, GIT_USER_NAME, GIT_USER_EMAIL.

-

Private GitHub clone uses GIT_AUTH_TOKEN or GITHUB_TOKEN. Optional GIT_AUTH_USER (default: x-access-token).

-
-
-
- -` -} diff --git a/packages/docker-git/src/shell/config.ts b/packages/docker-git/src/shell/config.ts deleted file mode 100644 index 8cccca9d..00000000 --- a/packages/docker-git/src/shell/config.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@effect-template/lib/shell/config" diff --git a/packages/docker-git/src/shell/docker.ts b/packages/docker-git/src/shell/docker.ts deleted file mode 100644 index 3ead1427..00000000 --- a/packages/docker-git/src/shell/docker.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@effect-template/lib/shell/docker" diff --git a/packages/docker-git/src/shell/errors.ts b/packages/docker-git/src/shell/errors.ts deleted file mode 100644 index 4f042681..00000000 --- a/packages/docker-git/src/shell/errors.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@effect-template/lib/shell/errors" diff --git a/packages/docker-git/src/shell/files.ts b/packages/docker-git/src/shell/files.ts deleted file mode 100644 index f12bac9e..00000000 --- a/packages/docker-git/src/shell/files.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@effect-template/lib/shell/files" diff --git a/packages/docker-git/tests/core/domain.test.ts b/packages/docker-git/tests/core/domain.test.ts deleted file mode 100644 index 0ecd5d3e..00000000 --- a/packages/docker-git/tests/core/domain.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { describe, expect, it } from "@effect/vitest" -import { Either, Effect } from "effect" - -import { deriveRepoSlug, parseMenuSelection } from "../../src/core/domain.js" - -describe("deriveRepoSlug", () => { - it.effect("handles https urls", () => - Effect.sync(() => { - Either.match(Either.right(deriveRepoSlug("https://github.com/org/repo.git")), { - onLeft: () => { - throw new Error("unexpected left") - }, - onRight: (slug) => { - expect(slug).toBe("repo") - } - }) - })) - - it.effect("handles ssh urls", () => - Effect.sync(() => { - Either.match(Either.right(deriveRepoSlug("git@github.com:org/awesome-repo.git")), { - onLeft: () => { - throw new Error("unexpected left") - }, - onRight: (slug) => { - expect(slug).toBe("awesome-repo") - } - }) - })) - - it.effect("falls back to app for empty", () => - Effect.sync(() => { - Either.match(Either.right(deriveRepoSlug("")), { - onLeft: () => { - throw new Error("unexpected left") - }, - onRight: (slug) => { - expect(slug).toBe("app") - } - }) - })) -}) - -describe("parseMenuSelection", () => { - it.effect("accepts create alias", () => - Effect.sync(() => { - Either.match(parseMenuSelection("1"), { - onLeft: () => { - throw new Error("expected right") - }, - onRight: (action) => { - expect(action._tag).toBe("Create") - } - }) - })) - - it.effect("rejects empty", () => - Effect.sync(() => { - Either.match(parseMenuSelection(""), { - onLeft: (error) => { - expect(error._tag).toBe("InvalidOption") - }, - onRight: () => { - throw new Error("expected left") - } - }) - })) -}) diff --git a/packages/docker-git/tests/core/templates.test.ts b/packages/docker-git/tests/core/templates.test.ts deleted file mode 100644 index e2b57d6e..00000000 --- a/packages/docker-git/tests/core/templates.test.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { describe, expect, it } from "@effect/vitest" -import { Effect } from "effect" - -import { planFiles } from "../../src/core/templates.js" -import { type TemplateConfig } from "../../src/core/domain.js" - -describe("planFiles", () => { - it.effect("includes docker and config files", () => - Effect.sync(() => { - const config: TemplateConfig = { - containerName: "dg-test", - serviceName: "dg-test", - sshUser: "dev", - sshPort: 2222, - repoUrl: "https://github.com/org/repo.git", - repoRef: "main", - targetDir: "/home/dev/app", - volumeName: "dg-test-home", - dockerGitPath: "./.docker-git", - authorizedKeysPath: "./authorized_keys", - envGlobalPath: "./.orch/env/global.env", - envProjectPath: "./.orch/env/project.env", - codexAuthPath: "./.orch/auth/codex", - codexSharedAuthPath: "../../.orch/auth/codex", - codexHome: "/home/dev/.codex", - enableMcpPlaywright: false, - pnpmVersion: "10.27.0" - } - - const specs = planFiles(config) - const composeSpec = specs.find( - (spec) => spec._tag === "File" && spec.relativePath === "docker-compose.yml" - ) - const ignoreSpec = specs.find( - (spec) => spec._tag === "File" && spec.relativePath === ".dockerignore" - ) - const configSpec = specs.find( - (spec) => spec._tag === "File" && spec.relativePath === "docker-git.json" - ) - const dockerfileSpec = specs.find( - (spec) => spec._tag === "File" && spec.relativePath === "Dockerfile" - ) - const entrypointSpec = specs.find( - (spec) => spec._tag === "File" && spec.relativePath === "entrypoint.sh" - ) - - expect(composeSpec !== undefined && composeSpec._tag === "File").toBe(true) - expect(ignoreSpec !== undefined && ignoreSpec._tag === "File").toBe(true) - expect(configSpec !== undefined && configSpec._tag === "File").toBe(true) - expect(dockerfileSpec !== undefined && dockerfileSpec._tag === "File").toBe(true) - expect(entrypointSpec !== undefined && entrypointSpec._tag === "File").toBe(true) - - if (configSpec && configSpec._tag === "File") { - expect(configSpec.contents).toContain(config.repoUrl) - expect(configSpec.contents).toContain(config.containerName) - } - - if (ignoreSpec && ignoreSpec._tag === "File") { - expect(ignoreSpec.contents).toContain(".orch/") - expect(ignoreSpec.contents).toContain("authorized_keys") - } - - if (dockerfileSpec && dockerfileSpec._tag === "File") { - expect(dockerfileSpec.contents).toContain("MENU_COMPLETE") - expect(dockerfileSpec.contents).toContain("AUTO_MENU") - expect(dockerfileSpec.contents).toContain("ncurses-term") - expect(dockerfileSpec.contents).toContain("tag-order builtins commands") - expect(dockerfileSpec.contents).toContain("gitleaks version") - } - - if (entrypointSpec && entrypointSpec._tag === "File") { - expect(entrypointSpec.contents).toContain( - "GIT_CREDENTIAL_HELPER_PATH=\"/usr/local/bin/docker-git-credential-helper\"" - ) - expect(entrypointSpec.contents).toContain("AUTH_LABEL_RAW=\"${GIT_AUTH_LABEL:-${GITHUB_AUTH_LABEL:-}}\"") - expect(entrypointSpec.contents).toContain("LABELED_GITHUB_TOKEN_KEY=\"GITHUB_TOKEN__$RESOLVED_AUTH_LABEL\"") - expect(entrypointSpec.contents).toContain("LABELED_GIT_TOKEN_KEY=\"GIT_AUTH_TOKEN__$RESOLVED_AUTH_LABEL\"") - expect(entrypointSpec.contents).toContain("SAFE_GH_TOKEN=\"$(printf \"%q\" \"$EFFECTIVE_GH_TOKEN\")\"") - expect(entrypointSpec.contents).toContain("docker_git_upsert_ssh_env \"GIT_AUTH_TOKEN\" \"$EFFECTIVE_GITHUB_TOKEN\"") - expect(entrypointSpec.contents).toContain("token=\"${GITHUB_TOKEN:-}\"") - expect(entrypointSpec.contents).toContain("token=\"${GH_TOKEN:-}\"") - expect(entrypointSpec.contents).toContain("issue_managed_start=''") - expect(entrypointSpec.contents).toContain("check_issue_managed_block_range") - expect(entrypointSpec.contents).toContain( - "push contains commit updating managed issue block in AGENTS.md" - ) - expect(entrypointSpec.contents).toContain("docker_git_short_pwd()") - expect(entrypointSpec.contents).toContain("local base=\"[\\t] $short_pwd\"") - expect(entrypointSpec.contents).toContain("local base=\"[%*] $short_pwd\"") - expect(entrypointSpec.contents).toContain("CACHE_ROOT=\"/home/dev/.docker-git/.cache/git-mirrors\"") - expect(entrypointSpec.contents).toContain("PACKAGE_CACHE_ROOT=\"/home/dev/.docker-git/.cache/packages\"") - expect(entrypointSpec.contents).toContain("npm_config_store_dir") - expect(entrypointSpec.contents).toContain("NPM_CONFIG_CACHE") - expect(entrypointSpec.contents).toContain("YARN_CACHE_FOLDER") - expect(entrypointSpec.contents).toContain("CLAUDE_REAL_BIN=\"/usr/local/bin/.docker-git-claude-real\"") - expect(entrypointSpec.contents).toContain("CLAUDE_WRAPPER_BIN=\"/usr/local/bin/claude\"") - expect(entrypointSpec.contents).toContain("cat <<'EOF' > \"$CLAUDE_WRAPPER_BIN\"") - expect(entrypointSpec.contents).toContain('CLAUDE_CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"') - expect(entrypointSpec.contents).toContain("unset CLAUDE_CODE_OAUTH_TOKEN || true") - expect(entrypointSpec.contents).toContain("CLONE_CACHE_ARGS=\"--reference-if-able '$CACHE_REPO_DIR' --dissociate\"") - expect(entrypointSpec.contents).toContain("[clone-cache] using mirror: $CACHE_REPO_DIR") - expect(entrypointSpec.contents).toContain("git clone --progress $CLONE_CACHE_ARGS") - expect(entrypointSpec.contents).toContain("[clone-cache] mirror created: $CACHE_REPO_DIR") - expect(entrypointSpec.contents).toContain("CACHE_REPO_DIR=\"${CACHE_REPO_DIR:-}\"") - expect(entrypointSpec.contents).toContain("fetch --progress --prune '$AUTH_REPO_URL' '+refs/*:refs/*'") - } - })) - - it.effect("includes Playwright sidecar files when enabled", () => - Effect.sync(() => { - const config: TemplateConfig = { - containerName: "dg-test", - serviceName: "dg-test", - sshUser: "dev", - sshPort: 2222, - repoUrl: "https://github.com/org/repo.git", - repoRef: "main", - targetDir: "/home/dev/app", - volumeName: "dg-test-home", - dockerGitPath: "./.docker-git", - authorizedKeysPath: "./authorized_keys", - envGlobalPath: "./.orch/env/global.env", - envProjectPath: "./.orch/env/project.env", - codexAuthPath: "./.orch/auth/codex", - codexSharedAuthPath: "../../.orch/auth/codex", - codexHome: "/home/dev/.codex", - enableMcpPlaywright: true, - pnpmVersion: "10.27.0" - } - - const specs = planFiles(config) - const browserDockerfile = specs.find( - (spec) => spec._tag === "File" && spec.relativePath === "Dockerfile.browser" - ) - const browserScript = specs.find( - (spec) => spec._tag === "File" && spec.relativePath === "mcp-playwright-start-extra.sh" - ) - - expect(browserDockerfile !== undefined && browserDockerfile._tag === "File").toBe(true) - expect(browserScript !== undefined && browserScript._tag === "File").toBe(true) - })) - - it.effect("embeds issue workspace AGENTS context in entrypoint", () => - Effect.sync(() => { - const config: TemplateConfig = { - containerName: "dg-repo-issue-5", - serviceName: "dg-repo-issue-5", - sshUser: "dev", - sshPort: 2222, - repoUrl: "https://github.com/org/repo.git", - repoRef: "issue-5", - targetDir: "/home/dev/org/repo/issue-5", - volumeName: "dg-repo-issue-5-home", - dockerGitPath: "./.docker-git", - authorizedKeysPath: "./authorized_keys", - envGlobalPath: "./.orch/env/global.env", - envProjectPath: "./.orch/env/project.env", - codexAuthPath: "./.orch/auth/codex", - codexSharedAuthPath: "../../.orch/auth/codex", - codexHome: "/home/dev/.codex", - enableMcpPlaywright: false, - pnpmVersion: "10.27.0" - } - - const specs = planFiles(config) - const entrypointSpec = specs.find( - (spec) => spec._tag === "File" && spec.relativePath === "entrypoint.sh" - ) - expect(entrypointSpec !== undefined && entrypointSpec._tag === "File").toBe(true) - if (entrypointSpec && entrypointSpec._tag === "File") { - expect(entrypointSpec.contents).toContain("Доступные workspace пути:") - expect(entrypointSpec.contents).toContain("Контекст workspace:") - expect(entrypointSpec.contents).toContain("docker_git_workspace_context_line()") - expect(entrypointSpec.contents).toContain("REPO_REF_VALUE=\"${REPO_REF:-issue-5}\"") - expect(entrypointSpec.contents).toContain("REPO_URL_VALUE=\"${REPO_URL:-https://github.com/org/repo.git}\"") - expect(entrypointSpec.contents).toContain("Контекст workspace: issue #$ISSUE_ID_VALUE ($ISSUE_URL_VALUE)") - expect(entrypointSpec.contents).not.toContain("ISSUE_AGENTS_HINT_LINE=") - expect(entrypointSpec.contents).not.toContain("Issue AGENTS.md: __TARGET_DIR__/AGENTS.md") - expect(entrypointSpec.contents).not.toContain("ISSUE_AGENTS_PATH=\"$TARGET_DIR/AGENTS.md\"") - expect(entrypointSpec.contents).not.toContain( - "ISSUE_MANAGED_START=\"\"" - ) - expect(entrypointSpec.contents).not.toContain("grep -qx \"AGENTS.md\" \"$EXCLUDE_PATH\"") - } - })) - - it.effect("embeds PR workspace URL context in entrypoint", () => - Effect.sync(() => { - const config: TemplateConfig = { - containerName: "dg-repo-pr-42", - serviceName: "dg-repo-pr-42", - sshUser: "dev", - sshPort: 2222, - repoUrl: "https://github.com/org/repo.git", - repoRef: "refs/pull/42/head", - targetDir: "/home/dev/org/repo/pr-42", - volumeName: "dg-repo-pr-42-home", - dockerGitPath: "./.docker-git", - authorizedKeysPath: "./authorized_keys", - envGlobalPath: "./.orch/env/global.env", - envProjectPath: "./.orch/env/project.env", - codexAuthPath: "./.orch/auth/codex", - codexSharedAuthPath: "../../.orch/auth/codex", - codexHome: "/home/dev/.codex", - enableMcpPlaywright: false, - pnpmVersion: "10.27.0" - } - - const specs = planFiles(config) - const entrypointSpec = specs.find( - (spec) => spec._tag === "File" && spec.relativePath === "entrypoint.sh" - ) - expect(entrypointSpec !== undefined && entrypointSpec._tag === "File").toBe(true) - if (entrypointSpec && entrypointSpec._tag === "File") { - expect(entrypointSpec.contents).toContain("REPO_REF_VALUE=\"${REPO_REF:-refs/pull/42/head}\"") - expect(entrypointSpec.contents).toContain("REPO_URL_VALUE=\"${REPO_URL:-https://github.com/org/repo.git}\"") - expect(entrypointSpec.contents).toContain( - "PR_ID=\"$(printf \"%s\" \"$REPO_REF\" | sed -nE 's#^refs/pull/([0-9]+)/head$#\\1#p')\"" - ) - expect(entrypointSpec.contents).toContain( - "PR_URL=\"https://github.com/$PR_REPO/pull/$PR_ID\"" - ) - expect(entrypointSpec.contents).toContain( - "WORKSPACE_INFO_LINE=\"Контекст workspace: PR #$PR_ID ($PR_URL)\"" - ) - expect(entrypointSpec.contents).toContain("Контекст workspace: PR #$PR_ID_VALUE ($PR_URL_VALUE)") - } - })) -}) diff --git a/packages/docker-git/tests/server/claude.test.ts b/packages/docker-git/tests/server/claude.test.ts deleted file mode 100644 index 2aa790a7..00000000 --- a/packages/docker-git/tests/server/claude.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, expect, it } from "@effect/vitest" -import { Effect } from "effect" - -import { - findClaudeApiKeyByLabel, - listClaudeApiKeys, - resolveClaudeLabelForApiKey, - resolveProjectClaudeApiKey, - resolveProjectClaudeLabel -} from "../../src/server/claude.js" - -const globalEnv = [ - "ANTHROPIC_API_KEY=sk-ant-default", - "ANTHROPIC_API_KEY__WORK=sk-ant-work", - "" -].join("\n") - -describe("listClaudeApiKeys", () => { - it.effect("lists labeled Claude keys", () => - Effect.sync(() => { - expect(listClaudeApiKeys(globalEnv)).toEqual([ - { label: "default", apiKey: "sk-ant-default" }, - { label: "WORK", apiKey: "sk-ant-work" } - ]) - })) -}) - -describe("findClaudeApiKeyByLabel", () => { - it.effect("finds keys by normalized label", () => - Effect.sync(() => { - expect(findClaudeApiKeyByLabel(globalEnv, "work")).toEqual({ - label: "WORK", - apiKey: "sk-ant-work" - }) - expect(findClaudeApiKeyByLabel(globalEnv, "missing")).toBeNull() - })) -}) - -describe("project claude state", () => { - it.effect("reads active project key and label", () => - Effect.sync(() => { - const projectEnv = [ - "ANTHROPIC_API_KEY=sk-ant-work", - "CLAUDE_AUTH_LABEL=WORK", - "" - ].join("\n") - expect(resolveProjectClaudeApiKey(projectEnv)).toBe("sk-ant-work") - expect(resolveProjectClaudeLabel(projectEnv)).toBe("WORK") - })) - - it.effect("resolves label by key value", () => - Effect.sync(() => { - expect(resolveClaudeLabelForApiKey(globalEnv, "sk-ant-default")).toBe("default") - expect(resolveClaudeLabelForApiKey(globalEnv, "missing")).toBeNull() - })) -}) diff --git a/packages/docker-git/tests/server/domain.test.ts b/packages/docker-git/tests/server/domain.test.ts deleted file mode 100644 index aed0cd70..00000000 --- a/packages/docker-git/tests/server/domain.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { describe, expect, it } from "@effect/vitest" -import { Effect } from "effect" - -import { - buildSshCommand, - resolveCodexAuthPath, - resolveGlobalEnvPath, - resolveOrchRoot, - resolveProjectsRoot, - resolveSecretsRoot -} from "../../src/server/core/domain.js" - -describe("resolveProjectsRoot", () => { - it.effect("uses env override", () => - Effect.sync(() => { - const root = resolveProjectsRoot("/cwd", { DOCKER_GIT_PROJECTS_ROOT: "/tmp/root" }) - expect(root).toBe("/tmp/root") - })) - - it.effect("falls back to cwd", () => - Effect.sync(() => { - const root = resolveProjectsRoot("/cwd", {}) - expect(root).toBe("/cwd/.docker-git") - })) - - it.effect("falls back to home", () => - Effect.sync(() => { - const root = resolveProjectsRoot("/cwd", { HOME: "/home/me" }) - expect(root).toBe("/home/me/.docker-git") - })) -}) - -describe("orch helpers", () => { - it.effect("builds orch paths", () => - Effect.sync(() => { - const root = "/root/.docker-git" - expect(resolveOrchRoot(root)).toBe("/root/.docker-git/.orch") - expect(resolveSecretsRoot(root)).toBe("/root/.docker-git/.orch") - expect(resolveGlobalEnvPath(root)).toBe("/root/.docker-git/.orch/env/global.env") - expect(resolveCodexAuthPath(root)).toBe("/root/.docker-git/.orch/auth/codex") - })) -}) - -describe("buildSshCommand", () => { - it.effect("builds with key", () => - Effect.sync(() => { - const command = buildSshCommand({ - sshUser: "dev", - sshHost: "localhost", - sshPort: 2222, - sshKeyPath: "/tmp/key" - }) - expect(command).toContain("-i /tmp/key") - expect(command).toContain("dev@localhost") - })) - - it.effect("builds without key", () => - Effect.sync(() => { - const command = buildSshCommand({ - sshUser: "dev", - sshHost: "localhost", - sshPort: 2222, - sshKeyPath: null - }) - expect(command).toBe( - "ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 dev@localhost" - ) - })) -}) diff --git a/packages/docker-git/tests/server/git-credentials.test.ts b/packages/docker-git/tests/server/git-credentials.test.ts deleted file mode 100644 index d0aae552..00000000 --- a/packages/docker-git/tests/server/git-credentials.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { describe, expect, it } from "@effect/vitest" -import { Effect } from "effect" - -import { - findGitCredentialByLabel, - listGitCredentials, - resolveGitLabelForToken, - resolveProjectGitLabel, - resolveProjectGitToken -} from "../../src/server/git-credentials.js" - -const globalEnv = [ - "GIT_AUTH_TOKEN=token_default", - "GIT_AUTH_USER=default-user", - "GIT_AUTH_TOKEN__WORK=token_work", - "GIT_AUTH_TOKEN__OPS=token_ops", - "GIT_AUTH_USER__OPS=ops-user", - "" -].join("\n") - -describe("listGitCredentials", () => { - it.effect("lists labeled git credentials with user fallback", () => - Effect.sync(() => { - const credentials = listGitCredentials(globalEnv) - expect(credentials).toEqual([ - { label: "default", token: "token_default", user: "default-user" }, - { label: "WORK", token: "token_work", user: "default-user" }, - { label: "OPS", token: "token_ops", user: "ops-user" } - ]) - })) -}) - -describe("findGitCredentialByLabel", () => { - it.effect("finds credentials by normalized label", () => - Effect.sync(() => { - const selected = findGitCredentialByLabel(globalEnv, "work") - expect(selected).toEqual({ - label: "WORK", - token: "token_work", - user: "default-user" - }) - })) -}) - -describe("project git state", () => { - it.effect("reads project git token and label", () => - Effect.sync(() => { - const projectEnv = [ - "GIT_AUTH_TOKEN=token_ops", - "GIT_AUTH_LABEL=OPS", - "" - ].join("\n") - expect(resolveProjectGitToken(projectEnv)).toBe("token_ops") - expect(resolveProjectGitLabel(projectEnv)).toBe("OPS") - })) - - it.effect("resolves label by token value", () => - Effect.sync(() => { - expect(resolveGitLabelForToken(globalEnv, "token_default")).toBe("default") - expect(resolveGitLabelForToken(globalEnv, "token_work")).toBe("WORK") - expect(resolveGitLabelForToken(globalEnv, "missing")).toBeNull() - })) -}) diff --git a/packages/docker-git/tests/server/ports.test.ts b/packages/docker-git/tests/server/ports.test.ts deleted file mode 100644 index e0954574..00000000 --- a/packages/docker-git/tests/server/ports.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, expect, it } from "@effect/vitest" -import { Effect } from "effect" - -import { findAvailablePort } from "../../src/server/core/ports.js" - -describe("findAvailablePort", () => { - it.effect("returns preferred when free", () => - Effect.sync(() => { - const port = findAvailablePort(2222, [2223, 2224], { min: 2222, max: 2230 }) - expect(port).toBe(2222) - })) - - it.effect("selects next free port in range", () => - Effect.sync(() => { - const port = findAvailablePort(2222, [2222, 2223], { min: 2222, max: 2225 }) - expect(port).toBe(2224) - })) - - it.effect("wraps around when needed", () => - Effect.sync(() => { - const port = findAvailablePort(2224, [2224, 2225], { min: 2222, max: 2225 }) - expect(port).toBe(2222) - })) -}) diff --git a/packages/docker-git/tsconfig.json b/packages/docker-git/tsconfig.json deleted file mode 100644 index 82672fda..00000000 --- a/packages/docker-git/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": ".", - "outDir": "dist", - "types": ["vitest"] - }, - "include": ["src/**/*", "tests/**/*", "vitest.config.ts"], - "exclude": ["dist", "node_modules"] -} diff --git a/packages/docker-git/vitest.config.ts b/packages/docker-git/vitest.config.ts deleted file mode 100644 index 60aea824..00000000 --- a/packages/docker-git/vitest.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from "vitest/config" - -export default defineConfig({ - test: { - environment: "node", - include: ["tests/**/*.{test,spec}.ts"], - exclude: ["node_modules", "dist"] - } -}) diff --git a/packages/docker-git/web/deploy-logs.js b/packages/docker-git/web/deploy-logs.js deleted file mode 100644 index cb00d656..00000000 --- a/packages/docker-git/web/deploy-logs.js +++ /dev/null @@ -1,111 +0,0 @@ -const logNode = document.querySelector("[data-deploy-log]") -const statusNode = document.querySelector("[data-deploy-status]") -const messageNode = document.querySelector("[data-deploy-message]") -const updatedNode = document.querySelector("[data-deploy-updated]") - -if (!logNode) { - return -} - -const projectId = logNode.dataset.projectId -if (!projectId) { - return -} - -const phaseLabels = { - idle: "idle", - down: "stopping", - build: "building", - up: "starting", - running: "running", - error: "error" -} - -const phaseClasses = { - idle: "deploy-badge--idle", - down: "deploy-badge--down", - build: "deploy-badge--build", - up: "deploy-badge--up", - running: "deploy-badge--running", - error: "deploy-badge--error" -} - -const applyStatus = (status) => { - if (!status || !statusNode) { - return - } - - const phase = typeof status.phase === "string" ? status.phase : "idle" - const label = phaseLabels[phase] ?? "idle" - const className = phaseClasses[phase] ?? phaseClasses.idle - statusNode.textContent = label - statusNode.className = `deploy-badge ${className}` - - if (messageNode) { - messageNode.textContent = status.message ?? "" - } - - if (updatedNode) { - updatedNode.textContent = status.updatedAt ?? "" - } -} - -const formatEntry = (entry) => `${entry.timestamp} ${entry.line}` - -const applySnapshot = (payload) => { - if (!payload || !Array.isArray(payload.entries)) { - return - } - - const lines = payload.entries.map(formatEntry) - logNode.textContent = lines.length === 0 ? "(no output)" : `${lines.join("\n")}\n` - applyStatus(payload.status) -} - -const appendLog = (entry) => { - if (!entry || typeof entry.timestamp !== "string") { - return - } - const nextLine = formatEntry(entry) - const current = logNode.textContent ?? "" - if (current.trim().length === 0 || current.trim() === "(no output)") { - logNode.textContent = `${nextLine}\n` - } else { - logNode.textContent = `${current.replace(/\s*$/, "")}\n${nextLine}\n` - } -} - -const source = new EventSource(`/api/deployments/${encodeURIComponent(projectId)}/stream`) - -source.addEventListener("snapshot", (event) => { - try { - const payload = JSON.parse(event.data) - applySnapshot(payload) - } catch { - // ignore - } -}) - -source.addEventListener("log", (event) => { - try { - const entry = JSON.parse(event.data) - appendLog(entry) - } catch { - // ignore - } -}) - -source.addEventListener("status", (event) => { - try { - const status = JSON.parse(event.data) - applyStatus(status) - } catch { - // ignore - } -}) - -source.addEventListener("error", () => { - if (messageNode) { - messageNode.textContent = "stream disconnected" - } -}) diff --git a/packages/docker-git/web/deploy.js b/packages/docker-git/web/deploy.js deleted file mode 100644 index 5432466b..00000000 --- a/packages/docker-git/web/deploy.js +++ /dev/null @@ -1,134 +0,0 @@ -const statusNodes = document.querySelectorAll("[data-deploy-status]") -const messageNodes = document.querySelectorAll("[data-deploy-message]") - -if (statusNodes.length === 0) { - return -} - -const statusById = new Map() -const messageById = new Map() - -statusNodes.forEach((node) => { - const id = node.dataset.projectId - if (id && id.length > 0) { - statusById.set(id, node) - } -}) - -messageNodes.forEach((node) => { - const id = node.dataset.projectId - if (id && id.length > 0) { - messageById.set(id, node) - } -}) - -const phaseLabels = { - idle: "idle", - down: "stopping", - build: "building", - up: "starting", - running: "running", - error: "error" -} - -const phaseClasses = { - idle: "deploy-badge--idle", - down: "deploy-badge--down", - build: "deploy-badge--build", - up: "deploy-badge--up", - running: "deploy-badge--running", - error: "deploy-badge--error" -} - -const applyStatus = (projectId, phase, message) => { - const node = statusById.get(projectId) - if (!node) { - return - } - - const normalized = phaseLabels[phase] ? phase : "idle" - const className = phaseClasses[phase] ?? phaseClasses.idle - node.textContent = phaseLabels[normalized] ?? "idle" - node.className = `deploy-badge ${className}` - - const messageNode = messageById.get(projectId) - if (messageNode) { - messageNode.textContent = message ?? "" - } -} - -const applyAll = (payload) => { - if (!payload || !Array.isArray(payload.deployments)) { - return - } - - const seen = new Set() - payload.deployments.forEach((status) => { - if (!status || typeof status.projectId !== "string") { - return - } - seen.add(status.projectId) - applyStatus(status.projectId, status.phase, status.message) - }) - - statusById.forEach((_, projectId) => { - if (!seen.has(projectId)) { - applyStatus(projectId, "idle", "") - } - }) -} - -const fetchStatuses = () => { - const request = new XMLHttpRequest() - request.open("GET", "/api/deployments") - request.responseType = "text" - request.onload = () => { - if (request.status < 200 || request.status >= 300) { - return - } - try { - const json = JSON.parse(request.responseText) - applyAll(json) - } catch { - // ignore invalid payloads - } - } - request.send() -} - -fetchStatuses() -setInterval(fetchStatuses, 2000) - -const actionForms = document.querySelectorAll("form[data-action][data-project-id]") -actionForms.forEach((form) => { - form.addEventListener("submit", (event) => { - event.preventDefault() - const action = form.dataset.action - const projectId = form.dataset.projectId - if (!action || !projectId) { - return - } - - if (action === "down") { - applyStatus(projectId, "down", "docker compose down") - } else if (action === "recreate") { - applyStatus(projectId, "build", "docker compose --progress=plain build") - } else { - applyStatus(projectId, "build", "docker compose --progress=plain build") - } - - const request = new XMLHttpRequest() - request.open("POST", form.action) - request.onload = () => { - if (request.status >= 200 && request.status < 400) { - fetchStatuses() - } else { - applyStatus(projectId, "error", "action failed") - } - } - request.onerror = () => { - applyStatus(projectId, "error", "action failed") - } - request.send() - }) -}) diff --git a/packages/docker-git/web/index.html b/packages/docker-git/web/index.html deleted file mode 100644 index 45afbf1c..00000000 --- a/packages/docker-git/web/index.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - docker-git orchestrator - - - -
-
-
- -
-

docker-git

-

Orchestrator

-

Use the server UI at /.

-
-
-
-
- - diff --git a/packages/docker-git/web/styles.css b/packages/docker-git/web/styles.css deleted file mode 100644 index 7901ae2f..00000000 --- a/packages/docker-git/web/styles.css +++ /dev/null @@ -1,381 +0,0 @@ -@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap"); - -:root { - color-scheme: light; - --bg: #f6f2ea; - --bg-accent: #fcebd7; - --card: #ffffff; - --text: #1f1f1f; - --muted: #6a6a6a; - --border: #e5d7c4; - --shadow: 0 20px 45px rgba(26, 19, 12, 0.12); - --primary: #f15732; - --primary-dark: #c04328; - --secondary: #0e6f6a; - --secondary-dark: #0a5955; - --ghost: #2a2a2a; - --mono: "IBM Plex Mono", "JetBrains Mono", Menlo, monospace; - --radius: 18px; -} - -* { - box-sizing: border-box; -} - -body { - margin: 0; - font-family: "Space Grotesk", "Segoe UI", sans-serif; - background: radial-gradient(circle at top, #fff6e8 0%, var(--bg) 45%, #efe5d8 100%); - color: var(--text); -} - -.scene { - max-width: 1200px; - margin: 0 auto; - padding: 32px 24px 80px; -} - -.hero { - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: space-between; - gap: 24px; - padding: 24px 28px; - border-radius: var(--radius); - background: var(--card); - box-shadow: var(--shadow); - border: 1px solid var(--border); -} - -.brand { - display: flex; - gap: 16px; - align-items: center; -} - -.logo { - width: 56px; - height: 56px; - border-radius: 16px; - background: linear-gradient(135deg, #ff8e3a, #f15732 60%, #c04328 100%); - color: #fff; - display: grid; - place-items: center; - font-weight: 700; - font-size: 20px; -} - -.eyebrow { - margin: 0; - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.18em; - color: var(--muted); -} - -.subtitle { - margin: 6px 0 0; - color: var(--muted); -} - -.hero-actions { - display: flex; - flex-wrap: wrap; - gap: 12px; - align-items: center; -} - -.status { - font-size: 13px; - padding: 6px 12px; - background: var(--bg-accent); - border-radius: 999px; - border: 1px solid var(--border); -} - -.section-head { - display: flex; - flex-wrap: wrap; - align-items: baseline; - justify-content: space-between; - gap: 12px; - margin-top: 28px; -} - -.summary { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 16px; - margin-top: 24px; -} - -.stat { - padding: 16px; - border-radius: 16px; - background: var(--card); - border: 1px solid var(--border); - box-shadow: var(--shadow); -} - -.stat .label { - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.14em; - color: var(--muted); -} - -.stat .value { - font-size: 20px; - font-weight: 600; -} - -.projects { - margin-top: 28px; -} - -.project-grid { - display: grid; - gap: 18px; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); -} - -.card { - background: var(--card); - padding: 20px; - border-radius: 18px; - border: 1px solid var(--border); - box-shadow: var(--shadow); -} - -.card h3 { - margin: 8px 0 10px; -} - -.meta { - display: grid; - gap: 6px; - font-size: 13px; - color: var(--muted); -} - -.badge { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 4px 10px; - border-radius: 999px; - border: 1px solid var(--border); - background: #fff7ef; - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.12em; -} - -.code { - margin-top: 10px; - padding: 12px; - background: #111827; - color: #f9f5f0; - font-family: var(--mono); - border-radius: 12px; - font-size: 12px; -} - -.actions { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin-top: 14px; -} - -.btn { - padding: 8px 14px; - border-radius: 999px; - border: 1px solid var(--border); - background: #fff; - color: var(--text); - text-decoration: none; - cursor: pointer; - font-weight: 600; - font-size: 13px; - transition: transform 0.2s ease, box-shadow 0.2s ease; -} - -.btn:hover { - transform: translateY(-1px); - box-shadow: 0 8px 18px rgba(31, 21, 13, 0.18); -} - -.btn.primary { - background: var(--primary); - color: #fff; - border-color: transparent; -} - -.btn.primary:hover { - background: var(--primary-dark); -} - -.btn.ghost { - background: transparent; - color: var(--ghost); -} - -.btn.ghost:hover { - border-color: var(--ghost); -} - -.deploy-line { - display: flex; - align-items: center; - gap: 8px; - margin-top: 8px; - flex-wrap: wrap; -} - -.deploy-badge { - padding: 4px 10px; - border-radius: 999px; - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.12em; - background: #f1f1f1; -} - -.deploy-badge--idle { background: #f1f1f1; } -.deploy-badge--down { background: #fee6c9; } -.deploy-badge--build { background: #ffe6e6; } -.deploy-badge--up { background: #e0f4e7; } -.deploy-badge--running { background: #d7f5f1; } -.deploy-badge--error { background: #ffe0e0; color: #942b2b; } - -.output { - margin-top: 20px; - background: var(--card); - border-radius: 18px; - padding: 20px; - border: 1px solid var(--border); - box-shadow: var(--shadow); -} - -.output-body { - white-space: pre-wrap; - font-family: var(--mono); - font-size: 12px; - background: #0b0d10; - color: #f5f7ff; - padding: 18px; - border-radius: 14px; - overflow: auto; - max-height: 60vh; -} - -.terminal { - height: 60vh; - background: #0b0d10; - border-radius: 14px; - border: 1px solid #1b1f2a; - overflow: hidden; -} - -.issues { - margin-top: 20px; -} - -.issue-card { - padding: 12px 16px; - border-radius: 14px; - background: #fff2f0; - border: 1px solid #f4c6bc; - color: #9b3a2c; -} - -.env-actions { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin-top: 10px; -} - -.env-actions--left { - justify-content: flex-start; -} - -.env-input, -.env-select, -.env-textarea { - border-radius: 12px; - border: 1px solid var(--border); - padding: 10px 12px; - font-family: inherit; - font-size: 13px; - background: #fff; -} - -.env-textarea { - width: 100%; - min-height: 180px; - font-family: var(--mono); -} - -.env-grid { - display: grid; - gap: 16px; - margin-top: 18px; -} - -.service-panel { - margin-bottom: 18px; -} - -.service-head { - font-weight: 600; - margin-bottom: 10px; -} - -.service-card { - padding: 14px; - border-radius: 14px; - border: 1px solid var(--border); - background: #fffdf9; - margin-bottom: 12px; -} - -.service-row { - display: grid; - gap: 6px; - margin-bottom: 8px; -} - -.muted { - color: var(--muted); -} - -.mono { - font-family: var(--mono); -} - -.link { - color: var(--secondary); -} - -.env-note { - margin-top: 14px; -} - -@media (max-width: 720px) { - .hero { - flex-direction: column; - align-items: flex-start; - } - - .actions { - flex-direction: column; - align-items: stretch; - } - - .terminal { - height: 50vh; - } -} diff --git a/packages/docker-git/web/terminal.js b/packages/docker-git/web/terminal.js deleted file mode 100644 index 37047344..00000000 --- a/packages/docker-git/web/terminal.js +++ /dev/null @@ -1,97 +0,0 @@ -const terminalRoot = document.getElementById("terminal") - -if (!terminalRoot) { - return -} - -const projectId = terminalRoot.dataset.projectId -const codexLabel = terminalRoot.dataset.codexLabel -const wsPort = terminalRoot.dataset.wsPort - -const protocol = window.location.protocol === "https:" ? "wss" : "ws" -const host = window.location.hostname -const resolvedPort = wsPort && wsPort.length > 0 ? wsPort : window.location.port -const portSegment = resolvedPort && resolvedPort.length > 0 ? `:${resolvedPort}` : "" - -const buildPath = () => { - if (projectId && projectId.length > 0) { - return `/term/${encodeURIComponent(projectId)}` - } - const safeLabel = codexLabel && codexLabel.length > 0 ? codexLabel : "default" - return `/codex/${encodeURIComponent(safeLabel)}` -} - -const estimateSize = () => { - const width = terminalRoot.clientWidth || 900 - const height = terminalRoot.clientHeight || 420 - const cols = Math.max(60, Math.floor(width / 8)) - const rows = Math.max(16, Math.floor(height / 18)) - return { cols, rows } -} - -const initialSize = estimateSize() -const term = new Terminal({ - cols: initialSize.cols, - rows: initialSize.rows, - convertEol: true, - cursorBlink: true, - fontFamily: "'IBM Plex Mono', 'JetBrains Mono', 'Menlo', monospace", - fontSize: 13, - theme: { - background: "#0b0d10", - foreground: "#f5f7ff" - } -}) - -term.open(terminalRoot) -term.focus() - -const connect = () => { - const size = estimateSize() - term.resize(size.cols, size.rows) - const path = buildPath() - const wsUrl = `${protocol}://${host}${portSegment}${path}?cols=${size.cols}&rows=${size.rows}` - const socket = new WebSocket(wsUrl) - - const sendResize = (cols, rows) => { - if (socket.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify({ type: "resize", cols, rows })) - } - } - - term.onData((data) => { - if (socket.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify({ type: "input", data })) - } - }) - - socket.onmessage = (event) => { - try { - const payload = JSON.parse(event.data) - if (payload.type === "output" && typeof payload.data === "string") { - term.write(payload.data) - } else if (payload.type === "error") { - term.writeln(`\r\n[error] ${payload.message ?? "unknown"}\r\n`) - } - } catch { - term.write(event.data) - } - } - - socket.onopen = () => { - const next = estimateSize() - sendResize(next.cols, next.rows) - } - - socket.onclose = () => { - term.writeln("\r\n[terminal disconnected]\r\n") - } - - window.addEventListener("resize", () => { - const next = estimateSize() - term.resize(next.cols, next.rows) - sendResize(next.cols, next.rows) - }) -} - -connect() diff --git a/packages/web/.gitignore b/packages/web/.gitignore deleted file mode 100644 index 5ef6a520..00000000 --- a/packages/web/.gitignore +++ /dev/null @@ -1,41 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files (can opt-in for committing if needed) -.env* - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/packages/web/README.md b/packages/web/README.md deleted file mode 100644 index e215bc4c..00000000 --- a/packages/web/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/packages/web/eslint.config.mjs b/packages/web/eslint.config.mjs deleted file mode 100644 index 05e726d1..00000000 --- a/packages/web/eslint.config.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import { defineConfig, globalIgnores } from "eslint/config"; -import nextVitals from "eslint-config-next/core-web-vitals"; -import nextTs from "eslint-config-next/typescript"; - -const eslintConfig = defineConfig([ - ...nextVitals, - ...nextTs, - // Override default ignores of eslint-config-next. - globalIgnores([ - // Default ignores of eslint-config-next: - ".next/**", - "out/**", - "build/**", - "next-env.d.ts", - ]), -]); - -export default eslintConfig; diff --git a/packages/web/next.config.mjs b/packages/web/next.config.mjs deleted file mode 100644 index e91dbdfa..00000000 --- a/packages/web/next.config.mjs +++ /dev/null @@ -1,20 +0,0 @@ -import path from "node:path" -import { fileURLToPath } from "node:url" - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) - -/** @type {import('next').NextConfig} */ -const nextConfig = { - turbopack: { - root: path.join(__dirname, "..", "..") - }, - allowedDevOrigins: [ - "http://localhost:3000", - "http://localhost:3001", - "http://192.168.0.164:3000", - "http://192.168.0.164:3001" - ] -} - -export default nextConfig diff --git a/packages/web/package.json b/packages/web/package.json deleted file mode 100644 index d8d3100c..00000000 --- a/packages/web/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "web", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "concurrently -k \"next dev\" \"node scripts/terminal-ws.mjs\"", - "build": "next build", - "start": "next start", - "lint": "eslint", - "test": "vitest run" - }, - "dependencies": { - "@effect-template/lib": "workspace:*", - "@effect/platform-node": "^0.104.0", - "effect": "^3.19.14", - "next": "16.1.6", - "react": "19.2.3", - "react-dom": "19.2.3", - "ssh2": "^1.15.0", - "ws": "^8.18.0", - "xterm": "^5.3.0", - "xterm-addon-fit": "^0.8.0" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "concurrently": "^9.1.0", - "eslint": "^9", - "eslint-config-next": "16.1.6", - "tailwindcss": "^4", - "typescript": "^5", - "vitest": "^4.0.17" - } -} diff --git a/packages/web/pnpm-lock.yaml b/packages/web/pnpm-lock.yaml deleted file mode 100644 index ec7233da..00000000 --- a/packages/web/pnpm-lock.yaml +++ /dev/null @@ -1,4008 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - next: - specifier: 16.1.6 - version: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react: - specifier: 19.2.3 - version: 19.2.3 - react-dom: - specifier: 19.2.3 - version: 19.2.3(react@19.2.3) - devDependencies: - '@tailwindcss/postcss': - specifier: ^4 - version: 4.1.18 - '@types/node': - specifier: ^20 - version: 20.19.30 - '@types/react': - specifier: ^19 - version: 19.2.10 - '@types/react-dom': - specifier: ^19 - version: 19.2.3(@types/react@19.2.10) - eslint: - specifier: ^9 - version: 9.39.2(jiti@2.6.1) - eslint-config-next: - specifier: 16.1.6 - version: 16.1.6(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - tailwindcss: - specifier: ^4 - version: 4.1.18 - typescript: - specifier: ^5 - version: 5.9.3 - -packages: - - '@alloc/quick-lru@5.2.0': - resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} - engines: {node: '>=10'} - - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.29.0': - resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.29.0': - resolution: {integrity: sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} - 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.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} - 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.6': - resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.29.0': - resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} - engines: {node: '>=6.9.0'} - - '@emnapi/core@1.8.1': - resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} - - '@emnapi/runtime@1.8.1': - resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} - - '@emnapi/wasi-threads@1.1.0': - resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} - - '@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/config-array@0.21.1': - resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/eslintrc@3.3.3': - resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@9.39.2': - resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@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'} - - '@img/colour@1.0.0': - resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} - engines: {node: '>=18'} - - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} - cpu: [arm64] - os: [darwin] - - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} - cpu: [arm64] - os: [linux] - - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} - cpu: [arm] - os: [linux] - - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} - cpu: [ppc64] - os: [linux] - - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} - cpu: [riscv64] - os: [linux] - - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} - cpu: [s390x] - os: [linux] - - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} - cpu: [x64] - os: [linux] - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} - cpu: [arm64] - os: [linux] - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} - cpu: [x64] - os: [linux] - - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ppc64] - os: [linux] - - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [riscv64] - os: [linux] - - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [win32] - - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - - '@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==} - - '@napi-rs/wasm-runtime@0.2.12': - resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - - '@next/env@16.1.6': - resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==} - - '@next/eslint-plugin-next@16.1.6': - resolution: {integrity: sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==} - - '@next/swc-darwin-arm64@16.1.6': - resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@next/swc-darwin-x64@16.1.6': - resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@next/swc-linux-arm64-gnu@16.1.6': - resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@next/swc-linux-arm64-musl@16.1.6': - resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@next/swc-linux-x64-gnu@16.1.6': - resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@next/swc-linux-x64-musl@16.1.6': - resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@next/swc-win32-arm64-msvc@16.1.6': - resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@next/swc-win32-x64-msvc@16.1.6': - resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@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'} - - '@nolyfill/is-core-module@1.0.39': - resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} - engines: {node: '>=12.4.0'} - - '@rtsao/scc@1.1.0': - resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - - '@swc/helpers@0.5.15': - resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - - '@tailwindcss/node@4.1.18': - resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} - - '@tailwindcss/oxide-android-arm64@4.1.18': - resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - - '@tailwindcss/oxide-darwin-arm64@4.1.18': - resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@tailwindcss/oxide-darwin-x64@4.1.18': - resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@tailwindcss/oxide-freebsd-x64@4.1.18': - resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [freebsd] - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': - resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': - resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-musl@4.1.18': - resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@tailwindcss/oxide-linux-x64-gnu@4.1.18': - resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@tailwindcss/oxide-linux-x64-musl@4.1.18': - resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@tailwindcss/oxide-wasm32-wasi@4.1.18': - resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib - - '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': - resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@tailwindcss/oxide-win32-x64-msvc@4.1.18': - resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@tailwindcss/oxide@4.1.18': - resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} - engines: {node: '>= 10'} - - '@tailwindcss/postcss@4.1.18': - resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==} - - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/json5@0.0.29': - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - - '@types/node@20.19.30': - resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==} - - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} - peerDependencies: - '@types/react': ^19.2.0 - - '@types/react@19.2.10': - resolution: {integrity: sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==} - - '@typescript-eslint/eslint-plugin@8.54.0': - resolution: {integrity: sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.54.0 - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/parser@8.54.0': - resolution: {integrity: sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==} - 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/project-service@8.54.0': - resolution: {integrity: sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/scope-manager@8.54.0': - resolution: {integrity: sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.54.0': - resolution: {integrity: sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/type-utils@8.54.0': - resolution: {integrity: sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==} - 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/types@8.54.0': - resolution: {integrity: sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.54.0': - resolution: {integrity: sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.54.0': - resolution: {integrity: sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==} - 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/visitor-keys@8.54.0': - resolution: {integrity: sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==} - 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] - - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} - cpu: [arm64] - os: [linux] - - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} - cpu: [ppc64] - os: [linux] - - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} - cpu: [riscv64] - os: [linux] - - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} - cpu: [riscv64] - os: [linux] - - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} - cpu: [s390x] - os: [linux] - - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} - cpu: [x64] - os: [linux] - - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} - cpu: [x64] - os: [linux] - - '@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] - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - aria-query@5.3.2: - resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} - engines: {node: '>= 0.4'} - - 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.prototype.findlast@1.2.5: - resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} - engines: {node: '>= 0.4'} - - 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'} - - array.prototype.tosorted@1.1.4: - resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} - engines: {node: '>= 0.4'} - - arraybuffer.prototype.slice@1.0.4: - resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} - engines: {node: '>= 0.4'} - - ast-types-flow@0.0.8: - resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} - - async-function@1.0.0: - resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} - engines: {node: '>= 0.4'} - - available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - - axe-core@4.11.1: - resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} - engines: {node: '>=4'} - - axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - baseline-browser-mapping@2.9.19: - resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} - hasBin: true - - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - 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.30001767: - resolution: {integrity: sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - client-only@0.0.1: - resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - - 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==} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - damerau-levenshtein@1.0.8: - resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - - 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'} - - 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 - - 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-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - electron-to-chromium@1.5.283: - resolution: {integrity: sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - - enhanced-resolve@5.18.4: - resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} - engines: {node: '>=10.13.0'} - - es-abstract@1.24.1: - resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} - 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-iterator-helpers@1.2.2: - resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} - engines: {node: '>= 0.4'} - - 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'} - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-config-next@16.1.6: - resolution: {integrity: sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==} - peerDependencies: - eslint: '>=9.0.0' - typescript: '>=3.3.1' - peerDependenciesMeta: - typescript: - optional: true - - eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - - eslint-import-resolver-typescript@3.10.1: - resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} - engines: {node: ^14.18.0 || >=16.0.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-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-jsx-a11y@6.10.2: - resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} - engines: {node: '>=4.0'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - - eslint-plugin-react-hooks@7.0.1: - resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} - engines: {node: '>=18'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - - eslint-plugin-react@7.37.5: - resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - - eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - 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@9.39.2: - resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - 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} - - 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'} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-glob@3.3.1: - resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} - 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==} - - fastq@1.20.1: - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - - 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-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'} - - 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'} - - 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-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-symbol-description@1.1.0: - resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} - engines: {node: '>= 0.4'} - - get-tsconfig@4.13.1: - resolution: {integrity: sha512-EoY1N2xCn44xU6750Sx7OjOIT59FkmstNc3X6y5xpz7D5cBtZRe/3pSlTkDJgqsOk3WwZPkWfonhhUJfttQo3w==} - - 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'} - - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - - globals@16.4.0: - resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} - engines: {node: '>=18'} - - globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - - 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'} - - hermes-estree@0.25.1: - resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} - - hermes-parser@0.25.1: - resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} - - 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'} - - internal-slot@1.1.0: - resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} - engines: {node: '>= 0.4'} - - is-array-buffer@3.0.5: - resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} - engines: {node: '>= 0.4'} - - 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-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-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-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-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-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-string@1.1.1: - resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} - engines: {node: '>= 0.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'} - - isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - iterator.prototype@1.1.5: - resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} - engines: {node: '>= 0.4'} - - jiti@2.6.1: - resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} - hasBin: true - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - 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-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - 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 - - jsx-ast-utils@3.3.5: - resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} - engines: {node: '>=4.0'} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - language-subtag-registry@0.3.23: - resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} - - language-tags@1.0.9: - resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} - engines: {node: '>=0.10'} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - lightningcss-android-arm64@1.30.2: - resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.30.2: - resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.30.2: - resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.30.2: - resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.30.2: - resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.30.2: - resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - - lightningcss-linux-arm64-musl@1.30.2: - resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - - lightningcss-linux-x64-gnu@1.30.2: - resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - - lightningcss-linux-x64-musl@1.30.2: - resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - - lightningcss-win32-arm64-msvc@1.30.2: - resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.30.2: - resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.30.2: - resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} - engines: {node: '>= 12.0.0'} - - 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==} - - loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - - 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==} - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - 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==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - 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@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - next@16.1.6: - resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==} - engines: {node: '>=20.9.0'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.51.1 - babel-plugin-react-compiler: '*' - react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@playwright/test': - optional: true - babel-plugin-react-compiler: - optional: true - sass: - optional: true - - node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} - - 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.entries@1.1.9: - resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} - 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'} - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - own-keys@1.0.1: - resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} - engines: {node: '>= 0.4'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - 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==} - - 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'} - - possible-typed-array-names@1.1.0: - resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} - engines: {node: '>= 0.4'} - - postcss@8.4.31: - resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} - engines: {node: ^10 || ^12 || >=14} - - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} - engines: {node: ^10 || ^12 || >=14} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - prop-types@15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - react-dom@19.2.3: - resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} - peerDependencies: - react: ^19.2.3 - - react-is@16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - - react@19.2.3: - resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} - engines: {node: '>=0.10.0'} - - reflect.getprototypeof@1.0.10: - resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} - engines: {node: '>= 0.4'} - - regexp.prototype.flags@1.5.4: - resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} - engines: {node: '>= 0.4'} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - 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 - - resolve@2.0.0-next.5: - resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} - hasBin: true - - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - 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'} - - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - - 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 - - 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'} - - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - - 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'} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - stable-hash@0.0.5: - resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} - - stop-iteration-iterator@1.1.0: - resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} - engines: {node: '>= 0.4'} - - string.prototype.includes@2.0.1: - resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} - engines: {node: '>= 0.4'} - - string.prototype.matchall@4.0.12: - resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} - engines: {node: '>= 0.4'} - - string.prototype.repeat@1.0.0: - resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} - - 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-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - styled-jsx@5.1.6: - resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@babel/core': '*' - babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' - peerDependenciesMeta: - '@babel/core': - optional: true - babel-plugin-macros: - optional: true - - 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'} - - tailwindcss@4.1.18: - resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} - - tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} - engines: {node: '>=6'} - - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - ts-api-utils@2.4.0: - resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - - 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==} - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - 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.54.0: - resolution: {integrity: sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==} - 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@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@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - - unrs-resolver@1.11.1: - resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - 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.20: - resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} - engines: {node: '>= 0.4'} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - - zod-validation-error@4.0.2: - resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - - zod@4.3.6: - resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} - -snapshots: - - '@alloc/quick-lru@5.2.0': {} - - '@babel/code-frame@7.29.0': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.29.0': {} - - '@babel/core@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.0 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.28.6 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@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.29.0': - dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/helper-compilation-targets@7.28.6': - dependencies: - '@babel/compat-data': 7.29.0 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.1 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-globals@7.28.0': {} - - '@babel/helper-module-imports@7.28.6': - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 - 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.6': - dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - - '@babel/parser@7.29.0': - dependencies: - '@babel/types': 7.29.0 - - '@babel/template@7.28.6': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - - '@babel/traverse@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.0 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.29.0': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - - '@emnapi/core@1.8.1': - dependencies: - '@emnapi/wasi-threads': 1.1.0 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.8.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.1.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': - dependencies: - eslint: 9.39.2(jiti@2.6.1) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} - - '@eslint/config-array@0.21.1': - dependencies: - '@eslint/object-schema': 2.1.7 - debug: 4.4.3 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.4.2': - dependencies: - '@eslint/core': 0.17.0 - - '@eslint/core@0.17.0': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/eslintrc@3.3.3': - dependencies: - ajv: 6.12.6 - 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.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.39.2': {} - - '@eslint/object-schema@2.1.7': {} - - '@eslint/plugin-kit@0.4.1': - dependencies: - '@eslint/core': 0.17.0 - 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': {} - - '@img/colour@1.0.0': - optional: true - - '@img/sharp-darwin-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 - optional: true - - '@img/sharp-darwin-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 - optional: true - - '@img/sharp-libvips-darwin-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-darwin-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm@1.2.4': - optional: true - - '@img/sharp-libvips-linux-ppc64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-riscv64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-s390x@1.2.4': - optional: true - - '@img/sharp-libvips-linux-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - optional: true - - '@img/sharp-linux-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 - optional: true - - '@img/sharp-linux-arm@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 - optional: true - - '@img/sharp-linux-ppc64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 - optional: true - - '@img/sharp-linux-riscv64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 - optional: true - - '@img/sharp-linux-s390x@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 - optional: true - - '@img/sharp-linux-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 - optional: true - - '@img/sharp-linuxmusl-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - optional: true - - '@img/sharp-linuxmusl-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - optional: true - - '@img/sharp-wasm32@0.34.5': - dependencies: - '@emnapi/runtime': 1.8.1 - optional: true - - '@img/sharp-win32-arm64@0.34.5': - optional: true - - '@img/sharp-win32-ia32@0.34.5': - optional: true - - '@img/sharp-win32-x64@0.34.5': - optional: true - - '@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 - - '@napi-rs/wasm-runtime@0.2.12': - dependencies: - '@emnapi/core': 1.8.1 - '@emnapi/runtime': 1.8.1 - '@tybys/wasm-util': 0.10.1 - optional: true - - '@next/env@16.1.6': {} - - '@next/eslint-plugin-next@16.1.6': - dependencies: - fast-glob: 3.3.1 - - '@next/swc-darwin-arm64@16.1.6': - optional: true - - '@next/swc-darwin-x64@16.1.6': - optional: true - - '@next/swc-linux-arm64-gnu@16.1.6': - optional: true - - '@next/swc-linux-arm64-musl@16.1.6': - optional: true - - '@next/swc-linux-x64-gnu@16.1.6': - optional: true - - '@next/swc-linux-x64-musl@16.1.6': - optional: true - - '@next/swc-win32-arm64-msvc@16.1.6': - optional: true - - '@next/swc-win32-x64-msvc@16.1.6': - 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.20.1 - - '@nolyfill/is-core-module@1.0.39': {} - - '@rtsao/scc@1.1.0': {} - - '@swc/helpers@0.5.15': - dependencies: - tslib: 2.8.1 - - '@tailwindcss/node@4.1.18': - dependencies: - '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.18.4 - jiti: 2.6.1 - lightningcss: 1.30.2 - magic-string: 0.30.21 - source-map-js: 1.2.1 - tailwindcss: 4.1.18 - - '@tailwindcss/oxide-android-arm64@4.1.18': - optional: true - - '@tailwindcss/oxide-darwin-arm64@4.1.18': - optional: true - - '@tailwindcss/oxide-darwin-x64@4.1.18': - optional: true - - '@tailwindcss/oxide-freebsd-x64@4.1.18': - optional: true - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': - optional: true - - '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': - optional: true - - '@tailwindcss/oxide-linux-arm64-musl@4.1.18': - optional: true - - '@tailwindcss/oxide-linux-x64-gnu@4.1.18': - optional: true - - '@tailwindcss/oxide-linux-x64-musl@4.1.18': - optional: true - - '@tailwindcss/oxide-wasm32-wasi@4.1.18': - optional: true - - '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': - optional: true - - '@tailwindcss/oxide-win32-x64-msvc@4.1.18': - optional: true - - '@tailwindcss/oxide@4.1.18': - optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.18 - '@tailwindcss/oxide-darwin-arm64': 4.1.18 - '@tailwindcss/oxide-darwin-x64': 4.1.18 - '@tailwindcss/oxide-freebsd-x64': 4.1.18 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 - '@tailwindcss/oxide-linux-x64-musl': 4.1.18 - '@tailwindcss/oxide-wasm32-wasi': 4.1.18 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 - - '@tailwindcss/postcss@4.1.18': - dependencies: - '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.1.18 - '@tailwindcss/oxide': 4.1.18 - postcss: 8.5.6 - tailwindcss: 4.1.18 - - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@types/estree@1.0.8': {} - - '@types/json-schema@7.0.15': {} - - '@types/json5@0.0.29': {} - - '@types/node@20.19.30': - dependencies: - undici-types: 6.21.0 - - '@types/react-dom@19.2.3(@types/react@19.2.10)': - dependencies: - '@types/react': 19.2.10 - - '@types/react@19.2.10': - dependencies: - csstype: 3.2.3 - - '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.54.0 - '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.54.0 - eslint: 9.39.2(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.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.54.0 - '@typescript-eslint/types': 8.54.0 - '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.54.0 - debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.54.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) - '@typescript-eslint/types': 8.54.0 - debug: 4.4.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@8.54.0': - dependencies: - '@typescript-eslint/types': 8.54.0 - '@typescript-eslint/visitor-keys': 8.54.0 - - '@typescript-eslint/tsconfig-utils@8.54.0(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@typescript-eslint/type-utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.54.0 - '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - debug: 4.4.3 - eslint: 9.39.2(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.54.0': {} - - '@typescript-eslint/typescript-estree@8.54.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.54.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) - '@typescript-eslint/types': 8.54.0 - '@typescript-eslint/visitor-keys': 8.54.0 - debug: 4.4.3 - minimatch: 9.0.5 - semver: 7.7.3 - 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.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.54.0 - '@typescript-eslint/types': 8.54.0 - '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.54.0': - dependencies: - '@typescript-eslint/types': 8.54.0 - eslint-visitor-keys: 4.2.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 - - acorn-jsx@5.3.2(acorn@8.15.0): - dependencies: - acorn: 8.15.0 - - acorn@8.15.0: {} - - ajv@6.12.6: - 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 - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - argparse@2.0.1: {} - - aria-query@5.3.2: {} - - 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.1 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - is-string: 1.1.1 - math-intrinsics: 1.1.0 - - array.prototype.findlast@1.2.5: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.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.1 - 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.1 - 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.1 - es-shim-unscopables: 1.1.0 - - array.prototype.tosorted@1.1.4: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.1 - es-errors: 1.3.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.1 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.5 - - ast-types-flow@0.0.8: {} - - async-function@1.0.0: {} - - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.1.0 - - axe-core@4.11.1: {} - - axobject-query@4.1.0: {} - - balanced-match@1.0.2: {} - - baseline-browser-mapping@2.9.19: {} - - 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 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.28.1: - dependencies: - baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001767 - electron-to-chromium: 1.5.283 - node-releases: 2.0.27 - update-browserslist-db: 1.2.3(browserslist@4.28.1) - - 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.30001767: {} - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - client-only@0.0.1: {} - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - concat-map@0.0.1: {} - - convert-source-map@2.0.0: {} - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - csstype@3.2.3: {} - - damerau-levenshtein@1.0.8: {} - - 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 - - debug@3.2.7: - dependencies: - ms: 2.1.3 - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - 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-libc@2.1.2: {} - - doctrine@2.1.0: - dependencies: - esutils: 2.0.3 - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - electron-to-chromium@1.5.283: {} - - emoji-regex@9.2.2: {} - - enhanced-resolve@5.18.4: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.0 - - es-abstract@1.24.1: - 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.20 - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-iterator-helpers@1.2.2: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.1 - es-errors: 1.3.0 - es-set-tostringtag: 2.1.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.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 - internal-slot: 1.1.0 - iterator.prototype: 1.1.5 - safe-array-concat: 1.1.3 - - 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 - - escalade@3.2.0: {} - - escape-string-regexp@4.0.0: {} - - eslint-config-next@16.1.6(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): - dependencies: - '@next/eslint-plugin-next': 16.1.6 - eslint: 9.39.2(jiti@2.6.1) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1)) - globals: 16.4.0 - typescript-eslint: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - '@typescript-eslint/parser' - - eslint-import-resolver-webpack - - eslint-plugin-import-x - - supports-color - - 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@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) - get-tsconfig: 4.13.1 - is-bun-module: 2.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.15 - unrs-resolver: 1.11.1 - optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) - transitivePeerDependencies: - - supports-color - - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(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: 9.39.2(jiti@2.6.1) - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(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.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.6.1)): - dependencies: - aria-query: 5.3.2 - array-includes: 3.1.9 - array.prototype.flatmap: 1.3.3 - ast-types-flow: 0.0.8 - axe-core: 4.11.1 - axobject-query: 4.1.0 - damerau-levenshtein: 1.0.8 - emoji-regex: 9.2.2 - eslint: 9.39.2(jiti@2.6.1) - hasown: 2.0.2 - jsx-ast-utils: 3.3.5 - language-tags: 1.0.9 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - safe-regex-test: 1.1.0 - string.prototype.includes: 2.0.1 - - eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)): - dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.0 - eslint: 9.39.2(jiti@2.6.1) - hermes-parser: 0.25.1 - zod: 4.3.6 - zod-validation-error: 4.0.2(zod@4.3.6) - transitivePeerDependencies: - - supports-color - - eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)): - dependencies: - array-includes: 3.1.9 - array.prototype.findlast: 1.2.5 - array.prototype.flatmap: 1.3.3 - array.prototype.tosorted: 1.1.4 - doctrine: 2.1.0 - es-iterator-helpers: 1.2.2 - eslint: 9.39.2(jiti@2.6.1) - estraverse: 5.3.0 - hasown: 2.0.2 - jsx-ast-utils: 3.3.5 - minimatch: 3.1.2 - object.entries: 1.1.9 - object.fromentries: 2.0.8 - object.values: 1.2.1 - prop-types: 15.8.1 - resolve: 2.0.0-next.5 - semver: 6.3.1 - string.prototype.matchall: 4.0.12 - string.prototype.repeat: 1.0.0 - - eslint-scope@8.4.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.2.1: {} - - eslint@9.39.2(jiti@2.6.1): - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.3 - '@eslint/js': 9.39.2 - '@eslint/plugin-kit': 0.4.1 - '@humanfs/node': 0.16.7 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.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 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - optionalDependencies: - jiti: 2.6.1 - transitivePeerDependencies: - - supports-color - - espree@10.4.0: - dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 4.2.1 - - esquery@1.7.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - esutils@2.0.3: {} - - fast-deep-equal@3.1.3: {} - - fast-glob@3.3.1: - 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: {} - - fastq@1.20.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-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 - - 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 - - functions-have-names@1.2.3: {} - - generator-function@2.0.1: {} - - gensync@1.0.0-beta.2: {} - - 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-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.1: - dependencies: - resolve-pkg-maps: 1.0.0 - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - globals@14.0.0: {} - - globals@16.4.0: {} - - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 - - 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 - - hermes-estree@0.25.1: {} - - hermes-parser@0.25.1: - dependencies: - hermes-estree: 0.25.1 - - 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: {} - - internal-slot@1.1.0: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.1.0 - - is-array-buffer@3.0.5: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - 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-bun-module@2.0.0: - dependencies: - semver: 7.7.3 - - 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-extglob@2.1.1: {} - - is-finalizationregistry@1.1.1: - dependencies: - call-bound: 1.0.4 - - 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-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-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-string@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - 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.20 - - 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 - - isarray@2.0.5: {} - - isexe@2.0.0: {} - - iterator.prototype@1.1.5: - dependencies: - define-data-property: 1.1.4 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - has-symbols: 1.1.0 - set-function-name: 2.0.2 - - jiti@2.6.1: {} - - js-tokens@4.0.0: {} - - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 - - jsesc@3.1.0: {} - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - json5@1.0.2: - dependencies: - minimist: 1.2.8 - - json5@2.2.3: {} - - jsx-ast-utils@3.3.5: - dependencies: - array-includes: 3.1.9 - array.prototype.flat: 1.3.3 - object.assign: 4.1.7 - object.values: 1.2.1 - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - language-subtag-registry@0.3.23: {} - - language-tags@1.0.9: - dependencies: - language-subtag-registry: 0.3.23 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - lightningcss-android-arm64@1.30.2: - optional: true - - lightningcss-darwin-arm64@1.30.2: - optional: true - - lightningcss-darwin-x64@1.30.2: - optional: true - - lightningcss-freebsd-x64@1.30.2: - optional: true - - lightningcss-linux-arm-gnueabihf@1.30.2: - optional: true - - lightningcss-linux-arm64-gnu@1.30.2: - optional: true - - lightningcss-linux-arm64-musl@1.30.2: - optional: true - - lightningcss-linux-x64-gnu@1.30.2: - optional: true - - lightningcss-linux-x64-musl@1.30.2: - optional: true - - lightningcss-win32-arm64-msvc@1.30.2: - optional: true - - lightningcss-win32-x64-msvc@1.30.2: - optional: true - - lightningcss@1.30.2: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.30.2 - lightningcss-darwin-arm64: 1.30.2 - lightningcss-darwin-x64: 1.30.2 - lightningcss-freebsd-x64: 1.30.2 - lightningcss-linux-arm-gnueabihf: 1.30.2 - lightningcss-linux-arm64-gnu: 1.30.2 - lightningcss-linux-arm64-musl: 1.30.2 - lightningcss-linux-x64-gnu: 1.30.2 - lightningcss-linux-x64-musl: 1.30.2 - lightningcss-win32-arm64-msvc: 1.30.2 - lightningcss-win32-x64-msvc: 1.30.2 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash.merge@4.6.2: {} - - loose-envify@1.4.0: - dependencies: - js-tokens: 4.0.0 - - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - math-intrinsics@1.1.0: {} - - merge2@1.4.1: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.12 - - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.2 - - minimist@1.2.8: {} - - ms@2.1.3: {} - - nanoid@3.3.11: {} - - napi-postinstall@0.3.4: {} - - natural-compare@1.4.0: {} - - next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): - dependencies: - '@next/env': 16.1.6 - '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001767 - postcss: 8.4.31 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.3) - optionalDependencies: - '@next/swc-darwin-arm64': 16.1.6 - '@next/swc-darwin-x64': 16.1.6 - '@next/swc-linux-arm64-gnu': 16.1.6 - '@next/swc-linux-arm64-musl': 16.1.6 - '@next/swc-linux-x64-gnu': 16.1.6 - '@next/swc-linux-x64-musl': 16.1.6 - '@next/swc-win32-arm64-msvc': 16.1.6 - '@next/swc-win32-x64-msvc': 16.1.6 - sharp: 0.34.5 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - - node-releases@2.0.27: {} - - 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.entries@1.1.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - object.fromentries@2.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.1 - 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.1 - - 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 - - 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 - - own-keys@1.0.1: - dependencies: - get-intrinsic: 1.3.0 - object-keys: 1.1.1 - safe-push-apply: 1.0.0 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - path-exists@4.0.0: {} - - path-key@3.1.1: {} - - path-parse@1.0.7: {} - - picocolors@1.1.1: {} - - picomatch@2.3.1: {} - - picomatch@4.0.3: {} - - possible-typed-array-names@1.1.0: {} - - postcss@8.4.31: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - postcss@8.5.6: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prelude-ls@1.2.1: {} - - prop-types@15.8.1: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react-is: 16.13.1 - - punycode@2.3.1: {} - - queue-microtask@1.2.3: {} - - react-dom@19.2.3(react@19.2.3): - dependencies: - react: 19.2.3 - scheduler: 0.27.0 - - react-is@16.13.1: {} - - react@19.2.3: {} - - reflect.getprototypeof@1.0.10: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.1 - 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.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 - - resolve-from@4.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 - - resolve@2.0.0-next.5: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - reusify@1.1.0: {} - - 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 - - scheduler@0.27.0: {} - - semver@6.3.1: {} - - semver@7.7.3: {} - - 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 - - sharp@0.34.5: - dependencies: - '@img/colour': 1.0.0 - detect-libc: 2.1.2 - semver: 7.7.3 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 - optional: true - - 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 - - source-map-js@1.2.1: {} - - stable-hash@0.0.5: {} - - stop-iteration-iterator@1.1.0: - dependencies: - es-errors: 1.3.0 - internal-slot: 1.1.0 - - string.prototype.includes@2.0.1: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.1 - - string.prototype.matchall@4.0.12: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - regexp.prototype.flags: 1.5.4 - set-function-name: 2.0.2 - side-channel: 1.1.0 - - string.prototype.repeat@1.0.0: - dependencies: - define-properties: 1.2.1 - es-abstract: 1.24.1 - - 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.1 - 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-bom@3.0.0: {} - - strip-json-comments@3.1.1: {} - - styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.3): - dependencies: - client-only: 0.0.1 - react: 19.2.3 - optionalDependencies: - '@babel/core': 7.29.0 - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - - tailwindcss@4.1.18: {} - - tapable@2.3.0: {} - - tinyglobby@0.2.15: - dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - ts-api-utils@2.4.0(typescript@5.9.3): - dependencies: - 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: {} - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - 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.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): - dependencies: - '@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.2(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@6.21.0: {} - - 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.2.3(browserslist@4.28.1): - dependencies: - browserslist: 4.28.1 - escalade: 3.2.0 - picocolors: 1.1.1 - - uri-js@4.4.1: - dependencies: - punycode: 2.3.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.20 - - 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.20: - 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 - - word-wrap@1.2.5: {} - - yallist@3.1.1: {} - - yocto-queue@0.1.0: {} - - zod-validation-error@4.0.2(zod@4.3.6): - dependencies: - zod: 4.3.6 - - zod@4.3.6: {} diff --git a/packages/web/pnpm-workspace.yaml b/packages/web/pnpm-workspace.yaml deleted file mode 100644 index 581a9d5b..00000000 --- a/packages/web/pnpm-workspace.yaml +++ /dev/null @@ -1,3 +0,0 @@ -ignoredBuiltDependencies: - - sharp - - unrs-resolver diff --git a/packages/web/postcss.config.mjs b/packages/web/postcss.config.mjs deleted file mode 100644 index 61e36849..00000000 --- a/packages/web/postcss.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -const config = { - plugins: { - "@tailwindcss/postcss": {}, - }, -}; - -export default config; diff --git a/packages/web/public/file.svg b/packages/web/public/file.svg deleted file mode 100644 index 004145cd..00000000 --- a/packages/web/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/web/public/globe.svg b/packages/web/public/globe.svg deleted file mode 100644 index 567f17b0..00000000 --- a/packages/web/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/web/public/next.svg b/packages/web/public/next.svg deleted file mode 100644 index 5174b28c..00000000 --- a/packages/web/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/web/public/vercel.svg b/packages/web/public/vercel.svg deleted file mode 100644 index 77053960..00000000 --- a/packages/web/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/web/public/window.svg b/packages/web/public/window.svg deleted file mode 100644 index b2b2a44f..00000000 --- a/packages/web/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/web/scripts/terminal-ws.mjs b/packages/web/scripts/terminal-ws.mjs deleted file mode 100644 index 83c6c300..00000000 --- a/packages/web/scripts/terminal-ws.mjs +++ /dev/null @@ -1,583 +0,0 @@ -import { createServer } from "node:http"; -import fs from "node:fs"; -import path from "node:path"; -import { randomUUID } from "node:crypto"; -import { WebSocketServer } from "ws"; -import { Client } from "ssh2"; -import { spawn } from "node:child_process"; -import { Effect } from "effect"; -import { NodeContext } from "@effect/platform-node"; -import { readProjectConfig as readProjectConfigEffect } from "@effect-template/lib/shell/config"; -import { createProject } from "@effect-template/lib/usecases/actions"; - -const basePort = Number(process.env.TERMINAL_WS_PORT ?? "3001"); -const maxAttempts = 20; -const infoFile = "/tmp/docker-git-terminal-ws.json"; -const sessionsFile = "/tmp/docker-git-terminal-sessions.json"; -const sessions = new Map(); -const liveSessions = new Map(); - -const nowIso = () => new Date().toISOString(); - -// CHANGE: persist active terminal sessions for discovery -// WHY: expose running terminals to the web UI and agents -// QUOTE(ТЗ): "мы можем получать список всех запущенных терминалов?" -// REF: user-request-2026-02-04-terminal-sessions -// SOURCE: n/a -// FORMAT THEOREM: ∀s ∈ sessions: stored(s) → visible(s) -// PURITY: SHELL -// EFFECT: Effect -// INVARIANT: session.id уникален в registry -// COMPLEXITY: O(n) -const writeSessions = () => { - try { - const payload = { - updatedAt: nowIso(), - sessions: Array.from(sessions.values()) - }; - fs.writeFileSync(sessionsFile, JSON.stringify(payload, null, 2)); - } catch { - // Ignore telemetry write failures. - } -}; - -writeSessions(); - -const registerSession = (session) => { - sessions.set(session.id, { ...session, updatedAt: nowIso() }); - writeSessions(); -}; - -const updateSession = (sessionId, patch) => { - const current = sessions.get(sessionId); - if (!current) { - return; - } - sessions.set(sessionId, { ...current, ...patch, updatedAt: nowIso() }); - writeSessions(); -}; - -const removeSession = (sessionId) => { - if (!sessions.has(sessionId)) { - return; - } - sessions.delete(sessionId); - writeSessions(); -}; - -const nextSessionId = () => { - try { - return randomUUID(); - } catch { - return `term-${Date.now()}-${Math.random().toString(16).slice(2)}`; - } -}; - -const sendMessage = (socket, payload) => { - if (socket.readyState !== socket.OPEN) { - return; - } - socket.send(JSON.stringify(payload)); -}; - -const broadcastSession = (session, payload) => { - for (const socket of session.sockets) { - sendMessage(socket, payload); - } -}; - -const decodeMessage = (raw) => { - try { - const parsed = JSON.parse(raw); - if (parsed.type === "input" && typeof parsed.data === "string") { - return parsed; - } - if (parsed.type === "resize" && typeof parsed.cols === "number" && typeof parsed.rows === "number") { - return parsed; - } - if (parsed.type === "control" && parsed.action === "close") { - return parsed; - } - return null; - } catch { - return null; - } -}; - -const readProjectTemplate = (projectDir) => { - const configPath = path.join(projectDir, "docker-git.json"); - const raw = fs.readFileSync(configPath, "utf8"); - const parsed = JSON.parse(raw); - return parsed?.template ?? parsed; -}; - -const findKeyPath = (startDir) => { - let current = startDir; - const root = path.parse(startDir).root; - while (true) { - const candidate = path.join(current, "dev_ssh_key"); - if (fs.existsSync(candidate)) { - return candidate; - } - if (current === root) { - break; - } - current = path.dirname(current); - } - return path.join("/home/user/docker-git", "dev_ssh_key"); -}; - -const runComposeUp = (projectDir) => - new Promise((resolve, reject) => { - const child = spawn("docker", ["compose", "up", "-d", "--build"], { - cwd: projectDir, - stdio: "ignore" - }); - child.on("exit", (code) => { - if (code === 0) { - resolve(); - } else { - reject(new Error(`docker compose up failed (${code})`)); - } - }); - child.on("error", reject); - }); - -const runComposeWithLogs = (projectDir, args, send, label) => - new Promise((resolve, reject) => { - send({ type: "info", data: `[recreate] ${label}` }); - const child = spawn("docker", ["compose", ...args], { - cwd: projectDir, - stdio: ["ignore", "pipe", "pipe"] - }); - child.stdout.on("data", (data) => { - send({ type: "output", data: data.toString("utf8") }); - }); - child.stderr.on("data", (data) => { - send({ type: "output", data: data.toString("utf8") }); - }); - child.on("exit", (code) => { - if (code === 0) { - resolve(); - } else { - reject(new Error(`docker compose ${args.join(" ")} failed (${code})`)); - } - }); - child.on("error", reject); - }); - -const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - -const runRecreateFlow = async (projectDir, send) => { - await runComposeWithLogs(projectDir, ["down", "--volumes"], send, "docker compose down --volumes"); - send({ type: "info", data: "[recreate] syncing project files" }); - const program = Effect.gen(function*(_) { - const config = yield* _(readProjectConfigEffect(projectDir)); - yield* _( - createProject({ - _tag: "Create", - config: config.template, - outDir: projectDir, - runUp: false, - openSsh: false, - force: true, - forceEnv: false, - waitForClone: false - }) - ); - }); - await Effect.runPromise(Effect.provide(program, NodeContext.layer)); - await runComposeWithLogs(projectDir, ["--progress", "plain", "build"], send, "docker compose --progress plain build"); - await runComposeWithLogs(projectDir, ["up", "-d"], send, "docker compose up -d"); -}; - -const runComposeLogsFollow = (projectDir, send) => { - send({ type: "info", data: "[recreate] docker compose logs --follow --tail 0" }); - const child = spawn("docker", ["compose", "logs", "--follow", "--tail", "0"], { - cwd: projectDir, - stdio: ["ignore", "pipe", "pipe"] - }); - child.stdout.on("data", (data) => { - send({ type: "output", data: data.toString("utf8") }); - }); - child.stderr.on("data", (data) => { - send({ type: "output", data: data.toString("utf8") }); - }); - return child; -}; - -const stopComposeLogsFollow = (child) => { - if (!child || child.killed) { - return; - } - child.kill("SIGINT"); -}; - -const postRecreateStatus = async (projectId, phase, message) => { - const base = process.env.WEB_BASE_URL ?? "http://localhost:3000"; - try { - await fetch(`${base}/api/projects/${encodeURIComponent(projectId)}/recreate/status`, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ phase, message }) - }); - } catch { - // Ignore status update failures; terminal output is source of truth. - } -}; - -const connectSshWithRetry = async (config, send, retries = 30, delayMs = 2000) => { - for (let attempt = 1; attempt <= retries; attempt += 1) { - try { - const client = await new Promise((resolve, reject) => { - const next = new Client(); - const privateKey = fs.readFileSync(config.keyPath); - next.on("ready", () => resolve(next)); - next.on("error", reject); - next.connect({ - host: "localhost", - port: config.sshPort, - username: config.sshUser, - privateKey, - readyTimeout: 15000, - hostHash: "sha256", - hostVerifier: () => true - }); - }); - return client; - } catch (error) { - if (attempt === retries) { - throw error; - } - send({ - type: "info", - data: `[recreate] waiting for SSH (${attempt}/${retries})...` - }); - await sleep(delayMs); - } - } - throw new Error("SSH connection failed"); -}; - -const startSshSession = async (projectDir) => { - const config = readProjectTemplate(projectDir); - await runComposeUp(projectDir); - const keyPath = findKeyPath(projectDir); - - return { - sshUser: config.sshUser ?? "dev", - sshPort: Number(config.sshPort ?? 2222), - keyPath, - displayName: config.repoUrl ? config.repoUrl.split("/").slice(-2).join("/") : projectDir - }; -}; - -const openShell = (client, displayName, send) => - new Promise((resolve, reject) => { - send({ type: "info", data: `[docker-git] attached to ${displayName}` }); - client.shell( - { - term: "xterm-256color", - cols: 120, - rows: 32 - }, - (error, stream) => { - if (error) { - reject(error); - return; - } - resolve(stream); - } - ); - }); - -const connectWithMode = async (projectId, mode, send) => { - if (mode === "recreate") { - send({ type: "info", data: "[recreate] mode=on" }); - void postRecreateStatus(projectId, "running", "Recreate started"); - send({ type: "info", data: "[recreate] starting..." }); - const config = readProjectTemplate(projectId); - const keyPath = findKeyPath(projectId); - let logsChild = null; - try { - await runRecreateFlow(projectId, send); - logsChild = runComposeLogsFollow(projectId, send); - void postRecreateStatus(projectId, "success", "Recreate completed"); - } catch (error) { - void postRecreateStatus(projectId, "error", String(error)); - throw error; - } - let client; - try { - client = await connectSshWithRetry( - { - sshUser: config.sshUser ?? "dev", - sshPort: Number(config.sshPort ?? 2222), - keyPath - }, - send - ); - } finally { - stopComposeLogsFollow(logsChild); - } - const displayName = config.repoUrl ? config.repoUrl.split("/").slice(-2).join("/") : projectId; - const stream = await openShell(client, displayName, send); - return { client, stream, displayName }; - } - - send({ type: "info", data: "[terminal] mode=default" }); - const { sshUser, sshPort, keyPath, displayName } = await startSshSession(projectId); - const client = new Client(); - const privateKey = fs.readFileSync(keyPath); - await new Promise((resolve, reject) => { - client.on("ready", resolve); - client.on("error", reject); - client.connect({ - host: "localhost", - port: sshPort, - username: sshUser, - privateKey, - readyTimeout: 15000, - hostHash: "sha256", - hostVerifier: () => true - }); - }); - const stream = await openShell(client, displayName, send); - return { client, stream, displayName }; -}; - -const closeRuntimeSession = (sessionId, reason) => { - const session = liveSessions.get(sessionId); - if (!session) { - removeSession(sessionId); - return; - } - liveSessions.delete(sessionId); - if (reason) { - broadcastSession(session, { type: "info", data: reason }); - } - for (const socket of session.sockets) { - try { - socket.close(); - } catch { - // ignore - } - } - session.sockets.clear(); - if (session.stream?.close) { - try { - session.stream.close(); - } catch { - // ignore - } - } - if (session.stream?.end) { - try { - session.stream.end(); - } catch { - // ignore - } - } - if (session.client) { - session.client.end(); - } - removeSession(sessionId); -}; - -const detachSocket = (session, socket) => { - session.sockets.delete(socket); - if (session.sockets.size === 0) { - updateSession(session.id, { status: "detached" }); - } -}; - -const attachSocket = (session, socket) => { - session.sockets.add(socket); - updateSession(session.id, { status: "connected" }); - - socket.on("message", (payload) => { - const raw = typeof payload === "string" ? payload : payload.toString("utf8"); - const command = decodeMessage(raw); - if (!command) { - return; - } - if (command.type === "control" && command.action === "close") { - closeRuntimeSession(session.id, "[terminal] session closed"); - return; - } - if (!session.stream) { - return; - } - if (command.type === "input") { - session.stream.write(command.data); - } - if (command.type === "resize") { - session.stream.setWindow(command.rows, command.cols, 0, 0); - } - }); - - socket.on("close", () => detachSocket(session, socket)); - socket.on("error", () => detachSocket(session, socket)); -}; - -const ensureRuntimeSession = (sessionId, projectId, mode, source, send) => { - const existing = liveSessions.get(sessionId); - if (existing) { - return existing.ready.then(() => existing); - } - - const session = { - id: sessionId, - projectId, - mode, - source, - sockets: new Set(), - client: null, - stream: null, - displayName: projectId, - ready: null - }; - - registerSession({ - id: sessionId, - projectId, - displayName: projectId, - mode, - source, - status: "connecting", - connectedAt: nowIso() - }); - - session.ready = connectWithMode(projectId, mode, send) - .then(({ client, stream, displayName }) => { - session.client = client; - session.stream = stream; - session.displayName = displayName; - updateSession(sessionId, { status: "connected", displayName }); - - stream.on("data", (data) => { - broadcastSession(session, { type: "output", data: data.toString("utf8") }); - }); - - stream.stderr.on("data", (data) => { - broadcastSession(session, { type: "output", data: data.toString("utf8") }); - }); - - client.on("error", (error) => { - broadcastSession(session, { type: "error", data: String(error) }); - closeRuntimeSession(sessionId, "[terminal] connection error"); - }); - - client.on("close", () => { - closeRuntimeSession(sessionId, "[terminal] connection closed"); - }); - - return session; - }) - .catch((error) => { - broadcastSession(session, { type: "error", data: String(error) }); - liveSessions.delete(sessionId); - removeSession(sessionId); - throw error; - }); - - liveSessions.set(sessionId, session); - return session.ready.then(() => session); -}; - -const server = createServer((_req, res) => { - res.writeHead(200); - res.end("docker-git terminal ws"); -}); - -const wss = new WebSocketServer({ server, path: "/terminal" }); - -wss.on("error", (error) => { - if (error?.code === "EADDRINUSE") { - return; - } - console.error("[terminal-ws] websocket error", error); -}); - -wss.on("connection", (socket, request) => { - const requestUrl = request.url ?? ""; - const url = new URL(requestUrl, "http://localhost"); - const projectId = url.searchParams.get("projectId"); - const sessionId = url.searchParams.get("sessionId") ?? nextSessionId(); - const action = url.searchParams.get("action"); - const source = url.searchParams.get("source") ?? "web"; - const mode = url.searchParams.get("mode") === "recreate" ? "recreate" : "default"; - - if (action === "close") { - closeRuntimeSession(sessionId, "[terminal] closed by request"); - socket.close(); - return; - } - - if (!projectId) { - sendMessage(socket, { type: "error", data: "projectId is required" }); - socket.close(); - return; - } - - const existing = liveSessions.get(sessionId); - if (existing && existing.projectId !== projectId) { - sendMessage(socket, { type: "error", data: "sessionId belongs to another project" }); - socket.close(); - return; - } - - const send = (payload) => sendMessage(socket, payload); - send({ type: "info", data: "[docker-git] connecting terminal…" }); - - const attachAfterReady = (session) => { - attachSocket(session, socket); - }; - - if (existing) { - existing.ready - .then(() => attachAfterReady(existing)) - .catch((error) => { - sendMessage(socket, { type: "error", data: String(error) }); - socket.close(); - }); - return; - } - - ensureRuntimeSession(sessionId, projectId, mode, source, send) - .then((session) => attachAfterReady(session)) - .catch((error) => { - sendMessage(socket, { type: "error", data: String(error) }); - socket.close(); - }); -}); - -const writeInfo = (port) => { - const payload = { - port, - host: "localhost", - path: "/terminal", - startedAt: new Date().toISOString() - }; - fs.writeFileSync(infoFile, JSON.stringify(payload, null, 2)); -}; - -const tryListen = (port, attempt = 0) => { - server.once("error", (error) => { - if (error?.code === "EADDRINUSE" && attempt < maxAttempts) { - const nextPort = port + 1; - console.warn(`[terminal-ws] port ${port} in use, retrying on ${nextPort}`); - tryListen(nextPort, attempt + 1); - return; - } - console.error("[terminal-ws] failed to start", error); - process.exit(1); - }); - - server.listen(port, () => { - writeInfo(port); - console.log(`docker-git terminal ws listening on ws://localhost:${port}/terminal`); - }); -}; - -tryListen(basePort); diff --git a/packages/web/src/app/api/projects/[projectId]/down/route.ts b/packages/web/src/app/api/projects/[projectId]/down/route.ts deleted file mode 100644 index 3fb7c8d5..00000000 --- a/packages/web/src/app/api/projects/[projectId]/down/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextResponse } from "next/server" - -import { downProject } from "../../../../../server/docker-git" -import { runEffect } from "../../../../../server/runtime" - -export const dynamic = "force-dynamic" - -type RouteParams = { - readonly params: { - readonly projectId: string - } -} - -export const POST = async (_request: Request, { params }: RouteParams) => { - const { projectId } = await Promise.resolve(params) - const decodedProjectId = decodeURIComponent(projectId) - return runEffect(downProject(decodedProjectId)) - .then(() => NextResponse.json({ status: "ok" })) - .catch((error: unknown) => - NextResponse.json({ error: String(error) }, { status: 500 }) - ) -} diff --git a/packages/web/src/app/api/projects/[projectId]/exec/route.ts b/packages/web/src/app/api/projects/[projectId]/exec/route.ts deleted file mode 100644 index 346ce37d..00000000 --- a/packages/web/src/app/api/projects/[projectId]/exec/route.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { NextResponse } from "next/server" -import { Either } from "effect" -import * as ParseResult from "effect/ParseResult" -import * as Schema from "effect/Schema" - -import { execProjectCommand } from "../../../../../server/docker-git" -import { runEffect } from "../../../../../server/runtime" - -export const dynamic = "force-dynamic" - -const CommandSchema = Schema.Struct({ - command: Schema.String -}) - -type RouteParams = { - readonly params: { - readonly projectId: string - } -} - -const formatParseError = (error: ParseResult.ParseError): string => - ParseResult.TreeFormatter.formatIssueSync(error.issue) - -export const POST = async (request: Request, { params }: RouteParams) => { - const { projectId } = await Promise.resolve(params) - const decodedProjectId = decodeURIComponent(projectId) - - return request.json() - .then((body) => - Either.match(Schema.decodeUnknownEither(CommandSchema)(body), { - onLeft: (error) => - NextResponse.json( - { error: formatParseError(error) }, - { status: 400 } - ), - onRight: ({ command }) => - runEffect(execProjectCommand(decodedProjectId, command)) - .then((output) => NextResponse.json({ output })) - .catch((error: unknown) => - NextResponse.json({ error: String(error) }, { status: 500 }) - ) - }) - ) - .catch((error: unknown) => - NextResponse.json({ error: String(error) }, { status: 500 }) - ) -} diff --git a/packages/web/src/app/api/projects/[projectId]/processes/route.ts b/packages/web/src/app/api/projects/[projectId]/processes/route.ts deleted file mode 100644 index deb98c96..00000000 --- a/packages/web/src/app/api/projects/[projectId]/processes/route.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { NextResponse } from "next/server" -import { Either } from "effect" -import * as ParseResult from "effect/ParseResult" -import * as Schema from "effect/Schema" - -import { execProjectCommand } from "../../../../../server/docker-git" -import { runEffect } from "../../../../../server/runtime" - -export const dynamic = "force-dynamic" - -type RouteParams = { - readonly params: { - readonly projectId: string - } -} - -type TtySession = { - readonly user: string - readonly tty: string - readonly date: string - readonly time: string - readonly idle: string - readonly pid: number - readonly host: string -} - -type ProcessInfo = { - readonly pid: number - readonly ppid: number - readonly tty: string - readonly stat: string - readonly start: string - readonly command: string -} - -const EmptySchema = Schema.Struct({}) - -const formatParseError = (error: ParseResult.ParseError): string => - ParseResult.TreeFormatter.formatIssueSync(error.issue) - -const parseWho = (raw: string): ReadonlyArray => { - const lines = raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0) - const sessions: TtySession[] = [] - for (const line of lines) { - const match = /^(?\S+)\s+(?\S+)\s+(?\d{4}-\d{2}-\d{2})\s+(?