diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd797df..e81d48b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,4 +25,5 @@ jobs: - run: npm run build - run: npm test - run: npm run check:file-size + - run: npm run check:audit - run: npm pack --dry-run diff --git a/.gitignore b/.gitignore index 31050ab..c701233 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,11 @@ coverage/ *.log logs/ tmp/ -/.opencode-agent/ - # Local runtime/dogfood state and other-tool configs — not part of the CLI repo. /.apeironcode-agent/ +# Repair-loop checkpoints (Phase 20B) and UI-smoke artifacts (Phase 20D). +.apeironcode/checkpoints/ +.apeironcode/ui-smoke/ /.agents/ /.codex/ /.claude/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 12c45be..2377f5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,8 @@ ### Changed -- `APEIRONCODE_TEST_OFFLINE` is now the preferred sandbox offline flag; `OPENCODE_TEST_OFFLINE` is kept as a deprecated legacy alias. -- Documentation now uses the `APEIRONCODE_AUTOMATION*` environment variables as the primary names (the `OPENCODE_AUTOMATION*` aliases remain deprecated). +- `APEIRONCODE_TEST_OFFLINE` disables sandbox backend probing in offline test runs. +- Documentation and automation use only the `APEIRONCODE_*` environment namespace. ### Added @@ -150,7 +150,7 @@ - `apeironcode lsp symbols` and `/lsp symbols` try live `documentSymbol` first and preserve fallback behavior when live LSP is missing or fails - `apeironcode lsp diagnostics`, `apeironcode lsp definition`, and `apeironcode lsp references` now report live-vs-fallback results instead of placeholder or ambiguous output - the shared agent project context now enriches relevant workflows with capped diagnostics and reports diagnostics source, files checked, counts, and fallback reasons in final summaries -- agent loop now supports multi-tool turns, ``, and malformed tool-call retry prompts +- agent loop now supports multi-tool turns, ``, and malformed tool-call retry prompts - one-shot CLI preamble, final execution summary, session metadata, and TUI status now share the same effective-mode resolution - slash workflow help now includes `/commands`, usage examples, and better missing-argument guidance - approval prompts, provider diagnostics, slash output, and error panels now safely stringify unexpected structured values diff --git a/LICENSE b/LICENSE index 33916df..537d7dc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 ApeironCode Agent contributors (formerly OpenCode Agent) +Copyright (c) 2026 ApeironCode contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/README.md b/README.md index d231cd2..f3e75f5 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,85 @@ Verify the installation: ```bash apeironcode --help -apeironcode setup --provider mock +apeironcode demo apeironcode doctor ``` -Mock mode is deterministic and needs no API key. For real work, configure -[Ollama or a cloud provider](./docs/providers.md). +## Try the demo (no API key) + +`apeironcode demo` runs deterministic, local examples — no API key, model, or +Ollama required: + +```bash +apeironcode demo todo-app # scaffold a working static todo app +apeironcode demo fix-test # reproduce a failing test, then fix it to green +apeironcode demo improve-ui # turn a plain page into a premium one +apeironcode demo review # deterministic review of a sample diff +``` + +Demo mode uses deterministic local examples, not a model. For real projects, +configure [Ollama or a cloud provider](./docs/providers.md). The `mock` +provider is a deterministic test/demo stub, **not** a real coding model. + +## Four workflows + +ApeironCode is a local-first coding agent centered on four workflows, all with +approval-gated writes and commands: + +```bash +apeironcode fix # detect failures, propose fixes, rerun checks +apeironcode new "a todo app" # create a new app from a template +apeironcode build "a CRM dashboard"# build also creates apps from templates +apeironcode improve "premium UI" # improve UI or code quality +apeironcode review # review the current changes +``` + +## Build an app + +`apeironcode new` (and `apeironcode build`) create a small app from a +**deterministic template** — no API key or model needed: + +```bash +apeironcode new "premium todo app" +apeironcode new "CRM dashboard" --stack dashboard +apeironcode new "landing page for an AI startup" --style premium-saas +apeironcode build "a React task app" --stack vite --dir my-app +``` + +It picks a template, shows an approval bundle, writes real files, and validates +them (linked assets, `node --check`, a design checklist). Static apps +(todo/landing/dashboard) are the most reliable and need no install; Vite + React +is alpha and needs a manual `npm install`. Auth/database/billing requests become +**mock/local UI**, not real backends — the report is honest about scope. A +non-empty directory is never overwritten silently. See +[docs/build-apps.md](./docs/build-apps.md). + +Verify a generated app actually renders in a browser: + +```bash +apeironcode test-ui ./my-app # page load, missing assets, console errors, screenshot +apeironcode new "todo app" --ui-smoke # run the smoke during creation +``` + +`test-ui` uses Playwright/Chromium when available; without a browser it reports +*skipped*, never a fake pass (`npx playwright install chromium` to enable). It is +not a full QA replacement. See [docs/ui-smoke.md](./docs/ui-smoke.md). + +`apeironcode fix` detects your project's check commands (typecheck/lint/test/ +build), runs them, proposes an **approval-gated** minimal patch, applies it, and +reruns the checks. Use `--dry-run` to preview, `--safe` to restrict the edit +scope, and `--until-green` to loop. See [docs/fix-code.md](./docs/fix-code.md). +It is alpha — you review and approve every change, and repair quality depends on +your configured provider. + +Workflows render a compact progress board (plan, files, command log, artifacts) +with one approval bundle before any write or command. High-risk commands +(`rm -rf`, `sudo`, `curl | sh`, `npm publish`, `git push --force`) are never +auto-approved. `apeironcode fix` checkpoints before applying, so you can +`apeironcode rollback --last`. See [docs/terminal-ux.md](./docs/terminal-ux.md). + +`apeironcode --help` is beginner-focused. Run `apeironcode help developer` or +`apeironcode help advanced` for the full command set. ## See It Work @@ -116,7 +189,7 @@ See the [complete action example](./examples/github-action-pr-review/). - Terminal-native interactive chat built with Commander.js, Ink, and React. - One-shot execution for direct prompts from the shell. - `apeironcode doctor` diagnostics and `apeironcode provider test` smoke checks. -- Optional Project Brain: `apeironcode brain plan` previews a user-approved `.apeironcode/` workspace memory for long app builds, with large-app orchestration, PLAN/TASKS diff-merge, a persistent sync-preview store, token-efficient context planning, and agent routing. All writes require explicit `--yes` — no silent creation. Runtime brain intelligence adds deterministic intent classification (<1ms) that selects relevant brain context per prompt, surfaced via the `brain.runtime`/`brain.explain` CLI. (A VS Code companion exists in development but is not part of this CLI alpha.) +- Optional Project Brain: `apeironcode brain plan` previews a user-approved `.apeironcode/` workspace memory for long app builds, with large-app orchestration, PLAN/TASKS diff-merge, a persistent sync-preview store, token-efficient context planning, and agent routing. All writes require explicit `--yes` — no silent creation. Runtime brain intelligence adds deterministic intent classification (<1ms) that selects relevant brain context per prompt, surfaced via the `brain.runtime`/`brain.explain` CLI. - First-run setup with `apeironcode setup`, no-key mock-provider trial mode, `/setup`, and setup status/reset commands. - TUI product-home flow with command echo, compact dashboard sections, `/commands beginner`, natural aliases such as `/show memory`, and temp-HOME smoke helpers via `npm run demo:tui`. - Provider readiness UX with `apeironcode provider list`, `apeironcode provider setup`, `apeironcode provider fallback`, `apeironcode provider doctor`, `apeironcode model list`, and `apeironcode model recommend`. @@ -155,7 +228,8 @@ See the [complete action example](./examples/github-action-pr-review/). - Lifecycle hooks wired into sessions, planning, tools, edits, commands, commits, memory suggestions, and skill runs, with disabled-by-default shell execution and explicit approval requirement. - Token-efficient repo brain modules for file summaries, dependency edges, context packing, prompt-context injection, and budget reporting. - Typed code quality workflow recipes for feature implementation, test fixes, debugging, review, refactor, tests, dependency upgrades, security/performance audits, docs, and release prep, with dry-run, report storage, and runtime execution through the agent loop. -- Honest sandbox status detection with `apeironcode sandbox status`; OS-level sandboxed command execution is not implemented. +- Central command policy with `apeironcode sandbox status`: blocked commands are refused before execution, Docker is used for suitable commands when available/configured, and native fallback is clearly reported as approval-gated but not OS-isolated. +- In native fallback, command execution is not process-isolated by default. - Mock-only local eval commands with `apeironcode eval list`, `apeironcode eval run smoke`, and `apeironcode eval report`. ## Provider Matrix @@ -177,13 +251,13 @@ ApeironCode uses a bring-your-own-key model for cloud providers. API keys are re - Read-only operations are low risk by default. - File edits and writes always show a diff before approval. -- Shell commands, tests, and commits always require approval unless you explicitly pass `--dangerously-skip-approvals`. +- Shell commands are classified before execution. `--yes` and `--dangerously-skip-approvals` do not approve high-risk or blocked commands. - Web access is ask-first by default because outbound requests must match an explicit `Network(...)` allow rule or be approved interactively. - `web_fetch` only supports `http:` and `https:` URLs and blocks `localhost` and private IP ranges unless `web.allowPrivateHosts=true` is set for trusted local testing. - `web_search` and `web_research` redact obvious secret-like query fragments before building outbound requests. - Sensitive files such as `.env`, SSH keys, and secret stores require explicit approval. - Commands like `sudo`, `curl | sh`, `wget | sh`, and system path permission changes are blocked. -- High-risk commands such as `rm -rf`, destructive git resets, and `npm publish` require extra confirmation. +- High-risk commands require explicit manual confirmation. Blocked commands such as `npm publish`, `sudo`, pipe-to-shell installers, broad workspace escapes, and forced/mirrored pushes do not run by default. ## Slash Commands @@ -509,6 +583,8 @@ npm run bench:agent - `docs/memory.md` covers project memory, global memory, and session learning. - `docs/history.md` covers `history`, `search`, and stored edit/task/session data. - `docs/mcp.md` covers the experimental MCP runtime and current limitations. +- `docs/testing.md` explains mock/scripted CI coverage and real-provider limits. +- `docs/sandbox.md` explains why approval gating is not an OS security boundary. ## Demo @@ -521,7 +597,7 @@ build / modify / error-fix flows. - Next: richer history and memory dashboards inside the TUI. - Next: additional web search providers and richer result extraction. - Next: broader MCP transport support beyond the current experimental stdio runtime. -- Later: a polished VS Code companion (in development, not part of this CLI alpha). +- Later: optional IDE integrations. None are included in this CLI alpha. ## Known limitations @@ -536,6 +612,8 @@ build / modify / error-fix flows. - The real TTY interaction harness is still limited. - No full autonomy is claimed — file writes and shell commands are approval-gated. +- Default CI uses fixtures, mocked network responses, and the mock provider. + Real-provider/real-LLM checks require user credentials and are not run by CI. ## Attribution diff --git a/action.yml b/action.yml index aa907b9..b18a7bd 100644 --- a/action.yml +++ b/action.yml @@ -30,9 +30,6 @@ inputs: apeironcode_config_path: description: 'Path to an ApeironCode config file (optional).' required: false - opencode_config_path: - description: 'Legacy alias of apeironcode_config_path. Path to an ApeironCode config file (optional).' - required: false runs: using: 'composite' steps: @@ -50,7 +47,6 @@ runs: INPUT_MAX_ITERATIONS: ${{ inputs.max_iterations }} INPUT_RUN_TESTS: ${{ inputs.run_tests }} INPUT_APEIRONCODE_CONFIG_PATH: ${{ inputs.apeironcode_config_path }} - INPUT_OPENCODE_CONFIG_PATH: ${{ inputs.opencode_config_path }} GITHUB_TOKEN: ${{ github.token }} run: '"$GITHUB_ACTION_PATH/node_modules/.bin/tsx" "$GITHUB_ACTION_PATH/src/githubAction/main.ts"' branding: diff --git a/docs/agents.md b/docs/agents.md index 046aa11..2f67d23 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -41,8 +41,6 @@ apeironcode brain route "review the auth module for security issues" # → selects: security-reviewer, reviewer ``` -In VS Code, use **ApeironCode: Route Prompt to Brain** or the **Route Prompt** button in the Project Brain panel. - ## Commands ```bash diff --git a/docs/bridge.md b/docs/bridge.md index 6fa1f1d..c9d2b0e 100644 --- a/docs/bridge.md +++ b/docs/bridge.md @@ -122,24 +122,14 @@ Protected paths (`.env`, `.git/`, private keys) are flagged as `risky: true`. Connect via: `ws://127.0.0.1:` -## VS Code Extension (Phase 16F) +## Future IDE Integrations -The extension at `extensions/vscode/` provides: - -- Sidebar chat panel streaming bridge events -- Bridge connection management (start/stop) -- Permission approve/deny UI -- Diff preview with risky-path warnings -- Task and worktree status view -- Context view (files, token budget, provider) -- Selected file/range context capture - -See `docs/vscode-extension.md` for setup. +The public alpha does not include an IDE extension. The bridge is an +experimental local API that may support future VS Code, JetBrains, desktop, or +web integrations. ## Known Limitations (Phase 16F) - No remote/cloud bridge (local-only is intentional and enforced). - `diff.apply_requested` / `diff.apply_result` not connected to tool path (auto-apply disabled in MVP). -- Prompt submission from VS Code webview pending bridge command routing. -- No JetBrains or other IDE extension yet. -- No VS Code Marketplace release yet. +- No public IDE extension or Marketplace release. diff --git a/docs/build-apps.md b/docs/build-apps.md new file mode 100644 index 0000000..c555948 --- /dev/null +++ b/docs/build-apps.md @@ -0,0 +1,93 @@ +# Building apps with `apeironcode new` + +ApeironCode builds small apps from **deterministic templates** — no API key or +model is required for template generation. AI customization (via a configured +provider) is an optional layer on top. + +```bash +apeironcode new "premium todo app" +apeironcode new "CRM dashboard" --stack dashboard +apeironcode new "landing page for an AI startup" --style premium-saas +apeironcode new "a React task app" --stack vite --dir my-app +apeironcode build "create a premium todo app" # build also creates apps +``` + +`apeironcode new` and `apeironcode build` run the same app-builder engine: +parse the idea → pick a template → show an approval bundle **with a diff preview +of the files to be created** → create real files → run validation → print a +**Plan** checklist and completion panel with how to open the app. + +## Supported templates + +| Stack | Template | Notes | +| --- | --- | --- | +| `static` | static todo | HTML/CSS/JS. **Most reliable today** — no install, always works. | +| `static` | static landing | Premium landing page with a mock waitlist. | +| `dashboard` | static dashboard | Responsive, frontend-only dashboard with local sample data. | +| `vite` | Vite + React (TypeScript) | Alpha. Requires `npm install` to run; the CLI does not auto-install. | + +Next.js is not available yet; a Next.js request downgrades to Vite + React and +says so honestly. + +## What's a mock, and what's real + +This phase builds **frontend prototypes**. To stay honest: + +- "login", "auth", "sign in" → a **mock login** UI only (no real backend). +- "CRM", "customers", "orders" → **local sample data** rendered in the browser. +- "billing", "payments" → a **mock** UI only. + +ApeironCode does not create real authentication, databases, or billing in this +phase, and the final report says so. + +## Validation + +After creating files, ApeironCode validates the app: + +- **Static apps**: files exist, the HTML's linked CSS/JS exist, `node --check` + passes on the JS, and a design checklist (title, viewport, labels, responsive + rules, no obvious overflow) passes. No network required. +- **Vite apps**: `npm run build` / `npm run typecheck` are detected. The CLI + does **not** auto-install or run them; run `npm install && npm run dev` + yourself. They are reported as *skipped*, never as a fake pass. + +If a static app fails validation, the report shows exactly which check failed — +it never claims a broken app passed. + +## Browser UI smoke + +After creating a static app you can open it in a real browser and verify it +actually renders: + +```bash +apeironcode test-ui ./my-app +apeironcode new "todo app" --ui-smoke --yes # run during creation +apeironcode new "todo app" --require-ui-smoke --yes # fail if it can't run/pass +``` + +It checks page load, missing assets, console errors, takes a screenshot, and +runs template-specific interactions (e.g. add a todo and confirm the list +grows). It needs Chromium (`npx playwright install chromium`); without a +browser it reports **skipped**, never a fake pass. See +[ui-smoke.md](./ui-smoke.md). UI smoke is not a full QA replacement. + +## Safety + +- App creation is **approval-gated**: you see a bundle (directory + files + + validation) before anything is written. Auto-approved only in bypass/trusted + mode or with `--yes`. +- A **non-empty directory** is never written into without `--overwrite`, and + even then **existing files are never replaced** — only new files are added. +- Files are only ever written inside the chosen app directory; `.git` and + `.env` are never written. +- Validation commands use the central command policy. Network is off by + default, Docker is used when available/configured, and native fallback is not + described as OS-isolated. +- `--dry-run` shows the plan and writes nothing. + +## Limitations (alpha) + +- Static templates are the most reliable. Vite is alpha and needs a manual + install/build. Next.js is not implemented yet. +- These are prototypes, not production apps. Use `apeironcode improve` and + `apeironcode fix` to iterate. diff --git a/docs/demo.md b/docs/demo.md new file mode 100644 index 0000000..38220c5 --- /dev/null +++ b/docs/demo.md @@ -0,0 +1,37 @@ +# Demo mode + +`apeironcode demo` is a deterministic, no-key way to see ApeironCode's +plan → write → validate flow in under a minute. It needs no API key, model, or +Ollama. + +Demo mode never calls a provider, model, Ollama, or the network. Each scenario +writes real files into a demo workspace (under your OS temp directory) and runs +real local validation (for example `node --check`). + +```bash +apeironcode demo # list the scenarios +apeironcode demo todo-app # scaffold a working static HTML/CSS/JS todo app +apeironcode demo fix-test # reproduce a failing test, apply a fix, re-run to green +apeironcode demo improve-ui # turn a plain page into a premium one +apeironcode demo review # deterministic review of a sample diff +``` + +## What each scenario does + +- **todo-app** — writes `index.html`, `styles.css`, and `app.js`, then validates + that the JS parses (`node --check app.js`) and that the HTML's linked assets + exist. Open `index.html` in a browser. +- **fix-test** — writes a tiny project with a deliberate bug and a small test + runner, runs it (fails), applies the deterministic fix, and re-runs (passes). +- **improve-ui** — writes a plain pricing card, then restyles it with a premium + dark theme and confirms no CSS/JS references are missing. +- **review** — loads a fixed sample diff and prints deterministic, rule-based + findings across security, bug risk, and maintainability. + +## Honest limitations + +- Demo mode is **deterministic local examples, not a model.** It does not use + AI. The output is fixed. +- The `mock` provider is a deterministic test/demo stub, **not** a real coding + model. It exists for tests and offline checks. +- For real projects, configure [Ollama or a cloud provider](./providers.md). diff --git a/docs/fix-code.md b/docs/fix-code.md new file mode 100644 index 0000000..814b42d --- /dev/null +++ b/docs/fix-code.md @@ -0,0 +1,57 @@ +# Fixing code with `apeironcode fix` + +`apeironcode fix` is ApeironCode's core repair workflow. The runtime owns the +loop; the model only proposes small patches. + +```bash +apeironcode fix # detect checks, run them, propose approval-gated fixes +apeironcode fix --dry-run # preview: detect project + checks, run/change nothing +apeironcode fix --safe # only edit files referenced by errors; max 2 attempts +apeironcode fix --all # also run build/e2e; allow more attempts +apeironcode fix --until-green # keep attempting until checks pass (up to 10) +``` + +## What it does + +1. Detects the project type, package manager (npm/pnpm/yarn/bun), and the + available check scripts (typecheck, lint, test, build, e2e). +2. Runs checks in order: typecheck → lint → test → build → e2e + (build/e2e only with `--all` / `--until-green`). +3. Parses failures into structured issues (TypeScript errors, ESLint errors, + failing tests, module-not-found, …) and selects a focused context packet. +4. Asks the configured provider for a **minimal** file plan, parsed robustly + (markdown/fenced/tool-call output is tolerated) and validated against a + schema. One correction attempt is made if the plan is invalid. +5. Shows a **single approval bundle** (issues + files + commands + risk) with a + redacted **diff preview** (old vs planned content) so you see the change + before approving. Each command shows risk, sandbox, and network policy. + High-risk and blocked commands are stripped and never auto-approved. +6. On approval, creates a checkpoint, applies the change through the + ToolRegistry, and reruns the failed checks. +7. Repeats up to a mode-based limit, then prints an honest final report. + +## Safety + +- **Approval is required before any change.** In `ask` mode you confirm the + bundle; in `bypass`/`trusted` mode it is auto-approved. +- **`--safe`** restricts edits to files referenced by the error and forbids + delete/rename and dependency installs. +- **`--dry-run`** never runs checks or writes files. +- High-risk commands (`rm -rf`, `sudo`, `curl | sh`, `npm publish`, `git push`, + …) are blocked from the auto-applied bundle. +- Checks use Docker in `auto` mode when it is genuinely available; native + fallback is reported honestly and is not OS-isolated. +- A checkpoint is created before applying; a failed apply is rolled back so the + workspace is never left half-changed. Checkpoints live under + `.apeironcode/checkpoints/` (gitignored). + +## Honest limitations (alpha) + +- This is alpha. It does not fix every repo, and it is not fully autonomous — + you review and approve changes. +- Repair quality depends on the configured provider/model. A weak model may + return plans that do not fix the issue; the loop stops safely after the + attempt limit. +- The `mock`/demo provider is deterministic and is **not** a real coding model; + use Ollama or a cloud provider for real repairs. +- `--commit` is planned for a later phase and is not active yet. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..08b223e --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,67 @@ +# Getting started + +ApeironCode is a local-first coding agent that fixes bugs, builds apps, improves +UI, and reviews code — with approval-gated writes and commands. + +## 1. Install (from source) + +```bash +git clone https://github.com/poolanithinreddy/ApeironCode.git +cd ApeironCode +npm ci +npm run build +npm link +apeironcode --help +``` + +The default `--help` is beginner-focused. Run `apeironcode help developer` or +`apeironcode help advanced` for the full command set. + +## 2. Try it with no API key + +```bash +apeironcode demo todo-app +``` + +Demo mode is deterministic and needs no API key, model, or Ollama. See +[demo.md](./demo.md). + +## 3. Choose how to run for real + +```bash +apeironcode setup +``` + +`setup` is intent-based — pick how you want to run: + +1. **Demo mode only** — `apeironcode demo` +2. **Free local model with Ollama** — `apeironcode local setup` +3. **Cheap cloud model** — `apeironcode setup --provider github-models` +4. **Best coding model** — `apeironcode setup --provider anthropic` +5. **Bring my own OpenAI-compatible endpoint** — `apeironcode setup --provider openaiCompatible` + +Local OpenAI-compatible endpoints (localhost/127.0.0.1/0.0.0.0) do **not** need +an API key. See [providers.md](./providers.md). + +Check your environment any time: + +```bash +apeironcode doctor +``` + +## 4. The four workflows + +```bash +apeironcode fix # detect failures, propose fixes, rerun checks +apeironcode new "a todo app" # create a new app from a template +apeironcode build "a CRM dashboard"# build also creates apps +apeironcode improve "premium UI" # improve UI or code quality +apeironcode review # review the current changes +``` + +`apeironcode new` creates a small app from a deterministic template (no API key +needed); see [build-apps.md](./build-apps.md). `apeironcode fix` runs the repair +loop: detect checks → run → propose an approval-gated patch → apply → rerun. Try +`apeironcode fix --dry-run` first. See [fix-code.md](./fix-code.md). + +Every write and command is approval-gated. See [safety.md](./safety.md). diff --git a/docs/github.md b/docs/github.md index 85743e9..729cffb 100644 --- a/docs/github.md +++ b/docs/github.md @@ -43,7 +43,6 @@ Current status: - Tokens are read from `GITHUB_TOKEN` or `GH_TOKEN` and are never printed. - Automation workflows are dry-run by default. Non-dry-run writes require `APEIRONCODE_AUTOMATION=1` plus the narrow action flag, such as `APEIRONCODE_AUTOMATION_REVIEW=1`, `APEIRONCODE_AUTOMATION_COMMENT=1`, `APEIRONCODE_AUTOMATION_COMMIT=1`, or `APEIRONCODE_AUTOMATION_PR_CREATE=1`. - Automation can also be constrained with `APEIRONCODE_AUTOMATION_REPOS`, `APEIRONCODE_AUTOMATION_ACTORS`, and `APEIRONCODE_AUTOMATION_DENY_ACTORS`. Explicit denies win. -- The matching `OPENCODE_AUTOMATION*` variables remain as deprecated legacy aliases; prefer the `APEIRONCODE_*` names. - Fork PRs are forced into dry-run/comment-only behavior by the GitHub Action runner. Protected base branches are not pushed directly. - Action comments include an ApeironCode run marker so replayed or duplicate events can be skipped safely. - Mention commands support `@apeironcode implement`, `@apeironcode review`, `@apeironcode fix-tests`, `@apeironcode explain`, and `@apeironcode apply-suggestion`. Unknown commands are rejected with a safe help response. diff --git a/docs/history.md b/docs/history.md index 39059b8..77bbaef 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,13 +1,5 @@ # History -> **Brand note (Phase 15A):** ApeironCode was formerly developed and shipped as -> *ApeironCode Agent*. The legacy `apeironcode` CLI binary, `.apeironcode-agent/` -> project directory, `.apeironcodeignore` ignore file, and `OPENCODE_*` -> environment variables are all preserved as backward-compatible aliases. -> New projects use the `apeironcode` binary, `.apeironcode-agent/` directory, -> `.apeironcodeignore`, and `APEIRONCODE_*` environment variables. Storage -> paths below show the legacy on-disk names that remain the default. - ApeironCode now persists and searches several kinds of local history: - saved sessions @@ -89,4 +81,4 @@ Saved sessions now include structured summaries from the run itself, such as dec - Search is text-based and local only. - Edit history is project-scoped; session history is stored globally and filtered by project path when requested. -- Search results are summary-oriented rather than full transcript excerpts. \ No newline at end of file +- Search results are summary-oriented rather than full transcript excerpts. diff --git a/docs/project-brain.md b/docs/project-brain.md index c600228..2a8fe12 100644 --- a/docs/project-brain.md +++ b/docs/project-brain.md @@ -34,19 +34,6 @@ Optional workflow folders: - `apeironcode brain memory` shows `MEMORY.md`. - `apeironcode brain update --yes --summary "..."` appends a short run summary when `RUNS.md` exists. -## VS Code - -The extension exposes: - -- `ApeironCode: Plan Project Brain` -- `ApeironCode: Initialize Project Brain` -- `ApeironCode: Show Project Brain` -- `ApeironCode: Show Project Plan` -- `ApeironCode: Show Project Tasks` -- `ApeironCode: Continue Current Plan` - -Initialization asks for confirmation before writing. - ## Safety Project Brain files are markdown data and workflow definitions. ApeironCode does not execute scripts from `.apeironcode/`. Project agents, skills, and commands still respect project trust and normal permission rules. Untrusted projects can show safe summaries, but behavior-changing workflow files are not auto-loaded. @@ -62,7 +49,7 @@ Sync previews are saved to `.apeironcode/runs/sync-previews/` before any write o - Flags previews older than 7 days as stale. - Requires `approved:true` before any preview is applied. -Use `apeironcode brain previews` to list, inspect, and apply saved previews from the CLI. In VS Code, open the **Sync Previews** panel for a full UI. +Use `apeironcode brain previews` to list, inspect, and apply saved previews from the CLI. ## PLAN/TASKS Merge Engine (Phase 16G.2) @@ -87,15 +74,8 @@ The brain context planner selects files relevant to the current prompt rather th - Verification strategy, risk list, first 3 next actions. - Token strategy for long builds. -This is **deterministic planning**, not autonomous execution. No files are written until you explicitly approve initialization via `--yes` or the VS Code confirmation dialog. - -## VS Code Brain Views (Phase 16G.2) - -**Project Brain Panel** — central control center with all brain actions. Access via `ApeironCode: Open Project Brain Panel`. - -**App Build Plan View** — rich display of orchestration results: vision, stack, phases/tasks, suggested agents/skills, verification plan, risks, token strategy, and action buttons. All writes require VS Code confirmation dialog. - -**Sync Previews View** — list, inspect, and apply saved sync previews. Stale previews (>7 days) are flagged. Applying requires explicit confirmation. Secrets are redacted in the display. +This is **deterministic planning**, not autonomous execution. No files are +written until you explicitly approve initialization via `--yes`. ## CLI Brain Commands (Phase 16G.2) @@ -126,12 +106,6 @@ apeironcode brain runtime "" — show intent classification and injecti apeironcode brain explain "" — show full debug explanation of context selection ``` -### VS Code - -- **`ApeironCode: Brain Runtime Context`** — analyze a prompt's runtime intent and context. -- **`ApeironCode: Brain Context Explanation`** — see detailed context selection debug info. -- An information notification appears when brain context is injected (shows intent and estimated token cost). - ### Token efficiency The runtime context injector respects a configurable token budget (default 900 tokens). For simple prompts (≤6 words, no intent signal), the brain is skipped entirely — zero overhead. diff --git a/docs/providers.md b/docs/providers.md index 8afe2c3..4b63b8c 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -20,6 +20,23 @@ Provider families in the current implementation: API keys are resolved from environment variables. Raw secrets are not stored in the local config file. ApeironCode does not reuse ChatGPT Plus, GitHub Copilot, browser cookies, or other subscription logins as provider credentials. +## No-key paths + +- **Demo mode** (`apeironcode demo`) is deterministic and needs no key, model, + or Ollama. See [demo.md](./demo.md). The `mock` provider is a deterministic + test/demo stub, **not** a real coding model. +- **Local OpenAI-compatible endpoints do not require an API key.** When the + base URL host is `localhost`, `127.0.0.1`, `0.0.0.0`, `::1`, + `host.docker.internal`, or a `*.local` name, ApeironCode treats the endpoint + as a self-hosted model server (Ollama's OpenAI shim, LM Studio, vLLM, …) and + sends no `Authorization` header when no key is set. +- **Remote OpenAI-compatible endpoints require `OPENAI_API_KEY`** and fail fast + with a clear message if it is missing, so an unauthenticated request is never + sent. + +Real coding needs Ollama or a cloud provider. Run `apeironcode setup` for the +intent-based picker, or `apeironcode local setup` for a free local model. + Provider routing adds a second layer above raw provider implementations. - `src/providers/modelCatalog.ts` stores known model roles, capabilities, context windows, and pricing metadata. @@ -122,9 +139,7 @@ ollama pull qwen2.5-coder:7b apeironcode setup --provider ollama ``` -Config is stored ApeironCode-first at `~/.apeironcode-agent/config.json`. A -legacy `~/.opencode-agent` home is migrated automatically on first run -(non-destructive — the legacy directory is left intact). +Config is stored at `~/.apeironcode-agent/config.json`. ### Verify your GitHub token with curl diff --git a/docs/publishing.md b/docs/publishing.md index 3d8e9db..12ec1d8 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -11,11 +11,16 @@ ## npm publishing notes +The intended public package name is `apeironcode`. Registry checks returned no +published version as of June 8, 2026, but npm availability is only confirmed +when a publish succeeds. The maintainer must authenticate with npm before +release. + The package is configured for global install with: ```json { - "name": "apeironcode-agent", + "name": "apeironcode", "bin": { "apeironcode": "./dist/cli/index.js" } @@ -27,6 +32,6 @@ Publish flow: 1. Bump the version in `package.json`. 2. Build the package. 3. Confirm `dist/` contains the CLI bundle. -4. Run `npm publish` from a trusted environment. +4. Run `npm publish --access public --tag alpha` from a trusted environment. -The CI workflow already validates install, typecheck, test, and build on Node 18, 20, and 22. \ No newline at end of file +The CI workflow already validates install, typecheck, test, and build on Node 18, 20, and 22. diff --git a/docs/release-notes/v0.1.0-alpha.md b/docs/release-notes/v0.1.0-alpha.md index 7cda1f5..3318344 100644 --- a/docs/release-notes/v0.1.0-alpha.md +++ b/docs/release-notes/v0.1.0-alpha.md @@ -34,6 +34,10 @@ public release: the VS Code extension and marketing website are not included. - Provider/model switching UX is still improving. - The real TTY interaction harness is still limited. - No full autonomy is claimed — writes and commands are approval-gated. +- Approval gating is not OS-level isolation; commands are not process-isolated + by default. +- Default CI uses mocked/scripted providers. Real-provider tests require user + credentials and are not run by default. ## Install @@ -47,6 +51,8 @@ apeironcode setup --provider mock # try with no API key apeironcode ``` +The npm package is not published yet. Install from source for this alpha. + ## Safety Reads are low-risk; file writes and shell commands require approval; dangerous diff --git a/docs/safety.md b/docs/safety.md index 9ebbb28..4f55c00 100644 --- a/docs/safety.md +++ b/docs/safety.md @@ -7,14 +7,19 @@ Approval modes: - `ask`: explicit approvals when a tool requests them. - `auto-read`: low-risk project reads can proceed automatically. - `trusted`: in-project reads and writes can auto-approve, but commands and git actions still require approval. -- `bypass`: all approvals are skipped for the current run. This should only be used in trusted automation. +- `bypass`: normal approvals are skipped for trusted automation. It does not + override high-risk manual confirmation or blocked command policy. Guardrails: - External paths and sensitive files require approval. - Diffs are shown before file writes and edits. - Commands are checked for blocked and high-risk patterns before execution. -- `sudo`, `curl | sh`, `wget | sh`, and system permission changes are blocked. -- Destructive deletion and destructive git operations require stronger confirmation. +- `sudo`, `curl | sh`, `wget | sh`, `npm publish`, forced/mirrored pushes, + workspace escapes, and direct sensitive metadata mutation are blocked. +- High-risk commands require manual confirmation; `--yes` does not approve them. +- Command logs and output redact common token/key formats. +- Docker is selected only when available/configured. Native fallback is + approval-gated but not OS-isolated. -The design goal is simple: no silent mutation, no silent execution, and no secret access without user visibility. \ No newline at end of file +The design goal is simple: no silent mutation, no silent execution, and no secret access without user visibility. diff --git a/docs/sandbox.md b/docs/sandbox.md index f161451..f05492c 100644 --- a/docs/sandbox.md +++ b/docs/sandbox.md @@ -1,36 +1,47 @@ -# Sandbox +# Sandbox and command policy -ApeironCode Agent does not currently provide OS-level command sandboxing. - -The Phase 18 sandbox surface is an honest detection and status layer: +ApeironCode evaluates every supported project command through a central command +policy before execution. The policy classifies risk, network access, workspace +scope, approval requirements, and whether Docker isolation is suitable. ```bash apeironcode sandbox status apeironcode sandbox doctor +apeironcode --sandbox auto fix +apeironcode --sandbox docker fix +apeironcode --sandbox native fix ``` -It checks whether optional backends such as Docker, Podman, or Firejail appear available, then reports current limitations. Agent tools still run in the local process unless a future sandbox runner is explicitly implemented and enabled. +Modes are `auto` (default), `docker`, `native`, and `none`. Set the mode with +`--sandbox` or `APEIRONCODE_SANDBOX`. -Current limits: +- `auto`: use Docker for suitable validation/build/test commands when Docker + and its daemon are actually available; otherwise use native fallback. +- `docker`: require Docker and fail clearly when unavailable. +- `native`: run approval-gated commands on the host. +- `none`: disable isolation selection; command policy and blocking remain on. -- No OS sandboxing for command execution. -- No per-subagent credential isolation. -- No cloud or distributed execution. -- Provider credentials are inherited from the current environment. +Docker mounts only the workspace at `/workspace`, does not mount the host home +or Docker socket, drops capabilities, uses a read-only container root, and +keeps network off unless policy explicitly permits it. Secrets are not passed +to Docker by default. -## Fallback Policy (Safety Engine 2.0) +Native fallback prints: `Running natively. Approval-gated, but not +OS-sandboxed.` This is not a claim of process isolation. -When the preferred sandbox backend is unavailable, ApeironCode consults the -`sandbox.fallbackPolicy` config option: +Current limits: -| Policy | Behavior | -|------------------|----------| -| `never` | Refuse to execute any command without a sandbox. | -| `safe-readonly` | Allow only commands classified as read-only and non-network. (default) | -| `always` | Run locally with a warning; risky commands generate a stronger warning. | +- Docker isolation applies only to commands that actually run in Docker. +- Native execution is not OS-isolated. +- No per-subagent credential isolation. +- No cloud or distributed execution. +- This is defense in depth, not a guarantee that arbitrary code cannot escape. -The decision is made via `getSandboxFallbackDecision(semantics, policy)` in -`src/sandbox/manager.ts`. Read-only / non-network classification reuses the -shell command parser and semantics module. There is no silent fallback in -`safe-readonly` or `never` modes. +## Command policy +Safe validation commands default to network off. Installs and other network-on +operations are shown as `network: ask`. High-risk commands require manual +confirmation and cannot be approved by `--yes`. Blocked commands, including +`npm publish`, `sudo`, pipe-to-shell downloads, forced/mirrored pushes, direct +`.git` mutation, `.env` mutation, and detectable workspace escapes, never reach +the child process. diff --git a/docs/security-model.md b/docs/security-model.md index bedadbf..29ceba9 100644 --- a/docs/security-model.md +++ b/docs/security-model.md @@ -3,11 +3,11 @@ ApeironCode uses a layered security model to protect users from accidental or malicious misuse. -## VS Code extension (Phase 16F) +## Local Bridge -- The extension talks only to the **local WebSocket bridge** on `127.0.0.1` with a workspace token; it does not ship API keys or call providers directly. -- Model selection is stored per **bridge connection** (`provider.set_session_model`) and merged into the next `session.send_prompt` as `providerName` / `model` overrides for the CLI agent runner (still subject to CLI config and credentials on the host). -- Approved diff applies from the bridge use **ToolRegistry** (`patch_file`) with structured `patchOperations`; there is no parallel write path that bypasses tools. +The experimental local bridge binds to `127.0.0.1` and requires a workspace +token. It is intended for future integrations and is not a public IDE +extension. Bridge-triggered tool execution still goes through `ToolRegistry`. ## Project Trust @@ -23,7 +23,9 @@ Temporary directories are **always untrusted** regardless of explicit trust gran ## Project Brain -Project Brain is optional and uses `.apeironcode/` files. ApeironCode only creates those files after explicit approval (`apeironcode brain init --yes` or a VS Code confirmation). Plan mode writes nothing. +Project Brain is optional and uses `.apeironcode/` files. ApeironCode only +creates those files after explicit CLI approval (`apeironcode brain init +--yes`). Plan mode writes nothing. Safe summaries can be shown in untrusted projects, but behavior-changing `.apeironcode/agents`, `.apeironcode/skills`, and `.apeironcode/commands` still follow the workflow trust model. Scripts from `.apeironcode/` are not executed. @@ -90,29 +92,25 @@ All Project Brain write operations use an **approval-first** model: | Operation | Requirement | |---|---| -| `brain init` | `--yes` flag (CLI) or VS Code confirmation dialog | -| `brain sync` / `brain sync-preview apply` | `--yes` flag (CLI) or VS Code confirmation dialog | -| `brain previews apply ` | `--yes` flag (CLI) or VS Code confirmation dialog | -| VS Code "Initialize Project Brain from Plan" | Modal VS Code confirmation dialog required | -| VS Code "Apply this Preview" | Modal VS Code confirmation dialog required | +| `brain init` | `--yes` flag | +| `brain sync` / `brain sync-preview apply` | `--yes` flag | +| `brain previews apply ` | `--yes` flag | | Orchestration plan display | Read-only. No files written. | | Agent routing plan | Read-only. No files written. | | Context preview | Read-only. No files written. | -There is **no silent creation** of `.apeironcode/` files. Plan and orchestration commands are read-only previews. The `approved:true` flag must be explicitly set before any write is accepted by the bridge handler. - -VS Code webview panels enforce this by: -1. Showing a modal `vscode.window.showWarningMessage` with `{modal: true}` before sending any write request. -2. Only sending `brain.init` or `brain.preview_apply` with `approved: true` after the user clicks the confirmation button. -3. Never writing on button click alone — the confirmation step is mandatory. +There is **no silent creation** of `.apeironcode/` files. Plan and +orchestration commands are read-only previews. The `approved:true` flag must be +explicitly set as confirmation before any write is accepted by the bridge +handler. ## Runtime Brain Context Redaction (Phase 16H) -All Project Brain content that enters agent prompts, bridge messages, doctor output, or the VS Code webview is passed through `redactProjectBrainText()`: +All Project Brain content that enters agent prompts, bridge messages, or doctor +output is passed through `redactProjectBrainText()`: - Tokens ≥ 32 characters matching `[A-Za-z0-9_-]{32,}` (secret-like patterns) are replaced with `[REDACTED]`. - No raw file content from `.apeironcode/` appears in bridge payloads. -- The VS Code brain panel uses `escapeHtml()` on all dynamic values before rendering. - Context injection text is capped at a configurable token budget (default 900 tokens). - For simple prompts (≤ 6 words, no intent signal), brain context is skipped entirely — zero overhead and no file reads. diff --git a/docs/skills.md b/docs/skills.md index ad064c5..56dd152 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -100,7 +100,11 @@ The routing plan explains why each skill is relevant but does not inject skill c ## Runtime Brain Skill Selection (Phase 16H) -The runtime brain intent classifier automatically considers project skills when routing a prompt. Skills listed in `.apeironcode/skills/` appear in the `selectedAgents` field of `brain.runtime` responses and in the VS Code Runtime Brain Decision panel. No extra command needed — skill routing happens as part of every agent run when Project Brain is present. +The runtime brain intent classifier automatically considers project skills +when routing a prompt. Skills listed in `.apeironcode/skills/` appear in the +`selectedAgents` field of `brain.runtime` responses. No extra command is +needed; skill routing happens as part of every agent run when Project Brain is +present. ## Known limitations diff --git a/docs/terminal-ux.md b/docs/terminal-ux.md new file mode 100644 index 0000000..18d66cf --- /dev/null +++ b/docs/terminal-ux.md @@ -0,0 +1,110 @@ +# Terminal UX: progress, approvals, diffs, rollback + +ApeironCode's workflows (`fix`, `new`/`build`, `test-ui`, `improve`, `review`) +render a shared, compact progress board and approval UX. + +**Where you'll see this today:** + +- `apeironcode new`/`build` show the approval bundle + **diff preview** of the + files to be created, then a **Plan** checklist and completion panel. +- `apeironcode fix` shows the repair approval bundle with a **diff preview** + (old vs planned content), strips high-risk commands, and ends with a + completion/failure panel that lists next actions (incl. `rollback --last`). +- `apeironcode test-ui` and `apeironcode review` print their own status panels. + +The interactive Ink agent chat keeps its existing compact status line; the live +in-chat progress board is a follow-up. + +## Progress board + +``` +Task: Build CRM dashboard + +Plan + ✓ Analyze request + ✓ Select template + ✓ Create files + → Run static validation + ○ Run UI smoke + +Files + + index.html +20/-0 + + styles.css +30/-0 + +Commands + ✓ node --check app.js 1.2s + ✗ npm test 3.8s (exit 1) + Failing test: should add task + +Status: failed +``` + +Symbols: `✓` passed · `→` running · `○` pending · `✗` failed · `–` skipped. +Normal mode stays compact (no raw logs or giant JSON). Run with `--verbose` or +`APEIRONCODE_DEBUG=1` for full command output. + +## Approval bundle + +Writes and commands are approval-gated. You see one compact bundle: + +``` +ApeironCode wants to fix detected failures: + +Create 1 file +Modify 1 file +Run 2 commands + +Risk: medium + +Files: + ~ styles.css +Commands: + - npm run build + risk: safe | sandbox: auto/docker when available | network: off + - npm install + risk: medium | sandbox: auto/docker when available | network: ask + +Options: [Approve all] [View diff] [Files only] [Reject] +``` + +High-risk and blocked commands (`rm -rf`, `sudo`, `curl | sh`, `npm publish`, +`git push --force`, broad `chmod -R`/`chown -R`) are **never** bundled or +auto-approved — even with `--yes`/trusted/bypass. In a non-interactive terminal, +the default is to reject unless `--yes` (or trusted/bypass mode) is set. + +## Diff preview + +`[View diff]` shows a redacted per-file summary: additions/deletions and the +first changed lines. Binary and very large files are summarized, secrets are +redacted, and full diffs are available in verbose/debug mode. + +## Rollback + +`apeironcode fix` creates a checkpoint before applying changes. Recover with: + +```bash +apeironcode rollback list # show checkpoints (newest first) +apeironcode rollback --last # restore the most recent checkpoint +apeironcode rollback # restore a specific checkpoint +``` + +Rollback only touches files recorded in the checkpoint (never foreign files) and +asks for confirmation unless `--yes`. No checkpoint is handled gracefully. + +## UI smoke artifacts + +After a UI smoke, the report surfaces the screenshot and report paths and a +`apeironcode test-ui ` next step. A skipped smoke is clearly *skipped*, +never a pass. See [ui-smoke.md](./ui-smoke.md). + +## Non-interactive behavior + +Direct commands (`fix --dry-run`, `new … --yes`, `test-ui --json`, `review`, +`rollback list`) print plain text and never require the interactive TUI. +`--yes` approves normal bundles but still cannot approve blocked high-risk +commands. + +## Honest limits + +Approval-gated does **not** mean OS-sandboxed. The browser smoke is not full QA. +ApeironCode is not fully autonomous — you review and approve changes. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..129ac95 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,37 @@ +# Testing and Validation + +ApeironCode is an early CLI alpha. Its default validation is broad, but it is +not a substitute for testing your chosen model and provider on your own code. + +## Default CI + +Default CI runs TypeScript checks, linting, builds, unit and integration tests, +scripted end-to-end flows, file-size checks, a dependency audit, and an npm +package dry run. + +Most agent workflows use deterministic fixtures, mocked network responses, or +the no-key mock provider. Default CI does not call paid model APIs and does not +require real provider credentials. + +## Smoke and Dogfood Checks + +The repository includes smoke commands for scripted coding, error-fixing, TUI, +and terminal workflows. These verify CLI orchestration and safety behavior with +controlled inputs. They do not prove that every real model will make correct +changes. + +Before trusting a provider on important work: + +1. Run the relevant smoke commands. +2. Test in a disposable repository. +3. Keep approval mode enabled. +4. Review every diff and command. +5. Report failures with the dogfood issue template. + +## Real Providers + +Real-provider checks require your own API key or local model service and are not +run in default CI. Provider behavior, availability, pricing, and model quality +can change independently of ApeironCode. + +Large-project autonomy and long multi-agent workflows remain experimental. diff --git a/docs/ui-smoke.md b/docs/ui-smoke.md new file mode 100644 index 0000000..fb99899 --- /dev/null +++ b/docs/ui-smoke.md @@ -0,0 +1,70 @@ +# UI smoke (`apeironcode test-ui`) + +`apeironcode test-ui` opens a generated app in a real browser and checks it +actually renders — not just that files exist. No API key or model is required. + +```bash +apeironcode test-ui ./premium-todo-app +apeironcode test-ui --json +apeironcode test-ui --screenshot +apeironcode test-ui --port 4173 --timeout 30000 +``` + +## What it checks + +- **page loads** without a navigation error +- **no missing assets** (CSS/JS that 404 or fail to load) +- **no console errors** / page errors +- **title** and **main heading** are present +- a **screenshot** is saved +- **template-specific interactions**, e.g. for a todo app: type a task, click + Add, and confirm the list grows + +Results are saved under the app's `.apeironcode/ui-smoke/` (a `latest.json` +pointer plus timestamped runs and a screenshot). These artifacts are gitignored. +Secrets are redacted and large logs are truncated. + +## Requirements + +The smoke uses [Playwright](https://playwright.dev). It is **optional** — if +Chromium is not installed, the smoke is reported as **skipped**, never as a fake +pass: + +``` +UI smoke: skipped — Chromium is not installed +Run: npx playwright install chromium +``` + +To enable real browser runs: + +```bash +npm install playwright +npx playwright install chromium +``` + +## App builder integration + +`apeironcode new` / `apeironcode build` can run the smoke after creating an app: + +```bash +apeironcode new "todo app" --ui-smoke --yes # run it; skip honestly if no browser +apeironcode new "todo app" --require-ui-smoke --yes # fail if it cannot run or does not pass +apeironcode new "todo app" --no-ui-smoke --yes # don't run it +``` + +The build report shows the result honestly: + +``` +Validation: + ✓ static files + ✓ JavaScript syntax + ✓ design checklist + ○ UI smoke skipped: Chromium not installed +``` + +## Limitations + +UI smoke is **not a full QA replacement** and does not catch every UI bug or +judge visual quality. It catches obviously-broken renders: missing assets, +console errors, and failed core interactions. Vite apps require installed +dependencies; this phase does not auto-install or run their dev server. diff --git a/docs/ui-ux.md b/docs/ui-ux.md index da26a14..22230a6 100644 --- a/docs/ui-ux.md +++ b/docs/ui-ux.md @@ -1,6 +1,7 @@ # ApeironCode UI/UX System -ApeironCode uses a shared product language across the terminal CLI and VS Code extension: calm surfaces, clear status, explicit permissions, and local-first trust signals. +ApeironCode's terminal CLI uses calm surfaces, clear status, explicit +permissions, and local-first trust signals. ## CLI Welcome @@ -101,31 +102,15 @@ Config options: Tool starts/results, permission requests, diff summaries, Project Brain context, tasks, and errors use compact text cards. Cards truncate huge content and redact token-like strings before rendering. -## VS Code - -The extension uses shared design tokens layered on VS Code theme variables. Dark mode is the primary target, but colors defer to `var(--vscode-*)` where possible. - -Polished areas: - -- Chat empty state with local-first onboarding -- Connection status header -- Selected model and Project Brain status -- Selected context chip -- Tool timeline -- Permission cards with approve/deny actions -- Status bar states: disconnected, connected, running, needs permission, error, brain active -- Project Brain dashboard cards and action groups -- Build plan and sync preview panels - ## Accessibility And Security -Webviews keep strict CSP and nonce usage. Dynamic content is escaped before insertion. Buttons and input controls have readable labels, and state is represented with text rather than color alone. +Terminal state is represented with text rather than color alone. Secrets are never intentionally displayed. UI renderers redact common API keys, bearer tokens, GitHub tokens, Slack tokens, and env-style secret assignments. ## Known Limitations -The CLI welcome is text-only and does not attempt terminal animations. VS Code screenshots are not bundled in this repo; public docs use descriptions until a release screenshot set is produced. +The CLI welcome is text-only and does not attempt terminal animations. ## Normal vs debug terminal output (Phase 17D) diff --git a/docs/vscode-extension.md b/docs/vscode-extension.md index b5cd6c3..39bce8d 100644 --- a/docs/vscode-extension.md +++ b/docs/vscode-extension.md @@ -1,384 +1,10 @@ -# ApeironCode VS Code Extension +# Future IDE Extension -The ApeironCode VS Code Extension provides a local-first AI coding agent interface directly in VS Code. +**Future/private work. Not included in the public CLI alpha.** -## Public beta (VSIX) +The public ApeironCode package is CLI-only. It does not include a VS Code +extension, a VSIX artifact, Marketplace installation, or extension support. -This extension is distributed as a **`.vsix` file** for public beta testing. It is **not** published on the VS Code Marketplace yet. Install with **Extensions: Install from VSIX…** in VS Code, then follow setup below. You still need the **ApeironCode CLI** and a **local bridge** (`apeironcode bridge start`); the extension does not embed the agent engine. - -### Quick start - -1. Build or obtain `apeironcode-.vsix` and install from VSIX. -2. In your project: `apeironcode bridge start` (listens on `127.0.0.1` only). -3. VS Code: **ApeironCode: Start Bridge Connection** and paste the token from `.apeironcode-agent/bridge-secret.json` (stored in workspace secrets; never logged in full). -4. **ApeironCode: Open Chat** — use **Clear Chat** in the webview or the command palette to reset local messages only. -5. **ApeironCode: Select Model / Provider** — updates workspace settings and forwards `provider.set_session_model` to the bridge for the next `session.send_prompt`. -6. **Send Selection to Chat** attaches editor context; risky edits require permission approval in the webview. -7. **Diff preview**: structured applies go through the bridge as `diff.apply_requested` with `patchOperations` (ToolRegistry / `patch_file`); raw unified diff text alone is not auto-applied. -8. **Project Brain**: use **ApeironCode: Plan Project Brain** to preview `.apeironcode/`; **Initialize Project Brain** asks before writing. - -### Known limitations - -- Marketplace listing and auto-updates are out of scope for this beta. -- Bridge is **local-only**; no hosted/cloud bridge in this build. -- JetBrains and remote bridge clients are not in scope. - -## Architecture - -``` -VS Code Extension - ├── BridgeClient (WebSocket, 127.0.0.1 only) - │ └── connects via ws://127.0.0.1: - ├── Sidebar Chat WebviewPanel - ├── Permission Request UI - ├── Diff Preview Panel - ├── Task/Worktree View - ├── Context View - └── Selection Context Capture - │ - ▼ -ApeironCode Core (CLI process) - └── bridge start ←→ BridgeServer + WebSocketTransport -``` - -## UI Overview - -The extension uses the ApeironCode premium UI system: dark-mode-first surfaces, VS Code design tokens, compact cards, clear status text, and explicit permission controls. The chat panel includes a local-first welcome state, connection header, model summary, Project Brain status, selected context chip, tool timeline, error banners, and Clear Chat. See [ui-ux.md](./ui-ux.md) for the shared CLI/VS Code design notes. - -## Setup - -### Step 1: Start the bridge server - -```bash -apeironcode bridge start -# Output: -# ApeironCode Bridge — local WebSocket server started. -# Endpoint: ws://127.0.0.1:51234 -# Token fingerprint: abc123def456 -# Token stored at: /your/project/.apeironcode-agent/bridge-secret.json -``` - -### Step 2: Connect the extension - -1. In VS Code, open the Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`). -2. Run **ApeironCode: Start Bridge Connection**. -3. When prompted, paste the full token from `bridge-secret.json`. -4. The extension stores the token in VS Code workspace secrets. - -### Step 3: Open the chat panel - -Run **ApeironCode: Open Chat** from the Command Palette, or click the ApeironCode icon in the Activity Bar. - -## Commands - -| Command | Description | -|---|---| -| `ApeironCode: Open Chat` | Open the sidebar chat panel | -| `ApeironCode: Start Bridge Connection` | Connect to local bridge | -| `ApeironCode: Stop Bridge Connection` | Disconnect from bridge | -| `ApeironCode: Show Setup Guide` | First-run onboarding guide | -| `ApeironCode: Select Model / Provider` | Choose agent model | -| `ApeironCode: Show Context` | Show context summary | -| `ApeironCode: Show Tasks` | Show task/worktree status | -| `ApeironCode: Plan Project Brain` | Preview optional `.apeironcode/` Project Brain files | -| `ApeironCode: Initialize Project Brain` | Create Project Brain files after confirmation | -| `ApeironCode: Show Project Brain` | Show safe Project Brain summary | -| `ApeironCode: Continue Current Plan` | Send a continuation prompt using Project Brain context | -| `ApeironCode: Send Selection to Chat` | Inject selected text as context | -| `ApeironCode: Approve Permission` | Approve a pending permission request | -| `ApeironCode: Deny Permission` | Deny a pending permission request | -| `ApeironCode: Run Doctor` | Troubleshooting hints | - -## Security Model - -- Bridge binds **only** to `127.0.0.1`. Not exposed on LAN or internet. -- Auth token is stored in VS Code **workspace secrets** (not settings). -- Token **fingerprint** (first 12 chars) is shown in logs/UI. Full token never logged. -- All webview content is HTML-escaped and protected by a strict Content Security Policy. -- Diff apply: IDE sends `diff.apply_requested` with explicit approval and optional structured `patchOperations`. Execution uses **ToolRegistry** (`patch_file`); raw unified diff strings without structured operations are **not** auto-applied. - -## Token / Fingerprint - -The token fingerprint is a safe short identifier: - -```bash -apeironcode bridge token -# Bridge token fingerprint: abc123def456 -# Full token stored at: /project/.apeironcode-agent/bridge-secret.json -``` - -Share the **fingerprint** for verification. Never share the full token over chat, email, or logs. - -## Prompt Submission (Phase 16F.1) - -The VS Code chat panel now drives live ApeironCode agent sessions: - -1. Type a prompt in the chat input and press **Send**. -2. The extension sends `session.send_prompt` to the bridge server. -3. The bridge creates or reuses an agent session and calls `Agent.run()`. -4. Assistant messages, tool events, and task events stream back to VS Code. -5. Permission requests appear as approve/deny cards. -6. Errors are shown safely (no raw secrets, no stack traces). - -### Selected Context Attachment - -When you send a prompt, the active editor selection is automatically attached: -- File path (workspace-relative) -- Language ID -- Line range -- Selected text (capped at 8,000 chars, secrets redacted) - -A small note shows: `Context attached: src/foo.ts lines 10–20`. - -If no selection, the prompt is sent without editor context. - -### Bridge Auto-Discovery - -The extension discovers the bridge endpoint automatically: - -1. **VS Code setting**: `apeironcode.bridgeEndpoint` if configured. -2. **Connection file**: `.apeironcode-agent/bridge-connection.json` written by `bridge start`. -3. **Manual**: User is prompted if neither is available. - -The connection file contains endpoint + fingerprint only — the full token is never written there. - -## Bridge CLI Commands (Phase 16F.1) - -```bash -# Start bridge — writes connection file at .apeironcode-agent/bridge-connection.json -apeironcode bridge start - -# Check status — shows endpoint from connection file -apeironcode bridge status - -# Show token fingerprint (never full token) -apeironcode bridge token - -# Show full token (requires --show flag) -apeironcode bridge token --show - -# Stop bridge — removes connection file -apeironcode bridge stop -``` - -## Project Brain Commands (Phase 16G.2) - -| Command | Description | -|---|---| -| `ApeironCode: Open Project Brain Panel` | Open the full brain control center panel | -| `ApeironCode: Route Prompt to Brain` | Route a prompt to the best agents/skills | -| `ApeironCode: Preview Brain Context` | Preview token-efficient context selection | -| `ApeironCode: Build App Plan` | Generate a large app build plan | -| `ApeironCode: Orchestrate App Build` | Full orchestration: vision, stack, phases, agents | -| `ApeironCode: Show Saved Brain Previews` | Open Sync Previews panel | -| `ApeironCode: Audit Project Brain` | Run brain health audit | -| `ApeironCode: Sync Preview` | Preview a brain sync before writing | -| `ApeironCode: Apply Brain Sync` | Apply a sync preview (requires confirmation) | - -### App Build Plan View - -The **App Build Plan** panel shows a rich orchestration result: - -- Product vision, assumptions, and up to 3 clarifying questions. -- Detected stack with technology tags. -- Architecture outline. -- Phased build plan with per-phase task lists. -- Suggested agents with role and reasoning. -- Suggested project skills. -- Verification plan, risk list, first 3 next actions. -- Token strategy for long builds. - -Action buttons: - -- **Initialize Project Brain from this Plan** — requires explicit VS Code confirmation dialog before writing anything. -- **Copy Plan** — copies the full plan text to clipboard. -- **Start First Task** — sends the first action to the agent session (requires connection). -- **Preview Brain Context** — opens context preview in Project Brain panel. - -### Sync Previews View - -The **Sync Previews** panel lists all saved sync previews: - -- Shows preview ID, creation timestamp, target files, risk level, and stale warning. -- Stale previews (older than 7 days) are flagged with a warning banner. -- Secrets are redacted in the changes summary display. -- **Apply** requires an explicit VS Code confirmation modal. No writes without `approved:true`. -- High-risk previews show an extra warning in the confirmation dialog. - -## Runtime Brain Intelligence (Phase 16H) - -The Project Brain panel now includes a **Runtime Brain Intelligence** section: - -| Button | Bridge message | What it does | -|---|---|---| -| **Preview Brain Runtime Decision** | `brain.runtime` | Classifies prompt intent, shows whether brain will be used, confidence %, brain files, agents, token estimate | -| **Explain Brain Context** | `brain.explain` | Full debug explanation of how context was selected for the prompt | - -When the bridge returns a `brain.runtime` response with `useBrain=true`, an information notification appears: -> `Project Brain: debug-fix context injected (120 tokens)` - -The **Brain Used / Brain Skipped** badge appears in the panel with: -- Intent label (e.g. "Debug / Fix", "Continuation", "App Build") -- Confidence percentage -- Token estimate -- List of selected brain files (up to 6) -- List of suggested agents/skills (up to 4) -- Full debug explanation (when brain.explain response arrives) - -All content is HTML-escaped and protected by the webview CSP. No secrets appear in the panel output. - -## Current MVP Features - -- ✅ Sidebar chat with **real prompt submission** to live agent sessions -- ✅ Connection status indicator -- ✅ Bridge start/stop commands + connection file auto-discovery -- ✅ Selected file/range context attachment -- ✅ Permission request approve/deny cards -- ✅ Diff preview with risky-path warnings -- ✅ Task and worktree status -- ✅ Context view (files, token budget, model, runtime brain section) -- ✅ Tool event streaming (tool.started / tool.completed in chat) -- ✅ Session busy detection (rejects concurrent prompts cleanly) -- ✅ Project Brain control center panel with all brain actions -- ✅ App Build Plan rich view with phases, agents, skills, risks -- ✅ Sync Previews view with stale detection and secret redaction -- ✅ Runtime Brain Intelligence panel (intent, confidence, files, agents, tokens) - -## VSIX Packaging + Beta Install (Phase 16F.2) - -### Build and package the extension - -```bash -cd extensions/vscode -npm install -npm run package -# Produces: apeironcode-0.1.0.vsix -``` - -Or from the project root: - -```bash -npm run vscode:package -``` - -### Install from VSIX - -```bash -code --install-extension apeironcode-0.1.0.vsix -``` - -Or via VS Code UI: **Extensions** → **…** → **Install from VSIX…**. - -### Troubleshooting - -| Problem | Solution | -|---|---| -| Bridge not found | Run `apeironcode bridge start` in project dir | -| Auth failed | Re-run **ApeironCode: Start Bridge Connection** | -| Port already in use | Run `apeironcode bridge stop` | -| CLI not found | Set `apeironcode.cli.path` setting | - -### Bridge Auto-Start - -Enable `apeironcode.bridge.autoStart: true` to automatically start the bridge when a trusted workspace is opened. Requires the CLI on PATH or configured via `apeironcode.cli.path`. - -**Security**: auto-start is blocked in untrusted workspaces. Never exposes the token. - -### Model Selection - -Run **ApeironCode: Select Model / Provider** to choose a model from a quick-pick list. The selection is stored in workspace settings as `apeironcode.selectedModel` and used for new agent sessions. - -When the bridge is connected, the extension requests the provider catalog. When disconnected, a safe static fallback list is shown. - -## Production Hardening (Phase 16F.3) - -### Live Provider / Model Catalog - -The bridge now serves `provider.list` returning a safe catalog (no API keys, no env values): - -```bash -# Response includes: provider id, label, kind, configured status, models with contextWindow/toolCalling/streaming -``` - -The VS Code model selector loads live catalog when connected; falls back to a static list when disconnected. - -### Diff Apply Flow - -`diff.apply_requested` messages are permission-gated: -- Non-approved requests return `approval_required` -- Risky paths (`.env`, `package.json`, `node_modules/`, etc.) flag explicitly -- Path traversal rejected -- Oversized patches rejected (>50,000 chars) -- Direct FS apply via bridge is staged (not auto-applied) — see Known Limitations - -### Health Checklist - -Run `ApeironCode: Show Setup` to see a 7-item health report: -- CLI path ✓/⚠ -- Bridge endpoint ✓/⚠ -- Bridge token ✓/✗ -- Bridge connection ✓/✗ -- Model selected ✓/⚠ -- Workspace trusted ✓/⚠ -- Extension version ✓ - -### Error Recovery - -Each error type maps to a human-readable message with a command hint: -- `AUTH_FAILED` → Start Bridge -- `CONNECTION_REFUSED` → Show Setup -- `AGENT_FAILED` → Run Doctor -- `MODEL_UNAVAILABLE` → Select Model -- `SESSION_BUSY` → Open Chat -- (and 9 more error codes) - -## Known Limitations - -- **No auto-apply** of diffs via bridge — returns `unsupported` with guidance; apply manually or via CLI. -- **No JetBrains** or other IDE extension yet. -- **No Marketplace release** yet — install from VSIX only. -- **No remote/cloud** bridge — local-only by design. -- The bridge server runs for the lifetime of the CLI process that started it. -- `provider.list` catalog is read-only; no API key configuration from extension. - -## Extension Location - -Source: `extensions/vscode/` - -``` -extensions/vscode/ -├── package.json # VS Code extension manifest -├── tsconfig.json -├── CHANGELOG.md -├── .vscodeignore # VSIX packaging exclusions -├── src/ -│ ├── extension.ts # activation entry point -│ ├── config.ts # settings + secrets -│ ├── logger.ts # safe logging (no token in logs) -│ ├── types.ts # shared type interfaces -│ ├── bridge/ -│ │ ├── client.ts # BridgeClient (WebSocket) -│ │ ├── messages.ts # message factories (incl. send_prompt) -│ │ ├── auth.ts # token fingerprint helpers -│ │ ├── discovery.ts # bridge endpoint auto-discovery -│ │ └── autoStart.ts # bridge auto-start helper -│ ├── views/ -│ │ ├── chatPanel.ts -│ │ ├── webviewHtml.ts # incl. buildSetupHtml onboarding -│ │ └── messageStore.ts -│ ├── context/ -│ │ ├── selection.ts -│ │ └── contextView.ts -│ ├── permissions/ -│ │ ├── permissionStore.ts -│ │ └── permissionPanel.ts -│ ├── diff/ -│ │ └── diffPreview.ts -│ ├── tasks/ -│ │ └── taskView.ts -│ ├── status/ -│ │ └── statusBar.ts # bridge status bar item -│ └── model/ -│ └── modelSelector.ts # model/provider quick-pick -└── test/ # Vitest tests (mock VS Code APIs) -``` +The local bridge APIs may support future IDE integrations, but they should be +treated as experimental internal interfaces. Use the `apeironcode` CLI for all +documented public-alpha workflows. diff --git a/docs/workflows.md b/docs/workflows.md index 6956f29..5766419 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -85,8 +85,6 @@ apeironcode brain previews apply --yes This workflow is **deterministic** — each command either previews or requires `--yes`. No silent writes. No autonomous execution without your explicit approval at each step. -In VS Code, the same workflow is available through the **Project Brain Panel**, **App Build Plan View**, and **Sync Previews View**, each of which shows a VS Code confirmation dialog before writing anything. - ## Runtime Brain Integration (Phase 16H) When you run any prompt, the agent automatically classifies intent and injects relevant Project Brain context without requiring any extra commands: diff --git a/eslint.config.js b/eslint.config.js index 26e4dd0..6d1920c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -47,6 +47,9 @@ export default [ 'extensions/**', 'ApeironCode-web/**', '.playwright-mcp/**', + // Deliberate type-error fixture for the repair workflow; excluded from the + // TS project and not linted. + 'tests/fixtures/repair/ts-error/**', ], }, ]; \ No newline at end of file diff --git a/examples/demo/workspace-ignore/.opencodeignore b/examples/demo/workspace-ignore/.apeironcodeignore similarity index 100% rename from examples/demo/workspace-ignore/.opencodeignore rename to examples/demo/workspace-ignore/.apeironcodeignore diff --git a/package-lock.json b/package-lock.json index 1c15523..c89a0aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "apeironcode-agent", + "name": "apeironcode", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "apeironcode-agent", + "name": "apeironcode", "version": "0.1.0", "license": "MIT", "dependencies": { @@ -13,7 +13,7 @@ "cli-highlight": "^2.1.11", "commander": "^13.1.0", "conf": "^13.1.0", - "diff": "^7.0.0", + "diff": "9.0.0", "execa": "^9.6.0", "fast-glob": "^3.3.3", "ignore": "^7.0.5", @@ -22,13 +22,12 @@ "ora": "^8.2.0", "picocolors": "^1.1.1", "react": "^18.3.1", - "ws": "^8.0.0", + "ws": "8.21.0", "zod": "^3.24.2", "zod-to-json-schema": "^3.25.2" }, "bin": { - "apeironcode": "dist/cli/index.js", - "opencode": "dist/cli/index.js" + "apeironcode": "dist/cli/index.js" }, "devDependencies": { "@eslint/js": "^9.21.0", @@ -47,7 +46,7 @@ "tsup": "^8.4.0", "tsx": "^4.19.3", "typescript": "^5.8.2", - "vitest": "^3.0.8" + "vitest": "4.1.8" }, "engines": { "node": ">=18.18.0" @@ -78,6 +77,40 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", @@ -564,24 +597,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/config-array/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/@eslint/config-array/node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -662,24 +677,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@eslint/eslintrc/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -865,6 +862,25 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -900,6 +916,280 @@ "node": ">= 8" } }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", @@ -1268,6 +1558,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/aws4": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@types/aws4/-/aws4-1.11.6.tgz", @@ -1589,39 +1897,40 @@ } }, "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", "dev": true, "license": "MIT", "dependencies": { + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.4", + "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -1633,42 +1942,42 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", "pathe": "^2.0.3" }, "funding": { @@ -1676,28 +1985,25 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1863,9 +2169,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -1924,18 +2230,11 @@ } }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } @@ -1956,16 +2255,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -2203,13 +2492,6 @@ "node": ">=18" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/conf": { "version": "13.1.0", "resolved": "https://registry.npmjs.org/conf/-/conf-13.1.0.tgz", @@ -2250,6 +2532,13 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/convert-to-spaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", @@ -2313,16 +2602,6 @@ } } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2330,10 +2609,20 @@ "dev": true, "license": "MIT" }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -2385,9 +2674,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, @@ -2588,24 +2877,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/eslint/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", @@ -2821,9 +3092,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "funding": [ { "type": "github", @@ -3295,107 +3566,368 @@ "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", "license": "MIT", "engines": { - "node": ">=18" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "license": "MIT", + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.8.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lilconfig": { @@ -3503,13 +4035,6 @@ "loose-envify": "cli.js" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -3611,9 +4136,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -3673,6 +4198,20 @@ "node": ">=0.10.0" } }, + "node_modules/obug": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", + "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -3900,16 +4439,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3951,9 +4480,9 @@ } }, "node_modules/postcss": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", - "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -3971,7 +4500,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4205,6 +4734,40 @@ "node": ">=0.10.0" } }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, "node_modules/rollup": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", @@ -4426,9 +4989,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, @@ -4501,26 +5064,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/stubborn-fs": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", @@ -4617,9 +5160,9 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -4664,30 +5207,10 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -4736,6 +5259,14 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/tsup": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", @@ -4907,18 +5438,17 @@ } }, "node_modules/vite": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", - "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -4934,9 +5464,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -4949,13 +5480,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -4981,47 +5515,6 @@ } } }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/vite/node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", @@ -5036,65 +5529,79 @@ } }, "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@types/debug": { + "@opentelemetry/api": { "optional": true }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -5105,6 +5612,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, @@ -5121,6 +5631,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/when-exit": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", @@ -5214,9 +5734,9 @@ } }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index 4398eef..e6e1b88 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "apeironcode-agent", + "name": "apeironcode", "version": "0.1.0", "description": "Open-source, local-first AI coding agent. Use any model, see every action, and keep your code under your rules.", "displayName": "ApeironCode", @@ -10,8 +10,7 @@ "type": "module", "license": "MIT", "bin": { - "apeironcode": "./dist/cli/index.js", - "opencode": "./dist/cli/index.js" + "apeironcode": "./dist/cli/index.js" }, "files": [ "dist", @@ -21,12 +20,16 @@ "LICENSE", "docs/agents.md", "docs/architecture.md", + "docs/build-apps.md", "docs/commands.md", "docs/connectors.md", "docs/context-engine.md", + "docs/demo.md", "docs/demo-recording.md", "docs/assets/apeironcode-demo.gif", "docs/assets/apeironcode-demo.txt", + "docs/fix-code.md", + "docs/getting-started.md", "docs/github.md", "docs/history.md", "docs/hooks.md", @@ -37,6 +40,7 @@ "docs/providers.md", "docs/publishing.md", "docs/safety.md", + "docs/sandbox.md", "docs/security-limits.md", "docs/security-model.md", "docs/sessions.md", @@ -44,9 +48,12 @@ "docs/share.md", "docs/skills.md", "docs/tasks.md", + "docs/terminal-ux.md", "docs/tools.md", + "docs/testing.md", "docs/troubleshooting.md", "docs/tui.md", + "docs/ui-smoke.md", "docs/ui-ux.md", "docs/web-research.md", "docs/workflows.md", @@ -117,10 +124,12 @@ "smoke:tui-dogfood": "node scripts/manual-tui-dogfood.mjs", "smoke:master-dogfood": "node scripts/manual-master-dogfood-18a.mjs", "smoke:terminal-ux": "node scripts/manual-terminal-ux-smoke.mjs", + "test:ui-smoke": "vitest run tests/uiSmoke/browser-e2e.test.ts", "smoke:workflows": "vitest run tests/workflows", "smoke:all": "npm run smoke:cli && npm run smoke:workflows", + "check:audit": "npm audit --audit-level=moderate", "acceptance": "npm run typecheck && npm run lint && npm run test && npm run build", - "ci": "npm run typecheck && npm run lint && npm run build && npm test && npm run check:file-size && npm pack --dry-run" + "ci": "npm run typecheck && npm run lint && npm run build && npm test && npm run check:file-size && npm run check:audit && npm pack --dry-run" }, "keywords": [ "ai", @@ -144,7 +153,7 @@ "cli-highlight": "^2.1.11", "commander": "^13.1.0", "conf": "^13.1.0", - "diff": "^7.0.0", + "diff": "9.0.0", "execa": "^9.6.0", "fast-glob": "^3.3.3", "ignore": "^7.0.5", @@ -153,7 +162,7 @@ "ora": "^8.2.0", "picocolors": "^1.1.1", "react": "^18.3.1", - "ws": "^8.0.0", + "ws": "8.21.0", "zod": "^3.24.2", "zod-to-json-schema": "^3.25.2" }, @@ -174,6 +183,10 @@ "tsup": "^8.4.0", "tsx": "^4.19.3", "typescript": "^5.8.2", - "vitest": "^3.0.8" + "vitest": "4.1.8" + }, + "overrides": { + "brace-expansion": "5.0.6", + "fast-uri": "3.1.2" } } diff --git a/src/agent/filePlanExecutor.ts b/src/agent/filePlanExecutor.ts index 1a7ff0f..5a19faa 100644 --- a/src/agent/filePlanExecutor.ts +++ b/src/agent/filePlanExecutor.ts @@ -11,6 +11,8 @@ import type {ToolRegistry} from '../tools/registry.js'; import type {ToolExecutionContext} from '../tools/types.js'; import {validateFilePlan, formatFilePlanPreview, type FilePlan} from './filePlanProtocol.js'; import type {ToolCallRecord} from './types.js'; +import {evaluateCommandPolicy, toApprovalRisk} from '../sandbox/commandPolicy.js'; +import {formatPolicySummary} from '../sandbox/manager.js'; export interface FilePlanExecutionContext { approvalManager: ApprovalManagerType; @@ -199,12 +201,19 @@ export async function executeFilePlan( } for (const command of plan.commands) { + const policy = evaluateCommandPolicy({command: command.command, cwd: context.cwd, workspace: context.cwd}); + if (policy.risk === 'blocked') { + toolCalls.push(record('run_command', {command: command.command}, 'error', undefined, policy.reason)); + errors.push(`Blocked command was not run: ${command.command} (${policy.reason})`); + continue; + } const approved = await context.approvalManager.request({ - details: command.reason, + details: `${command.reason}\n${formatPolicySummary(policy)}`, kind: 'command', message: command.command, resource: command.command, - riskLevel: 'medium', + requiresExtraConfirmation: policy.risk === 'high', + riskLevel: toApprovalRisk(policy.risk), scope: 'project', title: 'Approve shell command', }); diff --git a/src/agent/filePlanParser.ts b/src/agent/filePlanParser.ts new file mode 100644 index 0000000..5aa02e7 --- /dev/null +++ b/src/agent/filePlanParser.ts @@ -0,0 +1,108 @@ +import { + FilePlanSchema, + buildFilePlanCorrectionText, + formatZodIssues, + validateFilePlanSemantics, + type FilePlanSemanticOptions, +} from './filePlanSchema.js'; +import { + coercePlanShape, + extractBalancedObject, + extractBalancedObjects, + extractFencedJson, + repairLooseJson, + unwrapToolCallEnvelope, +} from './filePlanRepair.js'; +import type {FilePlan} from './filePlanProtocol.js'; + +export type ParseOptions = FilePlanSemanticOptions; + +export type FilePlanParseStage = 'json' | 'schema' | 'semantic'; + +export type ParseResult = + | {ok: true; plan: FilePlan; strategy: string} + | {ok: false; stage: FilePlanParseStage; errors: string[]; correctionText: string}; + +/** + * Yield candidate JSON strings from raw model output, cheapest/most-likely + * first: the raw text, a fenced block, the largest balanced object, and tolerant + * repairs of each. + */ +const candidateJsonStrings = (input: string): Array<{strategy: string; text: string}> => { + const trimmed = input.trim(); + const candidates: Array<{strategy: string; text: string}> = [{strategy: 'direct', text: trimmed}]; + + const fenced = extractFencedJson(input); + if (fenced) candidates.push({strategy: 'fenced', text: fenced}); + + // All top-level balanced objects, largest first (the real plan tends to be the + // largest), so prose like `{ not json }` before the plan does not win. + const balanced = [...extractBalancedObjects(input)].sort((a, b) => b.length - a.length); + for (const text of balanced) candidates.push({strategy: 'balanced', text}); + + const fencedBalanced = fenced ? extractBalancedObject(fenced) : undefined; + if (fencedBalanced) candidates.push({strategy: 'fenced-balanced', text: fencedBalanced}); + + // Tolerant repairs of each distinct candidate. + const repaired: Array<{strategy: string; text: string}> = []; + for (const candidate of candidates) { + const fixed = repairLooseJson(candidate.text); + if (fixed && fixed !== candidate.text) { + repaired.push({strategy: `${candidate.strategy}+repair`, text: fixed}); + } + } + return [...candidates, ...repaired]; +}; + +/** + * Robustly parse a model's file-plan response (Phase 20B, Task A). + * + * Strategy order: direct JSON → fenced block → largest balanced object → + * tolerant repair (comments/trailing commas/smart quotes) → tool-call envelope + * unwrap → shape coercion → zod schema → semantic safety validation. No single + * strict `JSON.parse` failure kills the workflow, and failures return exact, + * model-facing validation errors plus a ready-to-send correction prompt. + */ +export const parseFilePlanResponse = (input: string, options: ParseOptions = {}): ParseResult => { + if (!input || !input.trim()) { + const errors = ['empty response: expected a JSON file plan']; + return {ok: false, stage: 'json', errors, correctionText: buildFilePlanCorrectionText(errors)}; + } + + let parsedAny: {value: unknown; strategy: string} | undefined; + for (const candidate of candidateJsonStrings(input)) { + try { + parsedAny = {value: JSON.parse(candidate.text), strategy: candidate.strategy}; + break; + } catch { + // try next strategy + } + } + + if (!parsedAny) { + const errors = ['response was not valid JSON after fence/balance/repair extraction']; + return {ok: false, stage: 'json', errors, correctionText: buildFilePlanCorrectionText(errors)}; + } + + const coerced = coercePlanShape(unwrapToolCallEnvelope(parsedAny.value)); + + const schemaResult = FilePlanSchema.safeParse(coerced); + if (!schemaResult.success) { + const errors = formatZodIssues(schemaResult.error); + return {ok: false, stage: 'schema', errors, correctionText: buildFilePlanCorrectionText(errors)}; + } + + const semanticErrors = validateFilePlanSemantics(schemaResult.data, options); + if (semanticErrors.length > 0) { + return { + ok: false, + stage: 'semantic', + errors: semanticErrors, + correctionText: buildFilePlanCorrectionText(semanticErrors), + }; + } + + // schemaResult.data is structurally a FilePlan (patch is an extra optional field). + const plan: FilePlan = schemaResult.data; + return {ok: true, plan, strategy: parsedAny.strategy}; +}; diff --git a/src/agent/filePlanProtocol.ts b/src/agent/filePlanProtocol.ts index 7f07911..4feb3e5 100644 --- a/src/agent/filePlanProtocol.ts +++ b/src/agent/filePlanProtocol.ts @@ -1,6 +1,7 @@ import path from 'node:path'; import type {CodingIntent} from './codingIntent.js'; +import {parseFilePlanResponse as robustParseFilePlanResponse} from './filePlanParser.js'; export type FilePlanOperation = 'create' | 'overwrite' | 'modify' | 'delete' | 'rename'; @@ -9,6 +10,8 @@ export interface FilePlanFileChange { from?: string; operation: FilePlanOperation; path: string; + /** Optional unified-diff patch (Phase 20B). Reserved for patch-based edits. */ + patch?: string; } export interface FilePlanCommand { @@ -67,45 +70,6 @@ export const buildPremiumUiDirective = (): string => const SECRET_RE = /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*["']?[a-z0-9_./+=-]{12,}/iu; const BINARY_EXT_RE = /\.(?:png|jpe?g|gif|webp|ico|pdf|zip|gz|tar|mp4|mov|wasm|woff2?|ttf)$/iu; -const normalizePlan = (value: unknown): FilePlan | null => { - if (!value || typeof value !== 'object' || Array.isArray(value)) return null; - const obj = value as Record; - if (typeof obj.summary !== 'string') return null; - if (!Array.isArray(obj.files) || !Array.isArray(obj.commands) || !Array.isArray(obj.validation)) return null; - const files: FilePlanFileChange[] = []; - for (const entry of obj.files) { - if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; - const file = entry as Record; - if (typeof file.path !== 'string' || typeof file.operation !== 'string') return null; - if (!['create', 'overwrite', 'modify', 'delete', 'rename'].includes(file.operation)) return null; - files.push({ - content: typeof file.content === 'string' ? file.content : undefined, - from: typeof file.from === 'string' ? file.from : undefined, - operation: file.operation as FilePlanOperation, - path: file.path, - }); - } - const commands: FilePlanCommand[] = []; - for (const entry of obj.commands) { - if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null; - const command = entry as Record; - if (typeof command.command !== 'string' || typeof command.reason !== 'string') return null; - commands.push({command: command.command, reason: command.reason}); - } - const validation = obj.validation.every((item) => typeof item === 'string') - ? obj.validation - : null; - return validation ? {commands, files, summary: obj.summary, validation} : null; -}; - -const extractJson = (text: string): string => { - const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/iu); - if (fenced?.[1]) return fenced[1].trim(); - const first = text.indexOf('{'); - const last = text.lastIndexOf('}'); - return first >= 0 && last > first ? text.slice(first, last + 1) : text.trim(); -}; - export function buildFilePlanPrompt( intent: CodingIntent, workspaceSnapshot: string, @@ -138,15 +102,16 @@ export function buildFilePlanPrompt( } export function parseFilePlanResponse(text: string): FilePlanParseResult { - try { - const parsed = JSON.parse(extractJson(text)) as unknown; - const plan = normalizePlan(parsed); - if (!plan) return {error: 'File plan JSON did not match the required shape.', ok: false}; - return {ok: true, plan}; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return {error: `Invalid file plan JSON: ${message}`, ok: false}; - } + // Delegate to the robust multi-strategy parser (Phase 20B, Task A). The + // legacy shape `{ok, plan} | {ok:false, error}` is preserved for existing + // callers; semantic safety checks here stay permissive (allowDelete) because + // executeFilePlan re-validates with the caller's real options. + const result = robustParseFilePlanResponse(text, {allowDelete: true, allowEnv: true}); + if (result.ok) return {ok: true, plan: result.plan}; + const prefix = result.stage === 'json' + ? 'Invalid file plan JSON' + : 'File plan JSON did not match the required shape'; + return {error: `${prefix}: ${result.errors.join('; ')}`, ok: false}; } const isUnsafePath = (cwd: string, filePath: string): boolean => { diff --git a/src/agent/filePlanRepair.ts b/src/agent/filePlanRepair.ts new file mode 100644 index 0000000..8b76abf --- /dev/null +++ b/src/agent/filePlanRepair.ts @@ -0,0 +1,138 @@ +/** + * Low-level text → candidate-JSON extraction and tolerant repair (Phase 20B, + * Task A). These helpers never throw; they return candidate strings/objects for + * the parser to validate. Repair stays conservative — we do not "fix" content in + * ways that could change a plan's meaning. + */ + +/** Extract the body of the first ```json (or bare ```) fenced block, if any. */ +export const extractFencedJson = (text: string): string | undefined => { + const fenced = text.match(/```(?:json|json5)?\s*([\s\S]*?)```/iu); + return fenced?.[1]?.trim(); +}; + +/** + * Collect every top-level balanced `{...}` object substring, respecting string + * literals and escapes. Nested objects are not returned separately. Order is + * left-to-right. + */ +export const extractBalancedObjects = (text: string): string[] => { + const objects: string[] = []; + let depth = 0; + let start = -1; + let inString = false; + let escape = false; + for (let i = 0; i < text.length; i += 1) { + const ch = text[i]!; + if (escape) { + escape = false; + continue; + } + if (ch === '\\') { + if (inString) escape = true; + continue; + } + if (ch === '"') { + inString = !inString; + continue; + } + if (inString) continue; + if (ch === '{') { + if (depth === 0) start = i; + depth += 1; + } else if (ch === '}' && depth > 0) { + depth -= 1; + if (depth === 0 && start >= 0) { + objects.push(text.slice(start, i + 1)); + start = -1; + } + } + } + return objects; +}; + +/** + * The largest top-level balanced object substring, or undefined when none. + */ +export const extractBalancedObject = (text: string): string | undefined => { + const objects = extractBalancedObjects(text); + if (objects.length === 0) return undefined; + return objects.reduce((longest, current) => (current.length > longest.length ? current : longest)); +}; + +/** + * Tolerant cleanup of near-JSON: strip `//` and block comments, remove trailing + * commas, and normalize smart quotes. Returns a new string; never throws. + */ +export const repairLooseJson = (input: string): string => { + let out = input; + // Normalize smart quotes to straight quotes. + out = out.replace(/[“”]/gu, '"').replace(/[‘’]/gu, "'"); + // Strip block comments. + out = out.replace(/\/\*[\s\S]*?\*\//gu, ''); + // Strip line comments that are not inside a string (best-effort: only when the + // `//` is preceded by whitespace, a comma, or a brace/bracket). + out = out.replace(/(^|[\s,{}[\]])\/\/[^\n]*/gu, '$1'); + // Remove trailing commas before } or ]. + out = out.replace(/,(\s*[}\]])/gu, '$1'); + return out.trim(); +}; + +interface JsonObject { + [key: string]: unknown; +} + +const isObject = (value: unknown): value is JsonObject => + typeof value === 'object' && value !== null && !Array.isArray(value); + +/** + * Unwrap a known tool-call envelope to the inner plan object. Models sometimes + * wrap the plan as `{tool: "...", arguments: {...}}` / `{input: {...}}` / + * `{file_plan: {...}}` / `{plan: {...}}`. `arguments`/`input` may be a JSON + * string. Returns the inner object, or the value itself when no envelope. + */ +export const unwrapToolCallEnvelope = (value: unknown): unknown => { + if (!isObject(value)) return value; + for (const key of ['file_plan', 'filePlan', 'plan', 'arguments', 'input']) { + if (key in value) { + const inner = value[key]; + if (typeof inner === 'string') { + try { + return JSON.parse(inner) as unknown; + } catch { + return inner; + } + } + if (isObject(inner)) return inner; + } + } + return value; +}; + +/** + * Coerce common "almost right" plan shapes into the canonical object so schema + * validation can succeed: + * - a single file object → wrapped in an array + * - `commands` as a string or array of strings → `{command, reason}` objects + * - missing `commands`/`validation` → empty arrays + */ +export const coercePlanShape = (value: unknown): unknown => { + if (!isObject(value)) return value; + const out: JsonObject = {...value}; + + if (isObject(out.files)) out.files = [out.files]; + + if (out.commands === undefined) out.commands = []; + else if (typeof out.commands === 'string') { + out.commands = [{command: out.commands, reason: ''}]; + } else if (Array.isArray(out.commands)) { + out.commands = (out.commands as unknown[]).map((entry: unknown) => + typeof entry === 'string' ? {command: entry, reason: ''} : entry, + ); + } + + if (out.validation === undefined) out.validation = []; + else if (typeof out.validation === 'string') out.validation = [out.validation]; + + return out; +}; diff --git a/src/agent/filePlanSchema.ts b/src/agent/filePlanSchema.ts new file mode 100644 index 0000000..aa9ab6e --- /dev/null +++ b/src/agent/filePlanSchema.ts @@ -0,0 +1,142 @@ +import path from 'node:path'; +import {z} from 'zod'; + +/** + * Zod schema + semantic validation for file plans (Phase 20B, Task A). + * + * The schema validates the JSON shape; `validateFilePlanSemantics` enforces the + * safety rules (no absolute paths, no `..` escapes, no `.git`/`.env`, operation + * content requirements). Both produce structured, model-facing error strings so + * a malformed plan can be corrected rather than crashing the workflow. + */ + +export const FILE_PLAN_OPERATIONS = ['create', 'overwrite', 'modify', 'delete', 'rename'] as const; + +export const FilePlanOperationSchema = z.enum(FILE_PLAN_OPERATIONS); + +export const FilePlanFileSchema = z.object({ + path: z.string().min(1), + operation: FilePlanOperationSchema, + content: z.string().optional(), + from: z.string().optional(), + patch: z.string().optional(), +}); + +export const FilePlanCommandSchema = z.object({ + command: z.string().min(1), + reason: z.string(), +}); + +export const FilePlanSchema = z.object({ + summary: z.string(), + files: z.array(FilePlanFileSchema), + commands: z.array(FilePlanCommandSchema), + validation: z.array(z.string()), +}); + +export type FilePlanShape = z.infer; + +export interface FilePlanSemanticOptions { + cwd?: string; + /** Allow delete operations (off by default). */ + allowDelete?: boolean; + /** Allow modifying `.env*` files (off by default). */ + allowEnv?: boolean; + /** Max total content bytes. */ + maxContentBytes?: number; +} + +const SECRET_RE = /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*["']?[a-z0-9_./+=-]{12,}/iu; +const ENV_FILE_RE = /(?:^|\/)\.env(?:\.[\w.-]+)?$/iu; +const GIT_PATH_RE = /(?:^|\/)\.git(?:\/|$)/iu; + +const isAbsoluteOrEscaping = (cwd: string, filePath: string): boolean => { + if (!filePath.trim() || path.isAbsolute(filePath) || filePath.startsWith('~')) return true; + const resolved = path.resolve(cwd, filePath); + const relative = path.relative(cwd, resolved); + return relative === '' || relative.startsWith('..') || path.isAbsolute(relative); +}; + +/** + * Enforce file-plan safety semantics that the JSON schema cannot express. + * Returns an array of model-facing error strings (empty when the plan is safe). + */ +export const validateFilePlanSemantics = ( + plan: FilePlanShape, + options: FilePlanSemanticOptions = {}, +): string[] => { + const cwd = options.cwd ?? process.cwd(); + const maxBytes = options.maxContentBytes ?? 120_000; + const errors: string[] = []; + let contentBytes = 0; + + if (plan.files.length === 0 && plan.commands.length === 0) { + errors.push('plan is empty: provide at least one file change or command'); + } + + plan.files.forEach((file, index) => { + const at = `files[${index}]`; + if (isAbsoluteOrEscaping(cwd, file.path)) { + errors.push(`${at}.path must be a relative path inside the workspace (no absolute paths or "..")`); + } + if (GIT_PATH_RE.test(file.path)) errors.push(`${at}.path must not target the .git directory`); + if (ENV_FILE_RE.test(file.path) && !options.allowEnv) { + errors.push(`${at}.path must not modify .env files (use --allow-env to override)`); + } + + const needsBody = file.operation === 'create' || file.operation === 'overwrite' || file.operation === 'modify'; + if (needsBody && file.content === undefined && file.patch === undefined) { + errors.push(`${at}.content (or .patch) is required for operation=${file.operation}`); + } + if (file.operation === 'delete' && file.content !== undefined) { + errors.push(`${at}.content must not be set for operation=delete`); + } + if (file.operation === 'delete' && !options.allowDelete) { + errors.push(`${at} delete requires explicit intent and is not allowed here`); + } + if (file.operation === 'rename') { + if (!file.from) errors.push(`${at}.from is required for operation=rename`); + if (file.from && isAbsoluteOrEscaping(cwd, file.from)) { + errors.push(`${at}.from must be a relative path inside the workspace`); + } + } + if (typeof file.content === 'string') { + contentBytes += Buffer.byteLength(file.content, 'utf8'); + if (SECRET_RE.test(file.content)) errors.push(`${at}.content appears to contain a secret; remove it`); + } + }); + + if (contentBytes > maxBytes) { + errors.push(`total file content is too large (${contentBytes} > ${maxBytes} bytes)`); + } + + plan.commands.forEach((command, index) => { + if (!command.command.trim()) errors.push(`commands[${index}].command must not be empty`); + if (SECRET_RE.test(command.command)) errors.push(`commands[${index}].command appears to contain a secret`); + }); + + return errors; +}; + +/** + * Flatten zod issues into short model-facing strings like `files[0].content is required`. + */ +export const formatZodIssues = (error: z.ZodError): string[] => + error.issues.map((issue) => { + const at = issue.path.length > 0 ? issue.path.join('.') : '(root)'; + return `${at}: ${issue.message}`; + }); + +/** + * Build the correction prompt body shown to the model when a plan is invalid. + */ +export const buildFilePlanCorrectionText = (errors: string[]): string => + [ + 'Your file plan failed validation:', + ...errors.map((error) => `- ${error}`), + '', + 'Required JSON shape:', + '{"summary":"string","files":[{"path":"relative/path","operation":"create|overwrite|modify|delete|rename","content":"string optional","from":"string optional","patch":"string optional"}],"commands":[{"command":"string","reason":"string"}],"validation":["string"]}', + '', + 'Return corrected JSON only. No prose, no markdown fences.', + ].join('\n'); diff --git a/src/agent/planner.ts b/src/agent/planner.ts index 5580b0a..380c684 100644 --- a/src/agent/planner.ts +++ b/src/agent/planner.ts @@ -1,11 +1,9 @@ import {AppError} from '../utils/errors.js'; import type {ToolDirective} from './types.js'; -// Legacy XML-style tool-call envelope. The production tool-calling path is -// native ToolSchema/ToolRegistry; this pattern only exists to recognise -// pre-rebrand legacy emissions (`...`) and the canonical -// generic `...` form. -const TOOL_CALL_BLOCK_PATTERN = /<(?:apeironcode_tool_call|opencode_tool_call|tool_call)>\s*([\s\S]*?)\s*<\/(?:apeironcode_tool_call|opencode_tool_call|tool_call)>/gu; +// Compatibility parser only. Production tool execution uses ToolSchema and +// ToolRegistry rather than text directives. +const TOOL_CALL_BLOCK_PATTERN = /<(?:apeironcode_tool_call|tool_call)>\s*([\s\S]*?)\s*<\/(?:apeironcode_tool_call|tool_call)>/gu; export interface ToolDirectiveAnalysis { directives: ToolDirective[]; @@ -75,8 +73,7 @@ const looksLikeToolDirective = (raw: string): boolean => { || trimmed.startsWith('[') || trimmed.startsWith('```json') || trimmed.includes('') - || trimmed.includes('') - || trimmed.includes(''); + || trimmed.includes(''); }; export const analyzeToolDirectives = (raw: string): ToolDirectiveAnalysis => { @@ -119,4 +116,4 @@ export const assertIterationBudget = (iterations: number, maxIterations: number) 'AGENT_ITERATION_LIMIT', ); } -}; \ No newline at end of file +}; diff --git a/src/agents/workspace/ignoreRules.ts b/src/agents/workspace/ignoreRules.ts index 139c24f..96c64e9 100644 --- a/src/agents/workspace/ignoreRules.ts +++ b/src/agents/workspace/ignoreRules.ts @@ -22,7 +22,7 @@ const BUILTIN_IGNORES = [ export interface WorkspaceIgnoreMatch { path: string; rule: string; - source: 'builtin' | 'gitignore' | 'apeironcodeignore' | 'opencodeignore'; + source: 'builtin' | 'gitignore' | 'apeironcodeignore'; } export interface WorkspaceIgnoreRules { @@ -68,8 +68,6 @@ export const loadWorkspaceIgnoreRules = async (root: string): Promise ({pattern, source: 'builtin' as const})), ...await readIgnoreFile(root, '.gitignore', 'gitignore'), ...await readIgnoreFile(root, '.apeironcodeignore', 'apeironcodeignore'), - // Legacy fallback for projects that still use `.opencodeignore`. - ...await readIgnoreFile(root, '.opencodeignore', 'opencodeignore'), ]; return { diff --git a/src/agents/workspace/types.ts b/src/agents/workspace/types.ts index 6f8a5ae..84f72d9 100644 --- a/src/agents/workspace/types.ts +++ b/src/agents/workspace/types.ts @@ -33,7 +33,7 @@ export interface WorkspaceDiff { ignoredFiles?: Array<{ path: string; rule: string; - source: 'builtin' | 'gitignore' | 'apeironcodeignore' | 'opencodeignore'; + source: 'builtin' | 'gitignore' | 'apeironcodeignore'; }>; workspace: SubagentWorkspace; } diff --git a/src/appBuilder/appBuilder.ts b/src/appBuilder/appBuilder.ts new file mode 100644 index 0000000..303d424 --- /dev/null +++ b/src/appBuilder/appBuilder.ts @@ -0,0 +1,168 @@ +import {promises as fs} from 'node:fs'; +import path from 'node:path'; + +import {parseAppRequest, slugify, type ParseAppRequestOptions} from './requestParser.js'; +import {selectTemplate} from './templateRegistry.js'; +import {renderTemplate, writeTemplateFiles} from './templateRenderer.js'; +import {validateApp} from './appValidation.js'; +import {buildAppApprovalBundle, type AppBuildResult} from './appReport.js'; +import {runUiSmoke} from '../uiSmoke/runUiSmoke.js'; +import type {BrowserLauncher} from '../uiSmoke/types.js'; +import {ProgressStore} from '../progress/progressStore.js'; +import type {AppRequest, AppTemplate, TemplateVariables} from './types.js'; + +export interface BuildAppOptions extends ParseAppRequestOptions { + cwd: string; + /** Explicit output directory (absolute or relative to cwd). */ + dir?: string; + dryRun?: boolean; + /** Allow writing into a non-empty directory (still never overwrites files). */ + overwrite?: boolean; + /** Run command validation steps (Vite build) — only in temp/approved flows. */ + runCommands?: boolean; + /** Run a browser UI smoke after creation (static apps; skipped if no browser). */ + uiSmoke?: boolean; + /** Require the UI smoke to pass; a skip/fail marks the build as failed. */ + requireUiSmoke?: boolean; + /** Inject a browser launcher (tests use a deterministic one). */ + uiSmokeLauncher?: BrowserLauncher; + signal?: AbortSignal; + /** Approval gate. Returns true to proceed with writes. */ + approve: (bundle: {title: string; body: string; risk: 'low' | 'medium'}) => Promise; +} + +const toVars = (request: AppRequest): TemplateVariables => ({ + appName: request.appName, + appSlug: request.appSlug, + description: request.description, + style: request.style, + features: request.features, +}); + +const isNonEmptyDir = async (dir: string): Promise => { + try { + const entries = await fs.readdir(dir); + return entries.length > 0; + } catch { + return false; // does not exist → treated as empty/creatable + } +}; + +const resolveAppDir = (options: BuildAppOptions, request: AppRequest): string => + options.dir + ? path.resolve(options.cwd, options.dir) + : path.resolve(options.cwd, request.appSlug); + +/** + * Build a new app from a template (Phase 20C). Deterministic: parse → select → + * render → approve → write → validate. Never overwrites a non-empty directory + * without `overwrite`, never writes outside the app dir, and never fakes green. + */ +export const buildApp = async (idea: string, options: BuildAppOptions): Promise => { + const request = parseAppRequest(idea, options); + if (options.dir) { + // Keep the slug aligned with an explicit directory name for nicer reports. + request.appSlug = slugify(path.basename(path.resolve(options.cwd, options.dir))); + } + const template: AppTemplate = selectTemplate(request); + const vars = toVars(request); + const appDir = resolveAppDir(options, request); + const relativeDir = path.relative(options.cwd, appDir); + // Show a clean relative path when inside the workspace, otherwise the + // absolute path (avoids ugly ../../../ chains for temp/explicit dirs). + const displayDir = !relativeDir || relativeDir.startsWith('..') || path.isAbsolute(relativeDir) + ? appDir + : `./${relativeDir}`; + + const rendered = renderTemplate(template, vars); + if (rendered.errors.length > 0) { + return baseResult(request, template, appDir, displayDir, 'write-error', rendered.errors); + } + const fileList = rendered.files.map((file) => file.path); + + // Overwrite protection: a non-empty target needs explicit --overwrite. + if ((await isNonEmptyDir(appDir)) && !options.overwrite) { + return baseResult(request, template, appDir, displayDir, 'blocked-existing', []); + } + + if (options.dryRun) { + return {...baseResult(request, template, appDir, displayDir, 'dry-run', request.notes), filesCreated: fileList}; + } + + const bundle = buildAppApprovalBundle(request, template, displayDir, rendered.files); + if (!(await options.approve(bundle))) { + return baseResult(request, template, appDir, displayDir, 'rejected', []); + } + + // `--overwrite` only unblocks a non-empty directory; individual existing + // files are still never replaced (reported as refusals in notes). + const writeResult = await writeTemplateFiles(appDir, rendered.files, {overwrite: false}); + if (writeResult.errors.length > 0 && writeResult.written.length === 0) { + return baseResult(request, template, appDir, displayDir, 'write-error', writeResult.errors); + } + + // Lightweight metadata so `apeironcode test-ui` can pick template-specific + // interaction checks. `.apeironcode` is a runtime/gitignored directory. + await fs.mkdir(path.join(appDir, '.apeironcode'), {recursive: true}).catch(() => undefined); + await fs + .writeFile( + path.join(appDir, '.apeironcode', 'app.json'), + JSON.stringify({createdBy: 'ApeironCode', templateId: template.id, appName: request.appName, createdAt: new Date().toISOString()}, null, 2), + 'utf8', + ) + .catch(() => undefined); + + const validation = await validateApp(appDir, template, vars, rendered.files, { + runCommands: options.runCommands, + signal: options.signal, + }); + + let uiSmoke; + let uiSmokeRequiredFailed = false; + if ((options.uiSmoke || options.requireUiSmoke) && !template.requiresCommands) { + uiSmoke = await runUiSmoke(appDir, {cwd: appDir, screenshot: true, launcher: options.uiSmokeLauncher}); + if (options.requireUiSmoke && uiSmoke.status !== 'passed') uiSmokeRequiredFailed = true; + } + + // Build the progress run for the live board (Phase 20E.5, Task A). + const progress = new ProgressStore('new', `Build ${request.appName}`, {projectPath: displayDir}) + .planTasks([ + {id: 'parse', title: 'Parse request'}, + {id: 'template', title: 'Select template'}, + {id: 'approve', title: 'Await approval'}, + {id: 'write', title: 'Write files'}, + {id: 'validate', title: 'Run static validation'}, + {id: 'smoke', title: 'Run UI smoke'}, + ]) + .setTask('parse', 'passed', request.kind) + .setTask('template', 'passed', template.id) + .setTask('approve', 'passed') + .setTask('write', writeResult.written.length > 0 ? 'passed' : 'failed', `${writeResult.written.length} file(s)`) + .setTask('validate', validation.passed ? 'passed' : 'failed') + .setTask('smoke', uiSmoke ? (uiSmoke.status === 'passed' ? 'passed' : uiSmoke.status === 'failed' ? 'failed' : 'skipped') : 'skipped', uiSmoke?.summary) + .addFiles(writeResult.written.map((p) => ({path: p, operation: 'create' as const, status: 'applied' as const}))) + .snapshot(); + + return { + request, + template, + appDir, + displayDir, + filesCreated: writeResult.written, + validation, + uiSmoke, + uiSmokeRequiredFailed, + progress, + status: 'created', + notes: [...request.notes, ...writeResult.errors], + }; +}; + +const baseResult = ( + request: AppRequest, + template: AppTemplate, + appDir: string, + displayDir: string, + status: AppBuildResult['status'], + notes: string[], +): AppBuildResult => ({request, template, appDir, displayDir, filesCreated: [], status, notes}); diff --git a/src/appBuilder/appReport.ts b/src/appBuilder/appReport.ts new file mode 100644 index 0000000..f026a21 --- /dev/null +++ b/src/appBuilder/appReport.ts @@ -0,0 +1,162 @@ +import type {AppValidationResult} from './appValidation.js'; +import {formatValidation} from './appValidation.js'; +import {buildApprovalBundle} from '../safety/approvalBundle.js'; +import {buildDiffPreview} from '../safety/diffPreview.js'; +import {TASK_SYMBOL, type ProgressRun} from '../progress/types.js'; +import type {UiSmokeResult} from '../uiSmoke/types.js'; +import type {AppRequest, AppTemplate} from './types.js'; + +export interface AppBuildResult { + request: AppRequest; + template: AppTemplate; + appDir: string; + displayDir: string; + filesCreated: string[]; + validation?: AppValidationResult; + /** UI smoke result when `--ui-smoke`/`--require-ui-smoke` was requested. */ + uiSmoke?: UiSmokeResult; + /** True when `--require-ui-smoke` was set and the smoke did not pass. */ + uiSmokeRequiredFailed?: boolean; + /** Progress run for the live board (Phase 20E.5). */ + progress?: ProgressRun; + status: 'created' | 'rejected' | 'blocked-existing' | 'write-error' | 'dry-run'; + notes: string[]; +} + +const uiSmokeLine = (result: UiSmokeResult): string => { + if (result.status === 'passed') return ' ✓ UI smoke passed'; + if (result.status === 'skipped') return ` ○ UI smoke skipped: ${result.summary}`; + return ` ✗ UI smoke failed: ${result.summary}`; +}; + +/** + * Build the approval bundle text for creating a new app (Phase 20C, Task F). + */ +export const buildAppApprovalBundle = ( + request: AppRequest, + template: AppTemplate, + displayDir: string, + files: Array<{path: string; content: string}>, +): {title: string; body: string; risk: 'low' | 'medium'} => { + const risk: 'low' | 'medium' = template.requiresCommands ? 'medium' : 'low'; + const validation = template.validation(toVars(request)).map((step) => step.command ?? step.name); + // Approval bundle 2.0 (Phase 20E.5): shared compact bundle + diff preview of + // the created files so the user sees what will be written before approving. + const bundle = buildApprovalBundle({ + intent: `create a new app in ${displayDir}`, + files: files.map((file) => ({path: file.path, operation: 'create'})), + commands: [], + validation, + }); + const diff = buildDiffPreview(files.map((file) => ({path: file.path, operation: 'create', before: null, after: file.content}))); + + const lines: string[] = [bundle.body]; + if (request.notes.length > 0) { + lines.push(''); + lines.push('Notes:'); + for (const note of request.notes) lines.push(`- ${note}`); + } + lines.push(''); + lines.push(diff); + return {title: 'Approve new app', body: lines.join('\n'), risk: bundle.risk === 'high' ? 'medium' : (risk)}; +}; + +const toVars = (request: AppRequest) => ({ + appName: request.appName, + appSlug: request.appSlug, + description: request.description, + style: request.style, + features: request.features, +}); + +/** + * Format the final app-build report. Honest: never says a server started, never + * claims real auth/database, and surfaces validation results as-is. + */ +export const formatAppReport = (result: AppBuildResult): string => { + const lines: string[] = []; + + if (result.status === 'blocked-existing') { + lines.push(`Cannot create the app: ${result.displayDir} already exists and is not empty.`); + lines.push('Choose one:'); + lines.push(`- create in a subdirectory: apeironcode new "${result.request.description}" --dir ${result.displayDir}/${result.request.appSlug}`); + lines.push('- re-run with --overwrite to write into it (existing files are still never silently replaced)'); + lines.push('- cancel'); + return lines.join('\n'); + } + if (result.status === 'rejected') { + return 'No app was created because approval was declined.'; + } + if (result.status === 'write-error') { + lines.push('The app could not be created cleanly:'); + for (const note of result.notes) lines.push(`- ${note}`); + return lines.join('\n'); + } + + if (result.status === 'dry-run') { + lines.push(`Dry run. Would create ${result.template.name}: ${result.request.appName}`); + lines.push(''); + lines.push(`Directory: ${result.displayDir}`); + lines.push('Files:'); + for (const file of result.filesCreated) lines.push(` + ${file}`); + lines.push(''); + lines.push('Nothing was written.'); + return lines.join('\n'); + } + + lines.push(`Done. Created ${result.request.appName}.`); + if (result.progress) { + lines.push(''); + lines.push('Plan'); + for (const task of result.progress.tasks) { + lines.push(` ${TASK_SYMBOL[task.status]} ${task.title}${task.detail ? ` ${task.detail}` : ''}`); + } + } + lines.push(''); + lines.push(`App directory:\n${result.displayDir}`); + lines.push(''); + lines.push('Files created:'); + for (const file of result.filesCreated) lines.push(` + ${file}`); + + if (result.validation) { + lines.push(''); + lines.push(result.validation.passed ? 'Validation:' : 'Validation (some checks failed):'); + lines.push(formatValidation(result.validation)); + if (result.uiSmoke) lines.push(uiSmokeLine(result.uiSmoke)); + } + + if (result.uiSmokeRequiredFailed) { + lines.push(''); + lines.push('UI smoke was required (--require-ui-smoke) but did not pass.'); + } + + // Surface UI smoke artifacts (Phase 20E, Task G). + if (result.uiSmoke) { + const artifacts: string[] = []; + if (result.uiSmoke.screenshotPath) artifacts.push(` Screenshot: ${result.uiSmoke.screenshotPath}`); + if (result.uiSmoke.reportPath) artifacts.push(` Report: ${result.uiSmoke.reportPath}`); + if (artifacts.length > 0) { + lines.push(''); + lines.push('Artifacts:'); + lines.push(...artifacts); + } + } + + if (result.request.notes.length > 0) { + lines.push(''); + lines.push('Notes (honest scope):'); + for (const note of result.request.notes) lines.push(` - ${note}`); + } + + lines.push(''); + lines.push('Open:'); + for (const step of result.template.openInstructions(result.displayDir)) lines.push(` ${step}`); + + lines.push(''); + lines.push('Next:'); + if (!result.template.requiresCommands) lines.push(` - apeironcode test-ui ${result.displayDir} (browser UI smoke)`); + lines.push(' - apeironcode improve "make this more premium"'); + lines.push(' - apeironcode fix'); + lines.push(` - git add ${result.displayDir}`); + return lines.join('\n'); +}; diff --git a/src/appBuilder/appValidation.ts b/src/appBuilder/appValidation.ts new file mode 100644 index 0000000..0a66b76 --- /dev/null +++ b/src/appBuilder/appValidation.ts @@ -0,0 +1,134 @@ +import {promises as fs} from 'node:fs'; +import path from 'node:path'; + +import {redactOutput, truncateOutput} from '../repair/checkRunner.js'; +import {SandboxManager} from '../sandbox/manager.js'; +import {runDesignChecklist} from './designQuality.js'; +import type {AppTemplate, TemplateFile, TemplateVariables} from './types.js'; + +export interface AppValidationStepResult { + name: string; + command?: string; + passed: boolean; + skipped?: boolean; + outputExcerpt: string; +} + +export interface AppValidationResult { + passed: boolean; + steps: AppValidationStepResult[]; +} + +const fileExists = (abs: string): Promise => + fs.access(abs).then(() => true).catch(() => false); + +const linkedRefs = (html: string): string[] => { + const refs: string[] = []; + const re = /(?:href|src)\s*=\s*["']([^"']+)["']/giu; + let match: RegExpExecArray | null; + while ((match = re.exec(html)) !== null) { + const ref = match[1]!; + if (/^(?:https?:)?\/\//u.test(ref) || ref.startsWith('#') || ref.startsWith('data:') || ref.startsWith('/')) continue; + refs.push(ref.replace(/^\.\//u, '')); + } + return refs; +}; + +export interface ValidateAppOptions { + /** Run command steps (npm build/typecheck). Only in temp/approved workspaces. */ + runCommands?: boolean; + signal?: AbortSignal; +} + +/** + * Validate a generated app (Phase 20C, Task E). Static checks (files exist, + * linked assets, `node --check`, design checklist) always run and need no + * network. Command steps (Vite build/typecheck) only run when `runCommands` is + * set; otherwise they are reported as skipped (not failed). Never fakes green. + */ +export const validateApp = async ( + appDir: string, + template: AppTemplate, + vars: TemplateVariables, + files: TemplateFile[], + options: ValidateAppOptions = {}, +): Promise => { + const steps: AppValidationStepResult[] = []; + + for (const step of template.validation(vars)) { + if (step.kind === 'files-exist') { + const missing: string[] = []; + for (const file of files) { + if (!(await fileExists(path.join(appDir, file.path)))) missing.push(file.path); + } + steps.push({name: step.name, passed: missing.length === 0, outputExcerpt: missing.length ? `missing: ${missing.join(', ')}` : `${files.length} file(s)`}); + } else if (step.kind === 'linked-assets') { + const entry = step.entry ?? 'index.html'; + const html = await fs.readFile(path.join(appDir, entry), 'utf8').catch(() => ''); + const refs = linkedRefs(html); + const missing: string[] = []; + for (const ref of refs) { + if (!(await fileExists(path.join(appDir, ref)))) missing.push(ref); + } + steps.push({name: step.name, passed: missing.length === 0, outputExcerpt: missing.length ? `missing: ${missing.join(', ')}` : `${refs.length} reference(s)`}); + } else if (step.kind === 'node-check') { + const file = step.file ?? 'app.js'; + const result = await runNodeCheck(appDir, file, options.signal); + steps.push({name: step.name, passed: result.ok, outputExcerpt: result.detail}); + } else if (step.kind === 'design-checklist') { + const checklist = runDesignChecklist(files, step.entry ?? 'index.html'); + const failed = checklist.checks.filter((c) => !c.ok).map((c) => c.name); + steps.push({name: step.name, passed: checklist.ok, outputExcerpt: checklist.ok ? 'all design checks pass' : `failed: ${failed.join(', ')}`}); + } else if (step.kind === 'command') { + if (!options.runCommands || !step.command) { + steps.push({name: step.name, command: step.command, passed: true, skipped: true, outputExcerpt: 'skipped (run with install/approval)'}); + continue; + } + const result = await runCommandStep(appDir, step.command, options.signal); + steps.push({name: step.name, command: step.command, passed: result.ok, outputExcerpt: result.detail}); + } + } + + // Skipped command steps do not fail the overall result. + const passed = steps.every((step) => step.passed || step.skipped); + return {passed, steps}; +}; + +const runNodeCheck = async ( + cwd: string, + file: string, + signal?: AbortSignal, +): Promise<{ok: boolean; detail: string}> => { + const manager = new SandboxManager({workspace: cwd}); + try { + const result = await manager.executeCommand(`node --check ${JSON.stringify(file)}`, {cwd, timeout: 15_000, signal, workspace: cwd}); + return result.ok + ? {ok: true, detail: 'syntax OK'} + : {ok: false, detail: redactOutput(truncateOutput(result.stderr || result.stdout || 'syntax error', 400)).split('\n')[0] ?? 'syntax error'}; + } catch (error) { + return {ok: false, detail: redactOutput(truncateOutput(String(error), 400))}; + } finally { + await manager.dispose(); + } +}; + +const runCommandStep = async ( + cwd: string, + command: string, + signal?: AbortSignal, +): Promise<{ok: boolean; detail: string}> => { + const manager = new SandboxManager({workspace: cwd}); + try { + const result = await manager.executeCommand(command, {cwd, timeout: 300_000, signal, workspace: cwd}); + return {ok: result.exitCode === 0, detail: redactOutput(truncateOutput([result.stdout, result.stderr, result.warning].filter(Boolean).join('\n'), 600)).split('\n').slice(-3).join(' ')}; + } catch (error) { + return {ok: false, detail: redactOutput(truncateOutput(String(error), 400))}; + } finally { + await manager.dispose(); + } +}; + +export const formatValidation = (result: AppValidationResult): string => + result.steps + .map((step) => ` ${step.skipped ? '·' : step.passed ? '✓' : '✗'} ${step.name}${step.skipped ? ' (skipped)' : ''} — ${step.outputExcerpt}`) + .join('\n'); diff --git a/src/appBuilder/designQuality.ts b/src/appBuilder/designQuality.ts new file mode 100644 index 0000000..a408e05 --- /dev/null +++ b/src/appBuilder/designQuality.ts @@ -0,0 +1,64 @@ +import type {TemplateFile} from './types.js'; + +export interface DesignCheck { + name: string; + ok: boolean; + detail: string; +} + +const findFile = (files: TemplateFile[], rel: string): TemplateFile | undefined => + files.find((file) => file.path === rel || file.path === rel.replace(/^\.\//u, '')); + +const linkedRefs = (html: string): string[] => { + const refs: string[] = []; + const re = /(?:href|src)\s*=\s*["']([^"']+)["']/giu; + let match: RegExpExecArray | null; + while ((match = re.exec(html)) !== null) { + const ref = match[1]!; + if (/^(?:https?:)?\/\//u.test(ref) || ref.startsWith('#') || ref.startsWith('data:') || ref.startsWith('/')) continue; + refs.push(ref.replace(/^\.\//u, '')); + } + return refs; +}; + +/** + * Run the deterministic static design checklist over rendered files + * (Phase 20C, Task D). No browser/network: heuristics on the HTML/CSS source. + * Generated apps should pass every relevant check. + */ +export const runDesignChecklist = (files: TemplateFile[], entry: string): {ok: boolean; checks: DesignCheck[]} => { + const checks: DesignCheck[] = []; + const add = (name: string, ok: boolean, detail: string): void => { + checks.push({name, ok, detail}); + }; + + const html = findFile(files, entry)?.content ?? ''; + const css = files.filter((f) => f.path.endsWith('.css')).map((f) => f.content).join('\n'); + + add('entry HTML present', Boolean(html), html ? entry : `missing ${entry}`); + add('has ', /<title>[^<]+<\/title>/iu.test(html), 'document title'); + add('has viewport meta', /<meta[^>]+name=["']viewport["']/iu.test(html), 'responsive viewport'); + + const refs = linkedRefs(html); + const missing = refs.filter((ref) => !findFile(files, ref)); + add('linked assets exist', missing.length === 0, missing.length ? `missing: ${missing.join(', ')}` : `${refs.length} reference(s)`); + + const hasButtons = /<button|class=["'][^"']*\bbtn\b/iu.test(html); + add('buttons have visible states', !hasButtons || /:hover|:focus(?:-visible)?/u.test(css), 'hover/focus states'); + + const inputs = (html.match(/<input\b/giu) ?? []).length; + const labels = (html.match(/<label\b/giu) ?? []).length; + add('form inputs have labels', inputs === 0 || labels >= inputs, `${labels} label(s) for ${inputs} input(s)`); + + const mentionsList = /<ul|<table|class=["'][^"']*\blist\b/iu.test(html); + add('empty state present where relevant', !mentionsList || /empty-state/u.test(html), 'empty state'); + + add('no obvious horizontal overflow', /box-sizing\s*:\s*border-box/u.test(css), 'box-sizing border-box'); + add('responsive rules present', /@media/u.test(css), 'media query'); + + const ok = checks.every((check) => check.ok); + return {ok, checks}; +}; + +export const formatDesignChecks = (checks: DesignCheck[]): string => + checks.map((check) => ` ${check.ok ? '✓' : '✗'} ${check.name} — ${check.detail}`).join('\n'); diff --git a/src/appBuilder/requestParser.ts b/src/appBuilder/requestParser.ts new file mode 100644 index 0000000..f8d7eb0 --- /dev/null +++ b/src/appBuilder/requestParser.ts @@ -0,0 +1,140 @@ +import type {AppKind, AppRequest, AppStack, AppStyle} from './types.js'; + +const STOP_WORDS = new Set([ + 'a', 'an', 'the', 'app', 'application', 'for', 'with', 'and', 'to', 'of', 'create', 'build', + 'make', 'me', 'my', 'please', 'simple', 'small', +]); + +/** Convert free text to a directory-safe slug. */ +export const slugify = (text: string): string => + text + .toLowerCase() + .replace(/[^a-z0-9]+/gu, '-') + .replace(/^-+|-+$/gu, '') + .slice(0, 60) || 'app'; + +/** Build a Title Case display name from the idea, dropping filler words. */ +const toAppName = (idea: string): string => { + const words = idea + .replace(/[^a-zA-Z0-9\s]/gu, ' ') + .split(/\s+/u) + .filter((word) => word && !STOP_WORDS.has(word.toLowerCase())); + const picked = (words.length > 0 ? words : ['App']).slice(0, 5); + return picked.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' '); +}; + +const KIND_PATTERNS: Array<{kind: AppKind; re: RegExp}> = [ + {kind: 'dashboard', re: /\b(dashboard|crm|admin|analytics|console|panel|metrics)\b/iu}, + {kind: 'landing', re: /\b(landing|startup|portfolio|marketing|homepage|product page|waitlist)\b/iu}, + {kind: 'todo', re: /\b(todo|to-do|task|tasks|habit|checklist|notes?|reminder)\b/iu}, +]; + +const STYLE_PATTERNS: Array<{style: AppStyle; re: RegExp}> = [ + {style: 'premium-saas', re: /\b(premium|saas|professional|polished|sleek)\b/iu}, + {style: 'apple-like', re: /\b(apple|ios|iphone|cupertino)\b/iu}, + {style: 'dark', re: /\b(dark|midnight|black)\b/iu}, +]; + +const FEATURE_PATTERNS: Array<{feature: string; re: RegExp; mock?: boolean}> = [ + {feature: 'tasks', re: /\b(task|todo|checklist|habit)\b/iu}, + {feature: 'dashboard', re: /\b(dashboard|analytics|metrics|charts?)\b/iu}, + {feature: 'login-mock', re: /\b(login|sign\s?in|auth|account|sign\s?up)\b/iu, mock: true}, + {feature: 'sample-data', re: /\b(crm|customers?|contacts?|users?|orders?|leads?)\b/iu}, + {feature: 'billing-mock', re: /\b(billing|payment|stripe|subscription|pricing)\b/iu, mock: true}, +]; + +export interface ParseAppRequestOptions { + stack?: string; + style?: string; + /** Whether next-minimal is actually available; downgrade if not. */ + nextAvailable?: boolean; +} + +const detectKind = (idea: string): AppKind => { + for (const pattern of KIND_PATTERNS) if (pattern.re.test(idea)) return pattern.kind; + return 'custom'; +}; + +const normalizeStack = (raw: string | undefined): AppStack | undefined => { + if (!raw) return undefined; + const value = raw.toLowerCase(); + if (value === 'static' || value === 'dashboard' || value === 'dashboard-static') return 'static'; + if (value === 'vite' || value === 'vite-react' || value === 'react') return 'vite-react'; + if (value === 'next' || value === 'next-minimal' || value === 'nextjs') return 'next-minimal'; + return undefined; +}; + +const detectStackFromIdea = (idea: string): AppStack | undefined => { + if (/\bnext\.?js\b/iu.test(idea)) return 'next-minimal'; + if (/\b(react|vite)\b/iu.test(idea)) return 'vite-react'; + return undefined; +}; + +/** + * Parse a free-text app idea into a deterministic, honest AppRequest + * (Phase 20C, Task C). Auth/database/billing become mock/local UI features — + * never claimed as real. Unsupported Next.js downgrades to Vite when next is + * unavailable. + */ +export const parseAppRequest = (idea: string, options: ParseAppRequestOptions = {}): AppRequest => { + const trimmed = idea.trim() || 'app'; + const kind = detectKind(trimmed); + const notes: string[] = []; + + let style: AppStyle = 'minimal'; + if (options.style) { + style = (normalizeStyle(options.style) ?? 'premium-saas'); + } else { + for (const pattern of STYLE_PATTERNS) { + if (pattern.re.test(trimmed)) { + style = pattern.style; + break; + } + } + } + + const features: string[] = []; + let downgraded = false; + for (const pattern of FEATURE_PATTERNS) { + if (pattern.re.test(trimmed) && !features.includes(pattern.feature)) { + features.push(pattern.feature); + if (pattern.mock) { + downgraded = true; + notes.push(`${pattern.feature.replace('-mock', '')} is a mock/local UI only (no real backend in this phase)`); + } + } + } + + // Stack resolution: explicit flag → idea hint → kind default. + let stack = normalizeStack(options.stack) ?? detectStackFromIdea(trimmed); + if (!stack) { + stack = kind === 'dashboard' || kind === 'landing' || kind === 'todo' || kind === 'custom' ? 'static' : 'static'; + } + if (stack === 'next-minimal' && !options.nextAvailable) { + stack = 'vite-react'; + downgraded = true; + notes.push('Next.js is not available in this phase yet; using Vite + React instead'); + } + + const appName = toAppName(trimmed); + return { + appName, + appSlug: slugify(appName), + description: trimmed, + kind, + stack, + style, + features, + downgraded, + notes, + }; +}; + +const normalizeStyle = (raw: string): AppStyle | undefined => { + const value = raw.toLowerCase(); + if (value === 'premium-saas' || value === 'premium' || value === 'saas') return 'premium-saas'; + if (value === 'apple-like' || value === 'apple') return 'apple-like'; + if (value === 'dark') return 'dark'; + if (value === 'minimal') return 'minimal'; + return undefined; +}; diff --git a/src/appBuilder/templateRegistry.ts b/src/appBuilder/templateRegistry.ts new file mode 100644 index 0000000..04900d1 --- /dev/null +++ b/src/appBuilder/templateRegistry.ts @@ -0,0 +1,39 @@ +import {staticTodoTemplate} from './templates/staticTodo.js'; +import {staticLandingTemplate} from './templates/staticLanding.js'; +import {dashboardStaticTemplate} from './templates/dashboardStatic.js'; +import {viteReactTemplate} from './templates/viteReact.js'; +import type {AppRequest, AppTemplate} from './types.js'; + +export const APP_TEMPLATES: AppTemplate[] = [ + staticTodoTemplate, + staticLandingTemplate, + dashboardStaticTemplate, + viteReactTemplate, +]; + +export const getTemplateById = (id: string): AppTemplate | undefined => + APP_TEMPLATES.find((template) => template.id === id); + +/** + * Select a template for a parsed request (Phase 20C, Task C). Vite requests use + * the vite-react template; otherwise the kind maps to a static template, with a + * sensible fallback for `custom`. + */ +export const selectTemplate = (request: AppRequest): AppTemplate => { + if (request.stack === 'vite-react' || request.stack === 'next-minimal') { + return viteReactTemplate; + } + switch (request.kind) { + case 'todo': + return staticTodoTemplate; + case 'dashboard': + return dashboardStaticTemplate; + case 'landing': + return staticLandingTemplate; + case 'custom': + default: + // A generic "app" with no clear kind gets the most universally useful + // static starting point. + return staticTodoTemplate; + } +}; diff --git a/src/appBuilder/templateRenderer.ts b/src/appBuilder/templateRenderer.ts new file mode 100644 index 0000000..5930063 --- /dev/null +++ b/src/appBuilder/templateRenderer.ts @@ -0,0 +1,74 @@ +import {promises as fs} from 'node:fs'; +import path from 'node:path'; + +import type {AppTemplate, TemplateFile, TemplateVariables} from './types.js'; + +/** A rendered file path is unsafe if it is absolute, escapes the app dir, or hits .git/.env. */ +export const isUnsafeTemplatePath = (relPath: string): boolean => { + if (!relPath.trim() || path.isAbsolute(relPath) || relPath.startsWith('~')) return true; + const normalized = path.normalize(relPath); + if (normalized.startsWith('..') || normalized.split(/[\\/]/u).includes('..')) return true; + if (/(?:^|[\\/])\.git(?:[\\/]|$)/u.test(normalized)) return true; + if (/(?:^|[\\/])\.env(?:\.|$)/u.test(normalized)) return true; + return false; +}; + +export interface RenderResult { + files: TemplateFile[]; + errors: string[]; +} + +/** + * Render a template's files for the given variables and validate every output + * path is safe (relative, inside the app dir, never `.git`/`.env`). + */ +export const renderTemplate = (template: AppTemplate, vars: TemplateVariables): RenderResult => { + const files = template.render(vars); + const errors: string[] = []; + for (const file of files) { + if (isUnsafeTemplatePath(file.path)) errors.push(`unsafe template path: ${file.path}`); + } + return {files, errors}; +}; + +export interface WriteResult { + written: string[]; + errors: string[]; +} + +/** + * Write rendered files into `appDir`. Refuses to write outside the directory and + * never overwrites existing files (callers handle overwrite policy upstream). + */ +export const writeTemplateFiles = async ( + appDir: string, + files: TemplateFile[], + options: {overwrite?: boolean} = {}, +): Promise<WriteResult> => { + const written: string[] = []; + const errors: string[] = []; + const root = path.resolve(appDir); + + for (const file of files) { + if (isUnsafeTemplatePath(file.path)) { + errors.push(`refused unsafe path: ${file.path}`); + continue; + } + const target = path.resolve(root, file.path); + if (target !== root && !target.startsWith(root + path.sep)) { + errors.push(`refused path outside app dir: ${file.path}`); + continue; + } + if (!options.overwrite) { + const exists = await fs.access(target).then(() => true).catch(() => false); + if (exists) { + errors.push(`refused to overwrite existing file: ${file.path}`); + continue; + } + } + await fs.mkdir(path.dirname(target), {recursive: true}); + await fs.writeFile(target, file.content, 'utf8'); + written.push(file.path); + } + return {written, errors}; +}; diff --git a/src/appBuilder/templates/dashboardStatic.ts b/src/appBuilder/templates/dashboardStatic.ts new file mode 100644 index 0000000..6c549f3 --- /dev/null +++ b/src/appBuilder/templates/dashboardStatic.ts @@ -0,0 +1,125 @@ +import type {AppTemplate, TemplateFile, TemplateVariables} from '../types.js'; +import {designSystemCss, htmlHead, readme} from './shared.js'; + +const indexHtml = (vars: TemplateVariables): string => `${htmlHead(vars.appName)} +<body> + <div class="layout"> + <aside class="sidebar"> + <div class="brand">${vars.appName}</div> + <nav aria-label="Sidebar"> + <a href="#" class="active">Overview</a> + <a href="#">Customers</a> + <a href="#">Reports</a> + <a href="#">Settings</a> + </nav> + </aside> + <main class="main"> + <header class="topbar"> + <h1>Overview</h1> + <button class="btn" id="refresh">Refresh data</button> + </header> + <section class="stats" id="stats" aria-label="Key metrics"></section> + <section class="card table-card"> + <h2>Recent customers</h2> + <table class="table"> + <thead><tr><th>Name</th><th>Plan</th><th>Status</th><th>MRR</th></tr></thead> + <tbody id="rows"></tbody> + </table> + <p id="empty" class="empty-state" hidden>No data to show.</p> + </section> + </main> + </div> + <script src="app.js"></script> +</body> +</html> +`; + +const stylesCss = (vars: TemplateVariables): string => `${designSystemCss(vars.style)} +.layout { display: grid; grid-template-columns: 16rem 1fr; min-height: 100vh; } +.sidebar { background: var(--surface); border-right: 1px solid var(--border); padding: var(--space-6); } +.sidebar .brand { font-weight: 700; margin-bottom: var(--space-6); } +.sidebar nav { display: grid; gap: var(--space-1); } +.sidebar nav a { color: var(--muted); text-decoration: none; padding: var(--space-2) var(--space-3); border-radius: var(--radius); } +.sidebar nav a.active, .sidebar nav a:hover { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--fg); } +.main { padding: var(--space-8); } +.topbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-6); } +.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: var(--space-4); margin-bottom: var(--space-6); } +.stat { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-6); } +.stat .value { font-size: 1.75rem; font-weight: 700; } +.stat .label { color: var(--muted); font-size: .9rem; } +.table { width: 100%; border-collapse: collapse; } +.table th, .table td { text-align: left; padding: var(--space-3); border-bottom: 1px solid var(--border); } +.table th { color: var(--muted); font-weight: 600; font-size: .85rem; } +.badge { padding: 2px 8px; border-radius: 999px; font-size: .8rem; background: color-mix(in srgb, var(--accent) 18%, transparent); } +@media (max-width: 720px) { .layout { grid-template-columns: 1fr; } .sidebar { display: none; } } +`; + +const appJs = (): string => `'use strict'; +(function () { + // Local sample data (frontend-only, no backend). + var customers = [ + {name: 'Acme Co', plan: 'Pro', status: 'Active', mrr: 240}, + {name: 'Globex', plan: 'Team', status: 'Active', mrr: 120}, + {name: 'Initech', plan: 'Starter', status: 'Trial', mrr: 0}, + {name: 'Umbrella', plan: 'Pro', status: 'Active', mrr: 240}, + {name: 'Hooli', plan: 'Team', status: 'Past due', mrr: 120} + ]; + + function money(n) { return '$' + n.toLocaleString(); } + + function renderStats() { + var mrr = customers.reduce(function (sum, c) { return sum + c.mrr; }, 0); + var active = customers.filter(function (c) { return c.status === 'Active'; }).length; + var stats = [ + {label: 'Customers', value: String(customers.length)}, + {label: 'Active', value: String(active)}, + {label: 'MRR', value: money(mrr)}, + {label: 'Avg / customer', value: money(Math.round(mrr / customers.length))} + ]; + var el = document.getElementById('stats'); + el.innerHTML = stats.map(function (s) { + return '<div class="stat"><div class="value">' + s.value + '</div><div class="label">' + s.label + '</div></div>'; + }).join(''); + } + + function renderRows() { + var rows = document.getElementById('rows'); + var empty = document.getElementById('empty'); + empty.hidden = customers.length > 0; + rows.innerHTML = customers.map(function (c) { + return '<tr><td>' + c.name + '</td><td>' + c.plan + '</td><td><span class="badge">' + c.status + '</span></td><td>' + money(c.mrr) + '</td></tr>'; + }).join(''); + } + + document.getElementById('refresh').addEventListener('click', function () { + customers = customers.slice().sort(function () { return Math.random() - 0.5; }); + renderStats(); + renderRows(); + }); + + renderStats(); + renderRows(); +})(); +`; + +export const dashboardStaticTemplate: AppTemplate = { + id: 'dashboard-static', + name: 'Static Dashboard', + stack: 'static', + kind: 'dashboard', + description: 'A responsive, frontend-only dashboard UI with local sample data.', + requiresCommands: false, + render: (vars): TemplateFile[] => [ + {path: 'index.html', content: indexHtml(vars)}, + {path: 'styles.css', content: stylesCss(vars)}, + {path: 'app.js', content: appJs()}, + {path: 'README.md', content: readme(vars.appName, vars.description, ['open index.html'])}, + ], + validation: () => [ + {kind: 'files-exist', name: 'files created'}, + {kind: 'linked-assets', name: 'linked assets exist', entry: 'index.html'}, + {kind: 'node-check', name: 'JavaScript syntax', file: 'app.js'}, + {kind: 'design-checklist', name: 'design checklist', entry: 'index.html'}, + ], + openInstructions: (slug) => [`open ${slug}/index.html`], +}; diff --git a/src/appBuilder/templates/shared.ts b/src/appBuilder/templates/shared.ts new file mode 100644 index 0000000..503aba6 --- /dev/null +++ b/src/appBuilder/templates/shared.ts @@ -0,0 +1,120 @@ +import type {AppStyle} from '../types.js'; + +export interface Palette { + bg: string; + surface: string; + fg: string; + muted: string; + accent: string; + accentInk: string; + border: string; +} + +/** Deterministic palette per style. Premium-feeling, accessible contrast. */ +export const paletteFor = (style: AppStyle): Palette => { + switch (style) { + case 'apple-like': + return {bg: '#f5f5f7', surface: '#ffffff', fg: '#1d1d1f', muted: '#6e6e73', accent: '#0071e3', accentInk: '#ffffff', border: '#d2d2d7'}; + case 'dark': + return {bg: '#0b0d12', surface: '#151922', fg: '#e6e9ef', muted: '#9aa4b2', accent: '#8b5cf6', accentInk: '#0e0717', border: '#232a36'}; + case 'minimal': + return {bg: '#ffffff', surface: '#ffffff', fg: '#111827', muted: '#6b7280', accent: '#111827', accentInk: '#ffffff', border: '#e5e7eb'}; + case 'premium-saas': + default: + return {bg: '#0b0d12', surface: '#141821', fg: '#eef1f6', muted: '#9aa4b2', accent: '#14b8a6', accentInk: '#02110e', border: '#222a36'}; + } +}; + +/** + * Shared design-system CSS: tokens, reset, spacing/typography scale, accessible + * focus states, buttons, cards, and responsive helpers. Used by all static + * templates so generated apps don't look like student projects. + */ +export const designSystemCss = (style: AppStyle): string => { + const p = paletteFor(style); + return `:root { + --bg: ${p.bg}; + --surface: ${p.surface}; + --fg: ${p.fg}; + --muted: ${p.muted}; + --accent: ${p.accent}; + --accent-ink: ${p.accentInk}; + --border: ${p.border}; + --radius: 0.75rem; + --space-1: 0.25rem; --space-2: 0.5rem; --space-3: 0.75rem; --space-4: 1rem; + --space-6: 1.5rem; --space-8: 2rem; --space-12: 3rem; + --shadow: 0 10px 30px rgba(0,0,0,.18); + color-scheme: light dark; +} +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { + font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + background: var(--bg); + color: var(--fg); + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} +h1, h2, h3 { line-height: 1.15; letter-spacing: -0.01em; margin: 0 0 var(--space-3); } +h1 { font-size: clamp(1.6rem, 4vw, 2.5rem); } +h2 { font-size: clamp(1.25rem, 3vw, 1.6rem); } +p { color: var(--muted); margin: 0 0 var(--space-4); } +a { color: var(--accent); } +.container { width: min(64rem, 100% - 2rem); margin-inline: auto; } +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); + padding: var(--space-3) var(--space-6); border: 0; border-radius: var(--radius); + background: var(--accent); color: var(--accent-ink); font-weight: 600; cursor: pointer; + transition: transform .08s ease, opacity .15s ease; +} +.btn:hover { transform: translateY(-1px); } +.btn:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 50%, transparent); outline-offset: 2px; } +.btn.secondary { background: transparent; color: var(--fg); border: 1px solid var(--border); } +.card { + background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); + padding: var(--space-6); box-shadow: var(--shadow); +} +.input { + width: 100%; padding: var(--space-3) var(--space-4); border-radius: var(--radius); + border: 1px solid var(--border); background: var(--surface); color: var(--fg); +} +.input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } +.muted { color: var(--muted); } +.empty-state { text-align: center; color: var(--muted); padding: var(--space-8); } +.visually-hidden { + position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; + overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; +} +@media (max-width: 390px) { + .container { width: calc(100% - 1.5rem); } + .btn { width: 100%; } +} +`; +}; + +export const htmlHead = (title: string, extraLinks = ''): string => + `<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>${title} + +${extraLinks}`; + +export const readme = (appName: string, description: string, openSteps: string[]): string => + `# ${appName} + +${description} + +Generated by ApeironCode (template-based, no API key required). + +## Open + +${openSteps.map((s) => `- \`${s}\``).join('\n')} + +## Customize + +- \`apeironcode improve "make this more premium"\` +- \`apeironcode fix\` +`; diff --git a/src/appBuilder/templates/staticLanding.ts b/src/appBuilder/templates/staticLanding.ts new file mode 100644 index 0000000..b1b40f1 --- /dev/null +++ b/src/appBuilder/templates/staticLanding.ts @@ -0,0 +1,102 @@ +import type {AppTemplate, TemplateFile, TemplateVariables} from '../types.js'; +import {designSystemCss, htmlHead, readme} from './shared.js'; + +const indexHtml = (vars: TemplateVariables): string => `${htmlHead(vars.appName)} + + +
+
+

${vars.appName}

+

${vars.description}

+ +
+
+

Fast

Loads instantly with zero bloat.

+

Polished

Accessible, responsive, premium by default.

+

Local-first

Your data stays yours.

+
+
+
+

Ready to start?

+

Join the waitlist and we'll be in touch.

+
+ + + +
+

+
+
+
+

© ${new Date().getFullYear()} ${vars.appName}. Built with ApeironCode.

+ + + +`; + +const stylesCss = (vars: TemplateVariables): string => `${designSystemCss(vars.style)} +.nav { position: sticky; top: 0; backdrop-filter: blur(8px); background: color-mix(in srgb, var(--bg) 85%, transparent); border-bottom: 1px solid var(--border); } +.nav-inner { display: flex; align-items: center; justify-content: space-between; padding-block: var(--space-3); } +.brand { font-weight: 700; } +.nav nav { display: flex; align-items: center; gap: var(--space-4); } +.nav nav a:not(.btn) { color: var(--muted); text-decoration: none; } +.hero { text-align: center; padding-block: var(--space-12); } +.lead { font-size: 1.125rem; max-width: 40rem; margin-inline: auto; } +.hero-actions { display: flex; gap: var(--space-3); justify-content: center; flex-wrap: wrap; } +.features { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: var(--space-4); padding-block: var(--space-8); } +.cta { padding-block: var(--space-12); } +.cta-card { text-align: center; max-width: 36rem; margin-inline: auto; } +.waitlist { display: flex; gap: var(--space-2); margin-top: var(--space-4); } +.waitlist .input { flex: 1; } +.footer { padding-block: var(--space-8); text-align: center; } +@media (max-width: 390px) { .waitlist { flex-direction: column; } } +`; + +const appJs = (): string => `'use strict'; +(function () { + var form = document.getElementById('waitlist'); + var email = document.getElementById('email'); + var status = document.getElementById('waitlist-status'); + form.addEventListener('submit', function (event) { + event.preventDefault(); + var value = email.value.trim(); + if (!value) return; + status.textContent = 'Thanks! ' + value + ' is on the waitlist (demo only).'; + email.value = ''; + }); +})(); +`; + +export const staticLandingTemplate: AppTemplate = { + id: 'static-landing', + name: 'Static Landing Page', + stack: 'static', + kind: 'landing', + description: 'A premium static landing page with hero, features, and a mock waitlist.', + requiresCommands: false, + render: (vars): TemplateFile[] => [ + {path: 'index.html', content: indexHtml(vars)}, + {path: 'styles.css', content: stylesCss(vars)}, + {path: 'app.js', content: appJs()}, + {path: 'README.md', content: readme(vars.appName, vars.description, ['open index.html'])}, + ], + validation: () => [ + {kind: 'files-exist', name: 'files created'}, + {kind: 'linked-assets', name: 'linked assets exist', entry: 'index.html'}, + {kind: 'node-check', name: 'JavaScript syntax', file: 'app.js'}, + {kind: 'design-checklist', name: 'design checklist', entry: 'index.html'}, + ], + openInstructions: (slug) => [`open ${slug}/index.html`], +}; diff --git a/src/appBuilder/templates/staticTodo.ts b/src/appBuilder/templates/staticTodo.ts new file mode 100644 index 0000000..cfc580a --- /dev/null +++ b/src/appBuilder/templates/staticTodo.ts @@ -0,0 +1,104 @@ +import type {AppTemplate, TemplateFile, TemplateVariables} from '../types.js'; +import {designSystemCss, htmlHead, readme} from './shared.js'; + +const indexHtml = (vars: TemplateVariables): string => `${htmlHead(vars.appName)} + +
+
+

${vars.appName}

+

${vars.description}

+
+
+ + + +
+
+
    +

    No tasks yet. Add your first one above.

    +
    +
    + + + +`; + +const stylesCss = (vars: TemplateVariables): string => `${designSystemCss(vars.style)} +.app { padding-block: var(--space-12); max-width: 34rem; } +.app-header { margin-bottom: var(--space-6); } +.add-form { display: flex; gap: var(--space-2); margin-bottom: var(--space-6); } +.add-form .input { flex: 1; } +.list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); } +.list li { + display: flex; align-items: center; gap: var(--space-3); + padding: var(--space-3) var(--space-4); background: var(--surface); + border: 1px solid var(--border); border-radius: var(--radius); +} +.list li.done .label { text-decoration: line-through; color: var(--muted); } +.list .label { flex: 1; cursor: pointer; } +.list .remove { background: transparent; border: 0; color: var(--muted); cursor: pointer; font-size: 1.1rem; } +.list .remove:hover { color: var(--fg); } +`; + +const appJs = (): string => `'use strict'; +(function () { + var form = document.getElementById('add-form'); + var input = document.getElementById('new-todo'); + var list = document.getElementById('list'); + var empty = document.getElementById('empty'); + + function refreshEmpty() { + empty.style.display = list.children.length === 0 ? 'block' : 'none'; + } + + function addTodo(text) { + var li = document.createElement('li'); + var label = document.createElement('span'); + label.className = 'label'; + label.textContent = text; + label.addEventListener('click', function () { li.classList.toggle('done'); }); + var remove = document.createElement('button'); + remove.className = 'remove'; + remove.setAttribute('aria-label', 'Remove task'); + remove.textContent = '\\u00d7'; + remove.addEventListener('click', function () { li.remove(); refreshEmpty(); }); + li.appendChild(label); + li.appendChild(remove); + list.appendChild(li); + refreshEmpty(); + } + + form.addEventListener('submit', function (event) { + event.preventDefault(); + var text = input.value.trim(); + if (!text) return; + addTodo(text); + input.value = ''; + input.focus(); + }); + + refreshEmpty(); +})(); +`; + +export const staticTodoTemplate: AppTemplate = { + id: 'static-todo', + name: 'Static Todo App', + stack: 'static', + kind: 'todo', + description: 'A polished static HTML/CSS/JS todo app. No install required.', + requiresCommands: false, + render: (vars): TemplateFile[] => [ + {path: 'index.html', content: indexHtml(vars)}, + {path: 'styles.css', content: stylesCss(vars)}, + {path: 'app.js', content: appJs()}, + {path: 'README.md', content: readme(vars.appName, vars.description, ['open index.html'])}, + ], + validation: () => [ + {kind: 'files-exist', name: 'files created'}, + {kind: 'linked-assets', name: 'linked assets exist', entry: 'index.html'}, + {kind: 'node-check', name: 'JavaScript syntax', file: 'app.js'}, + {kind: 'design-checklist', name: 'design checklist', entry: 'index.html'}, + ], + openInstructions: (slug) => [`open ${slug}/index.html`], +}; diff --git a/src/appBuilder/templates/viteReact.ts b/src/appBuilder/templates/viteReact.ts new file mode 100644 index 0000000..0b6c2d0 --- /dev/null +++ b/src/appBuilder/templates/viteReact.ts @@ -0,0 +1,136 @@ +import type {AppTemplate, TemplateFile, TemplateVariables} from '../types.js'; +import {designSystemCss, readme} from './shared.js'; + +const packageJson = (vars: TemplateVariables): string => `${JSON.stringify({ + name: vars.appSlug, + private: true, + version: '0.1.0', + type: 'module', + scripts: { + dev: 'vite', + build: 'tsc -b && vite build', + typecheck: 'tsc --noEmit', + preview: 'vite preview', + }, + dependencies: {react: '^18.3.1', 'react-dom': '^18.3.1'}, + devDependencies: { + '@types/react': '^18.3.3', + '@types/react-dom': '^18.3.0', + '@vitejs/plugin-react': '^4.3.1', + typescript: '^5.5.0', + vite: '^5.4.0', + }, +}, null, 2)}\n`; + +const tsconfig = (): string => `${JSON.stringify({ + compilerOptions: { + target: 'ES2020', useDefineForClassFields: true, lib: ['ES2020', 'DOM', 'DOM.Iterable'], + module: 'ESNext', skipLibCheck: true, moduleResolution: 'bundler', resolveJsonModule: true, + isolatedModules: true, noEmit: true, jsx: 'react-jsx', strict: true, + }, + include: ['src'], +}, null, 2)}\n`; + +const viteConfig = (): string => `import {defineConfig} from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({plugins: [react()]}); +`; + +const indexHtml = (vars: TemplateVariables): string => ` + + + + + ${vars.appName} + + +
    + + + +`; + +const mainTsx = (): string => `import React from 'react'; +import {createRoot} from 'react-dom/client'; +import {App} from './App.js'; +import './styles.css'; + +createRoot(document.getElementById('root')!).render( + + + , +); +`; + +const appTsx = (vars: TemplateVariables): string => `import {useState} from 'react'; + +interface Todo { + id: number; + text: string; + done: boolean; +} + +export function App(): JSX.Element { + const [todos, setTodos] = useState([]); + const [text, setText] = useState(''); + + const add = (event: React.FormEvent): void => { + event.preventDefault(); + const value = text.trim(); + if (!value) return; + setTodos((prev) => [...prev, {id: Date.now(), text: value, done: false}]); + setText(''); + }; + + return ( +
    +

    ${vars.appName}

    +

    ${vars.description}

    +
    + + setText(e.target.value)} placeholder="Add a task…" /> + +
    + {todos.length === 0 ? ( +

    No tasks yet. Add your first one above.

    + ) : ( +
      + {todos.map((todo) => ( +
    • + setTodos((p) => p.map((t) => (t.id === todo.id ? {...t, done: !t.done} : t)))}> + {todo.text} + +
    • + ))} +
    + )} +
    + ); +} +`; + +export const viteReactTemplate: AppTemplate = { + id: 'vite-react', + name: 'Vite + React (TypeScript)', + stack: 'vite-react', + kind: 'custom', + description: 'A React + TypeScript app scaffolded with Vite. Requires npm install to run.', + requiresCommands: true, + render: (vars): TemplateFile[] => [ + {path: 'package.json', content: packageJson(vars)}, + {path: 'tsconfig.json', content: tsconfig()}, + {path: 'vite.config.ts', content: viteConfig()}, + {path: 'index.html', content: indexHtml(vars)}, + {path: 'src/main.tsx', content: mainTsx()}, + {path: 'src/App.tsx', content: appTsx(vars)}, + {path: 'src/styles.css', content: `${designSystemCss(vars.style)}\n.app { padding-block: var(--space-12); max-width: 34rem; }\n.add-form { display: flex; gap: var(--space-2); margin-bottom: var(--space-6); }\n.add-form .input { flex: 1; }\n.list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); }\n.list li { padding: var(--space-3) var(--space-4); background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); }\n.list li.done .label { text-decoration: line-through; color: var(--muted); }\n.list .label { cursor: pointer; }\n`}, + {path: 'README.md', content: readme(vars.appName, vars.description, ['cd ' + vars.appSlug, 'npm install', 'npm run dev'])}, + ], + validation: () => [ + {kind: 'files-exist', name: 'files created'}, + {kind: 'command', name: 'build', command: 'npm run build'}, + {kind: 'command', name: 'typecheck', command: 'npm run typecheck'}, + ], + openInstructions: (slug) => [`cd ${slug}`, 'npm install', 'npm run dev'], +}; diff --git a/src/appBuilder/types.ts b/src/appBuilder/types.ts new file mode 100644 index 0000000..169424a --- /dev/null +++ b/src/appBuilder/types.ts @@ -0,0 +1,70 @@ +/** + * Phase 20C app-builder types. + * + * The app builder is deterministic: a parsed request selects a template, the + * template renders real files, and a validator checks them. No API key or model + * is required for template generation; AI customization is an optional layer. + */ + +export type AppStack = 'static' | 'vite-react' | 'next-minimal'; +export type AppKind = 'todo' | 'dashboard' | 'landing' | 'custom'; +export type AppStyle = 'premium-saas' | 'minimal' | 'dark' | 'apple-like'; + +export interface AppRequest { + /** Display name, e.g. "Premium Todo App". */ + appName: string; + /** Directory-safe slug, e.g. "premium-todo-app". */ + appSlug: string; + description: string; + kind: AppKind; + stack: AppStack; + style: AppStyle; + /** Mock/local UI features detected (e.g. "tasks", "dashboard", "login-mock"). */ + features: string[]; + /** True when the user asked for something this phase can only mock (auth/db). */ + downgraded: boolean; + /** Human-readable notes about downgrades/assumptions (shown honestly in report). */ + notes: string[]; +} + +export interface TemplateFile { + path: string; + content: string; +} + +export interface TemplateVariables { + appName: string; + appSlug: string; + description: string; + style: AppStyle; + features: string[]; +} + +export type ValidationStepKind = 'files-exist' | 'linked-assets' | 'node-check' | 'design-checklist' | 'command'; + +export interface TemplateValidationStep { + kind: ValidationStepKind; + /** For 'command' steps: the script command to run (requires approval/temp). */ + command?: string; + /** For 'node-check': the JS file to syntax-check, relative to the app dir. */ + file?: string; + /** For 'linked-assets'/'design-checklist': the HTML entry file. */ + entry?: string; + name: string; +} + +export interface AppTemplate { + id: string; + name: string; + stack: AppStack; + kind: AppKind; + description: string; + /** Render deterministic files for the given variables. */ + render: (vars: TemplateVariables) => TemplateFile[]; + /** Validation steps run after files are created. */ + validation: (vars: TemplateVariables) => TemplateValidationStep[]; + /** How to open/run the app, shown in the final report. */ + openInstructions: (slug: string) => string[]; + /** True when this stack may require install/build commands (Vite/Next). */ + requiresCommands: boolean; +} diff --git a/src/bridge/connectionFile.ts b/src/bridge/connectionFile.ts index 26d8e92..81f99a8 100644 --- a/src/bridge/connectionFile.ts +++ b/src/bridge/connectionFile.ts @@ -1,6 +1,6 @@ /** * ApeironCode Bridge — Runtime connection file. - * Written when bridge starts so VS Code extension can auto-discover endpoint. + * Written when the bridge starts so a future IDE client can discover it. * Contains endpoint + token fingerprint ONLY — never the full token. */ diff --git a/src/bridge/server.ts b/src/bridge/server.ts index 8a09365..f7226c9 100644 --- a/src/bridge/server.ts +++ b/src/bridge/server.ts @@ -457,7 +457,7 @@ export class BridgeServer { const rawSummary = msg.payload['summary']; const summary = createRunSummaryFromAgentResult( {finalMessage: typeof rawSummary === 'string' ? rawSummary : 'Manual Project Brain update', status: 'completed'}, - {prompt: 'VS Code Project Brain update'}, + {prompt: 'IDE bridge Project Brain update'}, ); const appended = await appendRunSummary(cwd, summary, {approved, enabled: approved}); await connection.send(sanitizeBridgeMessage(createBridgeMessage('brain.update', {appended}, {requestId}))); diff --git a/src/cli/appBuilderHandler.ts b/src/cli/appBuilderHandler.ts new file mode 100644 index 0000000..ff9048c --- /dev/null +++ b/src/cli/appBuilderHandler.ts @@ -0,0 +1,80 @@ +import readline from 'node:readline/promises'; + +import {ConfigStore} from '../config/config.js'; +import {buildApp} from '../appBuilder/appBuilder.js'; +import {formatAppReport} from '../appBuilder/appReport.js'; + +export interface NewCliOptions { + stack?: string; + style?: string; + dir?: string; + yes?: boolean; + dryRun?: boolean; + overwrite?: boolean; + uiSmoke?: boolean; + requireUiSmoke?: boolean; +} + +/** + * `apeironcode new` / `apeironcode build` handler (Phase 20C). Deterministic + * template-based app creation — no API key or model required. Writes are + * approval-gated: auto in bypass/trusted mode or with `--yes`, otherwise a + * single bundled y/N prompt. Non-interactive without `--yes` declines. + */ +export const runNewCommand = async (cwd: string, idea: string | undefined, options: NewCliOptions): Promise => { + const trimmed = (idea ?? '').trim(); + if (!trimmed && !options.dir) { + process.stdout.write( + 'Describe the app to create, e.g.\n' + + ' apeironcode new "premium todo app"\n' + + ' apeironcode new "CRM dashboard" --stack dashboard\n' + + ' apeironcode new "landing page for an AI startup" --style premium-saas\n', + ); + return; + } + + const configStore = new ConfigStore(cwd); + const resolved = await configStore.load(); + const autoApprove = + resolved.effective.approvalMode === 'bypass' || resolved.effective.approvalMode === 'trusted'; + + const approve = async (bundle: {title: string; body: string; risk: string}): Promise => { + process.stdout.write(`\n${bundle.body}\n`); + if (options.dryRun) return false; + if (autoApprove || options.yes) { + process.stdout.write('Auto-approved (approval mode allows it).\n'); + return true; + } + if (!process.stdin.isTTY) { + process.stdout.write('Non-interactive terminal: declining (re-run with --yes to create the app).\n'); + return false; + } + const rl = readline.createInterface({input: process.stdin, output: process.stdout}); + try { + const answer = (await rl.question('Create this app? [y/N] ')).trim().toLowerCase(); + return answer === 'y' || answer === 'yes'; + } finally { + rl.close(); + } + }; + + const result = await buildApp(trimmed || 'app', { + cwd, + dir: options.dir, + stack: options.stack, + style: options.style, + dryRun: options.dryRun, + overwrite: options.overwrite, + uiSmoke: options.uiSmoke, + requireUiSmoke: options.requireUiSmoke, + // Command validation (Vite build) is left to the user via the open + // instructions; the CLI never auto-installs dependencies. + runCommands: false, + approve, + }); + + process.stdout.write(`${formatAppReport(result)}\n`); + if (result.status === 'blocked-existing' || result.status === 'write-error' || result.uiSmokeRequiredFailed) { + process.exitCode = 1; + } +}; diff --git a/src/cli/args.ts b/src/cli/args.ts index dad07e7..43eef72 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -1,5 +1,6 @@ import type {AgentMode} from '../agent/types.js'; import type {ApprovalMode, ConfigKey} from '../config/config.js'; +import type {SandboxMode} from '../sandbox/types.js'; export interface RootCliOptions { approvalMode?: ApprovalMode; @@ -11,6 +12,7 @@ export interface RootCliOptions { planOnly?: boolean; provider?: string; resume?: string; + sandbox?: SandboxMode; welcome?: boolean; } diff --git a/src/cli/commands.ts b/src/cli/commands.ts index 3b0610c..00f9b53 100644 --- a/src/cli/commands.ts +++ b/src/cli/commands.ts @@ -21,7 +21,7 @@ import {registerAdvancedCommands} from './commands/advanced.js'; import {registerContextAndRepoCommands} from './commands/contextRepoCommands.js'; import {registerProviderWebCommands} from './commands/providerWeb.js'; import {registerRuntimeCommands} from './commands/runtimeCommands.js'; - +import {registerWorkflowAndHelp} from './help.js'; export const collectOptions = (options: T, command?: Command): T => { return { ...(typeof command?.optsWithGlobals === 'function' ? command.optsWithGlobals() : {}), @@ -36,13 +36,26 @@ export const buildProgram = (handlers: CliHandlers): Command => { program .name('apeironcode') .description( - 'ApeironCode is an open-source, local-first AI coding assistant for local and cloud models.', + [ + 'ApeironCode is a local-first coding agent that fixes bugs, builds apps,', + 'improves UI, and reviews code with approval-gated changes.', + '', + 'Start here:', + ' apeironcode demo try it with no API key', + ' apeironcode fix fix a bug or failing tests', + ' apeironcode build "a todo app" build an app or feature', + ' apeironcode improve "premium UI" improve UI or code quality', + ' apeironcode review review the current changes', + '', + 'More: apeironcode help developer · apeironcode help advanced', + ].join('\n'), ) .argument('[prompt]', 'run in one-shot mode with the provided prompt') .option('--provider ', 'override the configured provider for this run') .option('--model ', 'override the configured model for this run') .option('--mode ', `run in a specific mode: ${AGENT_MODES.join(', ')}`) .option('--approval-mode ', 'override approval mode for this run') + .option('--sandbox ', 'override sandbox mode for this run: auto | docker | native | none') .option('--resume ', 'resume an existing session in one-shot or interactive mode') .option('--plan', 'create a plan, request approval, then execute if approved') .option('--plan-only', 'create a plan and show it without executing') @@ -578,5 +591,9 @@ export const buildProgram = (handlers: CliHandlers): Command => { registerAdvancedCommands(program, handlers); registerRuntimeCommands(program, handlers); + // Headline workflows, demo/local/real onboarding, grouped help, and the + // beginner-focused `--help` filter. Registered last for visibility filtering. + registerWorkflowAndHelp(program, handlers); + return program; }; diff --git a/src/cli/commands/types.ts b/src/cli/commands/types.ts index 326753e..f1afcbe 100644 --- a/src/cli/commands/types.ts +++ b/src/cli/commands/types.ts @@ -1,4 +1,9 @@ import type {AgentMode} from '../../agent/types.js'; +import type {FixCliOptions} from '../fixHandler.js'; +import type {NewCliOptions} from '../appBuilderHandler.js'; +import type {TestUiCliOptions} from '../testUiHandler.js'; +import type {RollbackCliOptions} from '../rollbackHandler.js'; +import type {ReviewResult} from '../reviewHandler.js'; export interface BgTaskCreateOptions { kind?: string; @@ -47,6 +52,12 @@ export interface CliHandlers { continueTask: (taskId?: string) => Promise; cost: (options: CostCliOptions) => Promise; deleteSession: (sessionId: string) => Promise; + demo: (scenario?: string) => Promise; + fix: (options: FixCliOptions) => Promise; + newApp: (idea: string | undefined, options: NewCliOptions) => Promise; + testUi: (target: string | undefined, options: TestUiCliOptions) => Promise; + review: (idea: string | undefined) => Promise; + rollback: (idOrSub: string | undefined, options: RollbackCliOptions) => Promise; doctor: (options: DoctorCliOptions) => Promise; debugConfig: () => Promise; debugLogs: (options?: {last?: number}) => Promise; diff --git a/src/cli/fixHandler.ts b/src/cli/fixHandler.ts new file mode 100644 index 0000000..2308ca6 --- /dev/null +++ b/src/cli/fixHandler.ts @@ -0,0 +1,124 @@ +import readline from 'node:readline/promises'; + +import {ConfigStore} from '../config/config.js'; +import {providerRegistry} from '../providers/registry.js'; +import type {ModelProvider} from '../providers/types.js'; +import {ApprovalManager} from '../safety/approvals.js'; +import {createDefaultToolRegistry} from '../tools/registry.js'; +import {executeFilePlan} from '../agent/filePlanExecutor.js'; +import type {FilePlan} from '../agent/filePlanProtocol.js'; +import {runRepairLoop, type RepairMode} from '../repair/repairLoop.js'; +import {formatRepairReport} from '../repair/finalReport.js'; +import type {ApprovalBundle} from '../repair/approvalBundle.js'; + +export interface FixCliOptions { + all?: boolean; + safe?: boolean; + untilGreen?: boolean; + dryRun?: boolean; + commit?: boolean; + yes?: boolean; +} + +const resolveMode = (options: FixCliOptions): RepairMode => { + if (options.safe) return 'safe'; + if (options.untilGreen) return 'until-green'; + if (options.all) return 'all'; + return 'normal'; +}; + +const streamProviderText = async ( + provider: ModelProvider, + model: string, + prompt: string, +): Promise => { + let text = ''; + for await (const chunk of provider.stream({ + messages: [{content: prompt, role: 'user'}], + model, + temperature: 0.2, + tools: [], + })) { + if (chunk.type === 'token') text += chunk.token ?? ''; + } + return text; +}; + +/** + * `apeironcode fix` handler (Phase 20B). Owns the repair workflow via + * `runRepairLoop`; the provider only proposes minimal plans. Dry-run never + * constructs a provider or writes anything. Approval is required before changes: + * auto in bypass/trusted mode, otherwise a single bundled y/N prompt. + */ +export const runFixCommand = async (cwd: string, options: FixCliOptions): Promise => { + const configStore = new ConfigStore(cwd); + const resolved = await configStore.load(); + const config = resolved.effective; + const mode = resolveMode(options); + + if (options.commit) { + process.stdout.write('--commit is planned for Phase 20E ship workflow and is not active yet.\n'); + } + + const autoApprove = config.approvalMode === 'bypass' || config.approvalMode === 'trusted'; + + const approve = async (bundle: ApprovalBundle): Promise => { + process.stdout.write(`\n${bundle.body}\n`); + if (autoApprove || options.yes) { + process.stdout.write('Auto-approved (approval mode allows it).\n'); + return true; + } + if (!process.stdin.isTTY) { + process.stdout.write('Non-interactive terminal: declining repair plan (use --yes or set approval mode).\n'); + return false; + } + const rl = readline.createInterface({input: process.stdin, output: process.stdout}); + try { + const answer = (await rl.question('Approve this repair plan? [y/N] ')).trim().toLowerCase(); + return answer === 'y' || answer === 'yes'; + } finally { + rl.close(); + } + }; + + const applyPlan = async ( + plan: FilePlan, + planCwd: string, + ): Promise<{ok: boolean; errors: string[]; filesChanged: string[]}> => { + // The loop already obtained bundled approval, so apply with a bypass manager + // (writes still go through ToolRegistry). + const toolRegistry = createDefaultToolRegistry(); + const result = await executeFilePlan(plan, { + approvalManager: new ApprovalManager('bypass'), + config, + cwd: planCwd, + toolRegistry, + }); + return {ok: result.ok, errors: result.errors, filesChanged: result.filesChanged}; + }; + + // Dry-run never constructs a provider. + const requestPlan = async (prompt: string): Promise => { + const provider = providerRegistry.createActive(config); + return streamProviderText(provider, config.defaultModel, prompt); + }; + + const result = await runRepairLoop( + {cwd, mode, dryRun: Boolean(options.dryRun)}, + {requestPlan, approve, applyPlan}, + ); + + process.stdout.write(`${formatRepairReport(result)}\n`); + + // Completion/failure panel with next actions (Phase 20E.5, Task F). + if (result.status !== 'dry-run' && result.status !== 'no-checks') { + const {buildFixProgress} = await import('./fixProgress.js'); + const {formatCompletionPanel} = await import('../progress/completionPanel.js'); + const {run, nextActions} = buildFixProgress(result); + process.stdout.write(`\n${formatCompletionPanel(run, {nextActions})}\n`); + } + + if (result.status !== 'green' && result.status !== 'dry-run' && result.status !== 'no-checks') { + process.exitCode = 1; + } +}; diff --git a/src/cli/fixProgress.ts b/src/cli/fixProgress.ts new file mode 100644 index 0000000..f7a840d --- /dev/null +++ b/src/cli/fixProgress.ts @@ -0,0 +1,37 @@ +import {ProgressStore} from '../progress/progressStore.js'; +import type {ProgressRun} from '../progress/types.js'; +import type {RepairLoopResult} from '../repair/finalReport.js'; + +/** + * Map a repair-loop result to a {@link ProgressRun} plus next-action suggestions + * for the completion/failure panel (Phase 20E.5, Tasks A/F). + */ +export const buildFixProgress = (result: RepairLoopResult): {run: ProgressRun; nextActions: string[]} => { + const store = new ProgressStore('fix', 'Fix detected failures', {projectPath: result.project.framework}); + store.planTasks([ + {id: 'detect', title: 'Detect project & checks'}, + {id: 'run', title: 'Run checks'}, + {id: 'repair', title: 'Generate & apply repair plan'}, + {id: 'rerun', title: 'Rerun checks'}, + ]); + store.setTask('detect', 'passed', `${result.plannedChecks.length} check(s)`); + store.setTask('run', result.status === 'green' && result.attempts.length === 0 ? 'passed' : 'failed'); + store.setTask('repair', result.attempts.some((a) => a.planApplied) ? 'passed' : result.attempts.length > 0 ? 'failed' : 'skipped'); + store.setTask('rerun', result.status === 'green' ? 'passed' : result.status === 'gave-up' || result.status === 'invalid-plan' ? 'failed' : 'skipped'); + + for (const check of result.finalChecks) { + store.addCommand({command: check.command, status: check.ok ? 'passed' : 'failed', exitCode: check.exitCode ?? undefined, outputExcerpt: check.ok ? undefined : check.output}); + } + for (const checkpoint of result.checkpoints) { + store.addArtifact({kind: 'checkpoint', label: 'Checkpoint', path: checkpoint}); + } + store.setStatus(result.status === 'green' ? 'passed' : result.status === 'rejected' ? 'skipped' : 'failed'); + + const nextActions: string[] = []; + if (result.status !== 'green') { + nextActions.push('apeironcode fix --until-green'); + if (result.checkpoints.length > 0) nextActions.push('apeironcode rollback --last'); + nextActions.push('apeironcode test-ui (if this is a web app)'); + } + return {run: store.snapshot(), nextActions}; +}; diff --git a/src/cli/help.ts b/src/cli/help.ts new file mode 100644 index 0000000..73efb2c --- /dev/null +++ b/src/cli/help.ts @@ -0,0 +1,276 @@ +import type {Command} from 'commander'; + +import type {CliHandlers} from './commands/types.js'; +import type {DoctorCliOptions} from './args.js'; +import {collectOptions} from './commands.js'; + +/** + * Commands shown in the default `apeironcode --help`. Everything else still + * works but is grouped under `apeironcode help developer` / `help advanced` so + * the first-run surface stays focused on the four killer workflows plus the + * three onboarding commands. + */ +export const BEGINNER_COMMANDS = [ + 'new', + 'fix', + 'build', + 'improve', + 'review', + 'demo', + 'doctor', + 'setup', + 'help', +] as const; + +const BEGINNER_SET = new Set(BEGINNER_COMMANDS); + +interface HelpGroup { + title: string; + intro: string; + rows: Array<{command: string; description: string}>; + footer?: string; +} + +const DEVELOPER_GROUP: HelpGroup = { + title: 'ApeironCode developer commands', + intro: 'Everyday commands for configuring providers and driving the agent.', + rows: [ + {command: 'provider', description: 'list, test, and configure model providers'}, + {command: 'model', description: 'list and recommend models for the active provider'}, + {command: 'context', description: 'inspect repo context, budget, and selected files'}, + {command: 'workflow', description: 'list and run typed multi-step workflows'}, + {command: 'test-ui', description: 'browser UI smoke for a generated app (needs Chromium)'}, + {command: 'github', description: 'issues, PRs, and CI inspection (dry-run by default)'}, + {command: 'memory', description: 'review and manage durable project memory'}, + {command: 'session(s)', description: 'manage saved local sessions and history'}, + {command: 'config', description: 'inspect or update local configuration'}, + {command: 'revert', description: 'undo the last edit or a specific edit id'}, + ], + footer: 'Run `apeironcode --help` for details.', +}; + +const ADVANCED_GROUP: HelpGroup = { + title: 'ApeironCode advanced / experimental commands', + intro: 'Power features. Some are experimental and may change.', + rows: [ + {command: 'mcp', description: 'manage Model Context Protocol servers'}, + {command: 'skills', description: 'discover, create, and run scoped skills'}, + {command: 'team', description: 'run isolated multi-agent team workspaces'}, + {command: 'hooks', description: 'manage lifecycle hooks'}, + {command: 'bridge', description: 'editor/IDE bridge integration'}, + {command: 'runtime', description: 'inspect runtime routing and intent'}, + {command: 'sandbox', description: 'inspect optional sandbox backends'}, + {command: 'rollback', description: 'restore files from a fix checkpoint (rollback list / --last)'}, + {command: 'plugins', description: 'list loaded plugins and their tools'}, + {command: 'brain', description: 'preview and manage optional Project Brain files'}, + {command: 'permissions', description: 'manage permission rules'}, + {command: 'debug', description: 'safe local debugging snapshots'}, + ], + footer: 'Run `apeironcode --help` for details.', +}; + +export const formatHelpGroup = (group: HelpGroup): string => { + const width = Math.max(...group.rows.map((row) => row.command.length)); + const lines: string[] = [group.title, '', group.intro, '']; + for (const row of group.rows) { + lines.push(` ${row.command.padEnd(width)} ${row.description}`); + } + if (group.footer) { + lines.push(''); + lines.push(group.footer); + } + return lines.join('\n'); +}; + +export const formatDeveloperHelp = (): string => formatHelpGroup(DEVELOPER_GROUP); +export const formatAdvancedHelp = (): string => formatHelpGroup(ADVANCED_GROUP); + +/** + * Register the headline workflow commands (fix/new/build/improve/review/ + * rollback) plus `help developer` / `help advanced`, and restrict the default + * `--help` listing to beginner commands. Every other command stays registered + * and functional. + */ +export const registerWorkflowAndHelp = (program: Command, handlers: CliHandlers): void => { + // `fix` runs the Phase 20B repair loop (detect → check → propose → approve → + // apply → rerun), not the generic agent prompt. + program + .command('fix') + .description('detect failures, propose approval-gated fixes, and rerun checks') + .argument('[description...]', 'optional note about what to fix') + .option('--all', 'include build/e2e checks and allow more attempts') + .option('--safe', 'restrict edits to files referenced by errors; max 2 attempts') + .option('--until-green', 'keep attempting until checks pass (up to 10)') + .option('--dry-run', 'detect project and list checks without running or writing') + .option('--commit', '(planned) commit after a successful fix') + .option('--yes', 'approve the repair bundle without prompting') + .action(async (_parts: string[] | undefined, options: Record) => { + await handlers.fix({ + all: options.all, + safe: options.safe, + untilGreen: options.untilGreen, + dryRun: options.dryRun, + commit: options.commit, + yes: options.yes, + }); + }); + + // `new` and `build` create a real app from a deterministic template + // (Phase 20C). No API key or model required for template generation. + const appBuilder = (name: 'new' | 'build', summary: string): void => { + program + .command(name) + .description(summary) + .argument('[idea...]', 'what to build, e.g. "premium todo app"') + .option('--stack ', 'static | dashboard | vite | next') + .option('--style