diff --git a/.agents/skills/building/SKILL.md b/.agents/skills/building/SKILL.md new file mode 100644 index 00000000000..fbb9dc7a4ab --- /dev/null +++ b/.agents/skills/building/SKILL.md @@ -0,0 +1,56 @@ +--- +name: building +description: Build Cap features in isolated Git worktrees with PlanetScale development branches, shared dependency caches, recorded feature demonstrations, and plain-language pull requests. Use for /building or $building requests, including resuming an existing building session. Ordinary coding requests do not select this publishing workflow. +--- + +# Building Cap + +Use `/building ` in Cursor or Claude Code, or `$building ` in Codex. Each invocation authorizes implementing the feature in a new worktree, provisioning its disposable development resources, running verification, recording and uploading a demonstration into the user's `PR's` Cap folder, and opening a pull request. Honor narrower user instructions. Merging and production deployment remain separate actions. + +Use the repository containing the requested Cap project. Keep the original checkout, its index, branches, running services, and recordings untouched. This workflow replaces the shared-checkout mechanics of `$build`; do not invoke that skill inside this workflow. + +The helper is `scripts/building.mjs` relative to this skill. Use its absolute path when working in a newly created worktree, which might predate the repository copy of this skill. Run `npm ci --prefix --ignore-scripts --no-audit --no-fund` once if the provider dependencies are missing. `node help` lists commands. Read [editors.md](references/editors.md) when installing or checking discovery in another editor; all editors use the same scripts and session registry. + +## Publication privacy + +Use neutral GitHub metadata. Branch names, commit messages, PR titles/descriptions, comments, release notes, and demo titles must not mention Codex, Cursor, Claude, model names, or automated authorship. Use `building/-` branches and conventional commit titles describing the actual change. Never add tool signatures, generated-by text, co-author trailers, or other attribution. This naming rule applies whichever editor runs the workflow. + +Never put secrets, sensitive information, or real people's personal data in committed content, filenames, commit messages, GitHub PR titles/descriptions, comments, attachments, or linked demonstrations. This includes credentials, tokens, cookies, signed URLs, connection strings, customer names, email addresses, phone numbers, account identifiers, recordings, and private support or billing details. Choose public-safe feature names before creating branches, recipes, or video titles. Use synthetic fixtures and reserved example domains. Keep raw logs, local absolute paths, database/provider receipts, and session state out of Git and GitHub text; a private repository is not an exception. + +Before every commit, inspect the exact staged diff and proposed message. Before every push, review all outgoing commits, including intermediate versions that a later commit removes. Before each GitHub write or Cap upload, inspect the final text or media actually being published. Summarize test outcomes in safe language instead of pasting commands or raw output; inspect screenshots, video frames, and audio too. Use an available secret scanner as an additional check, never as proof that personal data is absent. Use the user's established public Git identity and GitHub no-reply email rather than publishing a private author or committer email; do not alter shared Git configuration. + +If anything sensitive appears or cannot be confidently classified, sanitize or replace it before proceeding. Do not publish first and clean it up later. If exposure has already occurred, stop further publication, report the affected artifact without repeating the value, and coordinate credential revocation and history cleanup rather than silently force-pushing. Do not silently redact application behavior or required source values; use configuration or synthetic fixtures and preserve the intended feature. + +## Prepare and resume + +Read the selected repository's instructions. Fetch the remote default branch without moving the original checkout. Run `new --repo --name --target web|desktop|gpui|fullstack --base ` and use the returned worktree for every source edit, generator, command, commit, and push. Resume with the existing session ID; do not create replacement resources after an uncertain response. + +Read [environment.md](references/environment.md) before provisioning. Prefer the PlanetScale MCP to create the exact development branch named in the session, then run `database-attach`. The authenticated `database-create` helper is the fallback when that MCP is unavailable. Never attach production, staging, another session's branch, or production credentials. Create expiring credentials and seed the owned development branch. Generate migrations through Cap's existing generator; never edit generated SQL or metadata by hand. + +Run short commands through the helper's `run` wrapper and long-lived servers through `serve` so inherited production integration credentials and unmanaged dotenv files cannot accidentally configure the feature. Explicitly configure only required test services. There is no automatic production fallback for storage, payments, email, OAuth, or media processing. + +Read [desktop.md](references/desktop.md) when native crates or desktop applications are involved. Use Bun's shared package cache and private installations. Warm private native/build directories using copy-on-write only from a matching, idle checkout. Never symlink a writable `target`, whole `node_modules`, app profile, or user recording directory between worktrees. + +## Implement and verify + +Make the smallest complete change, trace its callers and platform behavior, and apply repository checks to the affected scope. Preserve an acceptance checklist including error states, permissions, and old data. Establish the demo's expected behavior before implementation; assertions must test that behavior, not just successful page loading. + +Commit coherent changes in the owned worktree. Run final checks through `check` so successful results are bound to the committed SHA. Recheck against current remote main, including generated migration conflicts and deployment ordering. A successful Git merge simulation does not prove runtime compatibility. + +Read [recording.md](references/recording.md) for the Daytona/Cap path. Use a tracked, reviewed feature recipe and an assertion-driven walkthrough of the actual change. Before credentialed capture, independently inspect source provenance, changed code and dependencies, recipe commands, and credential scope, then record the actual SHA-bound review with `capture-review`. Untrusted or fork-controlled code receives no credentials until that review is complete. Verify the resulting video's content and reviewer access in a browser before calling it a successful demonstration. Use `preview` for local feature browsing: each session gets a separate Chrome profile, preventing cookies and local storage from colliding across ports. Preserve failed evidence; never replace a failed Cap CLI capture with a different recorder without reporting the change. + +For macOS/Windows-specific behavior, obtain evidence on that OS. Linux browser or native demonstrations do not establish other-platform behavior. Infrastructure-only changes can give a concrete nonvisual explanation and relevant test evidence instead of a staged visual demo. + +## Publish + +Create or reuse a draft PR for the owned branch, then record its URL using `pr`. Push only the feature branch without force. Keep the PR draft while required verification, video playback, native checks, or review remain incomplete. Do not let fork PR code run with provider or publishing credentials in privileged CI. + +Upload the verified capture using `share`; the helper finds or creates `PR's` and moves the video there. An uncertain upload must be reconciled by its recorded title/ID, never blindly repeated. Write a short ELI5 file explaining the previous problem, the new behavior, and its benefit. `pr-body --eli5 ` produces a starting body with the recording and current-SHA checks. Add factual migration notes, platform coverage, and remaining limitations; remove limitations only after verification. Use `gh pr edit --body-file`, preserving unrelated reviewer-authored content. + +Use the installed `greptile-pr-loop` skill when available, fixing actionable findings through its stop condition. Any changed commit invalidates old evidence: rerun affected checks, update the recording, and review the final PR head. In Codex, attach the created PR to the current task using `attach_artifact` when available; Cursor and Claude Code can report the GitHub URL directly. These editor integrations are optional; resource management uses the shared CLI. Mark it ready only after the required gates pass; otherwise report the concrete blocker and keep the draft. + +## Finish + +Delete owned Daytona sandboxes after evidence is downloaded; the helper also sets expiry limits. After the PR is published, required checks pass, and the Cap upload is verified playable and accessible, run `finish` automatically. It stops owned server processes and removes the disposable development database and clean, published worktree while the PR remains open. Do not keep paid previews running unless the user asks for one. Keep recordings, Git branches, receipts, and shared caches. `resume` recreates the worktree from its retained feature branch; reprovision and reseed its development database for follow-up work. `close` also supports cleanup after a PR is merged or closed. Never delete resources based on age alone or break a lock without checking its host, process, and pending operation. + +Report the PR, Cap link, actual checks, unverified platforms, and retained resources. Do not call a scaffold, a passing compile, or an accepted upload end-to-end success. diff --git a/.agents/skills/building/agents/openai.yaml b/.agents/skills/building/agents/openai.yaml new file mode 100644 index 00000000000..9835344aaf8 --- /dev/null +++ b/.agents/skills/building/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Building Cap" + short_description: "Isolated worktrees and recorded Cap pull requests" + default_prompt: "Use $building to implement this Cap feature in an isolated worktree and prepare a verified pull request with an ELI5 and Cap recording." diff --git a/.agents/skills/building/package-lock.json b/.agents/skills/building/package-lock.json new file mode 100644 index 00000000000..1affc754846 --- /dev/null +++ b/.agents/skills/building/package-lock.json @@ -0,0 +1,2454 @@ +{ + "name": "cap-building", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cap-building", + "dependencies": { + "@daytonaio/sdk": "0.217.0", + "mysql2": "3.24.4", + "playwright-core": "1.62.1" + } + }, + "node_modules/@aws-sdk/checksums": { + "version": "3.1001.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1001.1.tgz", + "integrity": "sha512-x12Q17KYlJAd3nKf8LV5LV0vt8sh8/6YfQLGPtrGnQf/tW4jqxPGq5GPpuVitpQYM3eUR4XB7CbxZf751NMbLw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.1140.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1140.0.tgz", + "integrity": "sha512-7cf3xOyX6L5fyWZgPkVIrbsf/7cmKccKdRa3mkF467qchzgIiqAl/meG/SYdMdoY8JZYWGh0RobzoXiyKuGneA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/checksums": "^3.1001.1", + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/credential-provider-node": "^3.972.84", + "@aws-sdk/middleware-sdk-s3": "^3.972.77", + "@aws-sdk/signature-v4-multi-region": "^3.996.47", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.978.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.1.tgz", + "integrity": "sha512-LbY9aGsEiznDWmUc30Nwv3aIX/+dbwTx8KfS0yOC3NPYMO+O91e6jkT1azf34FwjOndq8/Q+RcVVZz5xnerwdg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.6", + "@aws-sdk/xml-builder": "^3.972.41", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.35.0", + "@smithy/signature-v4": "^5.7.3", + "@smithy/types": "^4.19.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.72", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.72.tgz", + "integrity": "sha512-xTKO/FWJPozTIXbozVnVGoNBhaGba8TBcx+KyUjRVeOlXE+dUc7GTR1cLvu0uTdIdmemzaFbqqCshXeZA1fZew==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.74", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.74.tgz", + "integrity": "sha512-u91E/hT8f4d1xy0Jl7VG4nVKJ3lxbrZkoBTeSVoJdWBiSEUMwMS/9+e0H/aJVQV//Lt5wuzP+E69v4aRSsNTmw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.17.tgz", + "integrity": "sha512-ged4KXdBkvIC81bLvNHHuQKdKak/VXhQTR1NWYTTqW0474nlmsxy9O/vlgTIohDDWH3xpBdtVMZRyjb+DnocDA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/credential-provider-env": "^3.972.72", + "@aws-sdk/credential-provider-http": "^3.972.74", + "@aws-sdk/credential-provider-login": "^3.972.79", + "@aws-sdk/credential-provider-process": "^3.972.72", + "@aws-sdk/credential-provider-sso": "^3.973.16", + "@aws-sdk/credential-provider-web-identity": "^3.972.78", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.79", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.79.tgz", + "integrity": "sha512-L+Z85anONJd8MaiuraO4wRxATCdEejBZ3K3eymzWI5JPXa9sOS9CkIm72PBKqXKX+Z9p9NGMX5AIMXm0LEflgw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.84", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.84.tgz", + "integrity": "sha512-oHt854odINVwzwsh+c5x69j0ajm4DbqqqVJ+O1ECsCIZeMDAbzFpXItaqP7UZstJj/ATdTk/KFSH0LaNAgV+kA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.72", + "@aws-sdk/credential-provider-http": "^3.972.74", + "@aws-sdk/credential-provider-ini": "^3.973.17", + "@aws-sdk/credential-provider-process": "^3.972.72", + "@aws-sdk/credential-provider-sso": "^3.973.16", + "@aws-sdk/credential-provider-web-identity": "^3.972.78", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.72", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.72.tgz", + "integrity": "sha512-rLIp2xbMjX/k9/od7APpqq1ZgXXnV0pOL1Th3ZsL8Wu0TRtBsDTVS8iPqcfRFcHakFxPvR04OSTv2ka2qOb/2A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.16.tgz", + "integrity": "sha512-IGihaJfFZYacJJr/odqILCoK7W/mvrZ7cuK7ECn3sAu4vLC6u0V8bS7mCGbdugJ8Aum2tnvqmx0F2MRFp2rn9g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/token-providers": "3.1138.0", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.78", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.78.tgz", + "integrity": "sha512-/y9WvNtlcPBGLR0qc1a+9J/xtYZfVczvLUOuXaVWylzttH7ewsxwHtjmiJSolNrVSDorIxHGHMU61CbonRkmwA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/lib-storage": { + "version": "3.1140.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1140.0.tgz", + "integrity": "sha512-Jb5VxiX/EoX+h4BV86n47y+HhQk1yNrWkIzQ6esBjuruoN96qwNwKPbm8VP2XNXbDbIpEVwcKtSPPe5Y4MHUzA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "buffer": "5.6.0", + "events": "3.3.0", + "stream-browserify": "3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.1140.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.77", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.77.tgz", + "integrity": "sha512-E7W2UOeUoc+lg3uIfR/dM7ZwusHwhBQrKMnlkRv4EXRR+C0YtV1pg25xC7GdZIhXH+NAMgZPCbE7o5to2cjFiw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/signature-v4-multi-region": "^3.996.47", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.46", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.46.tgz", + "integrity": "sha512-oRxtBcka/JGHGs9l9p9IVajGoTP8vTPmoAzdHGy4Qcy9P5vPnDf6nhIeM/COQNY9k/OahImTRaLkHftoXvfcmQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/signature-v4-multi-region": "^3.996.47", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.47", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.47.tgz", + "integrity": "sha512-Zk08macMvQTHzQJCLJVkOlviVoqwYMrpXv4lmLN7b7sAbiMoOK7Go0NYdR5UeF+MW8LIbRmwrNy9u/5VvX1U5g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.6", + "@smithy/signature-v4": "^5.7.3", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1138.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1138.0.tgz", + "integrity": "sha512-GpyAr0DD63YOEmYFM6Df+gJuIgC92MMTiBK4FTKfxii5MJ9ge20epR7LyroulscYlG89J+ZB2ivFDPjvfQhzdw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.974.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.6.tgz", + "integrity": "sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.41.tgz", + "integrity": "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@daytona/analytics-api-client": { + "version": "0.217.0", + "resolved": "https://registry.npmjs.org/@daytona/analytics-api-client/-/analytics-api-client-0.217.0.tgz", + "integrity": "sha512-UJxZdzynM984O8ZH64JnAq8HbxAT2fQ26Vbqr00UETw1+W8oi7s7I6JoBPV2WhohE0AZQ35yV+H0xDFuAURGrw==", + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.6.1" + } + }, + "node_modules/@daytona/api-client": { + "version": "0.217.0", + "resolved": "https://registry.npmjs.org/@daytona/api-client/-/api-client-0.217.0.tgz", + "integrity": "sha512-AMM0AIRyGUIsCdB0jWiuyLweqcE7csGhWnl3pNnk0zFCJlHVJLBmwLjf7QKhQM6CoQ3yVXtEcaH70R2442j2YA==", + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.6.1" + } + }, + "node_modules/@daytona/toolbox-api-client": { + "version": "0.217.0", + "resolved": "https://registry.npmjs.org/@daytona/toolbox-api-client/-/toolbox-api-client-0.217.0.tgz", + "integrity": "sha512-IODPuNUJhI2UxmE+lpFwGVkMY/RgKnXAmpiEAgRT1caO9iDs41Xzz93/sSeNm+hkIUFhVr5qQEVZzKRLwNliqw==", + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.6.1" + } + }, + "node_modules/@daytonaio/sdk": { + "version": "0.217.0", + "resolved": "https://registry.npmjs.org/@daytonaio/sdk/-/sdk-0.217.0.tgz", + "integrity": "sha512-WhRmyK4oojjb/RGZUl4ulhXGPcbbLAceh9ev6HZQQykMVhREq8P5noIWPXZdi9RbcYimM1W8u3zdtYICR0TF/A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-s3": "^3.1131.0", + "@aws-sdk/lib-storage": "^3.1131.0", + "@daytona/analytics-api-client": "0.217.0", + "@daytona/api-client": "0.217.0", + "@daytona/toolbox-api-client": "0.217.0", + "@iarna/toml": "^2.2.5", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/exporter-trace-otlp-http": "^0.222.0", + "@opentelemetry/instrumentation-http": "^0.222.0", + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/sdk-node": "^0.222.0", + "@opentelemetry/sdk-trace-base": "^2.11.0", + "@opentelemetry/semantic-conventions": "^1.43.0", + "axios": "^1.20.0", + "busboy": "^1.0.0", + "expand-tilde": "^2.0.2", + "fast-glob": "^3.3.0", + "form-data": "^4.0.4", + "isomorphic-ws": "^5.0.0", + "pathe": "^2.0.3", + "shell-quote": "^1.8.2", + "socket.io-client": "^4.8.1", + "tar": "^7.5.22", + "tslib": "2.8.1", + "ws": "^8.21.3" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.5", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.5.tgz", + "integrity": "sha512-7VZM+SVdEcUUqSQeNI3zM8Qs/BhQKZndPo2h5VkYkAM8Iz0wJIa8mKV5ekQGqG8UUsnkQ0NMxIxwkIHYvj0qOw==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", + "license": "ISC" + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.222.0.tgz", + "integrity": "sha512-9mb1If+IF6u0ZVXkHQ6ogEae5HwA6ajIVUgpSDQyRASxft6BSXHvBvPooRle3yFN/fKnCdSOnuu0OC3PLcF6+g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/configuration": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.222.0.tgz", + "integrity": "sha512-zNWGmEvyX7CUbS6F6PKi7hf3CF/SffzeBYyWVQAfadZ8hODrvLmqV3IUGVt1SiqyYanRPHc8VuhoO0Qh9OEuug==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "yaml": "^2.8.3" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.11.0.tgz", + "integrity": "sha512-Tr79DyWI8itsBdg+jH+opjfrwLzX+erk1/ExkIwhWoAVjVrJIn2y5+cGjTC0Vy8fyNIA/y8wuJPZwr1T3xCZeQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", + "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.222.0.tgz", + "integrity": "sha512-HH4Oi/E8HXBKtcjgcVPJGsrg+lSstdw1m93brRdpVj5CjMpyTkv/O0ZYMXP6QXVEaLjjFivtzUu4Y8+i1UJpjw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.222.0", + "@opentelemetry/otlp-transformer": "0.222.0", + "@opentelemetry/sdk-logs": "0.222.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.222.0.tgz", + "integrity": "sha512-GqwLZohJTf/sttYlOpT1oFTJ9ScYaKnk0sOx4Qe/+KmXYVOa+v94cfnkUkFKDuvTODWhCL50Hz3gvsZD6ngQpg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/otlp-transformer": "0.222.0", + "@opentelemetry/sdk-logs": "0.222.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.222.0.tgz", + "integrity": "sha512-LlCDJuaznfFPzjHoKXjN/r9T5pgCZpmVheZ/EmwMUhMvpxI7RXDmsdLXoYpl5suYQNkwyXAUZhVEXKe3lev8Hg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/otlp-transformer": "0.222.0", + "@opentelemetry/sdk-logs": "0.222.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.222.0.tgz", + "integrity": "sha512-nPU2gpQWJfQttf+A71aLkKEOQoO6n6QGs5QTtPagi/rb3Lc1I55bqRXoLQEZsWqWimc8eas2Vo168NkTaByxQw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/exporter-metrics-otlp-http": "0.222.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.222.0", + "@opentelemetry/otlp-transformer": "0.222.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.222.0.tgz", + "integrity": "sha512-6Ko+0bgNP6V4G/RsmNBSk5lO1B/lo9z6tvLpFY3ykPPevXNzFmOW3C9hEmTt0zKCvHovyrol3TYlg4HVb5gwjg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/otlp-transformer": "0.222.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/sdk-metrics": "2.11.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.222.0.tgz", + "integrity": "sha512-s1ZPwS8Ig7cpRmL3kCobPaFbemM81dJ03LNPrsuN32h3mCDvNF2NQ1pRHruOju0P0o1TZ24LkZA685rtQal1KA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/exporter-metrics-otlp-http": "0.222.0", + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/otlp-transformer": "0.222.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.222.0.tgz", + "integrity": "sha512-I7nalnnBZrhlTgjlqsVeEjJGcQCFK5kKAYVk7XTz/cbubvj5fvjLOal/Many0taHF2GIkOWmegsbQ8vbQJjhFg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/sdk-metrics": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.222.0.tgz", + "integrity": "sha512-uisilePEdOMa3hIfXW82XXU6FofF8gw3iv+PoboIMWEUBC7XbJB3XnIHSdrSDsqCw3njR/If08qD0yzOY1U9Sg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.222.0", + "@opentelemetry/otlp-transformer": "0.222.0", + "@opentelemetry/sdk-trace": "2.11.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.222.0.tgz", + "integrity": "sha512-RCnPWcHppwiquQ+cV3nWvNwdf0MG1w26e5jewW2T83nTZOlXgcg88sY9ulCVgagGHcq1mj0L0GP6YHgzk2v8oA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/otlp-transformer": "0.222.0", + "@opentelemetry/sdk-trace": "2.11.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.222.0.tgz", + "integrity": "sha512-imLrLzNhnABJt3tUyT63h6MqfZrG3Gdv3TE7+OUQTvwD088RfCUx3rF01lcgfu2KXBFk13vwi0yKZLvFEalkIA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/otlp-transformer": "0.222.0", + "@opentelemetry/sdk-trace": "2.11.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.11.0.tgz", + "integrity": "sha512-mFibA45yXLEETIj+VcTVycFFe4R91iD41d+r3CU0f3DDWJCrICiHxUIjmYBTCZIuywXEeNo++2lncQeWnH7lWQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/sdk-trace": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.222.0.tgz", + "integrity": "sha512-fhbRDuAgzPKpq1k5+hX3uS+3QAYNca0on0Ngot/R8dDsQtuFaADeZnG2uSgxABuvcI7S2phAaRhbct9jieeKZw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.222.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.222.0.tgz", + "integrity": "sha512-HhMHW32LyGzIJEmUElxaML4oUe6JiCq0Hvjt/Awxb60SlMsSFetFLFjed45g/0wrPr1n7z+xW+x1B/584WBAkg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/instrumentation": "0.222.0", + "@opentelemetry/semantic-conventions": "^1.29.0", + "forwarded-parse": "2.1.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.222.0.tgz", + "integrity": "sha512-YbywG3veEm2Fb6TbdxRkuquWob6eVWXuA8/Ba1tXz9jHfUqpdE3keilOHEtPboC4CvS1bjeeVfNkWGOOrLj+lw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/otlp-transformer": "0.222.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.222.0.tgz", + "integrity": "sha512-Kw5WqDBYYpCgY+edHWZS/Jp3bdguqNkCyEWOsqFhf8RmtcEuWYzPFA9i+L6UsNoJXTs8ngeQB6dwELH1czNYjA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.14.3", + "@opentelemetry/core": "2.11.0", + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/otlp-transformer": "0.222.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.222.0.tgz", + "integrity": "sha512-/F3BZ89+CJQnZkMh2tCrtcdB+XT2Dxhj4FFE+WPQ//413hmFL0/RfEX6vgOIWGhiSzrkHWTK3+6SiT7K5/g/jQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.222.0", + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/sdk-logs": "0.222.0", + "@opentelemetry/sdk-metrics": "2.11.0", + "@opentelemetry/sdk-trace": "2.11.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.11.0.tgz", + "integrity": "sha512-ldyRZXx6qsqu9Spqd7rVxo6SNAUJqP8Ir3vwIi4N8xyZLXNl/2XeQmIHvFneiVOqNwWkU+JGFg2O9/T1fefU0w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.11.0.tgz", + "integrity": "sha512-RUOqgoIqXOPQTOhpBrs07Y8sBlxVoLY5Fka6ueZ0prW+k6fWQsba/nHDY4gm+gmfuwtRqbX5Y9YKhDvktjbIqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz", + "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.222.0.tgz", + "integrity": "sha512-+19YHODIjaUCArxleaJtuufFZVpz/xvvK+VllQqE+W8hHolxdoRwHfK/s667zezwh1hkx6FFF+oYzetYgqK+Bg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.222.0", + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.11.0.tgz", + "integrity": "sha512-7GXXcObyHyDUUSG+L+kJoquty01bzm7ivE7+SSgXXJcHuPzGviptxwARmI2c+bnnxjexGQbJnyNlN8HxBP/Y7A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.222.0.tgz", + "integrity": "sha512-t8tlFN/ezYV7iI4k6u3t2jFbIReX99Z0iVMVSYLJscE4McxIW9YGHhdOldeY1elHeoq8mjEO4B9s4Fk7+zJbsg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.222.0", + "@opentelemetry/configuration": "0.222.0", + "@opentelemetry/context-async-hooks": "2.11.0", + "@opentelemetry/core": "2.11.0", + "@opentelemetry/exporter-logs-otlp-grpc": "0.222.0", + "@opentelemetry/exporter-logs-otlp-http": "0.222.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.222.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.222.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.222.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.222.0", + "@opentelemetry/exporter-prometheus": "0.222.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.222.0", + "@opentelemetry/exporter-trace-otlp-http": "0.222.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.222.0", + "@opentelemetry/exporter-zipkin": "2.11.0", + "@opentelemetry/instrumentation": "0.222.0", + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.222.0", + "@opentelemetry/propagator-b3": "2.11.0", + "@opentelemetry/propagator-jaeger": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/sdk-logs": "0.222.0", + "@opentelemetry/sdk-metrics": "2.11.0", + "@opentelemetry/sdk-trace": "2.11.0", + "@opentelemetry/sdk-trace-base": "2.11.0", + "@opentelemetry/sdk-trace-node": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.11.0.tgz", + "integrity": "sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.11.0.tgz", + "integrity": "sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/sdk-trace": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.11.0.tgz", + "integrity": "sha512-CuvCMJmZxswhNLlM2LfuLOW3h3fZujA4hsG4B+Sz4dX2zvaXO8Ng74cnDHWD64gLszTlhiG3c0iNUjj4g+0/sA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.11.0", + "@opentelemetry/core": "2.11.0", + "@opentelemetry/sdk-trace-base": "2.11.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@smithy/core": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.35.0.tgz", + "integrity": "sha512-zRMhfkByhT2snNdr1si24vJitU6Cr9ix2MikUfWmkAgp4jrNP0GcKSP5YvwQ+TlI8AZXER5QOGJn3JsVtSD9/A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz", + "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz", + "integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz", + "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.3.tgz", + "integrity": "sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.19.0.tgz", + "integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz", + "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.1.tgz", + "integrity": "sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/engine.io-client": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", + "integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-3.0.2.tgz", + "integrity": "sha512-BuIB67FngDSyQ/dpQNOZybwdEBDUGJQvOqwWr4ha/ufYiqzuEwPkKO2zLhRAgay28tStRIHUeWmszZAJo3GCOg==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "license": "MIT" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/import-in-the-middle": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.5.1.tgz", + "integrity": "sha512-mPKuL8bPQzecui2KK6Gb+M8JvJoHnhS1FeYGa22QopBmlevF5F0FE6ued/B5EgHDeIoMTONIpDWWFKUPOG0DBQ==", + "license": "Apache-2.0", + "dependencies": { + "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^3.0.2", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.5.tgz", + "integrity": "sha512-5J9ysMYUpYIg9RF2vJpy9SinEmSviFSe0GyPpCQ4L5QSkLAgeLXlTAOu2ZwWUU5m+0SBl6gUU1R1ZQB3aKypfA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.24.4.tgz", + "integrity": "sha512-A2olluVlj0mvgyIRRISMEzXc51m+21mRtcMVjJyIpt2GG98+XrC9m9HzsqcMsX2LcnfccJvY5NB22g8fENBnOA==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.3", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.5.1" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/shell-quote": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz", + "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sql-escaper": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.5.2.tgz", + "integrity": "sha512-6CKD38c31SENivxOADeMNLdukOnUxUcflKtzVWzace7Riv1v7cAEym5Cx9Q7gYZ3ezIJI7ZpqecQq8cqNeSSFg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/.agents/skills/building/package.json b/.agents/skills/building/package.json new file mode 100644 index 00000000000..13893c5aaf8 --- /dev/null +++ b/.agents/skills/building/package.json @@ -0,0 +1,13 @@ +{ + "name": "cap-building", + "private": true, + "type": "module", + "scripts": { + "test": "node --test scripts/*.test.mjs" + }, + "dependencies": { + "@daytonaio/sdk": "0.217.0", + "mysql2": "3.24.4", + "playwright-core": "1.62.1" + } +} diff --git a/.agents/skills/building/references/desktop.md b/.agents/skills/building/references/desktop.md new file mode 100644 index 00000000000..b6084aa898c --- /dev/null +++ b/.agents/skills/building/references/desktop.md @@ -0,0 +1,21 @@ +# Desktop worktrees + +There are separate Tauri and GPUI Cargo workspaces and toolchains. Never assume one cache key or one native runtime verifies both. + +`warm --session --source ` snapshots prepared native dependencies into the Git-common cache, then creates a private copy-on-write clone in the worktree. `--rust` also warms `target/debug` and `apps/desktop-gpui/target/debug`. It excludes profiles, user media, release artifacts, and arbitrary contents of `target`. + +The helper requires matching setup script, lockfiles, and toolchain files, uses a cache lock, checks for open files in the source, and refuses to overwrite existing destinations. Compiled Rust snapshots also require matching source trees, local changes, and both workspaces' Cargo configuration, including ignored configuration files. Prepare matching build configuration before `--rust`; use the default native-dependency-only warm when source or configuration differs. macOS uses `cp -cR`; Linux requires a filesystem supporting `cp --reflink=always`. There is no silent full-copy fallback. On unsupported filesystems, use fresh builds with compiler caching or a deliberately provisioned build runner. + +Only seed caches from a quiescent, owned checkout whose build processes have stopped. Open-file checks do not prevent an unrelated process from starting a new build; do not use a live shared developer checkout as an automatically refreshed cache source. Cache snapshots are immutable by convention. Setup, signing, formatting, and compiler writes happen in each worktree's private clone. + +APFS clones initially share unchanged blocks but grow as builds diverge. Always run Cargo in the destination before using a warmed binary: the snapshot does not prove which environment flags or toolchain binary produced the original artifacts. Cargo may rebuild because of source paths, toolchain, flags, or changed workspace crates. Measure `df` before and after real parallel builds to assess physical growth; `du` totals do not prove unique APFS usage. Do not promise a zero-space worktree or automatic reuse of every Rust artifact. + +The existing setup script supports opt-in `CAP_USE_SCCACHE=1`; evaluate it with the actual workload. Rust incremental compilation and sccache have compatibility constraints, and final executable linking is not cached. Never globally disable incremental compilation or rewrite another worktree's Cargo configuration to force caching. + +Native runtime isolation is separate from build isolation. Cap has single-instance behavior, fixed frontend ports, device permissions, deep-link handlers, keychain credentials, and persisted application data. `run --native` acquires an exclusive machine-local lease; that lease coordinates this workflow only and does not establish an isolated app profile. + +Use `run --native` and `check --native` for expensive Rust compilation as well. This queues native work across sessions instead of launching twenty large builds at once. Complete compile checks before holding the lease with a long-lived native runtime. Source editing and independent web work can continue while a native job waits. + +For v1, prefer a dedicated native runner/OS user with synthetic recordings and test credentials. Before any local native launch, explicitly verify its app identifier, data directory, recording library, frontend port, authentication callback, and existing process ownership. Do not launch it against the user's normal Cap profile. Native profile provisioning and parallel local Tauri/GPUI launch are not automated by this helper. + +Use the platform's actual Cap CLI to capture native demos. Linux Daytona evidence cannot close macOS ScreenCaptureKit, Windows capture, hardware GPU, microphone/camera, permissions, or updater gates. Retain a platform matrix in the PR and keep it draft when required native verification is missing. diff --git a/.agents/skills/building/references/editors.md b/.agents/skills/building/references/editors.md new file mode 100644 index 00000000000..8b1481a0ad3 --- /dev/null +++ b/.agents/skills/building/references/editors.md @@ -0,0 +1,19 @@ +# Editor setup + +Keep the skill named `building` and the helper named `building.mjs`. The repository source is `.agents/skills/building/`. Install one persistent copy at `~/.agents/skills/building/`, including its scripts, references, package manifest, and lockfile. Never point a personal installation at a disposable worktree. + +| Editor | Personal discovery path | Invocation | +| --- | --- | --- | +| Cursor | `~/.cursor/skills/building` → `../../.agents/skills/building` | `/building ` | +| Codex | `~/.agents/skills/building` | `$building ` | +| Claude Code | `~/.claude/skills/building` → `../../.agents/skills/building` | `/building ` | + +An existing `~/.codex/skills/building` installation can remain as a compatibility symlink to the shared directory so earlier absolute helper paths continue to resolve. Check existing destinations before adding links; never overwrite an unrelated skill. Resolve each editor's `SKILL.md` and helper to verify that they read the same files. Reopen a session if its skill list has not refreshed. + +Install the helper's pinned packages once with `npm ci --prefix ~/.agents/skills/building --ignore-scripts --no-audit --no-fund`. The helper uses Node, Git, and the provider CLIs/SDK, not editor-specific tool APIs. Configure provider credentials and MCP connections in the editor or runner being used. Prefer PlanetScale MCP when available and retain the CLI fallback. Browser verification can use that editor's browser tools or Playwright with the session's isolated profile. + +All three editors must operate through the same helper and Git-common session state, so their locks, port allocations, resource ownership checks, and cleanup coordinate across editors. Resume an existing session by ID; do not run two agents against the same session or create an editor-specific registry. + +These paths configure local editors. Remote/cloud workers need the skill package, dependencies, and scoped credentials installed on that worker; a local symlink does not provision a remote environment. Do not copy local secret files as part of the skill package. + +Discovery references: [Cursor](https://cursor.com/help/customization/skills), [Codex](https://learn.chatgpt.com/docs/build-skills), and [Claude Code](https://code.claude.com/docs/en/skills). diff --git a/.agents/skills/building/references/environment.md b/.agents/skills/building/references/environment.md new file mode 100644 index 00000000000..0b9180bce2d --- /dev/null +++ b/.agents/skills/building/references/environment.md @@ -0,0 +1,21 @@ +# Environment setup + +The helper stores resumable session records under `/building/sessions//`. These are machine-local, mode 0600, and outside the tracked worktree. Keep secrets out of tool output and PR bodies. Session IDs, resource identities, and source commits are recorded separately from credentials. + +The default Cap database configuration is organization `cap`, database `cap-production`, parent `main`; the owned development branch is `building-`. Verify that these names still describe the selected project before provisioning. MCP creation must copy schema only. Call `database-attach` to validate the actual ready, nonproduction branch, then `credentials` to create a seven-day development-only password. The admin role is limited to that branch and permits the required Drizzle schema push. Application production credentials are never copied. + +If credential creation is interrupted, inspect and revoke the password with the recorded name before clearing `passwordAttempted` under the session operation lock and trying again. Never print or reconstruct a secret from logs. If credentials expire, revoke the owned password and replace its private environment entry, preserving the resource identity. + +Use `run --session -- bun install --frozen-lockfile`. Each worktree keeps its own workspace links and dependency graph; Bun reuses its global download cache and macOS copy-on-write installs. Do not link the main checkout's `node_modules` into the worktree. + +Use the existing `db:generate` and `db:push` commands through `run` when the feature changes schema, then `seed`. The deterministic base fixture includes one test user with a simulated Pro entitlement, organization, membership, and folder. It does not create paid subscriptions, upload sample customer media, or exercise billing. Add feature-specific fixtures using synthetic media and only the session database. + +`login` creates a one-hour NextAuth browser storage state using the session's own secret and fixture identity. It requires the worktree's web dependencies. This supplies a real signed test session without adding a production authentication bypass. Capture uploads that file to the owned sandbox; walkthroughs can pass `/home/daytona/browser-state.json` to Playwright's browser context. Refresh it immediately before authenticated captures. + +`run` constructs the app environment from a small allowlist, never from the shared checkout's `.env`. Missing object storage points at an unreachable local endpoint. For upload features, provision a disposable bucket with credentials limited to that bucket and set `CAP_AWS_BUCKET`, `CAP_AWS_REGION`, `CAP_AWS_ENDPOINT`, `CAP_AWS_ACCESS_KEY`, and `CAP_AWS_SECRET_KEY` in the private `environment.json`; record its owner and cleanup obligation in the session. A bucket-name prefix alone is not credential isolation. Media processing needs its own test service, secret, and callback URL. + +For the web runtime, use `serve --session -- bun run --cwd apps/web dev --port --hostname 127.0.0.1`. Do not use root `bun run dev` or commands that start/stop shared Docker services. Keep the wrapper alive as the process owner. `serve` uses a separate server lease so login, checks, and capture remain available while it runs; `run` serializes short setup commands. Recheck port availability at launch; allocation prevents building-session collisions but cannot reserve a port against unrelated processes indefinitely. + +The orchestrator is not an OS security boundary. Execute trusted, reviewed project code only. Untrusted fork changes require a sandbox without provider credentials; uploading their artifacts is a separate trusted step. + +Use `preview --session ` for local browsing after starting `serve`. It uses installed Google Chrome with a private per-session profile and imports that session's generated login state. Do not open multiple feature URLs in the main browser profile: cookies ignore port numbers. `stop` and `finish` close owned previews and stop owned server process groups. diff --git a/.agents/skills/building/references/recording.md b/.agents/skills/building/references/recording.md new file mode 100644 index 00000000000..e569fe0d3f5 --- /dev/null +++ b/.agents/skills/building/references/recording.md @@ -0,0 +1,43 @@ +# Recorded PR review + +Use a trusted Linux Daytona snapshot with a known-good Cap CLI, browser, and required native libraries. The helper sets `VNC_RESOLUTION=1920x1080` at creation and verifies the actual display. This setting applies to Linux container sandboxes; do not assume it changes Windows or VM geometry. Local upload uses the installed authenticated Cap CLI, so its credential never enters Daytona. + +The runner dependency is pinned in this skill's package lock. Supply `DAYTONA_API_KEY` to the host process through existing secure configuration, not an argument or recipe. Recipe files and walkthrough code must be committed in the feature worktree. The sandbox receives `git archive` of the exact verified commit, not uncommitted local files. Review source provenance, the complete feature diff and dependency changes, and every setup/start/readiness/walkthrough command and network destination before trusting the capture. A tracked recipe or a passing test is not evidence that fork-controlled code is trusted. + +Credentialed captures require a private review receipt before any sandbox is created. After actually completing that review, record `sha`, the recipe file's SHA-256 as `recipeHash`, the private session `environment.json` file's SHA-256 as `environmentHash`, `sourceTrusted: true`, `commandsReviewed: true`, `credentialScopesReviewed: true`, and an ISO `reviewedAt` timestamp in a private JSON file. Use `capture-review --session --recipe --evidence ` to bind the review to the exact source, recipe, and credential configuration. A later commit, recipe edit, credential rotation, or service-configuration change requires a new review. The helper rechecks the configuration fingerprint when loading the app's environment after waiting for the capture slot. Never generate this receipt just to pass the gate: verify that database and storage credentials are restricted to disposable synthetic fixtures. Untrusted forks remain on `database: false` without credentials or browser login state until independently reviewed. + +The sandbox's initial environment and the setup, readiness, walkthrough, and recorder commands receive no configured credentials. Only the reviewed `start` command receives the session's development credentials, after setup completes; it runs in the background with private logs. Put credentialed application startup in `start`, not in setup or walkthrough commands. These phases share an OS user, so environment separation is defense in depth, not an OS boundary against malicious code. Do not approve untrusted source for this mode. + +A recipe has this shape: + +```json +{ + "version": 1, + "platform": "linux", + "snapshot": "daytona-small", + "setup": ["bun install --frozen-lockfile"], + "start": "bun run --cwd apps/web dev --port \"$PORT\" --hostname 127.0.0.1", + "ready": "node scripts/feature-demo/wait-for-server.mjs", + "walkthrough": "node scripts/feature-demo/walkthrough.mjs" +} +``` + +The paths above illustrate feature-owned scripts, not existing commands. Implement them for the feature. Their readiness checks must be bounded, and the walkthrough must fail on an incorrect outcome. Use Playwright for browser interaction and assertions, with a headed browser and a fixed viewport. Provide readable pauses around meaningful states, aiming for 20–60 seconds. Keep credentials, customer data, and unrelated windows off-screen. + +The snapshot must already contain `cap` on PATH, or the recipe must install a reviewed, pinned release before capture. Inspect `cap guide --json`, `cap doctor --json`, and screen targets. The official installer is at `https://cap.so/install-cli.sh`, but do not execute an unreviewed downloaded installer or silently change the recorder version during retries. Record the recorder version in the verification notes. + +`database` defaults to true. Set it false only for a demonstration that cannot access application data, such as a build-tooling smoke test. Otherwise the runner verifies the owned development branch and supplies its isolated configuration. External service fixtures must work from inside Daytona; host-local addresses are not reachable there. + +Run `capture --session --recipe `. The helper starts Cap, executes the walkthrough, stops/finalizes the recording, validates and exports it, downloads the MP4, and checks video dimensions/duration using local `ffprobe`. It deletes the owned sandbox in cleanup and sets provider expiration limits as a backstop. Do not treat an MP4's existence as visual verification: watch the exported file and confirm the intended feature is visible and behaves correctly. + +If sandbox creation has an uncertain result, locate the saved name and labels and run `sandbox-cleanup`; do not create a replacement until ownership is reconciled. If recording or a walkthrough fails, retain available artifacts and diagnose it. Failed evidence is never accepted as a passing capture. The helper's timeouts bound individual commands; provider TTL bounds abandoned sandboxes. + +After creating a draft PR, record its URL using `pr`, then call `share`. Uploads are named `# — — ` and moved into the personal root `PR's` folder within the user's default Cap organization. Folder creation, lookup, and the move all use the personal library; placement is verified through the Cap's own folder and organization fields. This does not create an organization share or change the recording's visibility. Folder identity is saved for retries. Never create duplicate folders or blindly retry an upload with an uncertain result. Reconcile by the saved title and metadata, then persist the discovered upload ID/link under the session lock. + +Open the resulting Cap share page and verify playable video and intended reviewer access. Keep organization-private visibility if appropriate; public GitHub reviewers require a deliberately public recording containing only synthetic data. Setting visibility is separate from creating a share URL. Do not mark the PR ready with an inaccessible or unplayed recording. + +Use the ELI5/recording/check template from `pr-body`, then add actual platform coverage and schema-deployment notes. The helper rejects stale SHA evidence. CI and review also need the current PR head. A later fix can require a new recording; retain prior Caps as historical evidence rather than deleting them. + +After actually checking playback and reviewer access, save a private browser receipt with `sha`, `url`, `played: true`, `reviewerAccess: true`, and `observedAt`, then run `video-verified --evidence `. This records observed browser evidence; do not fabricate it from upload metadata. Run `finish` as part of PR completion to release the database, server processes, and published worktree immediately. Retained source and seed recipes let `resume` rebuild a follow-up environment. + +Cost defaults: one recording slot per local repository, 15-minute idle stop, 45-minute sandbox TTL, and deletion after artifact download. The SDK snapshots determine CPU/RAM allocation; use a small prepared Linux snapshot for browser demos, and opt into larger/native/GPU runners only for a concrete verification need. Keep expensive compilation out of the recording stage by preparing snapshots. Provider quotas may queue or reject work even when source worktrees are independent. Database passwords expire after seven days, but password expiry does not stop database billing: `finish` must delete the branch. On a blocked or abandoned run, reconcile and delete owned paid resources while retaining source and failed local evidence. diff --git a/.agents/skills/building/scripts/building.mjs b/.agents/skills/building/scripts/building.mjs new file mode 100644 index 00000000000..8edbf95563c --- /dev/null +++ b/.agents/skills/building/scripts/building.mjs @@ -0,0 +1,433 @@ +import { existsSync, readFileSync, realpathSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { parseArgs } from "node:util"; +import { + capture, + deleteSandbox, + recordCaptureReview, + shareCapture, +} from "./capture.mjs"; +import { + assertClean, + assertIdle, + assertWorktree, + context, + createSession, + databaseCredentials, + ensureResume, + execInSession, + git, + inspectDatabase, + jsonCommand, + lock, + pscale, + readSession, + saveSession, + stopSessionProcesses, + waitForLock, + warmDependencies, +} from "./core.mjs"; +import { createDatabase, loginFixture, seedDatabase } from "./database.mjs"; +import { preview } from "./preview.mjs"; + +const help = `Cap building workflow + +new --name --target web|desktop|gpui|fullstack [--base origin/main] +status|resume --session +database-create|database-attach|credentials|seed|login --session +warm --session --source [--rust] +run --session [--native] -- [args...] +serve --session [--native] -- [args...] +preview --session +stop --session +check --session -- [args...] +capture-review --session --recipe --evidence +capture --session --recipe +sandbox-cleanup --session +share --session +pr --session --url +pr-body --session --eli5 [--no-visual ] +video-verified --session --evidence +finish --session [--no-visual ] +close --session + +All commands accept --repo . State and secrets stay in the Git common +directory. Secrets are never printed. Prefer PlanetScale MCP creation followed +by database-attach; database-create is the authenticated CLI fallback. +`; + +export function renderPrBody(session, sha, eli5, noVisual) { + if (!eli5.trim()) throw new Error("An ELI5 explanation is required"); + const latest = new Map(); + for (const check of session.checks) { + if (check.sha === sha) latest.set(check.label, check); + } + const currentChecks = [...latest.values()]; + if (currentChecks.some((check) => !check.passed)) + throw new Error( + "The latest check attempt failed; resolve it before preparing the PR body", + ); + if (!currentChecks.length) + throw new Error("Record passing checks for the current commit first"); + const upload = + session.capture?.sha === sha && session.capture?.status === "captured" + ? session.capture.upload + : undefined; + if (upload) noVisual = undefined; + if (!upload?.folderVerified && !noVisual?.trim()) + throw new Error( + "Current-commit recording is missing; supply a concrete nonvisual reason only when appropriate", + ); + const visual = upload?.folderVerified + ? `[Watch the change in Cap](${upload.link})\n\nRecorded on Linux at 1920×1080 from \`${sha}\`. ${upload.playbackVerified ? "Playback and reviewer access verified in a browser." : "Playback and reviewer access require a separate browser check."}` + : `Not applicable: ${noVisual.trim()}`; + return `## ELI5\n\n${eli5.trim()}\n\n## Demo\n\n${visual}\n\n## Verification\n\n${currentChecks.length} scoped verification checks passed for this commit.\n\nVerified commit: \`${sha}\`.\n\n## Limits and deployment\n\n${session.target === "web" ? "Native desktop platforms were not exercised by this workflow." : "Linux sandbox evidence does not verify macOS or Windows behavior. Record native checks separately."}\n\n${session.database.id ? "An isolated development database was used. Production schema deployment is separate from merging application code." : "No database branch was used."}\n`; +} + +export function recordPr(ctx, session, url) { + const sha = assertClean(ctx, session); + const parsed = new URL(url); + if ( + parsed.hostname !== "github.com" || + !/^\/[^/]+\/[^/]+\/pull\/\d+$/.test(parsed.pathname) + ) + throw new Error("Expected a GitHub PR URL"); + const remote = git(ctx.root, "remote", "get-url", "origin"); + const repository = remote.match( + /^(?:https:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([^/]+\/[^/]+?)(?:\.git)?$/, + )?.[1]; + if ( + !repository || + parsed.pathname.split("/").slice(1, 3).join("/").toLowerCase() !== + repository.toLowerCase() + ) + throw new Error("PR repository does not match the origin remote"); + const pr = jsonCommand( + "gh", + [ + "pr", + "view", + url, + "--json", + "number,url,headRefOid,headRefName,state,baseRefName", + ], + { cwd: session.worktree }, + ); + if (pr.headRefOid !== sha || pr.headRefName !== session.branch) + throw new Error("PR head does not match the session branch and commit"); + session.pr = { number: pr.number, url: pr.url, sha, base: pr.baseRefName }; + saveSession(ctx, session); + return session.pr; +} + +export async function closeSession( + ctx, + session, + { publication = false, noVisual } = {}, +) { + publication ||= + session.status === "closing" && session.cleanupMode === "published"; + if (publication && session.status === "published") + return { id: session.id, status: "published" }; + if (session.status === "closed") return { id: session.id, status: "closed" }; + const worktreeExists = existsSync(session.worktree); + const sha = worktreeExists ? assertClean(ctx, session) : session.closeSha; + if ( + !worktreeExists && + (!["closing", "published"].includes(session.status) || + !sha || + git(ctx.root, "rev-parse", session.branch) !== sha) + ) + throw new Error("Missing worktree without a matching cleanup checkpoint"); + if (!session.pr) + throw new Error( + "Keep unpublished work; close requires a recorded merged or closed PR", + ); + const pr = jsonCommand( + "gh", + ["pr", "view", session.pr.url, "--json", "state,headRefOid"], + { cwd: ctx.root }, + ); + if ( + (!publication && !["MERGED", "CLOSED"].includes(pr.state)) || + pr.headRefOid !== sha + ) + throw new Error("PR is still open or local commits are unpublished"); + if (publication) { + const upload = + session.capture?.sha === sha ? session.capture.upload : undefined; + const reason = upload + ? undefined + : noVisual?.trim() || + (session.noVisual?.sha === sha ? session.noVisual.reason : undefined); + renderPrBody(session, sha, "Publication cleanup", reason); + if (!reason && !upload?.playbackVerified) + throw new Error( + "Verify playback and reviewer access before publication cleanup", + ); + session.noVisual = reason ? { sha, reason } : undefined; + } + if (session.capture && !session.capture.sandboxDeleted) + throw new Error("Reconcile the owned sandbox before closing"); + if (session.database.createAttempted && !session.database.id) + throw new Error("Reconcile database creation before cleanup"); + const releaseLifecycle = await waitForLock( + join(ctx.state, "sessions", session.id, "lifecycle.lock"), + ); + try { + session.status = "closing"; + session.cleanupMode = publication ? "published" : "closed"; + session.closeSha = sha; + saveSession(ctx, session); + } finally { + releaseLifecycle(); + } + await stopSessionProcesses(ctx, session); + if (worktreeExists) { + if (assertClean(ctx, session) !== sha) + throw new Error( + "Source changed during cleanup; preserve unpublished work", + ); + assertIdle(session.worktree); + } + if (session.database.id && session.database.status !== "deleted") { + const branches = pscale(session, [ + "branch", + "list", + session.database.database, + ]); + if (branches.some((branch) => branch.name === session.database.name)) { + inspectDatabase(session); + pscale(session, [ + "branch", + "delete", + session.database.database, + session.database.name, + "--force", + ]); + } + session.database.status = "deleted"; + saveSession(ctx, session); + } + if (worktreeExists) git(ctx.root, "worktree", "remove", session.worktree); + session.status = publication ? "published" : "closed"; + saveSession(ctx, session); + return { + id: session.id, + status: session.status, + retained: "Git branch, receipts, recordings, and shared caches", + }; +} + +export async function main(argv = process.argv.slice(2)) { + const separator = argv.indexOf("--"); + const commandArgs = separator < 0 ? [] : argv.slice(separator + 1); + const { values, positionals } = parseArgs({ + args: separator < 0 ? argv : argv.slice(0, separator), + allowPositionals: true, + options: { + repo: { type: "string", default: process.cwd() }, + session: { type: "string" }, + name: { type: "string" }, + target: { type: "string", default: "web" }, + base: { type: "string" }, + source: { type: "string" }, + rust: { type: "boolean", default: false }, + native: { type: "boolean", default: false }, + recipe: { type: "string" }, + url: { type: "string" }, + eli5: { type: "string" }, + "no-visual": { type: "string" }, + evidence: { type: "string" }, + help: { type: "boolean", default: false }, + }, + }); + const command = positionals[0]; + if (!command || values.help || command === "help") return help; + if (positionals.length > 1) + throw new Error("Unexpected positional arguments"); + const ctx = context(values.repo); + if (command === "new") return createSession(ctx, values); + if (!values.session) throw new Error("--session is required"); + let session = readSession(ctx, values.session); + if (command === "status") return session; + if (command === "preview") return preview(ctx, session); + if (command === "serve") { + const releaseServer = lock( + join(ctx.state, "sessions", session.id, "server.lock"), + ); + let retainLease = false; + try { + return await execInSession(ctx, session, commandArgs, { + runtime: values.native, + }); + } catch (error) { + retainLease = error.retainLease === true; + throw error; + } finally { + if (!retainLease) releaseServer(); + } + } + const release = lock( + join(ctx.state, "sessions", session.id, "operation.lock"), + ); + try { + session = readSession(ctx, values.session); + if ( + !["resume", "close", "finish", "stop", "sandbox-cleanup"].includes( + command, + ) + ) + assertWorktree(ctx, session); + switch (command) { + case "stop": + await stopSessionProcesses(ctx, session); + return { stopped: true }; + case "resume": + return ensureResume(ctx, session); + case "database-create": + return createDatabase(ctx, session); + case "database-attach": { + const branch = inspectDatabase(session); + session.database.id = branch.id; + session.database.status = "ready"; + saveSession(ctx, session); + return session.database; + } + case "credentials": + return databaseCredentials(ctx, session); + case "seed": + return await seedDatabase(ctx, session); + case "login": + return await loginFixture(ctx, session); + case "warm": { + if (!values.source) throw new Error("--source is required"); + return await warmDependencies(ctx, session, values.source, values.rust); + } + case "run": + return await execInSession(ctx, session, commandArgs, { + runtime: values.native, + }); + case "check": { + const sha = assertClean(ctx, session); + let passed = false; + try { + await execInSession(ctx, session, commandArgs, { + runtime: values.native, + }); + if (assertClean(ctx, session) !== sha) + throw new Error("Source changed during verification"); + passed = true; + } finally { + session.checks.push({ + sha, + label: commandArgs.join(" "), + passed, + at: new Date().toISOString(), + }); + saveSession(ctx, session); + } + return { sha, passed: true }; + } + case "capture-review": { + if (!values.recipe || !values.evidence) + throw new Error("--recipe and --evidence are required"); + return recordCaptureReview( + ctx, + session, + resolve(values.recipe), + JSON.parse(readFileSync(values.evidence, "utf8")), + ); + } + case "capture": { + if (!values.recipe) throw new Error("--recipe is required"); + const { Daytona } = await import("@daytonaio/sdk"); + return await capture( + ctx, + session, + resolve(values.recipe), + new Daytona(), + ); + } + case "sandbox-cleanup": { + const { Daytona } = await import("@daytonaio/sdk"); + const daytona = new Daytona(); + if (session.capture && !session.capture.sandboxId) { + try { + const sandbox = await daytona.get(session.capture.name); + session.capture.sandboxId = sandbox.id; + } catch (error) { + if (error.statusCode !== 404 && error.response?.status !== 404) + throw error; + session.capture.sandboxDeleted = true; + } + saveSession(ctx, session); + } + await deleteSandbox(ctx, session, daytona); + return { deleted: session.capture?.sandboxDeleted }; + } + case "share": + return await shareCapture(ctx, session); + case "pr": + return recordPr(ctx, session, values.url); + case "pr-body": { + if (!values.eli5) throw new Error("--eli5 is required"); + return renderPrBody( + session, + assertClean(ctx, session), + readFileSync(values.eli5, "utf8"), + values["no-visual"], + ); + } + case "video-verified": { + if (!values.evidence) throw new Error("--evidence is required"); + const evidence = JSON.parse(readFileSync(values.evidence, "utf8")); + const sha = assertClean(ctx, session); + if ( + evidence.sha !== sha || + evidence.url !== session.capture?.upload?.link || + evidence.played !== true || + evidence.reviewerAccess !== true || + !evidence.observedAt + ) + throw new Error( + "Expected browser evidence of playback and reviewer access for this commit and Cap URL", + ); + session.capture.upload.playbackVerified = true; + session.capture.upload.browserEvidence = evidence; + saveSession(ctx, session); + return { verified: true, sha }; + } + case "finish": + return await closeSession(ctx, session, { + publication: true, + noVisual: values["no-visual"], + }); + case "close": + return await closeSession(ctx, session); + default: + throw new Error(`Unknown command: ${command}`); + } + } finally { + release(); + } +} + +if ( + process.argv[1] && + existsSync(process.argv[1]) && + realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]) +) { + main() + .then((result) => { + if (result !== undefined) + process.stdout.write( + `${typeof result === "string" ? result : JSON.stringify(result, null, "\t")}\n`, + ); + }) + .catch((error) => { + process.stderr.write(`${error.message}\n`); + process.exitCode = 1; + }); +} diff --git a/.agents/skills/building/scripts/capture.mjs b/.agents/skills/building/scripts/capture.mjs new file mode 100644 index 00000000000..07853abd3ed --- /dev/null +++ b/.agents/skills/building/scripts/capture.mjs @@ -0,0 +1,539 @@ +import { createHash } from "node:crypto"; +import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { dirname, join, relative, resolve } from "node:path"; +import { + artifactDirectory, + assertClean, + executionEnvironment, + git, + inspectDatabase, + jsonCommand, + readEnvironmentSnapshot, + run, + saveSession, + sessionPath, + temporaryPath, + waitForLock, +} from "./core.mjs"; + +export function shellQuote(value) { + return `'${String(value).replaceAll("'", "'\\''")}'`; +} + +export function readRecipe(session, path) { + const absolute = resolve(path); + const local = relative(session.worktree, absolute); + if (local.startsWith("..") || local.startsWith("/")) + throw new Error("Recipe must be inside the session worktree"); + git(session.worktree, "ls-files", "--error-unmatch", "--", local); + const recipe = JSON.parse(readFileSync(absolute, "utf8")); + if (recipe.version !== 1 || recipe.platform !== "linux") + throw new Error( + "This runner supports Linux recipes; use a native runner for macOS/Windows evidence", + ); + if ( + !Array.isArray(recipe.setup) || + !recipe.setup.every((command) => typeof command === "string") + ) + throw new Error("Recipe setup must be an array of shell commands"); + if (!recipe.walkthrough || typeof recipe.walkthrough !== "string") + throw new Error("Recipe requires a walkthrough command with assertions"); + if (recipe.start && typeof recipe.start !== "string") + throw new Error("Invalid start command"); + if (recipe.snapshot && typeof recipe.snapshot !== "string") + throw new Error("Invalid snapshot"); + return { + ...recipe, + path: local, + hash: createHash("sha256").update(readFileSync(absolute)).digest("hex"), + }; +} + +export function recordCaptureReview(ctx, session, recipePath, evidence) { + const sha = assertClean(ctx, session); + const recipe = readRecipe(session, recipePath); + const { fingerprint: environmentHash } = readEnvironmentSnapshot( + ctx, + session, + ); + if ( + evidence.sha !== sha || + evidence.recipeHash !== recipe.hash || + evidence.environmentHash !== environmentHash || + evidence.sourceTrusted !== true || + evidence.commandsReviewed !== true || + evidence.credentialScopesReviewed !== true || + !Number.isFinite(Date.parse(evidence.reviewedAt)) + ) + throw new Error( + "Expected trusted source, command, and credential-scope review for this commit and recipe", + ); + session.captureReview = { + sha, + recipeHash: recipe.hash, + environmentHash, + sourceTrusted: true, + commandsReviewed: true, + credentialScopesReviewed: true, + reviewedAt: evidence.reviewedAt, + }; + saveSession(ctx, session); + return { sha, recipe: recipe.path, reviewed: true }; +} + +function assertCaptureReviewed(ctx, session, sha, recipe) { + if (recipe.database === false) return; + const review = session.captureReview; + if ( + review?.sha !== sha || + review?.recipeHash !== recipe.hash || + review?.environmentHash !== + readEnvironmentSnapshot(ctx, session).fingerprint || + review.sourceTrusted !== true || + review.commandsReviewed !== true || + review.credentialScopesReviewed !== true + ) + throw new Error( + "Credentialed capture requires a trusted review of this exact commit and recipe; use capture-review first", + ); +} + +export async function remoteCommand(sandbox, command, env = {}, timeout = 300) { + const result = await sandbox.process.executeCommand( + command, + "/home/daytona/feature", + env, + timeout, + ); + if (result.exitCode !== 0) { + const error = new Error( + `Sandbox command failed (exit ${result.exitCode}); inspect retained private failure evidence`, + ); + error.remoteOutput = result.result; + throw error; + } + return result.result; +} + +export function assertOwnedSandbox(session, sandbox) { + if ( + sandbox.labels?.["cap.building.session"] !== session.id || + sandbox.labels?.["cap.building.sha"] !== session.capture?.sha + ) { + throw new Error("Sandbox ownership or source commit mismatch"); + } +} + +export function validateCaptureProbe(probe) { + const duration = Number(probe.format?.duration); + if ( + !probe.streams?.some( + (stream) => + stream.codec_type === "video" && + stream.width === 1920 && + stream.height === 1080, + ) || + !Number.isFinite(duration) || + duration <= 0 + ) + throw new Error("Export did not contain a valid 1080p video"); +} + +export async function deleteSandbox(ctx, session, daytona) { + if (!session.capture?.sandboxId || session.capture.sandboxDeleted) return; + let sandbox; + try { + sandbox = await daytona.get(session.capture.sandboxId); + } catch (error) { + if (error.statusCode !== 404 && error.response?.status !== 404) throw error; + session.capture.sandboxDeleted = true; + saveSession(ctx, session); + return; + } + assertOwnedSandbox(session, sandbox); + await daytona.delete(sandbox, 60, true); + session.capture.sandboxDeleted = true; + saveSession(ctx, session); +} + +export async function capture(ctx, session, recipePath, daytona) { + const sha = assertClean(ctx, session); + const recipe = readRecipe(session, recipePath); + assertCaptureReviewed(ctx, session, sha, recipe); + if (session.capture?.uploadAttempted && !session.capture.upload) + throw new Error( + "Reconcile the prior uncertain upload before creating another capture", + ); + if (session.capture && !session.capture.sandboxDeleted) { + throw new Error( + "Reconcile or delete the previous owned sandbox before starting another capture", + ); + } + if ( + session.capture?.sha === sha && + session.capture?.recipeHash === recipe.hash && + session.capture?.status === "captured" && + existsSync(session.capture.file) + ) { + return session.capture; + } + const release = await waitForLock(join(ctx.state, "capture.lock"), 3600000); + const archive = temporaryPath("source.tar"); + try { + if (assertClean(ctx, session) !== sha) + throw new Error("Source changed while waiting for the recording slot"); + const tracked = git(session.worktree, "ls-files", "-z").split("\0"); + if ( + tracked.some( + (path) => + /(^|\/)\.env($|\.)/.test(path) && + !/\.(example|sample|template)$/.test(path), + ) + ) { + throw new Error( + "Tracked environment files must be reviewed before sandbox upload", + ); + } + git(session.worktree, "archive", "--format=tar", "--output", archive, sha); + if (recipe.database !== false) inspectDatabase(session); + const env = executionEnvironment(ctx, session, { credentials: false }); + const runtimeEnv = executionEnvironment(ctx, session, { + credentials: recipe.database !== false, + expectedHash: + recipe.database !== false + ? session.captureReview.environmentHash + : undefined, + }); + for (const key of [ + "PATH", + "HOME", + "USER", + "TMPDIR", + "SHELL", + "SYSTEMROOT", + ]) { + delete env[key]; + delete runtimeEnv[key]; + } + if (session.capture) { + session.captureHistory ??= []; + session.captureHistory.push(session.capture); + } + session.capture = { + sha, + recipeHash: recipe.hash, + status: "creating", + name: `cap-building-${session.id}-${Date.now()}`, + sandboxDeleted: false, + }; + saveSession(ctx, session); + let sandbox; + try { + sandbox = await daytona.create( + { + name: session.capture.name, + snapshot: recipe.snapshot ?? "daytona-small", + labels: { + "cap.building.session": session.id, + "cap.building.sha": sha, + }, + envVars: { ...env, VNC_RESOLUTION: "1920x1080" }, + autoStopInterval: 15, + autoDeleteInterval: 60, + ttlMinutes: 45, + }, + { timeout: 120 }, + ); + } catch (error) { + session.capture.status = "failed"; + try { + const existing = await daytona.get(session.capture.name); + assertOwnedSandbox(session, existing); + session.capture.sandboxId = existing.id; + saveSession(ctx, session); + await deleteSandbox(ctx, session, daytona); + } catch (cleanupError) { + if ( + cleanupError.statusCode === 404 || + cleanupError.response?.status === 404 + ) + session.capture.sandboxDeleted = true; + else session.capture.cleanupPending = true; + } + saveSession(ctx, session); + throw error; + } + session.capture.sandboxId = sandbox.id; + saveSession(ctx, session); + try { + const mkdir = await sandbox.process.executeCommand( + "mkdir -p /home/daytona/feature", + undefined, + undefined, + 30, + ); + if (mkdir.exitCode !== 0) + throw new Error("Could not prepare sandbox workspace"); + await sandbox.fs.uploadFile(archive, "/home/daytona/source.tar"); + await remoteCommand( + sandbox, + "tar -xf /home/daytona/source.tar -C /home/daytona/feature", + ); + await sandbox.computerUse.start(); + const display = await sandbox.computerUse.display.getInfo(); + if ( + display.primary_display?.width !== 1920 || + display.primary_display?.height !== 1080 + ) + throw new Error("Sandbox did not provide a 1920x1080 display"); + for (const command of recipe.setup) + await remoteCommand(sandbox, command, env, 600); + await remoteCommand(sandbox, "cap guide --json", env, 30); + session.capture.recorder = JSON.parse( + await remoteCommand(sandbox, "cap version --json", env, 30), + ); + const targets = JSON.parse( + await remoteCommand(sandbox, "cap targets --json", env, 30), + ); + const screen = targets.screens?.[0]; + if (screen?.id === undefined || screen?.id === null) + throw new Error("Cap CLI could not find the sandbox screen"); + const browserState = join( + dirname(sessionPath(ctx, session.id)), + "browser-state.json", + ); + if (recipe.database !== false && existsSync(browserState)) { + await sandbox.fs.uploadFile( + browserState, + "/home/daytona/browser-state.json", + ); + } + if (recipe.start) { + await remoteCommand( + sandbox, + `nohup sh -c ${shellQuote(recipe.start)} > /home/daytona/feature.log 2>&1 < /dev/null &`, + runtimeEnv, + 30, + ); + } + if (recipe.ready) await remoteCommand(sandbox, recipe.ready, env, 180); + session.capture.recordingAttempted = true; + saveSession(ctx, session); + const started = JSON.parse( + await remoteCommand( + sandbox, + `cap record start --screen ${shellQuote(screen.id)} --detach --path /home/daytona/demo.cap --json`, + env, + 30, + ), + ); + if (!started.recordingId) + throw new Error("Cap CLI did not return a recording ID"); + session.capture.recordingId = started.recordingId; + session.capture.status = "recording"; + saveSession(ctx, session); + let walkthroughError; + try { + await remoteCommand(sandbox, recipe.walkthrough, env, 180); + } catch (error) { + walkthroughError = error; + } + const stopped = JSON.parse( + await remoteCommand( + sandbox, + `cap record stop --id ${shellQuote(started.recordingId)} --json`, + env, + 60, + ), + ); + if (stopped.recordingMetaExists !== true) + throw new Error("Recording did not finalize"); + await remoteCommand( + sandbox, + "cap project validate /home/daytona/demo.cap --json", + env, + 30, + ); + await remoteCommand( + sandbox, + "cap export /home/daytona/demo.cap --output /home/daytona/demo.mp4 --json", + env, + 300, + ); + const file = join( + artifactDirectory(ctx, session), + `${session.capture.name}.mp4`, + ); + await sandbox.fs.downloadFile("/home/daytona/demo.mp4", file, 120); + const probe = jsonCommand("ffprobe", [ + "-v", + "error", + "-show_streams", + "-show_format", + "-of", + "json", + file, + ]); + validateCaptureProbe(probe); + session.capture.file = file; + session.capture.fileHash = createHash("sha256") + .update(readFileSync(file)) + .digest("hex"); + session.capture.status = walkthroughError ? "failed" : "captured"; + saveSession(ctx, session); + if (walkthroughError) throw walkthroughError; + return session.capture; + } catch (error) { + session.capture.status = "failed"; + let output = String(error.remoteOutput ?? error.message); + for (const [key, value] of Object.entries(runtimeEnv)) { + if (/SECRET|KEY|TOKEN|DATABASE_URL/.test(key) && value) + output = output.replaceAll(value, "[redacted]"); + } + const log = join( + artifactDirectory(ctx, session), + `${session.capture.name}-failure.txt`, + ); + writeFileSync(log, output, { mode: 0o600 }); + session.capture.failureLog = log; + if (session.capture.recordingAttempted) { + try { + await sandbox.process.executeCommand( + `cap record stop ${session.capture.recordingId ? `--id ${shellQuote(session.capture.recordingId)}` : ""} --json`, + undefined, + env, + 60, + ); + await remoteCommand( + sandbox, + "tar -czf /home/daytona/demo-project.tar.gz -C /home/daytona demo.cap", + env, + 60, + ); + const project = join( + artifactDirectory(ctx, session), + `${session.capture.name}-failed-project.tar.gz`, + ); + await sandbox.fs.downloadFile( + "/home/daytona/demo-project.tar.gz", + project, + 120, + ); + session.capture.failedProject = project; + } catch { + session.capture.preserveSandbox = true; + } + } + saveSession(ctx, session); + throw error; + } finally { + if (!session.capture.preserveSandbox) + await deleteSandbox(ctx, session, daytona); + } + } finally { + rmSync(archive, { force: true }); + release(); + } +} + +export async function shareCapture(ctx, session) { + const sha = assertClean(ctx, session); + if (!session.pr || session.pr.sha !== sha) + throw new Error("Record the current published PR head before uploading"); + if (session.capture?.sha !== sha || session.capture.status !== "captured") + throw new Error("A successful capture of the current commit is required"); + if ( + createHash("sha256") + .update(readFileSync(session.capture.file)) + .digest("hex") !== session.capture.fileHash + ) + throw new Error("Capture file changed"); + const release = await waitForLock(join(ctx.state, "cap-library.lock")); + try { + const account = jsonCommand("cap", ["account", "get", "--json"]); + const organization = + session.cap?.organization ?? account.defaultOrganizationId; + if (!organization) + throw new Error("Choose a Cap organization for PR recordings"); + if ( + !session.capture.upload && + organization !== account.defaultOrganizationId + ) + throw new Error("Restore the selected Cap organization before uploading"); + const folders = jsonCommand("cap", [ + "library", + "folders", + "list", + organization, + "--root", + "--json", + ]); + const matching = folders.folders.filter((folder) => folder.name === "PR's"); + if (matching.length > 1) + throw new Error( + "Multiple PR's folders exist; resolve the folder identity first", + ); + let folder = matching[0]; + if (!folder) + folder = jsonCommand("cap", [ + "library", + "folders", + "create", + organization, + "PR's", + "--yes", + "--json", + ]); + const folderId = folder.id ?? folder.resource?.id ?? folder.folder?.id; + if (!folderId) throw new Error("Cap did not return a folder ID"); + session.cap = { organization, folderId }; + saveSession(ctx, session); + if (!session.capture.upload) { + if (session.capture.uploadAttempted) + throw new Error( + "Upload result is uncertain. Reconcile the named Cap before retrying; do not create a duplicate.", + ); + session.capture.uploadAttempted = true; + session.capture.uploadTitle = `${session.pr?.number ? `#${session.pr.number} — ` : ""}${session.name} — ${sha.slice(0, 8)}`; + saveSession(ctx, session); + const upload = jsonCommand("cap", [ + "upload", + session.capture.file, + "--name", + session.capture.uploadTitle, + "--json", + ]); + if (!upload.id || !upload.link) + throw new Error("Cap upload returned an incomplete receipt"); + session.capture.upload = { id: upload.id, link: upload.link }; + saveSession(ctx, session); + } + const { id, link } = session.capture.upload; + run("cap", [ + "caps", + "move", + id, + "--container", + "personal", + "--organization", + organization, + "--folder", + folderId, + "--yes", + "--json", + ]); + const metadata = jsonCommand("cap", ["caps", "get", id, "--json"]); + const cap = metadata.cap ?? metadata; + if ( + cap.id !== id || + cap.folderId !== folderId || + cap.organizationId !== organization + ) + throw new Error("Cap folder placement could not be verified"); + session.capture.upload.folderVerified = true; + saveSession(ctx, session); + return { id, link, folderId, playbackVerificationRequired: true }; + } finally { + release(); + } +} diff --git a/.agents/skills/building/scripts/core.mjs b/.agents/skills/building/scripts/core.mjs new file mode 100644 index 00000000000..ebeb49365d5 --- /dev/null +++ b/.agents/skills/building/scripts/core.mjs @@ -0,0 +1,828 @@ +import { execFileSync, spawn } from "node:child_process"; +import { createHash, randomBytes, randomUUID } from "node:crypto"; +import { + existsSync, + lstatSync, + mkdirSync, + readdirSync, + readFileSync, + realpathSync, + renameSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { createServer } from "node:net"; +import { hostname, tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; + +export function run(command, args, options = {}) { + try { + return execFileSync(command, args, { + encoding: "utf8", + maxBuffer: 16 * 1024 * 1024, + stdio: ["ignore", "pipe", "pipe"], + env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" }, + ...options, + }).trim(); + } catch (error) { + throw new Error(`${command} failed (exit ${error.status ?? "unknown"})`, { + cause: error, + }); + } +} + +export function git(repo, ...args) { + return run("git", ["-C", repo, ...args]); +} + +export function jsonCommand(command, args, options) { + return JSON.parse(run(command, args, options)); +} + +export function atomicJson(path, value) { + mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); + const temp = `${path}.${randomUUID()}.tmp`; + writeFileSync(temp, `${JSON.stringify(value, null, "\t")}\n`, { + mode: 0o600, + flag: "wx", + }); + renameSync(temp, path); +} + +export function lock(path) { + mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); + try { + mkdirSync(path, { mode: 0o700 }); + } catch (error) { + if (error.code === "EEXIST") { + const held = new Error( + `Resource is locked: ${path}. Inspect its owner before recovery.`, + ); + held.code = "LOCK_HELD"; + throw held; + } + throw error; + } + const token = randomUUID(); + atomicJson(join(path, "owner.json"), { + token, + pid: process.pid, + host: hostname(), + createdAt: new Date().toISOString(), + }); + return () => { + const owner = JSON.parse(readFileSync(join(path, "owner.json"), "utf8")); + if (owner.token !== token) throw new Error("Lock ownership changed"); + rmSync(path, { recursive: true }); + }; +} + +export async function waitForLock(path, timeoutMs = 120000) { + const start = Date.now(); + for (;;) { + try { + return lock(path); + } catch (error) { + if (error.code !== "LOCK_HELD" || Date.now() - start >= timeoutMs) + throw error; + const ownerPath = join(path, "owner.json"); + if (existsSync(ownerPath)) { + let owner; + try { + owner = JSON.parse(readFileSync(ownerPath, "utf8")); + } catch (readError) { + if (readError.code === "ENOENT") continue; + throw readError; + } + if (owner.host === hostname()) { + try { + process.kill(owner.pid, 0); + } catch (processError) { + if (processError.code === "ESRCH") + throw new Error( + `Inactive lock owner at ${path}; reconcile its recorded operation before resuming`, + ); + throw processError; + } + } + } + await new Promise((resolveWait) => + setTimeout(resolveWait, 50 + Math.floor(Math.random() * 100)), + ); + } + } +} + +export function context(repo) { + const root = git(resolve(repo), "rev-parse", "--show-toplevel"); + const common = realpathSync( + git(root, "rev-parse", "--path-format=absolute", "--git-common-dir"), + ); + return { root, common, state: join(common, "building") }; +} + +export function sessionPath(ctx, id) { + if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(id)) + throw new Error("Invalid session ID"); + return join(ctx.state, "sessions", id, "session.json"); +} + +export function readSession(ctx, id) { + const session = JSON.parse(readFileSync(sessionPath(ctx, id), "utf8")); + if ( + session.id !== id || + session.common !== ctx.common || + session.version !== 1 + ) { + throw new Error("Session identity mismatch"); + } + return session; +} + +export function saveSession(ctx, session) { + session.updatedAt = new Date().toISOString(); + atomicJson(sessionPath(ctx, session.id), session); +} + +export function assertWorktree(ctx, session) { + if (context(session.worktree).common !== ctx.common) { + throw new Error("Worktree belongs to another repository"); + } + if ( + git(session.worktree, "symbolic-ref", "--short", "HEAD") !== session.branch + ) { + throw new Error("Worktree branch changed"); + } +} + +export function assertClean(ctx, session) { + assertWorktree(ctx, session); + if (git(session.worktree, "status", "--porcelain", "--untracked-files=all")) { + throw new Error("Commit session changes before capturing or publishing"); + } + return git(session.worktree, "rev-parse", "HEAD"); +} + +export async function availablePort(start, excluded = new Set()) { + for (let port = start; port < start + 1000; port++) { + if (excluded.has(port)) continue; + const free = await new Promise((resolvePort) => { + const server = createServer(); + server.once("error", () => resolvePort(false)); + server.listen(port, "127.0.0.1", () => + server.close(() => resolvePort(true)), + ); + }); + if (free) return port; + } + throw new Error("No available building ports"); +} + +export async function createSession(ctx, options) { + if (!["web", "desktop", "gpui", "fullstack"].includes(options.target)) { + throw new Error("Target must be web, desktop, gpui, or fullstack"); + } + if (!options.name?.trim()) throw new Error("A feature name is required"); + const release = await waitForLock( + join(ctx.state, "registry.lock"), + options.lockWaitMs ?? 120000, + ); + try { + const slug = + options.name + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, "") + .slice(0, 24) || "feature"; + const id = `${slug}-${randomBytes(4).toString("hex")}`; + const base = options.base ?? "origin/main"; + const baseSha = git(ctx.root, "rev-parse", "--verify", `${base}^{commit}`); + const branch = `building/${id}`; + const worktree = join(dirname(ctx.common), "..", "Cap-building", id); + const sessionsDir = join(ctx.state, "sessions"); + const ports = new Set(); + if (existsSync(sessionsDir)) { + for (const entry of readdirSync(sessionsDir)) { + const path = join(sessionsDir, entry, "session.json"); + if (!existsSync(path)) continue; + const other = JSON.parse(readFileSync(path, "utf8")); + if (other.status !== "closed") { + for (const value of Object.values(other.ports ?? {})) + ports.add(value); + } + } + } + const web = await availablePort(4100, ports); + ports.add(web); + const desktop = await availablePort(5100, ports); + const session = { + version: 1, + id, + name: options.name, + target: options.target, + common: ctx.common, + worktree: resolve(worktree), + branch, + base, + baseSha, + ports: { web, desktop }, + status: "creating", + createdAt: new Date().toISOString(), + database: { + organization: "cap", + database: "cap-production", + name: `building-${id}`, + parent: "main", + status: "planned", + }, + checks: [], + }; + saveSession(ctx, session); + git(ctx.root, "worktree", "add", "-b", branch, session.worktree, baseSha); + session.status = "active"; + saveSession(ctx, session); + return session; + } finally { + release(); + } +} + +export function ensureResume(ctx, session) { + if (!["active", "creating", "published", "resuming"].includes(session.status)) + throw new Error("Complete pending cleanup before resuming this session"); + if (session.status === "published") { + if (existsSync(session.worktree)) + throw new Error( + "Published worktree unexpectedly exists; inspect before resuming", + ); + session.resumeSha = git(ctx.root, "rev-parse", session.branch); + session.status = "resuming"; + session.databaseHistory ??= []; + session.databaseHistory.push(session.database); + session.database = { + organization: session.database.organization, + database: session.database.database, + name: session.database.name, + parent: session.database.parent, + status: "planned", + }; + delete session.noVisual; + delete session.cleanupMode; + saveSession(ctx, session); + } + if (session.status === "resuming") { + if (git(ctx.root, "rev-parse", session.branch) !== session.resumeSha) + throw new Error( + "Branch changed during resume; inspect before continuing", + ); + if (!existsSync(session.worktree)) + git(ctx.root, "worktree", "add", session.worktree, session.branch); + if (assertClean(ctx, session) !== session.resumeSha) + throw new Error("Worktree changed during resume"); + rmSync(environmentPath(ctx, session), { force: true }); + rmSync(join(dirname(sessionPath(ctx, session.id)), "browser-state.json"), { + force: true, + }); + } + if (!existsSync(session.worktree) && session.status === "creating") { + let branchExists = false; + try { + git(ctx.root, "show-ref", "--verify", `refs/heads/${session.branch}`); + branchExists = true; + } catch {} + if (branchExists) { + if (git(ctx.root, "rev-parse", session.branch) !== session.baseSha) { + throw new Error("Partially created branch has changed"); + } + git(ctx.root, "worktree", "add", session.worktree, session.branch); + } else { + git( + ctx.root, + "worktree", + "add", + "-b", + session.branch, + session.worktree, + session.baseSha, + ); + } + } + assertWorktree(ctx, session); + session.status = "active"; + saveSession(ctx, session); + return session; +} + +export function assertDevBranch(session, branch) { + if ( + branch.name !== session.database.name || + !branch.name.startsWith("building-") || + branch.production !== false || + branch.ready !== true + ) { + throw new Error( + "Expected the ready session-owned PlanetScale development branch", + ); + } + if (branch.parent_branch !== session.database.parent) { + throw new Error("PlanetScale parent branch mismatch"); + } + if (session.database.id && branch.id !== session.database.id) { + throw new Error("PlanetScale branch identity changed"); + } +} + +export function pscale(session, args) { + return jsonCommand("pscale", [ + ...args, + "--org", + session.database.organization, + "--format", + "json", + ]); +} + +export function inspectDatabase(session) { + const branch = pscale(session, [ + "branch", + "show", + session.database.database, + session.database.name, + ]); + assertDevBranch(session, branch); + return branch; +} + +export function attachDatabase(ctx, session) { + const branch = inspectDatabase(session); + session.database = { ...session.database, id: branch.id, status: "ready" }; + saveSession(ctx, session); + return session.database; +} + +export function environmentPath(ctx, session) { + return join(dirname(sessionPath(ctx, session.id)), "environment.json"); +} + +export function readEnvironment(ctx, session) { + return readEnvironmentSnapshot(ctx, session).environment; +} + +export function readEnvironmentSnapshot(ctx, session) { + const path = environmentPath(ctx, session); + const serialized = existsSync(path) ? readFileSync(path, "utf8") : "{}"; + return { + environment: JSON.parse(serialized), + fingerprint: createHash("sha256").update(serialized).digest("hex"), + }; +} + +export function writeEnvironment(ctx, session, env) { + atomicJson(environmentPath(ctx, session), env); +} + +export function databaseCredentials(ctx, session) { + inspectDatabase(session); + const path = environmentPath(ctx, session); + const env = existsSync(path) ? readEnvironment(ctx, session) : {}; + if (env.DATABASE_URL) return { reused: true }; + if (session.database.passwordAttempted) { + throw new Error( + "Credential creation was interrupted. Revoke the recorded password name before explicitly retrying.", + ); + } + session.database.passwordName = `building-${session.id}`; + session.database.passwordAttempted = true; + saveSession(ctx, session); + const password = pscale(session, [ + "password", + "create", + session.database.database, + session.database.name, + session.database.passwordName, + "--role", + "admin", + "--ttl", + "168h", + ]); + if ( + !password.username || + !password.plain_text || + !password.access_host_url || + !password.id + ) { + throw new Error( + "PlanetScale returned an incomplete password; secret output was withheld", + ); + } + const url = new URL( + `mysql://${password.access_host_url}/${session.database.database}`, + ); + url.username = password.username; + url.password = password.plain_text; + url.searchParams.set("sslaccept", "strict"); + session.database.passwordId = password.id; + session.database.username = password.username; + session.database.host = password.access_host_url; + saveSession(ctx, session); + writeEnvironment(ctx, session, { + ...env, + DATABASE_URL: url.href, + NEXTAUTH_SECRET: randomBytes(32).toString("base64"), + DATABASE_ENCRYPTION_KEY: randomBytes(32).toString("hex"), + }); + session.database.expiresAt = password.expires_at ?? null; + saveSession(ctx, session); + return { created: true, passwordId: password.id }; +} + +export function executionEnvironment( + ctx, + session, + { credentials = true, expectedHash } = {}, +) { + const snapshot = credentials + ? readEnvironmentSnapshot(ctx, session) + : undefined; + if (expectedHash && snapshot?.fingerprint !== expectedHash) + throw new Error("Credential configuration changed since capture review"); + const stored = snapshot?.environment ?? {}; + if (stored.DATABASE_URL) { + const database = new URL(stored.DATABASE_URL); + if ( + !session.database.id || + decodeURIComponent(database.username) !== session.database.username || + database.hostname !== session.database.host || + database.pathname !== `/${session.database.database}` + ) { + throw new Error("Database credential does not match the session branch"); + } + } + const url = `http://127.0.0.1:${session.ports.web}`; + const env = {}; + for (const key of [ + "PATH", + "HOME", + "USER", + "TMPDIR", + "SHELL", + "LANG", + "LC_ALL", + "SYSTEMROOT", + ]) { + if (process.env[key]) env[key] = process.env[key]; + } + const allowed = new Set([ + "DATABASE_URL", + "NEXTAUTH_SECRET", + "DATABASE_ENCRYPTION_KEY", + "CAP_AWS_BUCKET", + "CAP_AWS_REGION", + "CAP_AWS_ACCESS_KEY", + "CAP_AWS_SECRET_KEY", + "CAP_AWS_ENDPOINT", + "S3_PUBLIC_ENDPOINT", + "S3_INTERNAL_ENDPOINT", + "MEDIA_SERVER_URL", + "MEDIA_SERVER_WEBHOOK_SECRET", + ]); + for (const [key, value] of Object.entries(stored)) { + if (allowed.has(key)) env[key] = value; + } + return { + ...env, + NODE_ENV: "development", + WEB_URL: url, + NEXTAUTH_URL: url, + NEXT_PUBLIC_WEB_URL: url, + VITE_SERVER_URL: url, + PORT: String(session.ports.web), + CAP_BUILDING_SESSION: session.id, + CAP_AWS_BUCKET: stored.CAP_AWS_BUCKET ?? `building-${session.id}`, + CAP_AWS_REGION: stored.CAP_AWS_REGION ?? "us-east-1", + CAP_AWS_ENDPOINT: stored.CAP_AWS_ENDPOINT ?? "http://127.0.0.1:1", + S3_PUBLIC_ENDPOINT: + stored.S3_PUBLIC_ENDPOINT ?? + stored.CAP_AWS_ENDPOINT ?? + "http://127.0.0.1:1", + S3_INTERNAL_ENDPOINT: + stored.S3_INTERNAL_ENDPOINT ?? + stored.CAP_AWS_ENDPOINT ?? + "http://127.0.0.1:1", + AWS_EC2_METADATA_DISABLED: "true", + AWS_SHARED_CREDENTIALS_FILE: join(ctx.state, "no-aws-credentials"), + AWS_CONFIG_FILE: join(ctx.state, "no-aws-config"), + }; +} + +export async function execInSession( + ctx, + session, + args, + { runtime = false } = {}, +) { + if (process.platform === "win32") + throw new Error( + "Local process ownership is supported on macOS/Linux; use a dedicated Windows runner", + ); + assertWorktree(ctx, session); + if (session.database.id) inspectDatabase(session); + if (!args.length) throw new Error("Expected a command after --"); + for (const path of [ + ".env", + ".env.local", + ".env.development", + ".env.development.local", + "apps/web/.env", + "apps/web/.env.local", + "apps/web/.env.development", + "apps/web/.env.development.local", + ]) { + if (existsSync(join(session.worktree, path))) + throw new Error(`Remove or review unmanaged environment file: ${path}`); + } + const release = runtime + ? await waitForLock(join(ctx.state, "native-runtime.lock"), 3600000) + : () => {}; + let released = true; + try { + const env = executionEnvironment(ctx, session); + let processPath; + let completion; + const child = await withSessionStartup(ctx, session, () => { + const child = spawn(args[0], args.slice(1), { + cwd: session.worktree, + env, + stdio: "inherit", + detached: true, + }); + completion = new Promise((resolveExit, reject) => { + child.once("error", reject); + child.once("exit", (status) => resolveExit(status ?? 1)); + }); + if (child.pid) + processPath = recordProcess(ctx, session, { + processGroup: child.pid, + native: runtime, + }); + return child; + }); + let termination; + const forward = () => { + termination ??= terminateGroup(child.pid).catch(() => false); + }; + process.once("SIGINT", forward); + process.once("SIGTERM", forward); + let commandError; + try { + const code = await completion; + if (code !== 0) throw new Error(`Session command exited ${code}`); + } catch (error) { + commandError = error; + } finally { + process.removeListener("SIGINT", forward); + process.removeListener("SIGTERM", forward); + if (child.pid) { + released = false; + released = await (termination ?? terminateGroup(child.pid)).catch( + () => false, + ); + if (released) rmSync(processPath, { force: true }); + } + } + if (!released) { + const error = new Error( + `Process group ${child.pid} remains active; retain its runtime lease and inspect ${processPath}`, + ); + error.retainLease = true; + throw error; + } + if (commandError) throw commandError; + } finally { + if (released) release(); + } +} + +export async function withSessionStartup(ctx, session, start) { + const release = await waitForLock( + join(dirname(sessionPath(ctx, session.id)), "lifecycle.lock"), + ); + try { + if (readSession(ctx, session.id).status !== "active") + throw new Error("Resume an active session before starting processes"); + return await start(); + } finally { + release(); + } +} + +export function recordProcess(ctx, session, metadata) { + const path = join( + dirname(sessionPath(ctx, session.id)), + `process-${process.pid}.json`, + ); + atomicJson(path, { + ...metadata, + host: hostname(), + wrapperPid: process.pid, + wrapperStarted: run("ps", ["-p", String(process.pid), "-o", "lstart="]), + }); + return path; +} + +export async function stopSessionProcesses(ctx, session) { + const directory = dirname(sessionPath(ctx, session.id)); + for (const entry of readdirSync(directory)) { + if (!/^process-\d+\.json$/.test(entry)) continue; + const path = join(directory, entry); + const owner = JSON.parse(readFileSync(path, "utf8")); + if (owner.host !== hostname() || owner.wrapperPid === process.pid) + throw new Error( + "Cannot stop a process owned by another host or the current command", + ); + let started; + try { + started = run("ps", ["-p", String(owner.wrapperPid), "-o", "lstart="]); + } catch { + if (signalGroup(owner.processGroup, 0)) + throw new Error( + `Orphaned process group ${owner.processGroup} needs identity reconciliation`, + ); + rmSync(path); + continue; + } + if (started !== owner.wrapperStarted) + throw new Error("Process ID was reused; refusing to stop it"); + process.kill(owner.wrapperPid, "SIGTERM"); + for (let attempt = 0; attempt < 100 && existsSync(path); attempt++) + await new Promise((resolveWait) => setTimeout(resolveWait, 100)); + if (existsSync(path)) + throw new Error( + "Owned process has not confirmed shutdown; cleanup remains pending", + ); + } +} + +function signalGroup(pid, signal) { + if (!pid) return false; + try { + process.kill(-pid, signal); + return true; + } catch (error) { + if (error.code === "ESRCH") return false; + throw error; + } +} + +async function terminateGroup(pid) { + for (const signal of ["SIGTERM", "SIGKILL"]) { + if (!signalGroup(pid, signal)) return true; + for (let attempt = 0; attempt < 20; attempt++) { + await new Promise((resolveWait) => setTimeout(resolveWait, 100)); + if (!signalGroup(pid, 0)) return true; + } + } + return false; +} + +export function hashFiles(root, files) { + const hash = createHash("sha256"); + for (const file of files) { + hash.update(file); + if (existsSync(join(root, file))) + hash.update(readFileSync(join(root, file))); + } + return hash.digest("hex"); +} + +export function rustBuildFingerprint(root) { + const hash = createHash("sha256"); + hash.update(git(root, "rev-parse", "HEAD^{tree}")); + hash.update( + git(root, "diff", "--no-ext-diff", "--no-textconv", "--binary", "HEAD"), + ); + const localFiles = git( + root, + "ls-files", + "--others", + "--exclude-standard", + "-z", + ) + .split("\0") + .filter(Boolean); + hash.update( + hashFiles( + root, + [ + ...new Set([ + ...localFiles, + ".cargo/config", + ".cargo/config.toml", + "apps/desktop-gpui/.cargo/config", + "apps/desktop-gpui/.cargo/config.toml", + ]), + ].sort(), + ), + ); + return hash.digest("hex"); +} + +export function cloneDirectory(source, destination) { + if (lstatSync(source).isSymbolicLink()) + throw new Error("Clone source must be a real directory"); + if (existsSync(destination)) + throw new Error("Clone destination already exists"); + mkdirSync(dirname(destination), { recursive: true }); + const args = + process.platform === "darwin" + ? ["-cR", source, destination] + : ["--reflink=always", "-R", source, destination]; + run("cp", args); +} + +export function assertIdle(path) { + try { + const output = execFileSync("lsof", ["-t", "+D", path], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (output.trim()) throw new Error(`Open files in cache source: ${path}`); + } catch (error) { + if (error.status === 1 && !String(error.stderr ?? "").trim()) return; + throw error; + } +} + +export async function warmDependencies(ctx, session, source, rust = false) { + assertWorktree(ctx, session); + source = realpathSync(source); + if (context(source).common !== ctx.common) + throw new Error("Cache source must belong to this repository"); + const files = [ + "scripts/setup.js", + "Cargo.lock", + "rust-toolchain.toml", + "apps/desktop-gpui/Cargo.lock", + "apps/desktop-gpui/rust-toolchain.toml", + ]; + const fingerprint = hashFiles(session.worktree, files); + if (fingerprint !== hashFiles(source, files)) + throw new Error( + "Dependency/toolchain versions differ; prepare a matching cache", + ); + const rustFingerprint = rust + ? rustBuildFingerprint(session.worktree) + : undefined; + if (rust && rustFingerprint !== rustBuildFingerprint(source)) + throw new Error( + "Rust source or Cargo configuration differs; prepare matching build inputs before warming compiled artifacts", + ); + const release = await waitForLock(join(ctx.state, "cache.lock")); + try { + const directories = [ + "target/native-deps", + ...(rust ? ["target/debug", "apps/desktop-gpui/target/debug"] : []), + ]; + for (const directory of directories) { + const origin = join(source, directory); + if (!existsSync(origin)) continue; + const destination = join(session.worktree, directory); + if (existsSync(destination)) + throw new Error(`Refusing to overwrite ${directory}`); + assertIdle(origin); + const cache = join( + ctx.state, + "cache", + `${process.platform}-${process.arch}-${directory === "target/native-deps" ? fingerprint : rustFingerprint}`, + directory, + ); + if (!existsSync(cache)) { + const staging = `${cache}.${randomUUID()}.tmp`; + cloneDirectory(origin, staging); + assertIdle(origin); + renameSync(staging, cache); + } + cloneDirectory(cache, destination); + } + session.dependencies = { + fingerprint, + rustFingerprint, + strategy: "copy-on-write", + rust, + }; + saveSession(ctx, session); + return session.dependencies; + } finally { + release(); + } +} + +export function artifactDirectory(ctx, session) { + const path = join(dirname(sessionPath(ctx, session.id)), "artifacts"); + mkdirSync(path, { recursive: true, mode: 0o700 }); + return path; +} + +export function temporaryPath(name) { + return join(tmpdir(), `cap-building-${randomUUID()}-${name}`); +} diff --git a/.agents/skills/building/scripts/core.test.mjs b/.agents/skills/building/scripts/core.test.mjs new file mode 100644 index 00000000000..689ac70b9f8 --- /dev/null +++ b/.agents/skills/building/scripts/core.test.mjs @@ -0,0 +1,979 @@ +import assert from "node:assert/strict"; +import { execFileSync, spawn } from "node:child_process"; +import { createHash } from "node:crypto"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + statSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import test from "node:test"; +import { setTimeout as delay } from "node:timers/promises"; +import { fileURLToPath } from "node:url"; +import { main, renderPrBody } from "./building.mjs"; +import { + assertOwnedSandbox, + capture, + readRecipe, + recordCaptureReview, + shareCapture, + shellQuote, + validateCaptureProbe, +} from "./capture.mjs"; +import { + assertClean, + assertDevBranch, + cloneDirectory, + context, + createSession, + ensureResume, + executionEnvironment, + git, + lock, + readEnvironmentSnapshot, + readSession, + rustBuildFingerprint, + saveSession, + sessionPath, + warmDependencies, + writeEnvironment, +} from "./core.mjs"; +import { fixtureIds } from "./database.mjs"; + +function fixture(t) { + const directory = mkdtempSync(join(tmpdir(), "cap-building-test-")); + const repo = join(directory, "repo"); + mkdirSync(repo); + execFileSync("git", ["init", "-b", "main", repo], { stdio: "ignore" }); + git(repo, "config", "user.email", "test@example.invalid"); + git(repo, "config", "user.name", "Building Test"); + writeFileSync(join(repo, "file.txt"), "base\n"); + git(repo, "add", "file.txt"); + git(repo, "commit", "-m", "test: base"); + t.after(() => rmSync(directory, { recursive: true, force: true })); + return context(repo); +} + +test("the CLI entry point runs through an editor skill symlink", (t) => { + const ctx = fixture(t); + const alias = join(ctx.root, "skill-alias"); + symlinkSync(dirname(fileURLToPath(import.meta.url)), alias, "dir"); + const output = execFileSync( + process.execPath, + [join(alias, "building.mjs"), "help"], + { encoding: "utf8" }, + ); + assert.match(output, /^Cap building workflow\n/); + const imported = execFileSync( + process.execPath, + ["--input-type=module", "-"], + { + encoding: "utf8", + input: `await import(${JSON.stringify(new URL("./building.mjs", import.meta.url).href)}); console.log("imported");`, + }, + ); + assert.equal(imported.trim(), "imported"); +}); + +test("creating two sessions preserves a dirty checkout and assigns independent branches and ports", async (t) => { + const ctx = fixture(t); + writeFileSync(join(ctx.root, "file.txt"), "staged\n"); + git(ctx.root, "add", "file.txt"); + writeFileSync(join(ctx.root, "file.txt"), "unstaged\n"); + writeFileSync(join(ctx.root, "untracked"), "user work\n"); + const index = readFileSync(join(ctx.common, "index")); + const before = git(ctx.root, "status", "--porcelain"); + const [a, b] = [ + await createSession(ctx, { + name: "New folders", + target: "web", + base: "main", + }), + await createSession(ctx, { + name: "New folders", + target: "web", + base: "main", + }), + ]; + assert.notEqual(a.worktree, b.worktree); + assert.notEqual(a.ports.web, b.ports.web); + assert.notEqual(a.ports.desktop, b.ports.desktop); + assert.equal(a.branch, `building/${a.id}`); + assert.equal(b.branch, `building/${b.id}`); + assert.equal(readFileSync(join(a.worktree, "file.txt"), "utf8"), "base\n"); + assert.equal(git(ctx.root, "status", "--porcelain"), before); + assert.deepEqual(readFileSync(join(ctx.common, "index")), index); + assert.equal(git(ctx.root, "symbolic-ref", "--short", "HEAD"), "main"); +}); + +test("registry lock prevents a concurrent creation without stealing ownership", async (t) => { + const ctx = fixture(t); + const release = lock(join(ctx.state, "registry.lock")); + await assert.rejects( + createSession(ctx, { + name: "blocked", + target: "web", + base: "main", + lockWaitMs: 0, + }), + /locked/, + ); + release(); + const session = await createSession(ctx, { + name: "allowed", + target: "web", + base: "main", + }); + assert.equal(session.status, "active"); +}); + +test("20 concurrent sessions preserve main and edit the same file independently", async (t) => { + const ctx = fixture(t); + writeFileSync(join(ctx.root, "file.txt"), "main user changes"); + const index = readFileSync(join(ctx.common, "index")); + const sessions = await Promise.all( + Array.from({ length: 20 }, (_, index) => + createSession(ctx, { + name: `feature-${index}`, + target: "web", + base: "main", + }), + ), + ); + assert.equal(new Set(sessions.map((session) => session.worktree)).size, 20); + assert.equal(new Set(sessions.map((session) => session.branch)).size, 20); + assert.equal( + new Set(sessions.map((session) => session.database.name)).size, + 20, + ); + assert.equal( + new Set(sessions.flatMap((session) => Object.values(session.ports))).size, + 40, + ); + for (const session of sessions) + writeFileSync(join(session.worktree, "file.txt"), session.id); + for (const session of sessions) + assert.equal( + readFileSync(join(session.worktree, "file.txt"), "utf8"), + session.id, + ); + assert.equal( + readFileSync(join(ctx.root, "file.txt"), "utf8"), + "main user changes", + ); + assert.deepEqual(readFileSync(join(ctx.common, "index")), index); + assert.equal(git(ctx.root, "symbolic-ref", "--short", "HEAD"), "main"); +}); + +test("resuming a partial creation keeps the same branch and worktree identity", async (t) => { + const ctx = fixture(t); + const session = await createSession(ctx, { + name: "resume", + target: "web", + base: "main", + }); + git(ctx.root, "worktree", "remove", session.worktree); + session.status = "creating"; + saveSession(ctx, session); + await main(["resume", "--repo", ctx.root, "--session", session.id]); + assert.equal(readSession(ctx, session.id).status, "active"); + assert.equal( + git(session.worktree, "symbolic-ref", "--short", "HEAD"), + session.branch, + ); + assert.equal(ensureResume(ctx, session).id, session.id); +}); + +test("session identifiers cannot escape the state directory", (t) => { + const ctx = fixture(t); + assert.throws( + () => sessionPath(ctx, "../../elsewhere"), + /Invalid session ID/, + ); +}); + +test("database protection rejects production, renamed, replaced, and unready branches", () => { + const session = { + database: { name: "building-test", parent: "main", id: "owned" }, + }; + const branch = { + name: "building-test", + parent_branch: "main", + id: "owned", + ready: true, + production: false, + }; + assert.doesNotThrow(() => assertDevBranch(session, branch)); + for (const change of [ + { production: true }, + { ready: false }, + { name: "main" }, + { id: "other" }, + { parent_branch: "staging" }, + ]) { + assert.throws(() => assertDevBranch(session, { ...branch, ...change })); + } +}); + +test("session commands do not inherit production integration secrets", async (t) => { + const ctx = fixture(t); + const session = await createSession(ctx, { + name: "env", + target: "web", + base: "main", + }); + const previous = process.env.STRIPE_SECRET_KEY; + process.env.STRIPE_SECRET_KEY = "sk_live_fixture"; + t.after(() => { + if (previous === undefined) delete process.env.STRIPE_SECRET_KEY; + else process.env.STRIPE_SECRET_KEY = previous; + }); + writeEnvironment(ctx, session, { + STRIPE_SECRET_KEY: "sk_live_other", + RESEND_API_KEY: "fixture", + }); + const env = executionEnvironment(ctx, session); + assert.equal(env.STRIPE_SECRET_KEY, undefined); + assert.equal(env.RESEND_API_KEY, undefined); + assert.equal(env.CAP_AWS_ENDPOINT, "http://127.0.0.1:1"); + assert.equal(env.WEB_URL, `http://127.0.0.1:${session.ports.web}`); + assert.equal( + statSync(join(dirname(sessionPath(ctx, session.id)), "environment.json")) + .mode & 0o777, + 0o600, + ); +}); + +test("database credentials must match the recorded session identity", async (t) => { + const ctx = fixture(t); + const session = await createSession(ctx, { + name: "env-identity", + target: "web", + base: "main", + }); + session.database = { + ...session.database, + id: "owned", + username: "dev", + host: "db.test", + }; + writeEnvironment(ctx, session, { + DATABASE_URL: "mysql://prod:secret@db.test/cap-production", + }); + assert.throws(() => executionEnvironment(ctx, session), /does not match/); + writeEnvironment(ctx, session, { + DATABASE_URL: "mysql://dev:secret@db.test/cap-production", + }); + assert.equal( + new URL(executionEnvironment(ctx, session).DATABASE_URL).username, + "dev", + ); +}); + +test("copy-on-write clones remain independent when one worktree changes a dependency", (t) => { + const ctx = fixture(t); + const source = join(ctx.root, "deps"); + mkdirSync(source); + writeFileSync(join(source, "library"), "original"); + const a = join(ctx.root, "clone-a"); + const b = join(ctx.root, "clone-b"); + cloneDirectory(source, a); + cloneDirectory(source, b); + writeFileSync(join(a, "library"), "changed"); + assert.equal(readFileSync(join(source, "library"), "utf8"), "original"); + assert.equal(readFileSync(join(b, "library"), "utf8"), "original"); + assert.throws(() => cloneDirectory(source, b), /already exists/); +}); + +test("compiled cache identity changes with source, manifests, and ignored Cargo configuration", async (t) => { + const ctx = fixture(t); + const session = await createSession(ctx, { + name: "cache", + target: "desktop", + base: "main", + }); + const initial = rustBuildFingerprint(ctx.root); + assert.equal(rustBuildFingerprint(session.worktree), initial); + writeFileSync(join(session.worktree, "file.txt"), "changed source"); + assert.notEqual(rustBuildFingerprint(session.worktree), initial); + await assert.rejects( + warmDependencies(ctx, session, ctx.root, true), + /Rust source or Cargo configuration differs/, + ); + writeFileSync(join(session.worktree, "file.txt"), "base\n"); + writeFileSync( + join(session.worktree, "Cargo.toml"), + "[profile.dev]\nopt-level = 2\n", + ); + assert.notEqual(rustBuildFingerprint(session.worktree), initial); + rmSync(join(session.worktree, "Cargo.toml")); + mkdirSync(join(session.worktree, ".cargo")); + writeFileSync(join(ctx.common, "info", "exclude"), ".cargo/\n"); + writeFileSync( + join(session.worktree, ".cargo", "config.toml"), + '[build]\ntarget = "aarch64-apple-darwin"\n', + ); + assert.equal(git(session.worktree, "status", "--porcelain"), ""); + assert.notEqual(rustBuildFingerprint(session.worktree), initial); + await assert.rejects( + warmDependencies(ctx, session, ctx.root, true), + /Rust source or Cargo configuration differs/, + ); +}); + +test("capture rejects dirty source and untracked or escaping recipes", async (t) => { + const ctx = fixture(t); + const session = await createSession(ctx, { + name: "recipe", + target: "web", + base: "main", + }); + assert.throws( + () => readRecipe(session, join(ctx.root, "file.txt")), + /inside/, + ); + writeFileSync(join(session.worktree, "demo.json"), "{}"); + assert.throws(() => assertClean(ctx, session), /Commit/); + assert.throws( + () => readRecipe(session, join(session.worktree, "demo.json")), + /git failed/, + ); +}); + +test("checks execute with a held session lock and are tied to a clean commit", async (t) => { + const ctx = fixture(t); + const session = await createSession(ctx, { + name: "check", + target: "web", + base: "main", + }); + const operationLock = join( + dirname(sessionPath(ctx, session.id)), + "operation.lock", + ); + await main([ + "check", + "--repo", + ctx.root, + "--session", + session.id, + "--", + process.execPath, + "-e", + `if(!require('fs').existsSync(${JSON.stringify(operationLock)})) process.exit(1)`, + ]); + const state = readSession(ctx, session.id); + assert.equal(state.checks.length, 1); + assert.equal(state.checks[0].sha, git(session.worktree, "rev-parse", "HEAD")); + assert.equal(existsSync(operationLock), false); + await assert.rejects( + main([ + "check", + "--repo", + ctx.root, + "--session", + session.id, + "--", + process.execPath, + "-e", + "process.exit(3)", + ]), + /exited 3/, + ); + assert.equal(readSession(ctx, session.id).checks.length, 2); + assert.equal(readSession(ctx, session.id).checks.at(-1).passed, false); +}); + +test("PR body refuses stale recordings and checks", () => { + const session = { + target: "web", + database: {}, + checks: [ + { sha: "current", passed: true, label: "Feature assertions passed" }, + ], + capture: { + sha: "old", + status: "captured", + upload: { link: "https://cap.so/s/demo", folderVerified: true }, + }, + }; + assert.throws( + () => renderPrBody(session, "new", "Explanation", "Backend only"), + /passing checks/, + ); + assert.throws( + () => renderPrBody(session, "current", "Explanation"), + /recording is missing/, + ); + assert.match( + renderPrBody( + session, + "current", + "Explanation", + "Changes only build tooling", + ), + /Changes only build tooling/, + ); +}); + +test("a failed repeat check supersedes a previous success at the same commit", () => { + const session = { + target: "web", + database: {}, + checks: [ + { sha: "current", passed: true, label: "tests" }, + { sha: "current", passed: false, label: "tests" }, + ], + }; + assert.throws( + () => renderPrBody(session, "current", "Explanation", "Infrastructure"), + /latest check attempt failed/, + ); +}); + +test("PR summaries do not copy private command arguments or resource identifiers", () => { + const session = { + target: "web", + database: { id: "private-resource-id", name: "private-resource-name" }, + checks: [ + { + sha: "current", + passed: true, + label: + "node /Users/synthetic-user/check.mjs --token synthetic-secret tester@example.invalid", + }, + ], + }; + const body = renderPrBody( + session, + "current", + "Build tooling", + "Infrastructure only", + ); + assert.match(body, /1 scoped verification checks passed/); + for (const value of [ + "/Users/", + "synthetic-secret", + "tester@example.invalid", + "private-resource-id", + "private-resource-name", + ]) + assert.equal(body.includes(value), false); +}); + +test("a running server allows checks in the same session", async (t) => { + const ctx = fixture(t); + const session = await createSession(ctx, { + name: "server", + target: "web", + base: "main", + }); + const serving = main([ + "serve", + "--repo", + ctx.root, + "--session", + session.id, + "--", + process.execPath, + "-e", + "setTimeout(() => {}, 500)", + ]); + await delay(100); + await main([ + "check", + "--repo", + ctx.root, + "--session", + session.id, + "--", + process.execPath, + "-e", + "process.exit(0)", + ]); + assert.equal( + existsSync(join(dirname(sessionPath(ctx, session.id)), "server.lock")), + true, + ); + await serving; +}); + +test("terminating a launcher stops its descendants before releasing the runtime lease", async (t) => { + const ctx = fixture(t); + const session = await createSession(ctx, { + name: "process", + target: "desktop", + base: "main", + }); + const pidFile = join(ctx.state, "descendant.pid"); + const source = `const c = require('child_process').spawn(process.execPath, ['-e', 'setInterval(()=>{},1000)'],{stdio:'ignore'}); require('fs').writeFileSync(${JSON.stringify(pidFile)},String(c.pid)); setInterval(()=>{},1000);`; + const wrapper = spawn( + process.execPath, + [ + fileURLToPath(new URL("./building.mjs", import.meta.url)), + "serve", + "--repo", + ctx.root, + "--session", + session.id, + "--native", + "--", + process.execPath, + "-e", + source, + ], + { stdio: "ignore" }, + ); + const exited = new Promise((resolveExit) => + wrapper.once("exit", resolveExit), + ); + for (let attempt = 0; attempt < 100 && !existsSync(pidFile); attempt++) + await delay(20); + assert.ok(existsSync(pidFile)); + const descendant = Number(readFileSync(pidFile, "utf8")); + t.after(() => { + try { + process.kill(descendant, "SIGKILL"); + } catch {} + }); + wrapper.kill("SIGTERM"); + await exited; + assert.throws(() => process.kill(descendant, 0), { code: "ESRCH" }); + assert.equal(existsSync(join(ctx.state, "native-runtime.lock")), false); +}); + +test("capture verification rejects missing, nonfinite, and empty video duration", () => { + const streams = [{ codec_type: "video", width: 1920, height: 1080 }]; + for (const duration of [undefined, "N/A", "Infinity", "", "0", "-1"]) { + assert.throws( + () => validateCaptureProbe({ streams, format: { duration } }), + /valid 1080p video/, + ); + } + assert.doesNotThrow(() => + validateCaptureProbe({ streams, format: { duration: "20.5" } }), + ); + assert.throws( + () => validateCaptureProbe({ streams: [], format: { duration: "20.5" } }), + /valid 1080p video/, + ); +}); + +test("failed export preserves the raw project before deleting the sandbox", async (t) => { + const ctx = fixture(t); + writeFileSync( + join(ctx.root, "demo.json"), + JSON.stringify({ + version: 1, + platform: "linux", + setup: [], + database: false, + walkthrough: "true", + }), + ); + git(ctx.root, "add", "demo.json"); + git(ctx.root, "commit", "-m", "test: demo recipe"); + const session = await createSession(ctx, { + name: "capture", + target: "web", + base: "main", + }); + writeEnvironment(ctx, session, { + DATABASE_URL: "omit-database-credential", + NEXTAUTH_SECRET: "omit-auth-credential", + CAP_AWS_SECRET_KEY: "omit-storage-credential", + }); + let downloaded = false; + let deleted = false; + const sandbox = { + id: "fixture-sandbox", + labels: { + "cap.building.session": session.id, + "cap.building.sha": git(session.worktree, "rev-parse", "HEAD"), + }, + process: { + executeCommand: async (command) => { + if (command.startsWith("cap targets")) + return { + exitCode: 0, + result: JSON.stringify({ screens: [{ id: 0 }] }), + }; + if (command.startsWith("cap record start")) + return { + exitCode: 0, + result: JSON.stringify({ recordingId: "fixture-recording" }), + }; + if (command.startsWith("cap record stop")) + return { + exitCode: 0, + result: JSON.stringify({ recordingMetaExists: true }), + }; + if (command.startsWith("cap export")) + return { exitCode: 3, result: "export failed" }; + return { exitCode: 0, result: "{}" }; + }, + }, + fs: { + uploadFile: async () => {}, + downloadFile: async (_remote, local) => { + writeFileSync(local, "project"); + downloaded = true; + }, + }, + computerUse: { + start: async () => {}, + display: { + getInfo: async () => ({ + primary_display: { width: 1920, height: 1080 }, + }), + }, + }, + }; + const daytona = { + create: async ({ envVars }) => { + assert.equal(envVars.PORT, String(session.ports.web)); + assert.equal(envVars.WEB_URL, `http://127.0.0.1:${session.ports.web}`); + for (const key of [ + "DATABASE_URL", + "NEXTAUTH_SECRET", + "CAP_AWS_SECRET_KEY", + "HOME", + "PATH", + ]) + assert.equal(envVars[key], undefined); + return sandbox; + }, + get: async () => sandbox, + delete: async () => { + assert.ok(downloaded); + deleted = true; + }, + }; + await assert.rejects( + capture(ctx, session, join(session.worktree, "demo.json"), daytona), + /Sandbox command failed/, + ); + assert.equal(deleted, true); + assert.equal(readFileSync(session.capture.failedProject, "utf8"), "project"); + assert.equal(session.capture.status, "failed"); + session.capture = undefined; + sandbox.fs.downloadFile = async () => { + throw new Error("network unavailable"); + }; + deleted = false; + await assert.rejects( + capture(ctx, session, join(session.worktree, "demo.json"), daytona), + /Sandbox command failed/, + ); + assert.equal(deleted, false); + assert.equal(session.capture.preserveSandbox, true); +}); + +test("credentialed capture requires current trust evidence and limits credentials to app startup", async (t) => { + const ctx = fixture(t); + writeFileSync( + join(ctx.root, "demo.json"), + JSON.stringify({ + version: 1, + platform: "linux", + setup: ["setup-check"], + start: "start-feature", + ready: "ready-check", + walkthrough: "walkthrough-check", + }), + ); + git(ctx.root, "add", "demo.json"); + git(ctx.root, "commit", "-m", "test: capture recipe"); + let session = await createSession(ctx, { + name: "trusted-capture", + target: "web", + base: "main", + }); + const recipePath = join(session.worktree, "demo.json"); + const sha = assertClean(ctx, session); + session.database = { + ...session.database, + id: "owned", + host: "db.test", + username: "fixture", + }; + const credentials = { + DATABASE_URL: "mysql://fixture:synthetic@db.test/cap-production", + NEXTAUTH_SECRET: "fixture-auth", + CAP_AWS_SECRET_KEY: "fixture-storage", + }; + writeEnvironment(ctx, session, credentials); + saveSession(ctx, session); + const evidence = { + sha, + recipeHash: readRecipe(session, recipePath).hash, + environmentHash: readEnvironmentSnapshot(ctx, session).fingerprint, + sourceTrusted: true, + commandsReviewed: true, + credentialScopesReviewed: true, + reviewedAt: new Date().toISOString(), + }; + await assert.rejects(capture(ctx, session, recipePath, {}), /trusted review/); + assert.throws( + () => + recordCaptureReview(ctx, session, recipePath, { + ...evidence, + sourceTrusted: false, + }), + /Expected trusted source/, + ); + const evidencePath = join(ctx.state, "review.json"); + writeFileSync(evidencePath, JSON.stringify(evidence)); + await main([ + "capture-review", + "--repo", + ctx.root, + "--session", + session.id, + "--recipe", + recipePath, + "--evidence", + evidencePath, + ]); + session = readSession(ctx, session.id); + session.captureReview.recipeHash = "stale"; + await assert.rejects(capture(ctx, session, recipePath, {}), /trusted review/); + session.captureReview.recipeHash = evidence.recipeHash; + writeEnvironment(ctx, session, { + ...credentials, + CAP_AWS_SECRET_KEY: "unreviewed-storage", + }); + await assert.rejects(capture(ctx, session, recipePath, {}), /trusted review/); + assert.throws( + () => recordCaptureReview(ctx, session, recipePath, evidence), + /Expected trusted source/, + ); + assert.throws( + () => + executionEnvironment(ctx, session, { + expectedHash: evidence.environmentHash, + }), + /Credential configuration changed/, + ); + writeEnvironment(ctx, session, credentials); + const bin = join(ctx.state, "bin"); + mkdirSync(bin); + writeFileSync( + join(bin, "pscale"), + `#!${process.execPath}\nconsole.log(${JSON.stringify(JSON.stringify({ id: "owned", name: session.database.name, parent_branch: "main", production: false, ready: true }))});\n`, + { mode: 0o755 }, + ); + const previousPath = process.env.PATH; + process.env.PATH = `${bin}:${previousPath}`; + const phases = new Set(); + let created = 0; + let deleted = false; + const sandbox = { + id: "fixture-sandbox", + labels: { "cap.building.session": session.id, "cap.building.sha": sha }, + process: { + executeCommand: async (command, _cwd, env = {}) => { + if (command.startsWith("nohup")) { + assert.ok(command.includes("start-feature")); + for (const [key, value] of Object.entries(credentials)) { + assert.equal(env[key], value); + assert.equal(command.includes(value), false); + } + phases.add("start"); + } else { + for (const key of Object.keys(credentials)) + assert.equal(env[key], undefined); + phases.add(command); + } + if (command.startsWith("cap targets")) + return { + exitCode: 0, + result: JSON.stringify({ screens: [{ id: 0 }] }), + }; + if (command.startsWith("cap record start")) + return { + exitCode: 0, + result: JSON.stringify({ recordingId: "fixture-recording" }), + }; + if (command.startsWith("cap record stop")) + return { + exitCode: 0, + result: JSON.stringify({ recordingMetaExists: true }), + }; + if (command.startsWith("cap export")) + return { exitCode: 3, result: "export failed" }; + return { exitCode: 0, result: "{}" }; + }, + }, + fs: { + uploadFile: async () => {}, + downloadFile: async (_remote, local) => writeFileSync(local, "project"), + }, + computerUse: { + start: async () => {}, + display: { + getInfo: async () => ({ + primary_display: { width: 1920, height: 1080 }, + }), + }, + }, + }; + const daytona = { + create: async ({ envVars }) => { + created++; + for (const key of Object.keys(credentials)) + assert.equal(envVars[key], undefined); + return sandbox; + }, + get: async () => sandbox, + delete: async () => { + deleted = true; + }, + }; + try { + await assert.rejects( + capture(ctx, session, recipePath, daytona), + /Sandbox command failed/, + ); + for (const phase of [ + "setup-check", + "start", + "ready-check", + "walkthrough-check", + ]) + assert.ok(phases.has(phase)); + assert.equal(created, 1); + assert.equal(deleted, true); + git( + session.worktree, + "commit", + "--allow-empty", + "-m", + "test: changed source commit", + ); + await assert.rejects( + capture(ctx, session, recipePath, daytona), + /trusted review/, + ); + assert.equal(created, 1); + } finally { + process.env.PATH = previousPath; + } +}); + +test("foreign sandboxes cannot be deleted", () => { + const session = { id: "owned", capture: { sha: "abc" } }; + assert.throws( + () => + assertOwnedSandbox(session, { + labels: { "cap.building.session": "other", "cap.building.sha": "abc" }, + }), + /ownership/, + ); + assert.doesNotThrow(() => + assertOwnedSandbox(session, { + labels: { "cap.building.session": "owned", "cap.building.sha": "abc" }, + }), + ); +}); + +test("Cap sharing uses the personal PR folder and verifies placement without duplicate uploads", async (t) => { + const ctx = fixture(t); + const session = await createSession(ctx, { + name: "sharing", + target: "web", + base: "main", + }); + const sha = git(session.worktree, "rev-parse", "HEAD"); + const file = join(ctx.state, "fixture.mp4"); + writeFileSync(file, "fixture-video"); + session.pr = { sha, number: 42 }; + session.capture = { + sha, + status: "captured", + file, + fileHash: createHash("sha256").update(readFileSync(file)).digest("hex"), + }; + saveSession(ctx, session); + const bin = join(ctx.state, "bin"); + mkdirSync(bin); + const statePath = join(ctx.state, "cap-fixture.json"); + writeFileSync( + statePath, + JSON.stringify({ folder: false, uploads: 0, incorrectOrganization: true }), + ); + writeFileSync( + join(bin, "cap"), + `#!${process.execPath} +const fs = require("node:fs"), assert = require("node:assert/strict"); +const path = ${JSON.stringify(statePath)}, state = JSON.parse(fs.readFileSync(path)), args = process.argv.slice(2); +let result; +if (args[0] === "account") result = {defaultOrganizationId: "organization"}; +else if (args[2] === "list") { + assert.equal(args.includes("--space"), false); + assert.equal(args.includes("--root"), true); + result = {folders: state.folder ? [{id: "folder", name: "PR's", organizationId: "organization", spaceId: null, parentId: null}] : []}; +} else if (args[2] === "create") { + assert.equal(args.includes("--space"), false); + state.folder = true; + result = {action: "created", resource: {type: "folder", id: "folder"}}; +} else if (args[0] === "upload") { + state.uploads++; + result = {id: "video", link: "https://cap.so/s/video"}; +} else if (args[1] === "move") { + assert.equal(args[args.indexOf("--container") + 1], "personal"); + assert.equal(args[args.indexOf("--organization") + 1], "organization"); + state.moved = args[args.indexOf("--folder") + 1]; + result = {action: "updated", resource: {id: "video"}}; +} else if (args[1] === "get") { + result = {id: "video", folderId: state.moved, organizationId: state.incorrectOrganization ? "other" : "organization"}; +} else process.exit(99); +fs.writeFileSync(path, JSON.stringify(state)); +console.log(JSON.stringify(result)); +`, + { mode: 0o755 }, + ); + const previousPath = process.env.PATH; + process.env.PATH = `${bin}:${previousPath}`; + try { + await assert.rejects( + shareCapture(ctx, session), + /placement could not be verified/, + ); + assert.notEqual(session.capture.upload.folderVerified, true); + const state = JSON.parse(readFileSync(statePath, "utf8")); + assert.equal(state.uploads, 1); + state.incorrectOrganization = false; + writeFileSync(statePath, JSON.stringify(state)); + const first = await shareCapture(ctx, session); + assert.equal(first.folderId, "folder"); + assert.equal(session.capture.upload.folderVerified, true); + const second = await shareCapture(ctx, session); + assert.equal(second.link, first.link); + assert.equal(JSON.parse(readFileSync(statePath, "utf8")).uploads, 1); + } finally { + process.env.PATH = previousPath; + } +}); + +test("fixture identities are deterministic and session-specific", () => { + assert.deepEqual(fixtureIds("one"), fixtureIds("one")); + assert.notDeepEqual(fixtureIds("one"), fixtureIds("two")); + assert.ok( + Object.values(fixtureIds("one")).every((value) => value.length === 15), + ); +}); + +test("shell arguments preserve metacharacters without executing them", () => { + const input = "a'$(echo unexpected)`uname` b"; + const output = execFileSync("sh", ["-c", `printf %s ${shellQuote(input)}`], { + encoding: "utf8", + }); + assert.equal(output, input); +}); diff --git a/.agents/skills/building/scripts/database.mjs b/.agents/skills/building/scripts/database.mjs new file mode 100644 index 00000000000..85cf687de2d --- /dev/null +++ b/.agents/skills/building/scripts/database.mjs @@ -0,0 +1,160 @@ +import { createHash } from "node:crypto"; +import { existsSync } from "node:fs"; +import { createRequire } from "node:module"; +import { join } from "node:path"; +import { + assertWorktree, + atomicJson, + attachDatabase, + executionEnvironment, + inspectDatabase, + pscale, + saveSession, + sessionPath, +} from "./core.mjs"; + +export function createDatabase(ctx, session) { + assertWorktree(ctx, session); + const branches = pscale(session, [ + "branch", + "list", + session.database.database, + ]); + const existing = branches.find( + (branch) => branch.name === session.database.name, + ); + if (existing) { + if (!session.database.createAttempted && !session.database.id) { + throw new Error("An unowned database branch already has this name"); + } + return attachDatabase(ctx, session); + } + if (session.database.id) + throw new Error( + "Owned database branch disappeared; inspect before recreating", + ); + session.database.createAttempted = true; + saveSession(ctx, session); + pscale(session, [ + "branch", + "create", + session.database.database, + session.database.name, + "--from", + session.database.parent, + "--wait", + ]); + return attachDatabase(ctx, session); +} + +export function fixtureIds(id) { + const key = (name) => + createHash("sha256").update(`${id}:${name}`).digest("hex").slice(0, 15); + return { + user: key("user"), + organization: key("organization"), + member: key("member"), + folder: key("folder"), + }; +} + +export async function seedDatabase(ctx, session) { + assertWorktree(ctx, session); + inspectDatabase(session); + const env = executionEnvironment(ctx, session); + const url = new URL(env.DATABASE_URL); + const { createConnection } = await import("mysql2/promise"); + const connection = await createConnection({ + host: url.hostname, + user: decodeURIComponent(url.username), + password: decodeURIComponent(url.password), + database: url.pathname.slice(1), + ssl: { rejectUnauthorized: true }, + }); + const ids = fixtureIds(session.id); + const email = `${session.id}@example.invalid`; + try { + await connection.beginTransaction(); + await connection.execute( + "INSERT INTO users (id, name, email, emailVerified, activeOrganizationId, defaultOrgId, onboarding_completed_at, stripeSubscriptionStatus) VALUES (?, ?, ?, NOW(), ?, ?, NOW(), ?) ON DUPLICATE KEY UPDATE id = id", + [ + ids.user, + "Building Tester", + email, + ids.organization, + ids.organization, + "active", + ], + ); + await connection.execute( + "INSERT INTO organizations (id, name, ownerId) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE id = id", + [ids.organization, "Building Demo", ids.user], + ); + await connection.execute( + "INSERT INTO organization_members (id, userId, organizationId, role, hasProSeat) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE id = id", + [ids.member, ids.user, ids.organization, "owner", true], + ); + await connection.execute( + "INSERT INTO folders (id, name, organizationId, createdById) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE id = id", + [ids.folder, "Demo recordings", ids.organization, ids.user], + ); + await connection.commit(); + } catch (error) { + await connection.rollback(); + throw error; + } finally { + await connection.end(); + } + session.fixtures = { ...ids, email }; + saveSession(ctx, session); + return session.fixtures; +} + +export async function loginFixture(ctx, session) { + inspectDatabase(session); + if (!session.fixtures) throw new Error("Seed the session database first"); + const env = executionEnvironment(ctx, session); + const require = createRequire( + join(session.worktree, "apps/web/package.json"), + ); + const { encode } = require("next-auth/jwt"); + const token = await encode({ + secret: env.NEXTAUTH_SECRET, + maxAge: 3600, + token: { + id: session.fixtures.user, + email: session.fixtures.email, + name: "Building Tester", + sessionVersion: 0, + }, + }); + const path = join(sessionPath(ctx, session.id), "..", "browser-state.json"); + atomicJson(path, { + cookies: [ + { + name: "next-auth.session-token", + value: token, + domain: "127.0.0.1", + path: "/", + httpOnly: true, + secure: false, + sameSite: "Lax", + expires: Math.floor(Date.now() / 1000) + 3600, + }, + ], + origins: [], + }); + return { browserState: path }; +} + +export function assertNoLocalSecrets(worktree) { + for (const file of [ + ".env", + ".env.local", + ".env.development", + ".env.development.local", + ]) { + if (existsSync(join(worktree, file))) + throw new Error(`Unmanaged environment file: ${file}`); + } +} diff --git a/.agents/skills/building/scripts/lifecycle.test.mjs b/.agents/skills/building/scripts/lifecycle.test.mjs new file mode 100644 index 00000000000..96463b35bf5 --- /dev/null +++ b/.agents/skills/building/scripts/lifecycle.test.mjs @@ -0,0 +1,286 @@ +import assert from "node:assert/strict"; +import { execFileSync, spawn } from "node:child_process"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { setTimeout as delay } from "node:timers/promises"; +import { fileURLToPath } from "node:url"; +import { main } from "./building.mjs"; +import { + context, + createSession, + environmentPath, + git, + readSession, + saveSession, + withSessionStartup, + writeEnvironment, +} from "./core.mjs"; +import { previewProfile } from "./preview.mjs"; + +async function fixture(t) { + const directory = mkdtempSync(join(tmpdir(), "cap-building-lifecycle-")); + const repo = join(directory, "repo"); + mkdirSync(repo); + execFileSync("git", ["init", "-b", "main", repo], { stdio: "ignore" }); + git(repo, "config", "user.email", "test@example.invalid"); + git(repo, "config", "user.name", "Building Test"); + writeFileSync(join(repo, "file"), "base"); + git(repo, "add", "file"); + git(repo, "commit", "-m", "test: base"); + const ctx = context(repo); + const session = await createSession(ctx, { + name: "finish", + target: "web", + base: "main", + }); + const sha = git(session.worktree, "rev-parse", "HEAD"); + const provider = join(directory, "provider.json"); + const state = { + sha, + state: "OPEN", + branches: [ + { + id: "owned", + name: session.database.name, + parent_branch: "main", + production: false, + ready: true, + }, + ], + deleted: [], + }; + writeFileSync(provider, JSON.stringify(state)); + const bin = join(directory, "bin"); + mkdirSync(bin); + writeFileSync( + join(bin, "gh"), + `#!${process.execPath}\nconst fs = require("node:fs"); const state = JSON.parse(fs.readFileSync(${JSON.stringify(provider)})); console.log(JSON.stringify({state: state.state, headRefOid: state.sha}));\n`, + { mode: 0o755 }, + ); + writeFileSync( + join(bin, "pscale"), + `#!${process.execPath}\nconst fs = require("node:fs"), path = ${JSON.stringify(provider)}, state = JSON.parse(fs.readFileSync(path)), args = process.argv.slice(2); if (args[1] === "list") console.log(JSON.stringify(state.branches)); else if (args[1] === "show") console.log(JSON.stringify(state.branches.find(branch => branch.name === args[3]))); else if (args[1] === "delete") {state.deleted.push(args[3]); state.branches = state.branches.filter(branch => branch.name !== args[3]); fs.writeFileSync(path, JSON.stringify(state)); console.log("{}");} else process.exit(99);\n`, + { mode: 0o755 }, + ); + const previousPath = process.env.PATH; + process.env.PATH = `${bin}:${previousPath}`; + t.after(() => { + process.env.PATH = previousPath; + rmSync(directory, { recursive: true, force: true }); + }); + session.database.id = "owned"; + session.database.status = "ready"; + session.pr = { url: "https://github.com/fixture/repo/pull/1" }; + session.checks = [{ sha, label: "tests", passed: true }]; + saveSession(ctx, session); + return { ctx, session, provider, state, sha }; +} + +async function serve(ctx, session) { + const ready = join(ctx.state, `${session.id}-ready`); + const child = spawn( + process.execPath, + [ + fileURLToPath(new URL("./building.mjs", import.meta.url)), + "serve", + "--repo", + ctx.root, + "--session", + session.id, + "--", + process.execPath, + "-e", + `require("node:fs").writeFileSync(${JSON.stringify(ready)}, "ready"); setInterval(() => {}, 1000);`, + ], + { stdio: "ignore" }, + ); + const exited = new Promise((resolveExit) => child.once("exit", resolveExit)); + for (let attempt = 0; attempt < 100 && !existsSync(ready); attempt++) + await delay(20); + assert.ok(existsSync(ready)); + return { child, exited }; +} + +test("finish cleans only its published resources and resume restores the retained feature", async (t) => { + const { ctx, session, provider, sha } = await fixture(t); + const other = await createSession(ctx, { + name: "other", + target: "web", + base: "main", + }); + writeFileSync(join(ctx.root, "file"), "staged"); + git(ctx.root, "add", "file"); + writeFileSync(join(ctx.root, "file"), "unstaged"); + writeFileSync(join(other.worktree, "file"), "other feature"); + const index = readFileSync(join(ctx.common, "index")); + const status = git(ctx.root, "status", "--porcelain"); + const a = await serve(ctx, session); + const b = await serve(ctx, other); + try { + const result = await main([ + "finish", + "--repo", + ctx.root, + "--session", + session.id, + "--no-visual", + "Build tooling only", + ]); + await a.exited; + assert.equal(result.status, "published"); + assert.equal(existsSync(session.worktree), false); + assert.doesNotThrow(() => process.kill(b.child.pid, 0)); + assert.equal( + readFileSync(join(other.worktree, "file"), "utf8"), + "other feature", + ); + assert.deepEqual(readFileSync(join(ctx.common, "index")), index); + assert.equal(git(ctx.root, "status", "--porcelain"), status); + assert.deepEqual(JSON.parse(readFileSync(provider, "utf8")).deleted, [ + session.database.name, + ]); + writeEnvironment(ctx, session, { DATABASE_URL: "obsolete" }); + const resumed = await main([ + "resume", + "--repo", + ctx.root, + "--session", + session.id, + ]); + assert.equal(git(resumed.worktree, "rev-parse", "HEAD"), sha); + assert.equal(resumed.database.id, undefined); + assert.equal(resumed.database.status, "planned"); + assert.equal(existsSync(environmentPath(ctx, session)), false); + assert.equal(resumed.noVisual, undefined); + assert.notEqual(previewProfile(ctx, session), previewProfile(ctx, other)); + } finally { + a.child.kill("SIGTERM"); + b.child.kill("SIGTERM"); + await Promise.all([a.exited, b.exited]); + } +}); + +test("resume recovers after worktree creation but before the final checkpoint", async (t) => { + const { ctx, session } = await fixture(t); + await main([ + "finish", + "--repo", + ctx.root, + "--session", + session.id, + "--no-visual", + "Build tooling only", + ]); + const published = readSession(ctx, session.id); + const bin = join(ctx.state, "interruption-bin"); + mkdirSync(bin); + const realGit = execFileSync("which", ["git"], { encoding: "utf8" }).trim(); + writeFileSync( + join(bin, "git"), + `#!${process.execPath}\nconst args = process.argv.slice(2); const output = require("node:child_process").execFileSync(${JSON.stringify(realGit)}, args); process.stdout.write(output); if (args.includes("add") && args.includes("worktree")) process.exit(3);\n`, + { mode: 0o755 }, + ); + const previousPath = process.env.PATH; + process.env.PATH = `${bin}:${previousPath}`; + try { + await assert.rejects( + main(["resume", "--repo", ctx.root, "--session", session.id]), + /git failed/, + ); + } finally { + process.env.PATH = previousPath; + } + assert.equal(readSession(ctx, session.id).status, "resuming"); + assert.equal(existsSync(published.worktree), true); + const recovered = await main([ + "resume", + "--repo", + ctx.root, + "--session", + session.id, + ]); + assert.equal(recovered.status, "active"); + assert.equal(recovered.databaseHistory.length, 1); +}); + +test("a prior nonvisual exemption cannot authorize cleanup of a later commit", async (t) => { + const { ctx, session, provider, state, sha } = await fixture(t); + session.noVisual = { sha, reason: "Build tooling only" }; + writeFileSync(join(session.worktree, "feature"), "new visual behavior"); + git(session.worktree, "add", "feature"); + git(session.worktree, "commit", "-m", "feat: visual feature"); + state.sha = git(session.worktree, "rev-parse", "HEAD"); + writeFileSync(provider, JSON.stringify(state)); + session.checks.push({ sha: state.sha, label: "tests", passed: true }); + saveSession(ctx, session); + await assert.rejects( + main(["finish", "--repo", ctx.root, "--session", session.id]), + /recording is missing/, + ); + assert.equal(existsSync(session.worktree), true); + assert.equal(JSON.parse(readFileSync(provider, "utf8")).deleted.length, 0); +}); + +test("a nonvisual exemption cannot bypass verification of an uploaded recording", async (t) => { + const { ctx, session, provider, sha } = await fixture(t); + session.capture = { + sha, + status: "captured", + sandboxDeleted: true, + upload: { link: "https://cap.so/s/fixture", folderVerified: false }, + }; + const args = [ + "finish", + "--repo", + ctx.root, + "--session", + session.id, + "--no-visual", + "Build tooling only", + ]; + saveSession(ctx, session); + await assert.rejects(main(args), /recording is missing/); + session.capture.upload.folderVerified = true; + saveSession(ctx, session); + await assert.rejects(main(args), /Verify playback and reviewer access/); + assert.equal(existsSync(session.worktree), true); + assert.equal(JSON.parse(readFileSync(provider, "utf8")).deleted.length, 0); + assert.equal(readSession(ctx, session.id).status, "active"); + session.capture.upload.playbackVerified = true; + saveSession(ctx, session); + assert.equal((await main(args)).status, "published"); + assert.equal(readSession(ctx, session.id).noVisual, undefined); +}); + +test("cleanup checkpoints prevent process startup and support closing a published PR", async (t) => { + const { ctx, session, provider, state } = await fixture(t); + await main([ + "finish", + "--repo", + ctx.root, + "--session", + session.id, + "--no-visual", + "Build tooling only", + ]); + await assert.rejects( + withSessionStartup(ctx, session, () => assert.fail("Must not start")), + /active session/, + ); + state.state = "MERGED"; + state.branches = []; + writeFileSync(provider, JSON.stringify(state)); + assert.equal( + (await main(["close", "--repo", ctx.root, "--session", session.id])).status, + "closed", + ); +}); diff --git a/.agents/skills/building/scripts/preview.mjs b/.agents/skills/building/scripts/preview.mjs new file mode 100644 index 00000000000..d2def21b0ff --- /dev/null +++ b/.agents/skills/building/scripts/preview.mjs @@ -0,0 +1,64 @@ +import { existsSync, readFileSync, rmSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { + assertWorktree, + lock, + recordProcess, + sessionPath, + withSessionStartup, +} from "./core.mjs"; + +export function previewProfile(ctx, session) { + return join(dirname(sessionPath(ctx, session.id)), "browser-profile"); +} + +export async function preview(ctx, session) { + assertWorktree(ctx, session); + const directory = dirname(sessionPath(ctx, session.id)); + const release = lock(join(directory, "preview.lock")); + let browser; + let close; + let processPath; + try { + const { chromium } = await import("playwright-core"); + let closed; + await withSessionStartup(ctx, session, async () => { + browser = await chromium.launchPersistentContext( + previewProfile(ctx, session), + { + channel: "chrome", + headless: false, + viewport: { width: 1440, height: 900 }, + }, + ); + closed = new Promise((resolveClose) => + browser.once("close", resolveClose), + ); + close = () => { + browser.close().catch(() => {}); + }; + process.once("SIGINT", close); + process.once("SIGTERM", close); + processPath = recordProcess(ctx, session, { kind: "preview" }); + }); + const storagePath = join(directory, "browser-state.json"); + if (existsSync(storagePath)) { + const state = JSON.parse(readFileSync(storagePath, "utf8")); + await browser.addCookies(state.cookies); + } + const page = browser.pages()[0] ?? (await browser.newPage()); + await page.goto(`http://127.0.0.1:${session.ports.web}`, { + waitUntil: "domcontentloaded", + timeout: 60000, + }); + await closed; + } finally { + if (close) { + process.removeListener("SIGINT", close); + process.removeListener("SIGTERM", close); + } + await browser?.close(); + if (processPath) rmSync(processPath, { force: true }); + release(); + } +} diff --git a/.config/hakari.toml b/.config/hakari.toml index 0dea44fe3f0..3b2ff13795f 100644 --- a/.config/hakari.toml +++ b/.config/hakari.toml @@ -16,12 +16,83 @@ resolver = "2" # Add triples corresponding to platforms commonly used by developers here. # https://doc.rust-lang.org/rustc/platform-support.html +# Every target desktop-release.yml ships. Leaving a target out here leaves its +# platform-specific deps (windows-sys, libc, ...) un-unified, so the sidecar +# `cargo build -p cap` and Tauri's `cargo build -p cap-desktop` resolve +# different feature sets and recompile ~500 shared crates in the same job. platforms = [ - # "x86_64-unknown-linux-gnu", - # "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "x86_64-apple-darwin", "aarch64-apple-darwin", "x86_64-pc-windows-msvc", ] # Write out exact versions rather than a semver range. (Defaults to false.) # exact-versions = true + +# --- cap release-build tuning --- +# cap-muxer is ~55 crates; letting it depend on the hack would drag the whole +# unified tree into every `cargo build -p cap-muxer`. Any drift it causes is +# bounded by its own tiny graph. +[traversal-excludes] +workspace-members = ["cap-muxer", "cap-muxer-protocol"] + +# hakari unifies dev-dependencies as well, so left alone it pins tauri with its +# `test` feature and tokio `test-util` into every release binary and pulls the +# Tauri/WebKit stack into the standalone GPUI workspace. Only crates whose +# feature drift actually cascades through the sidecar and desktop builds stay +# in the hack; everything else keeps its natural per-binary resolution. +[final-excludes] +third-party = [ + { name = "softbuffer" }, + { name = "aho-corasick" }, + { name = "block2" }, + { name = "cookie" }, + { name = "core-foundation" }, + { name = "core-foundation-sys" }, + { name = "coreaudio-rs" }, + { name = "coreaudio-sys" }, + { name = "crossbeam-utils" }, + { name = "errno" }, + { name = "event-listener" }, + { name = "event-listener-strategy" }, + { name = "fastrand" }, + { name = "futures-lite" }, + { name = "getrandom" }, + { name = "half" }, + { name = "hashbrown" }, + { name = "hyper" }, + { name = "hyper-util" }, + { name = "image" }, + { name = "indexmap" }, + { name = "itertools" }, + { name = "lru" }, + { name = "num-traits" }, + { name = "objc" }, + { name = "objc2-core-image" }, + { name = "objc2-foundation" }, + { name = "objc2-quartz-core" }, + { name = "once_cell" }, + { name = "phf_shared" }, + { name = "serde_json" }, + { name = "serde_spanned" }, + { name = "smallvec" }, + { name = "swift-rs" }, + { name = "syn" }, + { name = "tauri" }, + { name = "tauri-build" }, + { name = "tokio" }, + { name = "tokio-util" }, + { name = "toml" }, + { name = "tower" }, + { name = "uuid" }, + { name = "wayland-protocols" }, + { name = "wayland-sys" }, + { name = "winapi" }, + { name = "windows-numerics" }, + { name = "windows-strings" }, + { name = "x11" }, + { name = "zbus" }, + { name = "zbus_macros" }, + { name = "zvariant" }, +] diff --git a/.github/actions/setup-rust-cache/action.yml b/.github/actions/setup-rust-cache/action.yml index bb70b395c8a..7e8c62ac96e 100644 --- a/.github/actions/setup-rust-cache/action.yml +++ b/.github/actions/setup-rust-cache/action.yml @@ -8,13 +8,27 @@ inputs: description: "Whether to save the cache" required: false default: "false" + cache-on-failure: + description: "Save the cache even when the job fails (useful when a job fails after the build, e.g. at signing)" + required: false + default: "false" + prefix-key: + description: "Cache key prefix. Use a distinct prefix per build flavour (CI debug vs release) so restore-keys never pull an irrelevant cache" + required: false + default: "v1-rust" + workspaces: + description: "Newline-separated `path -> target` pairs of Cargo workspaces to cache (rust-cache format)" + required: false + default: ". -> target" runs: using: "composite" steps: - name: Cache Rust Dependencies uses: swatinem/rust-cache@v2 with: - prefix-key: v1-rust + prefix-key: ${{ inputs.prefix-key }} cache-bin: "false" save-if: ${{ inputs.save-cache }} + cache-on-failure: ${{ inputs.cache-on-failure }} shared-key: ${{ inputs.target }} + workspaces: ${{ inputs.workspaces }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4b31fb5220..d0d6e27d63e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,6 +87,9 @@ jobs: - run: bun run --cwd apps/web next typegen + - name: Check MCP card bundle + run: bun run --cwd apps/web mcp-card:check + - name: Test workspace tooling run: node --test scripts/bun-scripts.test.mjs scripts/check-tauri-plugin-versions.test.mjs @@ -96,6 +99,11 @@ jobs: bun test scripts/emails scripts/loops/profile.test.ts scripts/loops/lifecycle.test.ts scripts/loops/watchdog.test.ts bun run tsc --noEmit --strict --jsx react-jsx --allowImportingTsExtensions --target ESNext --module ESNext --moduleResolution bundler --resolveJsonModule --types bun --skipLibCheck scripts/loops/*.ts scripts/emails/*.ts emails/*.ts emails/marketing/*.ts + - name: Test render farm orchestration + run: | + bun test apps/render-farm + bun run --cwd apps/render-farm typecheck + - name: Test web React compatibility run: >- bun run --cwd apps/web test diff --git a/.github/workflows/database-profile-reliability.yml b/.github/workflows/database-profile-reliability.yml new file mode 100644 index 00000000000..26d2b2c4c60 --- /dev/null +++ b/.github/workflows/database-profile-reliability.yml @@ -0,0 +1,59 @@ +name: Database Profile Reliability + +on: + workflow_dispatch: + pull_request: + paths: + - "packages/database/**" + - "scripts/loops/**" + - "bun.lock" + - ".github/actions/setup-js/**" + - ".github/workflows/database-profile-reliability.yml" + push: + branches: [main] + paths: + - "packages/database/**" + - "scripts/loops/**" + - "bun.lock" + - ".github/actions/setup-js/**" + - ".github/workflows/database-profile-reliability.yml" + +permissions: + contents: read + +concurrency: + group: database-profile-reliability-${{ github.head_ref || github.ref_name }} + cancel-in-progress: true + +jobs: + profile-contract: + name: Profile, activation, and consent compatibility + runs-on: ubuntu-24.04 + timeout-minutes: 10 + services: + mysql: + image: mysql:8.4 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: "yes" + MYSQL_ROOT_HOST: "%" + MYSQL_DATABASE: loops_profile_tests + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping --host=127.0.0.1 --user=root" + --health-interval=5s + --health-timeout=3s + --health-retries=30 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-js + - name: Verify profile queries and lifecycle decisions without database writes + env: + LOOPS_ACTIVATION_READONLY_TEST_URL: mysql://root@127.0.0.1:3306/loops_profile_tests + run: >- + bun test + scripts/loops/activation.test.ts + scripts/loops/sources.test.ts + scripts/loops/profile.test.ts + scripts/loops/lifecycle.test.ts + scripts/loops/watchdog.test.ts diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index d1d7ea90fdb..6c5a62af553 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -226,28 +226,6 @@ jobs: if: ${{ hashFiles('scripts/build-macos-packages.test.mjs') != '' }} run: node --test scripts/build-macos-packages.test.mjs - # Nightly and promoted builds carry a version that differs from the - # committed Cargo.toml; Tauri reads the app version from package.version. - - name: Stamp build version - shell: bash - env: - BUILD_VERSION: ${{ inputs.version }} - run: | - node -e ' - const fs = require("fs"); - const path = "apps/desktop/src-tauri/Cargo.toml"; - const version = process.env.BUILD_VERSION; - let toml = fs.readFileSync(path, "utf8"); - const re = /^version = ".*"$/m; - if (!re.test(toml)) { - console.error("package.version line not found in " + path); - process.exit(1); - } - toml = toml.replace(re, "version = \"" + version + "\""); - fs.writeFileSync(path, toml); - console.log("Stamped " + path + " with " + version); - ' - - name: Install Linux desktop dependencies if: ${{ runner.os == 'Linux' }} uses: ./.github/actions/install-desktop-deps @@ -278,9 +256,44 @@ jobs: cargo -V rustup show active-toolchain + # Release builds get their own cache flavour (release-profile rlibs for + # both the root workspace and the standalone GPUI workspace). Save it + # even when the job fails later, because the Windows job most often dies + # at SignPath approval, long after the compile it would have cached. - uses: ./.github/actions/setup-rust-cache with: target: ${{ matrix.settings.target }} + prefix-key: v1-rust-release + save-cache: true + cache-on-failure: true + workspaces: | + . -> target + apps/desktop-gpui -> target + + # Nightly and promoted builds carry a version that differs from the + # committed Cargo.toml; Tauri reads the app version from package.version. + # Runs after the cache step so the stamped version never lands in the + # cache key's Cargo.toml hash (a fresh nightly version each night would + # otherwise write a fresh multi-GB cache entry per target per night). + - name: Stamp build version + shell: bash + env: + BUILD_VERSION: ${{ inputs.version }} + run: | + node -e ' + const fs = require("fs"); + const path = "apps/desktop/src-tauri/Cargo.toml"; + const version = process.env.BUILD_VERSION; + let toml = fs.readFileSync(path, "utf8"); + const re = /^version = ".*"$/m; + if (!re.test(toml)) { + console.error("package.version line not found in " + path); + process.exit(1); + } + toml = toml.replace(re, "version = \"" + version + "\""); + fs.writeFileSync(path, toml); + console.log("Stamped " + path + " with " + version); + ' - uses: ./.github/actions/setup-js @@ -315,11 +328,12 @@ jobs: fi ./scripts/build-desktop-binaries.sh ${{ matrix.settings.target }} - # The release profile uses fat LTO + full debuginfo, so the final link - # peaks well above the 16GB stock Linux runner and gets OOM-killed - # ("runner received a shutdown signal"). The runner already ships an - # active /swapfile (resizing it fails with "Text file busy"), so add a - # large swapfile on the roomy /mnt disk on top of the existing swap. + # The final link used to peak well above the 16GB stock Linux runner and + # get OOM-killed ("runner received a shutdown signal") under fat LTO + + # full debuginfo. The release profile is thin LTO + line tables now, but + # the headroom is cheap, so keep it. The runner already ships an active + # /swapfile (resizing it fails with "Text file busy"), so add a large + # swapfile on the roomy /mnt disk on top of the existing swap. # macOS runs on xlarge runners and is unaffected. - name: Increase swap space (Linux) if: ${{ runner.os == 'Linux' }} @@ -334,10 +348,10 @@ jobs: swapon --show free -h - # The same fat-LTO link exhausts commit on the 16GB Windows runner + # The same link used to exhaust commit on the 16GB Windows runner # ("rustc-LLVM ERROR: out of memory" while linking cap-desktop), so - # raise the pagefile on the roomy D: disk — the Windows analog of the - # Linux swapfile above. + # raise the pagefile on the roomy D: disk, the Windows analog of the + # Linux swapfile above. Kept for headroom after the move to thin LTO. - name: Increase pagefile size (Windows) if: ${{ runner.os == 'Windows' }} uses: al-cheb/configure-pagefile-action@a3b6ebd6b634da88790d9c58d4b37a7f4a7b8708 # v1.4 diff --git a/.github/workflows/sync-tests.yml b/.github/workflows/sync-tests.yml index b6f6731801d..cb63034c85b 100644 --- a/.github/workflows/sync-tests.yml +++ b/.github/workflows/sync-tests.yml @@ -23,9 +23,11 @@ on: - "crates/enc-mediafoundation/**" - "crates/timestamp/**" - "crates/rendering/**" + - "crates/render-farm/**" - "crates/editor/**" - "crates/export/**" - "crates/audio/**" + - "crates/rnnoise/**" - "crates/media-info/**" - "crates/project/**" - "scripts/recording-reliability.py" @@ -87,6 +89,9 @@ jobs: shell: pwsh run: Add-Content -Path $env:GITHUB_PATH -Value "${{ github.workspace }}\\target\\ffmpeg\\bin" + - name: RNNoise lifecycle regression tests + run: cargo test --locked -p cap-rnnoise --lib + - name: Recording metadata and harness regression tests shell: bash run: | @@ -153,6 +158,10 @@ jobs: cargo test --locked -p cap-editor --lib audio_output::tests:: cargo test --locked -p cap-editor --lib playback::tests:: + - name: Render farm engine + shell: bash + run: cargo test --locked -p cap-render-farm + # Real encoders + DASH muxer + remux/validation over full instant-mode # scenarios: pause/resume excision, stall-recovery bursts with # same-microsecond timestamps, segment assembly and A/V alignment. diff --git a/AGENTS.md b/AGENTS.md index d7e2ee4f213..3c2d080f28c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ These rules are enforced by CI (`cargo clippy -D warnings`, Biome). Fixing them ### Zero-tolerance rules - **Default to no code comments. Add a comment only after solving a bug or working through a complex issue, and only when it captures non-obvious context that a future investigator or reviewer genuinely needs** — e.g. why the fix looks the way it does, the upstream/platform bug being worked around, a non-obvious invariant or trade-off chosen after investigation, or a link to the PR/issue that explains the decision. Bad cases that remain banned: narrating what the code does, restating types, JSDoc that paraphrases parameter names, "TODO: refactor" or "this should be cleaner" notes, and any comment that just describes the change you are currently making. When in doubt, prefer better naming/types over a comment. Applies to every language: Rust, TS, JS, Python, shell, SQL, TOML, etc. - **Never hand-edit generated files**: `**/tauri.ts`, `apps/desktop/src-tauri/gen/**`, `packages/ui-solid/src/auto-imports.d.ts`, Drizzle migration SQL under `packages/database/migrations/`. These are regenerated (e.g. `tauri.ts` only on debug desktop runs) but stay committed because CI typecheck and fresh clones depend on them; commit binding changes alongside the Rust change that produced them. For database schema changes, run `bun run db:generate` and commit the generated SQL, snapshot, and journal changes alongside the schema change. Generating and committing these artifacts is required; modifying generated output by hand is prohibited. Note: `apps/desktop/src/utils/queries.ts` is hand-written, not generated — edit it normally. -- **Never start additional dev servers** (`bun run dev`, `bun run dev:web`, `bun run dev:desktop`, Docker services). Assume they are already running. +- **Never start additional dev servers** (`bun run dev`, `bun run dev:web`, `bun run dev:desktop`, Docker services). Assume they are already running. The `/building` workflow may start a session-owned web server in its isolated worktree on an allocated port through its environment wrapper, or services inside its owned sandbox. This exception does not permit restarting shared services or launching a native app against the user's existing profile. ### Post-edit checks (run before you say "done") - Prefer scoped, fast checks over full workspace gates. Do not run long full-repo checks by default. diff --git a/Cargo.lock b/Cargo.lock index 280a91700af..e56c17c25d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -387,12 +387,6 @@ version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" -[[package]] -name = "anymap3" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5dfbc6d8d2675589ccbe4d0fd61df2419075625f8c1a62325e718e2b0049f9" - [[package]] name = "apple-native-keyring-store" version = "1.0.1" @@ -445,12 +439,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "array-init" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" - [[package]] name = "arrayref" version = "0.3.9" @@ -717,8 +705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", - "axum-core", - "axum-macros", + "axum-core 0.4.5", "base64 0.22.1", "bytes", "futures-util", @@ -728,7 +715,7 @@ dependencies = [ "hyper", "hyper-util", "itoa", - "matchit", + "matchit 0.7.3", "memchr", "mime", "percent-encoding", @@ -741,7 +728,44 @@ dependencies = [ "sha1", "sync_wrapper", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.24.0", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core 0.5.6", + "axum-macros", + "base64 0.22.1", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.3.1", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit 0.8.4", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite 0.29.0", "tower", "tower-layer", "tower-service", @@ -769,11 +793,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-macros" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" dependencies = [ "proc-macro2", "quote", @@ -1281,10 +1324,10 @@ name = "cap-audio" version = "0.1.0" dependencies = [ "cap-enc-ffmpeg", + "cap-rnnoise", "cidre", "cpal 0.15.3 (git+https://github.com/CapSoftware/cpal?rev=6013cb5f8bd3)", "ffmpeg-next", - "nnnoiseless", "serde", "serde_json", "tempfile", @@ -1306,6 +1349,7 @@ dependencies = [ "tokio", "tracing", "uuid", + "workspace-hack", ] [[package]] @@ -1360,8 +1404,10 @@ dependencies = [ "libloading 0.9.0", "ndarray 0.16.1", "ort", + "tokio", "tracing", "wgpu", + "workspace-hack", ] [[package]] @@ -1471,7 +1517,7 @@ dependencies = [ "anyhow", "ashpd", "async-stream", - "axum", + "axum 0.8.9", "base64 0.22.1", "block2 0.6.1", "bytemuck", @@ -1574,7 +1620,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tokio-stream", - "tokio-tungstenite", + "tokio-tungstenite 0.24.0", "tokio-util", "tracing", "tracing-appender", @@ -1597,7 +1643,7 @@ dependencies = [ name = "cap-editor" version = "0.1.0" dependencies = [ - "axum", + "axum 0.7.9", "cap-audio", "cap-enc-ffmpeg", "cap-media", @@ -1934,11 +1980,33 @@ dependencies = [ "x11rb", ] +[[package]] +name = "cap-render-farm" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.22.1", + "cap-audio", + "cap-editor", + "cap-export", + "cap-project", + "cap-rendering", + "ffmpeg-next", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", + "wgpu", + "workspace-hack", +] + [[package]] name = "cap-rendering" version = "0.1.0" dependencies = [ "anyhow", + "ash", "bezier_easing", "build-time", "bytemuck", @@ -1959,6 +2027,7 @@ dependencies = [ "futures-intrusive", "glyphon", "image 0.25.8", + "libloading 0.8.8", "log", "metal 0.31.0", "objc2 0.6.2", @@ -1999,6 +2068,14 @@ dependencies = [ "workspace-hack", ] +[[package]] +name = "cap-rnnoise" +version = "0.1.0" +dependencies = [ + "cc", + "workspace-hack", +] + [[package]] name = "cap-test" version = "0.1.0" @@ -3446,19 +3523,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "easyfft" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "767e39eef2ad8a3b6f1d733be3ec70364d21d437d06d4f18ea76ce08df20b75f" -dependencies = [ - "array-init", - "generic_singleton", - "num-complex", - "realfft", - "rustfft", -] - [[package]] name = "either" version = "1.15.0" @@ -4165,16 +4229,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "generic_singleton" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab6e923c8e978e57cf63e2e200ca967d1d20f0ea2662b28f6d4e11c44aa6ab16" -dependencies = [ - "anymap3", - "parking_lot", -] - [[package]] name = "gethostname" version = "1.0.2" @@ -5952,6 +6006,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "matrixmultiply" version = "0.3.10" @@ -6415,16 +6475,6 @@ dependencies = [ "libc", ] -[[package]] -name = "nnnoiseless" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "805d5964d1e7a0006a7fdced7dae75084d66d18b35f1dfe81bd76929b1f8da0c" -dependencies = [ - "easyfft", - "once_cell", -] - [[package]] name = "nodrop" version = "0.1.14" @@ -6519,7 +6569,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", - "serde", ] [[package]] @@ -10572,7 +10621,6 @@ dependencies = [ [[package]] name = "tauri-nspanel" version = "2.0.1" -source = "git+https://github.com/ahkohd/tauri-nspanel?branch=v2#18ffb9a201fbf6fedfaa382fd4b92315ea30ab1a" dependencies = [ "bitflags 2.13.1", "block", @@ -10581,6 +10629,7 @@ dependencies = [ "core-graphics 0.25.0", "objc", "objc-foundation", + "objc_exception", "objc_id", "tauri", ] @@ -11393,7 +11442,19 @@ dependencies = [ "futures-util", "log", "tokio", - "tungstenite", + "tungstenite 0.24.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.29.0", ] [[package]] @@ -11802,6 +11863,22 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http 1.3.1", + "httparse", + "log", + "rand 0.9.2", + "sha1", + "thiserror 2.0.16", +] + [[package]] name = "twox-hash" version = "2.1.2" @@ -13575,40 +13652,50 @@ dependencies = [ name = "workspace-hack" version = "0.1.0" dependencies = [ + "ahash", "arrayvec", - "axum", "bitflags 2.13.1", "bytemuck", "chrono", "clang-sys", "clap", "clap_builder", + "concurrent-queue", "deranged", + "digest", + "dispatch2", + "dpi", "either", "flate2", "form_urlencoded", "futures-channel", "futures-core", "futures-executor", + "futures-io", "futures-sink", "futures-task", "futures-util", - "getrandom 0.2.16", "gif", - "hashbrown 0.15.5", "idna", - "itertools 0.12.1", - "itertools 0.13.0", "libc", + "linux-raw-sys 0.11.0", + "linux-raw-sys 0.4.15", "log", "memchr", "miniz_oxide", + "ndarray 0.17.2", "nom 7.1.3", + "num-complex", + "num-integer", "num-rational", - "num-traits", + "objc2 0.6.2", + "objc2-core-foundation", + "objc2-core-graphics", + "open", + "ort", + "ort-sys", "percent-encoding", - "phf_shared 0.10.0", - "phf_shared 0.11.3", + "prettyplease", "proc-macro2", "profiling", "quote", @@ -13619,25 +13706,44 @@ dependencies = [ "reqwest", "rgb", "rustc-hash 1.1.0", + "rustix 0.38.44", + "rustix 1.1.2", "schemars 0.8.22", + "scopeguard", "semver", "serde", "serde_core", - "serde_json", "simd-adler32", - "smallvec", + "smol_str", "specta", "specta-macros", "stable_deref_trait", - "syn 2.0.106", "tauri-utils", "thiserror 2.0.16", "time", - "tokio", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_edit 0.23.10+spec-1.0.0", + "toml_parser", + "toml_writer", "tracing", "tracing-core", "tracing-subscriber", - "uuid", + "ttf-parser 0.25.1", + "wayland-backend", + "windows 0.54.0", + "windows 0.58.0", + "windows 0.60.0", + "windows 0.61.3", + "windows-core 0.60.1", + "windows-core 0.61.2", + "windows-result 0.3.4", + "windows-sys 0.52.0", + "windows-sys 0.59.0", + "windows-sys 0.60.2", + "windows-sys 0.61.2", + "winnow 0.7.13", + "x11rb", + "x11rb-protocol", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 91e45dbbdc9..8a9be0ffb64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,13 +103,27 @@ debug = 1 [profile.dev.package."*"] debug = 0 +[profile.dev.package.cap-camera-effects] +opt-level = 2 + +[profile.dev.package.cap-audio] +opt-level = 2 + # Optimize for smaller binary size [profile.release] panic = "unwind" codegen-units = 1 # Compile crates one after another so the compiler can optimize better -lto = true # Enables link to optimizations +# Thin LTO runs the cross-crate optimisation in parallel across crates. Fat LTO +# serialised the whole program through one thread and one address space, which +# cost ~60 minutes of the Windows release job (18m for the CLI link + 41m for +# the desktop link) and needed the swapfile / pagefile workarounds in +# desktop-release.yml to survive 16GB runners. +lto = "thin" opt-level = "s" # Optimize for binary size -debug = true +# Line tables are all Sentry needs to symbolicate the uploaded dSYM / PDB +# (function names + file:line). Full debuginfo multiplied rlib size and link +# memory for variable/type info nothing consumes. +debug = "line-tables-only" [patch.crates-io] # screencapturekit = { git = "https://github.com/CapSoftware/screencapturekit-rs", rev = "7ff1e103742e56c8f6c2e940b5e52684ed0bed69" } # branch = "cap-main" @@ -121,3 +135,11 @@ tao = { path = "vendor/tao" } # https://github.com/CapSoftware/reqwest/commit/9b5ecbd5210a9510fde766015cabb724c1e70d2e reqwest = { git = "https://github.com/CapSoftware/reqwest", rev = "9b5ecbd5210a9510fde766015cabb724c1e70d2e" } + +# tauri-nspanel v2 @ 18ffb9a with ownership fixes: `to_panel()` no longer steals +# tao's NSWindow retain (every repeat call was an over-release), converting the +# same window twice is a no-op, the store forgets destroyed windows, and the +# broken `dealloc` override (NSObject dealloc + jump through a garbage pointer) +# is gone. See vendor/tauri-nspanel/CAP-CHANGES.md. +[patch."https://github.com/ahkohd/tauri-nspanel"] +tauri-nspanel = { path = "vendor/tauri-nspanel" } diff --git a/apps/cli/src/caps.rs b/apps/cli/src/caps.rs index 088a14f7122..a5382c5d92c 100644 --- a/apps/cli/src/caps.rs +++ b/apps/cli/src/caps.rs @@ -159,6 +159,17 @@ struct LoomImportArgs { format: OutputFormat, } +fn loom_import_payload(loom_url: &str, owner_email: Option<&str>, space: Option<&str>) -> Value { + let mut payload = json!({ "loomUrl": loom_url }); + if let Some(owner_email) = owner_email { + payload["ownerEmail"] = json!(owner_email); + } + if let Some(space) = space { + payload["spaceName"] = json!(space); + } + payload +} + #[derive(Args)] struct WaitArgs { cap: String, @@ -1274,11 +1285,11 @@ impl CapsArgs { .mutate_json_confirmed( Method::POST, &format!("/organizations/{organization}/imports/loom"), - &json!({ - "loomUrl": args.loom_url, - "ownerEmail": args.owner_email, - "spaceName": args.space, - }), + &loom_import_payload( + &args.loom_url, + args.owner_email.as_deref(), + args.space.as_deref(), + ), ) .await?; let value = if args.wait { @@ -1763,6 +1774,26 @@ mod tests { use super::*; use tokio::io::{AsyncReadExt, AsyncWriteExt}; + #[test] + fn loom_import_payload_omits_unset_optional_fields() { + assert_eq!( + loom_import_payload("https://www.loom.com/share/abc", None, None), + json!({ "loomUrl": "https://www.loom.com/share/abc" }) + ); + assert_eq!( + loom_import_payload( + "https://www.loom.com/share/abc", + Some("owner@example.com"), + Some("Team"), + ), + json!({ + "loomUrl": "https://www.loom.com/share/abc", + "ownerEmail": "owner@example.com", + "spaceName": "Team", + }) + ); + } + async fn read_request(stream: &mut tokio::net::TcpStream) -> String { let mut bytes = Vec::new(); let mut buffer = [0_u8; 1_024]; diff --git a/apps/cli/src/selftest/playback.rs b/apps/cli/src/selftest/playback.rs index 6612739e7a7..57e997d9471 100644 --- a/apps/cli/src/selftest/playback.rs +++ b/apps/cli/src/selftest/playback.rs @@ -903,6 +903,8 @@ mod fixture { timescale: 1.0, name: None, speed_audio_mode: None, + hide_cursor: None, + volume: None, }], transitions: Vec::new(), zoom_segments: Vec::new(), diff --git a/apps/desktop-gpui/Cargo.lock b/apps/desktop-gpui/Cargo.lock index 2a9f3e5aa36..bc3f373e1e4 100644 --- a/apps/desktop-gpui/Cargo.lock +++ b/apps/desktop-gpui/Cargo.lock @@ -453,12 +453,6 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" -[[package]] -name = "anymap3" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5dfbc6d8d2675589ccbe4d0fd61df2419075625f8c1a62325e718e2b0049f9" - [[package]] name = "ar_archive_writer" version = "0.5.3" @@ -485,12 +479,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "array-init" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" - [[package]] name = "arrayref" version = "0.3.9" @@ -860,7 +848,6 @@ checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", "axum-core", - "axum-macros", "base64 0.22.1", "bytes", "futures-util", @@ -911,17 +898,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "axum-macros" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "backtrace" version = "0.3.76" @@ -1409,10 +1385,10 @@ name = "cap-audio" version = "0.1.0" dependencies = [ "cap-enc-ffmpeg", + "cap-rnnoise", "cidre", "cpal 0.15.3 (git+https://github.com/CapSoftware/cpal?rev=6013cb5f8bd3)", "ffmpeg-next", - "nnnoiseless", "serde", "serde_json", "tokio", @@ -1470,6 +1446,7 @@ dependencies = [ "ort", "tracing", "wgpu 25.0.2", + "workspace-hack", ] [[package]] @@ -1595,6 +1572,7 @@ dependencies = [ "resvg 0.45.1", "rfd", "rodio", + "sanitize-filename", "scap-targets", "semver", "serde", @@ -1631,12 +1609,14 @@ dependencies = [ "ffmpeg-next", "flume 0.11.1", "futures", + "image 0.25.10", "lru", "ringbuf", "sentry", "serde", "serde_json", "specta", + "tempfile", "tokio", "tokio-util", "tracing", @@ -1962,6 +1942,14 @@ dependencies = [ "workspace-hack", ] +[[package]] +name = "cap-rnnoise" +version = "0.1.0" +dependencies = [ + "cc", + "workspace-hack", +] + [[package]] name = "cap-timestamp" version = "0.1.0" @@ -3367,6 +3355,9 @@ name = "dpi" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] [[package]] name = "dtoa" @@ -3422,19 +3413,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "easyfft" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "767e39eef2ad8a3b6f1d733be3ec70364d21d437d06d4f18ea76ce08df20b75f" -dependencies = [ - "array-init", - "generic_singleton", - "num-complex", - "realfft", - "rustfft", -] - [[package]] name = "either" version = "1.17.0" @@ -4151,16 +4129,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "generic_singleton" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab6e923c8e978e57cf63e2e200ca967d1d20f0ea2662b28f6d4e11c44aa6ab16" -dependencies = [ - "anymap3", - "parking_lot", -] - [[package]] name = "gethostname" version = "1.1.0" @@ -5909,6 +5877,17 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" +[[package]] +name = "lewton" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" +dependencies = [ + "byteorder", + "ogg", + "tinyvec", +] + [[package]] name = "libappindicator" version = "0.9.0" @@ -6051,6 +6030,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -6717,16 +6702,6 @@ dependencies = [ "libc", ] -[[package]] -name = "nnnoiseless" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "805d5964d1e7a0006a7fdced7dae75084d66d18b35f1dfe81bd76929b1f8da0c" -dependencies = [ - "easyfft", - "once_cell", -] - [[package]] name = "no_std_io2" version = "0.9.4" @@ -6848,7 +6823,6 @@ checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "bytemuck", "num-traits", - "serde", ] [[package]] @@ -6985,6 +6959,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", + "objc2-exception-helper", ] [[package]] @@ -7215,6 +7190,15 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + [[package]] name = "objc2-foundation" version = "0.2.2" @@ -7427,6 +7411,15 @@ dependencies = [ "cc", ] +[[package]] +name = "ogg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" +dependencies = [ + "byteorder", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -7502,6 +7495,7 @@ version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9cfef937e9c486488c7e3d949ae31c0f1d06bdacd75b99c086cb35356e30408" dependencies = [ + "dunce", "is-wsl", "libc", ] @@ -9117,6 +9111,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb" dependencies = [ "cpal 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)", + "lewton", "symphonia", "thiserror 1.0.69", ] @@ -9294,6 +9289,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "sanitize-filename" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc984f4f9ceb736a7bb755c3e3bd17dc56370af2600c9780dcc48c66453da34d" +dependencies = [ + "regex", +] + [[package]] name = "scap-cpal" version = "0.1.0" @@ -10079,6 +10083,9 @@ name = "smol_str" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] [[package]] name = "smol_str" @@ -11108,6 +11115,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -11153,6 +11169,19 @@ dependencies = [ "winnow 0.7.15", ] +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + [[package]] name = "toml_edit" version = "0.25.13+spec-1.1.0" @@ -13372,40 +13401,50 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" name = "workspace-hack" version = "0.1.0" dependencies = [ + "ahash", "arrayvec", - "axum", "bitflags 2.13.1", "bytemuck", "chrono", "clang-sys", "clap", "clap_builder", + "concurrent-queue", "deranged", + "digest", + "dispatch2", + "dpi", "either", "flate2", "form_urlencoded", "futures-channel", "futures-core", "futures-executor", + "futures-io", "futures-sink", "futures-task", "futures-util", - "getrandom 0.2.17", "gif 0.13.3", - "hashbrown 0.15.5", "idna", - "itertools 0.12.1", - "itertools 0.13.0", "libc", + "linux-raw-sys 0.11.0", + "linux-raw-sys 0.4.15", "log", "memchr", "miniz_oxide", + "ndarray 0.17.2", "nom 7.1.3", + "num-complex", + "num-integer", "num-rational", - "num-traits", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "open", + "ort", + "ort-sys", "percent-encoding", - "phf_shared 0.10.0", - "phf_shared 0.11.3", + "prettyplease", "proc-macro2", "profiling", "quote", @@ -13416,25 +13455,44 @@ dependencies = [ "reqwest 0.12.24", "rgb", "rustc-hash 1.1.0", + "rustix 0.38.44", + "rustix 1.1.4", "schemars 0.8.22", + "scopeguard", "semver", "serde", "serde_core", - "serde_json", "simd-adler32", - "smallvec", + "smol_str 0.2.2", "specta", "specta-macros", "stable_deref_trait", - "syn 2.0.119", "tauri-utils", "thiserror 2.0.20", "time", - "tokio", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_edit 0.23.10+spec-1.0.0", + "toml_parser", + "toml_writer", "tracing", "tracing-core", "tracing-subscriber", - "uuid", + "ttf-parser 0.25.1", + "wayland-backend", + "windows 0.54.0", + "windows 0.58.0", + "windows 0.60.0", + "windows 0.61.3", + "windows-core 0.60.1", + "windows-core 0.61.2", + "windows-result 0.3.4", + "windows-sys 0.52.0", + "windows-sys 0.59.0", + "windows-sys 0.60.2", + "windows-sys 0.61.2", + "winnow 0.7.15", + "x11rb", + "x11rb-protocol", ] [[package]] @@ -13472,6 +13530,8 @@ dependencies = [ "as-raw-xcb-connection", "gethostname", "libc", + "libloading 0.8.9", + "once_cell", "rustix 1.1.4", "x11rb-protocol", "xcursor", diff --git a/apps/desktop-gpui/Cargo.toml b/apps/desktop-gpui/Cargo.toml index b2411ea5a9f..5117c79ab5e 100644 --- a/apps/desktop-gpui/Cargo.toml +++ b/apps/desktop-gpui/Cargo.toml @@ -68,7 +68,7 @@ cap-project = { path = "../../crates/project" } # a `RenderImage` (`src/editor_sidebar/cursor.rs`). cap-cursor-info = { path = "../../crates/cursor-info" } resvg = "0.45" -rodio = { version = "0.19.0", default-features = false, features = ["mp3"] } +rodio = { version = "0.19.0", default-features = false, features = ["mp3", "vorbis"] } # The editor stack. `cap-rendering` is not a new subtree -- cap-recording # already depends on it, so wgpu 25 and its vendored wgpu-hal have been built # by this workspace since the recording unit; naming it here only makes the @@ -129,6 +129,7 @@ chrono = "0.4" # Read the Tauri app's settings store so both apps share one recordings library. serde_json = "1" serde = { version = "1", features = ["derive"] } +sanitize-filename = "0.6.0" semver = "1" base64 = "0.22" rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "tokio"] } diff --git a/apps/desktop-gpui/README.md b/apps/desktop-gpui/README.md index 1901b442ce1..9d01d6a963b 100644 --- a/apps/desktop-gpui/README.md +++ b/apps/desktop-gpui/README.md @@ -3,14 +3,8 @@ Cap's desktop app, rewritten in [gpui](https://www.gpui.rs/). No Tauri, no webview — the whole UI is drawn by gpui and every pixel is Rust. -This is milestone 1: **the main recording window**, compact and expanded, with -real device enumeration. It is a parallel implementation, not a replacement. -`apps/desktop` is untouched and remains the shipping app. - -| | | -|---|---| -| ![compact](docs/main-window-compact.png) | ![expanded](docs/main-window-expanded.png) | -| 330×395 | 600×660 | +The main recording window uses a fixed 330×432 layout with device controls +and recording and screenshot browsers. The expanded view is no longer available. ## Running it @@ -2202,10 +2196,6 @@ black (a useful way to read the tint's alpha back, in fact — a 0.55 white tint lands on `#8c8c8c`). Anything about the material actually being live has to be judged from a full-screen `screencapture -x`. -`CAP_GPUI_AUTO_EXPAND=1` opens the window expanded, the way clicking the zoom -light does, for the same reason as the other `CAP_GPUI_AUTO_*` hooks: -unprivileged synthetic clicks are dropped. - `CAP_GPUI_AUTO_SETTINGS=1` opens the settings window at startup, exactly as the header gear does (main window hidden included). Pass a page slug instead of `1` — `CAP_GPUI_AUTO_SETTINGS=hotkeys` — to land on another sidebar entry; @@ -2384,13 +2374,6 @@ posted at it are dropped — with or without Accessibility, and identically on the pre-consolidation binary. Its search field has to be checked through the panel it filters. -`CAP_GPUI_AUTO_RECENT=1` clicks the first Recents card once the library scan -has landed (expanding the window first, since the scan only runs while -expanded), through `main_window::activate_recent` — the card's own handler. -`=twice` clicks it again 2.5s later, which is how the one-window-per-project -rule is checked: the second activation logs `editor already open for this -project; focusing it` instead of opening a second window. - `CAP_GPUI_TAURI_STORE=` points every settings read and write at another file. Use it for anything that toggles a setting: the default is the store the shipping app is also writing, and a probe run should not be editing the real diff --git a/apps/desktop-gpui/assets/icons/arrow-left-to-line.svg b/apps/desktop-gpui/assets/icons/arrow-left-to-line.svg new file mode 100644 index 00000000000..acacb0348d7 --- /dev/null +++ b/apps/desktop-gpui/assets/icons/arrow-left-to-line.svg @@ -0,0 +1 @@ + diff --git a/apps/desktop-gpui/assets/icons/arrow-right-to-line.svg b/apps/desktop-gpui/assets/icons/arrow-right-to-line.svg new file mode 100644 index 00000000000..5f334654ece --- /dev/null +++ b/apps/desktop-gpui/assets/icons/arrow-right-to-line.svg @@ -0,0 +1 @@ + diff --git a/apps/desktop-gpui/assets/icons/diamond.svg b/apps/desktop-gpui/assets/icons/diamond.svg deleted file mode 100644 index e5ac92404a8..00000000000 --- a/apps/desktop-gpui/assets/icons/diamond.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/desktop-gpui/assets/icons/gem.svg b/apps/desktop-gpui/assets/icons/gem.svg new file mode 100644 index 00000000000..a69fafa6503 --- /dev/null +++ b/apps/desktop-gpui/assets/icons/gem.svg @@ -0,0 +1 @@ + diff --git a/apps/desktop-gpui/assets/icons/italic.svg b/apps/desktop-gpui/assets/icons/italic.svg deleted file mode 100644 index 21b1ab754c1..00000000000 --- a/apps/desktop-gpui/assets/icons/italic.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/desktop-gpui/assets/icons/mouse-pointer-ban.svg b/apps/desktop-gpui/assets/icons/mouse-pointer-ban.svg new file mode 100644 index 00000000000..d3e6a7126e1 --- /dev/null +++ b/apps/desktop-gpui/assets/icons/mouse-pointer-ban.svg @@ -0,0 +1 @@ + diff --git a/apps/desktop-gpui/build.rs b/apps/desktop-gpui/build.rs index 4faa84b72b7..ab7033cbe98 100644 --- a/apps/desktop-gpui/build.rs +++ b/apps/desktop-gpui/build.rs @@ -7,6 +7,11 @@ fn main() { return; } + if std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc") { + // Export dispatch constructs media futures on the UI thread before Tokio can poll them. + println!("cargo:rustc-link-arg-bin=cap-gpui=/STACK:16777216"); + } + let icon = "../desktop/src-tauri/icons/icon.ico"; println!("cargo:rerun-if-changed={icon}"); tauri_winres::WindowsResource::new() diff --git a/apps/desktop-gpui/dev.sh b/apps/desktop-gpui/dev.sh index cd92c3edd60..49aa48d645e 100755 --- a/apps/desktop-gpui/dev.sh +++ b/apps/desktop-gpui/dev.sh @@ -10,7 +10,7 @@ STATE_FILE="$PWD/target/dev-restore.json" BIN="$PWD/target/debug/cap-gpui" BUILD=cargo -WATCH_PATHS=(src assets Cargo.toml) +WATCH_PATHS=(src assets Cargo.toml ../desktop/src-tauri/sounds) [ -d resources ] && WATCH_PATHS+=(resources) for crate in camera scap-targets recording timestamp utils project rendering editor export; do [ -d "../../crates/$crate/src" ] && WATCH_PATHS+=("../../crates/$crate/src") diff --git a/apps/desktop-gpui/src/app_sounds.rs b/apps/desktop-gpui/src/app_sounds.rs new file mode 100644 index 00000000000..c2ab9dd6400 --- /dev/null +++ b/apps/desktop-gpui/src/app_sounds.rs @@ -0,0 +1,102 @@ +use std::io::Cursor; + +use anyhow::Context as _; +use rodio::{Decoder, OutputStream, Sink}; + +#[path = "../../desktop/src-tauri/src/recording_start_sound.rs"] +mod recording_start_sound; + +pub use recording_start_sound::StartCue; + +pub fn prime_recording_start_sound() -> StartCue { + recording_start_sound::prime(AppSound::StartRecording.bytes()) +} + +pub async fn play_recording_start_sound(cue: StartCue, gate: cap_recording::RecordingStartGate) { + recording_start_sound::play(cue, gate).await; +} + +#[derive(Clone, Copy, Debug)] +pub enum AppSound { + StartRecording, + StopRecording, + Notification, +} + +impl AppSound { + fn bytes(self) -> &'static [u8] { + match self { + Self::StartRecording => { + include_bytes!("../../desktop/src-tauri/sounds/start-recording.ogg") + } + Self::StopRecording => { + include_bytes!("../../desktop/src-tauri/sounds/stop-recording.ogg") + } + Self::Notification => include_bytes!("../../desktop/src-tauri/sounds/action.ogg"), + } + } + + pub fn play(self) { + if let Err(error) = std::thread::Builder::new() + .name("app-sound".into()) + .spawn(move || { + if let Err(error) = self.play_to_end() { + tracing::warn!(?self, %error, "App sound unavailable"); + } + }) + { + tracing::warn!(%error, "Could not start app sound playback"); + } + } + + fn play_to_end(self) -> anyhow::Result<()> { + let (_stream, handle) = OutputStream::try_default().context("opening audio output")?; + let source = Decoder::new(Cursor::new(self.bytes())).context("decoding app sound")?; + let sink = Sink::try_new(&handle).context("creating app sound sink")?; + sink.append(source); + sink.sleep_until_end(); + tracing::debug!(?self, "App sound played"); + Ok(()) + } +} + +pub fn play_notification() { + if crate::store::notification_sounds_enabled() { + AppSound::Notification.play(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rodio::Source as _; + + const SOUNDS: [AppSound; 3] = [ + AppSound::StartRecording, + AppSound::StopRecording, + AppSound::Notification, + ]; + + #[test] + fn tauri_sound_assets_decode_to_audible_pcm() { + for sound in SOUNDS { + let source = Decoder::new(Cursor::new(sound.bytes())).unwrap(); + let channels = source.channels(); + let rate = source.sample_rate(); + assert!(channels > 0); + assert!(rate > 0); + let samples: Vec<_> = source.collect(); + assert!(samples.iter().any(|sample| *sample != 0), "{sound:?}"); + let duration = samples.len() as f64 / f64::from(channels) / f64::from(rate); + assert!((0.05..10.0).contains(&duration), "{sound:?}: {duration}"); + } + } + + #[test] + #[ignore = "plays the recording and notification cues through the default audio output"] + fn native_app_sound_playback() { + for sound in SOUNDS { + sound.play_to_end().unwrap(); + } + } +} diff --git a/apps/desktop-gpui/src/app_windows.rs b/apps/desktop-gpui/src/app_windows.rs index d7ddafcc65a..539e0dbffec 100644 --- a/apps/desktop-gpui/src/app_windows.rs +++ b/apps/desktop-gpui/src/app_windows.rs @@ -819,12 +819,6 @@ pub(crate) fn show_main_window_after_capture_pause(cx: &mut App) { view.show_recorder(cx); view.clear_target(cx); } - // Every path back to the main window is a path a new capture may - // have arrived on -- a finished recording most of all. The Tauri - // app gets this from `invalidateRecentMedia` plus the query's - // focus gate; here the reshow *is* the trigger, so a recording - // made a moment ago is in the list without a restart. - view.refresh_recents(window, cx); #[cfg(target_os = "linux")] if window.retained_visibility().is_some() { match window.set_retained_visibility(true) { @@ -2971,7 +2965,7 @@ pub(crate) fn refresh_linux_instant_camera( effects: LinuxCameraProcessing { mirrored: snapshot.state.mirrored, blur: match snapshot.state.background_blur { - BlurMode::Off => LinuxCameraBlur::Off, + BlurMode::Off | BlurMode::Remove => LinuxCameraBlur::Off, BlurMode::Light => LinuxCameraBlur::Light, BlurMode::Heavy => LinuxCameraBlur::Heavy, }, @@ -4052,6 +4046,16 @@ fn camera_frame(cx: &mut App) -> Option { .filter(|camera| camera.snapshot.bounds.width > 0. && camera.snapshot.bounds.height > 0.) } +pub(crate) fn studio_camera_snapshot( + target: &ScreenCaptureTarget, + cx: &mut App, +) -> Option { + let handle = cx.global::().camera?; + handle + .update(cx, |view, window, _| view.studio_snapshot(window, target)) + .ok() +} + fn visible_camera_frame(cx: &mut App) -> Option { if camera_preview_is_inline(cx) { return None; @@ -5234,6 +5238,23 @@ fn load_editor_project_candidate( } async fn drive_auto_export(handle: WindowHandle, cx: &mut gpui::AsyncApp) { + if let Ok(page) = std::env::var("CAP_GPUI_AUTO_EXPORT_PAGE") { + cx.background_executor() + .timer(std::time::Duration::from_millis(300)) + .await; + let _ = handle.update(cx, |view, window, cx| { + view.open_export(window, cx); + if let Some(export) = view.export.as_mut() { + let flags: Vec<&str> = page.split(',').collect(); + export.advanced_open = flags.contains(&"advanced"); + if flags.contains(&"link") { + export.destination = crate::editor_export::ExportDestination::Link; + } + } + cx.notify(); + }); + return; + } let Some(path) = std::env::var_os("CAP_GPUI_AUTO_EXPORT").map(PathBuf::from) else { return; }; @@ -5354,8 +5375,9 @@ fn load_editor_waveforms( } /// `CAP_GPUI_AUTO_SIDEBAR=[:]` selects a config-sidebar tab and -/// optionally scrolls its body, and `CAP_GPUI_AUTO_SELECT=:[,]` -/// selects timeline segments so their panel opens. +/// optionally scrolls its body, `CAP_GPUI_AUTO_CAMERA3D=add[: