From 36f559b0c6cb61ebd3431d69d287809bf66acbc5 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 10 Nov 2026 22:46:24 -0500 Subject: [PATCH] chore: move the data site off sst the stats site, its og worker and the sync workflow deploy from services/data with cf as opencode-data-, opencode-data-og- and opencode-data-sync-. the beta web app and the whole sst setup (sst.config.ts, infra/, sst-env.d.ts, the sst dependency) are gone, so nothing deploys through sst anymore (security finding #920). --- .github/workflows/deploy.yml | 101 ++++---- .gitignore | 1 - bun.lock | 58 ++--- infra/stage.ts | 9 - infra/stats.ts | 215 ------------------ infra/webapp.ts | 21 -- package.json | 3 - packages/app/README.md | 23 +- packages/core/sst-env.d.ts | 10 - packages/desktop/README.md | 2 +- packages/effect-drizzle-sqlite/sst-env.d.ts | 10 - packages/http-recorder/sst-env.d.ts | 10 - packages/plugin/sst-env.d.ts | 10 - .../figma}/sst-env.d.ts | 2 +- .../npm}/sst-env.d.ts | 2 +- packages/protocol/sst-env.d.ts | 10 - packages/schema/sst-env.d.ts | 10 - packages/script/sst-env.d.ts | 10 - packages/sdk/sst-env.d.ts | 10 - packages/server/sst-env.d.ts | 10 - packages/session-ui/sst-env.d.ts | 10 - packages/stats/AGENTS.md | 2 +- packages/stats/README.md | 21 +- packages/stats/app/package.json | 1 - packages/stats/app/script/build-og-worker.ts | 4 +- packages/stats/app/src/lib/noindex.ts | 10 +- .../stats/app/src/lib/resource.cloudflare.ts | 2 +- packages/stats/app/src/og/worker.ts | 6 +- packages/stats/app/tsconfig.json | 2 +- packages/stats/core/drizzle.config.ts | 2 +- packages/stats/core/package.json | 4 +- packages/stats/core/src/config.ts | 2 +- packages/stats/core/src/database.ts | 2 +- packages/stats/core/src/domain/home.test.ts | 5 +- packages/stats/core/src/domain/home.ts | 2 +- packages/stats/core/src/domain/inference.ts | 2 +- .../stats/core/src/ensure-unique-users.ts | 2 +- packages/stats/core/src/r2-sql.ts | 2 +- packages/stats/core/src/resource.d.ts | 34 --- packages/stats/core/src/resource.ts | 61 +++++ packages/stats/core/src/stat-sync.test.ts | 10 +- packages/stats/core/src/stat-sync.ts | 2 +- packages/stats/server/package.json | 3 +- packages/stats/server/src/schedule.ts | 2 +- packages/stats/server/src/worker.ts | 3 + packages/stats/server/src/workflow.ts | 4 - packages/stats/server/tsconfig.json | 2 +- packages/stories/sst-env.d.ts | 10 - packages/theme/sst-env.d.ts | 10 - packages/tui/sst-env.d.ts | 10 - packages/ui/sst-env.d.ts | 10 - sdks/vscode/sst-env.d.ts | 10 - services/README.md | 13 +- services/data/.gitignore | 3 + services/data/README.md | 24 ++ services/data/cloudflare.config.ts | 42 ++++ services/data/og.config.ts | 18 ++ services/data/package.json | 24 ++ services/data/script/build.ts | 101 ++++++++ services/data/script/deploy.ts | 49 ++++ {packages/ai => services/data}/sst-env.d.ts | 0 services/data/sync.config.ts | 39 ++++ services/data/tsconfig.json | 13 ++ {packages/app => services/files}/sst-env.d.ts | 0 services/legacy/README.md | 5 +- .../cli => services/legacy}/sst-env.d.ts | 0 .../client => services/site}/sst-env.d.ts | 0 .../codemode => services/update}/sst-env.d.ts | 0 sst-env.d.ts | 31 --- sst.config.ts | 34 --- 70 files changed, 497 insertions(+), 678 deletions(-) delete mode 100644 infra/stage.ts delete mode 100644 infra/stats.ts delete mode 100644 infra/webapp.ts delete mode 100644 packages/core/sst-env.d.ts delete mode 100644 packages/effect-drizzle-sqlite/sst-env.d.ts delete mode 100644 packages/http-recorder/sst-env.d.ts delete mode 100644 packages/plugin/sst-env.d.ts rename packages/{httpapi-codegen => plugins/figma}/sst-env.d.ts (80%) rename packages/{desktop => process-daemon/npm}/sst-env.d.ts (80%) delete mode 100644 packages/protocol/sst-env.d.ts delete mode 100644 packages/schema/sst-env.d.ts delete mode 100644 packages/script/sst-env.d.ts delete mode 100644 packages/sdk/sst-env.d.ts delete mode 100644 packages/server/sst-env.d.ts delete mode 100644 packages/session-ui/sst-env.d.ts delete mode 100644 packages/stats/core/src/resource.d.ts create mode 100644 packages/stats/core/src/resource.ts create mode 100644 packages/stats/server/src/worker.ts delete mode 100644 packages/stories/sst-env.d.ts delete mode 100644 packages/theme/sst-env.d.ts delete mode 100644 packages/tui/sst-env.d.ts delete mode 100644 packages/ui/sst-env.d.ts delete mode 100644 sdks/vscode/sst-env.d.ts create mode 100644 services/data/.gitignore create mode 100644 services/data/README.md create mode 100644 services/data/cloudflare.config.ts create mode 100644 services/data/og.config.ts create mode 100644 services/data/package.json create mode 100644 services/data/script/build.ts create mode 100644 services/data/script/deploy.ts rename {packages/ai => services/data}/sst-env.d.ts (100%) create mode 100644 services/data/sync.config.ts create mode 100644 services/data/tsconfig.json rename {packages/app => services/files}/sst-env.d.ts (100%) rename {packages/cli => services/legacy}/sst-env.d.ts (100%) rename {packages/client => services/site}/sst-env.d.ts (100%) rename {packages/codemode => services/update}/sst-env.d.ts (100%) delete mode 100644 sst-env.d.ts delete mode 100644 sst.config.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2a799f56641..595d4814855 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,7 +5,6 @@ on: branches: - dev - production - - beta workflow_dispatch: concurrency: ${{ github.workflow }}-${{ github.ref }} @@ -14,58 +13,8 @@ permissions: contents: read jobs: - deploy: - if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production' || github.ref_name == 'beta') - runs-on: blacksmith-4vcpu-ubuntu-2404 - environment: ${{ github.ref_name }} - permissions: - contents: read - id-token: write - steps: - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - - - uses: ./.github/actions/setup-bun - - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: "24" - - # beta has no branch, environment or AWS role. Running it again needs a role trusting environment `beta` and - # opencode/beta/CLOUDFLARE_API_TOKEN and opencode/beta/PLANETSCALE_SERVICE_TOKEN. - - name: Configure AWS credentials - if: github.ref_name != 'beta' - uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0 - with: - role-to-assume: arn:aws:iam::604439173067:role/github/opencode-${{ github.ref_name }} - role-session-name: opencode-${{ github.run_id }} - aws-region: us-east-1 - - - name: Read deploy secrets - if: github.ref_name != 'beta' - uses: aws-actions/aws-secretsmanager-get-secrets@2297f9a879480a9e3af9b293ed15c70caf8e1c88 # v3.0.2 - with: - secret-ids: | - CLOUDFLARE_API_TOKEN,opencode/${{ github.ref_name }}/CLOUDFLARE_API_TOKEN - PLANETSCALE_SERVICE_TOKEN,opencode/${{ github.ref_name }}/PLANETSCALE_SERVICE_TOKEN - parse-json-secrets: false - - - run: bun sst deploy --stage=${{ github.ref_name }} - env: - GITHUB_TOKEN: ${{ github.token }} - PLANETSCALE_SERVICE_TOKEN_NAME: ${{ vars.PLANETSCALE_SERVICE_TOKEN_NAME }} - # Only the beta stage's web app uploads sourcemaps; packages/app skips the upload without a token. - SENTRY_AUTH_TOKEN: ${{ github.ref_name == 'beta' && secrets.SENTRY_AUTH_TOKEN || '' }} - SENTRY_ORG: ${{ vars.SENTRY_ORG }} - SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }} - SENTRY_RELEASE: web@${{ github.sha }} - VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }} - VITE_SENTRY_RELEASE: web@${{ github.sha }} - - # The SST stack owned api.opencode.ai until this push removes its Api Worker, so this job runs after the - # SST deploy to take the hostname without a custom-domain conflict. legacy: - needs: deploy - if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta' + if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production') runs-on: blacksmith-4vcpu-ubuntu-2404 environment: ${{ github.ref_name }} permissions: @@ -106,3 +55,51 @@ jobs: jq --arg name "$name" '{($name): .SecretString}' done | jq -s add > "$secrets" bun run deploy:$STAGE --secrets-file "$secrets" + + # The /data site, its OG image Worker and the sync Workflow (services/data). + data: + if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production') + runs-on: blacksmith-4vcpu-ubuntu-2404 + environment: ${{ github.ref_name }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + + - uses: ./.github/actions/setup-bun + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "24" + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0 + with: + role-to-assume: arn:aws:iam::604439173067:role/github/opencode-${{ github.ref_name }} + role-session-name: opencode-${{ github.run_id }} + aws-region: us-east-1 + + - name: Read deploy secrets + uses: aws-actions/aws-secretsmanager-get-secrets@2297f9a879480a9e3af9b293ed15c70caf8e1c88 # v3.0.2 + with: + secret-ids: CLOUDFLARE_API_TOKEN,opencode/${{ github.ref_name }}/CLOUDFLARE_API_TOKEN + parse-json-secrets: false + + # The Workers' secrets are read here, not in the step above, so only this step ever has them. They reach cf + # through files in the runner's temp directory, never argv, and are removed when the step ends. + - name: Deploy + working-directory: services/data + env: + STAGE: ${{ github.ref_name }} + # An explicit bash runs with pipefail, so a failed secret read fails the step. + shell: bash + run: | + umask 077 + secrets="$RUNNER_TEMP/data-secrets.json" + trap 'rm -f "$secrets" "$secrets".*' EXIT + for name in DATA_DATABASE_HOST DATA_DATABASE_USERNAME DATA_DATABASE_PASSWORD R2_SQL_AUTH_TOKEN DATA_HIDDEN_MODELS; do + aws secretsmanager get-secret-value --secret-id "opencode/$STAGE/$name" --output json | + jq --arg name "$name" '{($name): .SecretString}' + done | jq -s add > "$secrets" + bun run deploy:$STAGE --secrets-file "$secrets" diff --git a/.gitignore b/.gitignore index 630e621d3ed..25a1b473332 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ .DS_Store node_modules .worktrees -.sst .env .env.local .idea diff --git a/bun.lock b/bun.lock index 37f84043235..316cb9e3fc3 100644 --- a/bun.lock +++ b/bun.lock @@ -24,7 +24,6 @@ "oxlint": "1.60.0", "prettier": "3.6.2", "semver": "^7.6.0", - "sst": "catalog:", "turbo": "2.10.2", }, }, @@ -684,7 +683,6 @@ "nitro": "3.0.260903-beta", "satori": "0.63.2", "solid-js": "catalog:", - "sst": "catalog:", "vite": "8.2.4", }, "devDependencies": { @@ -702,7 +700,6 @@ "@planetscale/database": "1.19.0", "drizzle-orm": "catalog:", "effect": "catalog:", - "sst": "catalog:", }, "devDependencies": { "@tsconfig/node22": "catalog:", @@ -719,7 +716,6 @@ "dependencies": { "@opencode/stats-core": "workspace:*", "effect": "catalog:", - "sst": "catalog:", }, "devDependencies": { "@cloudflare/workers-types": "catalog:", @@ -869,6 +865,19 @@ "jsqr": "1.4.0", }, }, + "services/data": { + "name": "@opencode/data", + "devDependencies": { + "@cloudflare/build-output-utils": "0.8.5", + "@cloudflare/config": "0.23.0", + "@opencode/stats-app": "workspace:*", + "@opencode/stats-server": "workspace:*", + "@types/bun": "catalog:", + "@typescript/native": "catalog:", + "cf": "1.0.0-beta.12", + "typescript": "catalog:", + }, + }, "services/files": { "name": "@opencode/files", "devDependencies": { @@ -1065,7 +1074,6 @@ "solid-js": "1.9.17", "solid-list": "0.3.0", "solid-sonner": "0.3.1", - "sst": "4.13.1", "string-width": "7.2.0", "tailwindcss": "4.1.11", "typescript": "npm:@typescript/typescript6@6.0.2", @@ -1136,8 +1144,6 @@ "@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.1057.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-node": "^3.972.47", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-5MliYkp2u0+2arTp5fZIaxl+xmm90LEKv/VeSxhfNQW4t0fvWJrNO429/jchWQenNoDRrOGE59VfbuZUfwFujg=="], - "@aws-sdk/client-lambda": ["@aws-sdk/client-lambda@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-jG2rB6LY2rx3XT55axgH3C0aIv1egvI0kqPYC6jfghVp82kWssU7R/1dJxXr0PnPLv4wq+WXRyLEmjyKURUjFQ=="], - "@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.68", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.43", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Fo2RC5hZmEVhnVLMzNjYL6Iu78F4wDuaNtkgzRVU4DlUaFhcXxsnxWBOTH2drxZ59OHSipQ1LndfkCWPxGBJHA=="], @@ -1172,8 +1178,6 @@ "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], - "@aws/durable-execution-sdk-js": ["@aws/durable-execution-sdk-js@1.0.2", "", { "dependencies": { "@aws-sdk/client-lambda": "^3.943.0" } }, "sha512-KIYBVqV9gArkWdPEDYOjJqLlO+NecmCXOXadNBlOspJTmqztwuiyb6aZc468bYWSGuL4Me/gyTIK97ubkwFNCw=="], - "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], @@ -1792,6 +1796,8 @@ "@opencode/core": ["@opencode/core@workspace:packages/core"], + "@opencode/data": ["@opencode/data@workspace:services/data"], + "@opencode/desktop": ["@opencode/desktop@workspace:packages/desktop"], "@opencode/files": ["@opencode/files@workspace:services/files"], @@ -3728,8 +3734,6 @@ "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], @@ -3738,8 +3742,6 @@ "ocache": ["ocache@0.3.1", "", {}, "sha512-qeBrhQ0ly6tgXsakgNwLx6diRUppJZIcV5OjVGa5yp9HzztutkaGDDXJqVMo1fKWz5fNDtTp3iPU2fuhHf0+Lg=="], - "oidc-token-hash": ["oidc-token-hash@5.2.0", "", {}, "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw=="], - "on-finished": ["on-finished@2.4.2", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ATWUv387i+nLD49EiDoOft5scf32Vsdz3ktrQJG9bi7jRFmaXTssvyvj1EmeKVVgvesbqUtvQQvaS2TpTukAZQ=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], @@ -3752,8 +3754,6 @@ "openai": ["openai@6.49.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-aYCc0C6L864eR6WSYIwQGyXriw/nIyZx0ObvhzOEVuk0zoBDpynjSbrionWI7q65B5H8jJX0DXR9snEzM6bfPg=="], - "openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="], - "own-keys": ["own-keys@1.0.2", "", { "dependencies": { "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg=="], "oxc-parser": ["oxc-parser@0.141.0", "", { "dependencies": { "@oxc-project/types": "^0.141.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.141.0", "@oxc-parser/binding-android-arm64": "0.141.0", "@oxc-parser/binding-darwin-arm64": "0.141.0", "@oxc-parser/binding-darwin-x64": "0.141.0", "@oxc-parser/binding-freebsd-x64": "0.141.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.141.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.141.0", "@oxc-parser/binding-linux-arm64-gnu": "0.141.0", "@oxc-parser/binding-linux-arm64-musl": "0.141.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.141.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.141.0", "@oxc-parser/binding-linux-riscv64-musl": "0.141.0", "@oxc-parser/binding-linux-s390x-gnu": "0.141.0", "@oxc-parser/binding-linux-x64-gnu": "0.141.0", "@oxc-parser/binding-linux-x64-musl": "0.141.0", "@oxc-parser/binding-openharmony-arm64": "0.141.0", "@oxc-parser/binding-wasm32-wasi": "0.141.0", "@oxc-parser/binding-win32-arm64-msvc": "0.141.0", "@oxc-parser/binding-win32-ia32-msvc": "0.141.0", "@oxc-parser/binding-win32-x64-msvc": "0.141.0" } }, "sha512-uFkGGr1KMWd6aWv9UAqooYrN78trw8MWWmoPvgWokfBEUq1+eiIQ+qfj3wokhy0fxtZWZk+0dHoS7/yRTJtd6w=="], @@ -4132,24 +4132,6 @@ "ssri": ["ssri@13.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="], - "sst": ["sst@4.13.1", "", { "dependencies": { "@aws/durable-execution-sdk-js": "1.0.2", "aws4fetch": "1.0.18", "jose": "5.2.3", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "4.13.1", "sst-darwin-x64": "4.13.1", "sst-linux-arm64": "4.13.1", "sst-linux-x64": "4.13.1", "sst-linux-x86": "4.13.1", "sst-win32-arm64": "4.13.1", "sst-win32-x64": "4.13.1", "sst-win32-x86": "4.13.1" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-FKzhWuqV5NPhFWH7b+Ktf5+Mvox6DCMc8iOrjaS3mkjnnmyEcOZq7HNx2/ZlIcBwgOZE9sCPR3ot1zXtXkXzZg=="], - - "sst-darwin-arm64": ["sst-darwin-arm64@4.13.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gGwfgZGOsulU6iGeZJtxP5BpSZFsRUy1dG/yTqJa++n1P1APWXZC64u2qRHm6rdwLAjEeWaCQ9k7ouKgn0zh5Q=="], - - "sst-darwin-x64": ["sst-darwin-x64@4.13.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-RcKLI2oAY+Wk5fWUF0gJFKvwMSpid3qw9L3OcfgFL2ymZH652ybLpQPBxtoJ95FdyknWIMnYf6ZK9C2jnH5eaw=="], - - "sst-linux-arm64": ["sst-linux-arm64@4.13.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Lp/8BWhH8KZnb50xPd8A6bkZ7QqCH3Dar9l16TPwuAmPxxRUlkmg42h+ACZ9n02vSJl8y/WLnV4JB6SgJpRFVg=="], - - "sst-linux-x64": ["sst-linux-x64@4.13.1", "", { "os": "linux", "cpu": "x64" }, "sha512-f4eHOsApoP+CpUzLUGzfoTBYsqqr6nl/M0gjYsVNi1T+6v92lc84AhQ4DoDZx033QggyxkqRezj8jOkjkKxa7w=="], - - "sst-linux-x86": ["sst-linux-x86@4.13.1", "", { "os": "linux", "cpu": "none" }, "sha512-5OHdtPa0GmozDpBPdsD9Tv+SGnpjJSoTXptgK59ivQg3UYNWBBotidvEyUJs1MU9B/3+I+Fmw32zsF2O+ZtPgg=="], - - "sst-win32-arm64": ["sst-win32-arm64@4.13.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-qAUuCQj6wDdZ84a9W4FhnRTHYlEnYRHGYFqO3cmMImbnAQEoKSQR1XuWqt0Ev4RT3kU4Lz/fgvJjGkR7WUstLg=="], - - "sst-win32-x64": ["sst-win32-x64@4.13.1", "", { "os": "win32", "cpu": "x64" }, "sha512-r+83NMwpe4MLAvkVoOxi1KNs6nIqilFucsTr+VN1PgknsiVBY1emITjSWS2jDJJMuH8xJXL5xJzF/l5rcKWErg=="], - - "sst-win32-x86": ["sst-win32-x86@4.13.1", "", { "os": "win32", "cpu": "none" }, "sha512-YPxBVdac/MsrzwlC6pF0NrrvMcmfdBLYjv7MbzHc5jNh1FQ1WPh6bdWQqgv0KD9EQTNLLEkej0beydgUvcCWJg=="], - "standardwebhooks": ["standardwebhooks@1.1.1", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ=="], "stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="], @@ -4808,10 +4790,6 @@ "npm-package-arg/hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="], - "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], - - "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "oxc-parser/@oxc-project/types": ["@oxc-project/types@0.141.0", "", {}, "sha512-S4as7z0j0xQkXcJlyY5ehntwK8/wRkQb9Cyqw+J/N2rkWGQGK0SxD6X6DhQTc7qsxVTBxXbxZtBJh3mr3PtIzQ=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], @@ -4862,10 +4840,6 @@ "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "sst/aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="], - - "sst/jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], - "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -5162,8 +5136,6 @@ "nitro/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.14", "", { "os": "win32", "cpu": "x64" }, "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ=="], - "openid-client/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], "rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], diff --git a/infra/stage.ts b/infra/stage.ts deleted file mode 100644 index 885c880ce26..00000000000 --- a/infra/stage.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const domain = (() => { - if ($app.stage === "production") return "opencode.ai" - if ($app.stage === "dev") return "dev.opencode.ai" - return `${$app.stage}.dev.opencode.ai` -})() - -export const zoneID = "430ba34c138cfb5360826c4909f99be8" -export const awsStage = $app.stage === "production" ? "production" : "dev" -export const deployStats = $app.stage === awsStage diff --git a/infra/stats.ts b/infra/stats.ts deleted file mode 100644 index b348de94b3e..00000000000 --- a/infra/stats.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { execFileSync } from "node:child_process" -import { readdirSync } from "node:fs" -import path from "node:path" -import { LOCALES } from "../packages/stats/app/src/lib/language" -import { domain, zoneID } from "./stage" - -//////////////// -// DATABASE -//////////////// - -const cluster = planetscale.getDatabaseOutput({ - name: "opencode-stats", - organization: "anomalyco", -}) - -const branch = - $app.stage === "production" - ? planetscale.getBranchOutput({ - name: "production", - organization: cluster.organization, - database: cluster.name, - }) - : new planetscale.Branch("StatsDatabaseBranch", { - database: cluster.name, - organization: cluster.organization, - name: $app.stage, - parentBranch: "production", - }) - -const password = new planetscale.Password("StatsDatabasePassword", { - name: $app.stage, - database: cluster.name, - organization: cluster.organization, - branch: branch.name, -}) - -const databaseUrl = $interpolate`mysql://${password.username.apply(encodeURIComponent)}:${password.plaintext.apply( - encodeURIComponent, -)}@${password.accessHostUrl}/${cluster.name}` - -export const database = new sst.Linkable("StatsDatabase", { - properties: { - host: password.accessHostUrl, - database: cluster.name, - username: password.username, - password: password.plaintext, - port: 3306, - url: databaseUrl, - }, -}) - -new sst.x.DevCommand("StatsStudio", { - link: [database], - environment: { - DATABASE_URL: databaseUrl, - }, - dev: { - command: "bun db:studio", - directory: "packages/stats/core", - autostart: false, - }, -}) - -//////////////// -// APP -//////////////// - -export const publicUrl = `https://${domain}/data` -const accountId = "15d29c8639fd3733b1b5486a2acfd968" - -// The private Worker that renders the pages' Open Graph images (packages/stats/app/src/og/worker.ts). Its Wasm and font -// modules can't ship in the Stats Worker, which SST uploads as one file, so it is its own Worker with no URL or route, -// reached only through the Stats Worker's OG_WORKER Service Binding. It is built here, before any resource reads its -// files. `sst dev` deploys no Worker. -const ogWorker = $dev ? undefined : createOgWorker() - -const app = new sst.cloudflare.x.SolidStart("Stats", { - path: "packages/stats/app", - buildCommand: "bun run build", - link: [database], - environment: { - PUBLIC_URL: publicUrl, - }, - transform: { - server: (args) => { - args.url = false - if (!ogWorker) return - args.transform = { - worker: (worker) => { - // The deployment's script name, so the binding waits for the OG Worker to have a live version. - worker.bindings = $util - .all([$output(worker.bindings ?? []), ogWorker.scriptName]) - .apply(([bindings, service]) => [...bindings, { type: "service", name: "OG_WORKER", service }]) - }, - } - }, - }, -}) - -function createOgWorker() { - const root = path.join($cli.paths.root, "packages/stats/app") - execFileSync("bun", ["run", "build:og"], { cwd: root, stdio: "inherit" }) - const dir = path.join(root, ".og-worker") - const types: Record = { - ".mjs": "application/javascript+module", - ".wasm": "application/wasm", - ".bin": "application/octet-stream", - } - const worker = new cloudflare.Worker("StatsOg", { - accountId, - name: `${$app.name}-${$app.stage}-stats-og`, - subdomain: { enabled: false, previewsEnabled: false }, - observability: { enabled: true }, - }) - const version = new cloudflare.WorkerVersion("StatsOgVersion", { - accountId, - workerId: worker.id, - mainModule: "index.mjs", - compatibilityDate: "2026-07-01", - modules: readdirSync(dir).map((name) => ({ - name, - contentFile: path.join(dir, name), - contentType: types[path.extname(name)], - })), - }) - return new cloudflare.WorkersDeployment("StatsOgDeployment", { - accountId, - scriptName: worker.name, - strategy: "percentage", - versions: [{ percentage: 100, versionId: version.id }], - }) -} - -// The site serves itself under /data and //data, and these routes are its only way in: it has no custom domain -// or workers.dev URL. An exact `/data` pattern misses `/data?query`, so each pattern is a `/data*` prefix. Route patterns -// can't wildcard the middle of a path, hence one per locale. `sst dev` deploys no Worker. -const appServer = app.nodes.server -if (appServer) { - for (const prefix of ["data", ...LOCALES.map((locale) => `${locale}/data`)]) { - new cloudflare.WorkersRoute(`StatsRoute${prefix.replaceAll("/", "")}`, { - zoneId: zoneID, - pattern: `${domain}/${prefix}*`, - script: appServer.nodes.worker.scriptName, - }) - } -} - -//////////////// -// SERVICES -//////////////// - -const statsSyncConfig = new sst.Linkable("StatsSyncConfig", { - properties: { - dataset: "zen", - }, -}) - -// Comma-separated model IDs kept off the public stats site. A secret, so excluding -// an unreleased model does not publish its name in git. -const statsHiddenModels = new sst.Secret("StatsHiddenModels", "") - -const r2SqlAuthToken = new sst.Secret("R2SqlAuthToken") -const r2Sql = new sst.Linkable("R2Sql", { - properties: { - accountId, - bucket: `platform-${$app.stage}-lake`, - namespace: "inference", - table: "generation", - }, -}) - -// One instance per run: a plan step, then one step per stat period and retention -// cohort. See packages/stats/README.md for triggering a run by hand. -export const statsSyncWorkflow = new sst.cloudflare.Workflow("StatsSyncWorkflow", { - handler: "packages/stats/server/src/workflow.ts", - className: "StatsSyncWorkflow", - link: [database, r2Sql, r2SqlAuthToken, statsHiddenModels, statsSyncConfig], - transform: { - worker: { - transform: { - worker: (args) => { - // Raise CPU time from the 30-second default to the 5-minute maximum for - // ranking a full week of geo rows. - args.limits = { cpuMs: 300_000 } - args.observability = { enabled: true } - }, - }, - }, - }, -}) - -// Schedules must match packages/stats/server/src/schedule.ts, which maps the -// daily one to a history run. -if (!$dev) - new sst.cloudflare.Cron("StatsSyncCron", { - schedules: ["5 * * * *", "20 3 * * *"], - worker: { - handler: "packages/stats/server/src/cron.ts", - link: [statsSyncWorkflow], - transform: { - worker: (args) => { - args.observability = { enabled: true } - }, - }, - }, - }) - -new sst.x.DevCommand("StatsSync", { - link: [database, r2Sql, r2SqlAuthToken, statsHiddenModels, statsSyncConfig], - dev: { - command: "bun src/sync.ts", - directory: "packages/stats/server", - autostart: false, - }, -}) diff --git a/infra/webapp.ts b/infra/webapp.ts deleted file mode 100644 index ed0ef7914f1..00000000000 --- a/infra/webapp.ts +++ /dev/null @@ -1,21 +0,0 @@ -export function createWebApp(domain: string) { - // Workers Static Assets uploads files as-is, up to 25 MiB each. StaticSite stored them base64-encoded in KV, which - // pushed the 20 MB Word preview engine past KV's 25 MiB value limit. - return new sst.cloudflare.StaticSiteV2("WebApp", { - domain, - path: "packages/app", - environment: - $app.stage === "beta" - ? { - OPENCODE_CHANNEL: "beta", - VITE_OPENCODE_SERVER_MODE: "none", - VITE_SENTRY_ENVIRONMENT: "beta", - } - : undefined, - build: { - // Preserve Sentry credentials and run source-map uploads on every deployment. - command: "bun run build", - output: "./dist", - }, - }) -} diff --git a/package.json b/package.json index b4db56ea5c0..afe66bf1f87 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "dev:vite:live": "sh -c 'OPENCODE_TUI_CHANNEL=dev OPENCODE_PASSWORD=\"$(opencode service get password)\" exec bun run dev:vite \"$@\" --server \"$(opencode service status)\"' --", "dev:desktop": "bun --cwd packages/desktop dev", "dev:web": "bun --cwd packages/app dev", - "dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev", "dev:site": "bun run --cwd services/site dev", "lint": "oxlint && ast-grep scan -c script/ast-grep/gui-plugins/sgconfig.yml && bun script/sdk-docs.ts", "lint:changed": "bun script/lint-changed.ts", @@ -91,7 +90,6 @@ "@typescript/native": "npm:typescript@7.1.0-beta", "zod": "4.6.7", "remeda": "2.26.0", - "sst": "4.13.1", "shiki": "4.4.3", "solid-list": "0.3.0", "string-width": "7.2.0", @@ -123,7 +121,6 @@ "oxlint": "1.60.0", "prettier": "3.6.2", "semver": "^7.6.0", - "sst": "catalog:", "turbo": "2.10.2" }, "dependencies": { diff --git a/packages/app/README.md b/packages/app/README.md index 90951727d6d..32f8139af19 100644 --- a/packages/app/README.md +++ b/packages/app/README.md @@ -69,24 +69,13 @@ Environment options: - `PLAYWRIGHT_PORT` (managed dev or static server port, default: `3000`) - `PLAYWRIGHT_BASE_URL` (use an externally managed app instead of starting a server; otherwise defaults to `http://127.0.0.1:`) -## Deployment - -The `deploy` GitHub Actions workflow uses SST to deploy the web app to `beta.opencode.ai` on every push to the -`beta` branch in `anomalyco/opencode`. The `dev` and `production` stages do not deploy the web app. - -`beta` is the team's dogfooding branch: `dev` merged in by hand, plus commits pushed directly to it. Do not rebase or -force-push it. Each push also runs the `release` workflow, which ships the same commit as the OpenCode Beta desktop app -(see `packages/desktop/README.md`). - -The beta SST stage deploys only the web app, using the `WebApp` StaticSiteV2 definition in `infra/webapp.ts`. It sets -the build channel and Sentry environment to `beta` without deploying the API, console, database, or billing -infrastructure. +## Server mode `VITE_OPENCODE_SERVER_MODE` controls which server the web build provides at startup: | Mode | Initial server | | ------------------ | ------------------------------------------------------------------------------------ | -| `none` | No initial server. The beta deployment uses this mode. | +| `none` | No initial server. | | `origin` (default) | The current page's origin. CLI builds explicitly use this mode for `opencode serve`. | In Vite development mode, `origin` uses `VITE_OPENCODE_SERVER_HOST` / `VITE_OPENCODE_SERVER_PORT` @@ -116,11 +105,3 @@ HTTPS-to-HTTP error instead of the generic connection failure. HTTP servers on ` browser local-network permissions can allow some HTTP LAN connections. QR scanning is enabled only in a browser-reported secure context with camera support and an available video input; insecure pages and unavailable cameras show an explanation beside the disabled action. - -The workflow reuses the repository's `CLOUDFLARE_API_TOKEN` and web Sentry settings. The Cloudflare token -must cover SST's R2 state storage, KV assets, Workers, and custom-domain management in the account that -owns `opencode.ai`. The beta GitHub environment must allow deployments from the `beta` branch; it does not -need AWS credentials. - -SST manages the beta site's custom domain. The first deployment creates its DNS record and TLS certificate. -Do not create a CNAME for `beta.opencode.ai` first, because it would conflict with the Workers custom domain. diff --git a/packages/core/sst-env.d.ts b/packages/core/sst-env.d.ts deleted file mode 100644 index 64441936d7a..00000000000 --- a/packages/core/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/packages/desktop/README.md b/packages/desktop/README.md index 1812e100678..328bc8fcdba 100644 --- a/packages/desktop/README.md +++ b/packages/desktop/README.md @@ -37,7 +37,7 @@ default. `bun dev --download-server ` instead downloads that CLI versio ## Beta channel Every push to the `beta` branch publishes OpenCode Beta (`ai.opencode.desktop.beta`) as version `0.0.0-beta-` to -the `beta` update channel. Beta publishes only the desktop app and `beta.opencode.ai`; the CLI and SDK packages ship +the `beta` update channel. Beta publishes only the desktop app; the CLI and SDK packages ship from `dev`. Install it from `https://opencode.ai/update/api/beta/desktop/opencode/`, for example `opencode-desktop-mac-arm64.dmg` or `opencode-desktop-win-x64.exe`. Installed builds update in place on every platform. diff --git a/packages/effect-drizzle-sqlite/sst-env.d.ts b/packages/effect-drizzle-sqlite/sst-env.d.ts deleted file mode 100644 index 64441936d7a..00000000000 --- a/packages/effect-drizzle-sqlite/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/packages/http-recorder/sst-env.d.ts b/packages/http-recorder/sst-env.d.ts deleted file mode 100644 index f25b9714550..00000000000 --- a/packages/http-recorder/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} diff --git a/packages/plugin/sst-env.d.ts b/packages/plugin/sst-env.d.ts deleted file mode 100644 index 64441936d7a..00000000000 --- a/packages/plugin/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/packages/httpapi-codegen/sst-env.d.ts b/packages/plugins/figma/sst-env.d.ts similarity index 80% rename from packages/httpapi-codegen/sst-env.d.ts rename to packages/plugins/figma/sst-env.d.ts index 64441936d7a..301538ccb21 100644 --- a/packages/httpapi-codegen/sst-env.d.ts +++ b/packages/plugins/figma/sst-env.d.ts @@ -4,7 +4,7 @@ /* deno-fmt-ignore-file */ /* biome-ignore-all lint: auto-generated */ -/// +/// import "sst" export {} \ No newline at end of file diff --git a/packages/desktop/sst-env.d.ts b/packages/process-daemon/npm/sst-env.d.ts similarity index 80% rename from packages/desktop/sst-env.d.ts rename to packages/process-daemon/npm/sst-env.d.ts index 64441936d7a..301538ccb21 100644 --- a/packages/desktop/sst-env.d.ts +++ b/packages/process-daemon/npm/sst-env.d.ts @@ -4,7 +4,7 @@ /* deno-fmt-ignore-file */ /* biome-ignore-all lint: auto-generated */ -/// +/// import "sst" export {} \ No newline at end of file diff --git a/packages/protocol/sst-env.d.ts b/packages/protocol/sst-env.d.ts deleted file mode 100644 index 64441936d7a..00000000000 --- a/packages/protocol/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/packages/schema/sst-env.d.ts b/packages/schema/sst-env.d.ts deleted file mode 100644 index 64441936d7a..00000000000 --- a/packages/schema/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/packages/script/sst-env.d.ts b/packages/script/sst-env.d.ts deleted file mode 100644 index 64441936d7a..00000000000 --- a/packages/script/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/packages/sdk/sst-env.d.ts b/packages/sdk/sst-env.d.ts deleted file mode 100644 index 64441936d7a..00000000000 --- a/packages/sdk/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/packages/server/sst-env.d.ts b/packages/server/sst-env.d.ts deleted file mode 100644 index 64441936d7a..00000000000 --- a/packages/server/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/packages/session-ui/sst-env.d.ts b/packages/session-ui/sst-env.d.ts deleted file mode 100644 index 64441936d7a..00000000000 --- a/packages/session-ui/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/packages/stats/AGENTS.md b/packages/stats/AGENTS.md index 409271cf09a..a52cfa50a9c 100644 --- a/packages/stats/AGENTS.md +++ b/packages/stats/AGENTS.md @@ -1 +1 @@ -To start the stats site locally, run `bun dev:stats` from the repo root. +To start the stats site locally, run `bun run --cwd packages/stats/app dev` with the database variables in README.md exported. diff --git a/packages/stats/README.md b/packages/stats/README.md index fc5691ac2e3..18d1dde8950 100644 --- a/packages/stats/README.md +++ b/packages/stats/README.md @@ -6,40 +6,43 @@ Stats is a separate site from the console. Runtime, database, and domain service - `app`: SolidStart frontend/site. - `core`: Effect services, app config, Drizzle schema/migrations, and stats domains. -- `server`: the `StatsSyncWorkflow` Workflow, the `StatsSyncCron` Worker that starts it, and a local sync script. +- `server`: the `StatsSyncWorkflow` Workflow, the cron handler that starts it, and a local sync script. + +`services/data` builds and deploys them as the `opencode-data-`, `opencode-data-og-` and `opencode-data-sync-` Workers. ## Commands -- `bun run dev:stats` from the repo root starts the SolidStart app. +- `bun run --cwd packages/stats/app dev` starts the SolidStart app (see Stats sync for its environment). - `bun run --cwd packages/stats/app typecheck` typechecks the site. - `bun run --cwd packages/stats/core typecheck` typechecks the Effect/database package. - `bun run --cwd packages/stats/server typecheck` typechecks the sync Workers. ## Stats sync -The sync reads aggregates from R2 SQL and writes them to the stats PlanetScale database. It runs on the `dev` and `production` stages as a Cloudflare Workflow, `StatsSyncWorkflow`, defined in `infra/stats.ts`. +The sync reads aggregates from R2 SQL and writes them to the stats PlanetScale database. It runs on the `dev` and `production` stages as a Cloudflare Workflow, `StatsSyncWorkflow`, in the `opencode-data-sync-` Worker that `services/data` deploys. Each run has a `plan` step, then one step per stat period (`week 2026-W45`, `day 2026-11-03`) and one per retention cohort (`retention 2026-10-05`). The plan snapshots the model catalog once, so every step attributes models the same way. Steps run one at a time and retry twice with exponential backoff. A rejected query (HTTP 400, 401 or 403) or a result over the 10,000-row limit fails its step after one attempt, since a retry repeats the same R2 SQL spend. A failed step does not stop the others, but the run ends as errored and logs each failed step with its error. Because queries run per period, expect an hourly run to take about 15 to 25 minutes and a history run about 2 to 3.5 hours. These are estimates until measured on a deployed stage. -The `StatsSyncCron` Worker starts runs on two schedules: +The same Worker's cron handler starts runs on two schedules: - `5 * * * *`: an hourly run of the current ISO week and the most recent complete retention cohort, with instance ID `stats-hourly-YYYY-MM-DDTHH`. - `20 3 * * *`: a daily history run of the rest of the 56-day display window, with instance ID `stats-history-YYYY-MM-DD`. -Times and IDs are in UTC. Before starting a run, the Cron checks this slot's ID and the recent IDs of the same kind: the five previous hours for an hourly run, the two previous days for a history run. It skips the run if any of them is queued, running, waiting or paused, or if this slot's instance already exists, and logs the reason. Hourly and history runs can overlap, since they cover different periods and their writes are idempotent. +Times and IDs are in UTC. Before starting a run, the cron handler checks this slot's ID and the recent IDs of the same kind: the five previous hours for an hourly run, the two previous days for a history run. It skips the run if any of them is queued, running, waiting or paused, or if this slot's instance already exists, and logs the reason. Hourly and history runs can overlap, since they cover different periods and their writes are idempotent. -To start a run by hand, find the Workflow's name with `bunx wrangler workflows list` (it is `opencode--statssyncworkflowworkflow-`), then trigger it with an ID from the Cron's convention so scheduled runs of the same kind see it and wait: +To start a run by hand, trigger the `opencode-data-sync-` Workflow with an ID from the cron handler's convention so scheduled runs of the same kind see it and wait: ```sh -bunx wrangler workflows trigger '{"history":true}' --id stats-history-2026-11-03 +cf workflows instances create opencode-data-sync-dev --instance-id stats-history-2026-11-03 --params '{"history":true}' +cf workflows instances get stats-history-2026-11-03 --workflow-name opencode-data-sync-dev ``` Omit `history` for an hourly run. If the ID already exists, that slot has run; wait for the next one. You can also trigger a run from the Workflow's page in the Cloudflare dashboard; without params it runs as an hourly pass. -To run one sync pass on your machine against a stage's resources, run `bun sst shell --stage= -- bun run --cwd packages/stats/server sync`, adding `--history` for a history pass. `sst dev` also offers it as the `StatsSync` command. +To run one sync pass on your machine against a stage's resources, export `STAGE`, `DATA_DATABASE_HOST`, `DATA_DATABASE_USERNAME`, `DATA_DATABASE_PASSWORD`, `R2_SQL_AUTH_TOKEN` and `DATA_HIDDEN_MODELS` (AWS Secrets Manager `opencode//*`; `packages/stats/core/src/resource.ts` reads them), then run `bun run --cwd packages/stats/server sync`, adding `--history` for a history pass. `bun run --cwd packages/stats/app dev` reads the same database variables. ## Public page caching Stats HTML, JSON, and Markdown use a one-minute browser cache and a five-minute shared cache. Expired responses must revalidate instead of serving stale snapshots. -The Worker has no custom domain. `infra/stats.ts` routes `/data*` and `//data*` on the main host straight to it, so its responses skip the Cloudflare cache and reach browsers with these headers unchanged. Static assets are uploaded under `/data`, and the build moves Nitro's `_headers` to the assets root so they stay immutable. +The Worker has no custom domain. `services/data/cloudflare.config.ts` routes `/data*` and `//data*` on the main host straight to it, so its responses skip the Cloudflare cache and reach browsers with these headers unchanged. Static assets are uploaded under `/data`, and the build moves Nitro's `_headers` to the assets root so they stay immutable. diff --git a/packages/stats/app/package.json b/packages/stats/app/package.json index e3fc58cf425..ea99301b4be 100644 --- a/packages/stats/app/package.json +++ b/packages/stats/app/package.json @@ -35,7 +35,6 @@ "nitro": "3.0.260903-beta", "satori": "0.63.2", "solid-js": "catalog:", - "sst": "catalog:", "vite": "8.2.4" }, "devDependencies": { diff --git a/packages/stats/app/script/build-og-worker.ts b/packages/stats/app/script/build-og-worker.ts index 966454cd9d9..7e6dfe749de 100644 --- a/packages/stats/app/script/build-og-worker.ts +++ b/packages/stats/app/script/build-og-worker.ts @@ -1,5 +1,5 @@ -// Builds the StatsOg Worker (src/og/worker.ts) into .og-worker: the bundle, the Wasm engines, and the site's OpenTUI -// Mono fonts as TrueType (Satori can't read WOFF2). infra/stats.ts runs this and uploads every file there. It is +// Builds the OG Worker (src/og/worker.ts) into .og-worker: the bundle, the Wasm engines, and the site's OpenTUI +// Mono fonts as TrueType (Satori can't read WOFF2). services/data/script/build.ts runs this and deploys every file there. It is // outside .output because the site's own build replaces that directory. test/og.test.ts builds it too, so PR CI // catches a broken build before the deploy does. import { mkdir, rm } from "node:fs/promises" diff --git a/packages/stats/app/src/lib/noindex.ts b/packages/stats/app/src/lib/noindex.ts index fcb86e4e094..0d1639855f1 100644 --- a/packages/stats/app/src/lib/noindex.ts +++ b/packages/stats/app/src/lib/noindex.ts @@ -1,5 +1,5 @@ import type { createMiddleware } from "@solidjs/start/middleware" -import { Resource } from "sst/resource" +import { Resource } from "@opencode/stats-core/resource" // Only production may be indexed: dev.opencode.ai and the personal stages run unreleased builds, and // search engines must never list them in place of opencode.ai. Every response there asks them not @@ -9,12 +9,12 @@ import { Resource } from "sst/resource" type Middleware = ReturnType[number] /** - * The headers every response from this stage carries: none in production, or outside SST (`vite dev` on its own), - * where no App is linked. SST links the App to the build as well as the Worker. + * The headers every response from this stage carries: none in production, or when STAGE is unset (`vite dev` on its + * own). services/data sets STAGE for the build as well as on the Worker. */ export function stageHeaders(): Record { - const stage = "App" in Resource ? Resource.App.stage : "production" - return stage === "production" ? {} : { "X-Robots-Tag": "noindex, nofollow" } + if (!process.env.STAGE) return {} + return Resource.App.stage === "production" ? {} : { "X-Robots-Tag": "noindex, nofollow" } } // h3 adds the prepared headers to whatever a route returns, and the error headers to a status of 400 or more. diff --git a/packages/stats/app/src/lib/resource.cloudflare.ts b/packages/stats/app/src/lib/resource.cloudflare.ts index 3e5eea6fce2..0d8de1683a7 100644 --- a/packages/stats/app/src/lib/resource.cloudflare.ts +++ b/packages/stats/app/src/lib/resource.cloudflare.ts @@ -8,7 +8,7 @@ export { waitUntil } from "cloudflare:workers" type Fetcher = { fetch(request: Request): Promise } const bindings = env as { OG_WORKER: Fetcher; ASSETS: Fetcher } -// The private StatsOg Worker renders cards (infra/stats.ts, src/og/worker.ts). +// The private OG Worker renders cards (services/data/og.config.ts, src/og/worker.ts). export async function renderOgCard(card: OgCard) { const response = await bindings.OG_WORKER.fetch( new Request("https://og/render", { method: "POST", body: JSON.stringify(card) }), diff --git a/packages/stats/app/src/og/worker.ts b/packages/stats/app/src/og/worker.ts index ababff72915..18283d7191f 100644 --- a/packages/stats/app/src/og/worker.ts +++ b/packages/stats/app/src/og/worker.ts @@ -1,6 +1,6 @@ -// The private StatsOg Worker (infra/stats.ts). The Stats Worker posts a card to it over a Service Binding and gets a -// PNG back. It is its own Worker because its Wasm and font modules can't ship in the Stats Worker's single-file -// bundle. script/build-og-worker.ts bundles it and writes the modules imported here next to it. +// The private OG Worker (services/data/og.config.ts). The data Worker posts a card to it over a Service Binding and gets a +// PNG back. It is its own Worker so the data Worker doesn't load its Wasm engines and fonts. +// script/build-og-worker.ts bundles it and writes the modules imported here next to it. import regularLatin from "./opentui-mono-regular-latin.bin" import regularRest from "./opentui-mono-regular-rest.bin" import boldLatin from "./opentui-mono-bold-latin.bin" diff --git a/packages/stats/app/tsconfig.json b/packages/stats/app/tsconfig.json index 080f8a656bc..a55c090bb90 100644 --- a/packages/stats/app/tsconfig.json +++ b/packages/stats/app/tsconfig.json @@ -19,5 +19,5 @@ "~/*": ["./src/*"] } }, - "include": ["*.ts", "src", "../core/src/resource.d.ts"] + "include": ["*.ts", "src"] } diff --git a/packages/stats/core/drizzle.config.ts b/packages/stats/core/drizzle.config.ts index f94f86cb93a..d7ec5d75a50 100644 --- a/packages/stats/core/drizzle.config.ts +++ b/packages/stats/core/drizzle.config.ts @@ -1,4 +1,4 @@ -import { Resource } from "sst/resource" +import { Resource } from "./src/resource" import { defineConfig } from "drizzle-kit" export default defineConfig({ diff --git a/packages/stats/core/package.json b/packages/stats/core/package.json index f8ea45b3a9b..8c209003ac1 100644 --- a/packages/stats/core/package.json +++ b/packages/stats/core/package.json @@ -12,6 +12,7 @@ "./database/*": "./src/database/*.ts", "./domain/*": "./src/domain/*.ts", "./r2-sql": "./src/r2-sql.ts", + "./resource": "./src/resource.ts", "./runtime": "./src/runtime.ts", "./stat-sync": "./src/stat-sync.ts", "./sync-range": "./src/sync-range.ts" @@ -31,8 +32,7 @@ "dependencies": { "@planetscale/database": "1.19.0", "drizzle-orm": "catalog:", - "effect": "catalog:", - "sst": "catalog:" + "effect": "catalog:" }, "devDependencies": { "@tsconfig/node22": "catalog:", diff --git a/packages/stats/core/src/config.ts b/packages/stats/core/src/config.ts index c30a01d50bd..277749af77d 100644 --- a/packages/stats/core/src/config.ts +++ b/packages/stats/core/src/config.ts @@ -1,6 +1,6 @@ import { Config, ConfigProvider, Effect, Layer, Schema } from "effect" import * as Context from "effect/Context" -import { Resource } from "sst/resource" +import { Resource } from "./resource" export class AppConfigValue extends Schema.Class("AppConfigValue")({ stage: Schema.NonEmptyString, diff --git a/packages/stats/core/src/database.ts b/packages/stats/core/src/database.ts index ae1f23183c6..1a64980d804 100644 --- a/packages/stats/core/src/database.ts +++ b/packages/stats/core/src/database.ts @@ -2,7 +2,7 @@ import { Client } from "@planetscale/database" import { drizzle } from "drizzle-orm/planetscale-serverless" import { Config, ConfigProvider, Effect, Layer, Option, Schema } from "effect" import * as Context from "effect/Context" -import { Resource } from "sst/resource" +import { Resource } from "./resource" export const DatabaseUrl = Schema.NonEmptyString.pipe(Schema.brand("DatabaseUrl")) export type DatabaseUrl = typeof DatabaseUrl.Type diff --git a/packages/stats/core/src/domain/home.test.ts b/packages/stats/core/src/domain/home.test.ts index 7a480a29021..2e98860bc66 100644 --- a/packages/stats/core/src/domain/home.test.ts +++ b/packages/stats/core/src/domain/home.test.ts @@ -2,8 +2,9 @@ import { describe, expect, test } from "bun:test" import type { ModelStatMetric } from "./model" import type { RetentionMetricRow } from "./home" -process.env.SST_RESOURCE_App = JSON.stringify({ name: "opencode", stage: "test" }) -process.env.SST_RESOURCE_StatsDatabase = JSON.stringify({ url: "mysql://localhost/stats" }) +process.env.DATA_DATABASE_HOST = "localhost" +process.env.DATA_DATABASE_USERNAME = "stats" +process.env.DATA_DATABASE_PASSWORD = "stats" const { buildRetentionEntries, buildStatsHomeData, normalizeStatRows } = await import("./home") diff --git a/packages/stats/core/src/domain/home.ts b/packages/stats/core/src/domain/home.ts index fc278e75046..4f4a3e31cb8 100644 --- a/packages/stats/core/src/domain/home.ts +++ b/packages/stats/core/src/domain/home.ts @@ -1,6 +1,6 @@ import { Client } from "@planetscale/database" import { Effect } from "effect" -import { Resource } from "sst/resource" +import { Resource } from "../resource" import type { ModelStatMetric } from "./model" import { RETIRED_STAT_PROVIDERS, statProvider } from "./model-normalization" import { isMissingRetentionTable } from "./retention" diff --git a/packages/stats/core/src/domain/inference.ts b/packages/stats/core/src/domain/inference.ts index 912169a471f..bab176515a0 100644 --- a/packages/stats/core/src/domain/inference.ts +++ b/packages/stats/core/src/domain/inference.ts @@ -1,4 +1,4 @@ -import { Resource } from "sst/resource" +import { Resource } from "../resource" import type { R2SqlData } from "../r2-sql" import type { CatalogIdentity } from "./catalog-identity" import type { GeoStatAggregate } from "./geo" diff --git a/packages/stats/core/src/ensure-unique-users.ts b/packages/stats/core/src/ensure-unique-users.ts index 0786476281e..ff7373f07bd 100644 --- a/packages/stats/core/src/ensure-unique-users.ts +++ b/packages/stats/core/src/ensure-unique-users.ts @@ -1,5 +1,5 @@ import { Client } from "@planetscale/database" -import { Resource } from "sst/resource" +import { Resource } from "./resource" const tables = ["geo_stat", "model_stat", "provider_stat"] as const const checkOnly = process.argv.includes("--check") diff --git a/packages/stats/core/src/r2-sql.ts b/packages/stats/core/src/r2-sql.ts index 0afd7c15614..061d7290768 100644 --- a/packages/stats/core/src/r2-sql.ts +++ b/packages/stats/core/src/r2-sql.ts @@ -1,5 +1,5 @@ import { Context, Effect, Layer, Schedule, Schema } from "effect" -import { Resource } from "sst/resource" +import { Resource } from "./resource" const R2_SQL_MAX_ROWS = 10_000 const R2_SQL_TIMEOUT_MS = 10 * 60_000 diff --git a/packages/stats/core/src/resource.d.ts b/packages/stats/core/src/resource.d.ts deleted file mode 100644 index e66f8714ba7..00000000000 --- a/packages/stats/core/src/resource.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -import "sst/resource" - -declare module "sst/resource" { - export interface Resource { - R2Sql: { - accountId: string - bucket: string - namespace: string - table: string - type: "sst.sst.Linkable" - } - R2SqlAuthToken: { - type: "sst.sst.Secret" - value: string - } - StatsHiddenModels: { - type: "sst.sst.Secret" - value: string - } - StatsSyncConfig: { - dataset: string - type: "sst.sst.Linkable" - } - StatsDatabase: { - database: string - host: string - password: string - port: number - type: "sst.sst.Linkable" - url: string - username: string - } - } -} diff --git a/packages/stats/core/src/resource.ts b/packages/stats/core/src/resource.ts new file mode 100644 index 00000000000..736b3cc52a3 --- /dev/null +++ b/packages/stats/core/src/resource.ts @@ -0,0 +1,61 @@ +// The stage's resources, read from the environment. Deployed Workers (services/data) get them as bindings, which +// nodejs_compat copies into process.env; scripts on your machine read your exported variables. Each value is read +// when used, so importing this module never touches the environment. +const ACCOUNT_ID = "15d29c8639fd3733b1b5486a2acfd968" + +function required(name: string) { + const value = process.env[name] + if (!value) throw new Error(`${name} is not set`) + return value +} + +function stage() { + return process.env.STAGE === "production" ? "production" : "dev" +} + +export const Resource = { + App: { + get stage() { + return stage() + }, + }, + StatsDatabase: { + database: "opencode-stats", + port: 3306, + get host() { + return required("DATA_DATABASE_HOST") + }, + get username() { + return required("DATA_DATABASE_USERNAME") + }, + get password() { + return required("DATA_DATABASE_PASSWORD") + }, + get url() { + return `mysql://${encodeURIComponent(this.username)}:${encodeURIComponent(this.password)}@${this.host}/${this.database}` + }, + }, + R2Sql: { + accountId: ACCOUNT_ID, + namespace: "inference", + table: "generation", + get bucket() { + return `platform-${stage()}-lake` + }, + }, + R2SqlAuthToken: { + get value() { + return required("R2_SQL_AUTH_TOKEN") + }, + }, + // Comma-separated model IDs kept off the public stats site. A secret, so excluding an unreleased model does not + // publish its name in git. + StatsHiddenModels: { + get value() { + return process.env.DATA_HIDDEN_MODELS ?? "" + }, + }, + StatsSyncConfig: { + dataset: "zen", + }, +} diff --git a/packages/stats/core/src/stat-sync.test.ts b/packages/stats/core/src/stat-sync.test.ts index ed48809d6fe..d1edd30131f 100644 --- a/packages/stats/core/src/stat-sync.test.ts +++ b/packages/stats/core/src/stat-sync.test.ts @@ -4,15 +4,7 @@ import { catalogIdentity, type CatalogSnapshot } from "./domain/catalog-identity import type { StatPeriodScope } from "./domain/stat" import type { R2SqlData } from "./r2-sql" -process.env.SST_RESOURCE_App = JSON.stringify({ name: "opencode", stage: "test" }) -process.env.SST_RESOURCE_R2Sql = JSON.stringify({ - accountId: "account", - bucket: "bucket", - namespace: "inference", - table: "generation", -}) -process.env.SST_RESOURCE_StatsSyncConfig = JSON.stringify({ dataset: "zen" }) -process.env.SST_RESOURCE_StatsHiddenModels = JSON.stringify({ value: "secret-model" }) +process.env.DATA_HIDDEN_MODELS = "secret-model" const { GeoStatRepo } = await import("./domain/geo") const { ModelStatRepo } = await import("./domain/model") diff --git a/packages/stats/core/src/stat-sync.ts b/packages/stats/core/src/stat-sync.ts index cde8a5955e3..de321caeaf6 100644 --- a/packages/stats/core/src/stat-sync.ts +++ b/packages/stats/core/src/stat-sync.ts @@ -1,5 +1,5 @@ import { DateTime, Effect } from "effect" -import { Resource } from "sst/resource" +import { Resource } from "./resource" import { DatabaseError } from "./database" import { fromCatalogSnapshot, diff --git a/packages/stats/server/package.json b/packages/stats/server/package.json index 41c7eddb10f..2c0e1de5bb5 100644 --- a/packages/stats/server/package.json +++ b/packages/stats/server/package.json @@ -13,8 +13,7 @@ }, "dependencies": { "@opencode/stats-core": "workspace:*", - "effect": "catalog:", - "sst": "catalog:" + "effect": "catalog:" }, "devDependencies": { "@cloudflare/workers-types": "catalog:", diff --git a/packages/stats/server/src/schedule.ts b/packages/stats/server/src/schedule.ts index 8304efc67c2..5624e242d41 100644 --- a/packages/stats/server/src/schedule.ts +++ b/packages/stats/server/src/schedule.ts @@ -1,4 +1,4 @@ -// Keep in sync with the StatsSyncCron schedules in infra/stats.ts. +// Keep in sync with the schedules in services/data/sync.config.ts. export const HOURLY_SCHEDULE = "5 * * * *" export const HISTORY_SCHEDULE = "20 3 * * *" diff --git a/packages/stats/server/src/worker.ts b/packages/stats/server/src/worker.ts new file mode 100644 index 00000000000..f342e1295c0 --- /dev/null +++ b/packages/stats/server/src/worker.ts @@ -0,0 +1,3 @@ +// The sync Worker (services/data): the StatsSyncWorkflow and the cron handler that starts its runs. +export { StatsSyncWorkflow } from "./workflow" +export { default } from "./cron" diff --git a/packages/stats/server/src/workflow.ts b/packages/stats/server/src/workflow.ts index 4038aa08dba..a3f6772c773 100644 --- a/packages/stats/server/src/workflow.ts +++ b/packages/stats/server/src/workflow.ts @@ -69,10 +69,6 @@ export class StatsSyncWorkflow extends WorkflowEntrypoint(effect: Effect.Effect>) { return Effect.runPromise(effect.pipe(Effect.provide(runtimeLayer))) } diff --git a/packages/stats/server/tsconfig.json b/packages/stats/server/tsconfig.json index b7dd6615d54..bcf505dedad 100644 --- a/packages/stats/server/tsconfig.json +++ b/packages/stats/server/tsconfig.json @@ -9,6 +9,6 @@ "noEmit": true, "types": ["@cloudflare/workers-types", "node"] }, - "include": ["src", "../core/src/resource.d.ts"], + "include": ["src"], "exclude": ["src/**/*.test.ts"] } diff --git a/packages/stories/sst-env.d.ts b/packages/stories/sst-env.d.ts deleted file mode 100644 index 64441936d7a..00000000000 --- a/packages/stories/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/packages/theme/sst-env.d.ts b/packages/theme/sst-env.d.ts deleted file mode 100644 index f25b9714550..00000000000 --- a/packages/theme/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} diff --git a/packages/tui/sst-env.d.ts b/packages/tui/sst-env.d.ts deleted file mode 100644 index 64441936d7a..00000000000 --- a/packages/tui/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/packages/ui/sst-env.d.ts b/packages/ui/sst-env.d.ts deleted file mode 100644 index 64441936d7a..00000000000 --- a/packages/ui/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/sdks/vscode/sst-env.d.ts b/sdks/vscode/sst-env.d.ts deleted file mode 100644 index 64441936d7a..00000000000 --- a/sdks/vscode/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/services/README.md b/services/README.md index fa064818cf5..4d94052f660 100644 --- a/services/README.md +++ b/services/README.md @@ -3,12 +3,13 @@ Independently deployed services supporting OpenCode live here. Runtime packages and clients live in `packages/`. -| Service | Purpose | Deployment | -| ---------------------- | ----------------------------------------- | ---------------------------- | -| [`site`](./site) | The opencode.ai website and V2 docs | `deploy-site.yml`, `cf` | -| [`legacy`](./legacy) | Deprecated: opncd.ai and api.opencode.ai | `deploy.yml`, `cf` | -| [`updates`](./updates) | Release metadata and update selection | `release.yml`, Wrangler | -| [`files`](./files) | Public file distribution at `/files/*` | `deploy-files.yml`, Wrangler | +| Service | Purpose | Deployment | +| ---------------------- | ---------------------------------------- | ---------------------------- | +| [`site`](./site) | The opencode.ai website and V2 docs | `deploy-site.yml`, `cf` | +| [`legacy`](./legacy) | Deprecated: opncd.ai and api.opencode.ai | `deploy.yml`, `cf` | +| [`data`](./data) | The /data site, its OG images and sync | `deploy.yml`, `cf` | +| [`updates`](./updates) | Release metadata and update selection | `release.yml`, Wrangler | +| [`files`](./files) | Public file distribution at `/files/*` | `deploy-files.yml`, Wrangler | These directories are Bun workspaces. Run package commands from each service's directory. Repository-wide Turbo commands discover them through `services/*`. diff --git a/services/data/.gitignore b/services/data/.gitignore new file mode 100644 index 00000000000..c3055062284 --- /dev/null +++ b/services/data/.gitignore @@ -0,0 +1,3 @@ +node_modules +.cloudflare +.wrangler diff --git a/services/data/README.md b/services/data/README.md new file mode 100644 index 00000000000..187ab43c39e --- /dev/null +++ b/services/data/README.md @@ -0,0 +1,24 @@ +# Data + +Deploys the opencode.ai/data site from `packages/stats` as three Workers with `cf`: + +| Worker | Config | What it is | +| ---------------------------- | ---------------------- | ----------------------------------------------------------------------------- | +| `opencode-data-` | `cloudflare.config.ts` | The SolidStart site, on `/data*` and `//data*` of the stage's host | +| `opencode-data-og-` | `og.config.ts` | Open Graph images, reached only through the site's `OG_WORKER` binding | +| `opencode-data-sync-` | `sync.config.ts` | The `opencode-data-sync-` Workflow and the cron handler that starts it | + +`script/build.ts` builds all three into one Build Output tree and `script/deploy.ts` deploys them with +`cf deploy --prebuilt`, the OG Worker first. The `data` job in `.github/workflows/deploy.yml` runs +`deploy:dev` or `deploy:production` and passes the Worker secrets from AWS Secrets Manager +(`opencode//DATA_DATABASE_HOST`, `DATA_DATABASE_USERNAME`, `DATA_DATABASE_PASSWORD`, +`R2_SQL_AUTH_TOKEN`, `DATA_HIDDEN_MODELS`) with `--secrets-file`. Each Worker gets only the secrets +its config declares. + +The database is the `opencode-stats` PlanetScale database: the `dev` branch for dev and `production` +for production, through the `data-worker-` password. It is not managed as code. + +```bash +bun run build --mode dev +bun run typecheck +``` diff --git a/services/data/cloudflare.config.ts b/services/data/cloudflare.config.ts new file mode 100644 index 00000000000..231ce572b36 --- /dev/null +++ b/services/data/cloudflare.config.ts @@ -0,0 +1,42 @@ +import { bindings, defineConfig, triggers } from "cf/config" +import { LOCALES } from "../../packages/stats/app/src/lib/language" +import { ogWorkerName } from "./og.config" + +// The data site (packages/stats/app, SolidStart) at /data and //data on the main host. `--mode dev` deploys +// opencode-data-dev on dev.opencode.ai; any other mode deploys opencode-data-production on opencode.ai. The routes +// are its only way in: it has no custom domain or workers.dev URL. An exact `/data` pattern misses `/data?query`, so +// each pattern is a `/data*` prefix, and route patterns can't wildcard the middle of a path, hence one per locale. +// script/build.ts builds this, og.config.ts and sync.config.ts into one Build Output tree. +export default defineConfig(({ mode }) => { + const dev = mode === "dev" + const stage = dev ? "dev" : "production" + const host = dev ? "dev.opencode.ai" : "opencode.ai" + return { + // Anomaly + accountId: "15d29c8639fd3733b1b5486a2acfd968", + worker: { + name: `opencode-data-${stage}`, + compatibilityDate: "2026-07-25", + // The Nitro server is built with node compatibility, which also copies string bindings into process.env + // (packages/stats/core/src/resource.ts). + compatibilityFlags: ["nodejs_compat"], + env: { + ASSETS: bindings.assets(), + // The private Worker that renders Open Graph images (og.config.ts). + OG_WORKER: bindings.worker({ worker: ogWorkerName(mode) }), + STAGE: bindings.text(stage), + PUBLIC_URL: bindings.text(`https://${host}/data`), + // Set from AWS Secrets Manager (opencode//DATA_*) by the deploy job. + DATA_DATABASE_HOST: bindings.secret(), + DATA_DATABASE_USERNAME: bindings.secret(), + DATA_DATABASE_PASSWORD: bindings.secret(), + }, + triggers: ["data", ...LOCALES.map((locale) => `${locale}/data`)].map((prefix) => + triggers.fetch({ pattern: `${host}/${prefix}*`, zone: "opencode.ai" }), + ), + workersDev: false, + previewUrls: false, + observability: { enabled: true }, + }, + } +}) diff --git a/services/data/og.config.ts b/services/data/og.config.ts new file mode 100644 index 00000000000..a943526d0d4 --- /dev/null +++ b/services/data/og.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "cf/config" + +export const ogWorkerName = (mode: string | undefined) => `opencode-data-og-${mode === "dev" ? "dev" : "production"}` + +// The private Worker that renders the data pages' Open Graph images (packages/stats/app/src/og/worker.ts). It has no +// URL or route; the data Worker reaches it through its OG_WORKER Service Binding. It is its own Worker so the data +// Worker doesn't load the Wasm engines and fonts it ships as modules (script/build.ts). +export default defineConfig(({ mode }) => ({ + // Anomaly + accountId: "15d29c8639fd3733b1b5486a2acfd968", + worker: { + name: ogWorkerName(mode), + compatibilityDate: "2026-07-01", + workersDev: false, + previewUrls: false, + observability: { enabled: true }, + }, +})) diff --git a/services/data/package.json b/services/data/package.json new file mode 100644 index 00000000000..f30669bd7fb --- /dev/null +++ b/services/data/package.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@opencode/data", + "private": true, + "type": "module", + "license": "MIT", + "scripts": { + "build": "bun script/build.ts", + "deploy:dev": "bun script/deploy.ts --mode dev", + "deploy:production": "bun script/deploy.ts --mode production", + "typecheck": "bun check --no-script", + "typecheck:tsc": "tsc --noEmit" + }, + "devDependencies": { + "@cloudflare/build-output-utils": "0.8.5", + "@cloudflare/config": "0.23.0", + "@opencode/stats-app": "workspace:*", + "@opencode/stats-server": "workspace:*", + "@types/bun": "catalog:", + "@typescript/native": "catalog:", + "cf": "1.0.0-beta.12", + "typescript": "catalog:" + } +} diff --git a/services/data/script/build.ts b/services/data/script/build.ts new file mode 100644 index 00000000000..b015d976cdc --- /dev/null +++ b/services/data/script/build.ts @@ -0,0 +1,101 @@ +// Builds the data site's three Workers into one Build Output Specification tree (.cloudflare/output) for +// `cf deploy --prebuilt --worker ` (script/deploy.ts): +// - default: the SolidStart site (packages/stats/app, Nitro's cloudflare-module preset) with .output/public as assets +// - og: the Open Graph image Worker, whose Wasm engines and fonts ship as modules +// - sync: the StatsSyncWorkflow and its cron handler (packages/stats/server) +// cf's own build step can't detect these projects, so they deploy prebuilt. +// +// bun script/build.ts [--mode dev|production] +import { cp, mkdir, readdir } from "node:fs/promises" +import path from "node:path" +import { + cleanBuildOutputDir, + getWorkerBundleDir, + writeAssets, + writeRootConfig, + writeWorkerConfig, +} from "@cloudflare/build-output-utils" +import { resolveAndParseConfig } from "@cloudflare/config" + +const root = path.resolve(import.meta.dir, "..") +const app = path.resolve(root, "../../packages/stats/app") +const server = path.resolve(root, "../../packages/stats/server") +const modeIndex = process.argv.indexOf("--mode") +const mode = modeIndex === -1 ? "production" : process.argv[modeIndex + 1] +if (mode !== "dev" && mode !== "production") throw new Error(`Unknown --mode ${mode}; use dev or production`) +// STAGE picks the stage's headers for the static assets (packages/stats/app/src/lib/noindex.ts). +const env = { ...process.env, STAGE: mode } + +await Bun.$`bun run build`.cwd(app).env(env) +await Bun.$`bun run build:og`.cwd(app).env(env) + +// @cloudflare/config's file loader refuses Bun; it resolves an imported config the same way. +async function resolve(file: string) { + const parsed = await resolveAndParseConfig((await import(file)).default, { isPreview: false, mode }) + if (!parsed.success) throw new Error(`${file}: ${parsed.error.message}`) + if (!parsed.data.worker) throw new Error(`${file} defines no worker`) + return { ...parsed.data, worker: parsed.data.worker } +} +const site = await resolve("../cloudflare.config") +const og = await resolve("../og.config") +const sync = await resolve("../sync.config") + +await cleanBuildOutputDir(root) +await writeRootConfig( + root, + { accountId: site.accountId, complianceRegion: site.complianceRegion }, + { isPreview: false, mode }, +) + +// The site: Nitro's server bundle and public directory. +await writeAssets({ root, sourceDirectory: path.join(app, ".output", "public") }) +await cp(path.join(app, ".output", "server"), getWorkerBundleDir(root), { + recursive: true, + filter: (file) => !file.endsWith("wrangler.json") && !file.endsWith(".map"), +}) +await writeWorkerConfig({ + root, + config: site.worker, + manifest: { type: "partial", mainModule: "index.mjs", modules: {} }, +}) + +// The OG Worker: its bundle plus the Wasm and font modules it imports. +const ogFiles = await readdir(path.join(app, ".og-worker")) +await mkdir(getWorkerBundleDir(root, "og"), { recursive: true }) +await cp(path.join(app, ".og-worker"), getWorkerBundleDir(root, "og"), { recursive: true }) +await writeWorkerConfig({ + root, + directoryName: "og", + config: og.worker, + manifest: { + type: "partial", + mainModule: "index.mjs", + modules: Object.fromEntries( + ogFiles.flatMap((file): [string, { type: "wasm" | "data" }][] => { + if (file.endsWith(".wasm")) return [[file, { type: "wasm" }]] + if (file.endsWith(".bin")) return [[file, { type: "data" }]] + return [] + }), + ), + }, +}) + +// The sync Worker, bundled for workerd. +const result = await Bun.build({ + entrypoints: [path.join(server, "src/worker.ts")], + outdir: getWorkerBundleDir(root, "sync"), + naming: "index.mjs", + target: "browser", + format: "esm", + conditions: ["workerd", "worker", "browser"], + external: ["cloudflare:*", "node:*"], +}) +if (!result.success) throw new AggregateError(result.logs, "The sync Worker failed to build") +await writeWorkerConfig({ + root, + directoryName: "sync", + config: sync.worker, + manifest: { type: "partial", mainModule: "index.mjs", modules: {} }, +}) + +console.log(`data: built ${site.worker.name}, ${og.worker.name} and ${sync.worker.name} (${mode})`) diff --git a/services/data/script/deploy.ts b/services/data/script/deploy.ts new file mode 100644 index 00000000000..3d204e37246 --- /dev/null +++ b/services/data/script/deploy.ts @@ -0,0 +1,49 @@ +// Builds and deploys the data site's Workers: the OG Worker first, since the site binds to it, then the sync Worker, +// then the site. --secrets-file is a JSON object of secret values (the deploy job writes it from AWS Secrets Manager); +// each Worker gets only the secrets its config declares, through a file in the same directory removed afterwards. +// +// bun script/deploy.ts --mode dev|production [--secrets-file ] +import { rm } from "node:fs/promises" +import path from "node:path" +import { resolveAndParseConfig } from "@cloudflare/config" + +const root = path.resolve(import.meta.dir, "..") +const flag = (name: string) => { + const index = process.argv.indexOf(name) + return index === -1 ? undefined : process.argv[index + 1] +} +const mode = flag("--mode") +if (mode !== "dev" && mode !== "production") throw new Error(`Unknown --mode ${mode}; use dev or production`) +const secretsFile = flag("--secrets-file") +const secrets: Record = secretsFile ? await Bun.file(secretsFile).json() : {} + +await Bun.$`bun script/build.ts --mode ${mode}`.cwd(root) + +for (const [directory, file] of [ + ["og", "../og.config"], + ["sync", "../sync.config"], + ["default", "../cloudflare.config"], +] as const) { + const parsed = await resolveAndParseConfig((await import(file)).default, { isPreview: false, mode }) + if (!parsed.success) throw new Error(`${file}: ${parsed.error.message}`) + const names = Object.entries(parsed.data.worker?.env ?? {}) + .filter(([, binding]) => binding.type === "secret") + .map(([name]) => name) + const missing = names.filter((name) => !secrets[name]) + if (secretsFile && missing.length) throw new Error(`${file}: missing secrets ${missing.join(", ")}`) + const name = parsed.data.worker?.name + if (!name) throw new Error(`${file} defines no worker`) + const args = ["--prebuilt", "--mode", mode, "--worker", name] + const scoped = secretsFile && names.length ? `${secretsFile}.${directory}` : undefined + if (scoped) { + await Bun.write(scoped, JSON.stringify(Object.fromEntries(names.map((name) => [name, secrets[name]]))), { + mode: 0o600, + }) + args.push("--secrets-file", scoped) + } + try { + await Bun.$`cf deploy ${args}`.cwd(root) + } finally { + if (scoped) await rm(scoped, { force: true }) + } +} diff --git a/packages/ai/sst-env.d.ts b/services/data/sst-env.d.ts similarity index 100% rename from packages/ai/sst-env.d.ts rename to services/data/sst-env.d.ts diff --git a/services/data/sync.config.ts b/services/data/sync.config.ts new file mode 100644 index 00000000000..81ad132a2ef --- /dev/null +++ b/services/data/sync.config.ts @@ -0,0 +1,39 @@ +import { bindings, defineConfig, exports, triggers } from "cf/config" + +// The Worker that syncs R2 SQL aggregates into the data database (packages/stats/server/src/worker.ts): the +// StatsSyncWorkflow, and the cron handler that starts its runs. The schedules must match +// packages/stats/server/src/schedule.ts, which maps the daily one to a history run. +export default defineConfig(({ mode }) => { + const stage = mode === "dev" ? "dev" : "production" + const name = `opencode-data-sync-${stage}` + return { + // Anomaly + accountId: "15d29c8639fd3733b1b5486a2acfd968", + worker: { + name, + compatibilityDate: "2026-07-25", + // Also copies string bindings into process.env (packages/stats/core/src/resource.ts). + compatibilityFlags: ["nodejs_compat"], + // The 5-minute maximum, for ranking a full week of geo rows. + limits: { cpuMs: 300_000 }, + exports: { + // One instance per run: a plan step, then one step per stat period and retention cohort. + StatsSyncWorkflow: exports.workflow({ name }), + }, + env: { + StatsSyncWorkflow: bindings.workflow({ name, worker: name, exportName: "StatsSyncWorkflow" }), + STAGE: bindings.text(stage), + // Set from AWS Secrets Manager (opencode//*) by the deploy job. + DATA_DATABASE_HOST: bindings.secret(), + DATA_DATABASE_USERNAME: bindings.secret(), + DATA_DATABASE_PASSWORD: bindings.secret(), + R2_SQL_AUTH_TOKEN: bindings.secret(), + DATA_HIDDEN_MODELS: bindings.secret(), + }, + triggers: [triggers.scheduled({ schedule: "5 * * * *" }), triggers.scheduled({ schedule: "20 3 * * *" })], + workersDev: false, + previewUrls: false, + observability: { enabled: true }, + }, + } +}) diff --git a/services/data/tsconfig.json b/services/data/tsconfig.json new file mode 100644 index 00000000000..d2789921db1 --- /dev/null +++ b/services/data/tsconfig.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "types": ["bun"] + }, + "include": ["*.ts", "script"] +} diff --git a/packages/app/sst-env.d.ts b/services/files/sst-env.d.ts similarity index 100% rename from packages/app/sst-env.d.ts rename to services/files/sst-env.d.ts diff --git a/services/legacy/README.md b/services/legacy/README.md index bbbaff6e8fe..8aeb75cda19 100644 --- a/services/legacy/README.md +++ b/services/legacy/README.md @@ -13,9 +13,8 @@ One Worker that answers by hostname: OIDC token for a GitHub App installation token for the V1 GitHub Action (`POST /exchange_github_app_token`, `src/api.ts`). -`cloudflare.config.ts` defines the Worker. `deploy:dev` and `deploy:production` deploy it with `cf`, -after the SST job in `.github/workflows/deploy.yml`, because the SST stack owned these hostnames until -the Worker moved here. The deploy job reads the `GITHUB_APP_ID` and `GITHUB_APP_PRIVATE_KEY` Worker +`cloudflare.config.ts` defines the Worker. `deploy:dev` and `deploy:production` deploy it with `cf` +from `.github/workflows/deploy.yml`. The deploy job reads the `GITHUB_APP_ID` and `GITHUB_APP_PRIVATE_KEY` Worker secrets from AWS Secrets Manager (`opencode//GITHUB_APP_*`) and uploads them with `--secrets-file`. diff --git a/packages/cli/sst-env.d.ts b/services/legacy/sst-env.d.ts similarity index 100% rename from packages/cli/sst-env.d.ts rename to services/legacy/sst-env.d.ts diff --git a/packages/client/sst-env.d.ts b/services/site/sst-env.d.ts similarity index 100% rename from packages/client/sst-env.d.ts rename to services/site/sst-env.d.ts diff --git a/packages/codemode/sst-env.d.ts b/services/update/sst-env.d.ts similarity index 100% rename from packages/codemode/sst-env.d.ts rename to services/update/sst-env.d.ts diff --git a/sst-env.d.ts b/sst-env.d.ts deleted file mode 100644 index 242c8d4ffc9..00000000000 --- a/sst-env.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -declare module "sst" { - export interface Resource { - "ADMIN_SECRET": { - "type": "sst.sst.Secret" - "value": string - } - "Api": import("@cloudflare/workers-types").Service - "GITHUB_APP_ID": { - "type": "sst.sst.Secret" - "value": string - } - "GITHUB_APP_PRIVATE_KEY": { - "type": "sst.sst.Secret" - "value": string - } - "Share": import("@cloudflare/workers-types").Service - "WebApp": { - "type": "sst.cloudflare.StaticSite" - "url": string - } - } -} - -import "sst" -export {} \ No newline at end of file diff --git a/sst.config.ts b/sst.config.ts deleted file mode 100644 index ae77ad48bf3..00000000000 --- a/sst.config.ts +++ /dev/null @@ -1,34 +0,0 @@ -/// - -export default $config({ - app(input) { - return { - name: "opencode", - removal: input?.stage === "production" ? "retain" : "remove", - protect: ["production"].includes(input?.stage), - home: "cloudflare", - providers: - input.stage === "beta" - ? {} - : { - // Production state was written by 6.15.0; the default 6.13 provider can't read it back. - cloudflare: "6.15.0", - planetscale: "0.4.1", - }, - } - }, - async run() { - if ($app.stage === "beta") { - const { createWebApp } = await import("./infra/webapp.js") - return { WebAppUrl: createWebApp("beta.opencode.ai").url } - } - - const stage = await import("./infra/stage.js") - const stats = stage.deployStats ? await import("./infra/stats.js") : undefined - - return { - ...(stats ? { StatsUrl: stats.publicUrl } : {}), - AwsStage: stage.awsStage, - } - }, -})