From 516014f8f254308ddebd979aa35b5a4c8f621396 Mon Sep 17 00:00:00 2001 From: Anand Pant Date: Sat, 7 Mar 2026 12:28:27 -0600 Subject: [PATCH 1/2] feat: ship sandcode cli --- .github/workflows/check.yml | 42 +- .github/workflows/daytona-e2e.yml | 30 +- .../{publish-package.yml => publish.yml} | 44 +- .ignore | 1 + AGENTS.md | 3 + README.md | 279 ++----- RELEASE.md | 112 ++- bin/analyze.js | 2 - bin/sandcode.js | 11 + bin/setup.js | 2 - bin/start.js | 2 - bun.lock | 350 +++++++- bunfig.toml | 1 + package.json | 40 +- scripts/build.ts | 24 + scripts/install-gh-package.sh | 251 ------ scripts/install.sh | 30 + src/analyze-model.test.ts | 18 +- src/analyze-model.ts | 4 +- src/analyze-repos.ts | 99 ++- src/install.ts | 548 ------------- src/obsidian-catalog.test.ts | 12 +- src/obsidian-catalog.ts | 6 +- src/opencode-cli.test.ts | 12 +- src/opentui-compat.d.ts | 12 + ...config.test.ts => sandcode-config.test.ts} | 12 +- src/{shpit-config.ts => sandcode-config.ts} | 30 +- src/sandcode.ts | 79 ++ src/setup-core.ts | 498 ++++++++++++ src/setup-ui-state.ts | 3 + src/setup-ui.test.ts | 16 + src/setup-ui.tsx | 753 ++++++++++++++++++ src/setup.ts | 63 ++ src/start-opencode-daytona.ts | 57 +- tsconfig.json | 7 +- 35 files changed, 2230 insertions(+), 1223 deletions(-) rename .github/workflows/{publish-package.yml => publish.yml} (89%) create mode 100644 AGENTS.md delete mode 100755 bin/analyze.js create mode 100755 bin/sandcode.js delete mode 100755 bin/setup.js delete mode 100755 bin/start.js create mode 100644 bunfig.toml create mode 100644 scripts/build.ts delete mode 100755 scripts/install-gh-package.sh create mode 100644 scripts/install.sh delete mode 100644 src/install.ts create mode 100644 src/opentui-compat.d.ts rename src/{shpit-config.test.ts => sandcode-config.test.ts} (91%) rename src/{shpit-config.ts => sandcode-config.ts} (94%) create mode 100644 src/sandcode.ts create mode 100644 src/setup-core.ts create mode 100644 src/setup-ui-state.ts create mode 100644 src/setup-ui.test.ts create mode 100644 src/setup-ui.tsx create mode 100644 src/setup.ts diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a156204..d696277 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -12,7 +12,7 @@ permissions: env: DAYTONA_API_KEY: ${{ secrets.DAYTONA_API_KEY }} DAYTONA_API_URL: ${{ vars.DAYTONA_API_URL }} - ZHIPU_API_KEY: ${{ secrets.ZHIPU_API_KEY }} + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} jobs: Check: @@ -25,7 +25,7 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: 1.3.8 + bun-version: 1.3.10 - name: Install dependencies run: bun install --frozen-lockfile @@ -41,9 +41,10 @@ jobs: - name: CLI smoke checks run: | - bun run analyze -- --help - bun run start -- --help - bun run setup -- --help + bun src/sandcode.ts --help + bun src/sandcode.ts analyze --help + bun src/sandcode.ts start --help + bun src/sandcode.ts setup --help BuildPackage: name: Build Package Artifact @@ -61,7 +62,7 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: 1.3.8 + bun-version: 1.3.10 - name: Install dependencies run: bun install --frozen-lockfile @@ -87,6 +88,11 @@ jobs: runs-on: blacksmith-2vcpu-ubuntu-2404 needs: BuildPackage steps: + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.10 + - name: Setup Node uses: actions/setup-node@v4 with: @@ -105,9 +111,25 @@ jobs: npm init -y npm install ../artifacts/*.tgz - - name: Run installed CLI binaries + - name: Run installed CLI binary + run: | + cd e2e-install + ./node_modules/.bin/sandcode --help + ./node_modules/.bin/sandcode analyze --help + ./node_modules/.bin/sandcode start --help + ./node_modules/.bin/sandcode setup --help + + - name: Run installed setup smoke run: | cd e2e-install - ./node_modules/.bin/opencode-sandboxed-research-analyze --help - ./node_modules/.bin/opencode-sandboxed-research-start --help - ./node_modules/.bin/opencode-sandboxed-research-setup --help + mkdir -p home-smoke/vaults/test + HOME="$PWD/home-smoke" ./node_modules/.bin/sandcode setup --yes \ + --vault-path ~/vaults/test \ + --obsidian-integration desktop \ + --notes-root Research/Sandcode \ + --catalog-mode repo \ + --daytona-api-key daytona-test \ + --opencode-api-key opencode-test + + test -f home-smoke/.config/sandcode/sandcode.toml + test -f home-smoke/.config/sandcode/.env diff --git a/.github/workflows/daytona-e2e.yml b/.github/workflows/daytona-e2e.yml index b22a38a..fb65933 100644 --- a/.github/workflows/daytona-e2e.yml +++ b/.github/workflows/daytona-e2e.yml @@ -10,11 +10,11 @@ on: model: description: "Model to use" required: false - default: "openai/gpt-5.3-codex" + default: "opencode-go/glm-5" variant: description: "Model variant" required: false - default: "high" + default: "" analyze_timeout_sec: description: "Analyze timeout seconds" required: false @@ -30,8 +30,7 @@ permissions: env: DAYTONA_API_KEY: ${{ secrets.DAYTONA_API_KEY }} DAYTONA_API_URL: ${{ vars.DAYTONA_API_URL }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - ZHIPU_API_KEY: ${{ secrets.ZHIPU_API_KEY }} + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} jobs: E2E: @@ -45,7 +44,7 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: 1.3.8 + bun-version: 1.3.10 - name: Install dependencies run: bun install --frozen-lockfile @@ -56,17 +55,26 @@ jobs: echo "DAYTONA_API_KEY is required" >&2 exit 1 fi + if [ -z "${OPENCODE_API_KEY:-}" ]; then + echo "OPENCODE_API_KEY is required for the default opencode-go model flow" >&2 + exit 1 + fi - name: Run analyze e2e run: | mkdir -p .memory/daytona-e2e - bun run analyze -- \ - --out-dir .memory/daytona-e2e/findings \ - --model "${{ inputs.model }}" \ - --variant "${{ inputs.variant }}" \ - --install-timeout-sec "${{ inputs.install_timeout_sec }}" \ - --analyze-timeout-sec "${{ inputs.analyze_timeout_sec }}" \ + args=( + analyze + --out-dir .memory/daytona-e2e/findings + --model "${{ inputs.model }}" + --install-timeout-sec "${{ inputs.install_timeout_sec }}" + --analyze-timeout-sec "${{ inputs.analyze_timeout_sec }}" "${{ inputs.repo_url }}" + ) + if [ -n "${{ inputs.variant }}" ]; then + args+=(--variant "${{ inputs.variant }}") + fi + bun src/sandcode.ts "${args[@]}" - name: Upload findings artifact uses: actions/upload-artifact@v4 diff --git a/.github/workflows/publish-package.yml b/.github/workflows/publish.yml similarity index 89% rename from .github/workflows/publish-package.yml rename to .github/workflows/publish.yml index 710912e..baeeeb2 100644 --- a/.github/workflows/publish-package.yml +++ b/.github/workflows/publish.yml @@ -11,7 +11,7 @@ concurrency: permissions: contents: write pull-requests: write - packages: write + id-token: write jobs: resolve-merge-context: @@ -106,7 +106,7 @@ jobs: const isBumpPr = (mergedPr.headRefName ?? "").startsWith("ci/version-bump-") || - /^chore:\s*bump package version to 0\\.0\\.\\d+$/i.test( + /^chore:\s*bump package version to 0\.0\.\d+$/i.test( mergedPr.title ?? "", ); @@ -122,12 +122,12 @@ jobs: publish-and-manage-bump: name: Publish Package + Manage Bump PR - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: ubuntu-latest needs: resolve-merge-context permissions: contents: write pull-requests: write - packages: write + id-token: write steps: - name: Checkout uses: actions/checkout@v4 @@ -135,12 +135,16 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "22.14.0" + registry-url: "https://registry.npmjs.org" + + - name: Upgrade npm for trusted publishing support + run: npm install -g npm@latest - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: 1.3.8 + bun-version: 1.3.10 - name: Install dependencies run: bun install --frozen-lockfile @@ -211,40 +215,29 @@ jobs: fs.writeFileSync("package.json", `${JSON.stringify(pkg, null, 2)}\n`); NODE - - name: Configure npm for GitHub Packages - env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - npm config set @shpitdev:registry https://npm.pkg.github.com - npm config set //npm.pkg.github.com/:_authToken "$NODE_AUTH_TOKEN" - - name: Publish package id: publish - env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | PACKAGE_REF="${{ steps.meta.outputs.name }}@${{ steps.meta.outputs.publish_version }}" - if npm view "$PACKAGE_REF" version --registry https://npm.pkg.github.com >/dev/null 2>&1; then - echo "Version $PACKAGE_REF already exists in GitHub Packages; skipping publish." + if npm view "$PACKAGE_REF" version >/dev/null 2>&1; then + echo "Version $PACKAGE_REF already exists on npm; skipping publish." echo "published=false" >> "$GITHUB_OUTPUT" exit 0 fi - npm publish --registry https://npm.pkg.github.com --tag "${{ steps.meta.outputs.publish_tag }}" + npm publish --tag "${{ steps.meta.outputs.publish_tag }}" echo "published=true" >> "$GITHUB_OUTPUT" - name: Verify published dist-tag if: steps.publish.outputs.published == 'true' - env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | PACKAGE_NAME="${{ steps.meta.outputs.name }}" PUBLISH_TAG="${{ steps.meta.outputs.publish_tag }}" EXPECTED_VERSION="${{ steps.meta.outputs.publish_version }}" for attempt in 1 2 3 4 5 6; do - ACTUAL_VERSION="$(npm view "${PACKAGE_NAME}@${PUBLISH_TAG}" version --registry https://npm.pkg.github.com 2>/dev/null || true)" + ACTUAL_VERSION="$(npm view "${PACKAGE_NAME}@${PUBLISH_TAG}" version 2>/dev/null || true)" if [ "$ACTUAL_VERSION" = "$EXPECTED_VERSION" ]; then echo "Verified dist-tag ${PUBLISH_TAG}: ${PACKAGE_NAME}@${ACTUAL_VERSION}" @@ -262,8 +255,6 @@ jobs: - name: Verify registry install if: steps.publish.outputs.published == 'true' - env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | PACKAGE_REF="${{ steps.meta.outputs.name }}@${{ steps.meta.outputs.publish_version }}" @@ -284,9 +275,10 @@ jobs: sleep 10 done - ./node_modules/.bin/opencode-sandboxed-research-analyze --help - ./node_modules/.bin/opencode-sandboxed-research-start --help - ./node_modules/.bin/opencode-sandboxed-research-setup --help + ./node_modules/.bin/sandcode --help + ./node_modules/.bin/sandcode analyze --help + ./node_modules/.bin/sandcode start --help + ./node_modules/.bin/sandcode setup --help - name: Prepare next patch bump if: steps.meta.outputs.create_bump_pr == 'true' diff --git a/.ignore b/.ignore index 5f2598b..4f08158 100644 --- a/.ignore +++ b/.ignore @@ -1 +1,2 @@ !.memory +!.memory/** diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2d47091 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,3 @@ +# AGENTS + +- Interim work should be done in the `.memory/` folder. diff --git a/README.md b/README.md index 226b579..ad5244a 100644 --- a/README.md +++ b/README.md @@ -1,261 +1,142 @@ -# OpenCode Sandboxed Ad Hoc Research +# Sandcode -Run OpenCode inside Daytona sandboxes for two workflows: remote OpenCode web sessions and URL-driven repository audits. +Sandcode is a Bun CLI for ad hoc software research with OpenCode running inside Daytona sandboxes. - -[![Daytona SDK](https://img.shields.io/badge/Daytona_SDK-0.143.0-0891B2?logo=databricks&logoColor=white)](https://www.daytona.io/docs) -[![OpenCode](https://img.shields.io/badge/OpenCode-CLI-10B981?logo=terminal&logoColor=white)](https://github.com/opencode-ai/opencode) -[![Bun](https://img.shields.io/badge/Bun-1.3.8-FBF0DF?logo=bun&logoColor=black)](https://bun.sh/docs) -[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/docs/) +It does three things well: - -[![Biome](https://img.shields.io/badge/Biome-2.4.3-60A5FA?logo=biome&logoColor=white)](https://biomejs.dev/guides/getting-started/) +- `sandcode analyze` runs evidence-based repository audits from URLs or link files. +- `sandcode start` boots an OpenCode web session in a fresh Daytona sandbox. +- `sandcode setup` launches an OpenTUI wizard for Obsidian integration and local credentials. ---- +## Install -## What is this? - -This project automates Daytona sandbox setup and OpenCode execution. - -**It ships two entrypoints:** -- `bun run start` - boots OpenCode web in a fresh Daytona sandbox and prints an externally reachable preview URL. -- `bun run analyze` - runs headless OpenCode audits against one or more repository URLs and collects findings locally. - ---- - -## Table of Contents - -- [What is this?](#what-is-this) -- [Prerequisites](#prerequisites) -- [Install On New Machine](#install-on-new-machine) -- [Quick Start](#quick-start) -- [Installer & Obsidian Cataloging](#installer--obsidian-cataloging) -- [Commands](#commands) -- [Repository Audit Workflow](#repository-audit-workflow) -- [Output Layout](#output-layout) -- [Release Automation](#release-automation) -- [Release Process](#release-process) -- [Development](#development) -- [Compatibility Notes](#compatibility-notes) - ---- - -## Prerequisites - -- [Bun](https://bun.sh/) 1.3+ -- `DAYTONA_API_KEY` -- `DAYTONA_API_URL` for self-hosted Daytona (example: `https://daytona.example.com/api`) -- Optional but recommended: `OPENCODE_SERVER_PASSWORD` -- Obsidian Headless CLI in `PATH` (`ob`, installed via `npm install -g obsidian-headless`) for non-disruptive sync -- Obsidian Catalyst access (Headless Sync is currently open beta) -- Active Obsidian Sync subscription (required for `ob sync-*`) -- Optional: `obsidian` desktop CLI in `PATH` if you explicitly use desktop integration/open-after-catalog - ---- - -## Install On New Machine - -Use the bootstrap installer: +One-off use: ```bash -curl -fsSL https://raw.githubusercontent.com/shpitdev/opencode-sandboxed-ad-hoc-research/main/scripts/install-gh-package.sh | bash +bunx sandcode --help ``` -It will: - -- reuse `gh auth` token when available and auto-attempt `read:packages` scope refresh -- otherwise prompt for a GitHub token with `read:packages` -- configure `~/.npmrc` for GitHub Packages -- skip registry auth setup automatically when installing from a local tarball path -- install `@shpitdev/opencode-sandboxed-ad-hoc-research` globally -- launch the guided setup flow for Daytona/model credentials - ---- - -## Quick Start +Global install: ```bash -bun install -bun run start +bun add -g sandcode ``` -OpenCode will be available on the printed Daytona preview URL. - -Stop with `Ctrl+C`. +Bootstrap script: ---- +```bash +curl -fsSL https://raw.githubusercontent.com/shpitdev/sandcode/main/scripts/install.sh | bash +``` -## Installer & Obsidian Cataloging +## Quick Start -Run the guided installer: +First-run setup: ```bash -bun run setup +bunx sandcode setup ``` -It sets up: - -- `~/.config/opencode/shpit.toml` for shared preferences -- `~/.config/opencode/.env` for optional credential storage -- headless preflight checks when `obsidian.integration_mode = "headless"`: - - verifies `ob` command is installed - - runs `ob sync-list-remote` to validate account/login/sync access +Analyze a repository: -No provider API key is required if you only use free `opencode/*` models (for example `opencode/minimax-m2.5-free`). - -`analyze` automatically catalogs findings to Obsidian when enabled in `shpit.toml`. +```bash +bunx sandcode https://github.com/octocat/Hello-World +``` -Example config: +Analyze repositories from a link file: -```toml -[obsidian] -enabled = true -command = "obsidian" -integration_mode = "headless" # headless | desktop -headless_command = "ob" -vault_path = "/absolute/path/to/vault" -notes_root = "Research/OpenCode" -catalog_mode = "date" # date | repo -sync_after_catalog = true -sync_timeout_sec = 120 -open_after_catalog = false # desktop mode only +```bash +bunx sandcode links.md ``` -Project-level `shpit.toml` or `.shpit.toml` overrides global config. -The configured desktop command must be `obsidian` (not `obs`). - -Headless setup is one-time per local vault path: +Launch a remote OpenCode web session: ```bash -npm install -g obsidian-headless -ob login -ob sync-list-remote -mkdir -p ~/vaults/my-headless-vault -ob sync-setup --vault "My Vault" --path ~/vaults/my-headless-vault -ob sync --path ~/vaults/my-headless-vault +bunx sandcode start ``` -Do not run desktop Sync and Headless Sync on the same device for the same vault path; use a dedicated local path for headless workflows. +## Requirements ---- +- Bun 1.3+ +- `DAYTONA_API_KEY` +- `OPENCODE_API_KEY` for the built-in `opencode-go/*` model defaults +- Optional `DAYTONA_API_URL` for self-hosted Daytona +- Optional `OPENCODE_SERVER_PASSWORD` for `sandcode start` +- Optional `obsidian` CLI for desktop note opening +- Optional `ob` CLI for headless Obsidian Sync workflows ## Commands -| Command | Purpose | -|---|---| -| `scripts/install-gh-package.sh` | Bootstrap install from GitHub Packages on a new machine | -| `bun run setup` | Guided setup for shared config/env, Obsidian mode selection, and headless preflight checks | -| `bun run start` | Launch OpenCode web in a Daytona sandbox | -| `bun run analyze -- --input example.md` | Analyze repos listed in a file | -| `bun run analyze -- ` | Analyze direct repo URLs | -| `bun run build` | Compile distributable CLI files into `dist/` | -| `bun run lint` | Lint with Biome | -| `bun run format` | Format with Biome | -| `bun run check` | Run Biome checks | -| `bun run typecheck` | Run TypeScript checks | - -Useful `start` flags: - ```bash -bun run start -- --port 3000 --target us --sandbox-name opencode-dev -bun run start -- --keep-sandbox -bun run start -- --no-open +sandcode --help +sandcode analyze --help +sandcode start --help +sandcode setup --help ``` ---- +Examples: -## Repository Audit Workflow - -`src/analyze-repos.ts` supports evidence-based audits at scale. +```bash +sandcode analyze --input example.md +sandcode analyze --out-dir findings --model opencode-go/kimi-k2.5 https://github.com/owner/repo +sandcode start --port 3000 --target us --keep-sandbox +sandcode setup --yes --vault-path ~/vaults/research --obsidian-integration headless +``` -### What it does +## Setup UX -- Creates one Daytona sandbox per URL -- Clones each repo in its sandbox -- Runs OpenCode headlessly to generate findings -- Copies findings plus repo README back to local output -- Deletes sandboxes automatically unless `--keep-sandbox` +`sandcode setup` uses an OpenTUI wizard by default when a TTY is available. -### Defaults and behavior +It writes: -- Default model selection: - - Standard: `openai/gpt-5.3-codex` - - Standard variant: `high` - - Vision mode (`--vision`): `zai-coding-plan/glm-4.6v` -- Override with `--model`, `--variant`, `OPENCODE_ANALYZE_MODEL`, or `OPENCODE_ANALYZE_VARIANT` -- If you override the model, variant is opt-in (no forced default variant for custom/env model overrides) -- Auto-installs missing `git` and `node/npm` inside sandbox -- Forwards provider env vars (`OPENAI_*`, `ANTHROPIC_*`, `XAI_*`, `OPENROUTER_*`, `ZHIPU_*`, `MINIMAX_*`, etc.) -- Syncs local OpenCode config files from `~/.config/opencode` when present -- Syncs local OpenCode OAuth auth file (`~/.local/share/opencode/auth.json`) into sandbox with `chmod 600` when present -- When using `anthropic/*` models, runs `opencode models anthropic` preflight inside sandbox and fails early if the requested model is unavailable -- Produces more skimmable reports with concise summary bullets, sentence-fragment-friendly style, and an ASCII logic/data-flow diagram section -- Uses a fixed Daytona lifecycle policy: auto-stop after 15 minutes, auto-archive after 30 minutes, auto-delete disabled -- Auto-catalogs findings into Obsidian when enabled via `shpit.toml`, with optional automatic `ob sync` in headless mode +- `~/.config/sandcode/sandcode.toml` +- `~/.config/sandcode/.env` -### Examples +Project-level overrides are supported with: -```bash -bun run analyze -- --input example.md -bun run analyze -- https://github.com/owner/repo-one https://github.com/owner/repo-two -bun run analyze -- --out-dir findings --model openai/gpt-5.3-codex --variant high --target us -bun run analyze -- --vision -bun run analyze -- --analyze-timeout-sec 3600 --keep-sandbox -``` +- `sandcode.toml` +- `.sandcode.toml` -If no URLs and no `--input` are provided, the script uses `example.md` when it exists. +Default Obsidian notes root: ---- +```toml +[obsidian] +notes_root = "Research/Sandcode" +``` -## Output Layout +Headless mode runs a real `ob sync-list-remote` preflight before it saves. -- `/index.md` - summary across all URLs -- `//findings.md` - final report for each repository -- `//README.*` - copied repository README (if found) -- `//opencode-run.log` - raw OpenCode run output -- `//opencode-models-anthropic.log` - Anthropic model-list preflight output (only when `anthropic/*` model is requested) +## Repository Audit Workflow -Recommended naming convention for manual runs: +`sandcode analyze`: -```bash -bun run analyze -- --input example.md --out-dir findings-2026-03-03 --analyze-timeout-sec 3600 --keep-sandbox -``` +- creates one Daytona sandbox per target +- clones the repo inside the sandbox +- installs OpenCode inside the sandbox +- runs a headless audit prompt +- writes findings locally +- optionally catalogs findings into Obsidian ---- +Default output layout: -## Release Automation +- `/index.md` +- `//findings.md` +- `//README.*` +- `//opencode-run.log` -- `main` merges trigger `.github/workflows/publish-package.yml` automatically (no manual dispatch). -- Versioning is enforced as patch-only `0.0.x` and starts at `0.0.1`. -- Normal PR merges publish a prerelease for the next patch with npm tag `next` (for example `0.0.2-next...`), then keep/create a draft bump PR (for example `0.0.1 -> 0.0.2`). -- Merging the automated bump PR publishes that bumped version as the public release (`latest`) and does not create another `.next` publish. +If no URLs and no `--input` are provided, `example.md` is used when it exists. -## Release Process +## Publishing -Release operations, required repo settings, verification commands, and rollback steps are documented in [`RELEASE.md`](RELEASE.md). +The package is intended for npm distribution as `sandcode`. ---- +Release automation, dist-tags, verification, and rollback notes live in [RELEASE.md](./RELEASE.md). ## Development ```bash -bun run lint -bun run format +bun install bun run check bun run typecheck +bun test +bun run build ``` - -Project config files: - -- `biome.json` -- `.zed/settings.json` -- `.zed/tasks.json` -- `tsconfig.build.json` - ---- - -## Compatibility Notes - -- Works with modern Daytona control planes. -- Includes fallback support for older/self-hosted Daytona setups that expose `proxyToolboxUrl` in `/config` but not `/sandbox/:id/toolbox-proxy-url`. -- No Daytona OpenCode plugin is required for this flow. diff --git a/RELEASE.md b/RELEASE.md index 2ac917d..1526d7e 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,79 +1,103 @@ # Release Process -This project publishes to GitHub Packages, not npmjs.org. +This project publishes `sandcode` to npm. -- Registry: `https://npm.pkg.github.com` -- Package: `@shpitdev/opencode-sandboxed-ad-hoc-research` +- Registry: `https://registry.npmjs.org` +- Package: `sandcode` - Tags: - `next` for prerelease validation builds - - `latest` for stable public installs + - `latest` for stable releases ## Automated Flow -Workflow: `.github/workflows/publish-package.yml` +Workflow: `.github/workflows/publish.yml` 1. Any merge to `main` triggers publish automation. -2. The workflow resolves the merged PR context: - - Normal PR merge: - - publishes `0.0.(x+1)-next...` with npm tag `next` - - opens or updates draft bump PR `ci/version-bump-0.0.(x+1)` - - Bump PR merge (`ci/version-bump-0.0.x`): - - publishes `0.0.x` with npm tag `latest` - - does not create another bump PR -3. The workflow verifies: - - dist-tag points to the just-published version - - clean install from GitHub Packages into a fresh project - - installed CLI binaries execute (`--help`) +2. Normal PR merges publish the next patch as a prerelease tagged `next`. +3. The automated bump PR publishes the stable patch tagged `latest`. +4. The workflow verifies: + - the expected dist-tag points to the published version + - a clean registry install succeeds + - `sandcode --help` and subcommand help all execute ## Required Repository Configuration - GitHub Actions: - - `GITHUB_TOKEN` must keep `contents:write`, `pull-requests:write`, `packages:write` permissions in `publish-package.yml`. -- Optional token: - - `GH_PAT` can be set to let `create-pull-request` use a PAT instead of `GITHUB_TOKEN`. -- Branch governance: - - Keep required checks enforced for PRs into `main`: - - `CodeQL` + - `contents: write` + - `pull-requests: write` +- npm trusted publishing: + - configure `sandcode` on npm to trust this GitHub repository + - keep the publish job on a GitHub-hosted runner so npm can verify OIDC identity +- Optional: + - `GH_PAT` if bump PR creation should use a PAT instead of `GITHUB_TOKEN` ## Verify Current Published State ```bash -# requires a token with read:packages -export NODE_AUTH_TOKEN="" +npm view sandcode dist-tags +npm view sandcode versions --json +``` + +## First Publish From Local + +The first `sandcode` publish should be done locally to create the package on npm. After that, enable npm trusted publishing for the GitHub repo and point it at `.github/workflows/publish.yml`. -npm view @shpitdev/opencode-sandboxed-ad-hoc-research dist-tags --registry https://npm.pkg.github.com -npm view @shpitdev/opencode-sandboxed-ad-hoc-research versions --json --registry https://npm.pkg.github.com +```bash +bun install +bun run check +bun run typecheck +bun test +bun run build +npm pack ``` -## Rollback Playbook +Then install the tarball into a clean local test project and smoke it before publishing: -### Wrong `latest` version +```bash +cd /path/to/sandcode-testing +mkdir -p local-publish-check +cd local-publish-check +npm init -y +npm install /absolute/path/to/sandcode-.tgz +./node_modules/.bin/sandcode --help +./node_modules/.bin/sandcode analyze --help +./node_modules/.bin/sandcode start --help +./node_modules/.bin/sandcode setup --help +``` -Point `latest` back to a known-good version: +When that passes, publish from the repo root: ```bash -export NODE_AUTH_TOKEN="" -npm dist-tag add @shpitdev/opencode-sandboxed-ad-hoc-research@0.0. latest --registry https://npm.pkg.github.com +npm login +npm publish ``` -### Wrong `next` version +If your npm account requires publish-time 2FA, the npm CLI will prompt for the verification step. With a YubiKey/WebAuthn setup, that flow is handled interactively rather than by a static `--otp` value. + +## Rollback + +Reset `latest`: + +```bash +npm dist-tag add sandcode@0.0. latest +``` -Point `next` to a known-good prerelease or stable version: +Reset `next`: ```bash -export NODE_AUTH_TOKEN="" -npm dist-tag add @shpitdev/opencode-sandboxed-ad-hoc-research@0.0.-next. next --registry https://npm.pkg.github.com +npm dist-tag add sandcode@0.0.-next. next ``` -### Bad version must be removed +Delete a bad version: -Delete the package version from GitHub Packages (org package settings or API) using a token with package delete privileges. +- remove it from npm with an account allowed to manage package versions -## Manual Recovery Steps +## Manual Recovery -1. Revert incorrect code on a PR and merge to `main`. -2. If needed, retag `next`/`latest` first to stop new installs from pulling bad builds. -3. Confirm dist-tags and install: - - `npm view ... dist-tags` - - install into clean temp project -4. Keep bump PR (`ci/version-bump-*`) aligned with intended next stable patch. +1. Revert bad code on a PR and merge it. +2. Retag `next` or `latest` if installs need to be corrected immediately. +3. Verify: + - `npm view sandcode dist-tags` + - clean install into a temp project + - `sandcode --help` +4. Keep the automated version-bump PR aligned with the next intended stable patch. diff --git a/bin/analyze.js b/bin/analyze.js deleted file mode 100755 index 53d2824..0000000 --- a/bin/analyze.js +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env node -import "../dist/analyze-repos.js"; diff --git a/bin/sandcode.js b/bin/sandcode.js new file mode 100755 index 0000000..e387f88 --- /dev/null +++ b/bin/sandcode.js @@ -0,0 +1,11 @@ +#!/usr/bin/env bun +import process from "node:process"; +import { runSandcodeCli } from "../dist/sandcode.js"; + +const exitCode = await runSandcodeCli(process.argv.slice(2)).catch((error) => { + const message = error instanceof Error ? (error.stack ?? error.message) : String(error); + console.error(`sandcode: ${message}`); + return 1; +}); + +process.exit(exitCode); diff --git a/bin/setup.js b/bin/setup.js deleted file mode 100755 index 18d8d74..0000000 --- a/bin/setup.js +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env node -import "../dist/install.js"; diff --git a/bin/start.js b/bin/start.js deleted file mode 100755 index 7fb91ad..0000000 --- a/bin/start.js +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env node -import "../dist/start-opencode-daytona.js"; diff --git a/bun.lock b/bun.lock index a30e1a3..f02ed5a 100644 --- a/bun.lock +++ b/bun.lock @@ -3,15 +3,23 @@ "configVersion": 0, "workspaces": { "": { - "name": "opencode-sandboxed-ad-hoc-research", + "name": "sandcode", + "dependencies": { + "@daytonaio/sdk": "^0.143.0", + "@opentui/core": "0.1.86", + "@opentui/solid": "0.1.86", + "solid-js": "1.9.9", + }, "devDependencies": { "@biomejs/biome": "^2.4.3", - "@daytonaio/sdk": "^0.143.0", + "bun-types": "1.3.10", "typescript": "^5.9.3", }, }, }, "packages": { + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], "@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="], @@ -96,6 +104,62 @@ "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], + + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], + + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], + + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="], + + "@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="], + + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@biomejs/biome": ["@biomejs/biome@2.4.3", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.3", "@biomejs/cli-darwin-x64": "2.4.3", "@biomejs/cli-linux-arm64": "2.4.3", "@biomejs/cli-linux-arm64-musl": "2.4.3", "@biomejs/cli-linux-x64": "2.4.3", "@biomejs/cli-linux-x64-musl": "2.4.3", "@biomejs/cli-win32-arm64": "2.4.3", "@biomejs/cli-win32-x64": "2.4.3" }, "bin": { "biome": "bin/biome" } }, "sha512-cBrjf6PNF6yfL8+kcNl85AjiK2YHNsbU0EvDOwiZjBPbMbQ5QcgVGFpjD0O52p8nec5O8NYw7PKw3xUR7fPAkQ=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eOafSFlI/CF4id2tlwq9CVHgeEqvTL5SrhWff6ZORp6S3NL65zdsR3ugybItkgF8Pf4D9GSgtbB6sE3UNgOM9w=="], @@ -120,6 +184,8 @@ "@daytonaio/toolbox-api-client": ["@daytonaio/toolbox-api-client@0.143.0", "", { "dependencies": { "axios": "^1.6.1" } }, "sha512-E6+yHPnFygNqRIctoDheISHCpzQkHydAU7fiBfsA5pnElfB/yLTOXQlnZfP8gfvOmUkRNU8QCXKzaualRTlI7w=="], + "@dimforge/rapier2d-simd-compat": ["@dimforge/rapier2d-simd-compat@0.17.3", "", {}, "sha512-bijvwWz6NHsNj5e5i1vtd3dU2pDhthSaTUZSh14DUGGKJfw8eMnlWZsxwHBxB/a3AXVNDjL9abuHw1k9FGR+jg=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="], "@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], @@ -128,6 +194,70 @@ "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + "@jimp/core": ["@jimp/core@1.6.0", "", { "dependencies": { "@jimp/file-ops": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^16.0.0", "mime": "3" } }, "sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w=="], + + "@jimp/diff": ["@jimp/diff@1.6.0", "", { "dependencies": { "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "pixelmatch": "^5.3.0" } }, "sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw=="], + + "@jimp/file-ops": ["@jimp/file-ops@1.6.0", "", {}, "sha512-Dx/bVDmgnRe1AlniRpCKrGRm5YvGmUwbDzt+MAkgmLGf+jvBT75hmMEZ003n9HQI/aPnm/YKnXjg/hOpzNCpHQ=="], + + "@jimp/js-bmp": ["@jimp/js-bmp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "bmp-ts": "^1.0.9" } }, "sha512-FU6Q5PC/e3yzLyBDXupR3SnL3htU7S3KEs4e6rjDP6gNEOXRFsWs6YD3hXuXd50jd8ummy+q2WSwuGkr8wi+Gw=="], + + "@jimp/js-gif": ["@jimp/js-gif@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "gifwrap": "^0.10.1", "omggif": "^1.0.10" } }, "sha512-N9CZPHOrJTsAUoWkWZstLPpwT5AwJ0wge+47+ix3++SdSL/H2QzyMqxbcDYNFe4MoI5MIhATfb0/dl/wmX221g=="], + + "@jimp/js-jpeg": ["@jimp/js-jpeg@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "jpeg-js": "^0.4.4" } }, "sha512-6vgFDqeusblf5Pok6B2DUiMXplH8RhIKAryj1yn+007SIAQ0khM1Uptxmpku/0MfbClx2r7pnJv9gWpAEJdMVA=="], + + "@jimp/js-png": ["@jimp/js-png@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "pngjs": "^7.0.0" } }, "sha512-AbQHScy3hDDgMRNfG0tPjL88AV6qKAILGReIa3ATpW5QFjBKpisvUaOqhzJ7Reic1oawx3Riyv152gaPfqsBVg=="], + + "@jimp/js-tiff": ["@jimp/js-tiff@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "utif2": "^4.1.0" } }, "sha512-zhReR8/7KO+adijj3h0ZQUOiun3mXUv79zYEAKvE0O+rP7EhgtKvWJOZfRzdZSNv0Pu1rKtgM72qgtwe2tFvyw=="], + + "@jimp/plugin-blit": ["@jimp/plugin-blit@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-M+uRWl1csi7qilnSK8uxK4RJMSuVeBiO1AY0+7APnfUbQNZm6hCe0CCFv1Iyw1D/Dhb8ph8fQgm5mwM0eSxgVA=="], + + "@jimp/plugin-blur": ["@jimp/plugin-blur@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-zrM7iic1OTwUCb0g/rN5y+UnmdEsT3IfuCXCJJNs8SZzP0MkZ1eTvuwK9ZidCuMo4+J3xkzCidRwYXB5CyGZTw=="], + + "@jimp/plugin-circle": ["@jimp/plugin-circle@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-xt1Gp+LtdMKAXfDp3HNaG30SPZW6AQ7dtAtTnoRKorRi+5yCJjKqXRgkewS5bvj8DEh87Ko1ydJfzqS3P2tdWw=="], + + "@jimp/plugin-color": ["@jimp/plugin-color@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "tinycolor2": "^1.6.0", "zod": "^3.23.8" } }, "sha512-J5q8IVCpkBsxIXM+45XOXTrsyfblyMZg3a9eAo0P7VPH4+CrvyNQwaYatbAIamSIN1YzxmO3DkIZXzRjFSz1SA=="], + + "@jimp/plugin-contain": ["@jimp/plugin-contain@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-oN/n+Vdq/Qg9bB4yOBOxtY9IPAtEfES8J1n9Ddx+XhGBYT1/QTU/JYkGaAkIGoPnyYvmLEDqMz2SGihqlpqfzQ=="], + + "@jimp/plugin-cover": ["@jimp/plugin-cover@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-Iow0h6yqSC269YUJ8HC3Q/MpCi2V55sMlbkkTTx4zPvd8mWZlC0ykrNDeAy9IJegrQ7v5E99rJwmQu25lygKLA=="], + + "@jimp/plugin-crop": ["@jimp/plugin-crop@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-KqZkEhvs+21USdySCUDI+GFa393eDIzbi1smBqkUPTE+pRwSWMAf01D5OC3ZWB+xZsNla93BDS9iCkLHA8wang=="], + + "@jimp/plugin-displace": ["@jimp/plugin-displace@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-4Y10X9qwr5F+Bo5ME356XSACEF55485j5nGdiyJ9hYzjQP9nGgxNJaZ4SAOqpd+k5sFaIeD7SQ0Occ26uIng5Q=="], + + "@jimp/plugin-dither": ["@jimp/plugin-dither@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0" } }, "sha512-600d1RxY0pKwgyU0tgMahLNKsqEcxGdbgXadCiVCoGd6V6glyCvkNrnnwC0n5aJ56Htkj88PToSdF88tNVZEEQ=="], + + "@jimp/plugin-fisheye": ["@jimp/plugin-fisheye@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-E5QHKWSCBFtpgZarlmN3Q6+rTQxjirFqo44ohoTjzYVrDI6B6beXNnPIThJgPr0Y9GwfzgyarKvQuQuqCnnfbA=="], + + "@jimp/plugin-flip": ["@jimp/plugin-flip@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-/+rJVDuBIVOgwoyVkBjUFHtP+wmW0r+r5OQ2GpatQofToPVbJw1DdYWXlwviSx7hvixTWLKVgRWQ5Dw862emDg=="], + + "@jimp/plugin-hash": ["@jimp/plugin-hash@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "any-base": "^1.1.0" } }, "sha512-wWzl0kTpDJgYVbZdajTf+4NBSKvmI3bRI8q6EH9CVeIHps9VWVsUvEyb7rpbcwVLWYuzDtP2R0lTT6WeBNQH9Q=="], + + "@jimp/plugin-mask": ["@jimp/plugin-mask@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-Cwy7ExSJMZszvkad8NV8o/Z92X2kFUFM8mcDAhNVxU0Q6tA0op2UKRJY51eoK8r6eds/qak3FQkXakvNabdLnA=="], + + "@jimp/plugin-print": ["@jimp/plugin-print@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/types": "1.6.0", "parse-bmfont-ascii": "^1.0.6", "parse-bmfont-binary": "^1.0.6", "parse-bmfont-xml": "^1.1.6", "simple-xml-to-json": "^1.2.2", "zod": "^3.23.8" } }, "sha512-zarTIJi8fjoGMSI/M3Xh5yY9T65p03XJmPsuNet19K/Q7mwRU6EV2pfj+28++2PV2NJ+htDF5uecAlnGyxFN2A=="], + + "@jimp/plugin-quantize": ["@jimp/plugin-quantize@1.6.0", "", { "dependencies": { "image-q": "^4.0.0", "zod": "^3.23.8" } }, "sha512-EmzZ/s9StYQwbpG6rUGBCisc3f64JIhSH+ncTJd+iFGtGo0YvSeMdAd+zqgiHpfZoOL54dNavZNjF4otK+mvlg=="], + + "@jimp/plugin-resize": ["@jimp/plugin-resize@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-uSUD1mqXN9i1SGSz5ov3keRZ7S9L32/mAQG08wUwZiEi5FpbV0K8A8l1zkazAIZi9IJzLlTauRNU41Mi8IF9fA=="], + + "@jimp/plugin-rotate": ["@jimp/plugin-rotate@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-JagdjBLnUZGSG4xjCLkIpQOZZ3Mjbg8aGCCi4G69qR+OjNpOeGI7N2EQlfK/WE8BEHOW5vdjSyglNqcYbQBWRw=="], + + "@jimp/plugin-threshold": ["@jimp/plugin-threshold@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-M59m5dzLoHOVWdM41O8z9SyySzcDn43xHseOH0HavjsfQsT56GGCC4QzU1banJidbUrePhzoEdS42uFE8Fei8w=="], + + "@jimp/types": ["@jimp/types@1.6.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-7UfRsiKo5GZTAATxm2qQ7jqmUXP0DxTArztllTcYdyw6Xi5oT4RaoXynVtCD4UyLK5gJgkZJcwonoijrhYFKfg=="], + + "@jimp/utils": ["@jimp/utils@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "tinycolor2": "^1.6.0" } }, "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -194,6 +324,22 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.39.0", "", {}, "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg=="], + "@opentui/core": ["@opentui/core@0.1.86", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.86", "@opentui/core-darwin-x64": "0.1.86", "@opentui/core-linux-arm64": "0.1.86", "@opentui/core-linux-x64": "0.1.86", "@opentui/core-win32-arm64": "0.1.86", "@opentui/core-win32-x64": "0.1.86", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-3tRLbI9ADrQE1jEEn4x2aJexEOQZkv9Emk2BixMZqxfVhz2zr2SxtpimDAX0vmZK3+GnWAwBWxuaCAsxZpY4+w=="], + + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.86", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Zp7q64+d+Dcx6YrH3mRcnHq8EOBnrfc1RvjgSWLhpXr49hY6LzuhqpfZM57aGErPYlR+ff8QM6e5FUkFnDfyjw=="], + + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.86", "", { "os": "darwin", "cpu": "x64" }, "sha512-NcxfjCJm1kLnTMVOpAPdRYNi8W8XdAXNa6N7i9khiVFrl2v5KRQfUjbrSOUYVxFJNc3jKFG6rsn3jEApvn92qA=="], + + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.86", "", { "os": "linux", "cpu": "arm64" }, "sha512-EDHAvqSOr8CXzbDvo1aE5blJ6wu1aSbR2LqoXtoeXHemr2T2W42D2TdIWewG6K+/BuRbzZnqt9wnYFBksLW6lw=="], + + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.86", "", { "os": "linux", "cpu": "x64" }, "sha512-VBaBkVdQDxYV4WcKjb+jgyMS5PiVHepvfaoKWpz1Bq+J01xXW4XPcXyPGkgR1+2R93KzaugEnLscTW4mWtLHlQ=="], + + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.86", "", { "os": "win32", "cpu": "arm64" }, "sha512-xKbT7sEKYKGwUPkoqmLfHjbJU+vwHPDwf/r/mIunL41JXQBB35CSZ3/QgIwpp2kkteu7oE1tdBdg15ogUU4OMg=="], + + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.86", "", { "os": "win32", "cpu": "x64" }, "sha512-HRfgAUlcu71/MrtgfX4Gj7PsDtfXZiuC506Pkn1OnRN1Xomcu10BVRDweUa0/g8ldU9i9kLjMGGnpw6/NjaBFg=="], + + "@opentui/solid": ["@opentui/solid@0.1.86", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.86", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-pOZC9dlZIH+bpstVVZ2AvYukBnslZTKSl/y5H8FWcMTHGv/BzpGxXBxstL65E/IQASqPFbvFcs7yMRzdLhynmA=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], @@ -316,8 +462,14 @@ "@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], + "@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="], + + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + "acorn": ["acorn@8.16.0", "", { "bin": "bin/acorn" }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], @@ -326,22 +478,58 @@ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "any-base": ["any-base@1.1.0", "", {}, "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="], + "axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.5", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-8TFKemVLDYezqqv4mWz+PhRrkryTzivTGu0twyLrOkVZ0P63COx2Y04eVsUjFlwSOXui1z3P3Pn209dokWnirg=="], + + "babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="], + + "babel-preset-solid": ["babel-preset-solid@1.9.9", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.1" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.8" }, "optionalPeers": ["solid-js"] }, "sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], + + "bmp-ts": ["bmp-ts@1.0.9", "", {}, "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw=="], + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + "buffer": ["buffer@5.6.0", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" } }, "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw=="], + "bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="], + + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + + "bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="], + + "bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qM7W5IaFpWYGPDcNiQ8DOng3noQ97gxpH2MFH1mGsdKwI0T4oy++egSh5Z7s6AQx8WKgc9GzAsTUM4KZkFdacw=="], + + "bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-oVoIsme27pcXB68YxnQSAgdNGCa4A3PGWYIBUewOh9VnJaoik4JenGb5Yy+svGE+ETFhQXV9nhHqgMPsDRrO6A=="], + + "bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-+SYt09k+xDEl/GfcU7L1zdNgm7IlvAFKV5Xl/auBwuprKG5UwXNhjRlRAWfhTMCUZWN+NDf8E+ZQx0cQi9K2/g=="], + + "bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-zvnUl4EAsQbKsmZVu+lEJcH8axQ7MiCfqg2OmnHd6uw1THABmHaX0GbpKiHshdgadNN2Nf+4zDyTJB5YMcAdrA=="], + "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], "cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], @@ -354,16 +542,26 @@ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], + "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], @@ -374,8 +572,12 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + "exif-parser": ["exif-parser@0.1.12", "", {}, "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="], + "expand-tilde": ["expand-tilde@2.0.2", "", { "dependencies": { "homedir-polyfill": "^1.0.1" } }, "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw=="], "fast-glob": ["fast-glob@3.3.3", "", { "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" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -384,22 +586,36 @@ "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + "file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="], + + "find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], "forwarded-parse": ["forwarded-parse@2.1.2", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="], + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], + + "glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], @@ -412,12 +628,18 @@ "homedir-polyfill": ["homedir-polyfill@1.0.3", "", { "dependencies": { "parse-passwd": "^1.0.0" } }, "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA=="], + "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="], + "import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -428,20 +650,40 @@ "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], + "jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="], + + "jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], @@ -450,12 +692,52 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], + + "omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="], + + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + + "parse-bmfont-ascii": ["parse-bmfont-ascii@1.0.6", "", {}, "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA=="], + + "parse-bmfont-binary": ["parse-bmfont-binary@1.0.6", "", {}, "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA=="], + + "parse-bmfont-xml": ["parse-bmfont-xml@1.1.6", "", { "dependencies": { "xml-parse-from-string": "^1.0.0", "xml2js": "^0.5.0" } }, "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA=="], + "parse-passwd": ["parse-passwd@1.0.0", "", {}, "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="], + + "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], + + "planck": ["planck@1.4.3", "", { "peerDependencies": { "stage-js": "^1.0.0-alpha.12" } }, "sha512-B+lHKhRSeg7vZOfEyEzyQVu7nx8JHcX3QgnAcHXrPW0j04XYKX5eXSiUrxH2Z5QR8OoqvjD6zKIaPMdMYAd0uA=="], + + "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], + + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], + "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], @@ -464,18 +746,40 @@ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], + "reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="], + + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "s-js": ["s-js@0.4.9", "", {}, "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="], + + "seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="], + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], + "simple-xml-to-json": ["simple-xml-to-json@1.2.3", "", {}, "sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA=="], + + "solid-js": ["solid-js@1.9.9", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA=="], + + "stage-js": ["stage-js@1.0.1", "", {}, "sha512-cz14aPp/wY0s3bkb/B93BPP5ZAEhgBbRmAT3CCDqert8eCAqIpQ0RB2zpK8Ksxf+Pisl5oTzvPHtL4CVzzeHcw=="], + "stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="], "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], @@ -488,22 +792,44 @@ "strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + "strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "tar": ["tar@7.5.9", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg=="], + "three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="], + + "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="], + + "xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], + + "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], @@ -512,6 +838,10 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], @@ -544,12 +874,28 @@ "@opentelemetry/sdk-trace-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + + "glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], + + "image-q/@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="], + + "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], + + "readable-web-to-node-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "readable-web-to-node-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..7693482 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1 @@ +preload = ["@opentui/solid/preload"] diff --git a/package.json b/package.json index a212b97..305747d 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,10 @@ { - "name": "@shpitdev/opencode-sandboxed-ad-hoc-research", - "version": "0.0.5", - "description": "Run OpenCode in Daytona sandboxes for web sessions and ad hoc repository research", + "name": "sandcode", + "version": "0.0.1", + "description": "Bun-powered research CLI for OpenCode, Daytona sandboxes, and repository analysis", "private": false, "type": "module", - "packageManager": "bun@1.3.8", - "publishConfig": { - "registry": "https://npm.pkg.github.com" - }, + "packageManager": "bun@1.3.10", "files": [ "dist", "bin", @@ -15,15 +12,13 @@ "LICENSE" ], "bin": { - "opencode-sandboxed-research-start": "./bin/start.js", - "opencode-sandboxed-research-analyze": "./bin/analyze.js", - "opencode-sandboxed-research-setup": "./bin/setup.js" + "sandcode": "bin/sandcode.js" }, "scripts": { - "start": "bun run src/start-opencode-daytona.ts", - "analyze": "bun run src/analyze-repos.ts", - "setup": "bun run src/install.ts", - "build": "bunx tsc -p tsconfig.build.json", + "start": "bun src/sandcode.ts start", + "analyze": "bun src/sandcode.ts analyze", + "setup": "bun src/sandcode.ts setup", + "build": "bun run scripts/build.ts", "prepack": "bun run build", "test": "bun test", "typecheck": "bunx tsc --noEmit", @@ -32,20 +27,35 @@ "check": "biome check ." }, "keywords": [ + "research", + "tui", + "bun", "daytona", "opencode", "sandbox" ], "author": "", "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/shpitdev/sandcode.git" + }, + "bugs": { + "url": "https://github.com/shpitdev/sandcode/issues" + }, + "homepage": "https://github.com/shpitdev/sandcode#readme", "engines": { "bun": ">=1.3.0" }, "dependencies": { - "@daytonaio/sdk": "^0.143.0" + "@daytonaio/sdk": "^0.143.0", + "@opentui/core": "0.1.86", + "@opentui/solid": "0.1.86", + "solid-js": "1.9.9" }, "devDependencies": { "@biomejs/biome": "^2.4.3", + "bun-types": "1.3.10", "typescript": "^5.9.3" } } diff --git a/scripts/build.ts b/scripts/build.ts new file mode 100644 index 0000000..b79f1cd --- /dev/null +++ b/scripts/build.ts @@ -0,0 +1,24 @@ +import { mkdir, rm } from "node:fs/promises"; +import solidPlugin from "@opentui/solid/bun-plugin"; + +await rm("dist", { recursive: true, force: true }); +await mkdir("dist", { recursive: true }); + +const result = await Bun.build({ + entrypoints: ["./src/sandcode.ts"], + outdir: "./dist", + target: "bun", + format: "esm", + packages: "external", + plugins: [solidPlugin], + sourcemap: "none", + minify: false, + splitting: false, +}); + +if (!result.success) { + for (const log of result.logs) { + console.error(log); + } + process.exit(1); +} diff --git a/scripts/install-gh-package.sh b/scripts/install-gh-package.sh deleted file mode 100755 index d88d508..0000000 --- a/scripts/install-gh-package.sh +++ /dev/null @@ -1,251 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -PACKAGE_NAME="${OPENCODE_PACKAGE:-@shpitdev/opencode-sandboxed-ad-hoc-research}" -PACKAGE_SCOPE="${OPENCODE_SCOPE:-@shpitdev}" -REGISTRY_URL="${OPENCODE_REGISTRY:-https://npm.pkg.github.com}" -SETUP_BIN="${OPENCODE_SETUP_BIN:-opencode-sandboxed-research-setup}" -NPMRC_PATH="${HOME}/.npmrc" - -log() { - printf '[install] %s\n' "$*" -} - -fail() { - printf '[install] ERROR: %s\n' "$*" >&2 - exit 1 -} - -require_command() { - if ! command -v "$1" >/dev/null 2>&1; then - fail "Required command not found: $1" - fi -} - -is_interactive_tty() { - [[ -t 0 && -t 1 ]] -} - -is_local_package_ref() { - local ref="$1" - case "$ref" in - ./* | ../* | /* | file:* | *.tgz | *.tar.gz | http://* | https://*) - return 0 - ;; - esac - return 1 -} - -scope_list_contains() { - local scope_list="$1" - local scope="$2" - local normalized=",${scope_list// /}," - [[ "$normalized" == *",$scope,"* ]] -} - -has_package_read_scope() { - local scope_list="$1" - scope_list_contains "$scope_list" "read:packages" || - scope_list_contains "$scope_list" "write:packages" || - scope_list_contains "$scope_list" "delete:packages" -} - -get_gh_token_scopes() { - local token="$1" - if [[ -z "$token" ]]; then - return 0 - fi - - GH_TOKEN="$token" gh api -i /user 2>/dev/null | - tr -d '\r' | - awk 'BEGIN { IGNORECASE = 1 } /^x-oauth-scopes:/ { sub(/^[^:]*:[[:space:]]*/, ""); print; exit }' -} - -ensure_gh_token_has_package_scope() { - local token="$1" - local scopes - scopes="$(get_gh_token_scopes "$token")" - - if has_package_read_scope "$scopes"; then - printf '%s' "$token" - return 0 - fi - - log "gh auth token is missing read:packages scope." - if ! is_interactive_tty; then - return 1 - fi - - log "Attempting gh auth scope refresh (read:packages)..." - if ! gh auth refresh -h github.com -s read:packages; then - return 1 - fi - - token="$(gh auth token 2>/dev/null || true)" - if [[ -z "$token" ]]; then - return 1 - fi - - scopes="$(get_gh_token_scopes "$token")" - if has_package_read_scope "$scopes"; then - log "gh auth token refreshed with package scope." - printf '%s' "$token" - return 0 - fi - - return 1 -} - -install_global_package() { - local package_ref="$1" - local install_output - - if install_output="$(npm install -g "$package_ref" 2>&1)"; then - printf '%s\n' "$install_output" - return 0 - fi - - printf '%s\n' "$install_output" >&2 - if grep -Eqi "npm\\.pkg\\.github\\.com|permission_denied|e401|e403|read:packages" <<<"$install_output"; then - printf '[install] ERROR: GitHub Packages auth failed. Token likely missing read:packages.\n' >&2 - printf '[install] ERROR: Run: gh auth refresh -h github.com -s read:packages\n' >&2 - fi - return 1 -} - -upsert_npmrc_line() { - local key_prefix="$1" - local line_value="$2" - - touch "$NPMRC_PATH" - - if grep -Fq "$key_prefix" "$NPMRC_PATH"; then - awk -v key_prefix="$key_prefix" -v line_value="$line_value" ' - index($0, key_prefix) == 1 { - print line_value - next - } - { print } - ' "$NPMRC_PATH" >"${NPMRC_PATH}.tmp" - mv "${NPMRC_PATH}.tmp" "$NPMRC_PATH" - else - printf '%s\n' "$line_value" >>"$NPMRC_PATH" - fi -} - -remove_npmrc_line() { - local key_prefix="$1" - if [[ ! -f "$NPMRC_PATH" ]]; then - return 0 - fi - - awk -v key_prefix="$key_prefix" ' - index($0, key_prefix) == 1 { next } - { print } - ' "$NPMRC_PATH" >"${NPMRC_PATH}.tmp" - mv "${NPMRC_PATH}.tmp" "$NPMRC_PATH" -} - -read_token_interactive() { - local token="" - printf 'GitHub token (read:packages): ' - read -r -s token - printf '\n' >&2 - printf '%s' "$token" -} - -main() { - require_command npm - - local registry_host="${REGISTRY_URL#https://}" - registry_host="${registry_host#http://}" - registry_host="${registry_host%%/}" - local requires_registry_auth="true" - if is_local_package_ref "$PACKAGE_NAME"; then - requires_registry_auth="false" - fi - - local token="${NODE_AUTH_TOKEN:-}" - local token_source="env" - if [[ "$requires_registry_auth" == "true" ]] && [[ -z "$token" ]] && command -v gh >/dev/null 2>&1; then - if gh auth status >/dev/null 2>&1; then - token="$(gh auth token 2>/dev/null || true)" - if [[ -n "$token" ]]; then - log "Using token from gh auth session. Checking package scope..." - token="$(ensure_gh_token_has_package_scope "$token" || true)" - token_source="gh" - fi - fi - fi - - if [[ "$requires_registry_auth" == "true" ]] && [[ -z "$token" ]]; then - if ! is_interactive_tty; then - fail "No usable token found for GitHub Packages. Set NODE_AUTH_TOKEN with read:packages." - fi - log "A GitHub token with read:packages is required to install from GitHub Packages." - token="$(read_token_interactive)" - token_source="manual" - fi - - if [[ "$requires_registry_auth" == "true" ]] && [[ -z "$token" ]]; then - fail "No GitHub token provided." - fi - - if [[ "$requires_registry_auth" == "true" ]]; then - local npmrc_dir - npmrc_dir="$(dirname "$NPMRC_PATH")" - mkdir -p "$npmrc_dir" - - upsert_npmrc_line "${PACKAGE_SCOPE}:registry=" "${PACKAGE_SCOPE}:registry=${REGISTRY_URL}" - upsert_npmrc_line "//${registry_host}/:_authToken=" "//${registry_host}/:_authToken=${token}" - remove_npmrc_line "always-auth=" - log "Updated ${NPMRC_PATH} for ${PACKAGE_SCOPE}." - else - log "Local package reference detected; skipping GitHub Packages auth setup." - fi - - log "Installing ${PACKAGE_NAME} globally..." - if ! install_global_package "$PACKAGE_NAME"; then - if [[ "$requires_registry_auth" == "true" ]] && - [[ "$token_source" == "gh" ]] && - command -v gh >/dev/null 2>&1 && - is_interactive_tty; then - log "Retrying after gh auth refresh (read:packages)..." - if gh auth refresh -h github.com -s read:packages; then - token="$(gh auth token 2>/dev/null || true)" - if [[ -n "$token" ]]; then - upsert_npmrc_line "//${registry_host}/:_authToken=" "//${registry_host}/:_authToken=${token}" - if install_global_package "$PACKAGE_NAME"; then - log "Install succeeded after token refresh." - else - fail "Global install failed after token refresh." - fi - else - fail "Global install failed and gh did not return a token after refresh." - fi - else - fail "Global install failed and gh auth refresh was unsuccessful." - fi - else - fail "Global install failed." - fi - fi - - if ! command -v "$SETUP_BIN" >/dev/null 2>&1; then - fail "Install completed but ${SETUP_BIN} is not in PATH." - fi - - log "Installed successfully." - - local run_setup="" - printf 'Run guided setup now? [Y/n]: ' - read -r run_setup - run_setup="${run_setup:-Y}" - if [[ "$run_setup" =~ ^([yY]|[yY][eE][sS])$ ]]; then - "$SETUP_BIN" - else - log "You can run setup later with: ${SETUP_BIN}" - fi -} - -main "$@" diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100644 index 0000000..9619969 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +log() { + printf '[sandcode-install] %s\n' "$*" +} + +fail() { + printf '[sandcode-install] ERROR: %s\n' "$*" >&2 + exit 1 +} + +if ! command -v bun >/dev/null 2>&1; then + fail "Bun is required. Install Bun first, then re-run this script." +fi + +log "Installing sandcode globally with Bun..." +bun add -g sandcode + +SANDCODE_BIN="$(command -v sandcode || true)" +if [ -z "$SANDCODE_BIN" ] && [ -x "$HOME/.bun/bin/sandcode" ]; then + SANDCODE_BIN="$HOME/.bun/bin/sandcode" +fi + +if [ -z "$SANDCODE_BIN" ]; then + fail "Install completed but sandcode is not in PATH." +fi + +log "Launching setup..." +"$SANDCODE_BIN" setup diff --git a/src/analyze-model.test.ts b/src/analyze-model.test.ts index 029c0fa..c2331d6 100644 --- a/src/analyze-model.test.ts +++ b/src/analyze-model.test.ts @@ -20,8 +20,8 @@ describe("resolveAnalyzeModel", () => { delete process.env.OPENCODE_ANALYZE_VISION_MODEL; const resolved = resolveAnalyzeModel({}); - expect(resolved.model).toBe("openai/gpt-5.3-codex"); - expect(resolved.variant).toBe("high"); + expect(resolved.model).toBe("opencode-go/glm-5"); + expect(resolved.variant).toBeUndefined(); }); test("uses vision default when requested", () => { @@ -35,25 +35,25 @@ describe("resolveAnalyzeModel", () => { }); test("does not force default variant when model is overridden by env", () => { - process.env.OPENCODE_ANALYZE_MODEL = "openai/gpt-5.3-codex"; + process.env.OPENCODE_ANALYZE_MODEL = "opencode-go/glm-5"; delete process.env.OPENCODE_ANALYZE_VARIANT; const resolved = resolveAnalyzeModel({}); - expect(resolved.model).toBe("openai/gpt-5.3-codex"); + expect(resolved.model).toBe("opencode-go/glm-5"); expect(resolved.variant).toBeUndefined(); }); test("respects env overrides", () => { - process.env.OPENCODE_ANALYZE_MODEL = "zai-coding-plan/glm-5"; - process.env.OPENCODE_ANALYZE_VARIANT = "high"; + process.env.OPENCODE_ANALYZE_MODEL = "opencode-go/kimi-k2.5"; + process.env.OPENCODE_ANALYZE_VARIANT = "fast"; const resolved = resolveAnalyzeModel({}); - expect(resolved.model).toBe("zai-coding-plan/glm-5"); - expect(resolved.variant).toBe("high"); + expect(resolved.model).toBe("opencode-go/kimi-k2.5"); + expect(resolved.variant).toBe("fast"); }); test("cli args override env", () => { - process.env.OPENCODE_ANALYZE_MODEL = "openai/gpt-5.3-codex"; + process.env.OPENCODE_ANALYZE_MODEL = "opencode-go/glm-5"; process.env.OPENCODE_ANALYZE_VARIANT = "low"; const resolved = resolveAnalyzeModel({ diff --git a/src/analyze-model.ts b/src/analyze-model.ts index 463d610..04e208c 100644 --- a/src/analyze-model.ts +++ b/src/analyze-model.ts @@ -15,9 +15,9 @@ export function resolveAnalyzeModel(input: AnalyzeModelInput): ResolvedAnalyzeMo const modelOverride = input.model ?? process.env.OPENCODE_ANALYZE_MODEL; const defaultModel = input.vision ? (process.env.OPENCODE_ANALYZE_VISION_MODEL ?? "zai-coding-plan/glm-4.6v") - : "openai/gpt-5.3-codex"; + : "opencode-go/glm-5"; const model = modelOverride ?? defaultModel; - const defaultVariant = !input.vision && !modelOverride ? "high" : undefined; + const defaultVariant = undefined; const variant = input.variant ?? process.env.OPENCODE_ANALYZE_VARIANT ?? defaultVariant; return { diff --git a/src/analyze-repos.ts b/src/analyze-repos.ts index 02eb618..29c1f28 100644 --- a/src/analyze-repos.ts +++ b/src/analyze-repos.ts @@ -10,7 +10,11 @@ import { buildOpencodeModelsCommand, buildOpencodeRunCommand, } from "./opencode-cli.js"; -import { loadConfiguredEnv, type ResolvedShpitConfig, resolveShpitConfig } from "./shpit-config.js"; +import { + loadConfiguredEnv, + type ResolvedSandcodeConfig, + resolveSandcodeConfig, +} from "./sandcode-config.js"; type CliOptions = { inputFile?: string; @@ -232,8 +236,33 @@ function findNormalizedModelMatch( return availableModels.find((model) => normalizeModelId(model) === normalizedRequested); } -function parseCliOptions(): CliOptions { +export function formatAnalyzeHelp(invocation = "sandcode analyze"): string { + return `Usage: ${invocation} [repo-url ...] [options] + +Examples: + sandcode analyze --input example.md + sandcode analyze https://github.com/agenticnotetaking/arscontexta + sandcode analyze links.md + sandcode analyze --vision + +Options: + -i, --input Markdown/text file containing links + --out-dir Output directory for findings (default: findings) + --create-timeout-sec Sandbox creation timeout (default: 180) + --install-timeout-sec OpenCode install timeout (default: 900) + --analyze-timeout-sec Per-repo analysis timeout (default: 2400) + --target Daytona target override + --model OpenCode model (default: opencode-go/glm-5) + --variant Model variant override + --vision Prefer vision-capable default model (zai-coding-plan/glm-4.6v) + --keep-sandbox Keep each sandbox instead of deleting it + -h, --help Show this help +`; +} + +function parseCliOptions(args: string[]): CliOptions | undefined { const { values, positionals } = parseArgs({ + args, options: { help: { type: "boolean", short: "h", default: false }, input: { type: "string", short: "i" }, @@ -252,32 +281,19 @@ function parseCliOptions(): CliOptions { }); if (values.help) { - console.log(`Usage: bun run analyze -- [repo-url ...] [options] - -Examples: - bun run analyze -- --input example.md - bun run analyze -- https://github.com/agenticnotetaking/arscontexta - bun run analyze -- --input links.md --out-dir findings --model openai/gpt-5.3-codex --variant high - bun run analyze -- --vision - -Options: - -i, --input Markdown/text file containing links - --out-dir Output directory for findings (default: findings) - --create-timeout-sec Sandbox creation timeout (default: 180) - --install-timeout-sec OpenCode install timeout (default: 900) - --analyze-timeout-sec Per-repo analysis timeout (default: 2400) - --target Daytona target override - --model OpenCode model (default: openai/gpt-5.3-codex) - --variant Model variant (default: high, when using built-in default model) - --vision Prefer vision-capable default model (zai-coding-plan/glm-4.6v) - --keep-sandbox Keep each sandbox instead of deleting it - -h, --help Show this help -`); - process.exit(0); + return undefined; } + const directInputFile = + values.input === undefined && + positionals.length === 1 && + !normalizeUrlCandidate(positionals[0]) && + positionals[0].trim().length > 0 + ? positionals[0] + : undefined; + return { - inputFile: values.input, + inputFile: values.input ?? directInputFile, outDir: values["out-dir"], createTimeoutSec: parsePositiveInt(values["create-timeout-sec"], "--create-timeout-sec"), installTimeoutSec: parsePositiveInt(values["install-timeout-sec"], "--install-timeout-sec"), @@ -287,7 +303,7 @@ Options: model: values.model, variant: values.variant, vision: values.vision, - urls: positionals, + urls: directInputFile ? [] : positionals, }; } @@ -675,7 +691,7 @@ function buildAnalysisPrompt(params: { inputUrl: string; reportPath: string }): async function analyzeOneRepo(params: { daytona: Daytona; options: CliOptions; - config: ResolvedShpitConfig; + config: ResolvedSandcodeConfig; url: string; index: number; total: number; @@ -1024,7 +1040,7 @@ async function analyzeOneRepo(params: { } async function maybeCatalogResult(params: { - config: ResolvedShpitConfig; + config: ResolvedSandcodeConfig; result: AnalyzeResult; runPrefix: string; }): Promise { @@ -1092,16 +1108,20 @@ async function writeIndex(results: AnalyzeResult[], outDir: string): Promise { +export async function runAnalyzeCli(args = process.argv.slice(2)): Promise { const loadedEnv = await loadConfiguredEnv(); if (loadedEnv.keysLoaded.length > 0) { console.log( `[analyze] Loaded ${loadedEnv.keysLoaded.length} env var(s) from config (.env) files.`, ); } - const config = await resolveShpitConfig(); + const config = await resolveSandcodeConfig(); - const options = parseCliOptions(); + const options = parseCliOptions(args); + if (!options) { + console.log(formatAnalyzeHelp()); + return 0; + } const urls = await resolveInputUrls(options); const apiKey = requireEnv("DAYTONA_API_KEY"); const apiUrl = process.env.DAYTONA_API_URL; @@ -1134,13 +1154,14 @@ async function main(): Promise { await writeIndex(results, options.outDir); const failures = results.filter((result) => !result.success).length; console.log(`[analyze] Completed. Success: ${results.length - failures}, Failed: ${failures}`); - if (failures > 0) { - process.exitCode = 1; - } + return failures > 0 ? 1 : 0; } -main().catch((error: unknown) => { - const message = error instanceof Error ? (error.stack ?? error.message) : String(error); - console.error(`[analyze] Fatal error: ${message}`); - process.exit(1); -}); +if (import.meta.main) { + const exitCode = await runAnalyzeCli().catch((error: unknown) => { + const message = error instanceof Error ? (error.stack ?? error.message) : String(error); + console.error(`[analyze] Fatal error: ${message}`); + return 1; + }); + process.exit(exitCode); +} diff --git a/src/install.ts b/src/install.ts deleted file mode 100644 index a7c492c..0000000 --- a/src/install.ts +++ /dev/null @@ -1,548 +0,0 @@ -import { execFile } from "node:child_process"; -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import path from "node:path"; -import process from "node:process"; -import { createInterface } from "node:readline/promises"; -import { parseArgs, promisify } from "node:util"; -import { loadConfiguredEnv, resolveShpitConfig } from "./shpit-config.js"; - -type CliOptions = { - yes: boolean; - vaultPath?: string; - notesRoot?: string; - catalogMode?: "date" | "repo"; - openAfterCatalog?: boolean; - integrationMode?: "desktop" | "headless"; - syncAfterCatalog?: boolean; - syncTimeoutSec?: number; - daytonaApiKey?: string; - openaiApiKey?: string; - zhipuApiKey?: string; -}; - -const execFileAsync = promisify(execFile); - -function parseCliOptions(): CliOptions { - const { values } = parseArgs({ - options: { - help: { type: "boolean", short: "h", default: false }, - yes: { type: "boolean", short: "y", default: false }, - "vault-path": { type: "string" }, - "notes-root": { type: "string" }, - "catalog-mode": { type: "string" }, - "open-after-catalog": { type: "boolean" }, - "obsidian-integration": { type: "string" }, - "sync-after-catalog": { type: "boolean" }, - "sync-timeout-sec": { type: "string" }, - "daytona-api-key": { type: "string" }, - "openai-api-key": { type: "string" }, - "zhipu-api-key": { type: "string" }, - }, - strict: true, - allowPositionals: false, - }); - - if (values.help) { - console.log(`Usage: bun run setup -- [options] - -Options: - -y, --yes Non-interactive setup using defaults/flags - --vault-path Obsidian vault path (absolute or ~/...) - --notes-root Folder inside vault for audit notes (default: Research/OpenCode) - --catalog-mode date | repo (default: date) - --obsidian-integration headless | desktop (default: auto-detect) - --sync-after-catalog Run 'ob sync' after writing each note (headless mode) - --sync-timeout-sec Timeout for 'ob sync' (default: 120) - --open-after-catalog Open each new note via obsidian CLI (desktop mode) - --daytona-api-key Seed DAYTONA_API_KEY into ~/.config/opencode/.env - --openai-api-key Seed OPENAI_API_KEY into ~/.config/opencode/.env - --zhipu-api-key Seed ZHIPU_API_KEY into ~/.config/opencode/.env - -h, --help Show this help -`); - process.exit(0); - } - - const rawCatalogMode = values["catalog-mode"]; - if (rawCatalogMode && rawCatalogMode !== "date" && rawCatalogMode !== "repo") { - throw new Error(`--catalog-mode must be "date" or "repo". Received "${rawCatalogMode}".`); - } - - const rawIntegrationMode = values["obsidian-integration"]; - if (rawIntegrationMode && rawIntegrationMode !== "desktop" && rawIntegrationMode !== "headless") { - throw new Error( - `--obsidian-integration must be "desktop" or "headless". Received "${rawIntegrationMode}".`, - ); - } - - const rawSyncTimeoutSec = values["sync-timeout-sec"]; - let syncTimeoutSec: number | undefined; - if (rawSyncTimeoutSec !== undefined) { - const parsed = Number.parseInt(rawSyncTimeoutSec, 10); - if (!Number.isInteger(parsed) || parsed <= 0) { - throw new Error( - `--sync-timeout-sec must be a positive integer. Received "${rawSyncTimeoutSec}".`, - ); - } - syncTimeoutSec = parsed; - } - - return { - yes: values.yes, - vaultPath: values["vault-path"], - notesRoot: values["notes-root"], - catalogMode: rawCatalogMode as "date" | "repo" | undefined, - openAfterCatalog: values["open-after-catalog"], - integrationMode: rawIntegrationMode as "desktop" | "headless" | undefined, - syncAfterCatalog: values["sync-after-catalog"], - syncTimeoutSec, - daytonaApiKey: values["daytona-api-key"], - openaiApiKey: values["openai-api-key"], - zhipuApiKey: values["zhipu-api-key"], - }; -} - -function expandHomeDir(value: string | undefined): string | undefined { - if (!value) { - return undefined; - } - if (!value.startsWith("~")) { - return value; - } - const home = process.env.HOME; - if (!home) { - return value; - } - return path.join(home, value.slice(1)); -} - -async function detectCommandBinary(command: string): Promise { - try { - const { stdout } = await execFileAsync("which", [command]); - const resolved = stdout.trim(); - return resolved || undefined; - } catch { - return undefined; - } -} - -function countRemoteVaults(output: string): number { - return output.split(/\r?\n/).filter((line) => /^\s*[a-f0-9]{32}\s+"/.test(line)).length; -} - -function describeExecError(error: unknown): string { - if (!error || typeof error !== "object") { - return String(error); - } - - const message = "message" in error ? String(error.message) : "unknown error"; - const stdout = "stdout" in error ? String(error.stdout ?? "") : ""; - const stderr = "stderr" in error ? String(error.stderr ?? "") : ""; - const details = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n"); - return details ? `${message}\n${details}` : message; -} - -async function validateHeadlessSyncAccess(command: string): Promise { - try { - const { stdout, stderr } = await execFileAsync(command, ["sync-list-remote"], { - timeout: 30_000, - maxBuffer: 4 * 1024 * 1024, - }); - return countRemoteVaults(`${stdout}\n${stderr}`); - } catch (error) { - throw new Error( - `Headless Sync preflight failed. Ensure Obsidian Catalyst access, an active Obsidian Sync subscription, and successful \`ob login\`.\n${describeExecError(error)}`, - ); - } -} - -function parseEnvFile(content: string): Map { - const result = new Map(); - const lines = content.split(/\r?\n/); - for (const rawLine of lines) { - const line = rawLine.trim(); - if (!line || line.startsWith("#")) { - continue; - } - const separatorIndex = line.indexOf("="); - if (separatorIndex === -1) { - continue; - } - const key = line.slice(0, separatorIndex).trim(); - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) { - continue; - } - let value = line.slice(separatorIndex + 1).trim(); - if (value.startsWith('"') && value.endsWith('"')) { - value = value.slice(1, -1); - } - if (value.startsWith("'") && value.endsWith("'")) { - value = value.slice(1, -1); - } - result.set(key, value); - } - return result; -} - -async function loadEnvMap(filePath: string): Promise> { - try { - const content = await readFile(filePath, "utf8"); - return parseEnvFile(content); - } catch { - return new Map(); - } -} - -function serializeEnvMap(env: Map): string { - const lines: string[] = []; - lines.push("# Managed by bun run setup"); - lines.push("# Shell-exported env vars still override these values."); - lines.push(""); - - const keys = [...env.keys()].sort((a, b) => a.localeCompare(b)); - for (const key of keys) { - const value = env.get(key); - if (value === undefined) { - continue; - } - lines.push(`${key}=${JSON.stringify(value)}`); - } - - lines.push(""); - return lines.join("\n"); -} - -async function askYesNo(params: { - rl: ReturnType; - prompt: string; - defaultValue: boolean; -}): Promise { - const suffix = params.defaultValue ? "Y/n" : "y/N"; - const answer = (await params.rl.question(`${params.prompt} (${suffix}): `)).trim().toLowerCase(); - if (!answer) { - return params.defaultValue; - } - return answer === "y" || answer === "yes"; -} - -async function askText(params: { - rl: ReturnType; - prompt: string; - defaultValue?: string; -}): Promise { - const renderedPrompt = params.defaultValue - ? `${params.prompt} [${params.defaultValue}]: ` - : `${params.prompt}: `; - const answer = (await params.rl.question(renderedPrompt)).trim(); - if (!answer) { - return params.defaultValue; - } - return answer; -} - -function parsePositiveInteger(value: string, label: string): number { - const parsed = Number.parseInt(value, 10); - if (!Number.isInteger(parsed) || parsed <= 0) { - throw new Error(`${label} must be a positive integer. Received "${value}".`); - } - return parsed; -} - -async function main(): Promise { - const options = parseCliOptions(); - await loadConfiguredEnv(); - const existingConfig = await resolveShpitConfig(); - - const home = process.env.HOME; - if (!home) { - throw new Error("HOME is not set. Cannot write ~/.config/opencode files."); - } - - const configDir = path.join(home, ".config", "opencode"); - const configPath = path.join(configDir, "shpit.toml"); - const envPath = path.join(configDir, ".env"); - - const obsidianBinary = await detectCommandBinary("obsidian"); - const headlessBinary = await detectCommandBinary("ob"); - console.log( - obsidianBinary - ? `[install] Detected Obsidian desktop CLI at: ${obsidianBinary}` - : "[install] Obsidian desktop CLI not found in PATH (command: obsidian)", - ); - console.log( - headlessBinary - ? `[install] Detected Obsidian Headless CLI at: ${headlessBinary}` - : "[install] Obsidian Headless CLI not found in PATH (command: ob)", - ); - console.log( - "[install] Headless mode runs a real preflight against Obsidian Sync using `ob sync-list-remote`.", - ); - - const rl = createInterface({ input: process.stdin, output: process.stdout }); - try { - const nonInteractive = options.yes; - const hasExistingConfig = Boolean( - existingConfig.paths.globalConfigPath ?? existingConfig.paths.projectConfigPath, - ); - const recommendedIntegrationMode = hasExistingConfig - ? existingConfig.obsidian.integrationMode - : headlessBinary - ? "headless" - : "desktop"; - const headlessCommand = existingConfig.obsidian.headlessCommand; - - const enableObsidian = nonInteractive - ? Boolean(options.vaultPath ?? existingConfig.obsidian.enabled) - : await askYesNo({ - rl, - prompt: "Enable automatic Obsidian cataloging for analyze results?", - defaultValue: existingConfig.obsidian.enabled, - }); - - const requestedIntegrationMode = - options.integrationMode ?? - (nonInteractive - ? recommendedIntegrationMode - : await askText({ - rl, - prompt: "Obsidian integration mode (headless|desktop)", - defaultValue: recommendedIntegrationMode, - })); - const integrationMode = - (requestedIntegrationMode ?? recommendedIntegrationMode).trim() || recommendedIntegrationMode; - if (integrationMode !== "headless" && integrationMode !== "desktop") { - throw new Error(`Invalid integration mode "${integrationMode}".`); - } - - const vaultPath = expandHomeDir( - options.vaultPath ?? - (nonInteractive - ? existingConfig.obsidian.vaultPath - : await askText({ - rl, - prompt: "Obsidian vault path", - defaultValue: existingConfig.obsidian.vaultPath, - })), - ); - - const notesRoot = - options.notesRoot ?? - (nonInteractive - ? existingConfig.obsidian.notesRoot - : await askText({ - rl, - prompt: "Vault folder for repo audits", - defaultValue: existingConfig.obsidian.notesRoot, - })); - - const catalogMode = - options.catalogMode ?? - (nonInteractive - ? existingConfig.obsidian.catalogMode - : ((await askText({ - rl, - prompt: "Catalog mode (date|repo)", - defaultValue: existingConfig.obsidian.catalogMode, - })) as "date" | "repo" | undefined)); - - if (catalogMode && catalogMode !== "date" && catalogMode !== "repo") { - throw new Error(`Invalid catalog mode "${catalogMode}".`); - } - - const openAfterCatalog = - integrationMode === "desktop" - ? nonInteractive - ? (options.openAfterCatalog ?? existingConfig.obsidian.openAfterCatalog) - : await askYesNo({ - rl, - prompt: "Open each created note via obsidian command", - defaultValue: existingConfig.obsidian.openAfterCatalog, - }) - : false; - - const syncAfterCatalog = - integrationMode === "headless" - ? nonInteractive - ? (options.syncAfterCatalog ?? existingConfig.obsidian.syncAfterCatalog) - : await askYesNo({ - rl, - prompt: "Run `ob sync` after each note write", - defaultValue: existingConfig.obsidian.syncAfterCatalog, - }) - : false; - - let syncTimeoutSec = options.syncTimeoutSec ?? existingConfig.obsidian.syncTimeoutSec; - if (integrationMode === "headless" && !nonInteractive && options.syncTimeoutSec === undefined) { - const entered = await askText({ - rl, - prompt: "Headless sync timeout in seconds", - defaultValue: String(existingConfig.obsidian.syncTimeoutSec), - }); - if (!entered) { - throw new Error("Headless sync timeout is required in headless mode."); - } - syncTimeoutSec = parsePositiveInteger(entered, "Headless sync timeout"); - } - - if (enableObsidian && !vaultPath) { - throw new Error("Obsidian cataloging is enabled, but no vault path was provided."); - } - - if (enableObsidian && integrationMode === "desktop" && openAfterCatalog && !obsidianBinary) { - throw new Error( - "Desktop integration with open_after_catalog requires the `obsidian` command in PATH.", - ); - } - - if (enableObsidian && integrationMode === "headless") { - const resolvedHeadlessBinary = await detectCommandBinary(headlessCommand); - if (!resolvedHeadlessBinary) { - throw new Error( - "Headless integration requires `ob` in PATH. Install with: npm install -g obsidian-headless", - ); - } - const remoteVaultCount = await validateHeadlessSyncAccess(headlessCommand); - if (remoteVaultCount > 0) { - console.log( - `[install] Headless preflight passed. Remote vaults visible to this account: ${remoteVaultCount}`, - ); - } else { - console.warn( - '[install] Headless preflight succeeded but no remote vaults were found. Create one with `ob sync-create-remote --name "..."`.', - ); - } - } - - await mkdir(configDir, { recursive: true }); - - const shpitTomlLines: string[] = []; - shpitTomlLines.push("# Managed by bun run setup"); - shpitTomlLines.push(""); - shpitTomlLines.push("[obsidian]"); - shpitTomlLines.push(`enabled = ${enableObsidian ? "true" : "false"}`); - shpitTomlLines.push('command = "obsidian"'); - shpitTomlLines.push(`integration_mode = ${JSON.stringify(integrationMode)}`); - shpitTomlLines.push(`headless_command = ${JSON.stringify(headlessCommand)}`); - if (vaultPath) { - shpitTomlLines.push(`vault_path = ${JSON.stringify(vaultPath)}`); - } - shpitTomlLines.push( - `notes_root = ${JSON.stringify(notesRoot ?? existingConfig.obsidian.notesRoot)}`, - ); - shpitTomlLines.push( - `catalog_mode = ${JSON.stringify(catalogMode ?? existingConfig.obsidian.catalogMode)}`, - ); - shpitTomlLines.push(`sync_after_catalog = ${syncAfterCatalog ? "true" : "false"}`); - shpitTomlLines.push(`sync_timeout_sec = ${syncTimeoutSec}`); - shpitTomlLines.push(`open_after_catalog = ${openAfterCatalog ? "true" : "false"}`); - shpitTomlLines.push(""); - - await writeFile(configPath, shpitTomlLines.join("\n"), "utf8"); - console.log(`[install] Wrote ${configPath}`); - - const envMap = await loadEnvMap(envPath); - const seededKeys: string[] = []; - - const daytonaApiKey = options.daytonaApiKey ?? process.env.DAYTONA_API_KEY; - if (!daytonaApiKey && !nonInteractive) { - const entered = await askText({ - rl, - prompt: "DAYTONA_API_KEY (leave blank to skip)", - }); - if (entered) { - envMap.set("DAYTONA_API_KEY", entered); - seededKeys.push("DAYTONA_API_KEY"); - } - } else if (daytonaApiKey) { - envMap.set("DAYTONA_API_KEY", daytonaApiKey); - seededKeys.push("DAYTONA_API_KEY"); - } - - const hasProviderKey = - Boolean(process.env.OPENAI_API_KEY) || - Boolean(process.env.ANTHROPIC_API_KEY) || - Boolean(process.env.XAI_API_KEY) || - Boolean(process.env.OPENROUTER_API_KEY) || - Boolean(process.env.ZHIPU_API_KEY) || - Boolean(envMap.get("OPENAI_API_KEY")) || - Boolean(envMap.get("ANTHROPIC_API_KEY")) || - Boolean(envMap.get("XAI_API_KEY")) || - Boolean(envMap.get("OPENROUTER_API_KEY")) || - Boolean(envMap.get("ZHIPU_API_KEY")); - - const openaiApiKey = options.openaiApiKey ?? process.env.OPENAI_API_KEY; - const zhipuApiKey = options.zhipuApiKey ?? process.env.ZHIPU_API_KEY; - if (!hasProviderKey && !openaiApiKey && !zhipuApiKey && !nonInteractive) { - console.log( - "[install] No provider API key detected. Free opencode/* models can work without a key, but provider keys are needed for OpenAI/Anthropic/Z.AI/etc.", - ); - const entered = await askText({ - rl, - prompt: "OPENAI_API_KEY (leave blank to skip)", - }); - if (entered) { - envMap.set("OPENAI_API_KEY", entered); - seededKeys.push("OPENAI_API_KEY"); - } - } else if (openaiApiKey) { - envMap.set("OPENAI_API_KEY", openaiApiKey); - seededKeys.push("OPENAI_API_KEY"); - } - - if (zhipuApiKey) { - envMap.set("ZHIPU_API_KEY", zhipuApiKey); - seededKeys.push("ZHIPU_API_KEY"); - } else if (!hasProviderKey && !openaiApiKey && !nonInteractive) { - const entered = await askText({ - rl, - prompt: "ZHIPU_API_KEY (for Z.AI / GLM, leave blank to skip)", - }); - if (entered) { - envMap.set("ZHIPU_API_KEY", entered); - seededKeys.push("ZHIPU_API_KEY"); - } - } - - if (envMap.size > 0) { - await writeFile(envPath, serializeEnvMap(envMap), "utf8"); - console.log(`[install] Wrote ${envPath}`); - } - - if (!process.env.DAYTONA_API_KEY && !envMap.get("DAYTONA_API_KEY")) { - console.warn( - "[install] DAYTONA_API_KEY is still missing. start/analyze will fail until it is set.", - ); - } - - const hasAnyProviderKey = - Boolean(process.env.OPENAI_API_KEY) || - Boolean(process.env.ANTHROPIC_API_KEY) || - Boolean(process.env.XAI_API_KEY) || - Boolean(process.env.OPENROUTER_API_KEY) || - Boolean(process.env.ZHIPU_API_KEY) || - Boolean(envMap.get("OPENAI_API_KEY")) || - Boolean(envMap.get("ANTHROPIC_API_KEY")) || - Boolean(envMap.get("XAI_API_KEY")) || - Boolean(envMap.get("OPENROUTER_API_KEY")) || - Boolean(envMap.get("ZHIPU_API_KEY")); - - if (!hasAnyProviderKey) { - console.warn( - "[install] No model provider key configured. This is OK if you use free opencode/* models; set OPENAI_API_KEY or ZHIPU_API_KEY for broader model access.", - ); - } - - if (seededKeys.length > 0) { - console.log(`[install] Seeded credential keys: ${[...new Set(seededKeys)].join(", ")}`); - } - - console.log("[install] Setup complete."); - } finally { - rl.close(); - } -} - -main().catch((error: unknown) => { - const message = error instanceof Error ? (error.stack ?? error.message) : String(error); - console.error(`[install] Failed: ${message}`); - process.exit(1); -}); diff --git a/src/obsidian-catalog.test.ts b/src/obsidian-catalog.test.ts index 5b2902a..da24adb 100644 --- a/src/obsidian-catalog.test.ts +++ b/src/obsidian-catalog.test.ts @@ -5,33 +5,33 @@ import { __testables } from "./obsidian-catalog.js"; describe("obsidian catalog pathing", () => { test("builds date-based note path", () => { const relativePath = __testables.buildRelativeNotePath({ - notesRoot: "Research/OpenCode", + notesRoot: "Research/Sandcode", catalogMode: "date", slug: "owner-repo", runLabel: "01-owner-repo", }); expect(relativePath).toMatch( - /^Research[\\/]OpenCode[\\/]\d{4}[\\/]\d{2}[\\/]\d{2}-01-owner-repo\.md$/, + /^Research[\\/]Sandcode[\\/]\d{4}[\\/]\d{2}[\\/]\d{2}-01-owner-repo\.md$/, ); }); test("builds repo-based note path", () => { const relativePath = __testables.buildRelativeNotePath({ - notesRoot: "Research/OpenCode", + notesRoot: "Research/Sandcode", catalogMode: "repo", slug: "owner/repo", runLabel: "01-owner-repo", }); expect(relativePath).toMatch( - /^Research[\\/]OpenCode[\\/]owner-repo[\\/]\d{4}-\d{2}-\d{2}-01-owner-repo\.md$/, + /^Research[\\/]Sandcode[\\/]owner-repo[\\/]\d{4}-\d{2}-\d{2}-01-owner-repo\.md$/, ); }); test("keeps resolved note path inside vault", () => { - const notePath = __testables.resolveNotePathWithinVault("/vault", "Research/OpenCode/note.md"); - expect(notePath).toBe(path.resolve("/vault", "Research/OpenCode/note.md")); + const notePath = __testables.resolveNotePathWithinVault("/vault", "Research/Sandcode/note.md"); + expect(notePath).toBe(path.resolve("/vault", "Research/Sandcode/note.md")); }); test("rejects note path traversal outside vault", () => { diff --git a/src/obsidian-catalog.ts b/src/obsidian-catalog.ts index 610cfa7..ec7ec76 100644 --- a/src/obsidian-catalog.ts +++ b/src/obsidian-catalog.ts @@ -1,7 +1,7 @@ import { spawn } from "node:child_process"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; -import type { ResolvedShpitConfig } from "./shpit-config.js"; +import type { ResolvedSandcodeConfig } from "./sandcode-config.js"; type CommandResult = { exitCode: number | null; @@ -11,7 +11,7 @@ type CommandResult = { }; type CatalogInput = { - config: ResolvedShpitConfig; + config: ResolvedSandcodeConfig; slug: string; runLabel: string; sourceUrl: string; @@ -245,7 +245,7 @@ export async function catalogAnalysisResult(input: CatalogInput): Promise { resolveOpencodeBinCommand: "command -v opencode", workingDir: "/home/daytona/audit/repo", prompt: "Reply with exactly one word: ready", - model: "zai-coding-plan/glm-4.7-flash", - variant: "high", + model: "opencode-go/glm-5", forwardedEnvEntries: [ - ["OPENAI_API_KEY", "sk-test"], - ["ZHIPU_API_KEY", "z-test"], + ["OPENCODE_API_KEY", "oc-test"], + ["MINIMAX_API_KEY", "mm-test"], ], }); expect(command).toContain('OPENCODE_BIN="$(command -v opencode)"'); expect(command).toContain("cd '/home/daytona/audit/repo'"); - expect(command).toContain("env OPENAI_API_KEY='sk-test' ZHIPU_API_KEY='z-test'"); - expect(command).toContain("--model 'zai-coding-plan/glm-4.7-flash'"); - expect(command).toContain("--variant 'high'"); + expect(command).toContain("env OPENCODE_API_KEY='oc-test' MINIMAX_API_KEY='mm-test'"); + expect(command).toContain("--model 'opencode-go/glm-5'"); expect(command).not.toContain("--dir"); }); }); diff --git a/src/opentui-compat.d.ts b/src/opentui-compat.d.ts new file mode 100644 index 0000000..5588f81 --- /dev/null +++ b/src/opentui-compat.d.ts @@ -0,0 +1,12 @@ +import type { CliRenderer, CliRendererConfig, KeyEvent } from "@opentui/core"; + +declare module "@opentui/core" { + export function createCliRenderer(config?: CliRendererConfig): Promise; +} + +declare module "@opentui/solid" { + export function useKeyboard( + callback: (key: KeyEvent) => void, + options?: { release?: boolean }, + ): void; +} diff --git a/src/shpit-config.test.ts b/src/sandcode-config.test.ts similarity index 91% rename from src/shpit-config.test.ts rename to src/sandcode-config.test.ts index f01d9f0..dfca438 100644 --- a/src/shpit-config.test.ts +++ b/src/sandcode-config.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; -import { __testables } from "./shpit-config.js"; +import { __testables } from "./sandcode-config.js"; -describe("shpit config parsing", () => { +describe("sandcode config parsing", () => { test("parses obsidian table values", () => { const parsed = __testables.parseToml( [ @@ -9,7 +9,7 @@ describe("shpit config parsing", () => { "enabled = true", 'command = "obsidian"', 'vault_path = "/vault"', - 'notes_root = "Research/OpenCode"', + 'notes_root = "Research/Sandcode"', 'catalog_mode = "repo"', "open_after_catalog = false", 'integration_mode = "headless"', @@ -17,7 +17,7 @@ describe("shpit config parsing", () => { "sync_after_catalog = true", "sync_timeout_sec = 180", ].join("\n"), - "shpit.toml", + "sandcode.toml", ); expect(parsed).toBeObject(); @@ -25,7 +25,7 @@ describe("shpit config parsing", () => { expect(obsidian.enabled).toBe(true); expect(obsidian.command).toBe("obsidian"); expect(obsidian.vault_path).toBe("/vault"); - expect(obsidian.notes_root).toBe("Research/OpenCode"); + expect(obsidian.notes_root).toBe("Research/Sandcode"); expect(obsidian.catalog_mode).toBe("repo"); expect(obsidian.open_after_catalog).toBe(false); expect(obsidian.integration_mode).toBe("headless"); @@ -49,7 +49,7 @@ describe("shpit config parsing", () => { expect(config.enabled).toBe(false); expect(config.command).toBe("obsidian"); expect(config.catalogMode).toBe("date"); - expect(config.notesRoot).toBe("Research/OpenCode"); + expect(config.notesRoot).toBe("Research/Sandcode"); expect(config.openAfterCatalog).toBe(false); expect(config.integrationMode).toBe("desktop"); expect(config.headlessCommand).toBe("ob"); diff --git a/src/shpit-config.ts b/src/sandcode-config.ts similarity index 94% rename from src/shpit-config.ts rename to src/sandcode-config.ts index 6f5ae23..a066475 100644 --- a/src/shpit-config.ts +++ b/src/sandcode-config.ts @@ -21,11 +21,11 @@ type PartialObsidianConfig = { syncTimeoutSec?: number; }; -type PartialShpitConfig = { +type PartialSandcodeConfig = { obsidian?: PartialObsidianConfig; }; -export type ResolvedShpitConfig = { +export type ResolvedSandcodeConfig = { paths: { globalConfigPath?: string; projectConfigPath?: string; @@ -50,7 +50,7 @@ export type LoadedEnvInfo = { keysLoaded: string[]; }; -const DEFAULT_NOTES_ROOT = "Research/OpenCode"; +const DEFAULT_NOTES_ROOT = "Research/Sandcode"; function stripInlineComment(value: string): string { let inSingle = false; @@ -233,7 +233,7 @@ function asInteger(value: ParsedTomlValue | undefined): number | undefined { return typeof value === "number" && Number.isInteger(value) ? value : undefined; } -function toPartialConfig(parsed: ParsedTomlTable): PartialShpitConfig { +function toPartialConfig(parsed: ParsedTomlTable): PartialSandcodeConfig { const obsidian = asTable(parsed.obsidian); return { @@ -258,9 +258,9 @@ function toPartialConfig(parsed: ParsedTomlTable): PartialShpitConfig { } function mergePartialConfig( - base: PartialShpitConfig, - override: PartialShpitConfig, -): PartialShpitConfig { + base: PartialSandcodeConfig, + override: PartialSandcodeConfig, +): PartialSandcodeConfig { return { obsidian: { ...(base.obsidian ?? {}), @@ -284,7 +284,7 @@ function normalizeObsidianCommand(command: string | undefined): string { function normalizeIntegrationMode( mode: string | undefined, -): ResolvedShpitConfig["obsidian"]["integrationMode"] { +): ResolvedSandcodeConfig["obsidian"]["integrationMode"] { const normalized = (mode ?? "desktop").trim() || "desktop"; if (normalized !== "desktop" && normalized !== "headless") { throw new Error( @@ -324,8 +324,8 @@ function candidateProjectConfigPaths(startDir: string): string[] { let current = path.resolve(startDir); while (true) { - candidates.push(path.join(current, "shpit.toml")); - candidates.push(path.join(current, ".shpit.toml")); + candidates.push(path.join(current, "sandcode.toml")); + candidates.push(path.join(current, ".sandcode.toml")); const parent = path.dirname(current); if (parent === current) { break; @@ -350,10 +350,10 @@ function getGlobalConfigPath(): string | undefined { if (!home) { return undefined; } - return path.join(home, ".config", "opencode", "shpit.toml"); + return path.join(home, ".config", "sandcode", "sandcode.toml"); } -async function loadConfigAtPath(filePath: string | undefined): Promise { +async function loadConfigAtPath(filePath: string | undefined): Promise { if (!filePath || !(await fileExists(filePath))) { return {}; } @@ -362,7 +362,7 @@ async function loadConfigAtPath(filePath: string | undefined): Promise { +export async function resolveSandcodeConfig(cwd = process.cwd()): Promise { const globalConfigPath = getGlobalConfigPath(); const projectConfigPath = await findProjectConfigPath(cwd); @@ -462,7 +462,7 @@ export async function resolveShpitConfig(cwd = process.cwd()): Promise { const home = process.env.HOME; - const globalEnvPath = home ? path.join(home, ".config", "opencode", ".env") : undefined; + const globalEnvPath = home ? path.join(home, ".config", "sandcode", ".env") : undefined; const projectConfigPath = await findProjectConfigPath(cwd); const projectEnvPath = projectConfigPath ? path.join(path.dirname(projectConfigPath), ".env") diff --git a/src/sandcode.ts b/src/sandcode.ts new file mode 100644 index 0000000..da518bf --- /dev/null +++ b/src/sandcode.ts @@ -0,0 +1,79 @@ +import { existsSync } from "node:fs"; +import process from "node:process"; +import pkg from "../package.json" with { type: "json" }; +import { runAnalyzeCli } from "./analyze-repos.js"; +import { runSetupCli } from "./setup.js"; +import { runStartCli } from "./start-opencode-daytona.js"; + +function formatRootHelp(): string { + return `sandcode ${pkg.version} + +Usage: + sandcode + sandcode + sandcode [options] + +Commands: + analyze Audit one or more repositories in Daytona sandboxes + start Launch OpenCode web in a Daytona sandbox + setup Configure Obsidian integration and local credentials + help Show this help + +Examples: + bunx sandcode https://github.com/octocat/Hello-World + bunx sandcode links.md + bunx sandcode start + bunx sandcode setup +`; +} + +function isLikelyUrl(value: string): boolean { + return value.startsWith("http://") || value.startsWith("https://"); +} + +export async function runSandcodeCli(args = process.argv.slice(2)): Promise { + const [command, ...rest] = args; + + if (!command || command === "help" || command === "--help" || command === "-h") { + console.log(formatRootHelp()); + return 0; + } + + if (command === "--version" || command === "-v") { + console.log(pkg.version); + return 0; + } + + if (command === "start") { + return await runStartCli(rest); + } + + if (command === "setup") { + return await runSetupCli(rest); + } + + if (command === "analyze") { + return await runAnalyzeCli(rest); + } + + if (isLikelyUrl(command)) { + return await runAnalyzeCli(args); + } + + if (!command.startsWith("-") && existsSync(command)) { + return await runAnalyzeCli(args); + } + + console.error(`sandcode: unknown command or target "${command}"`); + console.log(formatRootHelp()); + return 1; +} + +if (import.meta.main) { + const exitCode = await runSandcodeCli().catch((error: unknown) => { + const message = error instanceof Error ? (error.stack ?? error.message) : String(error); + console.error(`sandcode: ${message}`); + return 1; + }); + process.exit(exitCode); +} diff --git a/src/setup-core.ts b/src/setup-core.ts new file mode 100644 index 0000000..0468817 --- /dev/null +++ b/src/setup-core.ts @@ -0,0 +1,498 @@ +import { execFile } from "node:child_process"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import process from "node:process"; +import { parseArgs, promisify } from "node:util"; +import { + type LoadedEnvInfo, + loadConfiguredEnv, + type ResolvedSandcodeConfig, + resolveSandcodeConfig, +} from "./sandcode-config.js"; + +const execFileAsync = promisify(execFile); + +export type SetupCliOptions = { + yes: boolean; + vaultPath?: string; + notesRoot?: string; + catalogMode?: "date" | "repo"; + openAfterCatalog?: boolean; + integrationMode?: "desktop" | "headless"; + syncAfterCatalog?: boolean; + syncTimeoutSec?: number; + daytonaApiKey?: string; + opencodeApiKey?: string; +}; + +export type SetupState = { + enableObsidian: boolean; + integrationMode: "desktop" | "headless"; + vaultPath: string; + notesRoot: string; + catalogMode: "date" | "repo"; + openAfterCatalog: boolean; + syncAfterCatalog: boolean; + syncTimeoutSec: number; + syncTimeoutInput: string; + syncTimeoutError?: string; + daytonaApiKey: string; + opencodeApiKey: string; +}; + +export type SetupContext = { + loadedEnv: LoadedEnvInfo; + existingConfig: ResolvedSandcodeConfig; + home: string; + configDir: string; + configPath: string; + envPath: string; + obsidianBinary?: string; + headlessBinary?: string; + hasExistingConfig: boolean; + recommendedIntegrationMode: "desktop" | "headless"; + headlessCommand: string; +}; + +export type SetupProgressEvent = { + level: "info" | "warn"; + message: string; +}; + +export type SetupResult = { + configPath: string; + envPath?: string; + seededKeys: string[]; + warnings: string[]; +}; + +function parsePositiveInteger(value: string, label: string): number { + const parsed = Number.parseInt(value, 10); + if (!Number.isInteger(parsed) || parsed <= 0) { + throw new Error(`${label} must be a positive integer. Received "${value}".`); + } + return parsed; +} + +function expandHomeDir(value: string | undefined): string | undefined { + if (!value) { + return undefined; + } + if (!value.startsWith("~")) { + return value; + } + const home = process.env.HOME; + if (!home) { + return value; + } + return path.join(home, value.slice(1)); +} + +export function formatSetupHelp(invocation = "sandcode setup"): string { + return `Usage: ${invocation} [options] + +Options: + -y, --yes Non-interactive setup using defaults/flags + --vault-path Obsidian vault path (absolute or ~/...) + --notes-root Folder inside vault for research notes (default: Research/Sandcode) + --catalog-mode date | repo (default: date) + --obsidian-integration headless | desktop (default: auto-detect) + --sync-after-catalog Run 'ob sync' after each note write (headless mode) + --sync-timeout-sec Timeout for 'ob sync' (default: 120) + --open-after-catalog Open each new note via obsidian CLI (desktop mode) + --daytona-api-key Seed DAYTONA_API_KEY into ~/.config/sandcode/.env + --opencode-api-key Seed OPENCODE_API_KEY into ~/.config/sandcode/.env + -h, --help Show this help +`; +} + +export function parseSetupCliOptions(args: string[]): SetupCliOptions | undefined { + const { values } = parseArgs({ + args, + options: { + help: { type: "boolean", short: "h", default: false }, + yes: { type: "boolean", short: "y", default: false }, + "vault-path": { type: "string" }, + "notes-root": { type: "string" }, + "catalog-mode": { type: "string" }, + "open-after-catalog": { type: "boolean" }, + "obsidian-integration": { type: "string" }, + "sync-after-catalog": { type: "boolean" }, + "sync-timeout-sec": { type: "string" }, + "daytona-api-key": { type: "string" }, + "opencode-api-key": { type: "string" }, + }, + strict: true, + allowPositionals: false, + }); + + if (values.help) { + return undefined; + } + + const rawCatalogMode = values["catalog-mode"]; + if (rawCatalogMode && rawCatalogMode !== "date" && rawCatalogMode !== "repo") { + throw new Error(`--catalog-mode must be "date" or "repo". Received "${rawCatalogMode}".`); + } + + const rawIntegrationMode = values["obsidian-integration"]; + if (rawIntegrationMode && rawIntegrationMode !== "desktop" && rawIntegrationMode !== "headless") { + throw new Error( + `--obsidian-integration must be "desktop" or "headless". Received "${rawIntegrationMode}".`, + ); + } + + const rawSyncTimeoutSec = values["sync-timeout-sec"]; + let syncTimeoutSec: number | undefined; + if (rawSyncTimeoutSec !== undefined) { + syncTimeoutSec = parsePositiveInteger(rawSyncTimeoutSec, "--sync-timeout-sec"); + } + + return { + yes: values.yes, + vaultPath: values["vault-path"], + notesRoot: values["notes-root"], + catalogMode: rawCatalogMode as "date" | "repo" | undefined, + openAfterCatalog: values["open-after-catalog"], + integrationMode: rawIntegrationMode as "desktop" | "headless" | undefined, + syncAfterCatalog: values["sync-after-catalog"], + syncTimeoutSec, + daytonaApiKey: values["daytona-api-key"], + opencodeApiKey: values["opencode-api-key"], + }; +} + +export async function detectCommandBinary(command: string): Promise { + try { + const { stdout } = await execFileAsync("which", [command]); + const resolved = stdout.trim(); + return resolved || undefined; + } catch { + return undefined; + } +} + +function countRemoteVaults(output: string): number { + return output.split(/\r?\n/).filter((line) => /^\s*[a-f0-9]{32}\s+"/.test(line)).length; +} + +function describeExecError(error: unknown): string { + if (!error || typeof error !== "object") { + return String(error); + } + + const message = "message" in error ? String(error.message) : "unknown error"; + const stdout = "stdout" in error ? String(error.stdout ?? "") : ""; + const stderr = "stderr" in error ? String(error.stderr ?? "") : ""; + const details = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n"); + return details ? `${message}\n${details}` : message; +} + +export async function validateHeadlessSyncAccess(command: string): Promise { + try { + const { stdout, stderr } = await execFileAsync(command, ["sync-list-remote"], { + timeout: 30_000, + maxBuffer: 4 * 1024 * 1024, + }); + return countRemoteVaults(`${stdout}\n${stderr}`); + } catch (error) { + throw new Error( + `Headless Sync preflight failed. Ensure Obsidian Catalyst access, an active Obsidian Sync subscription, and successful \`ob login\`.\n${describeExecError(error)}`, + ); + } +} + +function parseEnvFile(content: string): Map { + const result = new Map(); + const lines = content.split(/\r?\n/); + for (const rawLine of lines) { + const line = rawLine.trim(); + if (!line || line.startsWith("#")) { + continue; + } + const separatorIndex = line.indexOf("="); + if (separatorIndex === -1) { + continue; + } + const key = line.slice(0, separatorIndex).trim(); + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) { + continue; + } + let value = line.slice(separatorIndex + 1).trim(); + if (value.startsWith('"') && value.endsWith('"')) { + value = value.slice(1, -1); + } + if (value.startsWith("'") && value.endsWith("'")) { + value = value.slice(1, -1); + } + result.set(key, value); + } + return result; +} + +async function loadEnvMap(filePath: string): Promise> { + try { + const content = await readFile(filePath, "utf8"); + return parseEnvFile(content); + } catch { + return new Map(); + } +} + +function serializeEnvMap(env: Map): string { + const lines: string[] = []; + lines.push("# Managed by sandcode setup"); + lines.push("# Shell-exported env vars still override these values."); + lines.push(""); + + const keys = [...env.keys()].sort((a, b) => a.localeCompare(b)); + for (const key of keys) { + const value = env.get(key); + if (value === undefined) { + continue; + } + lines.push(`${key}=${JSON.stringify(value)}`); + } + + lines.push(""); + return lines.join("\n"); +} + +function hasProviderKey(envMap: Map): boolean { + return ( + Boolean(process.env.OPENCODE_API_KEY) || + Boolean(process.env.OPENAI_API_KEY) || + Boolean(process.env.ANTHROPIC_API_KEY) || + Boolean(process.env.XAI_API_KEY) || + Boolean(process.env.OPENROUTER_API_KEY) || + Boolean(process.env.ZHIPU_API_KEY) || + Boolean(envMap.get("OPENCODE_API_KEY")) || + Boolean(envMap.get("OPENAI_API_KEY")) || + Boolean(envMap.get("ANTHROPIC_API_KEY")) || + Boolean(envMap.get("XAI_API_KEY")) || + Boolean(envMap.get("OPENROUTER_API_KEY")) || + Boolean(envMap.get("ZHIPU_API_KEY")) + ); +} + +function sanitizeSetupState(state: SetupState): SetupState { + return { + ...state, + vaultPath: expandHomeDir(state.vaultPath)?.trim() ?? "", + notesRoot: state.notesRoot.trim() || "Research/Sandcode", + daytonaApiKey: state.daytonaApiKey.trim(), + opencodeApiKey: state.opencodeApiKey.trim(), + syncTimeoutInput: state.syncTimeoutInput.trim() || String(state.syncTimeoutSec), + syncTimeoutSec: + Number.isInteger(state.syncTimeoutSec) && state.syncTimeoutSec > 0 + ? state.syncTimeoutSec + : 120, + }; +} + +export async function loadSetupContext(cwd = process.cwd()): Promise { + const loadedEnv = await loadConfiguredEnv(cwd); + const existingConfig = await resolveSandcodeConfig(cwd); + + const home = process.env.HOME; + if (!home) { + throw new Error("HOME is not set. Cannot write ~/.config/sandcode files."); + } + + const configDir = path.join(home, ".config", "sandcode"); + const configPath = path.join(configDir, "sandcode.toml"); + const envPath = path.join(configDir, ".env"); + const [obsidianBinary, headlessBinary] = await Promise.all([ + detectCommandBinary("obsidian"), + detectCommandBinary(existingConfig.obsidian.headlessCommand), + ]); + const hasExistingConfig = Boolean( + existingConfig.paths.globalConfigPath ?? existingConfig.paths.projectConfigPath, + ); + const recommendedIntegrationMode = hasExistingConfig + ? existingConfig.obsidian.integrationMode + : headlessBinary + ? "headless" + : "desktop"; + + return { + loadedEnv, + existingConfig, + home, + configDir, + configPath, + envPath, + obsidianBinary, + headlessBinary, + hasExistingConfig, + recommendedIntegrationMode, + headlessCommand: existingConfig.obsidian.headlessCommand, + }; +} + +export function buildInitialSetupState( + context: SetupContext, + options: SetupCliOptions, +): SetupState { + return sanitizeSetupState({ + enableObsidian: Boolean( + options.vaultPath ?? + context.existingConfig.obsidian.vaultPath ?? + context.existingConfig.obsidian.enabled, + ), + integrationMode: options.integrationMode ?? context.recommendedIntegrationMode, + vaultPath: expandHomeDir(options.vaultPath ?? context.existingConfig.obsidian.vaultPath) ?? "", + notesRoot: options.notesRoot ?? context.existingConfig.obsidian.notesRoot, + catalogMode: options.catalogMode ?? context.existingConfig.obsidian.catalogMode, + openAfterCatalog: options.openAfterCatalog ?? context.existingConfig.obsidian.openAfterCatalog, + syncAfterCatalog: options.syncAfterCatalog ?? context.existingConfig.obsidian.syncAfterCatalog, + syncTimeoutSec: options.syncTimeoutSec ?? context.existingConfig.obsidian.syncTimeoutSec, + syncTimeoutInput: String( + options.syncTimeoutSec ?? context.existingConfig.obsidian.syncTimeoutSec, + ), + daytonaApiKey: options.daytonaApiKey ?? process.env.DAYTONA_API_KEY ?? "", + opencodeApiKey: options.opencodeApiKey ?? process.env.OPENCODE_API_KEY ?? "", + }); +} + +export function summarizeSetupContext(context: SetupContext): SetupProgressEvent[] { + return [ + { + level: "info", + message: context.obsidianBinary + ? `Detected Obsidian desktop CLI at ${context.obsidianBinary}` + : "Obsidian desktop CLI not found in PATH (command: obsidian)", + }, + { + level: "info", + message: context.headlessBinary + ? `Detected Obsidian Headless CLI at ${context.headlessBinary}` + : "Obsidian Headless CLI not found in PATH (command: ob)", + }, + { + level: "info", + message: + "Headless mode runs a real preflight against Obsidian Sync using `ob sync-list-remote`.", + }, + ]; +} + +export async function applySetupState( + context: SetupContext, + stateInput: SetupState, + report?: (event: SetupProgressEvent) => void, +): Promise { + const state = sanitizeSetupState(stateInput); + + if (state.enableObsidian && !state.vaultPath) { + throw new Error("Obsidian cataloging is enabled, but no vault path was provided."); + } + + if (state.enableObsidian && state.integrationMode === "desktop" && state.openAfterCatalog) { + if (!context.obsidianBinary) { + throw new Error( + "Desktop integration with open_after_catalog requires the `obsidian` command in PATH.", + ); + } + } + + if (state.enableObsidian && state.integrationMode === "headless") { + const resolvedHeadlessBinary = + context.headlessBinary ?? (await detectCommandBinary(context.headlessCommand)); + if (!resolvedHeadlessBinary) { + throw new Error( + "Headless integration requires `ob` in PATH. Install with: bun add -g obsidian-headless", + ); + } + + report?.({ level: "info", message: "Running Obsidian headless preflight..." }); + const remoteVaultCount = await validateHeadlessSyncAccess(context.headlessCommand); + if (remoteVaultCount > 0) { + report?.({ + level: "info", + message: `Headless preflight passed. Remote vaults visible: ${remoteVaultCount}`, + }); + } else { + report?.({ + level: "warn", + message: + 'Headless preflight succeeded but no remote vaults were found. Create one with `ob sync-create-remote --name "..."`.', + }); + } + } + + await mkdir(context.configDir, { recursive: true }); + + const configLines: string[] = []; + configLines.push("# Managed by sandcode setup"); + configLines.push(""); + configLines.push("[obsidian]"); + configLines.push(`enabled = ${state.enableObsidian ? "true" : "false"}`); + configLines.push('command = "obsidian"'); + configLines.push(`integration_mode = ${JSON.stringify(state.integrationMode)}`); + configLines.push(`headless_command = ${JSON.stringify(context.headlessCommand)}`); + if (state.vaultPath) { + configLines.push(`vault_path = ${JSON.stringify(state.vaultPath)}`); + } + configLines.push(`notes_root = ${JSON.stringify(state.notesRoot)}`); + configLines.push(`catalog_mode = ${JSON.stringify(state.catalogMode)}`); + configLines.push(`sync_after_catalog = ${state.syncAfterCatalog ? "true" : "false"}`); + configLines.push(`sync_timeout_sec = ${state.syncTimeoutSec}`); + configLines.push(`open_after_catalog = ${state.openAfterCatalog ? "true" : "false"}`); + configLines.push(""); + + await writeFile(context.configPath, configLines.join("\n"), "utf8"); + report?.({ level: "info", message: `Wrote ${context.configPath}` }); + + const envMap = await loadEnvMap(context.envPath); + const seededKeys = new Set(); + + const seedIfPresent = (key: string, value: string): void => { + if (!value) { + return; + } + envMap.set(key, value); + seededKeys.add(key); + }; + + seedIfPresent("DAYTONA_API_KEY", state.daytonaApiKey); + seedIfPresent("OPENCODE_API_KEY", state.opencodeApiKey); + + let envPath: string | undefined; + if (envMap.size > 0) { + await writeFile(context.envPath, serializeEnvMap(envMap), "utf8"); + envPath = context.envPath; + report?.({ level: "info", message: `Wrote ${context.envPath}` }); + } + + const warnings: string[] = []; + if (!state.daytonaApiKey && !envMap.get("DAYTONA_API_KEY") && !process.env.DAYTONA_API_KEY) { + warnings.push("DAYTONA_API_KEY is still missing. start/analyze will fail until it is set."); + } + + if (!hasProviderKey(envMap) && !state.opencodeApiKey) { + warnings.push( + "No model auth configured. The built-in default model uses OPENCODE_API_KEY; set it before running sandcode analyze.", + ); + } + + for (const warning of warnings) { + report?.({ level: "warn", message: warning }); + } + + if (seededKeys.size > 0) { + report?.({ + level: "info", + message: `Seeded credential keys: ${[...seededKeys].sort().join(", ")}`, + }); + } + + report?.({ level: "info", message: "Setup complete." }); + + return { + configPath: context.configPath, + envPath, + seededKeys: [...seededKeys].sort(), + warnings, + }; +} diff --git a/src/setup-ui-state.ts b/src/setup-ui-state.ts new file mode 100644 index 0000000..24f9314 --- /dev/null +++ b/src/setup-ui-state.ts @@ -0,0 +1,3 @@ +export function getNextWizardStepIndex(currentStepIndex: number, stepCount: number): number { + return Math.min(currentStepIndex + 1, Math.max(stepCount - 1, 0)); +} diff --git a/src/setup-ui.test.ts b/src/setup-ui.test.ts new file mode 100644 index 0000000..e4011a0 --- /dev/null +++ b/src/setup-ui.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from "bun:test"; +import { getNextWizardStepIndex } from "./setup-ui-state.js"; + +describe("getNextWizardStepIndex", () => { + test("advances to the next step when the wizard still has remaining steps", () => { + expect(getNextWizardStepIndex(1, 5)).toBe(2); + }); + + test("clamps to the final available step when the step count shrinks", () => { + expect(getNextWizardStepIndex(3, 2)).toBe(1); + }); + + test("returns zero when no steps remain", () => { + expect(getNextWizardStepIndex(0, 0)).toBe(0); + }); +}); diff --git a/src/setup-ui.tsx b/src/setup-ui.tsx new file mode 100644 index 0000000..4629869 --- /dev/null +++ b/src/setup-ui.tsx @@ -0,0 +1,753 @@ +import { createCliRenderer } from "@opentui/core"; +import { render, useKeyboard } from "@opentui/solid"; +import { createMemo, createSignal, For, Show } from "solid-js"; +import { + applySetupState, + type SetupContext, + type SetupProgressEvent, + type SetupResult, + type SetupState, + summarizeSetupContext, +} from "./setup-core.js"; +import { getNextWizardStepIndex } from "./setup-ui-state.js"; + +type WizardChoice = { + name: string; + description: string; + value: string; +}; + +type WizardStep = + | { + kind: "choice"; + key: string; + title: string; + eyebrow: string; + description: string; + hint: string; + value: string; + options: WizardChoice[]; + commit: (draft: SetupState, value: string) => void; + } + | { + kind: "input"; + key: string; + title: string; + eyebrow: string; + description: string; + hint: string; + value: string; + placeholder: string; + commit: (draft: SetupState, value: string) => void; + } + | { + kind: "summary"; + key: string; + }; + +function appendStatus( + lines: SetupProgressEvent[], + event: SetupProgressEvent, +): SetupProgressEvent[] { + return [...lines, event].slice(-10); +} + +function buildSteps(state: SetupState): WizardStep[] { + const steps: WizardStep[] = [ + { + kind: "choice", + key: "enable-obsidian", + eyebrow: "Setup", + title: "Catalog findings into Obsidian?", + description: "Enable note creation for research runs and keep the workflow in one place.", + hint: "Left/right changes the option. Enter commits it. Esc goes back.", + value: state.enableObsidian ? "yes" : "no", + options: [ + { name: "Yes", description: "Write findings into your vault automatically.", value: "yes" }, + { name: "No", description: "Skip vault integration for now.", value: "no" }, + ], + commit: () => {}, + }, + ]; + + if (state.enableObsidian) { + steps.push( + { + kind: "choice", + key: "integration-mode", + eyebrow: "Obsidian", + title: "Choose the integration mode", + description: + "Headless works well for automation. Desktop is simpler if you already use the Obsidian CLI locally.", + hint: "Enter commits the current mode.", + value: state.integrationMode, + options: [ + { + name: "Headless", + description: "Best for automation. Uses `ob` and can sync after note writes.", + value: "headless", + }, + { + name: "Desktop", + description: "Uses the `obsidian` command and can open notes after creation.", + value: "desktop", + }, + ], + commit: () => {}, + }, + { + kind: "input", + key: "vault-path", + eyebrow: "Obsidian", + title: "Where is the vault?", + description: + "Use an absolute path or `~/...`. This is required when cataloging is enabled.", + hint: "Type the path and press Enter to continue.", + value: state.vaultPath, + placeholder: "/Users/you/vaults/research", + commit: () => {}, + }, + { + kind: "input", + key: "notes-root", + eyebrow: "Obsidian", + title: "Which folder should Sandcode write into?", + description: "This becomes the root path inside the vault for all generated notes.", + hint: "Press Enter to continue.", + value: state.notesRoot, + placeholder: "Research/Sandcode", + commit: () => {}, + }, + { + kind: "choice", + key: "catalog-mode", + eyebrow: "Obsidian", + title: "How should notes be grouped?", + description: + "Date mode is broad and chronological. Repo mode clusters repeat analyses together.", + hint: "Enter commits the grouping mode.", + value: state.catalogMode, + options: [ + { + name: "Date", + description: "Folders by date. Good for broad research sessions.", + value: "date", + }, + { + name: "Repo", + description: "Folders by repository. Better for revisiting the same codebase.", + value: "repo", + }, + ], + commit: () => {}, + }, + ); + + if (state.integrationMode === "headless") { + steps.push( + { + kind: "choice", + key: "sync-after-catalog", + eyebrow: "Headless", + title: "Run `ob sync` after each note write?", + description: + "Useful if you want findings to appear remotely without a separate sync step.", + hint: "Enter commits the choice.", + value: state.syncAfterCatalog ? "yes" : "no", + options: [ + { name: "Yes", description: "Run headless sync after writes.", value: "yes" }, + { name: "No", description: "Leave syncing manual.", value: "no" }, + ], + commit: () => {}, + }, + { + kind: "input", + key: "sync-timeout", + eyebrow: "Headless", + title: "How long should sync wait before timing out?", + description: "Only used in headless mode.", + hint: "Enter a positive number of seconds and press Enter.", + value: state.syncTimeoutInput, + placeholder: "120", + commit: () => {}, + }, + ); + } else { + steps.push({ + kind: "choice", + key: "open-after-catalog", + eyebrow: "Desktop", + title: "Open each created note in Obsidian?", + description: "Convenient during live research. Leave it off if you want a quieter flow.", + hint: "Enter commits the choice.", + value: state.openAfterCatalog ? "yes" : "no", + options: [ + { name: "Yes", description: "Launch each note after creation.", value: "yes" }, + { name: "No", description: "Only write the files.", value: "no" }, + ], + commit: () => {}, + }); + } + } + + steps.push( + { + kind: "input", + key: "daytona-key", + eyebrow: "Credentials", + title: "DAYTONA_API_KEY", + description: "Required for `sandcode start` and `sandcode analyze`.", + hint: "Visible as typed in this terminal. Leave blank to keep the current shell or env-file value.", + value: state.daytonaApiKey, + placeholder: "daytona_...", + commit: () => {}, + }, + { + kind: "input", + key: "opencode-key", + eyebrow: "Credentials", + title: "OPENCODE_API_KEY", + description: "Required for the built-in opencode-go model defaults.", + hint: "Visible as typed in this terminal. Leave blank to keep the current shell or env-file value.", + value: state.opencodeApiKey, + placeholder: "oc_...", + commit: () => {}, + }, + { + kind: "summary", + key: "summary", + }, + ); + + return steps.map((step) => { + if (step.key === "enable-obsidian") { + return { + ...step, + commit: (draft: SetupState, value: string) => { + draft.enableObsidian = value === "yes"; + if (!draft.enableObsidian) { + draft.vaultPath = ""; + } + }, + }; + } + + if (step.key === "integration-mode") { + return { + ...step, + commit: (draft: SetupState, value: string) => { + draft.integrationMode = value === "headless" ? "headless" : "desktop"; + if (draft.integrationMode === "headless") { + draft.openAfterCatalog = false; + } else { + draft.syncAfterCatalog = false; + } + }, + }; + } + + if (step.key === "vault-path") { + return { + ...step, + commit: (draft: SetupState, value: string) => (draft.vaultPath = value.trim()), + }; + } + + if (step.key === "notes-root") { + return { + ...step, + commit: (draft: SetupState, value: string) => (draft.notesRoot = value.trim()), + }; + } + + if (step.key === "catalog-mode") { + return { + ...step, + commit: (draft: SetupState, value: string) => { + draft.catalogMode = value === "repo" ? "repo" : "date"; + }, + }; + } + + if (step.key === "sync-after-catalog") { + return { + ...step, + commit: (draft: SetupState, value: string) => { + draft.syncAfterCatalog = value === "yes"; + }, + }; + } + + if (step.key === "sync-timeout") { + return { + ...step, + commit: (draft: SetupState, value: string) => { + draft.syncTimeoutInput = value; + const parsed = Number.parseInt(value.trim(), 10); + if (Number.isInteger(parsed) && parsed > 0) { + draft.syncTimeoutSec = parsed; + draft.syncTimeoutError = undefined; + return; + } + draft.syncTimeoutError = "Sync timeout must be a positive integer."; + }, + }; + } + + if (step.key === "open-after-catalog") { + return { + ...step, + commit: (draft: SetupState, value: string) => { + draft.openAfterCatalog = value === "yes"; + }, + }; + } + + if (step.key === "daytona-key") { + return { + ...step, + commit: (draft: SetupState, value: string) => (draft.daytonaApiKey = value.trim()), + }; + } + + if (step.key === "opencode-key") { + return { + ...step, + commit: (draft: SetupState, value: string) => (draft.opencodeApiKey = value.trim()), + }; + } + + return step; + }); +} + +function stateSnapshot(state: SetupState): string[] { + const lines = [ + `Obsidian: ${state.enableObsidian ? "enabled" : "disabled"}`, + `Mode: ${state.integrationMode}`, + ]; + + if (state.enableObsidian) { + lines.push(`Vault: ${state.vaultPath || "(missing)"}`); + lines.push(`Notes root: ${state.notesRoot || "(missing)"}`); + lines.push(`Catalog: ${state.catalogMode}`); + lines.push( + state.integrationMode === "headless" + ? `Sync after catalog: ${state.syncAfterCatalog ? "yes" : "no"}` + : `Open after catalog: ${state.openAfterCatalog ? "yes" : "no"}`, + ); + } + + lines.push(`DAYTONA_API_KEY: ${state.daytonaApiKey ? "seeded" : "not set"}`); + lines.push(`OPENCODE_API_KEY: ${state.opencodeApiKey ? "seeded" : "not set"}`); + return lines; +} + +function SetupWizard(props: { + context: SetupContext; + initialState: SetupState; + complete: (result: SetupResult) => void; + cancel: (error: Error) => void; +}) { + const [state, setState] = createSignal({ ...props.initialState }); + const [stepIndex, setStepIndex] = createSignal(0); + const [phase, setPhase] = createSignal<"wizard" | "saving" | "done" | "error">("wizard"); + const [statusLines, setStatusLines] = createSignal( + summarizeSetupContext(props.context), + ); + const [errorMessage, setErrorMessage] = createSignal(); + const [result, setResult] = createSignal(); + + const steps = createMemo(() => buildSteps({ ...state() })); + const activeStep = createMemo(() => steps()[Math.min(stepIndex(), steps().length - 1)]); + const activeChoiceStep = createMemo(() => { + const step = activeStep(); + return step.kind === "choice" ? step : undefined; + }); + const activeInputStep = createMemo(() => { + const step = activeStep(); + return step.kind === "input" ? step : undefined; + }); + const activeChoiceIndex = createMemo(() => { + const step = activeChoiceStep(); + if (!step) { + return 0; + } + const index = step.options.findIndex((option) => option.value === step.value); + return index >= 0 ? index : 0; + }); + + const goBack = (): void => { + if (phase() === "saving") { + return; + } + + if (phase() === "wizard" && stepIndex() > 0) { + setStepIndex((value) => value - 1); + return; + } + + if (phase() === "done") { + const current = result(); + if (current) { + props.complete(current); + } + return; + } + + if (phase() === "error") { + setPhase("wizard"); + setErrorMessage(undefined); + return; + } + + props.cancel(new Error("Setup cancelled.")); + }; + + const commitAndAdvance = (commit: () => void): void => { + commit(); + const nextSteps = steps(); + setStepIndex((value) => getNextWizardStepIndex(value, nextSteps.length)); + }; + + let saveInFlight = false; + const save = async (): Promise => { + if (saveInFlight) { + return; + } + saveInFlight = true; + setPhase("saving"); + setErrorMessage(undefined); + setStatusLines((current) => + appendStatus(current, { level: "info", message: "Writing Sandcode configuration..." }), + ); + + try { + const saved = await applySetupState(props.context, state(), (event) => { + setStatusLines((current) => appendStatus(current, event)); + }); + setResult(saved); + setPhase("done"); + } catch (error) { + setErrorMessage(error instanceof Error ? error.message : String(error)); + setPhase("error"); + } finally { + saveInFlight = false; + } + }; + + useKeyboard( + (event: { + ctrl: boolean; + name: string; + preventDefault: () => void; + stopPropagation: () => void; + }) => { + if (event.ctrl && event.name === "c") { + event.preventDefault(); + event.stopPropagation(); + props.cancel(new Error("Setup cancelled.")); + return; + } + + if (event.name === "escape") { + event.preventDefault(); + event.stopPropagation(); + goBack(); + return; + } + + if ((phase() === "done" || phase() === "error") && event.name === "return") { + event.preventDefault(); + event.stopPropagation(); + goBack(); + } + }, + ); + + return ( + + + + sandcode + + {" "} + Daytona research, OpenCode sandboxes, and a setup flow worth reusing. + + + + + + + + Path + + + {(step, index) => ( + + {index() === stepIndex() ? "› " : " "} + {step.kind === "summary" ? "Review and save" : step.title} + + )} + + + + + Current state + + {(line) => {line}} + + + + + Signals + + + {(entry) => ( + {entry.message} + )} + + + + + + + + + {(stepAccessor) => ( + <> + {stepAccessor().eyebrow} + + {stepAccessor().title} + + {stepAccessor().description} + {stepAccessor().hint} + { + if (!option) { + return; + } + setState((current) => { + const next = { ...current }; + stepAccessor().commit(next, option.value); + return next; + }); + }} + onSelect={(_index: number, option: WizardChoice | null) => { + if (!option) { + return; + } + commitAndAdvance(() => { + setState((current) => { + const next = { ...current }; + stepAccessor().commit(next, option.value); + return next; + }); + }); + }} + /> + + )} + + + + {(stepAccessor) => ( + <> + {stepAccessor().eyebrow} + + {stepAccessor().title} + + {stepAccessor().description} + {stepAccessor().hint} + + {state().syncTimeoutError} + + { + setState((current) => { + const next = { ...current }; + stepAccessor().commit(next, value); + return next; + }); + }} + onSubmit={(value: string) => { + const parsed = Number.parseInt(value.trim(), 10); + const shouldAdvance = + stepAccessor().key !== "sync-timeout" || + (Number.isInteger(parsed) && parsed > 0); + + setState((current) => { + const next = { ...current }; + stepAccessor().commit(next, value); + return next; + }); + + if (shouldAdvance) { + setStepIndex((current) => + getNextWizardStepIndex(current, steps().length), + ); + } + }} + /> + + )} + + + } + > + + Ready + + Review your configuration + + + Press Enter on Save to write `sandcode.toml` and the optional `.env` file. + + + + {(line) => {line}} + + + { + if (option?.value === "cancel") { + props.cancel(new Error("Setup cancelled.")); + return; + } + void save(); + }} + /> + + + + + + + Writing + + Applying configuration + + Running validations and writing files. Stay on this screen. + + + + + + Complete + + Sandcode is configured. + + Press Enter or Esc to leave setup. + + {(saved) => ( + + Config: {saved().configPath} + + {(envPath) => Env: {envPath()}} + + + )} + + + + + + + Validation failed + {errorMessage()} + Press Enter or Esc to go back and edit the setup values. + + + + + + ); +} + +export async function runSetupTui( + context: SetupContext, + initialState: SetupState, +): Promise { + const renderer = await createCliRenderer(); + + return await new Promise((resolve, reject) => { + let settled = false; + + const finalize = (callback: () => void): void => { + if (settled) { + return; + } + settled = true; + renderer.destroy(); + callback(); + }; + + void render( + () => ( + finalize(() => resolve(result))} + cancel={(error) => finalize(() => reject(error))} + /> + ), + renderer, + ); + }); +} diff --git a/src/setup.ts b/src/setup.ts new file mode 100644 index 0000000..751942d --- /dev/null +++ b/src/setup.ts @@ -0,0 +1,63 @@ +import process from "node:process"; +import { + applySetupState, + buildInitialSetupState, + formatSetupHelp, + loadSetupContext, + parseSetupCliOptions, + summarizeSetupContext, +} from "./setup-core.js"; +import { runSetupTui } from "./setup-ui.js"; + +function logProgress(prefix: string, event: { level: "info" | "warn"; message: string }): void { + const log = event.level === "warn" ? console.warn : console.log; + log(`${prefix}${event.message}`); +} + +export async function runSetupCli(args = process.argv.slice(2)): Promise { + const options = parseSetupCliOptions(args); + if (!options) { + console.log(formatSetupHelp()); + return 0; + } + + const context = await loadSetupContext(); + if (context.loadedEnv.keysLoaded.length > 0) { + console.log( + `[setup] Loaded ${context.loadedEnv.keysLoaded.length} env var(s) from config (.env) files.`, + ); + } + + if (options.yes) { + for (const event of summarizeSetupContext(context)) { + logProgress("[setup] ", event); + } + + await applySetupState(context, buildInitialSetupState(context, options), (event) => { + logProgress("[setup] ", event); + }); + return 0; + } + + if (!process.stdin.isTTY || !process.stdout.isTTY) { + throw new Error( + "Interactive setup requires a TTY. Re-run with `sandcode setup --yes` and flags.", + ); + } + + const result = await runSetupTui(context, buildInitialSetupState(context, options)); + console.log(`[setup] Config: ${result.configPath}`); + if (result.envPath) { + console.log(`[setup] Env: ${result.envPath}`); + } + return 0; +} + +if (import.meta.main) { + const exitCode = await runSetupCli().catch((error: unknown) => { + const message = error instanceof Error ? (error.stack ?? error.message) : String(error); + console.error(`[setup] Failed: ${message}`); + return 1; + }); + process.exit(exitCode); +} diff --git a/src/start-opencode-daytona.ts b/src/start-opencode-daytona.ts index 7e22f27..578ba59 100644 --- a/src/start-opencode-daytona.ts +++ b/src/start-opencode-daytona.ts @@ -5,7 +5,7 @@ import { setTimeout as sleep } from "node:timers/promises"; import { parseArgs } from "node:util"; import { Daytona, type Sandbox } from "@daytonaio/sdk"; import { buildInstallOpencodeCommand } from "./opencode-cli.js"; -import { loadConfiguredEnv } from "./shpit-config.js"; +import { loadConfiguredEnv } from "./sandcode-config.js"; type CliOptions = { port: number; @@ -51,8 +51,24 @@ function parsePort(value: string): number { return parsed; } -function parseCliOptions(): CliOptions { +export function formatStartHelp(invocation = "sandcode start"): string { + return `Usage: ${invocation} [options] + +Options: + -p, --port Port to expose OpenCode web server (default: 3000) + --target Daytona target override + --sandbox-name Custom sandbox name + --create-timeout-sec Sandbox creation timeout seconds (default: 180) + --install-timeout-sec OpenCode install timeout seconds (default: 900) + --keep-sandbox Keep sandbox after stopping (default: false) + --no-open Do not auto-open OpenCode URL + -h, --help Show this help +`; +} + +function parseCliOptions(args: string[]): CliOptions | undefined { const { values } = parseArgs({ + args, options: { help: { type: "boolean", short: "h", default: false }, port: { type: "string", short: "p", default: "3000" }, @@ -68,19 +84,7 @@ function parseCliOptions(): CliOptions { }); if (values.help) { - console.log(`Usage: bun run start -- [options] - -Options: - -p, --port Port to expose OpenCode web server (default: 3000) - --target Daytona target override - --sandbox-name Custom sandbox name - --create-timeout-sec Sandbox creation timeout seconds (default: 180) - --install-timeout-sec OpenCode install timeout seconds (default: 900) - --keep-sandbox Keep sandbox after stopping (default: false) - --no-open Do not auto-open OpenCode URL - -h, --help Show this help -`); - process.exit(0); + return undefined; } return { @@ -326,7 +330,7 @@ async function streamCommandLogsUntilExit(params: { } } -async function main(): Promise { +export async function runStartCli(args = process.argv.slice(2)): Promise { const loadedEnv = await loadConfiguredEnv(); if (loadedEnv.keysLoaded.length > 0) { console.log( @@ -334,7 +338,11 @@ async function main(): Promise { ); } - const options = parseCliOptions(); + const options = parseCliOptions(args); + if (!options) { + console.log(formatStartHelp()); + return 0; + } const apiKey = requireEnv("DAYTONA_API_KEY"); const apiUrl = process.env.DAYTONA_API_URL; const effectiveTarget = options.target ?? process.env.DAYTONA_TARGET; @@ -505,10 +513,15 @@ async function main(): Promise { } finally { await cleanup(); } + + return 0; } -main().catch((error: unknown) => { - const message = error instanceof Error ? (error.stack ?? error.message) : String(error); - console.error(`[local] Failed: ${message}`); - process.exit(1); -}); +if (import.meta.main) { + const exitCode = await runStartCli().catch((error: unknown) => { + const message = error instanceof Error ? (error.stack ?? error.message) : String(error); + console.error(`[local] Failed: ${message}`); + return 1; + }); + process.exit(exitCode); +} diff --git a/tsconfig.json b/tsconfig.json index f944e7d..438cfd7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,12 +3,15 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", + "jsx": "preserve", + "jsxImportSource": "@opentui/solid", "strict": true, "skipLibCheck": true, "esModuleInterop": true, "resolveJsonModule": true, - "forceConsistentCasingInFileNames": true + "forceConsistentCasingInFileNames": true, + "types": ["bun-types"] }, - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "src/**/*.tsx", "scripts/**/*.ts"], "exclude": ["src/**/*.test.ts"] } From d72e0ffc3d00f831d0cd6712c477e047c62c8686 Mon Sep 17 00:00:00 2001 From: Anand Pant Date: Sat, 7 Mar 2026 12:29:57 -0600 Subject: [PATCH 2/2] chore: align local opencode config with sandcode defaults --- opencode.jsonc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opencode.jsonc b/opencode.jsonc index 54801e2..647ca3b 100644 --- a/opencode.jsonc +++ b/opencode.jsonc @@ -1,4 +1,4 @@ { "$schema": "https://opencode.ai/config.json", - "model": "openai/gpt-5.3-codex" + "model": "opencode-go/glm-5" }