From a3cf939b7d5e4e9e6ba33553ed04c0ee1568f357 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 06:59:25 +0000 Subject: [PATCH 01/60] Add PRD and design document for ADE CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Captures the entity model (Facet → Option → Recipe → Provision), the two-phase resolution pipeline (config.yaml → LogicalConfig → agent files), and the principle that ADE owns agent config format knowledge while MCP servers remain pure runtime. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- docs/DESIGN.md | 332 +++++++++++++++++++++++++++++++++++++++++++++++++ docs/PRD.md | 155 +++++++++++++++++++++++ 2 files changed, 487 insertions(+) create mode 100644 docs/DESIGN.md create mode 100644 docs/PRD.md diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 0000000..80949b4 --- /dev/null +++ b/docs/DESIGN.md @@ -0,0 +1,332 @@ +# ADE CLI — Design Document + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────┐ +│ CLI Layer │ +│ ade init · ade apply · ade add · ade remove │ +└──────────────────────┬──────────────────────────────┘ + │ + ┌────────▼────────┐ + │ Catalog │ facets, options, recipes + └────────┬────────┘ + │ + ┌────────▼────────┐ + │ Resolver │ config.yaml + catalog → provisions + └────────┬────────┘ + │ + ┌────────────▼────────────┐ + │ Provision Writers │ each writer produces LogicalConfig + │ │ fragments; some invoke external CLIs + │ workflows · skills │ + │ knowledge · mcp-server │ + │ instruction · tool │ + └────────────┬────────────┘ + │ merge + ┌────────▼────────┐ + │ LogicalConfig │ agent-agnostic intermediate repr + └────────┬────────┘ + │ + ┌────────────▼────────────┐ + │ Agent Writers │ ADE owns format knowledge + │ │ + │ claude-code · copilot │ + │ kiro │ + └─────────────────────────┘ + │ + agent-specific files +``` + +## Data Flow + +### 1. Resolution: config.yaml → LogicalConfig + +``` +read config.yaml + → for each choice, look up (facet, option) in catalog + → collect all provisions from the selected option's recipe + → merge extras from config.yaml + → for each provision, invoke the corresponding provision writer + → each writer returns a LogicalConfig fragment + → merge all fragments into one LogicalConfig + → write config.lock.yaml (serialized LogicalConfig) +``` + +### 2. Generation: LogicalConfig → agent files + +``` +read config.lock.yaml (or use in-memory LogicalConfig) + → select agent writer based on config.yaml `agent` field + → writer reads current agent files (if any) for merge/update + → writer produces updated agent-specific files + → write files to disk +``` + +### 3. CLI actions: provision writers that invoke CLIs + +Some provisions (notably `skills` and `knowledge`) don't produce +LogicalConfig entries directly. Instead, they invoke external CLIs that +manage their own state. ADE orchestrates these invocations during `apply`. + +``` +provision {writer: "skills", config: {name: "design", version: "1.0"}} + → ade invokes: npx @codemcp/skills install design@1.0 + → skills CLI manages its own files + → no LogicalConfig entry produced (or a marker entry for tracking) + +provision {writer: "knowledge", config: {name: "tanstack", source: "..."}} + → ade invokes: knowledge CLI to install/update the docset + → knowledge CLI manages .knowledge/ or equivalent + → LogicalConfig gets a docsets entry for agent writer reference +``` + +## Entity Model + +### Catalog Structure + +```typescript +interface Catalog { + facets: Facet[]; +} + +interface Facet { + id: string; // e.g. "workflow" + label: string; // e.g. "Workflow Framework" + description: string; + required: boolean; // false = skippable + options: Option[]; +} + +interface Option { + id: string; // e.g. "codemcp" + label: string; // e.g. "CodeMCP Workflows" + description: string; + recipe: Provision[]; +} + +interface Provision { + writer: ProvisionWriter; + config: Record; // writer-specific +} + +type ProvisionWriter = + | "workflows" + | "skills" + | "knowledge" + | "mcp-server" + | "instruction" + | "tool"; +``` + +### LogicalConfig (intermediate representation) + +```typescript +interface LogicalConfig { + mcp_servers: McpServerEntry[]; + instructions: string[]; + cli_actions: CliAction[]; + docsets: Docset[]; +} + +interface McpServerEntry { + ref: string; // unique key for dedup/update + command: string; // e.g. "npx" + args: string[]; // e.g. ["-y", "@anthropic/workflows"] + env: Record; +} + +interface CliAction { + command: string; + args: string[]; + phase: "setup" | "apply"; // when to run +} + +interface Docset { + path: string; + description: string; +} +``` + +### Config Files + +```typescript +// config.yaml — human-authored +interface UserConfig { + agent: string; // agent writer id + choices: Record; // facet_id → option_id + extras?: { + mcp_servers?: McpServerEntry[]; + instructions?: string[]; + }; +} + +// config.lock.yaml — generated +interface LockFile { + version: 1; + generated_at: string; // ISO timestamp + agent: string; + choices: Record; // snapshot of selections + logical_config: LogicalConfig; +} +``` + +## Agent Writers + +Each agent writer implements a single interface: + +```typescript +interface AgentWriter { + id: string; + apply(config: LogicalConfig, projectRoot: string): Promise; +} +``` + +The writer has full ownership of how to translate LogicalConfig into +agent-specific files. It reads existing files when needed to perform +incremental updates rather than full overwrites. + +### Claude Code Writer + +Produces: +- `.claude/settings.json` — MCP server declarations under + `mcpServers` key. Each `McpServerEntry` maps to a server object with + `command`, `args`, and `env`. +- `CLAUDE.md` — instructions block. Writer appends/replaces a clearly + delimited ADE-managed section. + +### Copilot Writer + +Produces: +- `.vscode/settings.json` — MCP server declarations under + `github.copilot.chat.mcpServers` or equivalent key. +- `copilot-instructions.md` or `.github/copilot-instructions.md` — + ADE-managed instructions section. + +### Kiro Writer + +Produces: +- `.kiro/` steering files — MCP server declarations and instruction + documents per Kiro's expected format. + +## Provision Writers + +Each provision writer transforms its config into LogicalConfig fragments +and/or CLI actions: + +### `workflows` writer + +```yaml +# provision config +config: + package: "@anthropic/workflows" + env: + WORKFLOW_DIR: "./workflows" +``` + +Produces: one `McpServerEntry` with `command: "npx"`, +`args: ["-y", "@anthropic/workflows"]`, and the given env vars. + +### `skills` writer + +```yaml +config: + name: "design" + version: "1.0" +``` + +Produces: one `CliAction` to invoke the skills CLI. May also produce an +`McpServerEntry` if the skills MCP server needs to be registered. + +### `knowledge` writer + +```yaml +config: + name: "tanstack" + source: "https://tanstack.com/query/latest/docs" +``` + +Produces: one `CliAction` to invoke the knowledge CLI, plus a `Docset` +entry so agent writers can reference it in instructions. + +### `mcp-server` writer + +```yaml +config: + ref: "my-server" + command: "npx" + args: ["-y", "@acme/mcp-server"] + env: + API_KEY: "${API_KEY}" +``` + +Pass-through: produces one `McpServerEntry` directly. + +### `instruction` writer + +```yaml +config: + text: "Always use pnpm, never npm." +``` + +Produces: one `instructions` entry. + +### `tool` writer + +```yaml +config: + command: "pnpm" + check: "pnpm --version" +``` + +Produces: one `CliAction` for validation/installation. + +## Package Structure + +``` +packages/ + shared/src/ + types.ts # LogicalConfig, Provision, Facet, etc. + config.ts # read/write config.yaml and config.lock.yaml + ade/src/ + commands/ + init.ts # interactive setup + apply.ts # resolve + generate + add.ts # modify single facet + remove.ts # remove facet selection + status.ts # show current state + core/ + resolver.ts # config.yaml + catalog → provisions → LogicalConfig + catalog.ts # load and query the catalog + adapters/ + writers/ # provision writers + workflows.ts + skills.ts + knowledge.ts + mcp-server.ts + instruction.ts + tool.ts + agents/ # agent writers + claude-code.ts + copilot.ts + kiro.ts + tui/ + prompts.ts # interactive facet selection UI + utils/ + ade/catalog/ + facets.yaml # the embedded catalog +``` + +## Open Questions + +1. **Catalog versioning.** When the catalog updates (new facets, changed + recipes), how does `ade apply` behave for existing config.yaml files? + Initial approach: warn on unknown facets/options, apply what resolves. + +2. **CLI delegation details.** The exact CLI interfaces for `@codemcp/skills` + and the knowledge tool are not finalized. ADE will invoke them as + subprocesses; the contract is their CLI flags, not internal APIs. + +3. **Merge strategy for agent files.** When agent files contain user-authored + content outside ADE-managed sections, writers must preserve it. Delimiter + conventions (e.g. ``) need to be defined per agent. diff --git a/docs/PRD.md b/docs/PRD.md new file mode 100644 index 0000000..124c1a1 --- /dev/null +++ b/docs/PRD.md @@ -0,0 +1,155 @@ +# ADE CLI — Product Requirements Document + +## Problem + +Coding agents (Claude Code, Copilot, Kiro, etc.) each require their own +configuration format to wire in MCP servers, instructions, and documentation. +Teams manually maintain these per-agent config files, leading to drift, +duplication, and onboarding friction. Adding a new MCP server or skill means +editing multiple agent-specific files by hand. + +ADE's information architecture (process, conventions, documentation) is +agent-agnostic, but the last mile — getting it into an agent's config — is not. + +## Goal + +Provide a single CLI that lets engineers declare *what* their project needs +(workflows, skills, knowledge, tools) in one place, and generates the correct +agent-specific configuration for whichever coding agent they use. + +## Users + +- **Individual developers** setting up a project for agentic development. +- **Tech leads** standardizing agent configuration across a team. +- **CI/CD pipelines** that need reproducible agent environments. + +## Core Concepts + +### Facet + +A user-facing configuration question representing a single concern (e.g. +"Which workflow framework?" or "Which testing convention?"). Each facet offers +a set of options, exactly one of which is selected. Facets can be skippable +(no selection = no provisions from that facet). + +### Option + +One possible answer to a facet. Each option carries a recipe. + +### Recipe + +A list of provisions that an option brings into the project. A recipe is +never referenced directly by the user — it is the payload behind an option. + +### Provision + +An atomic unit of configuration. Each provision names a **writer** and +carries writer-specific config. Provision types: + +| Writer | What it produces | +|---------------|---------------------------------------------------| +| `workflows` | MCP server entry for `@anthropic/workflows` | +| `skills` | Invokes `@codemcp/skills` CLI to install skills | +| `knowledge` | Invokes knowledge CLI to set up a docset | +| `mcp-server` | Generic MCP server entry (command + args + env) | +| `instruction` | Raw instruction text for the agent | +| `tool` | CLI tool dependency to be available | + +### LogicalConfig (intermediate representation) + +Agent-agnostic resolved configuration. This is the contract between the +resolution step and the agent writers: + +``` +mcp_servers: [{ref, command, args, env}] +instructions: [string] +cli_actions: [{command, args}] +docsets: [{path, description}] +``` + +### Agent Writer + +Translates LogicalConfig into agent-specific files. ADE owns the knowledge of +every supported agent's config format. When an agent changes its format, only +its writer needs updating. + +Supported agents (initial): + +| Agent | Output files | +|-------------|---------------------------------------------| +| Claude Code | `.claude/settings.json`, `CLAUDE.md` | +| Copilot | `.vscode/settings.json`, instructions MD | +| Kiro | `.kiro/` steering files | + +## User-Facing Files + +### `config.yaml` (checked into repo) + +The human-authored source of truth. Records facet selections and any manual +overrides. Minimal, readable, diffable. + +```yaml +agent: claude-code # which agent writer to use +choices: + workflow: codemcp # facet_id: option_id + testing: vitest + knowledge: tanstack +extras: # manual additions outside facets + mcp_servers: + - ref: custom-server + command: npx + args: ["-y", "@acme/mcp-server"] + instructions: + - "Always use pnpm, never npm." +``` + +### `config.lock.yaml` (checked into repo) + +Fully resolved LogicalConfig snapshot. Deterministic — same `config.yaml` +always produces the same lock file. Enables diffing what actually changed +when a facet selection or catalog version is updated. + +## CLI Commands + +``` +ade init Interactive TUI: select agent, walk through facets, + write config.yaml + config.lock.yaml + agent files. + +ade apply Re-resolve config.yaml → config.lock.yaml → agent files. + Non-interactive. Idempotent. + +ade add Add or change a single facet selection interactively. + +ade remove Remove a facet selection. + +ade status Show current selections and what would change on apply. +``` + +## Catalog + +Facets, options, and recipes live in a **catalog** — a structured data source +shipped with ADE (initially embedded, later fetchable/versioned). The catalog +is the single place that knows which provisions each option requires. + +## Non-Goals (initial release) + +- Runtime agent behavior (that is the MCP servers' job). +- Managing MCP server lifecycles or health checks. +- Supporting agent-specific features beyond config file generation. +- Plugin API for third-party provision writers (keep it internal first). + +## Key Design Decisions + +1. **ADE owns agent config format knowledge.** MCP servers are pure runtime; + they do not know or care which agent invoked them. + +2. **Provision writers may invoke external CLIs.** The `skills` and + `knowledge` writers delegate to existing `@codemcp/skills` and knowledge + CLIs rather than reimplementing their logic. ADE orchestrates; it does not + absorb. + +3. **LogicalConfig is the stable contract.** Provision writers produce it, + agent writers consume it. Neither side knows about the other. + +4. **Lock file is mandatory.** It makes the resolved state explicit, + reviewable, and reproducible. From 398159f5e839810da4498f53df4c8e03dcb94181 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 08:16:47 +0000 Subject: [PATCH 02/60] Revise PRD and design doc based on feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Scope both docs explicitly to ADE CLI, not the whole ADE concept - Correct package names: @codemcp/workflows-server, @codemcp/knowledge-server, @codemcp/knowledge - Rename commands: init→setup, apply→install - Replace docset with KnowledgeSource (origin-level abstraction) - Catalog is TypeScript code, not YAML — gives type safety and natural versioning - Provision writers import sibling packages directly instead of CLI subprocess - Add custom section in config.yaml for user-managed entries, rest is CLI-only - Resolve all open questions into decisions https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- docs/DESIGN.md | 176 ++++++++++++++++++++++++++----------------------- docs/PRD.md | 80 ++++++++++++++-------- 2 files changed, 144 insertions(+), 112 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 80949b4..b89713c 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1,15 +1,21 @@ # ADE CLI — Design Document +> **Scope.** This document covers the **ADE CLI** (`packages/ade`) — the +> setup and configuration tool. It does not cover the runtime MCP servers +> (`@codemcp/workflows-server`, `@codemcp/knowledge-server`) or the broader +> ADE information architecture. For the overall ADE vision, see the project +> README. + ## Architecture Overview ``` ┌─────────────────────────────────────────────────────┐ │ CLI Layer │ -│ ade init · ade apply · ade add · ade remove │ +│ ade setup · ade install · ade add · ade remove │ └──────────────────────┬──────────────────────────────┘ │ ┌────────▼────────┐ - │ Catalog │ facets, options, recipes + │ Catalog │ facets, options, recipes (TypeScript) └────────┬────────┘ │ ┌────────▼────────┐ @@ -18,7 +24,7 @@ │ ┌────────────▼────────────┐ │ Provision Writers │ each writer produces LogicalConfig - │ │ fragments; some invoke external CLIs + │ │ fragments; some call package APIs │ workflows · skills │ │ knowledge · mcp-server │ │ instruction · tool │ @@ -46,7 +52,7 @@ read config.yaml → for each choice, look up (facet, option) in catalog → collect all provisions from the selected option's recipe - → merge extras from config.yaml + → merge custom section from config.yaml → for each provision, invoke the corresponding provision writer → each writer returns a LogicalConfig fragment → merge all fragments into one LogicalConfig @@ -63,28 +69,36 @@ read config.lock.yaml (or use in-memory LogicalConfig) → write files to disk ``` -### 3. CLI actions: provision writers that invoke CLIs +### 3. Package API calls from provision writers -Some provisions (notably `skills` and `knowledge`) don't produce -LogicalConfig entries directly. Instead, they invoke external CLIs that -manage their own state. ADE orchestrates these invocations during `apply`. +Some provisions (notably `skills` and `knowledge`) delegate to sibling +packages. ADE imports them as TypeScript dependencies rather than shelling +out, giving type safety and avoiding CLI flag contracts. ``` provision {writer: "skills", config: {name: "design", version: "1.0"}} - → ade invokes: npx @codemcp/skills install design@1.0 - → skills CLI manages its own files - → no LogicalConfig entry produced (or a marker entry for tracking) - -provision {writer: "knowledge", config: {name: "tanstack", source: "..."}} - → ade invokes: knowledge CLI to install/update the docset - → knowledge CLI manages .knowledge/ or equivalent - → LogicalConfig gets a docsets entry for agent writer reference + → import { install } from "@codemcp/skills" + → install({name: "design", version: "1.0"}) + → skills package manages its own files + → may return a LogicalConfig fragment (e.g. MCP server entry) + +provision {writer: "knowledge", config: {name: "tanstack", origin: "https://..."}} + → import { addSource } from "@codemcp/knowledge" + → addSource({name: "tanstack", origin: "https://..."}) + → knowledge package manages docset artifacts + → LogicalConfig gets a knowledge_sources entry ``` +Where direct import is impractical (e.g. the dependency isn't TypeScript or +has incompatible runtimes), CLI subprocess invocation is the fallback. + ## Entity Model ### Catalog Structure +The catalog is TypeScript code, not YAML. This gives us type safety, registry +patterns, and explicit references between options. + ```typescript interface Catalog { facets: Facet[]; @@ -126,24 +140,25 @@ interface LogicalConfig { mcp_servers: McpServerEntry[]; instructions: string[]; cli_actions: CliAction[]; - docsets: Docset[]; + knowledge_sources: KnowledgeSource[]; } interface McpServerEntry { ref: string; // unique key for dedup/update command: string; // e.g. "npx" - args: string[]; // e.g. ["-y", "@anthropic/workflows"] + args: string[]; // e.g. ["-y", "@codemcp/workflows-server"] env: Record; } interface CliAction { command: string; args: string[]; - phase: "setup" | "apply"; // when to run + phase: "setup" | "install"; // when to run } -interface Docset { - path: string; +interface KnowledgeSource { + name: string; // e.g. "tanstack" + origin: string; // URL, path, or package ref description: string; } ``` @@ -151,17 +166,17 @@ interface Docset { ### Config Files ```typescript -// config.yaml — human-authored +// config.yaml — mostly CLI-managed interface UserConfig { agent: string; // agent writer id choices: Record; // facet_id → option_id - extras?: { + custom?: { // user-managed section mcp_servers?: McpServerEntry[]; instructions?: string[]; }; } -// config.lock.yaml — generated +// config.lock.yaml — generated, never hand-edited interface LockFile { version: 1; generated_at: string; // ISO timestamp @@ -178,7 +193,7 @@ Each agent writer implements a single interface: ```typescript interface AgentWriter { id: string; - apply(config: LogicalConfig, projectRoot: string): Promise; + install(config: LogicalConfig, projectRoot: string): Promise; } ``` @@ -212,71 +227,58 @@ Produces: ## Provision Writers Each provision writer transforms its config into LogicalConfig fragments -and/or CLI actions: +and/or CLI actions. ### `workflows` writer -```yaml -# provision config -config: - package: "@anthropic/workflows" - env: - WORKFLOW_DIR: "./workflows" +```typescript +// provision config +{ package: "@codemcp/workflows-server", env: { WORKFLOW_DIR: "./workflows" } } ``` Produces: one `McpServerEntry` with `command: "npx"`, -`args: ["-y", "@anthropic/workflows"]`, and the given env vars. +`args: ["-y", "@codemcp/workflows-server"]`, and the given env vars. ### `skills` writer -```yaml -config: - name: "design" - version: "1.0" +```typescript +{ name: "design", version: "1.0" } ``` -Produces: one `CliAction` to invoke the skills CLI. May also produce an -`McpServerEntry` if the skills MCP server needs to be registered. +Calls `@codemcp/skills` API to install. May also produce an `McpServerEntry` +if the skills MCP server needs to be registered. ### `knowledge` writer -```yaml -config: - name: "tanstack" - source: "https://tanstack.com/query/latest/docs" +```typescript +{ name: "tanstack", origin: "https://tanstack.com/query/latest/docs" } ``` -Produces: one `CliAction` to invoke the knowledge CLI, plus a `Docset` -entry so agent writers can reference it in instructions. +Calls `@codemcp/knowledge` API to add the source. Produces a +`KnowledgeSource` entry so agent writers can reference it. The knowledge +package manages the physical docset artifacts; multiple sources may be +combined into one docset by `@codemcp/knowledge-server` at runtime. ### `mcp-server` writer -```yaml -config: - ref: "my-server" - command: "npx" - args: ["-y", "@acme/mcp-server"] - env: - API_KEY: "${API_KEY}" +```typescript +{ ref: "my-server", command: "npx", args: ["-y", "@acme/mcp-server"], env: { API_KEY: "${API_KEY}" } } ``` Pass-through: produces one `McpServerEntry` directly. ### `instruction` writer -```yaml -config: - text: "Always use pnpm, never npm." +```typescript +{ text: "Always use pnpm, never npm." } ``` Produces: one `instructions` entry. ### `tool` writer -```yaml -config: - command: "pnpm" - check: "pnpm --version" +```typescript +{ command: "pnpm", check: "pnpm --version" } ``` Produces: one `CliAction` for validation/installation. @@ -286,47 +288,55 @@ Produces: one `CliAction` for validation/installation. ``` packages/ shared/src/ - types.ts # LogicalConfig, Provision, Facet, etc. - config.ts # read/write config.yaml and config.lock.yaml + types.ts # LogicalConfig, Provision, Facet, etc. + config.ts # read/write config.yaml and config.lock.yaml ade/src/ commands/ - init.ts # interactive setup - apply.ts # resolve + generate - add.ts # modify single facet - remove.ts # remove facet selection - status.ts # show current state + setup.ts # interactive TUI setup + install.ts # resolve + generate (idempotent) + add.ts # modify single facet + remove.ts # remove facet selection + status.ts # show current state core/ - resolver.ts # config.yaml + catalog → provisions → LogicalConfig - catalog.ts # load and query the catalog + resolver.ts # config.yaml + catalog → provisions → LogicalConfig + catalog/ + index.ts # catalog registry, exports all facets + facets/ + workflow.ts # workflow facet definition + testing.ts # testing facet definition + knowledge.ts # knowledge/documentation facet definition + ... adapters/ - writers/ # provision writers + writers/ # provision writers workflows.ts skills.ts knowledge.ts mcp-server.ts instruction.ts tool.ts - agents/ # agent writers + agents/ # agent writers claude-code.ts copilot.ts kiro.ts tui/ - prompts.ts # interactive facet selection UI + prompts.ts # interactive facet selection UI utils/ - ade/catalog/ - facets.yaml # the embedded catalog ``` -## Open Questions +## Decisions (formerly open questions) -1. **Catalog versioning.** When the catalog updates (new facets, changed - recipes), how does `ade apply` behave for existing config.yaml files? - Initial approach: warn on unknown facets/options, apply what resolves. +1. **Catalog is TypeScript code.** No YAML catalog files. Facets, options, + and recipes are defined as typed objects in `src/catalog/`. This gives + type safety, IDE support, and natural versioning with the package. + Each facet lives in its own file under `catalog/facets/`. -2. **CLI delegation details.** The exact CLI interfaces for `@codemcp/skills` - and the knowledge tool are not finalized. ADE will invoke them as - subprocesses; the contract is their CLI flags, not internal APIs. +2. **Direct package imports over CLI subprocesses.** Provision writers for + `skills` and `knowledge` import `@codemcp/skills` and `@codemcp/knowledge` + as TypeScript dependencies and call their APIs. This provides type safety + and avoids brittle CLI flag contracts. CLI subprocess invocation is the + fallback for non-TypeScript or cross-runtime cases. -3. **Merge strategy for agent files.** When agent files contain user-authored - content outside ADE-managed sections, writers must preserve it. Delimiter - conventions (e.g. ``) need to be defined per agent. +3. **`custom` section isolates user edits.** Only the `custom` block in + `config.yaml` is user-managed. The rest is CLI-managed. This eliminates + merge conflicts: the CLI never touches `custom`, and users never touch + the rest. Agent writers merge both sections when generating output. diff --git a/docs/PRD.md b/docs/PRD.md index 124c1a1..8f6cc64 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -1,5 +1,10 @@ # ADE CLI — Product Requirements Document +> **Scope.** This document covers the **ADE CLI** (`packages/ade`) — the +> setup and configuration tool. It does not cover the broader ADE information +> architecture (process, conventions, documentation layers) or the runtime +> MCP servers. For the overall ADE vision, see the project README. + ## Problem Coding agents (Claude Code, Copilot, Kiro, etc.) each require their own @@ -46,14 +51,22 @@ never referenced directly by the user — it is the payload behind an option. An atomic unit of configuration. Each provision names a **writer** and carries writer-specific config. Provision types: -| Writer | What it produces | -|---------------|---------------------------------------------------| -| `workflows` | MCP server entry for `@anthropic/workflows` | -| `skills` | Invokes `@codemcp/skills` CLI to install skills | -| `knowledge` | Invokes knowledge CLI to set up a docset | -| `mcp-server` | Generic MCP server entry (command + args + env) | -| `instruction` | Raw instruction text for the agent | -| `tool` | CLI tool dependency to be available | +| Writer | What it produces | +|---------------|-----------------------------------------------------------| +| `workflows` | MCP server entry for `@codemcp/workflows-server` | +| `skills` | Invokes `@codemcp/skills` to install skills | +| `knowledge` | Invokes `@codemcp/knowledge` CLI to set up knowledge sources | +| `mcp-server` | Generic MCP server entry (command + args + env) | +| `instruction` | Raw instruction text for the agent | +| `tool` | CLI tool dependency to be available | + +### KnowledgeSource + +Describes the origin of documentation content (e.g. a URL, a local path, or +a package reference). Multiple knowledge sources may be combined into a single +docset when the `@codemcp/knowledge-server` MCP is the selected option for the +documentation facet. The knowledge CLI (`@codemcp/knowledge`) manages the +physical docset artifacts; ADE only tracks the sources. ### LogicalConfig (intermediate representation) @@ -61,10 +74,10 @@ Agent-agnostic resolved configuration. This is the contract between the resolution step and the agent writers: ``` -mcp_servers: [{ref, command, args, env}] -instructions: [string] -cli_actions: [{command, args}] -docsets: [{path, description}] +mcp_servers: [{ref, command, args, env}] +instructions: [string] +cli_actions: [{command, args}] +knowledge_sources: [{name, origin, description}] ``` ### Agent Writer @@ -85,16 +98,16 @@ Supported agents (initial): ### `config.yaml` (checked into repo) -The human-authored source of truth. Records facet selections and any manual -overrides. Minimal, readable, diffable. +Records facet selections. The CLI manages most of this file via commands; +users may add manual entries in the `custom` section. ```yaml -agent: claude-code # which agent writer to use +agent: claude-code # which agent writer to use choices: - workflow: codemcp # facet_id: option_id + workflow: codemcp # facet_id: option_id testing: vitest knowledge: tanstack -extras: # manual additions outside facets +custom: # user-managed section (not touched by CLI) mcp_servers: - ref: custom-server command: npx @@ -103,6 +116,10 @@ extras: # manual additions outside facets - "Always use pnpm, never npm." ``` +The `custom` section is the only part users edit by hand. All other sections +are maintained exclusively through CLI commands, which simplifies merge +conflicts and keeps the file structure predictable. + ### `config.lock.yaml` (checked into repo) Fully resolved LogicalConfig snapshot. Deterministic — same `config.yaml` @@ -112,24 +129,26 @@ when a facet selection or catalog version is updated. ## CLI Commands ``` -ade init Interactive TUI: select agent, walk through facets, +ade setup Interactive TUI: select agent, walk through facets, write config.yaml + config.lock.yaml + agent files. -ade apply Re-resolve config.yaml → config.lock.yaml → agent files. +ade install Re-resolve config.yaml → config.lock.yaml → agent files. Non-interactive. Idempotent. ade add Add or change a single facet selection interactively. ade remove Remove a facet selection. -ade status Show current selections and what would change on apply. +ade status Show current selections and what would change on install. ``` ## Catalog -Facets, options, and recipes live in a **catalog** — a structured data source -shipped with ADE (initially embedded, later fetchable/versioned). The catalog -is the single place that knows which provisions each option requires. +Facets, options, and recipes live in a **catalog** — TypeScript code shipped +with ADE. Using code (not data files) gives us type safety, registry +patterns, and explicit references between options. The catalog is the single +place that knows which provisions each option requires, and it versions +naturally with the ADE package. ## Non-Goals (initial release) @@ -140,16 +159,19 @@ is the single place that knows which provisions each option requires. ## Key Design Decisions -1. **ADE owns agent config format knowledge.** MCP servers are pure runtime; - they do not know or care which agent invoked them. +1. **ADE CLI owns agent config format knowledge.** MCP servers are pure + runtime; they do not know or care which agent invoked them. -2. **Provision writers may invoke external CLIs.** The `skills` and - `knowledge` writers delegate to existing `@codemcp/skills` and knowledge - CLIs rather than reimplementing their logic. ADE orchestrates; it does not - absorb. +2. **Provision writers may call package APIs directly.** The `skills` and + `knowledge` writers import `@codemcp/skills` and `@codemcp/knowledge` as + TypeScript dependencies. This gives type safety over subprocess invocation. + CLI fallback remains an option where direct import is impractical. 3. **LogicalConfig is the stable contract.** Provision writers produce it, agent writers consume it. Neither side knows about the other. 4. **Lock file is mandatory.** It makes the resolved state explicit, reviewable, and reproducible. + +5. **User edits are confined to `custom`.** The rest of `config.yaml` is + CLI-managed, eliminating merge conflicts in the structured sections. From 363435b45a07ccae3d20257ba1447995453194cf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 08:25:09 +0000 Subject: [PATCH 03/60] =?UTF-8?q?Fix=20PRD=20and=20design=20doc:=20agent?= =?UTF-8?q?=20not=20in=20config,=20tool=E2=86=92installable,=20multi-provi?= =?UTF-8?q?sion=20recipes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove agent from config.yaml — it's a generation-time parameter, not a stored choice. Same config works for any supported agent. - Rename "tool" writer to "installable" to avoid ambiguity. - Document that recipes commonly contain multiple provisions targeting different writers (e.g. workflow option → MCP server entry + instructions). https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- docs/DESIGN.md | 29 +++++++++++++++++++---------- docs/PRD.md | 19 ++++++++++++++++--- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index b89713c..c9f62d9 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -27,7 +27,7 @@ │ │ fragments; some call package APIs │ workflows · skills │ │ knowledge · mcp-server │ - │ instruction · tool │ + │ instruction · install. │ └────────────┬────────────┘ │ merge ┌────────▼────────┐ @@ -63,12 +63,16 @@ read config.yaml ``` read config.lock.yaml (or use in-memory LogicalConfig) - → select agent writer based on config.yaml `agent` field + → select agent writer (from --agent flag or auto-detect from project files) → writer reads current agent files (if any) for merge/update → writer produces updated agent-specific files → write files to disk ``` +The target agent is a **generation-time parameter**, not stored in +`config.yaml`. This keeps the config agent-agnostic — the same choices +can produce output for any supported agent. + ### 3. Package API calls from provision writers Some provisions (notably `skills` and `knowledge`) delegate to sibling @@ -116,9 +120,15 @@ interface Option { id: string; // e.g. "codemcp" label: string; // e.g. "CodeMCP Workflows" description: string; - recipe: Provision[]; + recipe: Provision[]; // multiple provisions per option is common } +// A recipe typically contains multiple provisions for different writers. +// Example: the "codemcp" workflow option produces: +// 1. workflows provision → registers @codemcp/workflows-server as MCP server +// 2. instruction provision → adds workflow usage guidance to agent instructions +// This is how one logical concept materializes across different output channels. + interface Provision { writer: ProvisionWriter; config: Record; // writer-specific @@ -130,7 +140,7 @@ type ProvisionWriter = | "knowledge" | "mcp-server" | "instruction" - | "tool"; + | "installable"; ``` ### LogicalConfig (intermediate representation) @@ -166,9 +176,8 @@ interface KnowledgeSource { ### Config Files ```typescript -// config.yaml — mostly CLI-managed +// config.yaml — mostly CLI-managed, agent-agnostic interface UserConfig { - agent: string; // agent writer id choices: Record; // facet_id → option_id custom?: { // user-managed section mcp_servers?: McpServerEntry[]; @@ -180,7 +189,6 @@ interface UserConfig { interface LockFile { version: 1; generated_at: string; // ISO timestamp - agent: string; choices: Record; // snapshot of selections logical_config: LogicalConfig; } @@ -275,13 +283,14 @@ Pass-through: produces one `McpServerEntry` directly. Produces: one `instructions` entry. -### `tool` writer +### `installable` writer ```typescript { command: "pnpm", check: "pnpm --version" } ``` -Produces: one `CliAction` for validation/installation. +Produces: one `CliAction` for validation/installation of a CLI tool or +dependency. ## Package Structure @@ -313,7 +322,7 @@ packages/ knowledge.ts mcp-server.ts instruction.ts - tool.ts + installable.ts agents/ # agent writers claude-code.ts copilot.ts diff --git a/docs/PRD.md b/docs/PRD.md index 8f6cc64..37a01e6 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -46,6 +46,13 @@ One possible answer to a facet. Each option carries a recipe. A list of provisions that an option brings into the project. A recipe is never referenced directly by the user — it is the payload behind an option. +A single option often produces **multiple provisions targeting different +writers**. For example, the "codemcp" workflow option's recipe contains both +a `workflows` provision (registers the MCP server) and an `instruction` +provision (adds workflow guidance to the agent's instructions). This is how +one logical concept (e.g. "use codemcp workflows") materializes as both +runtime config and agent instructions. + ### Provision An atomic unit of configuration. Each provision names a **writer** and @@ -58,7 +65,7 @@ carries writer-specific config. Provision types: | `knowledge` | Invokes `@codemcp/knowledge` CLI to set up knowledge sources | | `mcp-server` | Generic MCP server entry (command + args + env) | | `instruction` | Raw instruction text for the agent | -| `tool` | CLI tool dependency to be available | +| `installable` | CLI tool or dependency to be installed | ### KnowledgeSource @@ -102,7 +109,6 @@ Records facet selections. The CLI manages most of this file via commands; users may add manual entries in the `custom` section. ```yaml -agent: claude-code # which agent writer to use choices: workflow: codemcp # facet_id: option_id testing: vitest @@ -116,6 +122,11 @@ custom: # user-managed section (not touched by CLI) - "Always use pnpm, never npm." ``` +The target agent (claude-code, copilot, kiro) is **not** stored in +`config.yaml`. It is specified at generation time (e.g. `ade setup` or +`ade install --agent claude-code`). This keeps the config agent-agnostic — +the same `config.yaml` can generate output for any supported agent. + The `custom` section is the only part users edit by hand. All other sections are maintained exclusively through CLI commands, which simplifies merge conflicts and keeps the file structure predictable. @@ -131,9 +142,11 @@ when a facet selection or catalog version is updated. ``` ade setup Interactive TUI: select agent, walk through facets, write config.yaml + config.lock.yaml + agent files. + Agent selection is a setup-time choice, not stored in config. ade install Re-resolve config.yaml → config.lock.yaml → agent files. - Non-interactive. Idempotent. + Non-interactive. Idempotent. Requires --agent flag or + detects agent from existing project files. ade add Add or change a single facet selection interactively. From d7efe39322f02674544dfeaa618013eb860ba338 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 08:32:01 +0000 Subject: [PATCH 04/60] Add facet dependencies and resolution context for provision writers Facets can declare dependsOn to reference other facets. The resolver processes facets in topological order and passes a ResolutionContext to provision writers containing the resolved options from dependent facets. This lets writers adapt output based on sibling selections (e.g. testing provisions vary based on which workflow was chosen). https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- docs/DESIGN.md | 35 ++++++++++++++++++++++++++++++----- docs/PRD.md | 7 +++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index c9f62d9..6dff410 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -50,11 +50,15 @@ ``` read config.yaml - → for each choice, look up (facet, option) in catalog - → collect all provisions from the selected option's recipe + → topologically sort facets by dependsOn + → for each facet (in dependency order): + → look up (facet, selected option) in catalog + → build ResolutionContext from already-resolved dependent facets + → collect all provisions from the selected option's recipe + → for each provision, invoke the writer with (config, context) + → each writer returns a LogicalConfig fragment + → record facet as resolved → merge custom section from config.yaml - → for each provision, invoke the corresponding provision writer - → each writer returns a LogicalConfig fragment → merge all fragments into one LogicalConfig → write config.lock.yaml (serialized LogicalConfig) ``` @@ -113,6 +117,7 @@ interface Facet { label: string; // e.g. "Workflow Framework" description: string; required: boolean; // false = skippable + dependsOn?: string[]; // facet IDs this facet depends on options: Option[]; } @@ -134,6 +139,17 @@ interface Provision { config: Record; // writer-specific } +// Passed to provision writers so they can adapt based on sibling selections. +// Only contains resolved options from facets declared in dependsOn. +interface ResolutionContext { + resolved: Record; // facet_id → resolved info +} + +interface ResolvedFacet { + optionId: string; + option: Option; +} + type ProvisionWriter = | "workflows" | "skills" @@ -235,7 +251,16 @@ Produces: ## Provision Writers Each provision writer transforms its config into LogicalConfig fragments -and/or CLI actions. +and/or CLI actions. Writers receive an optional `ResolutionContext` containing +the resolved options from dependent facets, allowing them to adapt their +output based on sibling selections. + +```typescript +type ProvisionWriterFn = ( + config: Record, + context: ResolutionContext +) => Promise>; +``` ### `workflows` writer diff --git a/docs/PRD.md b/docs/PRD.md index 37a01e6..3588bdc 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -37,6 +37,13 @@ A user-facing configuration question representing a single concern (e.g. a set of options, exactly one of which is selected. Facets can be skippable (no selection = no provisions from that facet). +Facets may **depend on other facets**. When a facet declares dependencies, +its provision writers receive the resolved options from those facets as +context. This allows provisions to adapt their output based on sibling +selections. For example, the testing facet may depend on the workflow facet +so that its skills writer knows which workflow-specific test conventions to +install. The resolver processes facets in dependency order. + ### Option One possible answer to a facet. Each option carries a recipe. From 2e4e1a933e704d07ae02bcaf1d659fb3b9a56501 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 10:51:47 +0000 Subject: [PATCH 05/60] Set up monorepo from template-typescript-monorepo and define v1 scope Project setup: - Copy template-typescript-monorepo structure (turbo, pnpm, vitest, eslint, prettier, husky pre-commit/pre-push hooks, GitHub CI workflows) - Three packages: @ade/shared (types), @ade/cli, @ade/mcp-server - Shared types implement full entity model from design doc - All packages build and tests pass PRD and design doc updates: - v1 agent writer: OpenCode (not Claude Code/Copilot/Kiro) - Multi-select facets (e.g. frameworks facet) - Dependency prompting: `ade add` prompts for unmet dependent facets - No agent auto-detection, --agent flag required - ADE-managed section delimiters for markdown files - Four v1 facets: process, conventions, documentation, frameworks - Concrete catalog example in TypeScript https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- .github/workflows/pr.yml | 38 + .github/workflows/release.yml | 124 + .gitignore | 23 + .husky/post-checkout | 2 + .husky/post-merge | 2 + .husky/pre-commit | 2 + .husky/pre-push | 8 + .lintstagedrc.js | 4 + .prettierrc.yaml | 3 + LICENSE | 21 + docs/DESIGN.md | 183 +- docs/PRD.md | 102 +- eslint.config.mjs | 38 + package.json | 73 + packages/ade-mcp-server/eslint.config.mjs | 40 + packages/ade-mcp-server/nodemon.json | 7 + packages/ade-mcp-server/package.json | 33 + packages/ade-mcp-server/src/index.spec.ts | 8 + packages/ade-mcp-server/src/index.ts | 2 + packages/ade-mcp-server/tsconfig.build.json | 8 + packages/ade-mcp-server/tsconfig.json | 10 + packages/ade-mcp-server/tsconfig.vitest.json | 7 + packages/ade-mcp-server/vitest.config.ts | 14 + packages/ade/eslint.config.mjs | 40 + packages/ade/nodemon.json | 7 + packages/ade/package.json | 36 + packages/ade/src/index.spec.ts | 8 + packages/ade/src/index.ts | 6 + packages/ade/src/version.ts | 1 + packages/ade/tsconfig.build.json | 8 + packages/ade/tsconfig.json | 10 + packages/ade/tsconfig.vitest.json | 7 + packages/ade/vitest.config.ts | 14 + packages/shared/eslint.config.mjs | 40 + packages/shared/nodemon.json | 7 + packages/shared/package.json | 30 + packages/shared/src/index.ts | 15 + packages/shared/src/types.spec.ts | 26 + packages/shared/src/types.ts | 91 + packages/shared/tsconfig.build.json | 8 + packages/shared/tsconfig.json | 7 + packages/shared/tsconfig.vitest.json | 7 + packages/shared/vitest.config.ts | 5 + pnpm-lock.yaml | 7500 ++++++++++++++++++ pnpm-workspace.yaml | 2 + tsconfig.base.json | 25 + tsconfig.build.json | 7 + tsconfig.json | 13 + turbo.json | 47 + vitest.config.ts | 12 + vitest.setup.ts | 0 51 files changed, 8655 insertions(+), 76 deletions(-) create mode 100644 .github/workflows/pr.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100755 .husky/post-checkout create mode 100755 .husky/post-merge create mode 100755 .husky/pre-commit create mode 100755 .husky/pre-push create mode 100644 .lintstagedrc.js create mode 100644 .prettierrc.yaml create mode 100644 LICENSE create mode 100644 eslint.config.mjs create mode 100644 package.json create mode 100644 packages/ade-mcp-server/eslint.config.mjs create mode 100644 packages/ade-mcp-server/nodemon.json create mode 100644 packages/ade-mcp-server/package.json create mode 100644 packages/ade-mcp-server/src/index.spec.ts create mode 100644 packages/ade-mcp-server/src/index.ts create mode 100644 packages/ade-mcp-server/tsconfig.build.json create mode 100644 packages/ade-mcp-server/tsconfig.json create mode 100644 packages/ade-mcp-server/tsconfig.vitest.json create mode 100644 packages/ade-mcp-server/vitest.config.ts create mode 100644 packages/ade/eslint.config.mjs create mode 100644 packages/ade/nodemon.json create mode 100644 packages/ade/package.json create mode 100644 packages/ade/src/index.spec.ts create mode 100644 packages/ade/src/index.ts create mode 100644 packages/ade/src/version.ts create mode 100644 packages/ade/tsconfig.build.json create mode 100644 packages/ade/tsconfig.json create mode 100644 packages/ade/tsconfig.vitest.json create mode 100644 packages/ade/vitest.config.ts create mode 100644 packages/shared/eslint.config.mjs create mode 100644 packages/shared/nodemon.json create mode 100644 packages/shared/package.json create mode 100644 packages/shared/src/index.ts create mode 100644 packages/shared/src/types.spec.ts create mode 100644 packages/shared/src/types.ts create mode 100644 packages/shared/tsconfig.build.json create mode 100644 packages/shared/tsconfig.json create mode 100644 packages/shared/tsconfig.vitest.json create mode 100644 packages/shared/vitest.config.ts create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 tsconfig.base.json create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json create mode 100644 turbo.json create mode 100644 vitest.config.ts create mode 100644 vitest.setup.ts diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..1e6e3c6 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,38 @@ +name: PR + +on: + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: "22" + cache: "pnpm" + + - name: Install dependencies + run: pnpm install + + - name: Check formatting + run: pnpm run format:check:all + + - name: Run linting + run: pnpm run lint:all + + - name: Build project + run: pnpm run build + + - name: Run tests + run: pnpm test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..cfa826a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,124 @@ +name: Release and Publish + +on: + push: + branches: [main] + +permissions: + contents: write + packages: write + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: "22" + cache: "pnpm" + + - name: Install dependencies + run: pnpm install + + - name: Check formatting + run: pnpm run format:check:all + + - name: Run linting + run: pnpm run lint:all + + - name: Build project + run: pnpm run build + + - name: Run tests + run: pnpm test + + release: + needs: test + runs-on: ubuntu-latest + steps: + - name: Generate GitHub App Token + id: generate_token + uses: tibdex/github-app-token@v1 + with: + app_id: ${{ vars.VERSION_BUMPER_APPID }} + private_key: ${{ secrets.VERSION_BUMPER_SECRET }} + + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + token: ${{ steps.generate_token.outputs.token }} + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: "22" + cache: "pnpm" + registry-url: "https://registry.npmjs.org" + + - name: Install dependencies + run: pnpm install + + - name: Build project + run: pnpm run build + + - name: Bump version and create tag + id: version + uses: mathieudutour/github-tag-action@v6.2 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + default_bump: patch + tag_prefix: v + + - name: Update package.json version + if: steps.version.outputs.new_tag + run: | + NEW_VERSION=${{ steps.version.outputs.new_version }} + + # Update root package.json + pnpm version $NEW_VERSION --no-git-tag-version + + # Update all workspace package versions + pnpm -r exec pnpm version $NEW_VERSION --no-git-tag-version + + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add package.json packages/*/package.json pnpm-lock.yaml + git commit -m "chore: bump version to $NEW_VERSION [skip ci]" || exit 0 + git push + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + + - name: Create GitHub Release + if: steps.version.outputs.new_tag + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ steps.version.outputs.new_tag }} + name: Release ${{ steps.version.outputs.new_tag }} + body: ${{ steps.version.outputs.changelog }} + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish to npm + if: steps.version.outputs.new_tag + run: | + # Publish all packages in workspace + # pnpm -r publishes packages in topological order (dependencies first) + # Root package is private and will be skipped automatically + pnpm -r publish --no-git-checks + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de62c3b --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Generated files +node_modules +dist +.DS_Store +.crowd + +# Typescript +*.tsbuildinfo +*.d.ts + +# Turborepo +.turbo + +# Jest +coverage +.coverage + +# Env files +.env* +!.env.example + +# vitepress +docs/.vitepress/cache \ No newline at end of file diff --git a/.husky/post-checkout b/.husky/post-checkout new file mode 100755 index 0000000..e6f0881 --- /dev/null +++ b/.husky/post-checkout @@ -0,0 +1,2 @@ +# Automate and ensure dependencies are installed/synced with the branch's codebase +pnpm install \ No newline at end of file diff --git a/.husky/post-merge b/.husky/post-merge new file mode 100755 index 0000000..e6f0881 --- /dev/null +++ b/.husky/post-merge @@ -0,0 +1,2 @@ +# Automate and ensure dependencies are installed/synced with the branch's codebase +pnpm install \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..e3416cf --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +# Ensure files are linted before commit +pnpm lint-staged \ No newline at end of file diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..f957683 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,8 @@ +# Ensure Typescript files have no errors before pushing +# Ensure there is no linting or formatting errors before pushing +pnpm run lint:all +pnpm run format:check:all +pnpm run typecheck:all + +# Run unit tests +pnpm test diff --git a/.lintstagedrc.js b/.lintstagedrc.js new file mode 100644 index 0000000..3a8942d --- /dev/null +++ b/.lintstagedrc.js @@ -0,0 +1,4 @@ +export default { + "*": "prettier --write --ignore-unknown", + "*.js,*.ts": "eslint --fix" +}; diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..5fa4d5d --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,3 @@ +tabWidth: 2 +trailingComma: "none" +useTabs: false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b216df2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Luke Baker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +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. diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 6dff410..edcc32f 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -52,9 +52,11 @@ read config.yaml → topologically sort facets by dependsOn → for each facet (in dependency order): - → look up (facet, selected option) in catalog + → if facet has unmet dependencies (via `ade add`), prompt for them first + → look up selected option(s) in catalog + (single-select: one option; multi-select: list of options) → build ResolutionContext from already-resolved dependent facets - → collect all provisions from the selected option's recipe + → for each selected option, collect all provisions from its recipe → for each provision, invoke the writer with (config, context) → each writer returns a LogicalConfig fragment → record facet as resolved @@ -63,19 +65,25 @@ read config.yaml → write config.lock.yaml (serialized LogicalConfig) ``` +For **multi-select facets**, each selected option's recipe is resolved +independently and their LogicalConfig fragments are merged. This means +selecting both `react` and `node-express` in the frameworks facet produces +the union of both recipes' provisions. + ### 2. Generation: LogicalConfig → agent files ``` read config.lock.yaml (or use in-memory LogicalConfig) - → select agent writer (from --agent flag or auto-detect from project files) + → select agent writer from --agent flag (no auto-detection) → writer reads current agent files (if any) for merge/update → writer produces updated agent-specific files → write files to disk ``` -The target agent is a **generation-time parameter**, not stored in -`config.yaml`. This keeps the config agent-agnostic — the same choices -can produce output for any supported agent. +The target agent is a **generation-time parameter** (`--agent` flag), +not stored in `config.yaml`. There is no auto-detection. This keeps the +config agent-agnostic — the same choices can produce output for any +supported agent. ### 3. Package API calls from provision writers @@ -113,19 +121,20 @@ interface Catalog { } interface Facet { - id: string; // e.g. "workflow" - label: string; // e.g. "Workflow Framework" + id: string; // e.g. "process" + label: string; // e.g. "Process Guidance" description: string; - required: boolean; // false = skippable - dependsOn?: string[]; // facet IDs this facet depends on + required: boolean; // false = skippable + multiSelect?: boolean; // true = user can pick multiple options + dependsOn?: string[]; // facet IDs this facet depends on options: Option[]; } interface Option { - id: string; // e.g. "codemcp" - label: string; // e.g. "CodeMCP Workflows" + id: string; // e.g. "codemcp" + label: string; // e.g. "CodeMCP Workflows" description: string; - recipe: Provision[]; // multiple provisions per option is common + recipe: Provision[]; // multiple provisions per option is common } // A recipe typically contains multiple provisions for different writers. @@ -136,13 +145,13 @@ interface Option { interface Provision { writer: ProvisionWriter; - config: Record; // writer-specific + config: Record; // writer-specific } // Passed to provision writers so they can adapt based on sibling selections. // Only contains resolved options from facets declared in dependsOn. interface ResolutionContext { - resolved: Record; // facet_id → resolved info + resolved: Record; // facet_id → resolved info } interface ResolvedFacet { @@ -170,21 +179,21 @@ interface LogicalConfig { } interface McpServerEntry { - ref: string; // unique key for dedup/update - command: string; // e.g. "npx" - args: string[]; // e.g. ["-y", "@codemcp/workflows-server"] + ref: string; // unique key for dedup/update + command: string; // e.g. "npx" + args: string[]; // e.g. ["-y", "@codemcp/workflows-server"] env: Record; } interface CliAction { command: string; args: string[]; - phase: "setup" | "install"; // when to run + phase: "setup" | "install"; // when to run } interface KnowledgeSource { - name: string; // e.g. "tanstack" - origin: string; // URL, path, or package ref + name: string; // e.g. "tanstack" + origin: string; // URL, path, or package ref description: string; } ``` @@ -194,8 +203,9 @@ interface KnowledgeSource { ```typescript // config.yaml — mostly CLI-managed, agent-agnostic interface UserConfig { - choices: Record; // facet_id → option_id - custom?: { // user-managed section + choices: Record; // single-select: string, multi-select: string[] + custom?: { + // user-managed section mcp_servers?: McpServerEntry[]; instructions?: string[]; }; @@ -204,8 +214,8 @@ interface UserConfig { // config.lock.yaml — generated, never hand-edited interface LockFile { version: 1; - generated_at: string; // ISO timestamp - choices: Record; // snapshot of selections + generated_at: string; // ISO timestamp + choices: Record; // snapshot of selections logical_config: LogicalConfig; } ``` @@ -225,28 +235,30 @@ The writer has full ownership of how to translate LogicalConfig into agent-specific files. It reads existing files when needed to perform incremental updates rather than full overwrites. -### Claude Code Writer +### OpenCode Writer (v1) + +Produces agent-specific config files for OpenCode. Exact output format TBD +based on OpenCode's config specification. + +Future agent writers: Claude Code, Copilot, Kiro. -Produces: -- `.claude/settings.json` — MCP server declarations under - `mcpServers` key. Each `McpServerEntry` maps to a server object with - `command`, `args`, and `env`. -- `CLAUDE.md` — instructions block. Writer appends/replaces a clearly - delimited ADE-managed section. +### ADE-Managed Section Delimiters -### Copilot Writer +Agent writers that produce markdown or text files (instructions, AGENTS.md, +etc.) use delimiters to mark ADE-managed sections. This allows the writer to +update its sections without clobbering user-authored content. -Produces: -- `.vscode/settings.json` — MCP server declarations under - `github.copilot.chat.mcpServers` or equivalent key. -- `copilot-instructions.md` or `.github/copilot-instructions.md` — - ADE-managed instructions section. +```markdown + -### Kiro Writer +(ADE-managed content — do not edit manually) +... -Produces: -- `.kiro/` steering files — MCP server declarations and instruction - documents per Kiro's expected format. + +``` + +For JSON config files (e.g. settings.json), the writer manages a top-level +key or object scope and merges with existing content. ## Provision Writers @@ -303,7 +315,9 @@ Pass-through: produces one `McpServerEntry` directly. ### `instruction` writer ```typescript -{ text: "Always use pnpm, never npm." } +{ + text: "Always use pnpm, never npm."; +} ``` Produces: one `instructions` entry. @@ -336,10 +350,10 @@ packages/ catalog/ index.ts # catalog registry, exports all facets facets/ - workflow.ts # workflow facet definition - testing.ts # testing facet definition - knowledge.ts # knowledge/documentation facet definition - ... + process.ts # process guidance facet + conventions.ts # conventions/skills facet + documentation.ts # documentation facet + frameworks.ts # development frameworks facet (multi-select) adapters/ writers/ # provision writers workflows.ts @@ -349,14 +363,83 @@ packages/ instruction.ts installable.ts agents/ # agent writers - claude-code.ts - copilot.ts - kiro.ts + opencode.ts # v1 agent writer tui/ prompts.ts # interactive facet selection UI utils/ ``` +## V1 Catalog (TypeScript) + +Example of how the catalog is defined in code: + +```typescript +// catalog/facets/process.ts +export const processFacet: Facet = { + id: "process", + label: "Process Guidance", + description: "How the agent receives workflow and process instructions", + required: false, + options: [ + { + id: "codemcp-workflows", + label: "CodeMCP Workflows", + description: + "Structured EPCC workflows via @codemcp/workflows-server MCP", + recipe: [ + { + writer: "workflows", + config: { package: "@codemcp/workflows-server" } + }, + { + writer: "instruction", + config: { text: "Use the workflows MCP server..." } + } + ] + }, + { + id: "native-agents-md", + label: "Native AGENTS.md", + description: "Inline EPCC instructions in AGENTS.md, no MCP dependency", + recipe: [ + { + writer: "instruction", + config: { text: "Follow the EPCC workflow..." } + } + ] + } + ] +}; + +// catalog/facets/frameworks.ts +export const frameworksFacet: Facet = { + id: "frameworks", + label: "Development Frameworks", + description: "Which tech stacks the project uses", + required: false, + multiSelect: true, + dependsOn: ["conventions"], // skills may vary by framework + options: [ + { + id: "react", + label: "React", + description: "React frontend framework", + recipe: [ + { + writer: "knowledge", + config: { name: "react", origin: "https://react.dev/reference" } + }, + { + writer: "instruction", + config: { text: "This project uses React..." } + } + ] + } + // ... vue, java-spring, node-express + ] +}; +``` + ## Decisions (formerly open questions) 1. **Catalog is TypeScript code.** No YAML catalog files. Facets, options, diff --git a/docs/PRD.md b/docs/PRD.md index 3588bdc..85b8503 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -18,7 +18,7 @@ agent-agnostic, but the last mile — getting it into an agent's config — is n ## Goal -Provide a single CLI that lets engineers declare *what* their project needs +Provide a single CLI that lets engineers declare _what_ their project needs (workflows, skills, knowledge, tools) in one place, and generates the correct agent-specific configuration for whichever coding agent they use. @@ -34,8 +34,9 @@ agent-specific configuration for whichever coding agent they use. A user-facing configuration question representing a single concern (e.g. "Which workflow framework?" or "Which testing convention?"). Each facet offers -a set of options, exactly one of which is selected. Facets can be skippable -(no selection = no provisions from that facet). +a set of options, exactly one of which is selected (or multiple, if the facet +allows multi-select). Facets can be skippable (no selection = no provisions +from that facet). Facets may **depend on other facets**. When a facet declares dependencies, its provision writers receive the resolved options from those facets as @@ -44,6 +45,9 @@ selections. For example, the testing facet may depend on the workflow facet so that its skills writer knows which workflow-specific test conventions to install. The resolver processes facets in dependency order. +When a user selects a facet whose dependencies are not yet satisfied (e.g. +via `ade add`), the CLI prompts for the missing dependent facets first. + ### Option One possible answer to a facet. Each option carries a recipe. @@ -65,14 +69,14 @@ runtime config and agent instructions. An atomic unit of configuration. Each provision names a **writer** and carries writer-specific config. Provision types: -| Writer | What it produces | -|---------------|-----------------------------------------------------------| -| `workflows` | MCP server entry for `@codemcp/workflows-server` | -| `skills` | Invokes `@codemcp/skills` to install skills | +| Writer | What it produces | +| ------------- | ------------------------------------------------------------ | +| `workflows` | MCP server entry for `@codemcp/workflows-server` | +| `skills` | Invokes `@codemcp/skills` to install skills | | `knowledge` | Invokes `@codemcp/knowledge` CLI to set up knowledge sources | -| `mcp-server` | Generic MCP server entry (command + args + env) | -| `instruction` | Raw instruction text for the agent | -| `installable` | CLI tool or dependency to be installed | +| `mcp-server` | Generic MCP server entry (command + args + env) | +| `instruction` | Raw instruction text for the agent | +| `installable` | CLI tool or dependency to be installed | ### KnowledgeSource @@ -100,13 +104,13 @@ Translates LogicalConfig into agent-specific files. ADE owns the knowledge of every supported agent's config format. When an agent changes its format, only its writer needs updating. -Supported agents (initial): +Supported agents (v1): + +| Agent | Output files | +| -------- | ---------------------------- | +| OpenCode | TBD — opencode config format | -| Agent | Output files | -|-------------|---------------------------------------------| -| Claude Code | `.claude/settings.json`, `CLAUDE.md` | -| Copilot | `.vscode/settings.json`, instructions MD | -| Kiro | `.kiro/` steering files | +Future agents: Claude Code, Copilot, Kiro. ## User-Facing Files @@ -117,10 +121,13 @@ users may add manual entries in the `custom` section. ```yaml choices: - workflow: codemcp # facet_id: option_id - testing: vitest - knowledge: tanstack -custom: # user-managed section (not touched by CLI) + process: codemcp-workflows # facet_id: option_id (single-select) + conventions: codemcp-skills + documentation: knowledge-mcp + frameworks: # multi-select facet: list of option_ids + - react + - node-express +custom: # user-managed section (not touched by CLI) mcp_servers: - ref: custom-server command: npx @@ -129,10 +136,10 @@ custom: # user-managed section (not touched by CLI) - "Always use pnpm, never npm." ``` -The target agent (claude-code, copilot, kiro) is **not** stored in -`config.yaml`. It is specified at generation time (e.g. `ade setup` or -`ade install --agent claude-code`). This keeps the config agent-agnostic — -the same `config.yaml` can generate output for any supported agent. +The target agent is **not** stored in `config.yaml`. It is specified at +generation time via `--agent` flag (e.g. `ade install --agent opencode`). +There is no auto-detection. This keeps the config agent-agnostic — the same +`config.yaml` can generate output for any supported agent. The `custom` section is the only part users edit by hand. All other sections are maintained exclusively through CLI commands, which simplifies merge @@ -152,8 +159,7 @@ ade setup Interactive TUI: select agent, walk through facets, Agent selection is a setup-time choice, not stored in config. ade install Re-resolve config.yaml → config.lock.yaml → agent files. - Non-interactive. Idempotent. Requires --agent flag or - detects agent from existing project files. + Non-interactive. Idempotent. Requires --agent flag. ade add Add or change a single facet selection interactively. @@ -170,6 +176,50 @@ patterns, and explicit references between options. The catalog is the single place that knows which provisions each option requires, and it versions naturally with the ADE package. +## V1 Catalog + +Four facets ship in v1: + +### 1. Process Guidance (`process`) + +How the agent receives workflow and process instructions. + +| Option | Description | +| ------------------- | ------------------------------------------------------------------ | +| `codemcp-workflows` | Uses `@codemcp/workflows-server` MCP for structured EPCC workflows | +| `native-agents-md` | Uses `AGENTS.md` with inline EPCC instructions (no MCP dependency) | + +### 2. Conventions (`conventions`) + +How project-specific skills and standards are delivered. + +| Option | Description | +| ---------------- | ----------------------------------------------------- | +| `codemcp-skills` | Uses `@codemcp/skills` MCP for dynamic skill delivery | +| `native-skills` | Installs skills as static files in the project | + +### 3. Documentation (`documentation`) + +How reference documentation is made available to the agent. + +| Option | Description | +| --------------- | --------------------------------------------------------- | +| `knowledge-mcp` | Uses `@codemcp/knowledge-server` MCP with managed docsets | +| `web-search` | Relies on agent's built-in web search capability | + +### 4. Development Frameworks (`frameworks`) — multi-select + +Which tech stacks the project uses. Multiple selections allowed. +Provisions install framework-specific knowledge sources, skills, and +instructions. + +| Option | Description | +| -------------- | ------------------------- | +| `react` | React frontend framework | +| `vue` | Vue.js frontend framework | +| `java-spring` | Java Spring Boot backend | +| `node-express` | Node.js Express backend | + ## Non-Goals (initial release) - Runtime agent behavior (that is the MCP servers' job). diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..296beed --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,38 @@ +import js from "@eslint/js"; +import { parser, configs } from "typescript-eslint"; +import prettier from "eslint-config-prettier"; + +export default [ + js.configs.recommended, + ...configs.recommended, + prettier, + { + // Config for TypeScript files + files: ["**/*.{ts,tsx}"], + languageOptions: { + parser, + parserOptions: { + project: [ + "./tsconfig.json", + "./docs/.vitepress/tsconfig.json", + "./packages/*/tsconfig.json" + ] + } + } + }, + { + // Config for JavaScript files - no TypeScript parsing + files: ["**/*.{js,jsx}"], + ...js.configs.recommended + }, + { + ignores: [ + "**/node_modules/**", + "**/dist/**", + ".pnpm-store/**", + "pnpm-lock.yaml", + "/packages/**", + "docs/.vitepress/cache/**" + ] + } +]; diff --git a/package.json b/package.json new file mode 100644 index 0000000..85b156a --- /dev/null +++ b/package.json @@ -0,0 +1,73 @@ +{ + "name": "ade", + "version": "0.0.0-development", + "private": true, + "description": "ADE CLI — Agentic Development Environment setup and configuration tool", + "license": "MIT", + "keywords": [ + "ade", + "agentic", + "development", + "cli", + "mcp" + ], + "repository": { + "type": "git", + "url": "https://github.com/codemcp/ade" + }, + "engines": { + "node": ">=22", + "pnpm": ">=9.0.0" + }, + "type": "module", + "scripts": { + "build": "turbo run build", + "build:clean": "turbo run clean:build", + "dev": "turbo run dev", + "docs:dev": "vitepress dev docs", + "docs:build": "vitepress build docs", + "docs:preview": "vitepress preview docs", + "test": "turbo run --parallel test", + "test:watch": "turbo run --parallel test:watch", + "lint:all": "turbo run --parallel lint", + "lint:fix:all": "turbo run --parallel lint:fix", + "format:check:all": "turbo run --parallel format", + "format:all": "turbo run --parallel format:fix", + "typecheck:all": "turbo run --parallel typecheck", + "lint": "eslint", + "lint:fix": "eslint --fix", + "format": "prettier --check .", + "format:fix": "prettier --write .", + "prepare": "husky" + }, + "devDependencies": { + "@braintree/sanitize-url": "7.1.1", + "@eslint/js": "9.18.0", + "@swc/core": "^1.10.9", + "@tsconfig/node22": "22.0.0", + "@tsconfig/strictest": "2.0.5", + "@types/eslint-config-prettier": "6.11.3", + "@types/node": "^22.10.7", + "@typescript-eslint/eslint-plugin": "^8.21.0", + "@typescript-eslint/parser": "^8.21.0", + "@vitest/coverage-v8": "^3.0.3", + "cytoscape": "3.31.0", + "cytoscape-cose-bilkent": "4.1.0", + "dayjs": "1.11.13", + "debug": "4.4.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "husky": "^9.1.7", + "lint-staged": "^15.4.1", + "nodemon": "^3.1.9", + "prettier": "^3.4.2", + "rimraf": "^6.0.1", + "turbo": "^2.3.3", + "typescript": "^5.7.3", + "typescript-eslint": "8.21.0", + "vitepress": "1.6.2", + "vitepress-plugin-mermaid": "2.0.17", + "vitest": "^3.0.3" + }, + "packageManager": "pnpm@9.14.2" +} diff --git a/packages/ade-mcp-server/eslint.config.mjs b/packages/ade-mcp-server/eslint.config.mjs new file mode 100644 index 0000000..1483555 --- /dev/null +++ b/packages/ade-mcp-server/eslint.config.mjs @@ -0,0 +1,40 @@ +import js from "@eslint/js"; +import { parser, configs } from "typescript-eslint"; +import prettier from "eslint-config-prettier"; + +export default [ + js.configs.recommended, + ...configs.recommended, + prettier, + { + // Config for TypeScript files + files: ["**/*.{ts,tsx}"], + languageOptions: { + parser, + parserOptions: { + project: ["./tsconfig.json", "./tsconfig.vitest.json"] + } + } + }, + { + // Config for JavaScript files - no TypeScript parsing + files: ["**/*.{js,jsx}"], + ...js.configs.recommended + }, + { + // Relaxed rules for test files + files: ["**/*.test.ts", "**/*.spec.ts"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": "off" + } + }, + { + ignores: [ + "**/node_modules/**", + "**/dist/**", + ".pnpm-store/**", + "pnpm-lock.yaml" + ] + } +]; diff --git a/packages/ade-mcp-server/nodemon.json b/packages/ade-mcp-server/nodemon.json new file mode 100644 index 0000000..e5d466d --- /dev/null +++ b/packages/ade-mcp-server/nodemon.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json.schemastore.org/nodemon.json", + "watch": ["./src/**", "./node_modules/@mme/**/dist/**"], + "ignoreRoot": [], + "ext": "ts,js", + "exec": "pnpm typecheck && pnpm build" +} diff --git a/packages/ade-mcp-server/package.json b/packages/ade-mcp-server/package.json new file mode 100644 index 0000000..04cbee8 --- /dev/null +++ b/packages/ade-mcp-server/package.json @@ -0,0 +1,33 @@ +{ + "name": "@ade/mcp-server", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "clean:build": "rimraf ./dist", + "dev": "nodemon", + "lint": "eslint .", + "lint:fix": "eslint --fix .", + "format": "prettier --check .", + "format:fix": "prettier --write .", + "test": "vitest --run", + "test:watch": "vitest", + "typecheck": "tsc" + }, + "dependencies": { + "@ade/shared": "workspace:*" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^8.21.0", + "@typescript-eslint/parser": "^8.21.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "prettier": "^3.4.2", + "rimraf": "^6.0.1", + "typescript": "^5.7.3" + } +} diff --git a/packages/ade-mcp-server/src/index.spec.ts b/packages/ade-mcp-server/src/index.spec.ts new file mode 100644 index 0000000..4476d36 --- /dev/null +++ b/packages/ade-mcp-server/src/index.spec.ts @@ -0,0 +1,8 @@ +import { describe, it, expect } from "vitest"; +import { name } from "./index.js"; + +describe("ade-mcp-server", () => { + it("should export a name", () => { + expect(name).toBe("@ade/mcp-server"); + }); +}); diff --git a/packages/ade-mcp-server/src/index.ts b/packages/ade-mcp-server/src/index.ts new file mode 100644 index 0000000..587d5d2 --- /dev/null +++ b/packages/ade-mcp-server/src/index.ts @@ -0,0 +1,2 @@ +// ADE MCP Server entry point +export const name = "@ade/mcp-server"; diff --git a/packages/ade-mcp-server/tsconfig.build.json b/packages/ade-mcp-server/tsconfig.build.json new file mode 100644 index 0000000..7cbd949 --- /dev/null +++ b/packages/ade-mcp-server/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["**/*.spec.ts"] +} diff --git a/packages/ade-mcp-server/tsconfig.json b/packages/ade-mcp-server/tsconfig.json new file mode 100644 index 0000000..f1452a0 --- /dev/null +++ b/packages/ade-mcp-server/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@ade/shared": ["../shared/src/index.ts"] + } + }, + "include": ["src/**/*"] +} diff --git a/packages/ade-mcp-server/tsconfig.vitest.json b/packages/ade-mcp-server/tsconfig.vitest.json new file mode 100644 index 0000000..f8add23 --- /dev/null +++ b/packages/ade-mcp-server/tsconfig.vitest.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "moduleResolution": "bundler" + }, + "include": ["vitest.config.ts"] +} diff --git a/packages/ade-mcp-server/vitest.config.ts b/packages/ade-mcp-server/vitest.config.ts new file mode 100644 index 0000000..0cec99a --- /dev/null +++ b/packages/ade-mcp-server/vitest.config.ts @@ -0,0 +1,14 @@ +// @ts-check +/** @type {import("vitest/config").defineConfig} */ + +import { resolve } from "path"; +const baseConfig = await import("../../vitest.config.js"); + +export default { + ...baseConfig.default, + resolve: { + alias: { + "@ade/shared": resolve(__dirname, "../shared/src/index.ts") + } + } +}; diff --git a/packages/ade/eslint.config.mjs b/packages/ade/eslint.config.mjs new file mode 100644 index 0000000..1483555 --- /dev/null +++ b/packages/ade/eslint.config.mjs @@ -0,0 +1,40 @@ +import js from "@eslint/js"; +import { parser, configs } from "typescript-eslint"; +import prettier from "eslint-config-prettier"; + +export default [ + js.configs.recommended, + ...configs.recommended, + prettier, + { + // Config for TypeScript files + files: ["**/*.{ts,tsx}"], + languageOptions: { + parser, + parserOptions: { + project: ["./tsconfig.json", "./tsconfig.vitest.json"] + } + } + }, + { + // Config for JavaScript files - no TypeScript parsing + files: ["**/*.{js,jsx}"], + ...js.configs.recommended + }, + { + // Relaxed rules for test files + files: ["**/*.test.ts", "**/*.spec.ts"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": "off" + } + }, + { + ignores: [ + "**/node_modules/**", + "**/dist/**", + ".pnpm-store/**", + "pnpm-lock.yaml" + ] + } +]; diff --git a/packages/ade/nodemon.json b/packages/ade/nodemon.json new file mode 100644 index 0000000..e5d466d --- /dev/null +++ b/packages/ade/nodemon.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json.schemastore.org/nodemon.json", + "watch": ["./src/**", "./node_modules/@mme/**/dist/**"], + "ignoreRoot": [], + "ext": "ts,js", + "exec": "pnpm typecheck && pnpm build" +} diff --git a/packages/ade/package.json b/packages/ade/package.json new file mode 100644 index 0000000..44e664a --- /dev/null +++ b/packages/ade/package.json @@ -0,0 +1,36 @@ +{ + "name": "@ade/cli", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "bin": { + "ade": "dist/index.js" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "clean:build": "rimraf ./dist", + "dev": "nodemon", + "lint": "eslint .", + "lint:fix": "eslint --fix .", + "format": "prettier --check .", + "format:fix": "prettier --write .", + "test": "vitest --run", + "test:watch": "vitest", + "typecheck": "tsc" + }, + "dependencies": { + "@ade/shared": "workspace:*" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^8.21.0", + "@typescript-eslint/parser": "^8.21.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "prettier": "^3.4.2", + "rimraf": "^6.0.1", + "typescript": "^5.7.3" + } +} diff --git a/packages/ade/src/index.spec.ts b/packages/ade/src/index.spec.ts new file mode 100644 index 0000000..4baf786 --- /dev/null +++ b/packages/ade/src/index.spec.ts @@ -0,0 +1,8 @@ +import { describe, it, expect } from "vitest"; +import { version } from "./version.js"; + +describe("ade cli", () => { + it("should export a version", () => { + expect(version).toBeDefined(); + }); +}); diff --git a/packages/ade/src/index.ts b/packages/ade/src/index.ts new file mode 100644 index 0000000..af2851a --- /dev/null +++ b/packages/ade/src/index.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env node + +// ADE CLI entry point +// Commands: setup, install, add, remove, status + +export { version } from "./version.js"; diff --git a/packages/ade/src/version.ts b/packages/ade/src/version.ts new file mode 100644 index 0000000..64d1c68 --- /dev/null +++ b/packages/ade/src/version.ts @@ -0,0 +1 @@ +export const version = "0.0.0-development"; diff --git a/packages/ade/tsconfig.build.json b/packages/ade/tsconfig.build.json new file mode 100644 index 0000000..7cbd949 --- /dev/null +++ b/packages/ade/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["**/*.spec.ts"] +} diff --git a/packages/ade/tsconfig.json b/packages/ade/tsconfig.json new file mode 100644 index 0000000..f1452a0 --- /dev/null +++ b/packages/ade/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@ade/shared": ["../shared/src/index.ts"] + } + }, + "include": ["src/**/*"] +} diff --git a/packages/ade/tsconfig.vitest.json b/packages/ade/tsconfig.vitest.json new file mode 100644 index 0000000..f8add23 --- /dev/null +++ b/packages/ade/tsconfig.vitest.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "moduleResolution": "bundler" + }, + "include": ["vitest.config.ts"] +} diff --git a/packages/ade/vitest.config.ts b/packages/ade/vitest.config.ts new file mode 100644 index 0000000..0cec99a --- /dev/null +++ b/packages/ade/vitest.config.ts @@ -0,0 +1,14 @@ +// @ts-check +/** @type {import("vitest/config").defineConfig} */ + +import { resolve } from "path"; +const baseConfig = await import("../../vitest.config.js"); + +export default { + ...baseConfig.default, + resolve: { + alias: { + "@ade/shared": resolve(__dirname, "../shared/src/index.ts") + } + } +}; diff --git a/packages/shared/eslint.config.mjs b/packages/shared/eslint.config.mjs new file mode 100644 index 0000000..1483555 --- /dev/null +++ b/packages/shared/eslint.config.mjs @@ -0,0 +1,40 @@ +import js from "@eslint/js"; +import { parser, configs } from "typescript-eslint"; +import prettier from "eslint-config-prettier"; + +export default [ + js.configs.recommended, + ...configs.recommended, + prettier, + { + // Config for TypeScript files + files: ["**/*.{ts,tsx}"], + languageOptions: { + parser, + parserOptions: { + project: ["./tsconfig.json", "./tsconfig.vitest.json"] + } + } + }, + { + // Config for JavaScript files - no TypeScript parsing + files: ["**/*.{js,jsx}"], + ...js.configs.recommended + }, + { + // Relaxed rules for test files + files: ["**/*.test.ts", "**/*.spec.ts"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": "off" + } + }, + { + ignores: [ + "**/node_modules/**", + "**/dist/**", + ".pnpm-store/**", + "pnpm-lock.yaml" + ] + } +]; diff --git a/packages/shared/nodemon.json b/packages/shared/nodemon.json new file mode 100644 index 0000000..e5d466d --- /dev/null +++ b/packages/shared/nodemon.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json.schemastore.org/nodemon.json", + "watch": ["./src/**", "./node_modules/@mme/**/dist/**"], + "ignoreRoot": [], + "ext": "ts,js", + "exec": "pnpm typecheck && pnpm build" +} diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..ad81667 --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,30 @@ +{ + "name": "@ade/shared", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "clean:build": "rimraf ./dist", + "dev": "nodemon", + "lint": "eslint .", + "lint:fix": "eslint --fix .", + "format": "prettier --check .", + "format:fix": "prettier --write .", + "test": "vitest --run", + "test:watch": "vitest", + "typecheck": "tsc" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^8.21.0", + "@typescript-eslint/parser": "^8.21.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "prettier": "^3.4.2", + "rimraf": "^6.0.1", + "typescript": "^5.7.3" + } +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..d7e4634 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,15 @@ +export { + type Catalog, + type Facet, + type Option, + type Provision +} from "./types.js"; +export { + type LogicalConfig, + type McpServerEntry, + type CliAction, + type KnowledgeSource +} from "./types.js"; +export { type ResolutionContext, type ResolvedFacet } from "./types.js"; +export { type UserConfig, type LockFile } from "./types.js"; +export { type ProvisionWriter } from "./types.js"; diff --git a/packages/shared/src/types.spec.ts b/packages/shared/src/types.spec.ts new file mode 100644 index 0000000..5e2ad05 --- /dev/null +++ b/packages/shared/src/types.spec.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from "vitest"; +import type { Facet, LogicalConfig } from "./types.js"; + +describe("types", () => { + it("should allow creating a facet with multi-select", () => { + const facet: Facet = { + id: "frameworks", + label: "Development Frameworks", + description: "Which tech stacks the project uses", + required: false, + multiSelect: true, + options: [] + }; + expect(facet.multiSelect).toBe(true); + }); + + it("should allow creating an empty logical config", () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [] + }; + expect(config.mcp_servers).toHaveLength(0); + }); +}); diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts new file mode 100644 index 0000000..bf81c97 --- /dev/null +++ b/packages/shared/src/types.ts @@ -0,0 +1,91 @@ +// --- Catalog types --- + +export interface Catalog { + facets: Facet[]; +} + +export interface Facet { + id: string; + label: string; + description: string; + required: boolean; + multiSelect?: boolean; + dependsOn?: string[]; + options: Option[]; +} + +export interface Option { + id: string; + label: string; + description: string; + recipe: Provision[]; +} + +export interface Provision { + writer: ProvisionWriter; + config: Record; +} + +export type ProvisionWriter = + | "workflows" + | "skills" + | "knowledge" + | "mcp-server" + | "instruction" + | "installable"; + +// --- LogicalConfig types --- + +export interface LogicalConfig { + mcp_servers: McpServerEntry[]; + instructions: string[]; + cli_actions: CliAction[]; + knowledge_sources: KnowledgeSource[]; +} + +export interface McpServerEntry { + ref: string; + command: string; + args: string[]; + env: Record; +} + +export interface CliAction { + command: string; + args: string[]; + phase: "setup" | "install"; +} + +export interface KnowledgeSource { + name: string; + origin: string; + description: string; +} + +// --- Resolution context --- + +export interface ResolutionContext { + resolved: Record; +} + +export interface ResolvedFacet { + optionId: string; + option: Option; +} + +// --- Config file types --- + +export interface UserConfig { + choices: Record; + custom?: { + mcp_servers?: McpServerEntry[]; + instructions?: string[]; + }; +} + +export interface LockFile { + version: 1; + generated_at: string; + choices: Record; + logical_config: LogicalConfig; +} diff --git a/packages/shared/tsconfig.build.json b/packages/shared/tsconfig.build.json new file mode 100644 index 0000000..7cbd949 --- /dev/null +++ b/packages/shared/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["**/*.spec.ts"] +} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..c17b099 --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "." + }, + "include": ["src/**/*"] +} diff --git a/packages/shared/tsconfig.vitest.json b/packages/shared/tsconfig.vitest.json new file mode 100644 index 0000000..f8add23 --- /dev/null +++ b/packages/shared/tsconfig.vitest.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "moduleResolution": "bundler" + }, + "include": ["vitest.config.ts"] +} diff --git a/packages/shared/vitest.config.ts b/packages/shared/vitest.config.ts new file mode 100644 index 0000000..7b62873 --- /dev/null +++ b/packages/shared/vitest.config.ts @@ -0,0 +1,5 @@ +// @ts-check +/** @type {import("vitest.config.ts").defineConfig} */ + +const baseConfig = await import("../../vitest.config.js"); +export default baseConfig.default; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..d988566 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,7500 @@ +lockfileVersion: "9.0" + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + .: + devDependencies: + "@braintree/sanitize-url": + specifier: 7.1.1 + version: 7.1.1 + "@eslint/js": + specifier: 9.18.0 + version: 9.18.0 + "@swc/core": + specifier: ^1.10.9 + version: 1.15.11 + "@tsconfig/node22": + specifier: 22.0.0 + version: 22.0.0 + "@tsconfig/strictest": + specifier: 2.0.5 + version: 2.0.5 + "@types/eslint-config-prettier": + specifier: 6.11.3 + version: 6.11.3 + "@types/node": + specifier: ^22.10.7 + version: 22.19.11 + "@typescript-eslint/eslint-plugin": + specifier: ^8.21.0 + version: 8.56.0(@typescript-eslint/parser@8.56.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) + "@typescript-eslint/parser": + specifier: ^8.21.0 + version: 8.56.0(eslint@9.39.2)(typescript@5.9.3) + "@vitest/coverage-v8": + specifier: ^3.0.3 + version: 3.2.4(vitest@3.2.4(@types/node@22.19.11)(yaml@2.8.2)) + cytoscape: + specifier: 3.31.0 + version: 3.31.0 + cytoscape-cose-bilkent: + specifier: 4.1.0 + version: 4.1.0(cytoscape@3.31.0) + dayjs: + specifier: 1.11.13 + version: 1.11.13 + debug: + specifier: 4.4.0 + version: 4.4.0(supports-color@5.5.0) + eslint: + specifier: ^9.18.0 + version: 9.39.2 + eslint-config-prettier: + specifier: ^10.0.1 + version: 10.1.8(eslint@9.39.2) + husky: + specifier: ^9.1.7 + version: 9.1.7 + lint-staged: + specifier: ^15.4.1 + version: 15.5.2 + nodemon: + specifier: ^3.1.9 + version: 3.1.11 + prettier: + specifier: ^3.4.2 + version: 3.8.1 + rimraf: + specifier: ^6.0.1 + version: 6.1.3 + turbo: + specifier: ^2.3.3 + version: 2.8.10 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + typescript-eslint: + specifier: 8.21.0 + version: 8.21.0(eslint@9.39.2)(typescript@5.9.3) + vitepress: + specifier: 1.6.2 + version: 1.6.2(@algolia/client-search@5.49.0)(@types/node@22.19.11)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.3) + vitepress-plugin-mermaid: + specifier: 2.0.17 + version: 2.0.17(mermaid@11.4.1)(vitepress@1.6.2(@algolia/client-search@5.49.0)(@types/node@22.19.11)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.3)) + vitest: + specifier: ^3.0.3 + version: 3.2.4(@types/node@22.19.11)(yaml@2.8.2) + + packages/ade: + dependencies: + "@ade/shared": + specifier: workspace:* + version: link:../shared + devDependencies: + "@typescript-eslint/eslint-plugin": + specifier: ^8.21.0 + version: 8.56.0(@typescript-eslint/parser@8.56.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) + "@typescript-eslint/parser": + specifier: ^8.21.0 + version: 8.56.0(eslint@9.39.2)(typescript@5.9.3) + eslint: + specifier: ^9.18.0 + version: 9.39.2 + eslint-config-prettier: + specifier: ^10.0.1 + version: 10.1.8(eslint@9.39.2) + prettier: + specifier: ^3.4.2 + version: 3.8.1 + rimraf: + specifier: ^6.0.1 + version: 6.1.3 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + packages/ade-mcp-server: + dependencies: + "@ade/shared": + specifier: workspace:* + version: link:../shared + devDependencies: + "@typescript-eslint/eslint-plugin": + specifier: ^8.21.0 + version: 8.56.0(@typescript-eslint/parser@8.56.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) + "@typescript-eslint/parser": + specifier: ^8.21.0 + version: 8.56.0(eslint@9.39.2)(typescript@5.9.3) + eslint: + specifier: ^9.18.0 + version: 9.39.2 + eslint-config-prettier: + specifier: ^10.0.1 + version: 10.1.8(eslint@9.39.2) + prettier: + specifier: ^3.4.2 + version: 3.8.1 + rimraf: + specifier: ^6.0.1 + version: 6.1.3 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + packages/shared: + devDependencies: + "@typescript-eslint/eslint-plugin": + specifier: ^8.21.0 + version: 8.56.0(@typescript-eslint/parser@8.56.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) + "@typescript-eslint/parser": + specifier: ^8.21.0 + version: 8.56.0(eslint@9.39.2)(typescript@5.9.3) + eslint: + specifier: ^9.18.0 + version: 9.39.2 + eslint-config-prettier: + specifier: ^10.0.1 + version: 10.1.8(eslint@9.39.2) + prettier: + specifier: ^3.4.2 + version: 3.8.1 + rimraf: + specifier: ^6.0.1 + version: 6.1.3 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + +packages: + "@algolia/abtesting@1.15.0": + resolution: + { + integrity: sha512-D1QZ8dQx5zC9yrxNao9ER9bojmmzUdL1i2P9waIRiwnZ5fI26YswcCd6VHR/Q4W3PASfVf2My4YQ2FhGGDewTQ== + } + engines: { node: ">= 14.0.0" } + + "@algolia/autocomplete-core@1.17.9": + resolution: + { + integrity: sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ== + } + + "@algolia/autocomplete-plugin-algolia-insights@1.17.9": + resolution: + { + integrity: sha512-u1fEHkCbWF92DBeB/KHeMacsjsoI0wFhjZtlCq2ddZbAehshbZST6Hs0Avkc0s+4UyBGbMDnSuXHLuvRWK5iDQ== + } + peerDependencies: + search-insights: ">= 1 < 3" + + "@algolia/autocomplete-preset-algolia@1.17.9": + resolution: + { + integrity: sha512-Na1OuceSJeg8j7ZWn5ssMu/Ax3amtOwk76u4h5J4eK2Nx2KB5qt0Z4cOapCsxot9VcEN11ADV5aUSlQF4RhGjQ== + } + peerDependencies: + "@algolia/client-search": ">= 4.9.1 < 6" + algoliasearch: ">= 4.9.1 < 6" + + "@algolia/autocomplete-shared@1.17.9": + resolution: + { + integrity: sha512-iDf05JDQ7I0b7JEA/9IektxN/80a2MZ1ToohfmNS3rfeuQnIKI3IJlIafD0xu4StbtQTghx9T3Maa97ytkXenQ== + } + peerDependencies: + "@algolia/client-search": ">= 4.9.1 < 6" + algoliasearch: ">= 4.9.1 < 6" + + "@algolia/client-abtesting@5.49.0": + resolution: + { + integrity: sha512-Q1MSRhh4Du9WeLIl1S9O+BDUMaL01uuQtmzCyEzOBtu1xBDr3wvqrTJtfEceEkA5/Nw1BdGSHa6sDT3xTAF90A== + } + engines: { node: ">= 14.0.0" } + + "@algolia/client-analytics@5.49.0": + resolution: + { + integrity: sha512-v50elhC80oyQw+8o8BwM+VvPuOo36+3W8VCfR4hsHoafQtGbMtP63U5eNcUydbVsM0py3JLoBaL1yKBK4L01sg== + } + engines: { node: ">= 14.0.0" } + + "@algolia/client-common@5.49.0": + resolution: + { + integrity: sha512-BDmVDtpDvymfLE5YQ2cPnfWJUVTDJqwpJa03Fsb7yJFJmbeKsUOGsnRkYsTbdzf0FfcvyvBB5zdcbrAIL249bg== + } + engines: { node: ">= 14.0.0" } + + "@algolia/client-insights@5.49.0": + resolution: + { + integrity: sha512-lDCXsnZDx7zQ5GzSi1EL3l07EbksjrdpMgixFRCdi2QqeBe42HIQJfPPqdWtwrAXjORRopsPx2z+gGYJP/79Uw== + } + engines: { node: ">= 14.0.0" } + + "@algolia/client-personalization@5.49.0": + resolution: + { + integrity: sha512-5k/KB+DsnesNKvMUEwTKSzExOf5zYbiPg7DVO7g1Y/+bhMb3wmxp9RFwfqwPfmoRTjptqvwhR6a0593tWVkmAw== + } + engines: { node: ">= 14.0.0" } + + "@algolia/client-query-suggestions@5.49.0": + resolution: + { + integrity: sha512-pjHNcrdjn7p3RQ5Ql1Baiwfdn9bkS+z4gqONJJP8kuZFqYP8Olthy4G7fl5bCB29UjdUj5EWlaElQKCtPluCtQ== + } + engines: { node: ">= 14.0.0" } + + "@algolia/client-search@5.49.0": + resolution: + { + integrity: sha512-uGv2P3lcviuaZy8ZOAyN60cZdhOVyjXwaDC27a1qdp3Pb5Azn+lLSJwkHU4TNRpphHmIei9HZuUxwQroujdPjw== + } + engines: { node: ">= 14.0.0" } + + "@algolia/ingestion@1.49.0": + resolution: + { + integrity: sha512-sH10mftYlmvfGbvAgTtHYbCIstmNUdiAkX//0NAyBcJRB6NnZmNsdLxdFGbE8ZqlGXzoe0zcUIau+DxKpXtqCw== + } + engines: { node: ">= 14.0.0" } + + "@algolia/monitoring@1.49.0": + resolution: + { + integrity: sha512-RqhGcVVxLpK+lA0GZKywlQIXsI704flc12nv/hOdrwiuk/Uyhxs46KLM4ngip7wutU+7t0PYZWiVayrqBPN/ZQ== + } + engines: { node: ">= 14.0.0" } + + "@algolia/recommend@5.49.0": + resolution: + { + integrity: sha512-kg8omGRvmIPhhqtUqSIpS3regFKWuoWh3WqyUhGk27N4T7q8I++8TsDYsV8vK7oBEzw706m2vUBtN5fw2fDjmw== + } + engines: { node: ">= 14.0.0" } + + "@algolia/requester-browser-xhr@5.49.0": + resolution: + { + integrity: sha512-BaZ6NTI9VdSbDcsMucdKhTuFFxv6B+3dAZZBozX12fKopYsELh7dBLfZwm8evDCIicmNjIjobi4VNnNshrCSuw== + } + engines: { node: ">= 14.0.0" } + + "@algolia/requester-fetch@5.49.0": + resolution: + { + integrity: sha512-2nxISxS5xO5DLAj6QzMImgJv6CqpZhJVkhcTFULESR/k4IpbkJTEHmViVTxw9MlrU8B5GfwHevFd7vKL3a7MXQ== + } + engines: { node: ">= 14.0.0" } + + "@algolia/requester-node-http@5.49.0": + resolution: + { + integrity: sha512-S/B94C6piEUXGpN3y5ysmNKMEqdfNVAXYY+FxivEAV5IGJjbEuLZfT8zPPZUWGw9vh6lgP80Hye2G5aVBNIa8Q== + } + engines: { node: ">= 14.0.0" } + + "@ampproject/remapping@2.3.0": + resolution: + { + integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + } + engines: { node: ">=6.0.0" } + + "@antfu/install-pkg@1.1.0": + resolution: + { + integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ== + } + + "@antfu/utils@8.1.1": + resolution: + { + integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ== + } + + "@babel/helper-string-parser@7.27.1": + resolution: + { + integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + } + engines: { node: ">=6.9.0" } + + "@babel/helper-validator-identifier@7.28.5": + resolution: + { + integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + } + engines: { node: ">=6.9.0" } + + "@babel/parser@7.29.0": + resolution: + { + integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww== + } + engines: { node: ">=6.0.0" } + hasBin: true + + "@babel/types@7.29.0": + resolution: + { + integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== + } + engines: { node: ">=6.9.0" } + + "@bcoe/v8-coverage@1.0.2": + resolution: + { + integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA== + } + engines: { node: ">=18" } + + "@braintree/sanitize-url@6.0.4": + resolution: + { + integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A== + } + + "@braintree/sanitize-url@7.1.1": + resolution: + { + integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw== + } + + "@chevrotain/cst-dts-gen@11.0.3": + resolution: + { + integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ== + } + + "@chevrotain/gast@11.0.3": + resolution: + { + integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q== + } + + "@chevrotain/regexp-to-ast@11.0.3": + resolution: + { + integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA== + } + + "@chevrotain/types@11.0.3": + resolution: + { + integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ== + } + + "@chevrotain/utils@11.0.3": + resolution: + { + integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ== + } + + "@docsearch/css@3.9.0": + resolution: + { + integrity: sha512-cQbnVbq0rrBwNAKegIac/t6a8nWoUAn8frnkLFW6YARaRmAQr5/Eoe6Ln2fqkUCZ40KpdrKbpSAmgrkviOxuWA== + } + + "@docsearch/js@3.9.0": + resolution: + { + integrity: sha512-4bKHcye6EkLgRE8ze0vcdshmEqxeiJM77M0JXjef7lrYZfSlMunrDOCqyLjiZyo1+c0BhUqA2QpFartIjuHIjw== + } + + "@docsearch/react@3.9.0": + resolution: + { + integrity: sha512-mb5FOZYZIkRQ6s/NWnM98k879vu5pscWqTLubLFBO87igYYT4VzVazh4h5o/zCvTIZgEt3PvsCOMOswOUo9yHQ== + } + peerDependencies: + "@types/react": ">= 16.8.0 < 20.0.0" + react: ">= 16.8.0 < 20.0.0" + react-dom: ">= 16.8.0 < 20.0.0" + search-insights: ">= 1 < 3" + peerDependenciesMeta: + "@types/react": + optional: true + react: + optional: true + react-dom: + optional: true + search-insights: + optional: true + + "@esbuild/aix-ppc64@0.21.5": + resolution: + { + integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + } + engines: { node: ">=12" } + cpu: [ppc64] + os: [aix] + + "@esbuild/aix-ppc64@0.27.3": + resolution: + { + integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg== + } + engines: { node: ">=18" } + cpu: [ppc64] + os: [aix] + + "@esbuild/android-arm64@0.21.5": + resolution: + { + integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + } + engines: { node: ">=12" } + cpu: [arm64] + os: [android] + + "@esbuild/android-arm64@0.27.3": + resolution: + { + integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg== + } + engines: { node: ">=18" } + cpu: [arm64] + os: [android] + + "@esbuild/android-arm@0.21.5": + resolution: + { + integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + } + engines: { node: ">=12" } + cpu: [arm] + os: [android] + + "@esbuild/android-arm@0.27.3": + resolution: + { + integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA== + } + engines: { node: ">=18" } + cpu: [arm] + os: [android] + + "@esbuild/android-x64@0.21.5": + resolution: + { + integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + } + engines: { node: ">=12" } + cpu: [x64] + os: [android] + + "@esbuild/android-x64@0.27.3": + resolution: + { + integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ== + } + engines: { node: ">=18" } + cpu: [x64] + os: [android] + + "@esbuild/darwin-arm64@0.21.5": + resolution: + { + integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + } + engines: { node: ">=12" } + cpu: [arm64] + os: [darwin] + + "@esbuild/darwin-arm64@0.27.3": + resolution: + { + integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg== + } + engines: { node: ">=18" } + cpu: [arm64] + os: [darwin] + + "@esbuild/darwin-x64@0.21.5": + resolution: + { + integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + } + engines: { node: ">=12" } + cpu: [x64] + os: [darwin] + + "@esbuild/darwin-x64@0.27.3": + resolution: + { + integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg== + } + engines: { node: ">=18" } + cpu: [x64] + os: [darwin] + + "@esbuild/freebsd-arm64@0.21.5": + resolution: + { + integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + } + engines: { node: ">=12" } + cpu: [arm64] + os: [freebsd] + + "@esbuild/freebsd-arm64@0.27.3": + resolution: + { + integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w== + } + engines: { node: ">=18" } + cpu: [arm64] + os: [freebsd] + + "@esbuild/freebsd-x64@0.21.5": + resolution: + { + integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + } + engines: { node: ">=12" } + cpu: [x64] + os: [freebsd] + + "@esbuild/freebsd-x64@0.27.3": + resolution: + { + integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA== + } + engines: { node: ">=18" } + cpu: [x64] + os: [freebsd] + + "@esbuild/linux-arm64@0.21.5": + resolution: + { + integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + } + engines: { node: ">=12" } + cpu: [arm64] + os: [linux] + + "@esbuild/linux-arm64@0.27.3": + resolution: + { + integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg== + } + engines: { node: ">=18" } + cpu: [arm64] + os: [linux] + + "@esbuild/linux-arm@0.21.5": + resolution: + { + integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + } + engines: { node: ">=12" } + cpu: [arm] + os: [linux] + + "@esbuild/linux-arm@0.27.3": + resolution: + { + integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw== + } + engines: { node: ">=18" } + cpu: [arm] + os: [linux] + + "@esbuild/linux-ia32@0.21.5": + resolution: + { + integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + } + engines: { node: ">=12" } + cpu: [ia32] + os: [linux] + + "@esbuild/linux-ia32@0.27.3": + resolution: + { + integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg== + } + engines: { node: ">=18" } + cpu: [ia32] + os: [linux] + + "@esbuild/linux-loong64@0.21.5": + resolution: + { + integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + } + engines: { node: ">=12" } + cpu: [loong64] + os: [linux] + + "@esbuild/linux-loong64@0.27.3": + resolution: + { + integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA== + } + engines: { node: ">=18" } + cpu: [loong64] + os: [linux] + + "@esbuild/linux-mips64el@0.21.5": + resolution: + { + integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + } + engines: { node: ">=12" } + cpu: [mips64el] + os: [linux] + + "@esbuild/linux-mips64el@0.27.3": + resolution: + { + integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw== + } + engines: { node: ">=18" } + cpu: [mips64el] + os: [linux] + + "@esbuild/linux-ppc64@0.21.5": + resolution: + { + integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + } + engines: { node: ">=12" } + cpu: [ppc64] + os: [linux] + + "@esbuild/linux-ppc64@0.27.3": + resolution: + { + integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA== + } + engines: { node: ">=18" } + cpu: [ppc64] + os: [linux] + + "@esbuild/linux-riscv64@0.21.5": + resolution: + { + integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + } + engines: { node: ">=12" } + cpu: [riscv64] + os: [linux] + + "@esbuild/linux-riscv64@0.27.3": + resolution: + { + integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ== + } + engines: { node: ">=18" } + cpu: [riscv64] + os: [linux] + + "@esbuild/linux-s390x@0.21.5": + resolution: + { + integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + } + engines: { node: ">=12" } + cpu: [s390x] + os: [linux] + + "@esbuild/linux-s390x@0.27.3": + resolution: + { + integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw== + } + engines: { node: ">=18" } + cpu: [s390x] + os: [linux] + + "@esbuild/linux-x64@0.21.5": + resolution: + { + integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + } + engines: { node: ">=12" } + cpu: [x64] + os: [linux] + + "@esbuild/linux-x64@0.27.3": + resolution: + { + integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA== + } + engines: { node: ">=18" } + cpu: [x64] + os: [linux] + + "@esbuild/netbsd-arm64@0.27.3": + resolution: + { + integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA== + } + engines: { node: ">=18" } + cpu: [arm64] + os: [netbsd] + + "@esbuild/netbsd-x64@0.21.5": + resolution: + { + integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + } + engines: { node: ">=12" } + cpu: [x64] + os: [netbsd] + + "@esbuild/netbsd-x64@0.27.3": + resolution: + { + integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA== + } + engines: { node: ">=18" } + cpu: [x64] + os: [netbsd] + + "@esbuild/openbsd-arm64@0.27.3": + resolution: + { + integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw== + } + engines: { node: ">=18" } + cpu: [arm64] + os: [openbsd] + + "@esbuild/openbsd-x64@0.21.5": + resolution: + { + integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + } + engines: { node: ">=12" } + cpu: [x64] + os: [openbsd] + + "@esbuild/openbsd-x64@0.27.3": + resolution: + { + integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ== + } + engines: { node: ">=18" } + cpu: [x64] + os: [openbsd] + + "@esbuild/openharmony-arm64@0.27.3": + resolution: + { + integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g== + } + engines: { node: ">=18" } + cpu: [arm64] + os: [openharmony] + + "@esbuild/sunos-x64@0.21.5": + resolution: + { + integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + } + engines: { node: ">=12" } + cpu: [x64] + os: [sunos] + + "@esbuild/sunos-x64@0.27.3": + resolution: + { + integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA== + } + engines: { node: ">=18" } + cpu: [x64] + os: [sunos] + + "@esbuild/win32-arm64@0.21.5": + resolution: + { + integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + } + engines: { node: ">=12" } + cpu: [arm64] + os: [win32] + + "@esbuild/win32-arm64@0.27.3": + resolution: + { + integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA== + } + engines: { node: ">=18" } + cpu: [arm64] + os: [win32] + + "@esbuild/win32-ia32@0.21.5": + resolution: + { + integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + } + engines: { node: ">=12" } + cpu: [ia32] + os: [win32] + + "@esbuild/win32-ia32@0.27.3": + resolution: + { + integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q== + } + engines: { node: ">=18" } + cpu: [ia32] + os: [win32] + + "@esbuild/win32-x64@0.21.5": + resolution: + { + integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== + } + engines: { node: ">=12" } + cpu: [x64] + os: [win32] + + "@esbuild/win32-x64@0.27.3": + resolution: + { + integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA== + } + engines: { node: ">=18" } + cpu: [x64] + os: [win32] + + "@eslint-community/eslint-utils@4.9.1": + resolution: + { + integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + "@eslint-community/regexpp@4.12.2": + resolution: + { + integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== + } + engines: { node: ^12.0.0 || ^14.0.0 || >=16.0.0 } + + "@eslint/config-array@0.21.1": + resolution: + { + integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/config-helpers@0.4.2": + resolution: + { + integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/core@0.17.0": + resolution: + { + integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/eslintrc@3.3.3": + resolution: + { + integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/js@9.18.0": + resolution: + { + integrity: sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/js@9.39.2": + resolution: + { + integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/object-schema@2.1.7": + resolution: + { + integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/plugin-kit@0.4.1": + resolution: + { + integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@humanfs/core@0.19.1": + resolution: + { + integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== + } + engines: { node: ">=18.18.0" } + + "@humanfs/node@0.16.7": + resolution: + { + integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ== + } + engines: { node: ">=18.18.0" } + + "@humanwhocodes/module-importer@1.0.1": + resolution: + { + integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + } + engines: { node: ">=12.22" } + + "@humanwhocodes/retry@0.4.3": + resolution: + { + integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== + } + engines: { node: ">=18.18" } + + "@iconify-json/simple-icons@1.2.71": + resolution: + { + integrity: sha512-rNoDFbq1fAYiEexBvrw613/xiUOPEu5MKVV/X8lI64AgdTzLQUUemr9f9fplxUMPoxCBP2rWzlhOEeTHk/Sf0Q== + } + + "@iconify/types@2.0.0": + resolution: + { + integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg== + } + + "@iconify/utils@2.3.0": + resolution: + { + integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA== + } + + "@isaacs/cliui@8.0.2": + resolution: + { + integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + } + engines: { node: ">=12" } + + "@istanbuljs/schema@0.1.3": + resolution: + { + integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + } + engines: { node: ">=8" } + + "@jridgewell/gen-mapping@0.3.13": + resolution: + { + integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + } + + "@jridgewell/resolve-uri@3.1.2": + resolution: + { + integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + } + engines: { node: ">=6.0.0" } + + "@jridgewell/sourcemap-codec@1.5.5": + resolution: + { + integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + } + + "@jridgewell/trace-mapping@0.3.31": + resolution: + { + integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + } + + "@mermaid-js/mermaid-mindmap@9.3.0": + resolution: + { + integrity: sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw== + } + + "@mermaid-js/parser@0.3.0": + resolution: + { + integrity: sha512-HsvL6zgE5sUPGgkIDlmAWR1HTNHz2Iy11BAWPTa4Jjabkpguy4Ze2gzfLrg6pdRuBvFwgUYyxiaNqZwrEEXepA== + } + + "@nodelib/fs.scandir@2.1.5": + resolution: + { + integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + } + engines: { node: ">= 8" } + + "@nodelib/fs.stat@2.0.5": + resolution: + { + integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + } + engines: { node: ">= 8" } + + "@nodelib/fs.walk@1.2.8": + resolution: + { + integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + } + engines: { node: ">= 8" } + + "@pkgjs/parseargs@0.11.0": + resolution: + { + integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + } + engines: { node: ">=14" } + + "@rollup/rollup-android-arm-eabi@4.57.1": + resolution: + { + integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg== + } + cpu: [arm] + os: [android] + + "@rollup/rollup-android-arm64@4.57.1": + resolution: + { + integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w== + } + cpu: [arm64] + os: [android] + + "@rollup/rollup-darwin-arm64@4.57.1": + resolution: + { + integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg== + } + cpu: [arm64] + os: [darwin] + + "@rollup/rollup-darwin-x64@4.57.1": + resolution: + { + integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w== + } + cpu: [x64] + os: [darwin] + + "@rollup/rollup-freebsd-arm64@4.57.1": + resolution: + { + integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug== + } + cpu: [arm64] + os: [freebsd] + + "@rollup/rollup-freebsd-x64@4.57.1": + resolution: + { + integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q== + } + cpu: [x64] + os: [freebsd] + + "@rollup/rollup-linux-arm-gnueabihf@4.57.1": + resolution: + { + integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw== + } + cpu: [arm] + os: [linux] + + "@rollup/rollup-linux-arm-musleabihf@4.57.1": + resolution: + { + integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw== + } + cpu: [arm] + os: [linux] + + "@rollup/rollup-linux-arm64-gnu@4.57.1": + resolution: + { + integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g== + } + cpu: [arm64] + os: [linux] + + "@rollup/rollup-linux-arm64-musl@4.57.1": + resolution: + { + integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q== + } + cpu: [arm64] + os: [linux] + + "@rollup/rollup-linux-loong64-gnu@4.57.1": + resolution: + { + integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA== + } + cpu: [loong64] + os: [linux] + + "@rollup/rollup-linux-loong64-musl@4.57.1": + resolution: + { + integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw== + } + cpu: [loong64] + os: [linux] + + "@rollup/rollup-linux-ppc64-gnu@4.57.1": + resolution: + { + integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w== + } + cpu: [ppc64] + os: [linux] + + "@rollup/rollup-linux-ppc64-musl@4.57.1": + resolution: + { + integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw== + } + cpu: [ppc64] + os: [linux] + + "@rollup/rollup-linux-riscv64-gnu@4.57.1": + resolution: + { + integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A== + } + cpu: [riscv64] + os: [linux] + + "@rollup/rollup-linux-riscv64-musl@4.57.1": + resolution: + { + integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw== + } + cpu: [riscv64] + os: [linux] + + "@rollup/rollup-linux-s390x-gnu@4.57.1": + resolution: + { + integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg== + } + cpu: [s390x] + os: [linux] + + "@rollup/rollup-linux-x64-gnu@4.57.1": + resolution: + { + integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg== + } + cpu: [x64] + os: [linux] + + "@rollup/rollup-linux-x64-musl@4.57.1": + resolution: + { + integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw== + } + cpu: [x64] + os: [linux] + + "@rollup/rollup-openbsd-x64@4.57.1": + resolution: + { + integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw== + } + cpu: [x64] + os: [openbsd] + + "@rollup/rollup-openharmony-arm64@4.57.1": + resolution: + { + integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ== + } + cpu: [arm64] + os: [openharmony] + + "@rollup/rollup-win32-arm64-msvc@4.57.1": + resolution: + { + integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ== + } + cpu: [arm64] + os: [win32] + + "@rollup/rollup-win32-ia32-msvc@4.57.1": + resolution: + { + integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew== + } + cpu: [ia32] + os: [win32] + + "@rollup/rollup-win32-x64-gnu@4.57.1": + resolution: + { + integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ== + } + cpu: [x64] + os: [win32] + + "@rollup/rollup-win32-x64-msvc@4.57.1": + resolution: + { + integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA== + } + cpu: [x64] + os: [win32] + + "@shikijs/core@2.5.0": + resolution: + { + integrity: sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg== + } + + "@shikijs/engine-javascript@2.5.0": + resolution: + { + integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w== + } + + "@shikijs/engine-oniguruma@2.5.0": + resolution: + { + integrity: sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw== + } + + "@shikijs/langs@2.5.0": + resolution: + { + integrity: sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w== + } + + "@shikijs/themes@2.5.0": + resolution: + { + integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw== + } + + "@shikijs/transformers@2.5.0": + resolution: + { + integrity: sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg== + } + + "@shikijs/types@2.5.0": + resolution: + { + integrity: sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw== + } + + "@shikijs/vscode-textmate@10.0.2": + resolution: + { + integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg== + } + + "@swc/core-darwin-arm64@1.15.11": + resolution: + { + integrity: sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg== + } + engines: { node: ">=10" } + cpu: [arm64] + os: [darwin] + + "@swc/core-darwin-x64@1.15.11": + resolution: + { + integrity: sha512-S52Gu1QtPSfBYDiejlcfp9GlN+NjTZBRRNsz8PNwBgSE626/FUf2PcllVUix7jqkoMC+t0rS8t+2/aSWlMuQtA== + } + engines: { node: ">=10" } + cpu: [x64] + os: [darwin] + + "@swc/core-linux-arm-gnueabihf@1.15.11": + resolution: + { + integrity: sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg== + } + engines: { node: ">=10" } + cpu: [arm] + os: [linux] + + "@swc/core-linux-arm64-gnu@1.15.11": + resolution: + { + integrity: sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA== + } + engines: { node: ">=10" } + cpu: [arm64] + os: [linux] + + "@swc/core-linux-arm64-musl@1.15.11": + resolution: + { + integrity: sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w== + } + engines: { node: ">=10" } + cpu: [arm64] + os: [linux] + + "@swc/core-linux-x64-gnu@1.15.11": + resolution: + { + integrity: sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ== + } + engines: { node: ">=10" } + cpu: [x64] + os: [linux] + + "@swc/core-linux-x64-musl@1.15.11": + resolution: + { + integrity: sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw== + } + engines: { node: ">=10" } + cpu: [x64] + os: [linux] + + "@swc/core-win32-arm64-msvc@1.15.11": + resolution: + { + integrity: sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA== + } + engines: { node: ">=10" } + cpu: [arm64] + os: [win32] + + "@swc/core-win32-ia32-msvc@1.15.11": + resolution: + { + integrity: sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw== + } + engines: { node: ">=10" } + cpu: [ia32] + os: [win32] + + "@swc/core-win32-x64-msvc@1.15.11": + resolution: + { + integrity: sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw== + } + engines: { node: ">=10" } + cpu: [x64] + os: [win32] + + "@swc/core@1.15.11": + resolution: + { + integrity: sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w== + } + engines: { node: ">=10" } + peerDependencies: + "@swc/helpers": ">=0.5.17" + peerDependenciesMeta: + "@swc/helpers": + optional: true + + "@swc/counter@0.1.3": + resolution: + { + integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== + } + + "@swc/types@0.1.25": + resolution: + { + integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g== + } + + "@tsconfig/node22@22.0.0": + resolution: + { + integrity: sha512-twLQ77zevtxobBOD4ToAtVmuYrpeYUh3qh+TEp+08IWhpsrIflVHqQ1F1CiPxQGL7doCdBIOOCF+1Tm833faNg== + } + + "@tsconfig/strictest@2.0.5": + resolution: + { + integrity: sha512-ec4tjL2Rr0pkZ5hww65c+EEPYwxOi4Ryv+0MtjeaSQRJyq322Q27eOQiFbuNgw2hpL4hB1/W/HBGk3VKS43osg== + } + + "@types/chai@5.2.3": + resolution: + { + integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA== + } + + "@types/d3-array@3.2.2": + resolution: + { + integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw== + } + + "@types/d3-axis@3.0.6": + resolution: + { + integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw== + } + + "@types/d3-brush@3.0.6": + resolution: + { + integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A== + } + + "@types/d3-chord@3.0.6": + resolution: + { + integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg== + } + + "@types/d3-color@3.1.3": + resolution: + { + integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== + } + + "@types/d3-contour@3.0.6": + resolution: + { + integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg== + } + + "@types/d3-delaunay@6.0.4": + resolution: + { + integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw== + } + + "@types/d3-dispatch@3.0.7": + resolution: + { + integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA== + } + + "@types/d3-drag@3.0.7": + resolution: + { + integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ== + } + + "@types/d3-dsv@3.0.7": + resolution: + { + integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g== + } + + "@types/d3-ease@3.0.2": + resolution: + { + integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== + } + + "@types/d3-fetch@3.0.7": + resolution: + { + integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA== + } + + "@types/d3-force@3.0.10": + resolution: + { + integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw== + } + + "@types/d3-format@3.0.4": + resolution: + { + integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g== + } + + "@types/d3-geo@3.1.0": + resolution: + { + integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ== + } + + "@types/d3-hierarchy@3.1.7": + resolution: + { + integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg== + } + + "@types/d3-interpolate@3.0.4": + resolution: + { + integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== + } + + "@types/d3-path@3.1.1": + resolution: + { + integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg== + } + + "@types/d3-polygon@3.0.2": + resolution: + { + integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA== + } + + "@types/d3-quadtree@3.0.6": + resolution: + { + integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg== + } + + "@types/d3-random@3.0.3": + resolution: + { + integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ== + } + + "@types/d3-scale-chromatic@3.1.0": + resolution: + { + integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ== + } + + "@types/d3-scale@4.0.9": + resolution: + { + integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw== + } + + "@types/d3-selection@3.0.11": + resolution: + { + integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w== + } + + "@types/d3-shape@3.1.8": + resolution: + { + integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w== + } + + "@types/d3-time-format@4.0.3": + resolution: + { + integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg== + } + + "@types/d3-time@3.0.4": + resolution: + { + integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== + } + + "@types/d3-timer@3.0.2": + resolution: + { + integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== + } + + "@types/d3-transition@3.0.9": + resolution: + { + integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg== + } + + "@types/d3-zoom@3.0.8": + resolution: + { + integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw== + } + + "@types/d3@7.4.3": + resolution: + { + integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww== + } + + "@types/deep-eql@4.0.2": + resolution: + { + integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw== + } + + "@types/eslint-config-prettier@6.11.3": + resolution: + { + integrity: sha512-3wXCiM8croUnhg9LdtZUJQwNcQYGWxxdOWDjPe1ykCqJFPVpzAKfs/2dgSoCtAvdPeaponcWPI7mPcGGp9dkKQ== + } + + "@types/estree@1.0.8": + resolution: + { + integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + } + + "@types/geojson@7946.0.16": + resolution: + { + integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== + } + + "@types/hast@3.0.4": + resolution: + { + integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + } + + "@types/json-schema@7.0.15": + resolution: + { + integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + } + + "@types/linkify-it@5.0.0": + resolution: + { + integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== + } + + "@types/markdown-it@14.1.2": + resolution: + { + integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== + } + + "@types/mdast@4.0.4": + resolution: + { + integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== + } + + "@types/mdurl@2.0.0": + resolution: + { + integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== + } + + "@types/node@22.19.11": + resolution: + { + integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w== + } + + "@types/trusted-types@2.0.7": + resolution: + { + integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + } + + "@types/unist@3.0.3": + resolution: + { + integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + } + + "@types/web-bluetooth@0.0.21": + resolution: + { + integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA== + } + + "@typescript-eslint/eslint-plugin@8.21.0": + resolution: + { + integrity: sha512-eTH+UOR4I7WbdQnG4Z48ebIA6Bgi7WO8HvFEneeYBxG8qCOYgTOFPSg6ek9ITIDvGjDQzWHcoWHCDO2biByNzA== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + "@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <5.8.0" + + "@typescript-eslint/eslint-plugin@8.56.0": + resolution: + { + integrity: sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + "@typescript-eslint/parser": ^8.56.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + + "@typescript-eslint/parser@8.21.0": + resolution: + { + integrity: sha512-Wy+/sdEH9kI3w9civgACwabHbKl+qIOu0uFZ9IMKzX3Jpv9og0ZBJrZExGrPpFAY7rWsXuxs5e7CPPP17A4eYA== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <5.8.0" + + "@typescript-eslint/parser@8.56.0": + resolution: + { + integrity: sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + + "@typescript-eslint/project-service@8.56.0": + resolution: + { + integrity: sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + + "@typescript-eslint/scope-manager@8.21.0": + resolution: + { + integrity: sha512-G3IBKz0/0IPfdeGRMbp+4rbjfSSdnGkXsM/pFZA8zM9t9klXDnB/YnKOBQ0GoPmoROa4bCq2NeHgJa5ydsQ4mA== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@typescript-eslint/scope-manager@8.56.0": + resolution: + { + integrity: sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@typescript-eslint/tsconfig-utils@8.56.0": + resolution: + { + integrity: sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + + "@typescript-eslint/type-utils@8.21.0": + resolution: + { + integrity: sha512-95OsL6J2BtzoBxHicoXHxgk3z+9P3BEcQTpBKriqiYzLKnM2DeSqs+sndMKdamU8FosiadQFT3D+BSL9EKnAJQ== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <5.8.0" + + "@typescript-eslint/type-utils@8.56.0": + resolution: + { + integrity: sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + + "@typescript-eslint/types@8.21.0": + resolution: + { + integrity: sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@typescript-eslint/types@8.56.0": + resolution: + { + integrity: sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@typescript-eslint/typescript-estree@8.21.0": + resolution: + { + integrity: sha512-x+aeKh/AjAArSauz0GiQZsjT8ciadNMHdkUSwBB9Z6PrKc/4knM4g3UfHml6oDJmKC88a6//cdxnO/+P2LkMcg== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + typescript: ">=4.8.4 <5.8.0" + + "@typescript-eslint/typescript-estree@8.56.0": + resolution: + { + integrity: sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + + "@typescript-eslint/utils@8.21.0": + resolution: + { + integrity: sha512-xcXBfcq0Kaxgj7dwejMbFyq7IOHgpNMtVuDveK7w3ZGwG9owKzhALVwKpTF2yrZmEwl9SWdetf3fxNzJQaVuxw== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <5.8.0" + + "@typescript-eslint/utils@8.56.0": + resolution: + { + integrity: sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + + "@typescript-eslint/visitor-keys@8.21.0": + resolution: + { + integrity: sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@typescript-eslint/visitor-keys@8.56.0": + resolution: + { + integrity: sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@ungap/structured-clone@1.3.0": + resolution: + { + integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== + } + + "@vitejs/plugin-vue@5.2.4": + resolution: + { + integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA== + } + engines: { node: ^18.0.0 || >=20.0.0 } + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + "@vitest/coverage-v8@3.2.4": + resolution: + { + integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ== + } + peerDependencies: + "@vitest/browser": 3.2.4 + vitest: 3.2.4 + peerDependenciesMeta: + "@vitest/browser": + optional: true + + "@vitest/expect@3.2.4": + resolution: + { + integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig== + } + + "@vitest/mocker@3.2.4": + resolution: + { + integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ== + } + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + "@vitest/pretty-format@3.2.4": + resolution: + { + integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA== + } + + "@vitest/runner@3.2.4": + resolution: + { + integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ== + } + + "@vitest/snapshot@3.2.4": + resolution: + { + integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ== + } + + "@vitest/spy@3.2.4": + resolution: + { + integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw== + } + + "@vitest/utils@3.2.4": + resolution: + { + integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA== + } + + "@vue/compiler-core@3.5.28": + resolution: + { + integrity: sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ== + } + + "@vue/compiler-dom@3.5.28": + resolution: + { + integrity: sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA== + } + + "@vue/compiler-sfc@3.5.28": + resolution: + { + integrity: sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g== + } + + "@vue/compiler-ssr@3.5.28": + resolution: + { + integrity: sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g== + } + + "@vue/devtools-api@7.7.9": + resolution: + { + integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g== + } + + "@vue/devtools-kit@7.7.9": + resolution: + { + integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA== + } + + "@vue/devtools-shared@7.7.9": + resolution: + { + integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA== + } + + "@vue/reactivity@3.5.28": + resolution: + { + integrity: sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw== + } + + "@vue/runtime-core@3.5.28": + resolution: + { + integrity: sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ== + } + + "@vue/runtime-dom@3.5.28": + resolution: + { + integrity: sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA== + } + + "@vue/server-renderer@3.5.28": + resolution: + { + integrity: sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg== + } + peerDependencies: + vue: 3.5.28 + + "@vue/shared@3.5.28": + resolution: + { + integrity: sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ== + } + + "@vueuse/core@12.8.2": + resolution: + { + integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ== + } + + "@vueuse/integrations@12.8.2": + resolution: + { + integrity: sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g== + } + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^5 + drauu: ^0.4 + focus-trap: ^7 + fuse.js: ^7 + idb-keyval: ^6 + jwt-decode: ^4 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^7 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + + "@vueuse/metadata@12.8.2": + resolution: + { + integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A== + } + + "@vueuse/shared@12.8.2": + resolution: + { + integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w== + } + + acorn-jsx@5.3.2: + resolution: + { + integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + } + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: + { + integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== + } + engines: { node: ">=0.4.0" } + hasBin: true + + ajv@6.12.6: + resolution: + { + integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + } + + algoliasearch@5.49.0: + resolution: + { + integrity: sha512-Tse7vx7WOvbU+kpq/L3BrBhSWTPbtMa59zIEhMn+Z2NoxZlpcCRUDCRxQ7kDFs1T3CHxDgvb+mDuILiBBpBaAA== + } + engines: { node: ">= 14.0.0" } + + ansi-escapes@7.3.0: + resolution: + { + integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg== + } + engines: { node: ">=18" } + + ansi-regex@5.0.1: + resolution: + { + integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + } + engines: { node: ">=8" } + + ansi-regex@6.2.2: + resolution: + { + integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== + } + engines: { node: ">=12" } + + ansi-styles@4.3.0: + resolution: + { + integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + } + engines: { node: ">=8" } + + ansi-styles@6.2.3: + resolution: + { + integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== + } + engines: { node: ">=12" } + + anymatch@3.1.3: + resolution: + { + integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + } + engines: { node: ">= 8" } + + argparse@2.0.1: + resolution: + { + integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + } + + assertion-error@2.0.1: + resolution: + { + integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== + } + engines: { node: ">=12" } + + ast-v8-to-istanbul@0.3.11: + resolution: + { + integrity: sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw== + } + + balanced-match@1.0.2: + resolution: + { + integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + } + + balanced-match@4.0.3: + resolution: + { + integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g== + } + engines: { node: 20 || >=22 } + + binary-extensions@2.3.0: + resolution: + { + integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + } + engines: { node: ">=8" } + + birpc@2.9.0: + resolution: + { + integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw== + } + + brace-expansion@1.1.12: + resolution: + { + integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== + } + + brace-expansion@2.0.2: + resolution: + { + integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== + } + + brace-expansion@5.0.2: + resolution: + { + integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw== + } + engines: { node: 20 || >=22 } + + braces@3.0.3: + resolution: + { + integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + } + engines: { node: ">=8" } + + cac@6.7.14: + resolution: + { + integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== + } + engines: { node: ">=8" } + + callsites@3.1.0: + resolution: + { + integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + } + engines: { node: ">=6" } + + ccount@2.0.1: + resolution: + { + integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + } + + chai@5.3.3: + resolution: + { + integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw== + } + engines: { node: ">=18" } + + chalk@4.1.2: + resolution: + { + integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + } + engines: { node: ">=10" } + + chalk@5.6.2: + resolution: + { + integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA== + } + engines: { node: ^12.17.0 || ^14.13 || >=16.0.0 } + + character-entities-html4@2.1.0: + resolution: + { + integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== + } + + character-entities-legacy@3.0.0: + resolution: + { + integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + } + + check-error@2.1.3: + resolution: + { + integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA== + } + engines: { node: ">= 16" } + + chevrotain-allstar@0.3.1: + resolution: + { + integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw== + } + peerDependencies: + chevrotain: ^11.0.0 + + chevrotain@11.0.3: + resolution: + { + integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw== + } + + chokidar@3.6.0: + resolution: + { + integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + } + engines: { node: ">= 8.10.0" } + + cli-cursor@5.0.0: + resolution: + { + integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw== + } + engines: { node: ">=18" } + + cli-truncate@4.0.0: + resolution: + { + integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA== + } + engines: { node: ">=18" } + + color-convert@2.0.1: + resolution: + { + integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + } + engines: { node: ">=7.0.0" } + + color-name@1.1.4: + resolution: + { + integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + } + + colorette@2.0.20: + resolution: + { + integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + } + + comma-separated-tokens@2.0.3: + resolution: + { + integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + } + + commander@13.1.0: + resolution: + { + integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw== + } + engines: { node: ">=18" } + + commander@7.2.0: + resolution: + { + integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + } + engines: { node: ">= 10" } + + commander@8.3.0: + resolution: + { + integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + } + engines: { node: ">= 12" } + + concat-map@0.0.1: + resolution: + { + integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + } + + confbox@0.1.8: + resolution: + { + integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w== + } + + confbox@0.2.4: + resolution: + { + integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ== + } + + copy-anything@4.0.5: + resolution: + { + integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA== + } + engines: { node: ">=18" } + + cose-base@1.0.3: + resolution: + { + integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg== + } + + cose-base@2.2.0: + resolution: + { + integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g== + } + + cross-spawn@7.0.6: + resolution: + { + integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + } + engines: { node: ">= 8" } + + csstype@3.2.3: + resolution: + { + integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== + } + + cytoscape-cose-bilkent@4.1.0: + resolution: + { + integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ== + } + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: + { + integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ== + } + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.31.0: + resolution: + { + integrity: sha512-zDGn1K/tfZwEnoGOcHc0H4XazqAAXAuDpcYw9mUnUjATjqljyCNGJv8uEvbvxGaGHaVshxMecyl6oc6uKzRfbw== + } + engines: { node: ">=0.10" } + + d3-array@2.12.1: + resolution: + { + integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== + } + + d3-array@3.2.4: + resolution: + { + integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + } + engines: { node: ">=12" } + + d3-axis@3.0.0: + resolution: + { + integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw== + } + engines: { node: ">=12" } + + d3-brush@3.0.0: + resolution: + { + integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ== + } + engines: { node: ">=12" } + + d3-chord@3.0.1: + resolution: + { + integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g== + } + engines: { node: ">=12" } + + d3-color@3.1.0: + resolution: + { + integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + } + engines: { node: ">=12" } + + d3-contour@4.0.2: + resolution: + { + integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA== + } + engines: { node: ">=12" } + + d3-delaunay@6.0.4: + resolution: + { + integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A== + } + engines: { node: ">=12" } + + d3-dispatch@3.0.1: + resolution: + { + integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== + } + engines: { node: ">=12" } + + d3-drag@3.0.0: + resolution: + { + integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== + } + engines: { node: ">=12" } + + d3-dsv@3.0.1: + resolution: + { + integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q== + } + engines: { node: ">=12" } + hasBin: true + + d3-ease@3.0.1: + resolution: + { + integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + } + engines: { node: ">=12" } + + d3-fetch@3.0.1: + resolution: + { + integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw== + } + engines: { node: ">=12" } + + d3-force@3.0.0: + resolution: + { + integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg== + } + engines: { node: ">=12" } + + d3-format@3.1.2: + resolution: + { + integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg== + } + engines: { node: ">=12" } + + d3-geo@3.1.1: + resolution: + { + integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q== + } + engines: { node: ">=12" } + + d3-hierarchy@3.1.2: + resolution: + { + integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA== + } + engines: { node: ">=12" } + + d3-interpolate@3.0.1: + resolution: + { + integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + } + engines: { node: ">=12" } + + d3-path@1.0.9: + resolution: + { + integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== + } + + d3-path@3.1.0: + resolution: + { + integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + } + engines: { node: ">=12" } + + d3-polygon@3.0.1: + resolution: + { + integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg== + } + engines: { node: ">=12" } + + d3-quadtree@3.0.1: + resolution: + { + integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw== + } + engines: { node: ">=12" } + + d3-random@3.0.1: + resolution: + { + integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ== + } + engines: { node: ">=12" } + + d3-sankey@0.12.3: + resolution: + { + integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ== + } + + d3-scale-chromatic@3.1.0: + resolution: + { + integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ== + } + engines: { node: ">=12" } + + d3-scale@4.0.2: + resolution: + { + integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== + } + engines: { node: ">=12" } + + d3-selection@3.0.0: + resolution: + { + integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== + } + engines: { node: ">=12" } + + d3-shape@1.3.7: + resolution: + { + integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw== + } + + d3-shape@3.2.0: + resolution: + { + integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + } + engines: { node: ">=12" } + + d3-time-format@4.1.0: + resolution: + { + integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + } + engines: { node: ">=12" } + + d3-time@3.1.0: + resolution: + { + integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== + } + engines: { node: ">=12" } + + d3-timer@3.0.1: + resolution: + { + integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + } + engines: { node: ">=12" } + + d3-transition@3.0.1: + resolution: + { + integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== + } + engines: { node: ">=12" } + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: + { + integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== + } + engines: { node: ">=12" } + + d3@7.9.0: + resolution: + { + integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA== + } + engines: { node: ">=12" } + + dagre-d3-es@7.0.11: + resolution: + { + integrity: sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw== + } + + dayjs@1.11.13: + resolution: + { + integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== + } + + debug@4.4.0: + resolution: + { + integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + } + engines: { node: ">=6.0" } + peerDependencies: + supports-color: "*" + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: + { + integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + } + engines: { node: ">=6.0" } + peerDependencies: + supports-color: "*" + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: + { + integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== + } + engines: { node: ">=6" } + + deep-is@0.1.4: + resolution: + { + integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + } + + delaunator@5.0.1: + resolution: + { + integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw== + } + + dequal@2.0.3: + resolution: + { + integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + } + engines: { node: ">=6" } + + devlop@1.1.0: + resolution: + { + integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + } + + dompurify@3.3.1: + resolution: + { + integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q== + } + + eastasianwidth@0.2.0: + resolution: + { + integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + } + + emoji-regex-xs@1.0.0: + resolution: + { + integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg== + } + + emoji-regex@10.6.0: + resolution: + { + integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A== + } + + emoji-regex@8.0.0: + resolution: + { + integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + } + + emoji-regex@9.2.2: + resolution: + { + integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + } + + entities@7.0.1: + resolution: + { + integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA== + } + engines: { node: ">=0.12" } + + environment@1.1.0: + resolution: + { + integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q== + } + engines: { node: ">=18" } + + es-module-lexer@1.7.0: + resolution: + { + integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== + } + + esbuild@0.21.5: + resolution: + { + integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + } + engines: { node: ">=12" } + hasBin: true + + esbuild@0.27.3: + resolution: + { + integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg== + } + engines: { node: ">=18" } + hasBin: true + + escape-string-regexp@4.0.0: + resolution: + { + integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + } + engines: { node: ">=10" } + + eslint-config-prettier@10.1.8: + resolution: + { + integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w== + } + hasBin: true + peerDependencies: + eslint: ">=7.0.0" + + eslint-scope@8.4.0: + resolution: + { + integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + eslint-visitor-keys@3.4.3: + resolution: + { + integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + + eslint-visitor-keys@4.2.1: + resolution: + { + integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + eslint-visitor-keys@5.0.0: + resolution: + { + integrity: sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q== + } + engines: { node: ^20.19.0 || ^22.13.0 || >=24 } + + eslint@9.39.2: + resolution: + { + integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + hasBin: true + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: + { + integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + esquery@1.7.0: + resolution: + { + integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== + } + engines: { node: ">=0.10" } + + esrecurse@4.3.0: + resolution: + { + integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + } + engines: { node: ">=4.0" } + + estraverse@5.3.0: + resolution: + { + integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + } + engines: { node: ">=4.0" } + + estree-walker@2.0.2: + resolution: + { + integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + } + + estree-walker@3.0.3: + resolution: + { + integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + } + + esutils@2.0.3: + resolution: + { + integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + } + engines: { node: ">=0.10.0" } + + eventemitter3@5.0.4: + resolution: + { + integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw== + } + + execa@8.0.1: + resolution: + { + integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== + } + engines: { node: ">=16.17" } + + expect-type@1.3.0: + resolution: + { + integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA== + } + engines: { node: ">=12.0.0" } + + exsolve@1.0.8: + resolution: + { + integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA== + } + + fast-deep-equal@3.1.3: + resolution: + { + integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + } + + fast-glob@3.3.3: + resolution: + { + integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + } + engines: { node: ">=8.6.0" } + + fast-json-stable-stringify@2.1.0: + resolution: + { + integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + } + + fast-levenshtein@2.0.6: + resolution: + { + integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + } + + fastq@1.20.1: + resolution: + { + integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw== + } + + fdir@6.5.0: + resolution: + { + integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + } + engines: { node: ">=12.0.0" } + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: + { + integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== + } + engines: { node: ">=16.0.0" } + + fill-range@7.1.1: + resolution: + { + integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + } + engines: { node: ">=8" } + + find-up@5.0.0: + resolution: + { + integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + } + engines: { node: ">=10" } + + flat-cache@4.0.1: + resolution: + { + integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== + } + engines: { node: ">=16" } + + flatted@3.3.3: + resolution: + { + integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== + } + + focus-trap@7.8.0: + resolution: + { + integrity: sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA== + } + + foreground-child@3.3.1: + resolution: + { + integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== + } + engines: { node: ">=14" } + + fsevents@2.3.3: + resolution: + { + integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + } + engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } + os: [darwin] + + get-east-asian-width@1.5.0: + resolution: + { + integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA== + } + engines: { node: ">=18" } + + get-stream@8.0.1: + resolution: + { + integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== + } + engines: { node: ">=16" } + + glob-parent@5.1.2: + resolution: + { + integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + } + engines: { node: ">= 6" } + + glob-parent@6.0.2: + resolution: + { + integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + } + engines: { node: ">=10.13.0" } + + glob@10.5.0: + resolution: + { + integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg== + } + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@13.0.5: + resolution: + { + integrity: sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw== + } + engines: { node: 20 || >=22 } + + globals@14.0.0: + resolution: + { + integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== + } + engines: { node: ">=18" } + + globals@15.15.0: + resolution: + { + integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg== + } + engines: { node: ">=18" } + + graphemer@1.4.0: + resolution: + { + integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + } + + hachure-fill@0.5.2: + resolution: + { + integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg== + } + + has-flag@3.0.0: + resolution: + { + integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + } + engines: { node: ">=4" } + + has-flag@4.0.0: + resolution: + { + integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + } + engines: { node: ">=8" } + + hast-util-to-html@9.0.5: + resolution: + { + integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw== + } + + hast-util-whitespace@3.0.0: + resolution: + { + integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== + } + + hookable@5.5.3: + resolution: + { + integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ== + } + + html-escaper@2.0.2: + resolution: + { + integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + } + + html-void-elements@3.0.0: + resolution: + { + integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== + } + + human-signals@5.0.0: + resolution: + { + integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== + } + engines: { node: ">=16.17.0" } + + husky@9.1.7: + resolution: + { + integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA== + } + engines: { node: ">=18" } + hasBin: true + + iconv-lite@0.6.3: + resolution: + { + integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + } + engines: { node: ">=0.10.0" } + + ignore-by-default@1.0.1: + resolution: + { + integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + } + + ignore@5.3.2: + resolution: + { + integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + } + engines: { node: ">= 4" } + + ignore@7.0.5: + resolution: + { + integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== + } + engines: { node: ">= 4" } + + import-fresh@3.3.1: + resolution: + { + integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== + } + engines: { node: ">=6" } + + imurmurhash@0.1.4: + resolution: + { + integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + } + engines: { node: ">=0.8.19" } + + internmap@1.0.1: + resolution: + { + integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== + } + + internmap@2.0.3: + resolution: + { + integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + } + engines: { node: ">=12" } + + is-binary-path@2.1.0: + resolution: + { + integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + } + engines: { node: ">=8" } + + is-extglob@2.1.1: + resolution: + { + integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + } + engines: { node: ">=0.10.0" } + + is-fullwidth-code-point@3.0.0: + resolution: + { + integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + } + engines: { node: ">=8" } + + is-fullwidth-code-point@4.0.0: + resolution: + { + integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== + } + engines: { node: ">=12" } + + is-fullwidth-code-point@5.1.0: + resolution: + { + integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ== + } + engines: { node: ">=18" } + + is-glob@4.0.3: + resolution: + { + integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + } + engines: { node: ">=0.10.0" } + + is-number@7.0.0: + resolution: + { + integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + } + engines: { node: ">=0.12.0" } + + is-stream@3.0.0: + resolution: + { + integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + + is-what@5.5.0: + resolution: + { + integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw== + } + engines: { node: ">=18" } + + isexe@2.0.0: + resolution: + { + integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + } + + istanbul-lib-coverage@3.2.2: + resolution: + { + integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== + } + engines: { node: ">=8" } + + istanbul-lib-report@3.0.1: + resolution: + { + integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== + } + engines: { node: ">=10" } + + istanbul-lib-source-maps@5.0.6: + resolution: + { + integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A== + } + engines: { node: ">=10" } + + istanbul-reports@3.2.0: + resolution: + { + integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA== + } + engines: { node: ">=8" } + + jackspeak@3.4.3: + resolution: + { + integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + } + + js-tokens@10.0.0: + resolution: + { + integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q== + } + + js-tokens@9.0.1: + resolution: + { + integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ== + } + + js-yaml@4.1.1: + resolution: + { + integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== + } + hasBin: true + + json-buffer@3.0.1: + resolution: + { + integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + } + + json-schema-traverse@0.4.1: + resolution: + { + integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + } + + json-stable-stringify-without-jsonify@1.0.1: + resolution: + { + integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + } + + katex@0.16.28: + resolution: + { + integrity: sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg== + } + hasBin: true + + keyv@4.5.4: + resolution: + { + integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + } + + khroma@2.1.0: + resolution: + { + integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw== + } + + kolorist@1.8.0: + resolution: + { + integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ== + } + + langium@3.0.0: + resolution: + { + integrity: sha512-+Ez9EoiByeoTu/2BXmEaZ06iPNXM6thWJp02KfBO/raSMyCJ4jw7AkWWa+zBCTm0+Tw1Fj9FOxdqSskyN5nAwg== + } + engines: { node: ">=16.0.0" } + + layout-base@1.0.2: + resolution: + { + integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg== + } + + layout-base@2.0.1: + resolution: + { + integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg== + } + + levn@0.4.1: + resolution: + { + integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + } + engines: { node: ">= 0.8.0" } + + lilconfig@3.1.3: + resolution: + { + integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== + } + engines: { node: ">=14" } + + lint-staged@15.5.2: + resolution: + { + integrity: sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w== + } + engines: { node: ">=18.12.0" } + hasBin: true + + listr2@8.3.3: + resolution: + { + integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ== + } + engines: { node: ">=18.0.0" } + + local-pkg@1.1.2: + resolution: + { + integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A== + } + engines: { node: ">=14" } + + locate-path@6.0.0: + resolution: + { + integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + } + engines: { node: ">=10" } + + lodash-es@4.17.21: + resolution: + { + integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + } + + lodash-es@4.17.23: + resolution: + { + integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg== + } + + lodash.merge@4.6.2: + resolution: + { + integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + } + + log-update@6.1.0: + resolution: + { + integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w== + } + engines: { node: ">=18" } + + loupe@3.2.1: + resolution: + { + integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ== + } + + lru-cache@10.4.3: + resolution: + { + integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + } + + lru-cache@11.2.6: + resolution: + { + integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ== + } + engines: { node: 20 || >=22 } + + magic-string@0.30.21: + resolution: + { + integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== + } + + magicast@0.3.5: + resolution: + { + integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ== + } + + make-dir@4.0.0: + resolution: + { + integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== + } + engines: { node: ">=10" } + + mark.js@8.11.1: + resolution: + { + integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ== + } + + marked@13.0.3: + resolution: + { + integrity: sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA== + } + engines: { node: ">= 18" } + hasBin: true + + mdast-util-to-hast@13.2.1: + resolution: + { + integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA== + } + + merge-stream@2.0.0: + resolution: + { + integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + } + + merge2@1.4.1: + resolution: + { + integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + } + engines: { node: ">= 8" } + + mermaid@11.4.1: + resolution: + { + integrity: sha512-Mb01JT/x6CKDWaxigwfZYuYmDZ6xtrNwNlidKZwkSrDaY9n90tdrJTV5Umk+wP1fZscGptmKFXHsXMDEVZ+Q6A== + } + + micromark-util-character@2.1.1: + resolution: + { + integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q== + } + + micromark-util-encode@2.0.1: + resolution: + { + integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== + } + + micromark-util-sanitize-uri@2.0.1: + resolution: + { + integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ== + } + + micromark-util-symbol@2.0.1: + resolution: + { + integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== + } + + micromark-util-types@2.0.2: + resolution: + { + integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA== + } + + micromatch@4.0.8: + resolution: + { + integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + } + engines: { node: ">=8.6" } + + mimic-fn@4.0.0: + resolution: + { + integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + } + engines: { node: ">=12" } + + mimic-function@5.0.1: + resolution: + { + integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== + } + engines: { node: ">=18" } + + minimatch@10.2.1: + resolution: + { + integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A== + } + engines: { node: 20 || >=22 } + + minimatch@3.1.2: + resolution: + { + integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + } + + minimatch@9.0.5: + resolution: + { + integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + } + engines: { node: ">=16 || 14 >=14.17" } + + minipass@7.1.3: + resolution: + { + integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A== + } + engines: { node: ">=16 || 14 >=14.17" } + + minisearch@7.2.0: + resolution: + { + integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg== + } + + mitt@3.0.1: + resolution: + { + integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== + } + + mlly@1.8.0: + resolution: + { + integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g== + } + + ms@2.1.3: + resolution: + { + integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + } + + nanoid@3.3.11: + resolution: + { + integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + } + engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } + hasBin: true + + natural-compare@1.4.0: + resolution: + { + integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + } + + nodemon@3.1.11: + resolution: + { + integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g== + } + engines: { node: ">=10" } + hasBin: true + + non-layered-tidy-tree-layout@2.0.2: + resolution: + { + integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw== + } + + normalize-path@3.0.0: + resolution: + { + integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + } + engines: { node: ">=0.10.0" } + + npm-run-path@5.3.0: + resolution: + { + integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ== + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + + onetime@6.0.0: + resolution: + { + integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + } + engines: { node: ">=12" } + + onetime@7.0.0: + resolution: + { + integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ== + } + engines: { node: ">=18" } + + oniguruma-to-es@3.1.1: + resolution: + { + integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ== + } + + optionator@0.9.4: + resolution: + { + integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + } + engines: { node: ">= 0.8.0" } + + p-limit@3.1.0: + resolution: + { + integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + } + engines: { node: ">=10" } + + p-locate@5.0.0: + resolution: + { + integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + } + engines: { node: ">=10" } + + package-json-from-dist@1.0.1: + resolution: + { + integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + } + + package-manager-detector@1.6.0: + resolution: + { + integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA== + } + + parent-module@1.0.1: + resolution: + { + integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + } + engines: { node: ">=6" } + + path-data-parser@0.1.0: + resolution: + { + integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w== + } + + path-exists@4.0.0: + resolution: + { + integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + } + engines: { node: ">=8" } + + path-key@3.1.1: + resolution: + { + integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + } + engines: { node: ">=8" } + + path-key@4.0.0: + resolution: + { + integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + } + engines: { node: ">=12" } + + path-scurry@1.11.1: + resolution: + { + integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + } + engines: { node: ">=16 || 14 >=14.18" } + + path-scurry@2.0.1: + resolution: + { + integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA== + } + engines: { node: 20 || >=22 } + + pathe@2.0.3: + resolution: + { + integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + } + + pathval@2.0.1: + resolution: + { + integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ== + } + engines: { node: ">= 14.16" } + + perfect-debounce@1.0.0: + resolution: + { + integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA== + } + + picocolors@1.1.1: + resolution: + { + integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + } + + picomatch@2.3.1: + resolution: + { + integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + } + engines: { node: ">=8.6" } + + picomatch@4.0.3: + resolution: + { + integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== + } + engines: { node: ">=12" } + + pidtree@0.6.0: + resolution: + { + integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== + } + engines: { node: ">=0.10" } + hasBin: true + + pkg-types@1.3.1: + resolution: + { + integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ== + } + + pkg-types@2.3.0: + resolution: + { + integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig== + } + + points-on-curve@0.2.0: + resolution: + { + integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A== + } + + points-on-path@0.2.1: + resolution: + { + integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g== + } + + postcss@8.5.6: + resolution: + { + integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== + } + engines: { node: ^10 || ^12 || >=14 } + + preact@10.28.4: + resolution: + { + integrity: sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ== + } + + prelude-ls@1.2.1: + resolution: + { + integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + } + engines: { node: ">= 0.8.0" } + + prettier@3.8.1: + resolution: + { + integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg== + } + engines: { node: ">=14" } + hasBin: true + + property-information@7.1.0: + resolution: + { + integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ== + } + + pstree.remy@1.1.8: + resolution: + { + integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + } + + punycode@2.3.1: + resolution: + { + integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + } + engines: { node: ">=6" } + + quansync@0.2.11: + resolution: + { + integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA== + } + + queue-microtask@1.2.3: + resolution: + { + integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + } + + readdirp@3.6.0: + resolution: + { + integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + } + engines: { node: ">=8.10.0" } + + regex-recursion@6.0.2: + resolution: + { + integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg== + } + + regex-utilities@2.3.0: + resolution: + { + integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng== + } + + regex@6.1.0: + resolution: + { + integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg== + } + + resolve-from@4.0.0: + resolution: + { + integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + } + engines: { node: ">=4" } + + restore-cursor@5.1.0: + resolution: + { + integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA== + } + engines: { node: ">=18" } + + reusify@1.1.0: + resolution: + { + integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== + } + engines: { iojs: ">=1.0.0", node: ">=0.10.0" } + + rfdc@1.4.1: + resolution: + { + integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + } + + rimraf@6.1.3: + resolution: + { + integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA== + } + engines: { node: 20 || >=22 } + hasBin: true + + robust-predicates@3.0.2: + resolution: + { + integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== + } + + rollup@4.57.1: + resolution: + { + integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A== + } + engines: { node: ">=18.0.0", npm: ">=8.0.0" } + hasBin: true + + roughjs@4.6.6: + resolution: + { + integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ== + } + + run-parallel@1.2.0: + resolution: + { + integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + } + + rw@1.3.3: + resolution: + { + integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== + } + + safer-buffer@2.1.2: + resolution: + { + integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + } + + search-insights@2.17.3: + resolution: + { + integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ== + } + + semver@7.7.4: + resolution: + { + integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== + } + engines: { node: ">=10" } + hasBin: true + + shebang-command@2.0.0: + resolution: + { + integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + } + engines: { node: ">=8" } + + shebang-regex@3.0.0: + resolution: + { + integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + } + engines: { node: ">=8" } + + shiki@2.5.0: + resolution: + { + integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ== + } + + siginfo@2.0.0: + resolution: + { + integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== + } + + signal-exit@4.1.0: + resolution: + { + integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + } + engines: { node: ">=14" } + + simple-update-notifier@2.0.0: + resolution: + { + integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== + } + engines: { node: ">=10" } + + slice-ansi@5.0.0: + resolution: + { + integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== + } + engines: { node: ">=12" } + + slice-ansi@7.1.2: + resolution: + { + integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w== + } + engines: { node: ">=18" } + + source-map-js@1.2.1: + resolution: + { + integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + } + engines: { node: ">=0.10.0" } + + space-separated-tokens@2.0.2: + resolution: + { + integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + } + + speakingurl@14.0.1: + resolution: + { + integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ== + } + engines: { node: ">=0.10.0" } + + stackback@0.0.2: + resolution: + { + integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== + } + + std-env@3.10.0: + resolution: + { + integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg== + } + + string-argv@0.3.2: + resolution: + { + integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== + } + engines: { node: ">=0.6.19" } + + string-width@4.2.3: + resolution: + { + integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + } + engines: { node: ">=8" } + + string-width@5.1.2: + resolution: + { + integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + } + engines: { node: ">=12" } + + string-width@7.2.0: + resolution: + { + integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== + } + engines: { node: ">=18" } + + stringify-entities@4.0.4: + resolution: + { + integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== + } + + strip-ansi@6.0.1: + resolution: + { + integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + } + engines: { node: ">=8" } + + strip-ansi@7.1.2: + resolution: + { + integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA== + } + engines: { node: ">=12" } + + strip-final-newline@3.0.0: + resolution: + { + integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + } + engines: { node: ">=12" } + + strip-json-comments@3.1.1: + resolution: + { + integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + } + engines: { node: ">=8" } + + strip-literal@3.1.0: + resolution: + { + integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg== + } + + stylis@4.3.6: + resolution: + { + integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ== + } + + superjson@2.2.6: + resolution: + { + integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA== + } + engines: { node: ">=16" } + + supports-color@5.5.0: + resolution: + { + integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + } + engines: { node: ">=4" } + + supports-color@7.2.0: + resolution: + { + integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + } + engines: { node: ">=8" } + + tabbable@6.4.0: + resolution: + { + integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg== + } + + test-exclude@7.0.1: + resolution: + { + integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg== + } + engines: { node: ">=18" } + + tinybench@2.9.0: + resolution: + { + integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== + } + + tinyexec@0.3.2: + resolution: + { + integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== + } + + tinyexec@1.0.2: + resolution: + { + integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg== + } + engines: { node: ">=18" } + + tinyglobby@0.2.15: + resolution: + { + integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== + } + engines: { node: ">=12.0.0" } + + tinypool@1.1.1: + resolution: + { + integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg== + } + engines: { node: ^18.0.0 || >=20.0.0 } + + tinyrainbow@2.0.0: + resolution: + { + integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw== + } + engines: { node: ">=14.0.0" } + + tinyspy@4.0.4: + resolution: + { + integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q== + } + engines: { node: ">=14.0.0" } + + to-regex-range@5.0.1: + resolution: + { + integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + } + engines: { node: ">=8.0" } + + touch@3.1.1: + resolution: + { + integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== + } + hasBin: true + + trim-lines@3.0.1: + resolution: + { + integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== + } + + ts-api-utils@2.4.0: + resolution: + { + integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA== + } + engines: { node: ">=18.12" } + peerDependencies: + typescript: ">=4.8.4" + + ts-dedent@2.2.0: + resolution: + { + integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ== + } + engines: { node: ">=6.10" } + + turbo-darwin-64@2.8.10: + resolution: + { + integrity: sha512-A03fXh+B7S8mL3PbdhTd+0UsaGrhfyPkODvzBDpKRY7bbeac4MDFpJ7I+Slf2oSkCEeSvHKR7Z4U71uKRUfX7g== + } + cpu: [x64] + os: [darwin] + + turbo-darwin-arm64@2.8.10: + resolution: + { + integrity: sha512-sidzowgWL3s5xCHLeqwC9M3s9M0i16W1nuQF3Mc7fPHpZ+YPohvcbVFBB2uoRRHYZg6yBnwD4gyUHKTeXfwtXA== + } + cpu: [arm64] + os: [darwin] + + turbo-linux-64@2.8.10: + resolution: + { + integrity: sha512-YK9vcpL3TVtqonB021XwgaQhY9hJJbKKUhLv16osxV0HkcQASQWUqR56yMge7puh6nxU67rQlTq1b7ksR1T3KA== + } + cpu: [x64] + os: [linux] + + turbo-linux-arm64@2.8.10: + resolution: + { + integrity: sha512-3+j2tL0sG95iBJTm+6J8/45JsETQABPqtFyYjVjBbi6eVGdtNTiBmHNKrbvXRlQ3ZbUG75bKLaSSDHSEEN+btQ== + } + cpu: [arm64] + os: [linux] + + turbo-windows-64@2.8.10: + resolution: + { + integrity: sha512-hdeF5qmVY/NFgiucf8FW0CWJWtyT2QPm5mIsX0W1DXAVzqKVXGq+Zf+dg4EUngAFKjDzoBeN6ec2Fhajwfztkw== + } + cpu: [x64] + os: [win32] + + turbo-windows-arm64@2.8.10: + resolution: + { + integrity: sha512-QGdr/Q8LWmj+ITMkSvfiz2glf0d7JG0oXVzGL3jxkGqiBI1zXFj20oqVY0qWi+112LO9SVrYdpHS0E/oGFrMbQ== + } + cpu: [arm64] + os: [win32] + + turbo@2.8.10: + resolution: + { + integrity: sha512-OxbzDES66+x7nnKGg2MwBA1ypVsZoDTLHpeaP4giyiHSixbsiTaMyeJqbEyvBdp5Cm28fc+8GG6RdQtic0ijwQ== + } + hasBin: true + + type-check@0.4.0: + resolution: + { + integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + } + engines: { node: ">= 0.8.0" } + + typescript-eslint@8.21.0: + resolution: + { + integrity: sha512-txEKYY4XMKwPXxNkN8+AxAdX6iIJAPiJbHE/FpQccs/sxw8Lf26kqwC3cn0xkHlW8kEbLhkhCsjWuMveaY9Rxw== + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <5.8.0" + + typescript@5.9.3: + resolution: + { + integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + } + engines: { node: ">=14.17" } + hasBin: true + + ufo@1.6.3: + resolution: + { + integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q== + } + + undefsafe@2.0.5: + resolution: + { + integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + } + + undici-types@6.21.0: + resolution: + { + integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + } + + unist-util-is@6.0.1: + resolution: + { + integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g== + } + + unist-util-position@5.0.0: + resolution: + { + integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== + } + + unist-util-stringify-position@4.0.0: + resolution: + { + integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== + } + + unist-util-visit-parents@6.0.2: + resolution: + { + integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ== + } + + unist-util-visit@5.1.0: + resolution: + { + integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg== + } + + uri-js@4.4.1: + resolution: + { + integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + } + + uuid@9.0.1: + resolution: + { + integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + } + hasBin: true + + vfile-message@4.0.3: + resolution: + { + integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw== + } + + vfile@6.0.3: + resolution: + { + integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== + } + + vite-node@3.2.4: + resolution: + { + integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg== + } + engines: { node: ^18.0.0 || ^20.0.0 || >=22.0.0 } + hasBin: true + + vite@5.4.21: + resolution: + { + integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw== + } + engines: { node: ^18.0.0 || >=20.0.0 } + hasBin: true + peerDependencies: + "@types/node": ^18.0.0 || >=20.0.0 + less: "*" + lightningcss: ^1.21.0 + sass: "*" + sass-embedded: "*" + stylus: "*" + sugarss: "*" + terser: ^5.4.0 + peerDependenciesMeta: + "@types/node": + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vite@7.3.1: + resolution: + { + integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA== + } + engines: { node: ^20.19.0 || >=22.12.0 } + hasBin: true + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.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" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + "@types/node": + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitepress-plugin-mermaid@2.0.17: + resolution: + { + integrity: sha512-IUzYpwf61GC6k0XzfmAmNrLvMi9TRrVRMsUyCA8KNXhg/mQ1VqWnO0/tBVPiX5UoKF1mDUwqn5QV4qAJl6JnUg== + } + peerDependencies: + mermaid: 10 || 11 + vitepress: ^1.0.0 || ^1.0.0-alpha + + vitepress@1.6.2: + resolution: + { + integrity: sha512-pKAgner8wqetwyL6VyjhJnrw/Kwc8hNlwaS/efUlEBhQsRiCvjIsKqjWyjyUMa6u39ktMle16nYAUOcZ6MhV6Q== + } + hasBin: true + peerDependencies: + markdown-it-mathjax3: ^4 + postcss: ^8 + peerDependenciesMeta: + markdown-it-mathjax3: + optional: true + postcss: + optional: true + + vitest@3.2.4: + resolution: + { + integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A== + } + engines: { node: ^18.0.0 || ^20.0.0 || >=22.0.0 } + hasBin: true + 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 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@types/debug": + optional: true + "@types/node": + optional: true + "@vitest/browser": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-jsonrpc@8.2.0: + resolution: + { + integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== + } + engines: { node: ">=14.0.0" } + + vscode-languageserver-protocol@3.17.5: + resolution: + { + integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== + } + + vscode-languageserver-textdocument@1.0.12: + resolution: + { + integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA== + } + + vscode-languageserver-types@3.17.5: + resolution: + { + integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== + } + + vscode-languageserver@9.0.1: + resolution: + { + integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g== + } + hasBin: true + + vscode-uri@3.0.8: + resolution: + { + integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== + } + + vue@3.5.28: + resolution: + { + integrity: sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg== + } + peerDependencies: + typescript: "*" + peerDependenciesMeta: + typescript: + optional: true + + which@2.0.2: + resolution: + { + integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + } + engines: { node: ">= 8" } + hasBin: true + + why-is-node-running@2.3.0: + resolution: + { + integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== + } + engines: { node: ">=8" } + hasBin: true + + word-wrap@1.2.5: + resolution: + { + integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + } + engines: { node: ">=0.10.0" } + + wrap-ansi@7.0.0: + resolution: + { + integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + } + engines: { node: ">=10" } + + wrap-ansi@8.1.0: + resolution: + { + integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + } + engines: { node: ">=12" } + + wrap-ansi@9.0.2: + resolution: + { + integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww== + } + engines: { node: ">=18" } + + yaml@2.8.2: + resolution: + { + integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A== + } + engines: { node: ">= 14.6" } + hasBin: true + + yocto-queue@0.1.0: + resolution: + { + integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + } + engines: { node: ">=10" } + + zwitch@2.0.4: + resolution: + { + integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== + } + +snapshots: + "@algolia/abtesting@1.15.0": + dependencies: + "@algolia/client-common": 5.49.0 + "@algolia/requester-browser-xhr": 5.49.0 + "@algolia/requester-fetch": 5.49.0 + "@algolia/requester-node-http": 5.49.0 + + "@algolia/autocomplete-core@1.17.9(@algolia/client-search@5.49.0)(algoliasearch@5.49.0)(search-insights@2.17.3)": + dependencies: + "@algolia/autocomplete-plugin-algolia-insights": 1.17.9(@algolia/client-search@5.49.0)(algoliasearch@5.49.0)(search-insights@2.17.3) + "@algolia/autocomplete-shared": 1.17.9(@algolia/client-search@5.49.0)(algoliasearch@5.49.0) + transitivePeerDependencies: + - "@algolia/client-search" + - algoliasearch + - search-insights + + "@algolia/autocomplete-plugin-algolia-insights@1.17.9(@algolia/client-search@5.49.0)(algoliasearch@5.49.0)(search-insights@2.17.3)": + dependencies: + "@algolia/autocomplete-shared": 1.17.9(@algolia/client-search@5.49.0)(algoliasearch@5.49.0) + search-insights: 2.17.3 + transitivePeerDependencies: + - "@algolia/client-search" + - algoliasearch + + "@algolia/autocomplete-preset-algolia@1.17.9(@algolia/client-search@5.49.0)(algoliasearch@5.49.0)": + dependencies: + "@algolia/autocomplete-shared": 1.17.9(@algolia/client-search@5.49.0)(algoliasearch@5.49.0) + "@algolia/client-search": 5.49.0 + algoliasearch: 5.49.0 + + "@algolia/autocomplete-shared@1.17.9(@algolia/client-search@5.49.0)(algoliasearch@5.49.0)": + dependencies: + "@algolia/client-search": 5.49.0 + algoliasearch: 5.49.0 + + "@algolia/client-abtesting@5.49.0": + dependencies: + "@algolia/client-common": 5.49.0 + "@algolia/requester-browser-xhr": 5.49.0 + "@algolia/requester-fetch": 5.49.0 + "@algolia/requester-node-http": 5.49.0 + + "@algolia/client-analytics@5.49.0": + dependencies: + "@algolia/client-common": 5.49.0 + "@algolia/requester-browser-xhr": 5.49.0 + "@algolia/requester-fetch": 5.49.0 + "@algolia/requester-node-http": 5.49.0 + + "@algolia/client-common@5.49.0": {} + + "@algolia/client-insights@5.49.0": + dependencies: + "@algolia/client-common": 5.49.0 + "@algolia/requester-browser-xhr": 5.49.0 + "@algolia/requester-fetch": 5.49.0 + "@algolia/requester-node-http": 5.49.0 + + "@algolia/client-personalization@5.49.0": + dependencies: + "@algolia/client-common": 5.49.0 + "@algolia/requester-browser-xhr": 5.49.0 + "@algolia/requester-fetch": 5.49.0 + "@algolia/requester-node-http": 5.49.0 + + "@algolia/client-query-suggestions@5.49.0": + dependencies: + "@algolia/client-common": 5.49.0 + "@algolia/requester-browser-xhr": 5.49.0 + "@algolia/requester-fetch": 5.49.0 + "@algolia/requester-node-http": 5.49.0 + + "@algolia/client-search@5.49.0": + dependencies: + "@algolia/client-common": 5.49.0 + "@algolia/requester-browser-xhr": 5.49.0 + "@algolia/requester-fetch": 5.49.0 + "@algolia/requester-node-http": 5.49.0 + + "@algolia/ingestion@1.49.0": + dependencies: + "@algolia/client-common": 5.49.0 + "@algolia/requester-browser-xhr": 5.49.0 + "@algolia/requester-fetch": 5.49.0 + "@algolia/requester-node-http": 5.49.0 + + "@algolia/monitoring@1.49.0": + dependencies: + "@algolia/client-common": 5.49.0 + "@algolia/requester-browser-xhr": 5.49.0 + "@algolia/requester-fetch": 5.49.0 + "@algolia/requester-node-http": 5.49.0 + + "@algolia/recommend@5.49.0": + dependencies: + "@algolia/client-common": 5.49.0 + "@algolia/requester-browser-xhr": 5.49.0 + "@algolia/requester-fetch": 5.49.0 + "@algolia/requester-node-http": 5.49.0 + + "@algolia/requester-browser-xhr@5.49.0": + dependencies: + "@algolia/client-common": 5.49.0 + + "@algolia/requester-fetch@5.49.0": + dependencies: + "@algolia/client-common": 5.49.0 + + "@algolia/requester-node-http@5.49.0": + dependencies: + "@algolia/client-common": 5.49.0 + + "@ampproject/remapping@2.3.0": + dependencies: + "@jridgewell/gen-mapping": 0.3.13 + "@jridgewell/trace-mapping": 0.3.31 + + "@antfu/install-pkg@1.1.0": + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.0.2 + + "@antfu/utils@8.1.1": {} + + "@babel/helper-string-parser@7.27.1": {} + + "@babel/helper-validator-identifier@7.28.5": {} + + "@babel/parser@7.29.0": + dependencies: + "@babel/types": 7.29.0 + + "@babel/types@7.29.0": + dependencies: + "@babel/helper-string-parser": 7.27.1 + "@babel/helper-validator-identifier": 7.28.5 + + "@bcoe/v8-coverage@1.0.2": {} + + "@braintree/sanitize-url@6.0.4": + optional: true + + "@braintree/sanitize-url@7.1.1": {} + + "@chevrotain/cst-dts-gen@11.0.3": + dependencies: + "@chevrotain/gast": 11.0.3 + "@chevrotain/types": 11.0.3 + lodash-es: 4.17.21 + + "@chevrotain/gast@11.0.3": + dependencies: + "@chevrotain/types": 11.0.3 + lodash-es: 4.17.21 + + "@chevrotain/regexp-to-ast@11.0.3": {} + + "@chevrotain/types@11.0.3": {} + + "@chevrotain/utils@11.0.3": {} + + "@docsearch/css@3.9.0": {} + + "@docsearch/js@3.9.0(@algolia/client-search@5.49.0)(search-insights@2.17.3)": + dependencies: + "@docsearch/react": 3.9.0(@algolia/client-search@5.49.0)(search-insights@2.17.3) + preact: 10.28.4 + transitivePeerDependencies: + - "@algolia/client-search" + - "@types/react" + - react + - react-dom + - search-insights + + "@docsearch/react@3.9.0(@algolia/client-search@5.49.0)(search-insights@2.17.3)": + dependencies: + "@algolia/autocomplete-core": 1.17.9(@algolia/client-search@5.49.0)(algoliasearch@5.49.0)(search-insights@2.17.3) + "@algolia/autocomplete-preset-algolia": 1.17.9(@algolia/client-search@5.49.0)(algoliasearch@5.49.0) + "@docsearch/css": 3.9.0 + algoliasearch: 5.49.0 + optionalDependencies: + search-insights: 2.17.3 + transitivePeerDependencies: + - "@algolia/client-search" + + "@esbuild/aix-ppc64@0.21.5": + optional: true + + "@esbuild/aix-ppc64@0.27.3": + optional: true + + "@esbuild/android-arm64@0.21.5": + optional: true + + "@esbuild/android-arm64@0.27.3": + optional: true + + "@esbuild/android-arm@0.21.5": + optional: true + + "@esbuild/android-arm@0.27.3": + optional: true + + "@esbuild/android-x64@0.21.5": + optional: true + + "@esbuild/android-x64@0.27.3": + optional: true + + "@esbuild/darwin-arm64@0.21.5": + optional: true + + "@esbuild/darwin-arm64@0.27.3": + optional: true + + "@esbuild/darwin-x64@0.21.5": + optional: true + + "@esbuild/darwin-x64@0.27.3": + optional: true + + "@esbuild/freebsd-arm64@0.21.5": + optional: true + + "@esbuild/freebsd-arm64@0.27.3": + optional: true + + "@esbuild/freebsd-x64@0.21.5": + optional: true + + "@esbuild/freebsd-x64@0.27.3": + optional: true + + "@esbuild/linux-arm64@0.21.5": + optional: true + + "@esbuild/linux-arm64@0.27.3": + optional: true + + "@esbuild/linux-arm@0.21.5": + optional: true + + "@esbuild/linux-arm@0.27.3": + optional: true + + "@esbuild/linux-ia32@0.21.5": + optional: true + + "@esbuild/linux-ia32@0.27.3": + optional: true + + "@esbuild/linux-loong64@0.21.5": + optional: true + + "@esbuild/linux-loong64@0.27.3": + optional: true + + "@esbuild/linux-mips64el@0.21.5": + optional: true + + "@esbuild/linux-mips64el@0.27.3": + optional: true + + "@esbuild/linux-ppc64@0.21.5": + optional: true + + "@esbuild/linux-ppc64@0.27.3": + optional: true + + "@esbuild/linux-riscv64@0.21.5": + optional: true + + "@esbuild/linux-riscv64@0.27.3": + optional: true + + "@esbuild/linux-s390x@0.21.5": + optional: true + + "@esbuild/linux-s390x@0.27.3": + optional: true + + "@esbuild/linux-x64@0.21.5": + optional: true + + "@esbuild/linux-x64@0.27.3": + optional: true + + "@esbuild/netbsd-arm64@0.27.3": + optional: true + + "@esbuild/netbsd-x64@0.21.5": + optional: true + + "@esbuild/netbsd-x64@0.27.3": + optional: true + + "@esbuild/openbsd-arm64@0.27.3": + optional: true + + "@esbuild/openbsd-x64@0.21.5": + optional: true + + "@esbuild/openbsd-x64@0.27.3": + optional: true + + "@esbuild/openharmony-arm64@0.27.3": + optional: true + + "@esbuild/sunos-x64@0.21.5": + optional: true + + "@esbuild/sunos-x64@0.27.3": + optional: true + + "@esbuild/win32-arm64@0.21.5": + optional: true + + "@esbuild/win32-arm64@0.27.3": + optional: true + + "@esbuild/win32-ia32@0.21.5": + optional: true + + "@esbuild/win32-ia32@0.27.3": + optional: true + + "@esbuild/win32-x64@0.21.5": + optional: true + + "@esbuild/win32-x64@0.27.3": + optional: true + + "@eslint-community/eslint-utils@4.9.1(eslint@9.39.2)": + dependencies: + eslint: 9.39.2 + eslint-visitor-keys: 3.4.3 + + "@eslint-community/regexpp@4.12.2": {} + + "@eslint/config-array@0.21.1": + dependencies: + "@eslint/object-schema": 2.1.7 + debug: 4.4.0(supports-color@5.5.0) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + "@eslint/config-helpers@0.4.2": + dependencies: + "@eslint/core": 0.17.0 + + "@eslint/core@0.17.0": + dependencies: + "@types/json-schema": 7.0.15 + + "@eslint/eslintrc@3.3.3": + dependencies: + ajv: 6.12.6 + debug: 4.4.0(supports-color@5.5.0) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + "@eslint/js@9.18.0": {} + + "@eslint/js@9.39.2": {} + + "@eslint/object-schema@2.1.7": {} + + "@eslint/plugin-kit@0.4.1": + dependencies: + "@eslint/core": 0.17.0 + levn: 0.4.1 + + "@humanfs/core@0.19.1": {} + + "@humanfs/node@0.16.7": + dependencies: + "@humanfs/core": 0.19.1 + "@humanwhocodes/retry": 0.4.3 + + "@humanwhocodes/module-importer@1.0.1": {} + + "@humanwhocodes/retry@0.4.3": {} + + "@iconify-json/simple-icons@1.2.71": + dependencies: + "@iconify/types": 2.0.0 + + "@iconify/types@2.0.0": {} + + "@iconify/utils@2.3.0": + dependencies: + "@antfu/install-pkg": 1.1.0 + "@antfu/utils": 8.1.1 + "@iconify/types": 2.0.0 + debug: 4.4.0(supports-color@5.5.0) + globals: 15.15.0 + kolorist: 1.8.0 + local-pkg: 1.1.2 + mlly: 1.8.0 + transitivePeerDependencies: + - supports-color + + "@isaacs/cliui@8.0.2": + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + "@istanbuljs/schema@0.1.3": {} + + "@jridgewell/gen-mapping@0.3.13": + dependencies: + "@jridgewell/sourcemap-codec": 1.5.5 + "@jridgewell/trace-mapping": 0.3.31 + + "@jridgewell/resolve-uri@3.1.2": {} + + "@jridgewell/sourcemap-codec@1.5.5": {} + + "@jridgewell/trace-mapping@0.3.31": + dependencies: + "@jridgewell/resolve-uri": 3.1.2 + "@jridgewell/sourcemap-codec": 1.5.5 + + "@mermaid-js/mermaid-mindmap@9.3.0": + dependencies: + "@braintree/sanitize-url": 6.0.4 + cytoscape: 3.31.0 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.31.0) + cytoscape-fcose: 2.2.0(cytoscape@3.31.0) + d3: 7.9.0 + khroma: 2.1.0 + non-layered-tidy-tree-layout: 2.0.2 + optional: true + + "@mermaid-js/parser@0.3.0": + dependencies: + langium: 3.0.0 + + "@nodelib/fs.scandir@2.1.5": + dependencies: + "@nodelib/fs.stat": 2.0.5 + run-parallel: 1.2.0 + + "@nodelib/fs.stat@2.0.5": {} + + "@nodelib/fs.walk@1.2.8": + dependencies: + "@nodelib/fs.scandir": 2.1.5 + fastq: 1.20.1 + + "@pkgjs/parseargs@0.11.0": + optional: true + + "@rollup/rollup-android-arm-eabi@4.57.1": + optional: true + + "@rollup/rollup-android-arm64@4.57.1": + optional: true + + "@rollup/rollup-darwin-arm64@4.57.1": + optional: true + + "@rollup/rollup-darwin-x64@4.57.1": + optional: true + + "@rollup/rollup-freebsd-arm64@4.57.1": + optional: true + + "@rollup/rollup-freebsd-x64@4.57.1": + optional: true + + "@rollup/rollup-linux-arm-gnueabihf@4.57.1": + optional: true + + "@rollup/rollup-linux-arm-musleabihf@4.57.1": + optional: true + + "@rollup/rollup-linux-arm64-gnu@4.57.1": + optional: true + + "@rollup/rollup-linux-arm64-musl@4.57.1": + optional: true + + "@rollup/rollup-linux-loong64-gnu@4.57.1": + optional: true + + "@rollup/rollup-linux-loong64-musl@4.57.1": + optional: true + + "@rollup/rollup-linux-ppc64-gnu@4.57.1": + optional: true + + "@rollup/rollup-linux-ppc64-musl@4.57.1": + optional: true + + "@rollup/rollup-linux-riscv64-gnu@4.57.1": + optional: true + + "@rollup/rollup-linux-riscv64-musl@4.57.1": + optional: true + + "@rollup/rollup-linux-s390x-gnu@4.57.1": + optional: true + + "@rollup/rollup-linux-x64-gnu@4.57.1": + optional: true + + "@rollup/rollup-linux-x64-musl@4.57.1": + optional: true + + "@rollup/rollup-openbsd-x64@4.57.1": + optional: true + + "@rollup/rollup-openharmony-arm64@4.57.1": + optional: true + + "@rollup/rollup-win32-arm64-msvc@4.57.1": + optional: true + + "@rollup/rollup-win32-ia32-msvc@4.57.1": + optional: true + + "@rollup/rollup-win32-x64-gnu@4.57.1": + optional: true + + "@rollup/rollup-win32-x64-msvc@4.57.1": + optional: true + + "@shikijs/core@2.5.0": + dependencies: + "@shikijs/engine-javascript": 2.5.0 + "@shikijs/engine-oniguruma": 2.5.0 + "@shikijs/types": 2.5.0 + "@shikijs/vscode-textmate": 10.0.2 + "@types/hast": 3.0.4 + hast-util-to-html: 9.0.5 + + "@shikijs/engine-javascript@2.5.0": + dependencies: + "@shikijs/types": 2.5.0 + "@shikijs/vscode-textmate": 10.0.2 + oniguruma-to-es: 3.1.1 + + "@shikijs/engine-oniguruma@2.5.0": + dependencies: + "@shikijs/types": 2.5.0 + "@shikijs/vscode-textmate": 10.0.2 + + "@shikijs/langs@2.5.0": + dependencies: + "@shikijs/types": 2.5.0 + + "@shikijs/themes@2.5.0": + dependencies: + "@shikijs/types": 2.5.0 + + "@shikijs/transformers@2.5.0": + dependencies: + "@shikijs/core": 2.5.0 + "@shikijs/types": 2.5.0 + + "@shikijs/types@2.5.0": + dependencies: + "@shikijs/vscode-textmate": 10.0.2 + "@types/hast": 3.0.4 + + "@shikijs/vscode-textmate@10.0.2": {} + + "@swc/core-darwin-arm64@1.15.11": + optional: true + + "@swc/core-darwin-x64@1.15.11": + optional: true + + "@swc/core-linux-arm-gnueabihf@1.15.11": + optional: true + + "@swc/core-linux-arm64-gnu@1.15.11": + optional: true + + "@swc/core-linux-arm64-musl@1.15.11": + optional: true + + "@swc/core-linux-x64-gnu@1.15.11": + optional: true + + "@swc/core-linux-x64-musl@1.15.11": + optional: true + + "@swc/core-win32-arm64-msvc@1.15.11": + optional: true + + "@swc/core-win32-ia32-msvc@1.15.11": + optional: true + + "@swc/core-win32-x64-msvc@1.15.11": + optional: true + + "@swc/core@1.15.11": + dependencies: + "@swc/counter": 0.1.3 + "@swc/types": 0.1.25 + optionalDependencies: + "@swc/core-darwin-arm64": 1.15.11 + "@swc/core-darwin-x64": 1.15.11 + "@swc/core-linux-arm-gnueabihf": 1.15.11 + "@swc/core-linux-arm64-gnu": 1.15.11 + "@swc/core-linux-arm64-musl": 1.15.11 + "@swc/core-linux-x64-gnu": 1.15.11 + "@swc/core-linux-x64-musl": 1.15.11 + "@swc/core-win32-arm64-msvc": 1.15.11 + "@swc/core-win32-ia32-msvc": 1.15.11 + "@swc/core-win32-x64-msvc": 1.15.11 + + "@swc/counter@0.1.3": {} + + "@swc/types@0.1.25": + dependencies: + "@swc/counter": 0.1.3 + + "@tsconfig/node22@22.0.0": {} + + "@tsconfig/strictest@2.0.5": {} + + "@types/chai@5.2.3": + dependencies: + "@types/deep-eql": 4.0.2 + assertion-error: 2.0.1 + + "@types/d3-array@3.2.2": {} + + "@types/d3-axis@3.0.6": + dependencies: + "@types/d3-selection": 3.0.11 + + "@types/d3-brush@3.0.6": + dependencies: + "@types/d3-selection": 3.0.11 + + "@types/d3-chord@3.0.6": {} + + "@types/d3-color@3.1.3": {} + + "@types/d3-contour@3.0.6": + dependencies: + "@types/d3-array": 3.2.2 + "@types/geojson": 7946.0.16 + + "@types/d3-delaunay@6.0.4": {} + + "@types/d3-dispatch@3.0.7": {} + + "@types/d3-drag@3.0.7": + dependencies: + "@types/d3-selection": 3.0.11 + + "@types/d3-dsv@3.0.7": {} + + "@types/d3-ease@3.0.2": {} + + "@types/d3-fetch@3.0.7": + dependencies: + "@types/d3-dsv": 3.0.7 + + "@types/d3-force@3.0.10": {} + + "@types/d3-format@3.0.4": {} + + "@types/d3-geo@3.1.0": + dependencies: + "@types/geojson": 7946.0.16 + + "@types/d3-hierarchy@3.1.7": {} + + "@types/d3-interpolate@3.0.4": + dependencies: + "@types/d3-color": 3.1.3 + + "@types/d3-path@3.1.1": {} + + "@types/d3-polygon@3.0.2": {} + + "@types/d3-quadtree@3.0.6": {} + + "@types/d3-random@3.0.3": {} + + "@types/d3-scale-chromatic@3.1.0": {} + + "@types/d3-scale@4.0.9": + dependencies: + "@types/d3-time": 3.0.4 + + "@types/d3-selection@3.0.11": {} + + "@types/d3-shape@3.1.8": + dependencies: + "@types/d3-path": 3.1.1 + + "@types/d3-time-format@4.0.3": {} + + "@types/d3-time@3.0.4": {} + + "@types/d3-timer@3.0.2": {} + + "@types/d3-transition@3.0.9": + dependencies: + "@types/d3-selection": 3.0.11 + + "@types/d3-zoom@3.0.8": + dependencies: + "@types/d3-interpolate": 3.0.4 + "@types/d3-selection": 3.0.11 + + "@types/d3@7.4.3": + dependencies: + "@types/d3-array": 3.2.2 + "@types/d3-axis": 3.0.6 + "@types/d3-brush": 3.0.6 + "@types/d3-chord": 3.0.6 + "@types/d3-color": 3.1.3 + "@types/d3-contour": 3.0.6 + "@types/d3-delaunay": 6.0.4 + "@types/d3-dispatch": 3.0.7 + "@types/d3-drag": 3.0.7 + "@types/d3-dsv": 3.0.7 + "@types/d3-ease": 3.0.2 + "@types/d3-fetch": 3.0.7 + "@types/d3-force": 3.0.10 + "@types/d3-format": 3.0.4 + "@types/d3-geo": 3.1.0 + "@types/d3-hierarchy": 3.1.7 + "@types/d3-interpolate": 3.0.4 + "@types/d3-path": 3.1.1 + "@types/d3-polygon": 3.0.2 + "@types/d3-quadtree": 3.0.6 + "@types/d3-random": 3.0.3 + "@types/d3-scale": 4.0.9 + "@types/d3-scale-chromatic": 3.1.0 + "@types/d3-selection": 3.0.11 + "@types/d3-shape": 3.1.8 + "@types/d3-time": 3.0.4 + "@types/d3-time-format": 4.0.3 + "@types/d3-timer": 3.0.2 + "@types/d3-transition": 3.0.9 + "@types/d3-zoom": 3.0.8 + + "@types/deep-eql@4.0.2": {} + + "@types/eslint-config-prettier@6.11.3": {} + + "@types/estree@1.0.8": {} + + "@types/geojson@7946.0.16": {} + + "@types/hast@3.0.4": + dependencies: + "@types/unist": 3.0.3 + + "@types/json-schema@7.0.15": {} + + "@types/linkify-it@5.0.0": {} + + "@types/markdown-it@14.1.2": + dependencies: + "@types/linkify-it": 5.0.0 + "@types/mdurl": 2.0.0 + + "@types/mdast@4.0.4": + dependencies: + "@types/unist": 3.0.3 + + "@types/mdurl@2.0.0": {} + + "@types/node@22.19.11": + dependencies: + undici-types: 6.21.0 + + "@types/trusted-types@2.0.7": + optional: true + + "@types/unist@3.0.3": {} + + "@types/web-bluetooth@0.0.21": {} + + "@typescript-eslint/eslint-plugin@8.21.0(@typescript-eslint/parser@8.21.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)": + dependencies: + "@eslint-community/regexpp": 4.12.2 + "@typescript-eslint/parser": 8.21.0(eslint@9.39.2)(typescript@5.9.3) + "@typescript-eslint/scope-manager": 8.21.0 + "@typescript-eslint/type-utils": 8.21.0(eslint@9.39.2)(typescript@5.9.3) + "@typescript-eslint/utils": 8.21.0(eslint@9.39.2)(typescript@5.9.3) + "@typescript-eslint/visitor-keys": 8.21.0 + eslint: 9.39.2 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/eslint-plugin@8.56.0(@typescript-eslint/parser@8.56.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)": + dependencies: + "@eslint-community/regexpp": 4.12.2 + "@typescript-eslint/parser": 8.56.0(eslint@9.39.2)(typescript@5.9.3) + "@typescript-eslint/scope-manager": 8.56.0 + "@typescript-eslint/type-utils": 8.56.0(eslint@9.39.2)(typescript@5.9.3) + "@typescript-eslint/utils": 8.56.0(eslint@9.39.2)(typescript@5.9.3) + "@typescript-eslint/visitor-keys": 8.56.0 + eslint: 9.39.2 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/parser@8.21.0(eslint@9.39.2)(typescript@5.9.3)": + dependencies: + "@typescript-eslint/scope-manager": 8.21.0 + "@typescript-eslint/types": 8.21.0 + "@typescript-eslint/typescript-estree": 8.21.0(typescript@5.9.3) + "@typescript-eslint/visitor-keys": 8.21.0 + debug: 4.4.0(supports-color@5.5.0) + eslint: 9.39.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/parser@8.56.0(eslint@9.39.2)(typescript@5.9.3)": + dependencies: + "@typescript-eslint/scope-manager": 8.56.0 + "@typescript-eslint/types": 8.56.0 + "@typescript-eslint/typescript-estree": 8.56.0(typescript@5.9.3) + "@typescript-eslint/visitor-keys": 8.56.0 + debug: 4.4.3 + eslint: 9.39.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/project-service@8.56.0(typescript@5.9.3)": + dependencies: + "@typescript-eslint/tsconfig-utils": 8.56.0(typescript@5.9.3) + "@typescript-eslint/types": 8.56.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/scope-manager@8.21.0": + dependencies: + "@typescript-eslint/types": 8.21.0 + "@typescript-eslint/visitor-keys": 8.21.0 + + "@typescript-eslint/scope-manager@8.56.0": + dependencies: + "@typescript-eslint/types": 8.56.0 + "@typescript-eslint/visitor-keys": 8.56.0 + + "@typescript-eslint/tsconfig-utils@8.56.0(typescript@5.9.3)": + dependencies: + typescript: 5.9.3 + + "@typescript-eslint/type-utils@8.21.0(eslint@9.39.2)(typescript@5.9.3)": + dependencies: + "@typescript-eslint/typescript-estree": 8.21.0(typescript@5.9.3) + "@typescript-eslint/utils": 8.21.0(eslint@9.39.2)(typescript@5.9.3) + debug: 4.4.0(supports-color@5.5.0) + eslint: 9.39.2 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/type-utils@8.56.0(eslint@9.39.2)(typescript@5.9.3)": + dependencies: + "@typescript-eslint/types": 8.56.0 + "@typescript-eslint/typescript-estree": 8.56.0(typescript@5.9.3) + "@typescript-eslint/utils": 8.56.0(eslint@9.39.2)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.2 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/types@8.21.0": {} + + "@typescript-eslint/types@8.56.0": {} + + "@typescript-eslint/typescript-estree@8.21.0(typescript@5.9.3)": + dependencies: + "@typescript-eslint/types": 8.21.0 + "@typescript-eslint/visitor-keys": 8.21.0 + debug: 4.4.0(supports-color@5.5.0) + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.4 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/typescript-estree@8.56.0(typescript@5.9.3)": + dependencies: + "@typescript-eslint/project-service": 8.56.0(typescript@5.9.3) + "@typescript-eslint/tsconfig-utils": 8.56.0(typescript@5.9.3) + "@typescript-eslint/types": 8.56.0 + "@typescript-eslint/visitor-keys": 8.56.0 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/utils@8.21.0(eslint@9.39.2)(typescript@5.9.3)": + dependencies: + "@eslint-community/eslint-utils": 4.9.1(eslint@9.39.2) + "@typescript-eslint/scope-manager": 8.21.0 + "@typescript-eslint/types": 8.21.0 + "@typescript-eslint/typescript-estree": 8.21.0(typescript@5.9.3) + eslint: 9.39.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/utils@8.56.0(eslint@9.39.2)(typescript@5.9.3)": + dependencies: + "@eslint-community/eslint-utils": 4.9.1(eslint@9.39.2) + "@typescript-eslint/scope-manager": 8.56.0 + "@typescript-eslint/types": 8.56.0 + "@typescript-eslint/typescript-estree": 8.56.0(typescript@5.9.3) + eslint: 9.39.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/visitor-keys@8.21.0": + dependencies: + "@typescript-eslint/types": 8.21.0 + eslint-visitor-keys: 4.2.1 + + "@typescript-eslint/visitor-keys@8.56.0": + dependencies: + "@typescript-eslint/types": 8.56.0 + eslint-visitor-keys: 5.0.0 + + "@ungap/structured-clone@1.3.0": {} + + "@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@22.19.11))(vue@3.5.28(typescript@5.9.3))": + dependencies: + vite: 5.4.21(@types/node@22.19.11) + vue: 3.5.28(typescript@5.9.3) + + "@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@22.19.11)(yaml@2.8.2))": + dependencies: + "@ampproject/remapping": 2.3.0 + "@bcoe/v8-coverage": 1.0.2 + ast-v8-to-istanbul: 0.3.11 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.1 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/node@22.19.11)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + + "@vitest/expect@3.2.4": + dependencies: + "@types/chai": 5.2.3 + "@vitest/spy": 3.2.4 + "@vitest/utils": 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + "@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.11)(yaml@2.8.2))": + dependencies: + "@vitest/spy": 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@22.19.11)(yaml@2.8.2) + + "@vitest/pretty-format@3.2.4": + dependencies: + tinyrainbow: 2.0.0 + + "@vitest/runner@3.2.4": + dependencies: + "@vitest/utils": 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + "@vitest/snapshot@3.2.4": + dependencies: + "@vitest/pretty-format": 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + "@vitest/spy@3.2.4": + dependencies: + tinyspy: 4.0.4 + + "@vitest/utils@3.2.4": + dependencies: + "@vitest/pretty-format": 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + "@vue/compiler-core@3.5.28": + dependencies: + "@babel/parser": 7.29.0 + "@vue/shared": 3.5.28 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + "@vue/compiler-dom@3.5.28": + dependencies: + "@vue/compiler-core": 3.5.28 + "@vue/shared": 3.5.28 + + "@vue/compiler-sfc@3.5.28": + dependencies: + "@babel/parser": 7.29.0 + "@vue/compiler-core": 3.5.28 + "@vue/compiler-dom": 3.5.28 + "@vue/compiler-ssr": 3.5.28 + "@vue/shared": 3.5.28 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + "@vue/compiler-ssr@3.5.28": + dependencies: + "@vue/compiler-dom": 3.5.28 + "@vue/shared": 3.5.28 + + "@vue/devtools-api@7.7.9": + dependencies: + "@vue/devtools-kit": 7.7.9 + + "@vue/devtools-kit@7.7.9": + dependencies: + "@vue/devtools-shared": 7.7.9 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + "@vue/devtools-shared@7.7.9": + dependencies: + rfdc: 1.4.1 + + "@vue/reactivity@3.5.28": + dependencies: + "@vue/shared": 3.5.28 + + "@vue/runtime-core@3.5.28": + dependencies: + "@vue/reactivity": 3.5.28 + "@vue/shared": 3.5.28 + + "@vue/runtime-dom@3.5.28": + dependencies: + "@vue/reactivity": 3.5.28 + "@vue/runtime-core": 3.5.28 + "@vue/shared": 3.5.28 + csstype: 3.2.3 + + "@vue/server-renderer@3.5.28(vue@3.5.28(typescript@5.9.3))": + dependencies: + "@vue/compiler-ssr": 3.5.28 + "@vue/shared": 3.5.28 + vue: 3.5.28(typescript@5.9.3) + + "@vue/shared@3.5.28": {} + + "@vueuse/core@12.8.2(typescript@5.9.3)": + dependencies: + "@types/web-bluetooth": 0.0.21 + "@vueuse/metadata": 12.8.2 + "@vueuse/shared": 12.8.2(typescript@5.9.3) + vue: 3.5.28(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + "@vueuse/integrations@12.8.2(focus-trap@7.8.0)(typescript@5.9.3)": + dependencies: + "@vueuse/core": 12.8.2(typescript@5.9.3) + "@vueuse/shared": 12.8.2(typescript@5.9.3) + vue: 3.5.28(typescript@5.9.3) + optionalDependencies: + focus-trap: 7.8.0 + transitivePeerDependencies: + - typescript + + "@vueuse/metadata@12.8.2": {} + + "@vueuse/shared@12.8.2(typescript@5.9.3)": + dependencies: + vue: 3.5.28(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + algoliasearch@5.49.0: + dependencies: + "@algolia/abtesting": 1.15.0 + "@algolia/client-abtesting": 5.49.0 + "@algolia/client-analytics": 5.49.0 + "@algolia/client-common": 5.49.0 + "@algolia/client-insights": 5.49.0 + "@algolia/client-personalization": 5.49.0 + "@algolia/client-query-suggestions": 5.49.0 + "@algolia/client-search": 5.49.0 + "@algolia/ingestion": 1.49.0 + "@algolia/monitoring": 1.49.0 + "@algolia/recommend": 5.49.0 + "@algolia/requester-browser-xhr": 5.49.0 + "@algolia/requester-fetch": 5.49.0 + "@algolia/requester-node-http": 5.49.0 + + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@2.0.1: {} + + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@0.3.11: + dependencies: + "@jridgewell/trace-mapping": 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + balanced-match@1.0.2: {} + + balanced-match@4.0.3: {} + + binary-extensions@2.3.0: {} + + birpc@2.9.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.2: + dependencies: + balanced-match: 4.0.3 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + cac@6.7.14: {} + + callsites@3.1.0: {} + + ccount@2.0.1: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + check-error@2.1.3: {} + + chevrotain-allstar@0.3.1(chevrotain@11.0.3): + dependencies: + chevrotain: 11.0.3 + lodash-es: 4.17.23 + + chevrotain@11.0.3: + dependencies: + "@chevrotain/cst-dts-gen": 11.0.3 + "@chevrotain/gast": 11.0.3 + "@chevrotain/regexp-to-ast": 11.0.3 + "@chevrotain/types": 11.0.3 + "@chevrotain/utils": 11.0.3 + lodash-es: 4.17.21 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + comma-separated-tokens@2.0.3: {} + + commander@13.1.0: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + confbox@0.2.4: {} + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + cytoscape-cose-bilkent@4.1.0(cytoscape@3.31.0): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.31.0 + + cytoscape-fcose@2.2.0(cytoscape@3.31.0): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.31.0 + + cytoscape@3.31.0: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.11: + dependencies: + d3: 7.9.0 + lodash-es: 4.17.23 + + dayjs@1.11.13: {} + + debug@4.4.0(supports-color@5.5.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dompurify@3.3.1: + optionalDependencies: + "@types/trusted-types": 2.0.7 + + eastasianwidth@0.2.0: {} + + emoji-regex-xs@1.0.0: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + entities@7.0.1: {} + + environment@1.1.0: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.21.5: + optionalDependencies: + "@esbuild/aix-ppc64": 0.21.5 + "@esbuild/android-arm": 0.21.5 + "@esbuild/android-arm64": 0.21.5 + "@esbuild/android-x64": 0.21.5 + "@esbuild/darwin-arm64": 0.21.5 + "@esbuild/darwin-x64": 0.21.5 + "@esbuild/freebsd-arm64": 0.21.5 + "@esbuild/freebsd-x64": 0.21.5 + "@esbuild/linux-arm": 0.21.5 + "@esbuild/linux-arm64": 0.21.5 + "@esbuild/linux-ia32": 0.21.5 + "@esbuild/linux-loong64": 0.21.5 + "@esbuild/linux-mips64el": 0.21.5 + "@esbuild/linux-ppc64": 0.21.5 + "@esbuild/linux-riscv64": 0.21.5 + "@esbuild/linux-s390x": 0.21.5 + "@esbuild/linux-x64": 0.21.5 + "@esbuild/netbsd-x64": 0.21.5 + "@esbuild/openbsd-x64": 0.21.5 + "@esbuild/sunos-x64": 0.21.5 + "@esbuild/win32-arm64": 0.21.5 + "@esbuild/win32-ia32": 0.21.5 + "@esbuild/win32-x64": 0.21.5 + + esbuild@0.27.3: + optionalDependencies: + "@esbuild/aix-ppc64": 0.27.3 + "@esbuild/android-arm": 0.27.3 + "@esbuild/android-arm64": 0.27.3 + "@esbuild/android-x64": 0.27.3 + "@esbuild/darwin-arm64": 0.27.3 + "@esbuild/darwin-x64": 0.27.3 + "@esbuild/freebsd-arm64": 0.27.3 + "@esbuild/freebsd-x64": 0.27.3 + "@esbuild/linux-arm": 0.27.3 + "@esbuild/linux-arm64": 0.27.3 + "@esbuild/linux-ia32": 0.27.3 + "@esbuild/linux-loong64": 0.27.3 + "@esbuild/linux-mips64el": 0.27.3 + "@esbuild/linux-ppc64": 0.27.3 + "@esbuild/linux-riscv64": 0.27.3 + "@esbuild/linux-s390x": 0.27.3 + "@esbuild/linux-x64": 0.27.3 + "@esbuild/netbsd-arm64": 0.27.3 + "@esbuild/netbsd-x64": 0.27.3 + "@esbuild/openbsd-arm64": 0.27.3 + "@esbuild/openbsd-x64": 0.27.3 + "@esbuild/openharmony-arm64": 0.27.3 + "@esbuild/sunos-x64": 0.27.3 + "@esbuild/win32-arm64": 0.27.3 + "@esbuild/win32-ia32": 0.27.3 + "@esbuild/win32-x64": 0.27.3 + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@9.39.2): + dependencies: + eslint: 9.39.2 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.0: {} + + eslint@9.39.2: + dependencies: + "@eslint-community/eslint-utils": 4.9.1(eslint@9.39.2) + "@eslint-community/regexpp": 4.12.2 + "@eslint/config-array": 0.21.1 + "@eslint/config-helpers": 0.4.2 + "@eslint/core": 0.17.0 + "@eslint/eslintrc": 3.3.3 + "@eslint/js": 9.39.2 + "@eslint/plugin-kit": 0.4.1 + "@humanfs/node": 0.16.7 + "@humanwhocodes/module-importer": 1.0.1 + "@humanwhocodes/retry": 0.4.3 + "@types/estree": 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.0(supports-color@5.5.0) + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + "@types/estree": 1.0.8 + + esutils@2.0.3: {} + + eventemitter3@5.0.4: {} + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + expect-type@1.3.0: {} + + exsolve@1.0.8: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + "@nodelib/fs.stat": 2.0.5 + "@nodelib/fs.walk": 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + focus-trap@7.8.0: + dependencies: + tabbable: 6.4.0 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fsevents@2.3.3: + optional: true + + get-east-asian-width@1.5.0: {} + + get-stream@8.0.1: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@13.0.5: + dependencies: + minimatch: 10.2.1 + minipass: 7.1.3 + path-scurry: 2.0.1 + + globals@14.0.0: {} + + globals@15.15.0: {} + + graphemer@1.4.0: {} + + hachure-fill@0.5.2: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + hast-util-to-html@9.0.5: + dependencies: + "@types/hast": 3.0.4 + "@types/unist": 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + "@types/hast": 3.0.4 + + hookable@5.5.3: {} + + html-escaper@2.0.2: {} + + html-void-elements@3.0.0: {} + + human-signals@5.0.0: {} + + husky@9.1.7: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ignore-by-default@1.0.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + internmap@1.0.1: {} + + internmap@2.0.3: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@4.0.0: {} + + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.5.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-stream@3.0.0: {} + + is-what@5.5.0: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + "@jridgewell/trace-mapping": 0.3.31 + debug: 4.4.0(supports-color@5.5.0) + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + "@isaacs/cliui": 8.0.2 + optionalDependencies: + "@pkgjs/parseargs": 0.11.0 + + js-tokens@10.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + katex@0.16.28: + dependencies: + commander: 8.3.0 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + khroma@2.1.0: {} + + kolorist@1.8.0: {} + + langium@3.0.0: + dependencies: + chevrotain: 11.0.3 + chevrotain-allstar: 0.3.1(chevrotain@11.0.3) + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.3: {} + + lint-staged@15.5.2: + dependencies: + chalk: 5.6.2 + commander: 13.1.0 + debug: 4.4.0(supports-color@5.5.0) + execa: 8.0.1 + lilconfig: 3.1.3 + listr2: 8.3.3 + micromatch: 4.0.8 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.8.2 + transitivePeerDependencies: + - supports-color + + listr2@8.3.3: + dependencies: + cli-truncate: 4.0.0 + colorette: 2.0.20 + eventemitter3: 5.0.4 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.2 + + local-pkg@1.1.2: + dependencies: + mlly: 1.8.0 + pkg-types: 2.3.0 + quansync: 0.2.11 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.21: {} + + lodash-es@4.17.23: {} + + lodash.merge@4.6.2: {} + + log-update@6.1.0: + dependencies: + ansi-escapes: 7.3.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.1.2 + wrap-ansi: 9.0.2 + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + lru-cache@11.2.6: {} + + magic-string@0.30.21: + dependencies: + "@jridgewell/sourcemap-codec": 1.5.5 + + magicast@0.3.5: + dependencies: + "@babel/parser": 7.29.0 + "@babel/types": 7.29.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.4 + + mark.js@8.11.1: {} + + marked@13.0.3: {} + + mdast-util-to-hast@13.2.1: + dependencies: + "@types/hast": 3.0.4 + "@types/mdast": 4.0.4 + "@ungap/structured-clone": 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + mermaid@11.4.1: + dependencies: + "@braintree/sanitize-url": 7.1.1 + "@iconify/utils": 2.3.0 + "@mermaid-js/parser": 0.3.0 + "@types/d3": 7.4.3 + cytoscape: 3.31.0 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.31.0) + cytoscape-fcose: 2.2.0(cytoscape@3.31.0) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.11 + dayjs: 1.11.13 + dompurify: 3.3.1 + katex: 0.16.28 + khroma: 2.1.0 + lodash-es: 4.17.23 + marked: 13.0.3 + roughjs: 4.6.6 + stylis: 4.3.6 + ts-dedent: 2.2.0 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-fn@4.0.0: {} + + mimic-function@5.0.1: {} + + minimatch@10.2.1: + dependencies: + brace-expansion: 5.0.2 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minipass@7.1.3: {} + + minisearch@7.2.0: {} + + mitt@3.0.1: {} + + mlly@1.8.0: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + nodemon@3.1.11: + dependencies: + chokidar: 3.6.0 + debug: 4.4.0(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.7.4 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + + non-layered-tidy-tree-layout@2.0.2: + optional: true + + normalize-path@3.0.0: {} + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + oniguruma-to-es@3.1.1: + dependencies: + emoji-regex-xs: 1.0.0 + regex: 6.1.0 + regex-recursion: 6.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-json-from-dist@1.0.1: {} + + package-manager-detector@1.6.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-data-parser@0.1.0: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + path-scurry@2.0.1: + dependencies: + lru-cache: 11.2.6 + minipass: 7.1.3 + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pidtree@0.6.0: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + preact@10.28.4: {} + + prelude-ls@1.2.1: {} + + prettier@3.8.1: {} + + property-information@7.1.0: {} + + pstree.remy@1.1.8: {} + + punycode@2.3.1: {} + + quansync@0.2.11: {} + + queue-microtask@1.2.3: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + resolve-from@4.0.0: {} + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rimraf@6.1.3: + dependencies: + glob: 13.0.5 + package-json-from-dist: 1.0.1 + + robust-predicates@3.0.2: {} + + rollup@4.57.1: + dependencies: + "@types/estree": 1.0.8 + optionalDependencies: + "@rollup/rollup-android-arm-eabi": 4.57.1 + "@rollup/rollup-android-arm64": 4.57.1 + "@rollup/rollup-darwin-arm64": 4.57.1 + "@rollup/rollup-darwin-x64": 4.57.1 + "@rollup/rollup-freebsd-arm64": 4.57.1 + "@rollup/rollup-freebsd-x64": 4.57.1 + "@rollup/rollup-linux-arm-gnueabihf": 4.57.1 + "@rollup/rollup-linux-arm-musleabihf": 4.57.1 + "@rollup/rollup-linux-arm64-gnu": 4.57.1 + "@rollup/rollup-linux-arm64-musl": 4.57.1 + "@rollup/rollup-linux-loong64-gnu": 4.57.1 + "@rollup/rollup-linux-loong64-musl": 4.57.1 + "@rollup/rollup-linux-ppc64-gnu": 4.57.1 + "@rollup/rollup-linux-ppc64-musl": 4.57.1 + "@rollup/rollup-linux-riscv64-gnu": 4.57.1 + "@rollup/rollup-linux-riscv64-musl": 4.57.1 + "@rollup/rollup-linux-s390x-gnu": 4.57.1 + "@rollup/rollup-linux-x64-gnu": 4.57.1 + "@rollup/rollup-linux-x64-musl": 4.57.1 + "@rollup/rollup-openbsd-x64": 4.57.1 + "@rollup/rollup-openharmony-arm64": 4.57.1 + "@rollup/rollup-win32-arm64-msvc": 4.57.1 + "@rollup/rollup-win32-ia32-msvc": 4.57.1 + "@rollup/rollup-win32-x64-gnu": 4.57.1 + "@rollup/rollup-win32-x64-msvc": 4.57.1 + fsevents: 2.3.3 + + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rw@1.3.3: {} + + safer-buffer@2.1.2: {} + + search-insights@2.17.3: {} + + semver@7.7.4: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shiki@2.5.0: + dependencies: + "@shikijs/core": 2.5.0 + "@shikijs/engine-javascript": 2.5.0 + "@shikijs/engine-oniguruma": 2.5.0 + "@shikijs/langs": 2.5.0 + "@shikijs/themes": 2.5.0 + "@shikijs/types": 2.5.0 + "@shikijs/vscode-textmate": 10.0.2 + "@types/hast": 3.0.4 + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.4 + + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 4.0.0 + + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + + speakingurl@14.0.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + string-argv@0.3.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.1.2 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-final-newline@3.0.0: {} + + strip-json-comments@3.1.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + stylis@4.3.6: {} + + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tabbable@6.4.0: {} + + test-exclude@7.0.1: + dependencies: + "@istanbuljs/schema": 0.1.3 + glob: 10.5.0 + minimatch: 9.0.5 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyexec@1.0.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + touch@3.1.1: {} + + trim-lines@3.0.1: {} + + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-dedent@2.2.0: {} + + turbo-darwin-64@2.8.10: + optional: true + + turbo-darwin-arm64@2.8.10: + optional: true + + turbo-linux-64@2.8.10: + optional: true + + turbo-linux-arm64@2.8.10: + optional: true + + turbo-windows-64@2.8.10: + optional: true + + turbo-windows-arm64@2.8.10: + optional: true + + turbo@2.8.10: + optionalDependencies: + turbo-darwin-64: 2.8.10 + turbo-darwin-arm64: 2.8.10 + turbo-linux-64: 2.8.10 + turbo-linux-arm64: 2.8.10 + turbo-windows-64: 2.8.10 + turbo-windows-arm64: 2.8.10 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.21.0(eslint@9.39.2)(typescript@5.9.3): + dependencies: + "@typescript-eslint/eslint-plugin": 8.21.0(@typescript-eslint/parser@8.21.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) + "@typescript-eslint/parser": 8.21.0(eslint@9.39.2)(typescript@5.9.3) + "@typescript-eslint/utils": 8.21.0(eslint@9.39.2)(typescript@5.9.3) + eslint: 9.39.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + ufo@1.6.3: {} + + undefsafe@2.0.5: {} + + undici-types@6.21.0: {} + + unist-util-is@6.0.1: + dependencies: + "@types/unist": 3.0.3 + + unist-util-position@5.0.0: + dependencies: + "@types/unist": 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + "@types/unist": 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + "@types/unist": 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + "@types/unist": 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + uuid@9.0.1: {} + + vfile-message@4.0.3: + dependencies: + "@types/unist": 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + "@types/unist": 3.0.3 + vfile-message: 4.0.3 + + vite-node@3.2.4(@types/node@22.19.11)(yaml@2.8.2): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.1(@types/node@22.19.11)(yaml@2.8.2) + transitivePeerDependencies: + - "@types/node" + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@5.4.21(@types/node@22.19.11): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.57.1 + optionalDependencies: + "@types/node": 22.19.11 + fsevents: 2.3.3 + + vite@7.3.1(@types/node@22.19.11)(yaml@2.8.2): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.57.1 + tinyglobby: 0.2.15 + optionalDependencies: + "@types/node": 22.19.11 + fsevents: 2.3.3 + yaml: 2.8.2 + + vitepress-plugin-mermaid@2.0.17(mermaid@11.4.1)(vitepress@1.6.2(@algolia/client-search@5.49.0)(@types/node@22.19.11)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.3)): + dependencies: + mermaid: 11.4.1 + vitepress: 1.6.2(@algolia/client-search@5.49.0)(@types/node@22.19.11)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.3) + optionalDependencies: + "@mermaid-js/mermaid-mindmap": 9.3.0 + + vitepress@1.6.2(@algolia/client-search@5.49.0)(@types/node@22.19.11)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.3): + dependencies: + "@docsearch/css": 3.9.0 + "@docsearch/js": 3.9.0(@algolia/client-search@5.49.0)(search-insights@2.17.3) + "@iconify-json/simple-icons": 1.2.71 + "@shikijs/core": 2.5.0 + "@shikijs/transformers": 2.5.0 + "@shikijs/types": 2.5.0 + "@types/markdown-it": 14.1.2 + "@vitejs/plugin-vue": 5.2.4(vite@5.4.21(@types/node@22.19.11))(vue@3.5.28(typescript@5.9.3)) + "@vue/devtools-api": 7.7.9 + "@vue/shared": 3.5.28 + "@vueuse/core": 12.8.2(typescript@5.9.3) + "@vueuse/integrations": 12.8.2(focus-trap@7.8.0)(typescript@5.9.3) + focus-trap: 7.8.0 + mark.js: 8.11.1 + minisearch: 7.2.0 + shiki: 2.5.0 + vite: 5.4.21(@types/node@22.19.11) + vue: 3.5.28(typescript@5.9.3) + optionalDependencies: + postcss: 8.5.6 + transitivePeerDependencies: + - "@algolia/client-search" + - "@types/node" + - "@types/react" + - async-validator + - axios + - change-case + - drauu + - fuse.js + - idb-keyval + - jwt-decode + - less + - lightningcss + - nprogress + - qrcode + - react + - react-dom + - sass + - sass-embedded + - search-insights + - sortablejs + - stylus + - sugarss + - terser + - typescript + - universal-cookie + + vitest@3.2.4(@types/node@22.19.11)(yaml@2.8.2): + dependencies: + "@types/chai": 5.2.3 + "@vitest/expect": 3.2.4 + "@vitest/mocker": 3.2.4(vite@7.3.1(@types/node@22.19.11)(yaml@2.8.2)) + "@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.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.1(@types/node@22.19.11)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@22.19.11)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + "@types/node": 22.19.11 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.0.8: {} + + vue@3.5.28(typescript@5.9.3): + dependencies: + "@vue/compiler-dom": 3.5.28 + "@vue/compiler-sfc": 3.5.28 + "@vue/runtime-dom": 3.5.28 + "@vue/server-renderer": 3.5.28(vue@3.5.28(typescript@5.9.3)) + "@vue/shared": 3.5.28 + optionalDependencies: + typescript: 5.9.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.1.2 + + yaml@2.8.2: {} + + yocto-queue@0.1.0: {} + + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..dee51e9 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "packages/*" diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..3c5a00a --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,25 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + /* Basic Options */ + "incremental": true, + "declaration": true, + "allowJs": true, + "noEmit": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + /* Strict Type-Checking Options */ + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": false, + "noImplicitThis": true, + "alwaysStrict": true, + "resolveJsonModule": true, + + /* Experimental Options */ + "experimentalDecorators": true, + "emitDecoratorMetadata": false + } +} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..f43754d --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "incremental": false, + "noEmit": false + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a4c6b29 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.base.json", + "include": ["**/*", ".*.js"], + "compilerOptions": { + "types": ["vitest/globals"], + "baseUrl": ".", + "paths": { + "@ade/shared/*": ["packages/shared/src/*"], + "@ade/cli/*": ["packages/ade/src/*"], + "@ade/mcp-server/*": ["packages/ade-mcp-server/src/*"] + } + } +} diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..063db38 --- /dev/null +++ b/turbo.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://turbo.build/schema.json", + "tasks": { + "//#lint": { + "inputs": ["!.git/**", "!node_modules/**", "!packages/**"] + }, + "//#lint:fix": { + "inputs": ["!.git/**", "!node_modules/**", "!packages/**"] + }, + "//#format": { + "inputs": ["!.git/**", "!node_modules/**", "!packages/**"] + }, + "//#format:fix": { + "inputs": ["!.git/**", "!node_modules/**", "!packages/**"] + }, + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", "tsconfig.tsbuildinfo"] + }, + "clean:build": { + "dependsOn": ["build"] + }, + "dev": { + "dependsOn": ["build"], + "cache": false, + "persistent": true + }, + "lint": {}, + "lint:fix": {}, + "format": {}, + "format:fix": {}, + "test": { + "dependsOn": ["build"] + }, + "typecheck": { + "outputs": ["tsconfig.tsbuildinfo"] + } + }, + "globalDependencies": [ + "eslint.config.mjs", + ".lintstagedrc.js", + ".prettierrc.yaml", + "tsconfig.base.json", + "tsconfig.build.json", + "tsconfig.json" + ] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..721ac73 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + coverage: { + provider: "v8", + reporter: ["text", "json", "html"] + } + } +}); diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 0000000..e69de29 From f9c8bc540601cb25aee6b2e11036fa86f286aaec Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 13:32:13 +0000 Subject: [PATCH 06/60] Restructure design: two packages, interface-based writer registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Two packages: @ade/core (types, logic, catalog, writers) and @ade/cli (thin shell for arg parsing and TUI). Drop @ade/mcp-server. - Writer contracts use interfaces (ProvisionWriterDef, AgentWriterDef) for runtime extensibility — future packages can register writers without modifying core. - Dispatch via Map-based WriterRegistry, initialized with built-in writers via createDefaultRegistry(). - Built-in writers get internal type safety by defining their own config interfaces and narrowing from Record at the boundary. - Document the design tension (type safety vs extensibility) and why discriminated unions are insufficient for an open plugin model. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- docs/DESIGN.md | 368 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 258 insertions(+), 110 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index edcc32f..e83d765 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1,47 +1,90 @@ # ADE CLI — Design Document -> **Scope.** This document covers the **ADE CLI** (`packages/ade`) — the -> setup and configuration tool. It does not cover the runtime MCP servers +> **Scope.** This document covers the **ADE CLI** — the setup and +> configuration tool. It does not cover the runtime MCP servers > (`@codemcp/workflows-server`, `@codemcp/knowledge-server`) or the broader > ADE information architecture. For the overall ADE vision, see the project > README. +## Package Structure + +Two packages with clear responsibilities: + +### `@ade/core` (`packages/core`) + +All types, logic, and built-in writers. No CLI framework, no TUI, no user +interaction. Independently importable for programmatic use (CI scripts, +other tools). + +``` +core/src/ + types.ts # all interfaces and type definitions + config.ts # read/write config.yaml and config.lock.yaml + resolver.ts # config + catalog → LogicalConfig + registry.ts # writer registry (provision + agent) + catalog/ + index.ts # catalog registry, exports all facets + facets/ + process.ts + conventions.ts + documentation.ts + frameworks.ts + writers/ # built-in provision writers + workflows.ts + skills.ts + knowledge.ts + mcp-server.ts + instruction.ts + installable.ts + agents/ # built-in agent writers + opencode.ts +``` + +### `@ade/cli` (`packages/cli`) + +Thin shell: CLI framework wiring and interactive TUI. All business logic +lives in core; CLI commands are thin handlers that parse args and delegate. + +``` +cli/src/ + index.ts # entry point, arg parser, command routing + commands/ + setup.ts # interactive TUI setup + install.ts # resolve + generate (idempotent) + add.ts # modify single facet + remove.ts # remove facet selection + status.ts # show current state + tui/ + prompts.ts # interactive facet selection UI +``` + +`@ade/cli` depends on `@ade/core`. Nothing depends on `@ade/cli`. + ## Architecture Overview ``` -┌─────────────────────────────────────────────────────┐ -│ CLI Layer │ -│ ade setup · ade install · ade add · ade remove │ -└──────────────────────┬──────────────────────────────┘ - │ - ┌────────▼────────┐ - │ Catalog │ facets, options, recipes (TypeScript) - └────────┬────────┘ - │ - ┌────────▼────────┐ - │ Resolver │ config.yaml + catalog → provisions - └────────┬────────┘ - │ - ┌────────────▼────────────┐ - │ Provision Writers │ each writer produces LogicalConfig - │ │ fragments; some call package APIs - │ workflows · skills │ - │ knowledge · mcp-server │ - │ instruction · install. │ - └────────────┬────────────┘ - │ merge - ┌────────▼────────┐ - │ LogicalConfig │ agent-agnostic intermediate repr - └────────┬────────┘ - │ - ┌────────────▼────────────┐ - │ Agent Writers │ ADE owns format knowledge - │ │ - │ claude-code · copilot │ - │ kiro │ - └─────────────────────────┘ - │ - agent-specific files +┌─────────────────────────────────────────────────────────────┐ +│ @ade/cli │ +│ ade setup · ade install · ade add · ade remove · ade status │ +│ TUI prompts │ +└──────────────────────────┬──────────────────────────────────┘ + │ delegates to +┌──────────────────────────▼──────────────────────────────────┐ +│ @ade/core │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌────────────────────────┐ │ +│ │ Catalog │──▶│ Resolver │──▶│ Writer Registry │ │ +│ │ (facets) │ │ │ │ │ │ +│ └──────────┘ └────┬─────┘ │ provision: Map │ │ +│ │ │ agents: Map │ │ +│ ▼ └───────────┬────────────┘ │ +│ ┌──────────────┐ │ │ +│ │ LogicalConfig│◀────────────┘ │ +│ └──────┬───────┘ merge fragments │ +│ │ │ +│ ▼ │ +│ agent-specific files │ +└─────────────────────────────────────────────────────────────┘ ``` ## Data Flow @@ -144,8 +187,8 @@ interface Option { // This is how one logical concept materializes across different output channels. interface Provision { - writer: ProvisionWriter; - config: Record; // writer-specific + writer: string; // references a registered ProvisionWriterDef.id + config: Record; // writer-specific, validated at boundary } // Passed to provision writers so they can adapt based on sibling selections. @@ -158,14 +201,6 @@ interface ResolvedFacet { optionId: string; option: Option; } - -type ProvisionWriter = - | "workflows" - | "skills" - | "knowledge" - | "mcp-server" - | "instruction" - | "installable"; ``` ### LogicalConfig (intermediate representation) @@ -220,20 +255,164 @@ interface LockFile { } ``` -## Agent Writers +## Extensibility and Type Safety -Each agent writer implements a single interface: +### Design Tension + +Provision and agent writers need two properties that pull in opposite +directions: + +1. **Type safety** — built-in writers should have typed configs, not + `Record` everywhere. +2. **Runtime extensibility** — future packages must be able to register + new writers without modifying core's source. + +### Solution: Interfaces for Contracts, Registries for Dispatch + +Writers are defined as **interfaces** (open contracts, implementable by +anyone) and collected in **runtime registries** (`Map`-based, open for +insertion). Built-in writers get typed configs internally while conforming +to the open interface at the boundary. ```typescript -interface AgentWriter { +// --- Writer contracts (open, any package can implement) --- + +interface ProvisionWriterDef { + id: string; + write( + config: Record, + context: ResolutionContext + ): Promise>; +} + +interface AgentWriterDef { id: string; install(config: LogicalConfig, projectRoot: string): Promise; } + +// --- Writer registry (open at runtime) --- + +interface WriterRegistry { + provisions: Map; + agents: Map; +} +``` + +### How Built-In Writers Get Type Safety + +Each built-in writer defines a typed config interface and validates/narrows +at the boundary. The registry doesn't care — it passes +`Record` through. The writer narrows internally: + +```typescript +// writers/workflows.ts +interface WorkflowsConfig { + package: string; + env?: Record; +} + +export const workflowsWriter: ProvisionWriterDef = { + id: "workflows", + async write(config, _context) { + const c = config as WorkflowsConfig; // validated at boundary + return { + mcp_servers: [ + { + ref: c.package, + command: "npx", + args: ["-y", c.package], + env: c.env ?? {} + } + ] + }; + } +}; ``` -The writer has full ownership of how to translate LogicalConfig into -agent-specific files. It reads existing files when needed to perform -incremental updates rather than full overwrites. +The catalog definitions reference writers by string ID, not by import. +This is what makes the system open — a provision `{ writer: "my-custom", config: {...} }` +works as long as `"my-custom"` is registered before resolution runs. + +### Registry Lifecycle + +Core ships a `createDefaultRegistry()` that pre-registers all built-in +writers. The CLI calls this at startup. A future plugin would call +`registry.provisions.set("my-writer", myWriter)` before resolution. + +```typescript +function createDefaultRegistry(): WriterRegistry { + const provisions = new Map(); + provisions.set("workflows", workflowsWriter); + provisions.set("skills", skillsWriter); + provisions.set("knowledge", knowledgeWriter); + provisions.set("mcp-server", mcpServerWriter); + provisions.set("instruction", instructionWriter); + provisions.set("installable", installableWriter); + + const agents = new Map(); + agents.set("opencode", opencodeWriter); + + return { provisions, agents }; +} +``` + +### Why Not Pure Functions + Discriminated Unions? + +A discriminated union (`type Provision = { writer: "workflows", config: WorkflowsConfig } | ...`) +gives excellent compile-time safety but is a **closed set**. Adding a writer +from another package means modifying the union in core, which defeats +extensibility. + +The interface-based registry trades compile-time exhaustiveness for runtime +openness. The `Provision.writer` field is `string`, not a union — the +registry validates at resolution time that the writer exists. Built-in +writers still get internal type safety via their own config interfaces. + +### Built-In Provision Config Types + +For reference, the typed configs used internally by built-in writers: + +```typescript +interface WorkflowsConfig { + package: string; + env?: Record; +} + +interface SkillsConfig { + name: string; + version?: string; +} + +interface KnowledgeConfig { + name: string; + origin: string; +} + +interface McpServerConfig { + ref: string; + command: string; + args: string[]; + env?: Record; +} + +interface InstructionConfig { + text: string; +} + +interface InstallableConfig { + command: string; + check?: string; +} +``` + +These are not exported as part of the public contract. They are +implementation details of the built-in writers. + +## Agent Writers + +Each agent writer implements `AgentWriterDef`. The writer has full ownership +of how to translate LogicalConfig into agent-specific files. It reads +existing files when needed to perform incremental updates. ### OpenCode Writer (v1) @@ -262,17 +441,9 @@ key or object scope and merges with existing content. ## Provision Writers -Each provision writer transforms its config into LogicalConfig fragments -and/or CLI actions. Writers receive an optional `ResolutionContext` containing -the resolved options from dependent facets, allowing them to adapt their -output based on sibling selections. - -```typescript -type ProvisionWriterFn = ( - config: Record, - context: ResolutionContext -) => Promise>; -``` +Each provision writer implements `ProvisionWriterDef`. Writers receive a +`ResolutionContext` containing the resolved options from dependent facets, +allowing them to adapt output based on sibling selections. ### `workflows` writer @@ -331,44 +502,6 @@ Produces: one `instructions` entry. Produces: one `CliAction` for validation/installation of a CLI tool or dependency. -## Package Structure - -``` -packages/ - shared/src/ - types.ts # LogicalConfig, Provision, Facet, etc. - config.ts # read/write config.yaml and config.lock.yaml - ade/src/ - commands/ - setup.ts # interactive TUI setup - install.ts # resolve + generate (idempotent) - add.ts # modify single facet - remove.ts # remove facet selection - status.ts # show current state - core/ - resolver.ts # config.yaml + catalog → provisions → LogicalConfig - catalog/ - index.ts # catalog registry, exports all facets - facets/ - process.ts # process guidance facet - conventions.ts # conventions/skills facet - documentation.ts # documentation facet - frameworks.ts # development frameworks facet (multi-select) - adapters/ - writers/ # provision writers - workflows.ts - skills.ts - knowledge.ts - mcp-server.ts - instruction.ts - installable.ts - agents/ # agent writers - opencode.ts # v1 agent writer - tui/ - prompts.ts # interactive facet selection UI - utils/ -``` - ## V1 Catalog (TypeScript) Example of how the catalog is defined in code: @@ -440,20 +573,35 @@ export const frameworksFacet: Facet = { }; ``` -## Decisions (formerly open questions) +## Design Decisions + +1. **Two packages: `@ade/core` + `@ade/cli`.** Core owns all types, logic, + catalog, and writers. CLI is a thin shell for arg parsing and TUI. Core + is independently importable for programmatic use. No MCP server package — + runtime MCP servers are separate projects. + +2. **Interfaces for contracts, registries for dispatch.** Writer contracts + are open interfaces (`ProvisionWriterDef`, `AgentWriterDef`). Dispatch + uses `Map`-based registries, open at runtime. This enables future + extensibility from other packages without modifying core. + +3. **Built-in writers get internal type safety.** Each built-in writer + defines its own typed config interface and narrows from + `Record` at the boundary. The registry contract stays + generic; the implementation is specific. -1. **Catalog is TypeScript code.** No YAML catalog files. Facets, options, - and recipes are defined as typed objects in `src/catalog/`. This gives - type safety, IDE support, and natural versioning with the package. - Each facet lives in its own file under `catalog/facets/`. +4. **Catalog is TypeScript code.** No YAML catalog files. Facets, options, + and recipes are defined as typed objects in `core/src/catalog/`. This + gives type safety, IDE support, and natural versioning with the package. + Kept inside core for now; extractable to a separate package later along + the `Catalog` interface seam. -2. **Direct package imports over CLI subprocesses.** Provision writers for +5. **Direct package imports over CLI subprocesses.** Provision writers for `skills` and `knowledge` import `@codemcp/skills` and `@codemcp/knowledge` - as TypeScript dependencies and call their APIs. This provides type safety - and avoids brittle CLI flag contracts. CLI subprocess invocation is the - fallback for non-TypeScript or cross-runtime cases. + as TypeScript dependencies and call their APIs. CLI subprocess invocation + is the fallback for non-TypeScript or cross-runtime cases. -3. **`custom` section isolates user edits.** Only the `custom` block in +6. **`custom` section isolates user edits.** Only the `custom` block in `config.yaml` is user-managed. The rest is CLI-managed. This eliminates merge conflicts: the CLI never touches `custom`, and users never touch the rest. Agent writers merge both sections when generating output. From 80317903258c95bafa95a13fdb3c3c41ca8b7168 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 19:12:01 +0000 Subject: [PATCH 07/60] Add ADR 0001: Select @clack/prompts as TUI framework MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Evaluated @clack/prompts, @inquirer/prompts, and Ink against weighted criteria (visual polish, wizard suitability, ESM compatibility, LLM streaming, simplicity, footprint). Clack wins at +14 vs Ink +5 and Inquirer +1 in the Pugh matrix — best fit for a terminating wizard CLI. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- docs/adrs/0001-tui-framework-selection.md | 77 +++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 docs/adrs/0001-tui-framework-selection.md diff --git a/docs/adrs/0001-tui-framework-selection.md b/docs/adrs/0001-tui-framework-selection.md new file mode 100644 index 0000000..1623a4f --- /dev/null +++ b/docs/adrs/0001-tui-framework-selection.md @@ -0,0 +1,77 @@ +# ADR 0001: TUI Framework Selection for ADE CLI + +## Status + +Accepted + +## Context + +ADE (Agentic Development Environment) is a CLI tool that guides users through setup and configuration via interactive terminal prompts. The CLI needs to: + +- Present guided wizard-style flows (select, multiselect, confirm, text input) +- Display streaming output from LLM/agent processes +- Look visually polished and modern ("world-class-nerdy") while remaining enterprise-appropriate +- Terminate after completion (not a long-lived interactive TUI) +- Run in standard terminals including CI environments +- Be ESM-compatible (the project is `"type": "module"`) +- Integrate cleanly into a TypeScript monorepo (Node >= 22, pnpm) + +We evaluated the actively-maintained Node.js TUI/prompt frameworks available as of March 2026. Legacy libraries (blessed, neo-blessed, enquirer, prompts by terkelg) were excluded upfront due to abandonment or CJS-only distribution. + +## Decision + +We will use **@clack/prompts** as the TUI framework for the ADE CLI. + +## Evaluation: Weighted Pugh Matrix + +Criteria were weighted on a 3-point scale (1 = nice-to-have, 2 = important, 3 = critical). Each candidate was scored relative to a baseline of 0 (meets expectations), with +1 (better) and -1 (worse). + +| # | Criterion | Weight | @clack/prompts | @inquirer/prompts | Ink + @inkjs/ui | +| --- | ---------------------------------- | ------ | -------------- | ----------------- | --------------- | +| 1 | Visual polish out-of-the-box | 3 | +1 | -1 | +1 | +| 2 | Wizard/prompt flow suitability | 3 | +1 | +1 | 0 | +| 3 | ESM-native compatibility | 3 | +1 | 0 | 0 | +| 4 | LLM/streaming output support | 2 | +1 | -1 | +1 | +| 5 | Learning curve / simplicity | 2 | +1 | 0 | -1 | +| 6 | Bundle size / dependency footprint | 2 | +1 | 0 | -1 | +| 7 | Ecosystem / plugin breadth | 1 | -1 | +1 | 0 | +| 8 | Community size / adoption | 1 | 0 | +1 | +1 | +| 9 | Custom prompt authoring | 1 | 0 | +1 | +1 | + +**Weighted totals:** + +| Candidate | Calculation | Total | +| --------------------- | ------------------------------------------------------------------- | ------- | +| **@clack/prompts** | 3(+1) + 3(+1) + 3(+1) + 2(+1) + 2(+1) + 2(+1) + 1(-1) + 1(0) + 1(0) | **+14** | +| **@inquirer/prompts** | 3(-1) + 3(+1) + 3(0) + 2(-1) + 2(0) + 2(0) + 1(+1) + 1(+1) + 1(+1) | **+1** | +| **Ink + @inkjs/ui** | 3(+1) + 3(0) + 3(0) + 2(+1) + 2(-1) + 2(-1) + 1(0) + 1(+1) + 1(+1) | **+5** | + +@clack/prompts scores highest by a significant margin. + +## Rationale + +**@clack/prompts wins on the criteria that matter most to ADE:** + +1. **Visual polish (weight 3):** Clack's pre-styled prompts are the most visually striking of any Node.js prompt library. Unicode box-drawing, colored indicators, and thoughtful spacing produce a premium feel with zero configuration. @inquirer/prompts looks functional but plain; Ink can match Clack's aesthetics but requires manual styling. + +2. **Wizard suitability (weight 3):** ADE's CLI is a terminating wizard, not a persistent dashboard. Clack was purpose-built for sequential prompt flows with `intro()`, `outro()`, `group()`, and `spinner()`. Ink is designed for persistent, React-rendered UIs — architectural overkill for a flow that collects answers and exits. + +3. **ESM-native (weight 3):** Clack is ESM-only, aligning perfectly with ADE's `"type": "module"` configuration. No dual-format complications, no CJS shims. + +4. **LLM streaming (weight 2):** Clack includes native `stream` utilities designed for rendering LLM/agent output in the terminal — a direct match for ADE's agentic use case. Inquirer has no equivalent. + +5. **Simplicity (weight 2):** Clack's API is a flat set of async functions (`select()`, `text()`, `confirm()`, `spinner()`). No React knowledge required, no component tree to manage. This lowers the contribution barrier and reduces maintenance surface. + +6. **Footprint (weight 2):** Clack uses Node's built-in `styleText` instead of external color libraries. Minimal transitive dependencies. Ink pulls in React, Yoga (native binary), and a reconciler. + +**Where Clack is weaker — and why it doesn't matter:** + +- _Ecosystem breadth (weight 1):_ Inquirer has more community plugins (table prompts, file selectors, i18n). ADE's current scope doesn't require these, and `@clack/core` allows building custom prompts if needed. +- _Persistent UI:_ Clack can't render a dashboard or split-pane view. ADE doesn't need one — it's a wizard that terminates. + +## Consequences + +- The `@ade/cli` package will add `@clack/prompts` as a production dependency. +- All interactive CLI flows (setup, configuration, MCP server management) will use Clack's prompt primitives. +- If a future requirement emerges for persistent/dashboard-style terminal UI (e.g., a live agent monitoring view), we can evaluate adding Ink as a complementary dependency at that time. The two libraries are not mutually exclusive. +- Custom prompts beyond Clack's built-in set will be authored using `@clack/core`. From 0d0238e52c1141a55c093847bbf2682cffef3e72 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 19:27:05 +0000 Subject: [PATCH 08/60] =?UTF-8?q?Restructure=20packages:=20shared=E2=86=92?= =?UTF-8?q?core,=20ade=E2=86=92cli,=20remove=20mcp-server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename packages/shared to packages/core (@ade/shared → @ade/core), rename packages/ade to packages/cli, and delete packages/ade-mcp-server. Update all path mappings, aliases, and dependencies accordingly. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/ade-mcp-server/package.json | 33 --------------- packages/ade-mcp-server/src/index.spec.ts | 8 ---- packages/ade-mcp-server/src/index.ts | 2 - packages/ade-mcp-server/tsconfig.json | 10 ----- packages/ade/vitest.config.ts | 14 ------- .../{ade-mcp-server => cli}/eslint.config.mjs | 0 packages/{ade-mcp-server => cli}/nodemon.json | 0 packages/{ade => cli}/package.json | 2 +- packages/{ade => cli}/src/index.spec.ts | 0 packages/{ade => cli}/src/index.ts | 0 packages/{ade => cli}/src/version.ts | 0 .../tsconfig.build.json | 0 packages/{ade => cli}/tsconfig.json | 2 +- .../tsconfig.vitest.json | 0 .../{ade-mcp-server => cli}/vitest.config.ts | 2 +- packages/{ade => core}/eslint.config.mjs | 0 packages/{ade => core}/nodemon.json | 0 packages/{shared => core}/package.json | 2 +- packages/{shared => core}/src/index.ts | 0 packages/{shared => core}/src/types.spec.ts | 0 packages/{shared => core}/src/types.ts | 0 packages/{ade => core}/tsconfig.build.json | 0 packages/{shared => core}/tsconfig.json | 0 packages/{ade => core}/tsconfig.vitest.json | 0 packages/{shared => core}/vitest.config.ts | 0 packages/shared/eslint.config.mjs | 40 ------------------- packages/shared/nodemon.json | 7 ---- packages/shared/tsconfig.build.json | 8 ---- packages/shared/tsconfig.vitest.json | 7 ---- pnpm-lock.yaml | 36 ++--------------- tsconfig.json | 5 +-- 31 files changed, 10 insertions(+), 168 deletions(-) delete mode 100644 packages/ade-mcp-server/package.json delete mode 100644 packages/ade-mcp-server/src/index.spec.ts delete mode 100644 packages/ade-mcp-server/src/index.ts delete mode 100644 packages/ade-mcp-server/tsconfig.json delete mode 100644 packages/ade/vitest.config.ts rename packages/{ade-mcp-server => cli}/eslint.config.mjs (100%) rename packages/{ade-mcp-server => cli}/nodemon.json (100%) rename packages/{ade => cli}/package.json (96%) rename packages/{ade => cli}/src/index.spec.ts (100%) rename packages/{ade => cli}/src/index.ts (100%) rename packages/{ade => cli}/src/version.ts (100%) rename packages/{ade-mcp-server => cli}/tsconfig.build.json (100%) rename packages/{ade => cli}/tsconfig.json (73%) rename packages/{ade-mcp-server => cli}/tsconfig.vitest.json (100%) rename packages/{ade-mcp-server => cli}/vitest.config.ts (78%) rename packages/{ade => core}/eslint.config.mjs (100%) rename packages/{ade => core}/nodemon.json (100%) rename packages/{shared => core}/package.json (96%) rename packages/{shared => core}/src/index.ts (100%) rename packages/{shared => core}/src/types.spec.ts (100%) rename packages/{shared => core}/src/types.ts (100%) rename packages/{ade => core}/tsconfig.build.json (100%) rename packages/{shared => core}/tsconfig.json (100%) rename packages/{ade => core}/tsconfig.vitest.json (100%) rename packages/{shared => core}/vitest.config.ts (100%) delete mode 100644 packages/shared/eslint.config.mjs delete mode 100644 packages/shared/nodemon.json delete mode 100644 packages/shared/tsconfig.build.json delete mode 100644 packages/shared/tsconfig.vitest.json diff --git a/packages/ade-mcp-server/package.json b/packages/ade-mcp-server/package.json deleted file mode 100644 index 04cbee8..0000000 --- a/packages/ade-mcp-server/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "@ade/mcp-server", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "type": "module", - "publishConfig": { - "access": "public" - }, - "scripts": { - "build": "tsc -p tsconfig.build.json", - "clean:build": "rimraf ./dist", - "dev": "nodemon", - "lint": "eslint .", - "lint:fix": "eslint --fix .", - "format": "prettier --check .", - "format:fix": "prettier --write .", - "test": "vitest --run", - "test:watch": "vitest", - "typecheck": "tsc" - }, - "dependencies": { - "@ade/shared": "workspace:*" - }, - "devDependencies": { - "@typescript-eslint/eslint-plugin": "^8.21.0", - "@typescript-eslint/parser": "^8.21.0", - "eslint": "^9.18.0", - "eslint-config-prettier": "^10.0.1", - "prettier": "^3.4.2", - "rimraf": "^6.0.1", - "typescript": "^5.7.3" - } -} diff --git a/packages/ade-mcp-server/src/index.spec.ts b/packages/ade-mcp-server/src/index.spec.ts deleted file mode 100644 index 4476d36..0000000 --- a/packages/ade-mcp-server/src/index.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { name } from "./index.js"; - -describe("ade-mcp-server", () => { - it("should export a name", () => { - expect(name).toBe("@ade/mcp-server"); - }); -}); diff --git a/packages/ade-mcp-server/src/index.ts b/packages/ade-mcp-server/src/index.ts deleted file mode 100644 index 587d5d2..0000000 --- a/packages/ade-mcp-server/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// ADE MCP Server entry point -export const name = "@ade/mcp-server"; diff --git a/packages/ade-mcp-server/tsconfig.json b/packages/ade-mcp-server/tsconfig.json deleted file mode 100644 index f1452a0..0000000 --- a/packages/ade-mcp-server/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@ade/shared": ["../shared/src/index.ts"] - } - }, - "include": ["src/**/*"] -} diff --git a/packages/ade/vitest.config.ts b/packages/ade/vitest.config.ts deleted file mode 100644 index 0cec99a..0000000 --- a/packages/ade/vitest.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-check -/** @type {import("vitest/config").defineConfig} */ - -import { resolve } from "path"; -const baseConfig = await import("../../vitest.config.js"); - -export default { - ...baseConfig.default, - resolve: { - alias: { - "@ade/shared": resolve(__dirname, "../shared/src/index.ts") - } - } -}; diff --git a/packages/ade-mcp-server/eslint.config.mjs b/packages/cli/eslint.config.mjs similarity index 100% rename from packages/ade-mcp-server/eslint.config.mjs rename to packages/cli/eslint.config.mjs diff --git a/packages/ade-mcp-server/nodemon.json b/packages/cli/nodemon.json similarity index 100% rename from packages/ade-mcp-server/nodemon.json rename to packages/cli/nodemon.json diff --git a/packages/ade/package.json b/packages/cli/package.json similarity index 96% rename from packages/ade/package.json rename to packages/cli/package.json index 44e664a..5dfd3e7 100644 --- a/packages/ade/package.json +++ b/packages/cli/package.json @@ -22,7 +22,7 @@ "typecheck": "tsc" }, "dependencies": { - "@ade/shared": "workspace:*" + "@ade/core": "workspace:*" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^8.21.0", diff --git a/packages/ade/src/index.spec.ts b/packages/cli/src/index.spec.ts similarity index 100% rename from packages/ade/src/index.spec.ts rename to packages/cli/src/index.spec.ts diff --git a/packages/ade/src/index.ts b/packages/cli/src/index.ts similarity index 100% rename from packages/ade/src/index.ts rename to packages/cli/src/index.ts diff --git a/packages/ade/src/version.ts b/packages/cli/src/version.ts similarity index 100% rename from packages/ade/src/version.ts rename to packages/cli/src/version.ts diff --git a/packages/ade-mcp-server/tsconfig.build.json b/packages/cli/tsconfig.build.json similarity index 100% rename from packages/ade-mcp-server/tsconfig.build.json rename to packages/cli/tsconfig.build.json diff --git a/packages/ade/tsconfig.json b/packages/cli/tsconfig.json similarity index 73% rename from packages/ade/tsconfig.json rename to packages/cli/tsconfig.json index f1452a0..d905c61 100644 --- a/packages/ade/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "@ade/shared": ["../shared/src/index.ts"] + "@ade/core": ["../core/src/index.ts"] } }, "include": ["src/**/*"] diff --git a/packages/ade-mcp-server/tsconfig.vitest.json b/packages/cli/tsconfig.vitest.json similarity index 100% rename from packages/ade-mcp-server/tsconfig.vitest.json rename to packages/cli/tsconfig.vitest.json diff --git a/packages/ade-mcp-server/vitest.config.ts b/packages/cli/vitest.config.ts similarity index 78% rename from packages/ade-mcp-server/vitest.config.ts rename to packages/cli/vitest.config.ts index 0cec99a..30c6dca 100644 --- a/packages/ade-mcp-server/vitest.config.ts +++ b/packages/cli/vitest.config.ts @@ -8,7 +8,7 @@ export default { ...baseConfig.default, resolve: { alias: { - "@ade/shared": resolve(__dirname, "../shared/src/index.ts") + "@ade/core": resolve(__dirname, "../core/src/index.ts") } } }; diff --git a/packages/ade/eslint.config.mjs b/packages/core/eslint.config.mjs similarity index 100% rename from packages/ade/eslint.config.mjs rename to packages/core/eslint.config.mjs diff --git a/packages/ade/nodemon.json b/packages/core/nodemon.json similarity index 100% rename from packages/ade/nodemon.json rename to packages/core/nodemon.json diff --git a/packages/shared/package.json b/packages/core/package.json similarity index 96% rename from packages/shared/package.json rename to packages/core/package.json index ad81667..f699f57 100644 --- a/packages/shared/package.json +++ b/packages/core/package.json @@ -1,5 +1,5 @@ { - "name": "@ade/shared", + "name": "@ade/core", "main": "dist/index.js", "types": "dist/index.d.ts", "type": "module", diff --git a/packages/shared/src/index.ts b/packages/core/src/index.ts similarity index 100% rename from packages/shared/src/index.ts rename to packages/core/src/index.ts diff --git a/packages/shared/src/types.spec.ts b/packages/core/src/types.spec.ts similarity index 100% rename from packages/shared/src/types.spec.ts rename to packages/core/src/types.spec.ts diff --git a/packages/shared/src/types.ts b/packages/core/src/types.ts similarity index 100% rename from packages/shared/src/types.ts rename to packages/core/src/types.ts diff --git a/packages/ade/tsconfig.build.json b/packages/core/tsconfig.build.json similarity index 100% rename from packages/ade/tsconfig.build.json rename to packages/core/tsconfig.build.json diff --git a/packages/shared/tsconfig.json b/packages/core/tsconfig.json similarity index 100% rename from packages/shared/tsconfig.json rename to packages/core/tsconfig.json diff --git a/packages/ade/tsconfig.vitest.json b/packages/core/tsconfig.vitest.json similarity index 100% rename from packages/ade/tsconfig.vitest.json rename to packages/core/tsconfig.vitest.json diff --git a/packages/shared/vitest.config.ts b/packages/core/vitest.config.ts similarity index 100% rename from packages/shared/vitest.config.ts rename to packages/core/vitest.config.ts diff --git a/packages/shared/eslint.config.mjs b/packages/shared/eslint.config.mjs deleted file mode 100644 index 1483555..0000000 --- a/packages/shared/eslint.config.mjs +++ /dev/null @@ -1,40 +0,0 @@ -import js from "@eslint/js"; -import { parser, configs } from "typescript-eslint"; -import prettier from "eslint-config-prettier"; - -export default [ - js.configs.recommended, - ...configs.recommended, - prettier, - { - // Config for TypeScript files - files: ["**/*.{ts,tsx}"], - languageOptions: { - parser, - parserOptions: { - project: ["./tsconfig.json", "./tsconfig.vitest.json"] - } - } - }, - { - // Config for JavaScript files - no TypeScript parsing - files: ["**/*.{js,jsx}"], - ...js.configs.recommended - }, - { - // Relaxed rules for test files - files: ["**/*.test.ts", "**/*.spec.ts"], - rules: { - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": "off" - } - }, - { - ignores: [ - "**/node_modules/**", - "**/dist/**", - ".pnpm-store/**", - "pnpm-lock.yaml" - ] - } -]; diff --git a/packages/shared/nodemon.json b/packages/shared/nodemon.json deleted file mode 100644 index e5d466d..0000000 --- a/packages/shared/nodemon.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/nodemon.json", - "watch": ["./src/**", "./node_modules/@mme/**/dist/**"], - "ignoreRoot": [], - "ext": "ts,js", - "exec": "pnpm typecheck && pnpm build" -} diff --git a/packages/shared/tsconfig.build.json b/packages/shared/tsconfig.build.json deleted file mode 100644 index 7cbd949..0000000 --- a/packages/shared/tsconfig.build.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.build.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": ["src/**/*"], - "exclude": ["**/*.spec.ts"] -} diff --git a/packages/shared/tsconfig.vitest.json b/packages/shared/tsconfig.vitest.json deleted file mode 100644 index f8add23..0000000 --- a/packages/shared/tsconfig.vitest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "moduleResolution": "bundler" - }, - "include": ["vitest.config.ts"] -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d988566..4532f41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,11 +89,11 @@ importers: specifier: ^3.0.3 version: 3.2.4(@types/node@22.19.11)(yaml@2.8.2) - packages/ade: + packages/cli: dependencies: - "@ade/shared": + "@ade/core": specifier: workspace:* - version: link:../shared + version: link:../core devDependencies: "@typescript-eslint/eslint-plugin": specifier: ^8.21.0 @@ -117,35 +117,7 @@ importers: specifier: ^5.7.3 version: 5.9.3 - packages/ade-mcp-server: - dependencies: - "@ade/shared": - specifier: workspace:* - version: link:../shared - devDependencies: - "@typescript-eslint/eslint-plugin": - specifier: ^8.21.0 - version: 8.56.0(@typescript-eslint/parser@8.56.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) - "@typescript-eslint/parser": - specifier: ^8.21.0 - version: 8.56.0(eslint@9.39.2)(typescript@5.9.3) - eslint: - specifier: ^9.18.0 - version: 9.39.2 - eslint-config-prettier: - specifier: ^10.0.1 - version: 10.1.8(eslint@9.39.2) - prettier: - specifier: ^3.4.2 - version: 3.8.1 - rimraf: - specifier: ^6.0.1 - version: 6.1.3 - typescript: - specifier: ^5.7.3 - version: 5.9.3 - - packages/shared: + packages/core: devDependencies: "@typescript-eslint/eslint-plugin": specifier: ^8.21.0 diff --git a/tsconfig.json b/tsconfig.json index a4c6b29..0cf3371 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,9 +5,8 @@ "types": ["vitest/globals"], "baseUrl": ".", "paths": { - "@ade/shared/*": ["packages/shared/src/*"], - "@ade/cli/*": ["packages/ade/src/*"], - "@ade/mcp-server/*": ["packages/ade-mcp-server/src/*"] + "@ade/core/*": ["packages/core/src/*"], + "@ade/cli/*": ["packages/cli/src/*"] } } } From 5d7ae9df66060ffa046b95a4f5afe6de969cf28c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 19:28:05 +0000 Subject: [PATCH 09/60] Add .prettierignore to exclude dist directories from formatting checks https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- .prettierignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1521c8b --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +dist From 34e63334e6a94481cf9691849306c3de3d97a123 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 19:32:33 +0000 Subject: [PATCH 10/60] feat(core): add writer registry with ProvisionWriterDef and AgentWriterDef - Add ProvisionWriterDef, AgentWriterDef, WriterRegistry interfaces to types - Implement createRegistry, registerProvisionWriter, registerAgentWriter - Implement createDefaultRegistry with stub writers for all 6 provision types + opencode agent - Remove static-structure type tests, replace with behavioral registry tests - 8 behavioral tests covering registration, lookup, overwrite, and default registry https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/core/src/index.ts | 13 +++ packages/core/src/registry.spec.ts | 141 +++++++++++++++++++++++++++++ packages/core/src/registry.ts | 67 ++++++++++++++ packages/core/src/types.spec.ts | 26 ------ packages/core/src/types.ts | 20 ++++ 5 files changed, 241 insertions(+), 26 deletions(-) create mode 100644 packages/core/src/registry.spec.ts create mode 100644 packages/core/src/registry.ts delete mode 100644 packages/core/src/types.spec.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d7e4634..74b923c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -13,3 +13,16 @@ export { export { type ResolutionContext, type ResolvedFacet } from "./types.js"; export { type UserConfig, type LockFile } from "./types.js"; export { type ProvisionWriter } from "./types.js"; +export { + type ProvisionWriterDef, + type AgentWriterDef, + type WriterRegistry +} from "./types.js"; +export { + createRegistry, + registerProvisionWriter, + getProvisionWriter, + registerAgentWriter, + getAgentWriter, + createDefaultRegistry +} from "./registry.js"; diff --git a/packages/core/src/registry.spec.ts b/packages/core/src/registry.spec.ts new file mode 100644 index 0000000..c5747d0 --- /dev/null +++ b/packages/core/src/registry.spec.ts @@ -0,0 +1,141 @@ +import { describe, it, expect, vi } from "vitest"; +import { + createRegistry, + registerProvisionWriter, + registerAgentWriter, + getProvisionWriter, + getAgentWriter, + createDefaultRegistry +} from "./registry.js"; +import type { + ProvisionWriterDef, + AgentWriterDef, + LogicalConfig, + ResolutionContext +} from "./types.js"; + +describe("registry", () => { + describe("createRegistry", () => { + it("returns a registry with empty provisions and agents maps", () => { + const registry = createRegistry(); + expect(registry.provisions.size).toBe(0); + expect(registry.agents.size).toBe(0); + }); + }); + + describe("registerProvisionWriter / getProvisionWriter", () => { + it("registers a provision writer and retrieves it by id", async () => { + const registry = createRegistry(); + + const mockFragment: Partial = { + instructions: ["use typescript strict mode"] + }; + + const writer: ProvisionWriterDef = { + id: "skills", + write: vi.fn().mockResolvedValue(mockFragment) + }; + + registerProvisionWriter(registry, writer); + + const found = getProvisionWriter(registry, "skills"); + expect(found).toBeDefined(); + expect(found!.id).toBe("skills"); + + // Behavioral: actually call write() and verify the result + const context: ResolutionContext = { resolved: {} }; + const result = await found!.write({ lang: "ts" }, context); + expect(result).toEqual(mockFragment); + expect(writer.write).toHaveBeenCalledWith({ lang: "ts" }, context); + }); + + it("overwrites a writer when registering with the same id", async () => { + const registry = createRegistry(); + + const first: ProvisionWriterDef = { + id: "workflows", + write: vi.fn().mockResolvedValue({ instructions: ["first"] }) + }; + const second: ProvisionWriterDef = { + id: "workflows", + write: vi.fn().mockResolvedValue({ instructions: ["second"] }) + }; + + registerProvisionWriter(registry, first); + registerProvisionWriter(registry, second); + + const found = getProvisionWriter(registry, "workflows"); + const result = await found!.write({}, { resolved: {} }); + expect(result).toEqual({ instructions: ["second"] }); + expect(first.write).not.toHaveBeenCalled(); + }); + + it("returns undefined for a non-existent provision writer", () => { + const registry = createRegistry(); + const found = getProvisionWriter(registry, "does-not-exist"); + expect(found).toBeUndefined(); + }); + }); + + describe("registerAgentWriter / getAgentWriter", () => { + it("registers an agent writer and can call install()", async () => { + const registry = createRegistry(); + + const mockInstall = vi.fn().mockResolvedValue(undefined); + const agent: AgentWriterDef = { + id: "opencode", + install: mockInstall + }; + + registerAgentWriter(registry, agent); + + const found = getAgentWriter(registry, "opencode"); + expect(found).toBeDefined(); + expect(found!.id).toBe("opencode"); + + // Behavioral: call install() and verify it was invoked correctly + const config: LogicalConfig = { + mcp_servers: [], + instructions: ["be helpful"], + cli_actions: [], + knowledge_sources: [] + }; + await found!.install(config, "/tmp/my-project"); + expect(mockInstall).toHaveBeenCalledWith(config, "/tmp/my-project"); + }); + + it("returns undefined for a non-existent agent writer", () => { + const registry = createRegistry(); + const found = getAgentWriter(registry, "nope"); + expect(found).toBeUndefined(); + }); + }); + + describe("createDefaultRegistry", () => { + it("has all 6 built-in provision writer IDs registered", () => { + const registry = createDefaultRegistry(); + const expectedIds = [ + "workflows", + "skills", + "knowledge", + "mcp-server", + "instruction", + "installable" + ]; + for (const id of expectedIds) { + expect( + getProvisionWriter(registry, id), + `expected provision writer "${id}" to be registered` + ).toBeDefined(); + } + expect(registry.provisions.size).toBe(6); + }); + + it("has the 'opencode' agent writer registered", () => { + const registry = createDefaultRegistry(); + const agent = getAgentWriter(registry, "opencode"); + expect(agent).toBeDefined(); + expect(agent!.id).toBe("opencode"); + }); + }); +}); diff --git a/packages/core/src/registry.ts b/packages/core/src/registry.ts new file mode 100644 index 0000000..91aa7fa --- /dev/null +++ b/packages/core/src/registry.ts @@ -0,0 +1,67 @@ +import type { + WriterRegistry, + ProvisionWriterDef, + AgentWriterDef +} from "./types.js"; + +export function createRegistry(): WriterRegistry { + return { + provisions: new Map(), + agents: new Map() + }; +} + +export function registerProvisionWriter( + registry: WriterRegistry, + writer: ProvisionWriterDef +): void { + registry.provisions.set(writer.id, writer); +} + +export function getProvisionWriter( + registry: WriterRegistry, + id: string +): ProvisionWriterDef | undefined { + return registry.provisions.get(id); +} + +export function registerAgentWriter( + registry: WriterRegistry, + agent: AgentWriterDef +): void { + registry.agents.set(agent.id, agent); +} + +export function getAgentWriter( + registry: WriterRegistry, + id: string +): AgentWriterDef | undefined { + return registry.agents.get(id); +} + +export function createDefaultRegistry(): WriterRegistry { + const registry = createRegistry(); + + const provisionIds = [ + "workflows", + "skills", + "knowledge", + "mcp-server", + "instruction", + "installable" + ] as const; + + for (const id of provisionIds) { + registerProvisionWriter(registry, { + id, + write: async () => ({}) + }); + } + + registerAgentWriter(registry, { + id: "opencode", + install: async () => {} + }); + + return registry; +} diff --git a/packages/core/src/types.spec.ts b/packages/core/src/types.spec.ts deleted file mode 100644 index 5e2ad05..0000000 --- a/packages/core/src/types.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, it, expect } from "vitest"; -import type { Facet, LogicalConfig } from "./types.js"; - -describe("types", () => { - it("should allow creating a facet with multi-select", () => { - const facet: Facet = { - id: "frameworks", - label: "Development Frameworks", - description: "Which tech stacks the project uses", - required: false, - multiSelect: true, - options: [] - }; - expect(facet.multiSelect).toBe(true); - }); - - it("should allow creating an empty logical config", () => { - const config: LogicalConfig = { - mcp_servers: [], - instructions: [], - cli_actions: [], - knowledge_sources: [] - }; - expect(config.mcp_servers).toHaveLength(0); - }); -}); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index bf81c97..c17fc12 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -89,3 +89,23 @@ export interface LockFile { choices: Record; logical_config: LogicalConfig; } + +// --- Writer contracts (open, any package can implement) --- + +export interface ProvisionWriterDef { + id: string; + write( + config: Record, + context: ResolutionContext + ): Promise>; +} + +export interface AgentWriterDef { + id: string; + install(config: LogicalConfig, projectRoot: string): Promise; +} + +export interface WriterRegistry { + provisions: Map; + agents: Map; +} From 55db837bc44824e5dd2d6f8c2ab19331ddbc0cd5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 19:33:14 +0000 Subject: [PATCH 11/60] chore(core): add .prettierignore to exclude dist from format checks The core package's format script runs prettier from its own directory, so it needs its own .prettierignore to skip built output in dist/. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/core/.prettierignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/core/.prettierignore diff --git a/packages/core/.prettierignore b/packages/core/.prettierignore new file mode 100644 index 0000000..1521c8b --- /dev/null +++ b/packages/core/.prettierignore @@ -0,0 +1 @@ +dist From f2e1bf462129e97f95ec42e37c1b24e66b2c21e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 19:36:15 +0000 Subject: [PATCH 12/60] feat(core): add instruction provision writer - Implement instructionWriter (ProvisionWriterDef) that extracts text config and returns it as a LogicalConfig instructions fragment - 5 behavioral tests: text passthrough, multi-line, output shape https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/core/src/writers/instruction.spec.ts | 42 +++++++++++++++++++ packages/core/src/writers/instruction.ts | 8 ++++ 2 files changed, 50 insertions(+) create mode 100644 packages/core/src/writers/instruction.spec.ts create mode 100644 packages/core/src/writers/instruction.ts diff --git a/packages/core/src/writers/instruction.spec.ts b/packages/core/src/writers/instruction.spec.ts new file mode 100644 index 0000000..51c846b --- /dev/null +++ b/packages/core/src/writers/instruction.spec.ts @@ -0,0 +1,42 @@ +import { describe, it, expect } from "vitest"; +import { instructionWriter } from "./instruction.js"; +import type { ResolutionContext } from "../types.js"; + +describe("instructionWriter", () => { + const context: ResolutionContext = { resolved: {} }; + + it("has id 'instruction'", () => { + expect(instructionWriter.id).toBe("instruction"); + }); + + it("returns the text wrapped in an instructions array", async () => { + const result = await instructionWriter.write( + { text: "Always use strict mode" }, + context + ); + expect(result).toEqual({ instructions: ["Always use strict mode"] }); + }); + + it("passes through the exact text without modification", async () => { + const verbatim = " leading spaces and trailing spaces "; + const result = await instructionWriter.write({ text: verbatim }, context); + expect(result).toEqual({ instructions: [verbatim] }); + }); + + it("only returns instructions, not other LogicalConfig keys", async () => { + const result = await instructionWriter.write( + { text: "some instruction" }, + context + ); + expect(Object.keys(result)).toEqual(["instructions"]); + expect(result).not.toHaveProperty("mcp_servers"); + expect(result).not.toHaveProperty("cli_actions"); + expect(result).not.toHaveProperty("knowledge_sources"); + }); + + it("handles multi-line text correctly", async () => { + const multiLine = "Line one\nLine two\nLine three"; + const result = await instructionWriter.write({ text: multiLine }, context); + expect(result).toEqual({ instructions: [multiLine] }); + }); +}); diff --git a/packages/core/src/writers/instruction.ts b/packages/core/src/writers/instruction.ts new file mode 100644 index 0000000..c3d3ab2 --- /dev/null +++ b/packages/core/src/writers/instruction.ts @@ -0,0 +1,8 @@ +import type { ProvisionWriterDef } from "../types.js"; + +export const instructionWriter: ProvisionWriterDef = { + id: "instruction", + async write(config) { + return { instructions: [(config as { text: string }).text] }; + } +}; From d8b07cfd831ea2126e2bfed4968f6a499462ac65 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 19:39:28 +0000 Subject: [PATCH 13/60] feat(core): add workflows provision writer - Implement workflowsWriter (ProvisionWriterDef) producing McpServerEntry with npx command and configurable env - 5 behavioral tests https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/core/src/writers/workflows.spec.ts | 61 +++++++++++++++++++++ packages/core/src/writers/workflows.ts | 21 +++++++ 2 files changed, 82 insertions(+) create mode 100644 packages/core/src/writers/workflows.spec.ts create mode 100644 packages/core/src/writers/workflows.ts diff --git a/packages/core/src/writers/workflows.spec.ts b/packages/core/src/writers/workflows.spec.ts new file mode 100644 index 0000000..343ced0 --- /dev/null +++ b/packages/core/src/writers/workflows.spec.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from "vitest"; +import { workflowsWriter } from "./workflows.js"; +import type { ResolutionContext } from "../types.js"; + +describe("workflowsWriter", () => { + const context: ResolutionContext = { resolved: {} }; + + it("has id 'workflows'", () => { + expect(workflowsWriter.id).toBe("workflows"); + }); + + it("returns mcp_servers with correct ref, command, and args for a given package", async () => { + const result = await workflowsWriter.write( + { package: "@codemcp/workflows-server" }, + context + ); + expect(result).toEqual({ + mcp_servers: [ + { + ref: "@codemcp/workflows-server", + command: "npx", + args: ["-y", "@codemcp/workflows-server"], + env: {} + } + ] + }); + }); + + it("includes env in the entry when env is specified", async () => { + const result = await workflowsWriter.write( + { + package: "@codemcp/workflows-server", + env: { API_KEY: "secret", NODE_ENV: "production" } + }, + context + ); + expect(result.mcp_servers![0].env).toEqual({ + API_KEY: "secret", + NODE_ENV: "production" + }); + }); + + it("defaults env to an empty object when not specified", async () => { + const result = await workflowsWriter.write( + { package: "@codemcp/workflows-server" }, + context + ); + expect(result.mcp_servers![0].env).toEqual({}); + }); + + it("only returns mcp_servers, not other LogicalConfig keys", async () => { + const result = await workflowsWriter.write( + { package: "@codemcp/workflows-server" }, + context + ); + expect(Object.keys(result)).toEqual(["mcp_servers"]); + expect(result).not.toHaveProperty("instructions"); + expect(result).not.toHaveProperty("cli_actions"); + expect(result).not.toHaveProperty("knowledge_sources"); + }); +}); diff --git a/packages/core/src/writers/workflows.ts b/packages/core/src/writers/workflows.ts new file mode 100644 index 0000000..c60aee4 --- /dev/null +++ b/packages/core/src/writers/workflows.ts @@ -0,0 +1,21 @@ +import type { ProvisionWriterDef } from "../types.js"; + +export const workflowsWriter: ProvisionWriterDef = { + id: "workflows", + async write(config) { + const { package: pkg, env } = config as { + package: string; + env?: Record; + }; + return { + mcp_servers: [ + { + ref: pkg, + command: "npx", + args: ["-y", pkg], + env: env ?? {} + } + ] + }; + } +}; From 2584b51ec7dd124f7e2337b4a2be3136f54eeb43 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 19:43:12 +0000 Subject: [PATCH 14/60] feat(core): add catalog module with process facet - Implement getDefaultCatalog(), getFacet(), getOption() lookup functions - Add process facet with codemcp-workflows and native-agents-md options - 6 behavioral tests including registry integration check https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/core/src/catalog/catalog.spec.ts | 64 +++++++++++++++++++++ packages/core/src/catalog/facets/process.ts | 32 +++++++++++ packages/core/src/catalog/index.ts | 16 ++++++ 3 files changed, 112 insertions(+) create mode 100644 packages/core/src/catalog/catalog.spec.ts create mode 100644 packages/core/src/catalog/facets/process.ts create mode 100644 packages/core/src/catalog/index.ts diff --git a/packages/core/src/catalog/catalog.spec.ts b/packages/core/src/catalog/catalog.spec.ts new file mode 100644 index 0000000..10239e7 --- /dev/null +++ b/packages/core/src/catalog/catalog.spec.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from "vitest"; +import { getDefaultCatalog, getFacet, getOption } from "./index.js"; +import { createDefaultRegistry, getProvisionWriter } from "../registry.js"; + +describe("catalog", () => { + describe("getDefaultCatalog", () => { + it("returns a catalog containing at least the 'process' facet", () => { + const catalog = getDefaultCatalog(); + const process = getFacet(catalog, "process"); + expect(process).toBeDefined(); + expect(process!.id).toBe("process"); + }); + }); + + describe("getFacet / getOption", () => { + it("process facet's 'codemcp-workflows' option has a recipe referencing the 'workflows' writer", () => { + const catalog = getDefaultCatalog(); + const process = getFacet(catalog, "process")!; + const option = getOption(process, "codemcp-workflows"); + + expect(option).toBeDefined(); + expect(option!.recipe.some((p) => p.writer === "workflows")).toBe(true); + }); + + it("process facet's 'native-agents-md' option has a recipe referencing only the 'instruction' writer", () => { + const catalog = getDefaultCatalog(); + const process = getFacet(catalog, "process")!; + const option = getOption(process, "native-agents-md"); + + expect(option).toBeDefined(); + const writers = option!.recipe.map((p) => p.writer); + expect(writers).toEqual(["instruction"]); + }); + + it("returns undefined for a nonexistent facet id", () => { + const catalog = getDefaultCatalog(); + expect(getFacet(catalog, "nonexistent")).toBeUndefined(); + }); + + it("returns undefined for a nonexistent option id", () => { + const catalog = getDefaultCatalog(); + const process = getFacet(catalog, "process")!; + expect(getOption(process, "nonexistent")).toBeUndefined(); + }); + }); + + describe("catalog + registry integration", () => { + it("every recipe provision references a writer that exists in the default registry", () => { + const catalog = getDefaultCatalog(); + const registry = createDefaultRegistry(); + + for (const facet of catalog.facets) { + for (const option of facet.options) { + for (const provision of option.recipe) { + expect( + getProvisionWriter(registry, provision.writer), + `writer "${provision.writer}" referenced in ${facet.id}/${option.id} must exist in default registry` + ).toBeDefined(); + } + } + } + }); + }); +}); diff --git a/packages/core/src/catalog/facets/process.ts b/packages/core/src/catalog/facets/process.ts new file mode 100644 index 0000000..b7c5e3d --- /dev/null +++ b/packages/core/src/catalog/facets/process.ts @@ -0,0 +1,32 @@ +import type { Facet } from "../../types.js"; + +export const processFacet: Facet = { + id: "process", + label: "Process", + description: "How your AI agent receives and executes tasks", + required: true, + options: [ + { + id: "codemcp-workflows", + label: "CodeMCP Workflows", + description: "Use codemcp workflow files to drive agent tasks", + recipe: [ + { + writer: "workflows", + config: {} + } + ] + }, + { + id: "native-agents-md", + label: "Native agents.md", + description: "Use a plain agents.md instruction file", + recipe: [ + { + writer: "instruction", + config: {} + } + ] + } + ] +}; diff --git a/packages/core/src/catalog/index.ts b/packages/core/src/catalog/index.ts new file mode 100644 index 0000000..e455164 --- /dev/null +++ b/packages/core/src/catalog/index.ts @@ -0,0 +1,16 @@ +import type { Catalog, Facet, Option } from "../types.js"; +import { processFacet } from "./facets/process.js"; + +export function getDefaultCatalog(): Catalog { + return { + facets: [processFacet] + }; +} + +export function getFacet(catalog: Catalog, id: string): Facet | undefined { + return catalog.facets.find((f) => f.id === id); +} + +export function getOption(facet: Facet, id: string): Option | undefined { + return facet.options.find((o) => o.id === id); +} From 50396050ac471b8f2025898826551ae2eb8e8fe7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 19:46:40 +0000 Subject: [PATCH 15/60] feat(core): add config module for YAML read/write - Implement readUserConfig/writeUserConfig for config.yaml - Implement readLockFile/writeLockFile for config.lock.yaml - Add yaml package dependency - 7 behavioral tests: roundtrip, null on missing, multi-select, custom section https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/core/package.json | 3 + packages/core/src/config.spec.ts | 162 +++++++++++++++++++++++++++++++ packages/core/src/config.ts | 39 ++++++++ packages/core/src/index.ts | 6 ++ pnpm-lock.yaml | 8 ++ 5 files changed, 218 insertions(+) create mode 100644 packages/core/src/config.spec.ts create mode 100644 packages/core/src/config.ts diff --git a/packages/core/package.json b/packages/core/package.json index f699f57..7547de7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -26,5 +26,8 @@ "prettier": "^3.4.2", "rimraf": "^6.0.1", "typescript": "^5.7.3" + }, + "dependencies": { + "yaml": "^2.8.2" } } diff --git a/packages/core/src/config.spec.ts b/packages/core/src/config.spec.ts new file mode 100644 index 0000000..c5d7451 --- /dev/null +++ b/packages/core/src/config.spec.ts @@ -0,0 +1,162 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { parse as parseYaml } from "yaml"; + +import { + readUserConfig, + writeUserConfig, + readLockFile, + writeLockFile +} from "./config.js"; + +import type { UserConfig, LockFile } from "./types.js"; + +describe("config", () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), "ade-config-test-")); + }); + + afterEach(async () => { + await rm(tempDir, { recursive: true, force: true }); + }); + + describe("UserConfig roundtrip", () => { + it("write then read produces identical data", async () => { + const config: UserConfig = { + choices: { + language: "typescript", + framework: "react" + } + }; + + await writeUserConfig(tempDir, config); + const result = await readUserConfig(tempDir); + + expect(result).toEqual(config); + }); + + it("returns null when config.yaml does not exist", async () => { + const result = await readUserConfig(tempDir); + expect(result).toBeNull(); + }); + + it("multi-select choices (string[]) survive roundtrip", async () => { + const config: UserConfig = { + choices: { + language: "typescript", + plugins: ["eslint", "prettier", "vitest"] + } + }; + + await writeUserConfig(tempDir, config); + const result = await readUserConfig(tempDir); + + expect(result).toEqual(config); + expect(Array.isArray(result!.choices.plugins)).toBe(true); + expect(result!.choices.plugins).toEqual(["eslint", "prettier", "vitest"]); + }); + + it("custom section with mcp_servers and instructions survives roundtrip", async () => { + const config: UserConfig = { + choices: { + language: "python" + }, + custom: { + mcp_servers: [ + { + ref: "my-server", + command: "npx", + args: ["-y", "my-mcp-server"], + env: { API_KEY: "test-key" } + } + ], + instructions: ["Always use type hints", "Follow PEP 8 style guide"] + } + }; + + await writeUserConfig(tempDir, config); + const result = await readUserConfig(tempDir); + + expect(result).toEqual(config); + expect(result!.custom!.mcp_servers).toHaveLength(1); + expect(result!.custom!.mcp_servers![0].ref).toBe("my-server"); + expect(result!.custom!.instructions).toEqual([ + "Always use type hints", + "Follow PEP 8 style guide" + ]); + }); + }); + + describe("LockFile roundtrip", () => { + it("write then read produces identical data", async () => { + const lock: LockFile = { + version: 1, + generated_at: "2026-03-14T00:00:00.000Z", + choices: { + language: "typescript", + framework: "react" + }, + logical_config: { + mcp_servers: [ + { + ref: "typescript-server", + command: "npx", + args: ["-y", "ts-server"], + env: {} + } + ], + instructions: ["Use strict TypeScript"], + cli_actions: [ + { + command: "npm", + args: ["install"], + phase: "install" + } + ], + knowledge_sources: [ + { + name: "ts-docs", + origin: "https://typescriptlang.org", + description: "TypeScript documentation" + } + ] + } + }; + + await writeLockFile(tempDir, lock); + const result = await readLockFile(tempDir); + + expect(result).toEqual(lock); + }); + + it("returns null when config.lock.yaml does not exist", async () => { + const result = await readLockFile(tempDir); + expect(result).toBeNull(); + }); + }); + + describe("YAML validity", () => { + it("config file written is valid YAML", async () => { + const config: UserConfig = { + choices: { + language: "typescript", + tools: ["eslint", "prettier"] + }, + custom: { + instructions: ["Be concise"] + } + }; + + await writeUserConfig(tempDir, config); + + const raw = await readFile(join(tempDir, "config.yaml"), "utf-8"); + const parsed = parseYaml(raw); + + expect(parsed).toEqual(config); + }); + }); +}); diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts new file mode 100644 index 0000000..07dd5ac --- /dev/null +++ b/packages/core/src/config.ts @@ -0,0 +1,39 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { parse, stringify } from "yaml"; +import type { UserConfig, LockFile } from "./types.js"; + +const CONFIG_FILE = "config.yaml"; +const LOCK_FILE = "config.lock.yaml"; + +export async function readUserConfig(dir: string): Promise { + try { + const raw = await readFile(join(dir, CONFIG_FILE), "utf-8"); + return parse(raw) as UserConfig; + } catch { + return null; + } +} + +export async function writeUserConfig( + dir: string, + config: UserConfig +): Promise { + await writeFile(join(dir, CONFIG_FILE), stringify(config), "utf-8"); +} + +export async function readLockFile(dir: string): Promise { + try { + const raw = await readFile(join(dir, LOCK_FILE), "utf-8"); + return parse(raw) as LockFile; + } catch { + return null; + } +} + +export async function writeLockFile( + dir: string, + lock: LockFile +): Promise { + await writeFile(join(dir, LOCK_FILE), stringify(lock), "utf-8"); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 74b923c..7052e8e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -13,6 +13,12 @@ export { export { type ResolutionContext, type ResolvedFacet } from "./types.js"; export { type UserConfig, type LockFile } from "./types.js"; export { type ProvisionWriter } from "./types.js"; +export { + readUserConfig, + writeUserConfig, + readLockFile, + writeLockFile +} from "./config.js"; export { type ProvisionWriterDef, type AgentWriterDef, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4532f41..d5f9059 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,10 @@ settings: importers: .: + dependencies: + yaml: + specifier: ^2.8.2 + version: 2.8.2 devDependencies: "@braintree/sanitize-url": specifier: 7.1.1 @@ -118,6 +122,10 @@ importers: version: 5.9.3 packages/core: + dependencies: + yaml: + specifier: ^2.8.2 + version: 2.8.2 devDependencies: "@typescript-eslint/eslint-plugin": specifier: ^8.21.0 From 1a71f47f4e6a83c80b56a0ceaa54630ce90d0087 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 19:50:39 +0000 Subject: [PATCH 16/60] =?UTF-8?q?feat(core):=20add=20resolver=20engine=20f?= =?UTF-8?q?or=20config=20=E2=86=92=20LogicalConfig=20resolution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement resolve(userConfig, catalog, registry) producing merged LogicalConfig - Supports single-select facets, custom section merge, mcp_server dedup by ref - Throws on unknown options, silently skips unknown facets - 8 behavioral tests including integration with real writers and catalog https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/core/src/index.ts | 1 + packages/core/src/resolver.spec.ts | 194 +++++++++++++++++++++++++++++ packages/core/src/resolver.ts | 79 ++++++++++++ 3 files changed, 274 insertions(+) create mode 100644 packages/core/src/resolver.spec.ts create mode 100644 packages/core/src/resolver.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7052e8e..ac2a235 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -32,3 +32,4 @@ export { getAgentWriter, createDefaultRegistry } from "./registry.js"; +export { resolve } from "./resolver.js"; diff --git a/packages/core/src/resolver.spec.ts b/packages/core/src/resolver.spec.ts new file mode 100644 index 0000000..e57a1e3 --- /dev/null +++ b/packages/core/src/resolver.spec.ts @@ -0,0 +1,194 @@ +import { describe, it, expect } from "vitest"; +import { resolve } from "./resolver.js"; +import { getDefaultCatalog } from "./catalog/index.js"; +import { createRegistry, registerProvisionWriter } from "./registry.js"; +import { instructionWriter } from "./writers/instruction.js"; +import { workflowsWriter } from "./writers/workflows.js"; +import type { UserConfig, WriterRegistry, Catalog } from "./types.js"; + +function buildRegistry(): WriterRegistry { + const registry = createRegistry(); + registerProvisionWriter(registry, instructionWriter); + registerProvisionWriter(registry, workflowsWriter); + return registry; +} + +describe("resolve", () => { + let catalog: Catalog; + let registry: WriterRegistry; + + beforeEach(() => { + catalog = getDefaultCatalog(); + registry = buildRegistry(); + }); + + describe("single-select resolution", () => { + it("resolves codemcp-workflows to LogicalConfig with mcp_servers", async () => { + const userConfig: UserConfig = { + choices: { process: "codemcp-workflows" } + }; + + const result = await resolve(userConfig, catalog, registry); + + // workflows writer produces mcp_servers + expect(result.mcp_servers).toBeDefined(); + expect(result.mcp_servers.length).toBeGreaterThanOrEqual(1); + // Should have all LogicalConfig fields + expect(result).toHaveProperty("instructions"); + expect(result).toHaveProperty("cli_actions"); + expect(result).toHaveProperty("knowledge_sources"); + }); + }); + + describe("different option selection", () => { + it("resolves native-agents-md to LogicalConfig with instructions but no mcp_servers", async () => { + const userConfig: UserConfig = { + choices: { process: "native-agents-md" } + }; + + const result = await resolve(userConfig, catalog, registry); + + // instruction writer produces instructions + expect(result.instructions).toBeDefined(); + // native-agents-md has no workflows provision, so no mcp_servers + expect(result.mcp_servers).toEqual([]); + }); + }); + + describe("empty choices", () => { + it("returns an empty LogicalConfig when no choices are provided", async () => { + const userConfig: UserConfig = { + choices: {} + }; + + const result = await resolve(userConfig, catalog, registry); + + expect(result).toEqual({ + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [] + }); + }); + }); + + describe("custom section merge", () => { + it("merges custom instructions and mcp_servers into the output", async () => { + const userConfig: UserConfig = { + choices: {}, + custom: { + instructions: ["Always use TypeScript strict mode"], + mcp_servers: [ + { + ref: "my-custom-server", + command: "node", + args: ["server.js"], + env: {} + } + ] + } + }; + + const result = await resolve(userConfig, catalog, registry); + + expect(result.instructions).toContain( + "Always use TypeScript strict mode" + ); + expect(result.mcp_servers).toContainEqual( + expect.objectContaining({ ref: "my-custom-server" }) + ); + }); + + it("merges custom section with recipe-produced config", async () => { + const userConfig: UserConfig = { + choices: { process: "codemcp-workflows" }, + custom: { + instructions: ["Extra instruction"] + } + }; + + const result = await resolve(userConfig, catalog, registry); + + // Should have both recipe mcp_servers and custom instructions + expect(result.mcp_servers.length).toBeGreaterThanOrEqual(1); + expect(result.instructions).toContain("Extra instruction"); + }); + }); + + describe("unknown facet in choices", () => { + it("ignores unknown facet ids without throwing", async () => { + const userConfig: UserConfig = { + choices: { "nonexistent-facet": "some-option" } + }; + + // Should not throw + const result = await resolve(userConfig, catalog, registry); + + expect(result).toEqual({ + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [] + }); + }); + }); + + describe("unknown option in choices", () => { + it("throws when facet exists but option id does not", async () => { + const userConfig: UserConfig = { + choices: { process: "nonexistent-option" } + }; + + await expect(resolve(userConfig, catalog, registry)).rejects.toThrow(); + }); + }); + + describe("MCP server dedup by ref", () => { + it("deduplicates mcp_servers by ref, keeping the last one", async () => { + // Create a custom registry with a writer that produces duplicate refs + const dedupRegistry = createRegistry(); + registerProvisionWriter(dedupRegistry, { + id: "workflows", + async write() { + return { + mcp_servers: [ + { + ref: "duplicate-server", + command: "npx", + args: ["-y", "pkg-a"], + env: {} + } + ] + }; + } + }); + registerProvisionWriter(dedupRegistry, instructionWriter); + + // Also add a custom mcp_server with the same ref but different args + const userConfig: UserConfig = { + choices: { process: "codemcp-workflows" }, + custom: { + mcp_servers: [ + { + ref: "duplicate-server", + command: "node", + args: ["custom-server.js"], + env: { CUSTOM: "true" } + } + ] + } + }; + + const result = await resolve(userConfig, catalog, dedupRegistry); + + // Should only have one entry with ref "duplicate-server" + const duplicates = result.mcp_servers.filter( + (s) => s.ref === "duplicate-server" + ); + expect(duplicates).toHaveLength(1); + // Last one wins — the custom one should survive + expect(duplicates[0].command).toBe("node"); + expect(duplicates[0].env).toEqual({ CUSTOM: "true" }); + }); + }); +}); diff --git a/packages/core/src/resolver.ts b/packages/core/src/resolver.ts new file mode 100644 index 0000000..607376d --- /dev/null +++ b/packages/core/src/resolver.ts @@ -0,0 +1,79 @@ +import type { + UserConfig, + Catalog, + WriterRegistry, + LogicalConfig, + McpServerEntry, + ResolutionContext +} from "./types.js"; +import { getFacet, getOption } from "./catalog/index.js"; +import { getProvisionWriter } from "./registry.js"; + +export async function resolve( + userConfig: UserConfig, + catalog: Catalog, + registry: WriterRegistry +): Promise { + const result: LogicalConfig = { + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [] + }; + + const context: ResolutionContext = { resolved: {} }; + + for (const [facetId, optionId] of Object.entries(userConfig.choices)) { + const facet = getFacet(catalog, facetId); + if (!facet) { + continue; + } + + const selectedId = Array.isArray(optionId) ? optionId[0] : optionId; + const option = getOption(facet, selectedId); + if (!option) { + throw new Error(`Unknown option "${selectedId}" for facet "${facetId}"`); + } + + context.resolved[facetId] = { optionId: selectedId, option }; + + for (const provision of option.recipe) { + const writer = getProvisionWriter(registry, provision.writer); + if (!writer) { + continue; + } + const partial = await writer.write(provision.config, context); + if (partial.mcp_servers) { + result.mcp_servers.push(...partial.mcp_servers); + } + if (partial.instructions) { + result.instructions.push(...partial.instructions); + } + if (partial.cli_actions) { + result.cli_actions.push(...partial.cli_actions); + } + if (partial.knowledge_sources) { + result.knowledge_sources.push(...partial.knowledge_sources); + } + } + } + + // Merge custom section + if (userConfig.custom) { + if (userConfig.custom.instructions) { + result.instructions.push(...userConfig.custom.instructions); + } + if (userConfig.custom.mcp_servers) { + result.mcp_servers.push(...userConfig.custom.mcp_servers); + } + } + + // Dedup mcp_servers by ref (last wins) + const serversByRef = new Map(); + for (const server of result.mcp_servers) { + serversByRef.set(server.ref, server); + } + result.mcp_servers = Array.from(serversByRef.values()); + + return result; +} From 128d03411f842d2005ae92e0720f8de1ba1c3485 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 20:00:55 +0000 Subject: [PATCH 17/60] feat(cli): add setup command with @clack/prompts TUI - Implement runSetup() orchestrating facet selection, resolution, and config writing - Uses @clack/prompts for intro/outro, select prompts with skip option - Supports user cancellation, skippable facets - 5 behavioral tests with mocked @clack/prompts and @ade/core - Add @clack/prompts dependency https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- package.json | 5 +- packages/cli/package.json | 3 +- packages/cli/src/commands/setup.spec.ts | 179 ++++++++++++++++++++++++ packages/cli/src/commands/setup.ts | 61 ++++++++ packages/cli/src/index.spec.ts | 8 -- pnpm-lock.yaml | 32 +++++ 6 files changed, 278 insertions(+), 10 deletions(-) create mode 100644 packages/cli/src/commands/setup.spec.ts create mode 100644 packages/cli/src/commands/setup.ts delete mode 100644 packages/cli/src/index.spec.ts diff --git a/package.json b/package.json index 85b156a..c64d153 100644 --- a/package.json +++ b/package.json @@ -69,5 +69,8 @@ "vitepress-plugin-mermaid": "2.0.17", "vitest": "^3.0.3" }, - "packageManager": "pnpm@9.14.2" + "packageManager": "pnpm@9.14.2", + "dependencies": { + "yaml": "^2.8.2" + } } diff --git a/packages/cli/package.json b/packages/cli/package.json index 5dfd3e7..1874ac5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -22,7 +22,8 @@ "typecheck": "tsc" }, "dependencies": { - "@ade/core": "workspace:*" + "@ade/core": "workspace:*", + "@clack/prompts": "^1.1.0" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^8.21.0", diff --git a/packages/cli/src/commands/setup.spec.ts b/packages/cli/src/commands/setup.spec.ts new file mode 100644 index 0000000..aea4b5d --- /dev/null +++ b/packages/cli/src/commands/setup.spec.ts @@ -0,0 +1,179 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { Catalog, UserConfig, LogicalConfig, LockFile } from "@ade/core"; + +// ── Mocks ──────────────────────────────────────────────────────────────────── + +vi.mock("@clack/prompts", () => ({ + intro: vi.fn(), + outro: vi.fn(), + select: vi.fn(), + confirm: vi.fn(), + isCancel: vi.fn().mockReturnValue(false), + cancel: vi.fn(), + spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) +})); + +vi.mock("@ade/core", async (importOriginal) => { + const actual = (await importOriginal()) as typeof import("@ade/core"); + return { + ...actual, + writeUserConfig: vi.fn().mockResolvedValue(undefined), + writeLockFile: vi.fn().mockResolvedValue(undefined), + resolve: vi.fn().mockResolvedValue({ + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [] + } satisfies LogicalConfig) + }; +}); + +import * as clack from "@clack/prompts"; +import { writeUserConfig, writeLockFile, resolve } from "@ade/core"; +import { runSetup } from "./setup.js"; + +// ── Test catalog fixture ───────────────────────────────────────────────────── + +const testCatalog: Catalog = { + facets: [ + { + id: "process", + label: "Process", + description: "How your agent works", + required: true, + options: [ + { + id: "workflow-a", + label: "Workflow A", + description: "First workflow option", + recipe: [] + }, + { + id: "workflow-b", + label: "Workflow B", + description: "Second workflow option", + recipe: [] + } + ] + }, + { + id: "testing", + label: "Testing", + description: "Testing strategy", + required: false, + options: [ + { + id: "vitest", + label: "Vitest", + description: "Use vitest", + recipe: [] + }, + { + id: "jest", + label: "Jest", + description: "Use jest", + recipe: [] + } + ] + } + ] +}; + +// ── Tests ──────────────────────────────────────────────────────────────────── + +describe("runSetup", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("prompts for each catalog facet and writes user config", async () => { + // User selects "workflow-a" for process, "vitest" for testing + vi.mocked(clack.select) + .mockResolvedValueOnce("workflow-a") + .mockResolvedValueOnce("vitest"); + + await runSetup("/tmp/test-project", testCatalog); + + // select() called once per facet + expect(clack.select).toHaveBeenCalledTimes(2); + + // writeUserConfig called with collected choices + expect(writeUserConfig).toHaveBeenCalledWith( + "/tmp/test-project", + expect.objectContaining({ + choices: { process: "workflow-a", testing: "vitest" } + }) + ); + }); + + it("resolves the config and writes the lock file", async () => { + const mockLogical: LogicalConfig = { + mcp_servers: [], + instructions: ["do stuff"], + cli_actions: [], + knowledge_sources: [] + }; + vi.mocked(resolve).mockResolvedValueOnce(mockLogical); + vi.mocked(clack.select) + .mockResolvedValueOnce("workflow-a") + .mockResolvedValueOnce("vitest"); + + await runSetup("/tmp/test-project", testCatalog); + + // resolve() called with the user config, catalog, and a registry + expect(resolve).toHaveBeenCalledOnce(); + const resolveArgs = vi.mocked(resolve).mock.calls[0]; + expect(resolveArgs[0]).toMatchObject({ + choices: { process: "workflow-a", testing: "vitest" } + }); + + // writeLockFile called with the resolved logical config + expect(writeLockFile).toHaveBeenCalledWith( + "/tmp/test-project", + expect.objectContaining({ + version: 1, + logical_config: mockLogical + }) + ); + }); + + it("excludes skipped facets from choices", async () => { + // User selects workflow-a for process, skips testing (returns null sentinel) + vi.mocked(clack.select) + .mockResolvedValueOnce("workflow-a") + .mockResolvedValueOnce("__skip__"); + + await runSetup("/tmp/test-project", testCatalog); + + expect(writeUserConfig).toHaveBeenCalledWith( + "/tmp/test-project", + expect.objectContaining({ + choices: { process: "workflow-a" } + }) + ); + }); + + it("aborts without writing files when user cancels", async () => { + // First select returns a cancel symbol + const cancelSymbol = Symbol("cancel"); + vi.mocked(clack.select).mockResolvedValueOnce(cancelSymbol); + vi.mocked(clack.isCancel).mockReturnValue(true); + + await runSetup("/tmp/test-project", testCatalog); + + expect(writeUserConfig).not.toHaveBeenCalled(); + expect(writeLockFile).not.toHaveBeenCalled(); + expect(clack.cancel).toHaveBeenCalled(); + }); + + it("calls intro and outro from @clack/prompts", async () => { + vi.mocked(clack.select) + .mockResolvedValueOnce("workflow-a") + .mockResolvedValueOnce("vitest"); + + await runSetup("/tmp/test-project", testCatalog); + + expect(clack.intro).toHaveBeenCalled(); + expect(clack.outro).toHaveBeenCalled(); + }); +}); diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts new file mode 100644 index 0000000..1544786 --- /dev/null +++ b/packages/cli/src/commands/setup.ts @@ -0,0 +1,61 @@ +import * as clack from "@clack/prompts"; +import { + type Catalog, + type UserConfig, + type LockFile, + writeUserConfig, + writeLockFile, + resolve, + createDefaultRegistry +} from "@ade/core"; + +export async function runSetup( + projectRoot: string, + catalog: Catalog +): Promise { + clack.intro("ade setup"); + + const choices: Record = {}; + + for (const facet of catalog.facets) { + const options = facet.options.map((o) => ({ + value: o.id, + label: o.label, + hint: o.description + })); + + if (!facet.required) { + options.push({ value: "__skip__", label: "Skip", hint: "" }); + } + + const selected = await clack.select({ + message: facet.label, + options + }); + + if (typeof selected === "symbol") { + clack.cancel("Setup cancelled."); + return; + } + + if (selected !== "__skip__") { + choices[facet.id] = selected as string; + } + } + + const userConfig: UserConfig = { choices }; + const registry = createDefaultRegistry(); + const logicalConfig = await resolve(userConfig, catalog, registry); + + await writeUserConfig(projectRoot, userConfig); + + const lockFile: LockFile = { + version: 1, + generated_at: new Date().toISOString(), + choices: userConfig.choices, + logical_config: logicalConfig + }; + await writeLockFile(projectRoot, lockFile); + + clack.outro("Setup complete!"); +} diff --git a/packages/cli/src/index.spec.ts b/packages/cli/src/index.spec.ts deleted file mode 100644 index 4baf786..0000000 --- a/packages/cli/src/index.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { version } from "./version.js"; - -describe("ade cli", () => { - it("should export a version", () => { - expect(version).toBeDefined(); - }); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5f9059..b8ab063 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,6 +98,9 @@ importers: "@ade/core": specifier: workspace:* version: link:../core + "@clack/prompts": + specifier: ^1.1.0 + version: 1.1.0 devDependencies: "@typescript-eslint/eslint-plugin": specifier: ^8.21.0 @@ -377,6 +380,18 @@ packages: integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ== } + "@clack/core@1.1.0": + resolution: + { + integrity: sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA== + } + + "@clack/prompts@1.1.0": + resolution: + { + integrity: sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g== + } + "@docsearch/css@3.9.0": resolution: { @@ -4077,6 +4092,12 @@ packages: } engines: { node: ">=10" } + sisteransi@1.0.5: + resolution: + { + integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + } + slice-ansi@5.0.0: resolution: { @@ -4883,6 +4904,15 @@ snapshots: "@chevrotain/utils@11.0.3": {} + "@clack/core@1.1.0": + dependencies: + sisteransi: 1.0.5 + + "@clack/prompts@1.1.0": + dependencies: + "@clack/core": 1.1.0 + sisteransi: 1.0.5 + "@docsearch/css@3.9.0": {} "@docsearch/js@3.9.0(@algolia/client-search@5.49.0)(search-insights@2.17.3)": @@ -7075,6 +7105,8 @@ snapshots: dependencies: semver: 7.7.4 + sisteransi@1.0.5: {} + slice-ansi@5.0.0: dependencies: ansi-styles: 6.2.3 From bbd91828c810180cff59b9d59d5e561a997a1138 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 20:05:18 +0000 Subject: [PATCH 18/60] fix(cli): remove unused type imports flagged by root eslint Root-level eslint config (unlike package-level) enforces no-unused-vars on test files. Remove UserConfig and LockFile type imports that were only used inside vi.mock() factory context. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/cli/src/commands/setup.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/setup.spec.ts b/packages/cli/src/commands/setup.spec.ts index aea4b5d..9fb67d2 100644 --- a/packages/cli/src/commands/setup.spec.ts +++ b/packages/cli/src/commands/setup.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import type { Catalog, UserConfig, LogicalConfig, LockFile } from "@ade/core"; +import type { Catalog, LogicalConfig } from "@ade/core"; // ── Mocks ──────────────────────────────────────────────────────────────────── From 09bd02be03a8e7b8535ac876e152f9beda943381 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 20:27:20 +0000 Subject: [PATCH 19/60] test(cli): add setup integration test in real temp dir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exercises the full pipeline end-to-end (catalog → resolver → YAML I/O) with only @clack/prompts mocked. Verifies config.yaml and config.lock.yaml are written correctly for both process options, cancellation produces no files, and YAML roundtrips cleanly. Also exports getDefaultCatalog, getFacet, getOption from @ade/core. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- .../src/commands/setup.integration.spec.ts | 111 ++++++++++++++++++ packages/core/src/index.ts | 1 + 2 files changed, 112 insertions(+) create mode 100644 packages/cli/src/commands/setup.integration.spec.ts diff --git a/packages/cli/src/commands/setup.integration.spec.ts b/packages/cli/src/commands/setup.integration.spec.ts new file mode 100644 index 0000000..72d1bde --- /dev/null +++ b/packages/cli/src/commands/setup.integration.spec.ts @@ -0,0 +1,111 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +// Mock only the TUI — everything else (catalog, registry, resolver, config I/O) is real +vi.mock("@clack/prompts", () => ({ + intro: vi.fn(), + outro: vi.fn(), + select: vi.fn(), + confirm: vi.fn(), + isCancel: vi.fn().mockReturnValue(false), + cancel: vi.fn(), + spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) +})); + +import * as clack from "@clack/prompts"; +import { runSetup } from "./setup.js"; +import { readUserConfig, readLockFile } from "@ade/core"; +import { getDefaultCatalog } from "../../../core/src/catalog/index.js"; + +describe("setup integration (real temp dir)", () => { + let dir: string; + + beforeEach(async () => { + vi.clearAllMocks(); + dir = await mkdtemp(join(tmpdir(), "ade-setup-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("writes config.yaml and config.lock.yaml for codemcp-workflows", async () => { + const catalog = getDefaultCatalog(); + + vi.mocked(clack.select).mockResolvedValueOnce("codemcp-workflows"); + + await runSetup(dir, catalog); + + // ── config.yaml ────────────────────────────────────────────────────── + const config = await readUserConfig(dir); + expect(config).not.toBeNull(); + expect(config!.choices).toEqual({ process: "codemcp-workflows" }); + + // ── config.lock.yaml ───────────────────────────────────────────────── + const lock = await readLockFile(dir); + expect(lock).not.toBeNull(); + expect(lock!.version).toBe(1); + expect(lock!.choices).toEqual({ process: "codemcp-workflows" }); + expect(lock!.generated_at).toBeTruthy(); + + // LogicalConfig was produced by the resolver (stubs return empty partials) + const lc = lock!.logical_config; + expect(lc).toMatchObject({ + mcp_servers: expect.any(Array), + instructions: expect.any(Array), + cli_actions: expect.any(Array), + knowledge_sources: expect.any(Array) + }); + }); + + it("writes config.yaml and config.lock.yaml for native-agents-md", async () => { + const catalog = getDefaultCatalog(); + + vi.mocked(clack.select).mockResolvedValueOnce("native-agents-md"); + + await runSetup(dir, catalog); + + const config = await readUserConfig(dir); + expect(config!.choices).toEqual({ process: "native-agents-md" }); + + const lock = await readLockFile(dir); + expect(lock!.choices).toEqual({ process: "native-agents-md" }); + }); + + it("does not write any files when user cancels", async () => { + const catalog = getDefaultCatalog(); + const cancelSymbol = Symbol("cancel"); + + vi.mocked(clack.select).mockResolvedValueOnce(cancelSymbol); + vi.mocked(clack.isCancel).mockReturnValue(true); + + await runSetup(dir, catalog); + + const config = await readUserConfig(dir); + expect(config).toBeNull(); + + const lock = await readLockFile(dir); + expect(lock).toBeNull(); + }); + + it("produces valid YAML that roundtrips through read", async () => { + const catalog = getDefaultCatalog(); + + vi.mocked(clack.select).mockResolvedValueOnce("codemcp-workflows"); + + await runSetup(dir, catalog); + + // Read the raw file and re-parse to ensure valid YAML + const { readFile } = await import("node:fs/promises"); + const rawConfig = await readFile(join(dir, "config.yaml"), "utf-8"); + const rawLock = await readFile(join(dir, "config.lock.yaml"), "utf-8"); + + // Both files should be non-empty valid YAML (not "undefined" or empty) + expect(rawConfig.length).toBeGreaterThan(0); + expect(rawLock.length).toBeGreaterThan(0); + expect(rawConfig).toContain("codemcp-workflows"); + expect(rawLock).toContain("codemcp-workflows"); + }); +}); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ac2a235..7a66f62 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -33,3 +33,4 @@ export { createDefaultRegistry } from "./registry.js"; export { resolve } from "./resolver.js"; +export { getDefaultCatalog, getFacet, getOption } from "./catalog/index.js"; From e08125cbfcd38b50aa0488387211c9ebe013bb36 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 20:27:43 +0000 Subject: [PATCH 20/60] chore(cli): add .prettierignore to exclude dist from format checks https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/cli/.prettierignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/cli/.prettierignore diff --git a/packages/cli/.prettierignore b/packages/cli/.prettierignore new file mode 100644 index 0000000..1521c8b --- /dev/null +++ b/packages/cli/.prettierignore @@ -0,0 +1 @@ +dist From 45ec7bbc6d0c8dd1f3d372cb3e17c531dd778dc3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 10:24:49 +0000 Subject: [PATCH 21/60] feat(cli): add CLI entry point with setup command Wire up `ade setup [dir]` as a runnable command. Uses the real catalog and calls runSetup with @clack/prompts TUI. Also supports `--version` and prints usage on unknown commands. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/cli/src/index.ts | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index af2851a..04bb61c 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,6 +1,29 @@ #!/usr/bin/env node -// ADE CLI entry point -// Commands: setup, install, add, remove, status +import { version } from "./version.js"; +import { runSetup } from "./commands/setup.js"; +import { getDefaultCatalog } from "@ade/core"; -export { version } from "./version.js"; +const args = process.argv.slice(2); +const command = args[0]; + +if (command === "setup") { + const projectRoot = args[1] ?? process.cwd(); + const catalog = getDefaultCatalog(); + await runSetup(projectRoot, catalog); +} else if (command === "--version" || command === "-v") { + console.log(version); +} else { + console.log(`ade v${version}`); + console.log(); + console.log("Usage: ade [options]"); + console.log(); + console.log("Commands:"); + console.log( + " setup [dir] Configure your AI agent (default: current dir)" + ); + console.log(); + console.log("Options:"); + console.log(" -v, --version Show version"); + process.exitCode = command ? 1 : 0; +} From 0c01011125319a747ad420271d6289f58f309e7d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 10:35:21 +0000 Subject: [PATCH 22/60] feat: complete vertical slice with Claude Code agent writer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire real provision writers (instruction, workflows) into the default registry instead of stubs. Add config data to process facet recipes so the resolver produces real LogicalConfig output. Implement a Claude Code agent writer that takes the resolved LogicalConfig and writes: - AGENTS.md with instruction bullets - .claude/settings.json with MCP server entries (preserving existing keys) Call the agent writer from setup after resolution, closing the full loop: TUI → resolve → config files → agent output files. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- .../src/commands/setup.integration.spec.ts | 31 ++++- packages/cli/src/commands/setup.spec.ts | 6 +- packages/cli/src/commands/setup.ts | 8 +- packages/core/src/agents/claude-code.spec.ts | 122 ++++++++++++++++++ packages/core/src/agents/claude-code.ts | 67 ++++++++++ packages/core/src/catalog/facets/process.ts | 12 +- packages/core/src/index.ts | 1 + packages/core/src/registry.spec.ts | 6 +- packages/core/src/registry.ts | 21 ++- 9 files changed, 247 insertions(+), 27 deletions(-) create mode 100644 packages/core/src/agents/claude-code.spec.ts create mode 100644 packages/core/src/agents/claude-code.ts diff --git a/packages/cli/src/commands/setup.integration.spec.ts b/packages/cli/src/commands/setup.integration.spec.ts index 72d1bde..7a9fa6e 100644 --- a/packages/cli/src/commands/setup.integration.spec.ts +++ b/packages/cli/src/commands/setup.integration.spec.ts @@ -50,17 +50,28 @@ describe("setup integration (real temp dir)", () => { expect(lock!.choices).toEqual({ process: "codemcp-workflows" }); expect(lock!.generated_at).toBeTruthy(); - // LogicalConfig was produced by the resolver (stubs return empty partials) + // LogicalConfig was produced by the real resolver with real writers const lc = lock!.logical_config; - expect(lc).toMatchObject({ - mcp_servers: expect.any(Array), - instructions: expect.any(Array), - cli_actions: expect.any(Array), - knowledge_sources: expect.any(Array) + expect(lc.mcp_servers).toHaveLength(1); + expect(lc.mcp_servers[0].ref).toBe("@anthropic/codemcp"); + expect(lc.instructions.length).toBeGreaterThan(0); + + // ── Agent output: .claude/settings.json ────────────────────────────── + const { readFile } = await import("node:fs/promises"); + const settings = JSON.parse( + await readFile(join(dir, ".claude", "settings.json"), "utf-8") + ); + expect(settings.mcpServers["@anthropic/codemcp"]).toMatchObject({ + command: "npx", + args: ["-y", "@anthropic/codemcp"] }); + + // ── Agent output: AGENTS.md ───────────────────────────────────────── + const agentsMd = await readFile(join(dir, "AGENTS.md"), "utf-8"); + expect(agentsMd).toContain("codemcp workflow files"); }); - it("writes config.yaml and config.lock.yaml for native-agents-md", async () => { + it("writes config.yaml, lock, and AGENTS.md for native-agents-md", async () => { const catalog = getDefaultCatalog(); vi.mocked(clack.select).mockResolvedValueOnce("native-agents-md"); @@ -72,6 +83,12 @@ describe("setup integration (real temp dir)", () => { const lock = await readLockFile(dir); expect(lock!.choices).toEqual({ process: "native-agents-md" }); + expect(lock!.logical_config.instructions.length).toBeGreaterThan(0); + + // Agent output: AGENTS.md is written with instruction text + const { readFile } = await import("node:fs/promises"); + const agentsMd = await readFile(join(dir, "AGENTS.md"), "utf-8"); + expect(agentsMd).toContain("AGENTS.md"); }); it("does not write any files when user cancels", async () => { diff --git a/packages/cli/src/commands/setup.spec.ts b/packages/cli/src/commands/setup.spec.ts index 9fb67d2..7d78cfa 100644 --- a/packages/cli/src/commands/setup.spec.ts +++ b/packages/cli/src/commands/setup.spec.ts @@ -24,7 +24,11 @@ vi.mock("@ade/core", async (importOriginal) => { instructions: [], cli_actions: [], knowledge_sources: [] - } satisfies LogicalConfig) + } satisfies LogicalConfig), + getAgentWriter: vi.fn().mockReturnValue({ + id: "claude-code", + install: vi.fn().mockResolvedValue(undefined) + }) }; }); diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 1544786..cdbdd19 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -6,7 +6,8 @@ import { writeUserConfig, writeLockFile, resolve, - createDefaultRegistry + createDefaultRegistry, + getAgentWriter } from "@ade/core"; export async function runSetup( @@ -57,5 +58,10 @@ export async function runSetup( }; await writeLockFile(projectRoot, lockFile); + const agentWriter = getAgentWriter(registry, "claude-code"); + if (agentWriter) { + await agentWriter.install(logicalConfig, projectRoot); + } + clack.outro("Setup complete!"); } diff --git a/packages/core/src/agents/claude-code.spec.ts b/packages/core/src/agents/claude-code.spec.ts new file mode 100644 index 0000000..e76752a --- /dev/null +++ b/packages/core/src/agents/claude-code.spec.ts @@ -0,0 +1,122 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { LogicalConfig } from "../types.js"; +import { claudeCodeWriter } from "./claude-code.js"; + +describe("claudeCodeWriter", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "ade-agent-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("writes AGENTS.md with instructions", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: ["Use workflow files.", "Follow conventions."], + cli_actions: [], + knowledge_sources: [] + }; + + await claudeCodeWriter.install(config, dir); + + const content = await readFile(join(dir, "AGENTS.md"), "utf-8"); + expect(content).toContain("# AGENTS"); + expect(content).toContain("- Use workflow files."); + expect(content).toContain("- Follow conventions."); + }); + + it("writes .claude/settings.json with MCP servers", async () => { + const config: LogicalConfig = { + mcp_servers: [ + { + ref: "@anthropic/codemcp", + command: "npx", + args: ["-y", "@anthropic/codemcp"], + env: {} + } + ], + instructions: [], + cli_actions: [], + knowledge_sources: [] + }; + + await claudeCodeWriter.install(config, dir); + + const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8"); + const settings = JSON.parse(raw); + expect(settings.mcpServers["@anthropic/codemcp"]).toEqual({ + command: "npx", + args: ["-y", "@anthropic/codemcp"] + }); + }); + + it("preserves existing settings.json keys", async () => { + const { mkdir, writeFile } = await import("node:fs/promises"); + await mkdir(join(dir, ".claude"), { recursive: true }); + await writeFile( + join(dir, ".claude", "settings.json"), + JSON.stringify({ customKey: true }), + "utf-8" + ); + + const config: LogicalConfig = { + mcp_servers: [ + { + ref: "my-server", + command: "node", + args: ["server.js"], + env: { API_KEY: "secret" } + } + ], + instructions: [], + cli_actions: [], + knowledge_sources: [] + }; + + await claudeCodeWriter.install(config, dir); + + const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8"); + const settings = JSON.parse(raw); + expect(settings.customKey).toBe(true); + expect(settings.mcpServers["my-server"]).toEqual({ + command: "node", + args: ["server.js"], + env: { API_KEY: "secret" } + }); + }); + + it("skips AGENTS.md when no instructions", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [] + }; + + await claudeCodeWriter.install(config, dir); + + await expect(readFile(join(dir, "AGENTS.md"), "utf-8")).rejects.toThrow(); + }); + + it("skips settings.json when no MCP servers", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: ["hello"], + cli_actions: [], + knowledge_sources: [] + }; + + await claudeCodeWriter.install(config, dir); + + await expect( + readFile(join(dir, ".claude", "settings.json"), "utf-8") + ).rejects.toThrow(); + }); +}); diff --git a/packages/core/src/agents/claude-code.ts b/packages/core/src/agents/claude-code.ts new file mode 100644 index 0000000..f4b1add --- /dev/null +++ b/packages/core/src/agents/claude-code.ts @@ -0,0 +1,67 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { AgentWriterDef, LogicalConfig } from "../types.js"; + +export const claudeCodeWriter: AgentWriterDef = { + id: "claude-code", + async install(config: LogicalConfig, projectRoot: string) { + await writeAgentsMd(config, projectRoot); + await writeClaudeSettings(config, projectRoot); + } +}; + +async function writeAgentsMd( + config: LogicalConfig, + projectRoot: string +): Promise { + if (config.instructions.length === 0) return; + + const lines = ["# AGENTS", ""]; + for (const instruction of config.instructions) { + lines.push(`- ${instruction}`); + } + lines.push(""); + + await writeFile(join(projectRoot, "AGENTS.md"), lines.join("\n"), "utf-8"); +} + +async function writeClaudeSettings( + config: LogicalConfig, + projectRoot: string +): Promise { + if (config.mcp_servers.length === 0) return; + + const claudeDir = join(projectRoot, ".claude"); + await mkdir(claudeDir, { recursive: true }); + + const settingsPath = join(claudeDir, "settings.json"); + + // Read existing settings to avoid clobbering user data + let existing: Record = {}; + try { + const raw = await readFile(settingsPath, "utf-8"); + existing = JSON.parse(raw); + } catch { + // No existing file — start fresh + } + + const mcpServers: Record< + string, + { command: string; args: string[]; env?: Record } + > = (existing.mcpServers as typeof mcpServers) ?? {}; + + for (const server of config.mcp_servers) { + mcpServers[server.ref] = { + command: server.command, + args: server.args, + ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) + }; + } + + const settings = { ...existing, mcpServers }; + await writeFile( + settingsPath, + JSON.stringify(settings, null, 2) + "\n", + "utf-8" + ); +} diff --git a/packages/core/src/catalog/facets/process.ts b/packages/core/src/catalog/facets/process.ts index b7c5e3d..0bd06c5 100644 --- a/packages/core/src/catalog/facets/process.ts +++ b/packages/core/src/catalog/facets/process.ts @@ -13,7 +13,13 @@ export const processFacet: Facet = { recipe: [ { writer: "workflows", - config: {} + config: { package: "@anthropic/codemcp" } + }, + { + writer: "instruction", + config: { + text: "Use codemcp workflow files (.workflow.md) to structure and execute tasks." + } } ] }, @@ -24,7 +30,9 @@ export const processFacet: Facet = { recipe: [ { writer: "instruction", - config: {} + config: { + text: "Read AGENTS.md for project conventions and task instructions." + } } ] } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7a66f62..43ac6c4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -34,3 +34,4 @@ export { } from "./registry.js"; export { resolve } from "./resolver.js"; export { getDefaultCatalog, getFacet, getOption } from "./catalog/index.js"; +export { claudeCodeWriter } from "./agents/claude-code.js"; diff --git a/packages/core/src/registry.spec.ts b/packages/core/src/registry.spec.ts index c5747d0..5355e85 100644 --- a/packages/core/src/registry.spec.ts +++ b/packages/core/src/registry.spec.ts @@ -131,11 +131,11 @@ describe("registry", () => { expect(registry.provisions.size).toBe(6); }); - it("has the 'opencode' agent writer registered", () => { + it("has the 'claude-code' agent writer registered", () => { const registry = createDefaultRegistry(); - const agent = getAgentWriter(registry, "opencode"); + const agent = getAgentWriter(registry, "claude-code"); expect(agent).toBeDefined(); - expect(agent!.id).toBe("opencode"); + expect(agent!.id).toBe("claude-code"); }); }); }); diff --git a/packages/core/src/registry.ts b/packages/core/src/registry.ts index 91aa7fa..099bd2b 100644 --- a/packages/core/src/registry.ts +++ b/packages/core/src/registry.ts @@ -3,6 +3,9 @@ import type { ProvisionWriterDef, AgentWriterDef } from "./types.js"; +import { instructionWriter } from "./writers/instruction.js"; +import { workflowsWriter } from "./writers/workflows.js"; +import { claudeCodeWriter } from "./agents/claude-code.js"; export function createRegistry(): WriterRegistry { return { @@ -42,26 +45,18 @@ export function getAgentWriter( export function createDefaultRegistry(): WriterRegistry { const registry = createRegistry(); - const provisionIds = [ - "workflows", - "skills", - "knowledge", - "mcp-server", - "instruction", - "installable" - ] as const; + registerProvisionWriter(registry, instructionWriter); + registerProvisionWriter(registry, workflowsWriter); - for (const id of provisionIds) { + // Stub writers for types not yet implemented + for (const id of ["skills", "knowledge", "mcp-server", "installable"]) { registerProvisionWriter(registry, { id, write: async () => ({}) }); } - registerAgentWriter(registry, { - id: "opencode", - install: async () => {} - }); + registerAgentWriter(registry, claudeCodeWriter); return registry; } From 9b505f69040b95dab8941e9c334cdc534d7e219c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 10:42:12 +0000 Subject: [PATCH 23/60] fix: point workflows option to @codemcp/workflows package Update the codemcp-workflows facet option to use the correct package from github.com/mrsimpson/responsible-vibe-mcp. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/cli/src/commands/setup.integration.spec.ts | 8 ++++---- packages/core/src/agents/claude-code.spec.ts | 8 ++++---- packages/core/src/catalog/facets/process.ts | 7 ++++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/cli/src/commands/setup.integration.spec.ts b/packages/cli/src/commands/setup.integration.spec.ts index 7a9fa6e..d13aad7 100644 --- a/packages/cli/src/commands/setup.integration.spec.ts +++ b/packages/cli/src/commands/setup.integration.spec.ts @@ -53,7 +53,7 @@ describe("setup integration (real temp dir)", () => { // LogicalConfig was produced by the real resolver with real writers const lc = lock!.logical_config; expect(lc.mcp_servers).toHaveLength(1); - expect(lc.mcp_servers[0].ref).toBe("@anthropic/codemcp"); + expect(lc.mcp_servers[0].ref).toBe("@codemcp/workflows"); expect(lc.instructions.length).toBeGreaterThan(0); // ── Agent output: .claude/settings.json ────────────────────────────── @@ -61,14 +61,14 @@ describe("setup integration (real temp dir)", () => { const settings = JSON.parse( await readFile(join(dir, ".claude", "settings.json"), "utf-8") ); - expect(settings.mcpServers["@anthropic/codemcp"]).toMatchObject({ + expect(settings.mcpServers["@codemcp/workflows"]).toMatchObject({ command: "npx", - args: ["-y", "@anthropic/codemcp"] + args: ["-y", "@codemcp/workflows"] }); // ── Agent output: AGENTS.md ───────────────────────────────────────── const agentsMd = await readFile(join(dir, "AGENTS.md"), "utf-8"); - expect(agentsMd).toContain("codemcp workflow files"); + expect(agentsMd).toContain("@codemcp/workflows"); }); it("writes config.yaml, lock, and AGENTS.md for native-agents-md", async () => { diff --git a/packages/core/src/agents/claude-code.spec.ts b/packages/core/src/agents/claude-code.spec.ts index e76752a..e882ba9 100644 --- a/packages/core/src/agents/claude-code.spec.ts +++ b/packages/core/src/agents/claude-code.spec.ts @@ -36,9 +36,9 @@ describe("claudeCodeWriter", () => { const config: LogicalConfig = { mcp_servers: [ { - ref: "@anthropic/codemcp", + ref: "@codemcp/workflows", command: "npx", - args: ["-y", "@anthropic/codemcp"], + args: ["-y", "@codemcp/workflows"], env: {} } ], @@ -51,9 +51,9 @@ describe("claudeCodeWriter", () => { const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8"); const settings = JSON.parse(raw); - expect(settings.mcpServers["@anthropic/codemcp"]).toEqual({ + expect(settings.mcpServers["@codemcp/workflows"]).toEqual({ command: "npx", - args: ["-y", "@anthropic/codemcp"] + args: ["-y", "@codemcp/workflows"] }); }); diff --git a/packages/core/src/catalog/facets/process.ts b/packages/core/src/catalog/facets/process.ts index 0bd06c5..5fe7cdb 100644 --- a/packages/core/src/catalog/facets/process.ts +++ b/packages/core/src/catalog/facets/process.ts @@ -9,16 +9,17 @@ export const processFacet: Facet = { { id: "codemcp-workflows", label: "CodeMCP Workflows", - description: "Use codemcp workflow files to drive agent tasks", + description: + "Use @codemcp/workflows to drive agent tasks with structured engineering workflows", recipe: [ { writer: "workflows", - config: { package: "@anthropic/codemcp" } + config: { package: "@codemcp/workflows" } }, { writer: "instruction", config: { - text: "Use codemcp workflow files (.workflow.md) to structure and execute tasks." + text: "Use @codemcp/workflows to follow structured engineering workflows for all tasks." } } ] From c05bd96c78def8af11535c12d1b0d21572e6b430 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 10:50:53 +0000 Subject: [PATCH 24/60] fix: configure workflows MCP server from responsible-vibe-mcp docs Update the codemcp-workflows option to use the correct MCP server package (@codemcp/workflows-server@latest) with ref "workflows", matching the output of `npx @codemcp/workflows setup config claude`. Also update the workflows writer to support an optional ref override and drop the -y flag from npx args to match upstream conventions. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/cli/src/commands/setup.integration.spec.ts | 6 +++--- packages/core/src/catalog/facets/process.ts | 5 ++++- packages/core/src/writers/workflows.spec.ts | 13 ++++++++++++- packages/core/src/writers/workflows.ts | 11 ++++++++--- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/commands/setup.integration.spec.ts b/packages/cli/src/commands/setup.integration.spec.ts index d13aad7..a7e728c 100644 --- a/packages/cli/src/commands/setup.integration.spec.ts +++ b/packages/cli/src/commands/setup.integration.spec.ts @@ -53,7 +53,7 @@ describe("setup integration (real temp dir)", () => { // LogicalConfig was produced by the real resolver with real writers const lc = lock!.logical_config; expect(lc.mcp_servers).toHaveLength(1); - expect(lc.mcp_servers[0].ref).toBe("@codemcp/workflows"); + expect(lc.mcp_servers[0].ref).toBe("workflows"); expect(lc.instructions.length).toBeGreaterThan(0); // ── Agent output: .claude/settings.json ────────────────────────────── @@ -61,9 +61,9 @@ describe("setup integration (real temp dir)", () => { const settings = JSON.parse( await readFile(join(dir, ".claude", "settings.json"), "utf-8") ); - expect(settings.mcpServers["@codemcp/workflows"]).toMatchObject({ + expect(settings.mcpServers["workflows"]).toMatchObject({ command: "npx", - args: ["-y", "@codemcp/workflows"] + args: ["@codemcp/workflows-server@latest"] }); // ── Agent output: AGENTS.md ───────────────────────────────────────── diff --git a/packages/core/src/catalog/facets/process.ts b/packages/core/src/catalog/facets/process.ts index 5fe7cdb..5487d75 100644 --- a/packages/core/src/catalog/facets/process.ts +++ b/packages/core/src/catalog/facets/process.ts @@ -14,7 +14,10 @@ export const processFacet: Facet = { recipe: [ { writer: "workflows", - config: { package: "@codemcp/workflows" } + config: { + package: "@codemcp/workflows-server@latest", + ref: "workflows" + } }, { writer: "instruction", diff --git a/packages/core/src/writers/workflows.spec.ts b/packages/core/src/writers/workflows.spec.ts index 343ced0..b8e51e3 100644 --- a/packages/core/src/writers/workflows.spec.ts +++ b/packages/core/src/writers/workflows.spec.ts @@ -19,13 +19,24 @@ describe("workflowsWriter", () => { { ref: "@codemcp/workflows-server", command: "npx", - args: ["-y", "@codemcp/workflows-server"], + args: ["@codemcp/workflows-server"], env: {} } ] }); }); + it("uses ref override when provided", async () => { + const result = await workflowsWriter.write( + { package: "@codemcp/workflows-server@latest", ref: "workflows" }, + context + ); + expect(result.mcp_servers![0].ref).toBe("workflows"); + expect(result.mcp_servers![0].args).toEqual([ + "@codemcp/workflows-server@latest" + ]); + }); + it("includes env in the entry when env is specified", async () => { const result = await workflowsWriter.write( { diff --git a/packages/core/src/writers/workflows.ts b/packages/core/src/writers/workflows.ts index c60aee4..a11ccfd 100644 --- a/packages/core/src/writers/workflows.ts +++ b/packages/core/src/writers/workflows.ts @@ -3,16 +3,21 @@ import type { ProvisionWriterDef } from "../types.js"; export const workflowsWriter: ProvisionWriterDef = { id: "workflows", async write(config) { - const { package: pkg, env } = config as { + const { + package: pkg, + ref, + env + } = config as { package: string; + ref?: string; env?: Record; }; return { mcp_servers: [ { - ref: pkg, + ref: ref ?? pkg, command: "npx", - args: ["-y", pkg], + args: [pkg], env: env ?? {} } ] From c3a9651d01456c079ce1c00b844bcd5aaa1ae34c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 13:43:36 +0000 Subject: [PATCH 25/60] feat: add real process instructions from responsible-vibe-mcp Replace the generic one-liner instruction for the codemcp-workflows option with the actual process instructions from upstream (vibe.md): call whats_next(), follow returned instructions, use the development plan, and don't use own task management tools. Also switch AGENTS.md format from bullet points to paragraphs so multi-line instructions render properly. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/cli/src/commands/setup.integration.spec.ts | 2 +- packages/core/src/agents/claude-code.spec.ts | 4 ++-- packages/core/src/agents/claude-code.ts | 3 +-- packages/core/src/catalog/facets/process.ts | 8 +++++++- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/commands/setup.integration.spec.ts b/packages/cli/src/commands/setup.integration.spec.ts index a7e728c..ad94dd3 100644 --- a/packages/cli/src/commands/setup.integration.spec.ts +++ b/packages/cli/src/commands/setup.integration.spec.ts @@ -68,7 +68,7 @@ describe("setup integration (real temp dir)", () => { // ── Agent output: AGENTS.md ───────────────────────────────────────── const agentsMd = await readFile(join(dir, "AGENTS.md"), "utf-8"); - expect(agentsMd).toContain("@codemcp/workflows"); + expect(agentsMd).toContain("Call whats_next()"); }); it("writes config.yaml, lock, and AGENTS.md for native-agents-md", async () => { diff --git a/packages/core/src/agents/claude-code.spec.ts b/packages/core/src/agents/claude-code.spec.ts index e882ba9..ddb6f63 100644 --- a/packages/core/src/agents/claude-code.spec.ts +++ b/packages/core/src/agents/claude-code.spec.ts @@ -28,8 +28,8 @@ describe("claudeCodeWriter", () => { const content = await readFile(join(dir, "AGENTS.md"), "utf-8"); expect(content).toContain("# AGENTS"); - expect(content).toContain("- Use workflow files."); - expect(content).toContain("- Follow conventions."); + expect(content).toContain("Use workflow files."); + expect(content).toContain("Follow conventions."); }); it("writes .claude/settings.json with MCP servers", async () => { diff --git a/packages/core/src/agents/claude-code.ts b/packages/core/src/agents/claude-code.ts index f4b1add..5814c6f 100644 --- a/packages/core/src/agents/claude-code.ts +++ b/packages/core/src/agents/claude-code.ts @@ -18,9 +18,8 @@ async function writeAgentsMd( const lines = ["# AGENTS", ""]; for (const instruction of config.instructions) { - lines.push(`- ${instruction}`); + lines.push(instruction, ""); } - lines.push(""); await writeFile(join(projectRoot, "AGENTS.md"), lines.join("\n"), "utf-8"); } diff --git a/packages/core/src/catalog/facets/process.ts b/packages/core/src/catalog/facets/process.ts index 5487d75..4274c3d 100644 --- a/packages/core/src/catalog/facets/process.ts +++ b/packages/core/src/catalog/facets/process.ts @@ -22,7 +22,13 @@ export const processFacet: Facet = { { writer: "instruction", config: { - text: "Use @codemcp/workflows to follow structured engineering workflows for all tasks." + text: [ + "You are an AI assistant that helps users develop software features using the workflows server.", + "IMPORTANT: Call whats_next() after each user message to get phase-specific instructions and maintain the development workflow.", + 'Each tool call returns a JSON response with an "instructions" field. Follow these instructions immediately after you receive them.', + "Use the development plan which you will retrieve via whats_next() to record important insights and decisions as per the structure of the plan.", + "Do not use your own task management tools." + ].join("\n") } } ] From 6bde4c5601507536a5677d505952fea7193085dc Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 13:57:14 +0000 Subject: [PATCH 26/60] feat(cli): add install command to regenerate agent files from config.yaml Closes the team onboarding workflow: one person runs `ade setup`, commits config.yaml, teammates run `ade install` to regenerate agent files without going through the TUI. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- .../src/commands/install.integration.spec.ts | 105 +++++++++++++ packages/cli/src/commands/install.spec.ts | 144 ++++++++++++++++++ packages/cli/src/commands/install.ts | 46 ++++++ packages/cli/src/index.ts | 15 +- 4 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 packages/cli/src/commands/install.integration.spec.ts create mode 100644 packages/cli/src/commands/install.spec.ts create mode 100644 packages/cli/src/commands/install.ts diff --git a/packages/cli/src/commands/install.integration.spec.ts b/packages/cli/src/commands/install.integration.spec.ts new file mode 100644 index 0000000..a8c182f --- /dev/null +++ b/packages/cli/src/commands/install.integration.spec.ts @@ -0,0 +1,105 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +// Mock only the TUI — everything else is real +vi.mock("@clack/prompts", () => ({ + intro: vi.fn(), + outro: vi.fn(), + log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, + select: vi.fn(), + isCancel: vi.fn().mockReturnValue(false), + cancel: vi.fn(), + spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) +})); + +import * as clack from "@clack/prompts"; +import { runSetup } from "./setup.js"; +import { runInstall } from "./install.js"; +import { readUserConfig, readLockFile } from "@ade/core"; +import { getDefaultCatalog } from "../../../core/src/catalog/index.js"; + +describe("install integration (real temp dir)", () => { + let dir: string; + + beforeEach(async () => { + vi.clearAllMocks(); + dir = await mkdtemp(join(tmpdir(), "ade-install-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("re-resolves from existing config.yaml and regenerates agent files", async () => { + const catalog = getDefaultCatalog(); + + // Step 1: Run setup to create config.yaml + vi.mocked(clack.select).mockResolvedValueOnce("codemcp-workflows"); + await runSetup(dir, catalog); + + // Step 2: Delete agent output files to simulate a fresh clone + await rm(join(dir, "AGENTS.md")); + await rm(join(dir, ".claude"), { recursive: true, force: true }); + + // Step 3: Run install — should regenerate from config.yaml + await runInstall(dir, "claude-code"); + + // Agent files should be back + const agentsMd = await readFile(join(dir, "AGENTS.md"), "utf-8"); + expect(agentsMd).toContain("Call whats_next()"); + + const settings = JSON.parse( + await readFile(join(dir, ".claude", "settings.json"), "utf-8") + ); + expect(settings.mcpServers["workflows"]).toMatchObject({ + command: "npx", + args: ["@codemcp/workflows-server@latest"] + }); + }); + + it("updates lock file on install", async () => { + const catalog = getDefaultCatalog(); + + // Setup first + vi.mocked(clack.select).mockResolvedValueOnce("codemcp-workflows"); + await runSetup(dir, catalog); + + const lockBefore = await readLockFile(dir); + + // Small delay so timestamp differs + await new Promise((r) => setTimeout(r, 10)); + + // Re-install + await runInstall(dir, "claude-code"); + + const lockAfter = await readLockFile(dir); + expect(lockAfter).not.toBeNull(); + expect(lockAfter!.generated_at).not.toBe(lockBefore!.generated_at); + expect(lockAfter!.logical_config).toEqual(lockBefore!.logical_config); + }); + + it("fails when no config.yaml exists", async () => { + await expect(runInstall(dir, "claude-code")).rejects.toThrow( + /config\.yaml not found/i + ); + }); + + it("works with native-agents-md option", async () => { + const catalog = getDefaultCatalog(); + + // Setup with native-agents-md + vi.mocked(clack.select).mockResolvedValueOnce("native-agents-md"); + await runSetup(dir, catalog); + + // Delete agent output + await rm(join(dir, "AGENTS.md")); + + // Re-install + await runInstall(dir, "claude-code"); + + const agentsMd = await readFile(join(dir, "AGENTS.md"), "utf-8"); + expect(agentsMd).toContain("AGENTS.md"); + }); +}); diff --git a/packages/cli/src/commands/install.spec.ts b/packages/cli/src/commands/install.spec.ts new file mode 100644 index 0000000..c080234 --- /dev/null +++ b/packages/cli/src/commands/install.spec.ts @@ -0,0 +1,144 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { LogicalConfig } from "@ade/core"; + +// ── Mocks ──────────────────────────────────────────────────────────────────── + +vi.mock("@clack/prompts", () => ({ + intro: vi.fn(), + outro: vi.fn(), + log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() } +})); + +vi.mock("@ade/core", async (importOriginal) => { + const actual = (await importOriginal()) as typeof import("@ade/core"); + return { + ...actual, + readUserConfig: vi.fn(), + writeLockFile: vi.fn().mockResolvedValue(undefined), + resolve: vi.fn().mockResolvedValue({ + mcp_servers: [], + instructions: ["test instruction"], + cli_actions: [], + knowledge_sources: [] + } satisfies LogicalConfig), + getAgentWriter: vi.fn().mockReturnValue({ + id: "claude-code", + install: vi.fn().mockResolvedValue(undefined) + }) + }; +}); + +import * as clack from "@clack/prompts"; +import { + readUserConfig, + writeLockFile, + resolve, + getAgentWriter +} from "@ade/core"; +import { runInstall } from "./install.js"; + +// ── Tests ──────────────────────────────────────────────────────────────────── + +describe("runInstall", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("reads config.yaml and resolves to logical config", async () => { + vi.mocked(readUserConfig).mockResolvedValueOnce({ + choices: { process: "codemcp-workflows" } + }); + + await runInstall("/tmp/project", "claude-code"); + + expect(readUserConfig).toHaveBeenCalledWith("/tmp/project"); + expect(resolve).toHaveBeenCalledOnce(); + const resolveArgs = vi.mocked(resolve).mock.calls[0]; + expect(resolveArgs[0]).toMatchObject({ + choices: { process: "codemcp-workflows" } + }); + }); + + it("writes lock file with resolved config", async () => { + const mockLogical: LogicalConfig = { + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["@codemcp/workflows-server@latest"], + env: {} + } + ], + instructions: ["do stuff"], + cli_actions: [], + knowledge_sources: [] + }; + vi.mocked(readUserConfig).mockResolvedValueOnce({ + choices: { process: "codemcp-workflows" } + }); + vi.mocked(resolve).mockResolvedValueOnce(mockLogical); + + await runInstall("/tmp/project", "claude-code"); + + expect(writeLockFile).toHaveBeenCalledWith( + "/tmp/project", + expect.objectContaining({ + version: 1, + choices: { process: "codemcp-workflows" }, + logical_config: mockLogical + }) + ); + }); + + it("calls agent writer install with resolved config", async () => { + const mockInstall = vi.fn().mockResolvedValue(undefined); + vi.mocked(getAgentWriter).mockReturnValueOnce({ + id: "claude-code", + install: mockInstall + }); + vi.mocked(readUserConfig).mockResolvedValueOnce({ + choices: { process: "codemcp-workflows" } + }); + + await runInstall("/tmp/project", "claude-code"); + + expect(getAgentWriter).toHaveBeenCalledWith( + expect.anything(), + "claude-code" + ); + expect(mockInstall).toHaveBeenCalledWith( + expect.objectContaining({ instructions: expect.any(Array) }), + "/tmp/project" + ); + }); + + it("throws when config.yaml is missing", async () => { + vi.mocked(readUserConfig).mockResolvedValueOnce(null); + + await expect(runInstall("/tmp/project", "claude-code")).rejects.toThrow( + /config\.yaml not found/i + ); + }); + + it("throws when agent writer is unknown", async () => { + vi.mocked(readUserConfig).mockResolvedValueOnce({ + choices: { process: "codemcp-workflows" } + }); + vi.mocked(getAgentWriter).mockReturnValueOnce(undefined); + + await expect(runInstall("/tmp/project", "unknown-agent")).rejects.toThrow( + /unknown agent/i + ); + }); + + it("shows intro and outro messages", async () => { + vi.mocked(readUserConfig).mockResolvedValueOnce({ + choices: { process: "codemcp-workflows" } + }); + + await runInstall("/tmp/project", "claude-code"); + + expect(clack.intro).toHaveBeenCalled(); + expect(clack.outro).toHaveBeenCalled(); + }); +}); diff --git a/packages/cli/src/commands/install.ts b/packages/cli/src/commands/install.ts new file mode 100644 index 0000000..20ae60d --- /dev/null +++ b/packages/cli/src/commands/install.ts @@ -0,0 +1,46 @@ +import * as clack from "@clack/prompts"; +import { + readUserConfig, + writeLockFile, + resolve, + createDefaultRegistry, + getAgentWriter, + getDefaultCatalog, + type LockFile +} from "@ade/core"; + +export async function runInstall( + projectRoot: string, + agent: string +): Promise { + clack.intro("ade install"); + + const userConfig = await readUserConfig(projectRoot); + if (!userConfig) { + throw new Error( + "config.yaml not found. Run `ade setup` first to create one." + ); + } + + const registry = createDefaultRegistry(); + const catalog = getDefaultCatalog(); + + const agentWriter = getAgentWriter(registry, agent); + if (!agentWriter) { + throw new Error(`Unknown agent "${agent}". Available: claude-code`); + } + + const logicalConfig = await resolve(userConfig, catalog, registry); + + const lockFile: LockFile = { + version: 1, + generated_at: new Date().toISOString(), + choices: userConfig.choices, + logical_config: logicalConfig + }; + await writeLockFile(projectRoot, lockFile); + + await agentWriter.install(logicalConfig, projectRoot); + + clack.outro("Install complete!"); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 04bb61c..60ff8c0 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -2,6 +2,7 @@ import { version } from "./version.js"; import { runSetup } from "./commands/setup.js"; +import { runInstall } from "./commands/install.js"; import { getDefaultCatalog } from "@ade/core"; const args = process.argv.slice(2); @@ -11,6 +12,12 @@ if (command === "setup") { const projectRoot = args[1] ?? process.cwd(); const catalog = getDefaultCatalog(); await runSetup(projectRoot, catalog); +} else if (command === "install") { + const projectRoot = args[1] ?? process.cwd(); + const agent = args.includes("--agent") + ? args[args.indexOf("--agent") + 1] + : "claude-code"; + await runInstall(projectRoot, agent); } else if (command === "--version" || command === "-v") { console.log(version); } else { @@ -20,10 +27,14 @@ if (command === "setup") { console.log(); console.log("Commands:"); console.log( - " setup [dir] Configure your AI agent (default: current dir)" + " setup [dir] Configure your AI agent (default: current dir)" + ); + console.log( + " install [dir] Re-resolve config and regenerate agent files" ); console.log(); console.log("Options:"); - console.log(" -v, --version Show version"); + console.log(" --agent Agent writer to use (default: claude-code)"); + console.log(" -v, --version Show version"); process.exitCode = command ? 1 : 0; } From a8a6bb2d2daf563b777fc243cfe91caba6ede641 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 15:16:39 +0000 Subject: [PATCH 27/60] feat(core): add skills support to LogicalConfig and agent writer - Add SkillDefinition type and skills field to LogicalConfig - Create skills provision writer that passes through skill definitions - Update resolver to merge skills from provisions - Update claude-code agent writer to write SKILL.md files and auto-register the agentskills MCP server when skills are present https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/core/src/agents/claude-code.spec.ts | 102 ++++++++++++++++++- packages/core/src/agents/claude-code.ts | 45 +++++++- packages/core/src/index.ts | 4 +- packages/core/src/registry.ts | 4 +- packages/core/src/resolver.spec.ts | 54 +++++++++- packages/core/src/resolver.ts | 6 +- packages/core/src/types.ts | 7 ++ packages/core/src/writers/skills.spec.ts | 67 ++++++++++++ packages/core/src/writers/skills.ts | 9 ++ 9 files changed, 285 insertions(+), 13 deletions(-) create mode 100644 packages/core/src/writers/skills.spec.ts create mode 100644 packages/core/src/writers/skills.ts diff --git a/packages/core/src/agents/claude-code.spec.ts b/packages/core/src/agents/claude-code.spec.ts index ddb6f63..f264e03 100644 --- a/packages/core/src/agents/claude-code.spec.ts +++ b/packages/core/src/agents/claude-code.spec.ts @@ -21,7 +21,8 @@ describe("claudeCodeWriter", () => { mcp_servers: [], instructions: ["Use workflow files.", "Follow conventions."], cli_actions: [], - knowledge_sources: [] + knowledge_sources: [], + skills: [] }; await claudeCodeWriter.install(config, dir); @@ -44,7 +45,8 @@ describe("claudeCodeWriter", () => { ], instructions: [], cli_actions: [], - knowledge_sources: [] + knowledge_sources: [], + skills: [] }; await claudeCodeWriter.install(config, dir); @@ -77,7 +79,8 @@ describe("claudeCodeWriter", () => { ], instructions: [], cli_actions: [], - knowledge_sources: [] + knowledge_sources: [], + skills: [] }; await claudeCodeWriter.install(config, dir); @@ -97,7 +100,8 @@ describe("claudeCodeWriter", () => { mcp_servers: [], instructions: [], cli_actions: [], - knowledge_sources: [] + knowledge_sources: [], + skills: [] }; await claudeCodeWriter.install(config, dir); @@ -110,7 +114,8 @@ describe("claudeCodeWriter", () => { mcp_servers: [], instructions: ["hello"], cli_actions: [], - knowledge_sources: [] + knowledge_sources: [], + skills: [] }; await claudeCodeWriter.install(config, dir); @@ -119,4 +124,91 @@ describe("claudeCodeWriter", () => { readFile(join(dir, ".claude", "settings.json"), "utf-8") ).rejects.toThrow(); }); + + it("writes SKILL.md files to .agentskills/skills//", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [ + { + name: "tanstack-architecture", + description: "TanStack architecture conventions", + body: "# Architecture\n\nUse file-based routing." + } + ] + }; + + await claudeCodeWriter.install(config, dir); + + const skillMd = await readFile( + join(dir, ".agentskills", "skills", "tanstack-architecture", "SKILL.md"), + "utf-8" + ); + expect(skillMd).toContain("name: tanstack-architecture"); + expect(skillMd).toContain("description: TanStack architecture conventions"); + expect(skillMd).toContain("# Architecture"); + expect(skillMd).toContain("Use file-based routing."); + }); + + it("writes multiple SKILL.md files", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [ + { name: "skill-a", description: "First skill", body: "Body A" }, + { name: "skill-b", description: "Second skill", body: "Body B" } + ] + }; + + await claudeCodeWriter.install(config, dir); + + const a = await readFile( + join(dir, ".agentskills", "skills", "skill-a", "SKILL.md"), + "utf-8" + ); + const b = await readFile( + join(dir, ".agentskills", "skills", "skill-b", "SKILL.md"), + "utf-8" + ); + expect(a).toContain("name: skill-a"); + expect(b).toContain("name: skill-b"); + }); + + it("adds agentskills MCP server when skills are present", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [{ name: "my-skill", description: "A skill", body: "Do stuff." }] + }; + + await claudeCodeWriter.install(config, dir); + + const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8"); + const settings = JSON.parse(raw); + expect(settings.mcpServers["agentskills"]).toEqual({ + command: "npx", + args: ["-y", "@anthropic-ai/agentskills-mcp-server"] + }); + }); + + it("skips skills directory when no skills", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: ["hello"], + cli_actions: [], + knowledge_sources: [], + skills: [] + }; + + await claudeCodeWriter.install(config, dir); + + const { access } = await import("node:fs/promises"); + await expect(access(join(dir, ".agentskills"))).rejects.toThrow(); + }); }); diff --git a/packages/core/src/agents/claude-code.ts b/packages/core/src/agents/claude-code.ts index 5814c6f..e37cf55 100644 --- a/packages/core/src/agents/claude-code.ts +++ b/packages/core/src/agents/claude-code.ts @@ -1,11 +1,16 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import type { AgentWriterDef, LogicalConfig } from "../types.js"; +import type { + AgentWriterDef, + LogicalConfig, + McpServerEntry +} from "../types.js"; export const claudeCodeWriter: AgentWriterDef = { id: "claude-code", async install(config: LogicalConfig, projectRoot: string) { await writeAgentsMd(config, projectRoot); + await writeSkills(config, projectRoot); await writeClaudeSettings(config, projectRoot); } }; @@ -24,11 +29,45 @@ async function writeAgentsMd( await writeFile(join(projectRoot, "AGENTS.md"), lines.join("\n"), "utf-8"); } +async function writeSkills( + config: LogicalConfig, + projectRoot: string +): Promise { + if (config.skills.length === 0) return; + + for (const skill of config.skills) { + const skillDir = join(projectRoot, ".agentskills", "skills", skill.name); + await mkdir(skillDir, { recursive: true }); + + const frontmatter = [ + "---", + `name: ${skill.name}`, + `description: ${skill.description}`, + "---" + ].join("\n"); + + const content = `${frontmatter}\n\n${skill.body}\n`; + await writeFile(join(skillDir, "SKILL.md"), content, "utf-8"); + } +} + async function writeClaudeSettings( config: LogicalConfig, projectRoot: string ): Promise { - if (config.mcp_servers.length === 0) return; + // Collect all MCP servers: explicit ones + agentskills if skills exist + const allServers: McpServerEntry[] = [...config.mcp_servers]; + + if (config.skills.length > 0) { + allServers.push({ + ref: "agentskills", + command: "npx", + args: ["-y", "@anthropic-ai/agentskills-mcp-server"], + env: {} + }); + } + + if (allServers.length === 0) return; const claudeDir = join(projectRoot, ".claude"); await mkdir(claudeDir, { recursive: true }); @@ -49,7 +88,7 @@ async function writeClaudeSettings( { command: string; args: string[]; env?: Record } > = (existing.mcpServers as typeof mcpServers) ?? {}; - for (const server of config.mcp_servers) { + for (const server of allServers) { mcpServers[server.ref] = { command: server.command, args: server.args, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 43ac6c4..08b6890 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,7 +8,8 @@ export { type LogicalConfig, type McpServerEntry, type CliAction, - type KnowledgeSource + type KnowledgeSource, + type SkillDefinition } from "./types.js"; export { type ResolutionContext, type ResolvedFacet } from "./types.js"; export { type UserConfig, type LockFile } from "./types.js"; @@ -35,3 +36,4 @@ export { export { resolve } from "./resolver.js"; export { getDefaultCatalog, getFacet, getOption } from "./catalog/index.js"; export { claudeCodeWriter } from "./agents/claude-code.js"; +export { skillsWriter } from "./writers/skills.js"; diff --git a/packages/core/src/registry.ts b/packages/core/src/registry.ts index 099bd2b..bb5ece3 100644 --- a/packages/core/src/registry.ts +++ b/packages/core/src/registry.ts @@ -5,6 +5,7 @@ import type { } from "./types.js"; import { instructionWriter } from "./writers/instruction.js"; import { workflowsWriter } from "./writers/workflows.js"; +import { skillsWriter } from "./writers/skills.js"; import { claudeCodeWriter } from "./agents/claude-code.js"; export function createRegistry(): WriterRegistry { @@ -47,9 +48,10 @@ export function createDefaultRegistry(): WriterRegistry { registerProvisionWriter(registry, instructionWriter); registerProvisionWriter(registry, workflowsWriter); + registerProvisionWriter(registry, skillsWriter); // Stub writers for types not yet implemented - for (const id of ["skills", "knowledge", "mcp-server", "installable"]) { + for (const id of ["knowledge", "mcp-server", "installable"]) { registerProvisionWriter(registry, { id, write: async () => ({}) diff --git a/packages/core/src/resolver.spec.ts b/packages/core/src/resolver.spec.ts index e57a1e3..71038a0 100644 --- a/packages/core/src/resolver.spec.ts +++ b/packages/core/src/resolver.spec.ts @@ -4,12 +4,14 @@ import { getDefaultCatalog } from "./catalog/index.js"; import { createRegistry, registerProvisionWriter } from "./registry.js"; import { instructionWriter } from "./writers/instruction.js"; import { workflowsWriter } from "./writers/workflows.js"; +import { skillsWriter } from "./writers/skills.js"; import type { UserConfig, WriterRegistry, Catalog } from "./types.js"; function buildRegistry(): WriterRegistry { const registry = createRegistry(); registerProvisionWriter(registry, instructionWriter); registerProvisionWriter(registry, workflowsWriter); + registerProvisionWriter(registry, skillsWriter); return registry; } @@ -67,7 +69,8 @@ describe("resolve", () => { mcp_servers: [], instructions: [], cli_actions: [], - knowledge_sources: [] + knowledge_sources: [], + skills: [] }); }); }); @@ -128,7 +131,8 @@ describe("resolve", () => { mcp_servers: [], instructions: [], cli_actions: [], - knowledge_sources: [] + knowledge_sources: [], + skills: [] }); }); }); @@ -143,6 +147,52 @@ describe("resolve", () => { }); }); + describe("skills merging", () => { + it("merges skills from provision writers into the output", async () => { + // Use a custom catalog with a facet that produces skills + const skillsCatalog: Catalog = { + facets: [ + { + id: "conventions", + label: "Conventions", + description: "Team conventions", + required: false, + options: [ + { + id: "test-conv", + label: "Test Convention", + description: "A test convention with skills", + recipe: [ + { + writer: "skills", + config: { + skills: [ + { + name: "test-skill", + description: "A test skill", + body: "Do the thing." + } + ] + } + } + ] + } + ] + } + ] + }; + + const userConfig: UserConfig = { + choices: { conventions: "test-conv" } + }; + + const result = await resolve(userConfig, skillsCatalog, registry); + + expect(result.skills).toHaveLength(1); + expect(result.skills[0].name).toBe("test-skill"); + }); + }); + describe("MCP server dedup by ref", () => { it("deduplicates mcp_servers by ref, keeping the last one", async () => { // Create a custom registry with a writer that produces duplicate refs diff --git a/packages/core/src/resolver.ts b/packages/core/src/resolver.ts index 607376d..16081a2 100644 --- a/packages/core/src/resolver.ts +++ b/packages/core/src/resolver.ts @@ -18,7 +18,8 @@ export async function resolve( mcp_servers: [], instructions: [], cli_actions: [], - knowledge_sources: [] + knowledge_sources: [], + skills: [] }; const context: ResolutionContext = { resolved: {} }; @@ -55,6 +56,9 @@ export async function resolve( if (partial.knowledge_sources) { result.knowledge_sources.push(...partial.knowledge_sources); } + if (partial.skills) { + result.skills.push(...partial.skills); + } } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index c17fc12..d1de8a0 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -36,11 +36,18 @@ export type ProvisionWriter = // --- LogicalConfig types --- +export interface SkillDefinition { + name: string; + description: string; + body: string; +} + export interface LogicalConfig { mcp_servers: McpServerEntry[]; instructions: string[]; cli_actions: CliAction[]; knowledge_sources: KnowledgeSource[]; + skills: SkillDefinition[]; } export interface McpServerEntry { diff --git a/packages/core/src/writers/skills.spec.ts b/packages/core/src/writers/skills.spec.ts new file mode 100644 index 0000000..46a5433 --- /dev/null +++ b/packages/core/src/writers/skills.spec.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from "vitest"; +import { skillsWriter } from "./skills.js"; + +describe("skillsWriter", () => { + const emptyContext = { resolved: {} }; + + it("returns skills from config", async () => { + const result = await skillsWriter.write( + { + skills: [ + { + name: "my-skill", + description: "A test skill", + body: "Do the thing." + } + ] + }, + emptyContext + ); + + expect(result.skills).toHaveLength(1); + expect(result.skills![0]).toEqual({ + name: "my-skill", + description: "A test skill", + body: "Do the thing." + }); + }); + + it("returns multiple skills", async () => { + const result = await skillsWriter.write( + { + skills: [ + { name: "skill-a", description: "First", body: "Body A" }, + { name: "skill-b", description: "Second", body: "Body B" } + ] + }, + emptyContext + ); + + expect(result.skills).toHaveLength(2); + expect(result.skills!.map((s) => s.name)).toEqual(["skill-a", "skill-b"]); + }); + + it("returns only the skills key", async () => { + const result = await skillsWriter.write( + { + skills: [{ name: "x", description: "desc", body: "body" }] + }, + emptyContext + ); + + expect(Object.keys(result)).toEqual(["skills"]); + }); + + it("preserves multi-line body content", async () => { + const body = + "# Architecture\n\nUse layered architecture.\n\n## Rules\n- Rule 1\n- Rule 2"; + const result = await skillsWriter.write( + { + skills: [{ name: "arch", description: "Architecture", body }] + }, + emptyContext + ); + + expect(result.skills![0].body).toBe(body); + }); +}); diff --git a/packages/core/src/writers/skills.ts b/packages/core/src/writers/skills.ts new file mode 100644 index 0000000..ca7c818 --- /dev/null +++ b/packages/core/src/writers/skills.ts @@ -0,0 +1,9 @@ +import type { ProvisionWriterDef, SkillDefinition } from "../types.js"; + +export const skillsWriter: ProvisionWriterDef = { + id: "skills", + async write(config) { + const { skills } = config as { skills: SkillDefinition[] }; + return { skills }; + } +}; From a5fb2cdb89f0ba062e4925980217be7be9ec583b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 15:23:06 +0000 Subject: [PATCH 28/60] feat: add conventions facet with TanStack, commits, TDD, and ADR options Introduces multi-select conventions facet that registers skills via the agentskills-mcp server. Each convention maps to one or more SKILL.md files written to .agentskills/skills/. Options: - TanStack: 4 skills (architecture, design, code, testing) - Conventional Commits: structured commit message format - TDD London Style: outside-in mockist TDD methodology - ADR Nygard: architecture decision records template Also updates setup TUI to support multiSelect facets via clack.multiselect, and fixes resolver to process all selected options for array choices. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- .../commands/conventions.integration.spec.ts | 152 +++++++++ .../src/commands/install.integration.spec.ts | 1 + .../src/commands/setup.integration.spec.ts | 1 + packages/cli/src/commands/setup.spec.ts | 4 +- packages/cli/src/commands/setup.ts | 80 +++-- .../core/src/catalog/facets/conventions.ts | 293 ++++++++++++++++++ packages/core/src/catalog/index.ts | 3 +- packages/core/src/resolver.ts | 57 ++-- 8 files changed, 540 insertions(+), 51 deletions(-) create mode 100644 packages/cli/src/commands/conventions.integration.spec.ts create mode 100644 packages/core/src/catalog/facets/conventions.ts diff --git a/packages/cli/src/commands/conventions.integration.spec.ts b/packages/cli/src/commands/conventions.integration.spec.ts new file mode 100644 index 0000000..033a0f5 --- /dev/null +++ b/packages/cli/src/commands/conventions.integration.spec.ts @@ -0,0 +1,152 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm, readFile, access } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +vi.mock("@clack/prompts", () => ({ + intro: vi.fn(), + outro: vi.fn(), + select: vi.fn(), + multiselect: vi.fn(), + confirm: vi.fn(), + isCancel: vi.fn().mockReturnValue(false), + cancel: vi.fn(), + spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) +})); + +import * as clack from "@clack/prompts"; +import { runSetup } from "./setup.js"; +import { readUserConfig, readLockFile } from "@ade/core"; +import { getDefaultCatalog } from "../../../core/src/catalog/index.js"; + +describe("conventions facet integration", () => { + let dir: string; + + beforeEach(async () => { + vi.clearAllMocks(); + dir = await mkdtemp(join(tmpdir(), "ade-conventions-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("writes SKILL.md files for tanstack conventions", async () => { + const catalog = getDefaultCatalog(); + + // Select codemcp-workflows for process, tanstack for conventions + vi.mocked(clack.select).mockResolvedValueOnce("codemcp-workflows"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["tanstack"]); + + await runSetup(dir, catalog); + + // Should have 4 skill files + for (const skill of [ + "tanstack-architecture", + "tanstack-design", + "tanstack-code", + "tanstack-testing" + ]) { + const skillMd = await readFile( + join(dir, ".agentskills", "skills", skill, "SKILL.md"), + "utf-8" + ); + expect(skillMd).toContain(`name: ${skill}`); + expect(skillMd).toContain("---"); + } + + // agentskills MCP server should be in settings.json + const settings = JSON.parse( + await readFile(join(dir, ".claude", "settings.json"), "utf-8") + ); + expect(settings.mcpServers["agentskills"]).toMatchObject({ + command: "npx", + args: ["-y", "@anthropic-ai/agentskills-mcp-server"] + }); + }); + + it("writes skills for multiple selected conventions", async () => { + const catalog = getDefaultCatalog(); + + vi.mocked(clack.select).mockResolvedValueOnce("native-agents-md"); + vi.mocked(clack.multiselect).mockResolvedValueOnce([ + "conventional-commits", + "tdd-london" + ]); + + await runSetup(dir, catalog); + + // Both skills should exist + const commits = await readFile( + join(dir, ".agentskills", "skills", "conventional-commits", "SKILL.md"), + "utf-8" + ); + expect(commits).toContain("name: conventional-commits"); + expect(commits).toContain("Conventional Commits"); + + const tdd = await readFile( + join(dir, ".agentskills", "skills", "tdd-london", "SKILL.md"), + "utf-8" + ); + expect(tdd).toContain("name: tdd-london"); + expect(tdd).toContain("London"); + + // config.yaml should have array of choices + const config = await readUserConfig(dir); + expect(config!.choices.conventions).toEqual([ + "conventional-commits", + "tdd-london" + ]); + + // Lock file should reflect both + const lock = await readLockFile(dir); + expect(lock!.logical_config.skills.length).toBeGreaterThanOrEqual(2); + }); + + it("writes ADR skill with template content", async () => { + const catalog = getDefaultCatalog(); + + vi.mocked(clack.select).mockResolvedValueOnce("native-agents-md"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["adr-nygard"]); + + await runSetup(dir, catalog); + + const adr = await readFile( + join(dir, ".agentskills", "skills", "adr-nygard", "SKILL.md"), + "utf-8" + ); + expect(adr).toContain("name: adr-nygard"); + expect(adr).toContain("## Context"); + expect(adr).toContain("## Decision"); + expect(adr).toContain("## Consequences"); + }); + + it("skips conventions when none selected", async () => { + const catalog = getDefaultCatalog(); + + vi.mocked(clack.select).mockResolvedValueOnce("native-agents-md"); + vi.mocked(clack.multiselect).mockResolvedValueOnce([]); + + await runSetup(dir, catalog); + + // No .agentskills directory should exist + await expect(access(join(dir, ".agentskills"))).rejects.toThrow(); + + // config.yaml should not have conventions key + const config = await readUserConfig(dir); + expect(config!.choices).not.toHaveProperty("conventions"); + }); + + it("includes convention instructions in AGENTS.md", async () => { + const catalog = getDefaultCatalog(); + + vi.mocked(clack.select).mockResolvedValueOnce("native-agents-md"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["tdd-london"]); + + await runSetup(dir, catalog); + + const agentsMd = await readFile(join(dir, "AGENTS.md"), "utf-8"); + expect(agentsMd).toContain("tdd-london"); + expect(agentsMd).toContain("use_skill()"); + }); +}); diff --git a/packages/cli/src/commands/install.integration.spec.ts b/packages/cli/src/commands/install.integration.spec.ts index a8c182f..b7b73bb 100644 --- a/packages/cli/src/commands/install.integration.spec.ts +++ b/packages/cli/src/commands/install.integration.spec.ts @@ -9,6 +9,7 @@ vi.mock("@clack/prompts", () => ({ outro: vi.fn(), log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, select: vi.fn(), + multiselect: vi.fn().mockResolvedValue([]), isCancel: vi.fn().mockReturnValue(false), cancel: vi.fn(), spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) diff --git a/packages/cli/src/commands/setup.integration.spec.ts b/packages/cli/src/commands/setup.integration.spec.ts index ad94dd3..cdcf67f 100644 --- a/packages/cli/src/commands/setup.integration.spec.ts +++ b/packages/cli/src/commands/setup.integration.spec.ts @@ -8,6 +8,7 @@ vi.mock("@clack/prompts", () => ({ intro: vi.fn(), outro: vi.fn(), select: vi.fn(), + multiselect: vi.fn().mockResolvedValue([]), confirm: vi.fn(), isCancel: vi.fn().mockReturnValue(false), cancel: vi.fn(), diff --git a/packages/cli/src/commands/setup.spec.ts b/packages/cli/src/commands/setup.spec.ts index 7d78cfa..72769c5 100644 --- a/packages/cli/src/commands/setup.spec.ts +++ b/packages/cli/src/commands/setup.spec.ts @@ -7,6 +7,7 @@ vi.mock("@clack/prompts", () => ({ intro: vi.fn(), outro: vi.fn(), select: vi.fn(), + multiselect: vi.fn(), confirm: vi.fn(), isCancel: vi.fn().mockReturnValue(false), cancel: vi.fn(), @@ -23,7 +24,8 @@ vi.mock("@ade/core", async (importOriginal) => { mcp_servers: [], instructions: [], cli_actions: [], - knowledge_sources: [] + knowledge_sources: [], + skills: [] } satisfies LogicalConfig), getAgentWriter: vi.fn().mockReturnValue({ id: "claude-code", diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index cdbdd19..1160638 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -16,31 +16,27 @@ export async function runSetup( ): Promise { clack.intro("ade setup"); - const choices: Record = {}; + const choices: Record = {}; for (const facet of catalog.facets) { - const options = facet.options.map((o) => ({ - value: o.id, - label: o.label, - hint: o.description - })); - - if (!facet.required) { - options.push({ value: "__skip__", label: "Skip", hint: "" }); - } - - const selected = await clack.select({ - message: facet.label, - options - }); - - if (typeof selected === "symbol") { - clack.cancel("Setup cancelled."); - return; - } - - if (selected !== "__skip__") { - choices[facet.id] = selected as string; + if (facet.multiSelect) { + const selected = await promptMultiSelect(facet); + if (typeof selected === "symbol") { + clack.cancel("Setup cancelled."); + return; + } + if (selected.length > 0) { + choices[facet.id] = selected; + } + } else { + const selected = await promptSelect(facet); + if (typeof selected === "symbol") { + clack.cancel("Setup cancelled."); + return; + } + if (selected !== "__skip__") { + choices[facet.id] = selected as string; + } } } @@ -65,3 +61,41 @@ export async function runSetup( clack.outro("Setup complete!"); } + +function promptSelect(facet: { + label: string; + required: boolean; + options: { id: string; label: string; description: string }[]; +}) { + const options = facet.options.map((o) => ({ + value: o.id, + label: o.label, + hint: o.description + })); + + if (!facet.required) { + options.push({ value: "__skip__", label: "Skip", hint: "" }); + } + + return clack.select({ + message: facet.label, + options + }); +} + +function promptMultiSelect(facet: { + label: string; + options: { id: string; label: string; description: string }[]; +}) { + const options = facet.options.map((o) => ({ + value: o.id, + label: o.label, + hint: o.description + })); + + return clack.multiselect({ + message: facet.label, + options, + required: false + }); +} diff --git a/packages/core/src/catalog/facets/conventions.ts b/packages/core/src/catalog/facets/conventions.ts new file mode 100644 index 0000000..fee13ed --- /dev/null +++ b/packages/core/src/catalog/facets/conventions.ts @@ -0,0 +1,293 @@ +import type { Facet } from "../../types.js"; + +export const conventionsFacet: Facet = { + id: "conventions", + label: "Conventions", + description: "Team conventions your AI agent should follow", + required: false, + multiSelect: true, + options: [ + { + id: "tanstack", + label: "TanStack", + description: + "Full-stack conventions for TanStack (Router, Query, Form, Table)", + recipe: [ + { + writer: "skills", + config: { + skills: [ + { + name: "tanstack-architecture", + description: + "Architecture conventions for TanStack applications", + body: [ + "# TanStack Architecture Conventions", + "", + "## Project Structure", + "- Use file-based routing with TanStack Router (`routes/` directory)", + "- Colocate route components with their loaders and actions", + "- Organize by feature, not by type (e.g. `features/auth/`, not `components/auth/`)", + "", + "## Data Flow", + "- Use TanStack Query for all server state management", + "- Use TanStack Router loaders for route-level data requirements", + "- Keep client state minimal — prefer server state via Query", + "- Use `queryOptions()` factory pattern for reusable query definitions", + "", + "## Module Boundaries", + "- Each feature exports a public API via `index.ts`", + "- Features must not import from other features' internals", + "- Shared code goes in `lib/` or `shared/`" + ].join("\n") + }, + { + name: "tanstack-design", + description: "Design patterns for TanStack applications", + body: [ + "# TanStack Design Patterns", + "", + "## Query Patterns", + "- Define query options as standalone functions: `export const userQueryOptions = (id: string) => queryOptions({ queryKey: ['user', id], queryFn: () => fetchUser(id) })`", + "- Use `useSuspenseQuery` in route components paired with `loader` for prefetching", + "- Use `useMutation` with `onSettled` for cache invalidation", + "", + "## Router Patterns", + "- Define routes using `createFileRoute` for type-safe file-based routing", + "- Use `beforeLoad` for auth guards and redirects", + "- Use search params validation with `zodSearchValidator` for type-safe URL state", + "", + "## Form Patterns", + "- Use TanStack Form with Zod validators for form state and validation", + "- Prefer field-level validation over form-level where possible", + "- Connect form submission to `useMutation` for server sync" + ].join("\n") + }, + { + name: "tanstack-code", + description: "Code style conventions for TanStack applications", + body: [ + "# TanStack Code Conventions", + "", + "## TypeScript", + "- Enable strict mode in tsconfig", + "- Infer types from TanStack APIs rather than writing manual type annotations", + "- Use `satisfies` operator for type-safe object literals", + "", + "## Naming", + "- Query keys: `['entity', ...params]` (e.g. `['user', userId]`)", + "- Query option factories: `entityQueryOptions` (e.g. `userQueryOptions`)", + "- Route files: `$param` for dynamic segments (e.g. `users/$userId.tsx`)", + "- Loaders: export as named `loader` from route file", + "", + "## Imports", + "- Import from `@tanstack/react-query`, `@tanstack/react-router`, etc.", + "- Never import internal modules from TanStack packages", + "- Use path aliases for project imports (`@/features/...`)" + ].join("\n") + }, + { + name: "tanstack-testing", + description: "Testing conventions for TanStack applications", + body: [ + "# TanStack Testing Conventions", + "", + "## Query Testing", + "- Wrap components in `QueryClientProvider` with a fresh `QueryClient` per test", + "- Use `@testing-library/react` with `renderHook` for testing custom query hooks", + "- Mock at the network level with MSW, not at the query level", + "", + "## Router Testing", + "- Use `createMemoryHistory` and `createRouter` for route testing", + "- Test route loaders independently as plain async functions", + "- Test search param validation with unit tests on the validator schema", + "", + "## Integration Tests", + "- Test full user flows through route transitions", + "- Assert on visible UI state, not internal query cache state", + "- Use `waitFor` for async query resolution in component tests" + ].join("\n") + } + ] + } + }, + { + writer: "instruction", + config: { + text: "This project follows TanStack conventions. Use use_skill() to access the tanstack-architecture, tanstack-design, tanstack-code, and tanstack-testing skills before making changes." + } + } + ] + }, + { + id: "conventional-commits", + label: "Conventional Commits", + description: + "Structured commit messages following the Conventional Commits specification", + recipe: [ + { + writer: "skills", + config: { + skills: [ + { + name: "conventional-commits", + description: + "Conventional Commits specification for structured commit messages", + body: [ + "# Conventional Commits", + "", + "## Format", + "```", + "[optional scope]: ", + "", + "[optional body]", + "", + "[optional footer(s)]", + "```", + "", + "## Types", + "- `feat`: A new feature (correlates with MINOR in SemVer)", + "- `fix`: A bug fix (correlates with PATCH in SemVer)", + "- `docs`: Documentation only changes", + "- `style`: Changes that do not affect the meaning of the code", + "- `refactor`: A code change that neither fixes a bug nor adds a feature", + "- `perf`: A code change that improves performance", + "- `test`: Adding missing tests or correcting existing tests", + "- `chore`: Changes to the build process or auxiliary tools", + "", + "## Rules", + "- Subject line must not exceed 72 characters", + '- Use imperative mood in the subject line ("add" not "added")', + "- Do not end the subject line with a period", + "- Separate subject from body with a blank line", + "- Use the body to explain what and why, not how", + "- `BREAKING CHANGE:` footer or `!` after type/scope for breaking changes" + ].join("\n") + } + ] + } + }, + { + writer: "instruction", + config: { + text: "Use the conventional-commits skill (via use_skill()) when writing commit messages." + } + } + ] + }, + { + id: "tdd-london", + label: "TDD (London Style)", + description: + "Test-Driven Development using the London school (mockist) approach", + recipe: [ + { + writer: "skills", + config: { + skills: [ + { + name: "tdd-london", + description: + "London-school TDD methodology with outside-in design", + body: [ + "# TDD — London Style (Mockist)", + "", + "## Core Cycle", + "1. **Red** — Write a failing test for the next behavior", + "2. **Green** — Write the minimum code to make the test pass", + "3. **Refactor** — Improve the code while keeping tests green", + "", + "## London School Principles", + "- Work **outside-in**: start from the outermost layer (API / UI) and drive inward", + "- **Mock collaborators**: each unit test isolates the unit under test by mocking its direct dependencies", + "- Discover interfaces through tests — let the test define the collaborator contract before implementing it", + "- Prefer **role-based interfaces** over concrete classes", + "", + "## Test Structure", + "- **Arrange**: Set up mocks and the unit under test", + "- **Act**: Call the method being tested", + "- **Assert**: Verify the unit's output and interactions with mocks", + "", + "## Guidelines", + "- One logical assertion per test", + '- Test names describe behavior, not methods (e.g. "notifies user when order is placed")', + "- Only mock types you own — wrap third-party APIs in adapters and mock those", + "- Use the test doubles: stubs for queries, mocks for commands", + "- Do not test implementation details — test observable behavior", + "- Refactor step is mandatory, not optional" + ].join("\n") + } + ] + } + }, + { + writer: "instruction", + config: { + text: "This project uses London-style TDD. Use the tdd-london skill (via use_skill()) before writing tests. Always follow the Red-Green-Refactor cycle." + } + } + ] + }, + { + id: "adr-nygard", + label: "ADR (Nygard)", + description: + "Architecture Decision Records following Michael Nygard's template", + recipe: [ + { + writer: "skills", + config: { + skills: [ + { + name: "adr-nygard", + description: + "Architecture Decision Records following Nygard's lightweight template", + body: [ + "# Architecture Decision Records (Nygard)", + "", + "## When to Write an ADR", + "- When making a significant architectural decision", + "- When choosing between multiple viable options", + "- When the decision will be hard to reverse", + '- When future developers will ask "why did we do this?"', + "", + "## Template", + "Store ADRs in `docs/adr/` as numbered markdown files: `NNNN-title-with-dashes.md`", + "", + "```markdown", + "# N. Title", + "", + "## Status", + "Proposed | Accepted | Deprecated | Superseded by [ADR-NNNN]", + "", + "## Context", + "What is the issue that we're seeing that is motivating this decision or change?", + "", + "## Decision", + "What is the change that we're proposing and/or doing?", + "", + "## Consequences", + "What becomes easier or more difficult to do because of this change?", + "```", + "", + "## Rules", + "- ADRs are immutable once accepted — supersede, don't edit", + "- Keep context focused on forces at play at the time of the decision", + "- Write consequences as both positive and negative impacts", + "- Number sequentially, never reuse numbers", + '- Title should be a short noun phrase (e.g. "Use PostgreSQL for persistence")' + ].join("\n") + } + ] + } + }, + { + writer: "instruction", + config: { + text: "This project uses Architecture Decision Records. Use the adr-nygard skill (via use_skill()) when making or documenting architectural decisions. Store ADRs in docs/adr/." + } + } + ] + } + ] +}; diff --git a/packages/core/src/catalog/index.ts b/packages/core/src/catalog/index.ts index e455164..0c4cfb8 100644 --- a/packages/core/src/catalog/index.ts +++ b/packages/core/src/catalog/index.ts @@ -1,9 +1,10 @@ import type { Catalog, Facet, Option } from "../types.js"; import { processFacet } from "./facets/process.js"; +import { conventionsFacet } from "./facets/conventions.js"; export function getDefaultCatalog(): Catalog { return { - facets: [processFacet] + facets: [processFacet, conventionsFacet] }; } diff --git a/packages/core/src/resolver.ts b/packages/core/src/resolver.ts index 16081a2..502c8b5 100644 --- a/packages/core/src/resolver.ts +++ b/packages/core/src/resolver.ts @@ -30,34 +30,39 @@ export async function resolve( continue; } - const selectedId = Array.isArray(optionId) ? optionId[0] : optionId; - const option = getOption(facet, selectedId); - if (!option) { - throw new Error(`Unknown option "${selectedId}" for facet "${facetId}"`); - } - - context.resolved[facetId] = { optionId: selectedId, option }; + const selectedIds = Array.isArray(optionId) ? optionId : [optionId]; - for (const provision of option.recipe) { - const writer = getProvisionWriter(registry, provision.writer); - if (!writer) { - continue; - } - const partial = await writer.write(provision.config, context); - if (partial.mcp_servers) { - result.mcp_servers.push(...partial.mcp_servers); - } - if (partial.instructions) { - result.instructions.push(...partial.instructions); + for (const selectedId of selectedIds) { + const option = getOption(facet, selectedId); + if (!option) { + throw new Error( + `Unknown option "${selectedId}" for facet "${facetId}"` + ); } - if (partial.cli_actions) { - result.cli_actions.push(...partial.cli_actions); - } - if (partial.knowledge_sources) { - result.knowledge_sources.push(...partial.knowledge_sources); - } - if (partial.skills) { - result.skills.push(...partial.skills); + + context.resolved[facetId] = { optionId: selectedId, option }; + + for (const provision of option.recipe) { + const writer = getProvisionWriter(registry, provision.writer); + if (!writer) { + continue; + } + const partial = await writer.write(provision.config, context); + if (partial.mcp_servers) { + result.mcp_servers.push(...partial.mcp_servers); + } + if (partial.instructions) { + result.instructions.push(...partial.instructions); + } + if (partial.cli_actions) { + result.cli_actions.push(...partial.cli_actions); + } + if (partial.knowledge_sources) { + result.knowledge_sources.push(...partial.knowledge_sources); + } + if (partial.skills) { + result.skills.push(...partial.skills); + } } } } From 0719c7a0e56ffe6477843a8e4863b0d63ac778d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 15:24:52 +0000 Subject: [PATCH 29/60] fix: add skills field to all LogicalConfig test fixtures https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/cli/src/commands/install.spec.ts | 4 +++- packages/cli/src/commands/setup.spec.ts | 3 ++- packages/core/src/config.spec.ts | 3 ++- packages/core/src/registry.spec.ts | 3 ++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/install.spec.ts b/packages/cli/src/commands/install.spec.ts index c080234..8e2f926 100644 --- a/packages/cli/src/commands/install.spec.ts +++ b/packages/cli/src/commands/install.spec.ts @@ -19,7 +19,8 @@ vi.mock("@ade/core", async (importOriginal) => { mcp_servers: [], instructions: ["test instruction"], cli_actions: [], - knowledge_sources: [] + knowledge_sources: [], + skills: [] } satisfies LogicalConfig), getAgentWriter: vi.fn().mockReturnValue({ id: "claude-code", @@ -71,6 +72,7 @@ describe("runInstall", () => { ], instructions: ["do stuff"], cli_actions: [], + skills: [], knowledge_sources: [] }; vi.mocked(readUserConfig).mockResolvedValueOnce({ diff --git a/packages/cli/src/commands/setup.spec.ts b/packages/cli/src/commands/setup.spec.ts index 72769c5..e630f99 100644 --- a/packages/cli/src/commands/setup.spec.ts +++ b/packages/cli/src/commands/setup.spec.ts @@ -117,7 +117,8 @@ describe("runSetup", () => { mcp_servers: [], instructions: ["do stuff"], cli_actions: [], - knowledge_sources: [] + knowledge_sources: [], + skills: [] }; vi.mocked(resolve).mockResolvedValueOnce(mockLogical); vi.mocked(clack.select) diff --git a/packages/core/src/config.spec.ts b/packages/core/src/config.spec.ts index c5d7451..771a9e6 100644 --- a/packages/core/src/config.spec.ts +++ b/packages/core/src/config.spec.ts @@ -123,7 +123,8 @@ describe("config", () => { origin: "https://typescriptlang.org", description: "TypeScript documentation" } - ] + ], + skills: [] } }; diff --git a/packages/core/src/registry.spec.ts b/packages/core/src/registry.spec.ts index 5355e85..4a5800a 100644 --- a/packages/core/src/registry.spec.ts +++ b/packages/core/src/registry.spec.ts @@ -98,7 +98,8 @@ describe("registry", () => { mcp_servers: [], instructions: ["be helpful"], cli_actions: [], - knowledge_sources: [] + knowledge_sources: [], + skills: [] }; await found!.install(config, "/tmp/my-project"); expect(mockInstall).toHaveBeenCalledWith(config, "/tmp/my-project"); From f79245bbfb7cf9095a566284279596ecdc36985c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 15:25:50 +0000 Subject: [PATCH 30/60] test: add conventions facet catalog tests https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/core/src/catalog/catalog.spec.ts | 67 +++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/packages/core/src/catalog/catalog.spec.ts b/packages/core/src/catalog/catalog.spec.ts index 10239e7..fa499ee 100644 --- a/packages/core/src/catalog/catalog.spec.ts +++ b/packages/core/src/catalog/catalog.spec.ts @@ -44,6 +44,73 @@ describe("catalog", () => { }); }); + describe("conventions facet", () => { + it("exists in the default catalog", () => { + const catalog = getDefaultCatalog(); + const conventions = getFacet(catalog, "conventions"); + expect(conventions).toBeDefined(); + expect(conventions!.required).toBe(false); + }); + + it("has tanstack option with skills for architecture, design, code, testing", () => { + const catalog = getDefaultCatalog(); + const conventions = getFacet(catalog, "conventions")!; + const tanstack = getOption(conventions, "tanstack"); + + expect(tanstack).toBeDefined(); + const skillsProvisions = tanstack!.recipe.filter( + (p) => p.writer === "skills" + ); + expect(skillsProvisions).toHaveLength(1); + + const skills = ( + skillsProvisions[0].config as { skills: { name: string }[] } + ).skills; + const names = skills.map((s) => s.name); + expect(names).toContain("tanstack-architecture"); + expect(names).toContain("tanstack-design"); + expect(names).toContain("tanstack-code"); + expect(names).toContain("tanstack-testing"); + }); + + it("has conventional-commits option with a single skill", () => { + const catalog = getDefaultCatalog(); + const conventions = getFacet(catalog, "conventions")!; + const option = getOption(conventions, "conventional-commits"); + + expect(option).toBeDefined(); + const skills = ( + option!.recipe.find((p) => p.writer === "skills")!.config as { + skills: { name: string }[]; + } + ).skills; + expect(skills).toHaveLength(1); + expect(skills[0].name).toBe("conventional-commits"); + }); + + it("has tdd-london option with a single skill", () => { + const catalog = getDefaultCatalog(); + const conventions = getFacet(catalog, "conventions")!; + const option = getOption(conventions, "tdd-london"); + + expect(option).toBeDefined(); + }); + + it("has adr-nygard option with a single skill", () => { + const catalog = getDefaultCatalog(); + const conventions = getFacet(catalog, "conventions")!; + const option = getOption(conventions, "adr-nygard"); + + expect(option).toBeDefined(); + }); + + it("is multi-select", () => { + const catalog = getDefaultCatalog(); + const conventions = getFacet(catalog, "conventions")!; + expect(conventions.multiSelect).toBe(true); + }); + }); + describe("catalog + registry integration", () => { it("every recipe provision references a writer that exists in the default registry", () => { const catalog = getDefaultCatalog(); From 8cdd07ccebe221e076c378ff20ece1557f960b12 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 20:00:44 +0000 Subject: [PATCH 31/60] refactor: use @codemcp/skills ecosystem for skill management - Fix MCP server to @codemcp/skills-server (was @anthropic-ai/...) - Split SkillDefinition into InlineSkill (name/description/body) and ExternalSkill (name/source) union type - Agent writer now writes inline skills to .ade/catalog/skills/ staging area and registers all skills in package.json agentskills section (file: refs for inline, source refs for external) - Add playwright-cli as external skill to TanStack conventions (source: microsoft/playwright-cli/skills/playwright-cli) This aligns with the @codemcp/skills install workflow: skills are declared in package.json and materialized via `npx @codemcp/skills install`. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- .../commands/conventions.integration.spec.ts | 49 ++++-- packages/core/src/agents/claude-code.spec.ts | 147 +++++++++++++++--- packages/core/src/agents/claude-code.ts | 77 +++++++-- packages/core/src/catalog/catalog.spec.ts | 12 +- .../core/src/catalog/facets/conventions.ts | 6 +- packages/core/src/index.ts | 4 +- packages/core/src/types.ts | 9 +- packages/core/src/writers/skills.spec.ts | 44 +++++- 8 files changed, 297 insertions(+), 51 deletions(-) diff --git a/packages/cli/src/commands/conventions.integration.spec.ts b/packages/cli/src/commands/conventions.integration.spec.ts index 033a0f5..e2e9cc3 100644 --- a/packages/cli/src/commands/conventions.integration.spec.ts +++ b/packages/cli/src/commands/conventions.integration.spec.ts @@ -31,16 +31,15 @@ describe("conventions facet integration", () => { await rm(dir, { recursive: true, force: true }); }); - it("writes SKILL.md files for tanstack conventions", async () => { + it("writes inline SKILL.md files and registers all skills in package.json for tanstack", async () => { const catalog = getDefaultCatalog(); - // Select codemcp-workflows for process, tanstack for conventions vi.mocked(clack.select).mockResolvedValueOnce("codemcp-workflows"); vi.mocked(clack.multiselect).mockResolvedValueOnce(["tanstack"]); await runSetup(dir, catalog); - // Should have 4 skill files + // Inline skills should have SKILL.md in .ade/catalog/skills/ for (const skill of [ "tanstack-architecture", "tanstack-design", @@ -48,20 +47,34 @@ describe("conventions facet integration", () => { "tanstack-testing" ]) { const skillMd = await readFile( - join(dir, ".agentskills", "skills", skill, "SKILL.md"), + join(dir, ".ade", "catalog", "skills", skill, "SKILL.md"), "utf-8" ); expect(skillMd).toContain(`name: ${skill}`); expect(skillMd).toContain("---"); } - // agentskills MCP server should be in settings.json + // External skill (playwright) should NOT have a local SKILL.md + await expect( + access(join(dir, ".ade", "catalog", "skills", "playwright-cli")) + ).rejects.toThrow(); + + // All skills should be registered in package.json + const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf-8")); + expect(pkg.agentskills["tanstack-architecture"]).toBe( + "file:./.ade/catalog/skills/tanstack-architecture" + ); + expect(pkg.agentskills["playwright-cli"]).toBe( + "microsoft/playwright-cli/skills/playwright-cli" + ); + + // skills-server MCP server should be in settings.json const settings = JSON.parse( await readFile(join(dir, ".claude", "settings.json"), "utf-8") ); expect(settings.mcpServers["agentskills"]).toMatchObject({ command: "npx", - args: ["-y", "@anthropic-ai/agentskills-mcp-server"] + args: ["-y", "@codemcp/skills-server"] }); }); @@ -76,21 +89,33 @@ describe("conventions facet integration", () => { await runSetup(dir, catalog); - // Both skills should exist + // Both inline skills should exist in .ade/catalog/skills/ const commits = await readFile( - join(dir, ".agentskills", "skills", "conventional-commits", "SKILL.md"), + join( + dir, + ".ade", + "catalog", + "skills", + "conventional-commits", + "SKILL.md" + ), "utf-8" ); expect(commits).toContain("name: conventional-commits"); expect(commits).toContain("Conventional Commits"); const tdd = await readFile( - join(dir, ".agentskills", "skills", "tdd-london", "SKILL.md"), + join(dir, ".ade", "catalog", "skills", "tdd-london", "SKILL.md"), "utf-8" ); expect(tdd).toContain("name: tdd-london"); expect(tdd).toContain("London"); + // Both registered in package.json + const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf-8")); + expect(pkg.agentskills["conventional-commits"]).toContain("file:"); + expect(pkg.agentskills["tdd-london"]).toContain("file:"); + // config.yaml should have array of choices const config = await readUserConfig(dir); expect(config!.choices.conventions).toEqual([ @@ -112,7 +137,7 @@ describe("conventions facet integration", () => { await runSetup(dir, catalog); const adr = await readFile( - join(dir, ".agentskills", "skills", "adr-nygard", "SKILL.md"), + join(dir, ".ade", "catalog", "skills", "adr-nygard", "SKILL.md"), "utf-8" ); expect(adr).toContain("name: adr-nygard"); @@ -129,8 +154,8 @@ describe("conventions facet integration", () => { await runSetup(dir, catalog); - // No .agentskills directory should exist - await expect(access(join(dir, ".agentskills"))).rejects.toThrow(); + // No .ade directory should exist + await expect(access(join(dir, ".ade"))).rejects.toThrow(); // config.yaml should not have conventions key const config = await readUserConfig(dir); diff --git a/packages/core/src/agents/claude-code.spec.ts b/packages/core/src/agents/claude-code.spec.ts index f264e03..24e3bfd 100644 --- a/packages/core/src/agents/claude-code.spec.ts +++ b/packages/core/src/agents/claude-code.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { mkdtemp, rm, readFile } from "node:fs/promises"; +import { mkdtemp, rm, readFile, access } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { LogicalConfig } from "../types.js"; @@ -109,7 +109,7 @@ describe("claudeCodeWriter", () => { await expect(readFile(join(dir, "AGENTS.md"), "utf-8")).rejects.toThrow(); }); - it("skips settings.json when no MCP servers", async () => { + it("skips settings.json when no MCP servers and no skills", async () => { const config: LogicalConfig = { mcp_servers: [], instructions: ["hello"], @@ -125,7 +125,9 @@ describe("claudeCodeWriter", () => { ).rejects.toThrow(); }); - it("writes SKILL.md files to .agentskills/skills//", async () => { + // ── Skills: inline ──────────────────────────────────────────────────── + + it("writes inline SKILL.md files to .ade/catalog/skills//", async () => { const config: LogicalConfig = { mcp_servers: [], instructions: [], @@ -143,7 +145,14 @@ describe("claudeCodeWriter", () => { await claudeCodeWriter.install(config, dir); const skillMd = await readFile( - join(dir, ".agentskills", "skills", "tanstack-architecture", "SKILL.md"), + join( + dir, + ".ade", + "catalog", + "skills", + "tanstack-architecture", + "SKILL.md" + ), "utf-8" ); expect(skillMd).toContain("name: tanstack-architecture"); @@ -152,33 +161,108 @@ describe("claudeCodeWriter", () => { expect(skillMd).toContain("Use file-based routing."); }); - it("writes multiple SKILL.md files", async () => { + it("registers inline skills in package.json as file: refs", async () => { const config: LogicalConfig = { mcp_servers: [], instructions: [], cli_actions: [], knowledge_sources: [], skills: [ - { name: "skill-a", description: "First skill", body: "Body A" }, - { name: "skill-b", description: "Second skill", body: "Body B" } + { + name: "tanstack-code", + description: "Code conventions", + body: "# Code\nStuff." + } ] }; await claudeCodeWriter.install(config, dir); - const a = await readFile( - join(dir, ".agentskills", "skills", "skill-a", "SKILL.md"), - "utf-8" + const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf-8")); + expect(pkg.agentskills["tanstack-code"]).toBe( + "file:./.ade/catalog/skills/tanstack-code" + ); + }); + + // ── Skills: external ────────────────────────────────────────────────── + + it("registers external skills in package.json by source", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [ + { + name: "playwright-cli", + source: "microsoft/playwright-cli/skills/playwright-cli" + } + ] + }; + + await claudeCodeWriter.install(config, dir); + + const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf-8")); + expect(pkg.agentskills["playwright-cli"]).toBe( + "microsoft/playwright-cli/skills/playwright-cli" ); - const b = await readFile( - join(dir, ".agentskills", "skills", "skill-b", "SKILL.md"), + }); + + it("does not write SKILL.md for external skills", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [ + { + name: "playwright-cli", + source: "microsoft/playwright-cli/skills/playwright-cli" + } + ] + }; + + await claudeCodeWriter.install(config, dir); + + await expect( + access(join(dir, ".ade", "catalog", "skills", "playwright-cli")) + ).rejects.toThrow(); + }); + + // ── Skills: mixed ───────────────────────────────────────────────────── + + it("handles mixed inline and external skills", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [ + { name: "my-conv", description: "Inline", body: "Do stuff." }, + { name: "ext-skill", source: "org/repo/skills/ext" } + ] + }; + + await claudeCodeWriter.install(config, dir); + + // Inline skill has SKILL.md + const skillMd = await readFile( + join(dir, ".ade", "catalog", "skills", "my-conv", "SKILL.md"), "utf-8" ); - expect(a).toContain("name: skill-a"); - expect(b).toContain("name: skill-b"); + expect(skillMd).toContain("name: my-conv"); + + // Both registered in package.json + const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf-8")); + expect(pkg.agentskills["my-conv"]).toBe( + "file:./.ade/catalog/skills/my-conv" + ); + expect(pkg.agentskills["ext-skill"]).toBe("org/repo/skills/ext"); }); - it("adds agentskills MCP server when skills are present", async () => { + // ── Skills: MCP server ──────────────────────────────────────────────── + + it("adds skills-server MCP server when skills are present", async () => { const config: LogicalConfig = { mcp_servers: [], instructions: [], @@ -193,11 +277,11 @@ describe("claudeCodeWriter", () => { const settings = JSON.parse(raw); expect(settings.mcpServers["agentskills"]).toEqual({ command: "npx", - args: ["-y", "@anthropic-ai/agentskills-mcp-server"] + args: ["-y", "@codemcp/skills-server"] }); }); - it("skips skills directory when no skills", async () => { + it("skips skills when none present", async () => { const config: LogicalConfig = { mcp_servers: [], instructions: ["hello"], @@ -208,7 +292,32 @@ describe("claudeCodeWriter", () => { await claudeCodeWriter.install(config, dir); - const { access } = await import("node:fs/promises"); - await expect(access(join(dir, ".agentskills"))).rejects.toThrow(); + await expect(access(join(dir, ".ade"))).rejects.toThrow(); + }); + + it("preserves existing package.json fields when adding skills", async () => { + const { writeFile } = await import("node:fs/promises"); + await writeFile( + join(dir, "package.json"), + JSON.stringify({ name: "my-project", version: "1.0.0" }), + "utf-8" + ); + + const config: LogicalConfig = { + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [{ name: "my-skill", description: "A skill", body: "Body." }] + }; + + await claudeCodeWriter.install(config, dir); + + const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf-8")); + expect(pkg.name).toBe("my-project"); + expect(pkg.version).toBe("1.0.0"); + expect(pkg.agentskills["my-skill"]).toBe( + "file:./.ade/catalog/skills/my-skill" + ); }); }); diff --git a/packages/core/src/agents/claude-code.ts b/packages/core/src/agents/claude-code.ts index e37cf55..fc7e941 100644 --- a/packages/core/src/agents/claude-code.ts +++ b/packages/core/src/agents/claude-code.ts @@ -3,9 +3,16 @@ import { join } from "node:path"; import type { AgentWriterDef, LogicalConfig, - McpServerEntry + McpServerEntry, + InlineSkill } from "../types.js"; +function isInlineSkill( + skill: LogicalConfig["skills"][number] +): skill is InlineSkill { + return "body" in skill; +} + export const claudeCodeWriter: AgentWriterDef = { id: "claude-code", async install(config: LogicalConfig, projectRoot: string) { @@ -35,34 +42,74 @@ async function writeSkills( ): Promise { if (config.skills.length === 0) return; + const agentskills: Record = {}; + for (const skill of config.skills) { - const skillDir = join(projectRoot, ".agentskills", "skills", skill.name); - await mkdir(skillDir, { recursive: true }); - - const frontmatter = [ - "---", - `name: ${skill.name}`, - `description: ${skill.description}`, - "---" - ].join("\n"); - - const content = `${frontmatter}\n\n${skill.body}\n`; - await writeFile(join(skillDir, "SKILL.md"), content, "utf-8"); + if (isInlineSkill(skill)) { + // Write inline skill to catalog staging area + const skillDir = join( + projectRoot, + ".ade", + "catalog", + "skills", + skill.name + ); + await mkdir(skillDir, { recursive: true }); + + const frontmatter = [ + "---", + `name: ${skill.name}`, + `description: ${skill.description}`, + "---" + ].join("\n"); + + const content = `${frontmatter}\n\n${skill.body}\n`; + await writeFile(join(skillDir, "SKILL.md"), content, "utf-8"); + + agentskills[skill.name] = `file:./.ade/catalog/skills/${skill.name}`; + } else { + // External skill — just register the source reference + agentskills[skill.name] = skill.source; + } } + + // Register skills in package.json + await updatePackageJson(projectRoot, agentskills); +} + +async function updatePackageJson( + projectRoot: string, + agentskills: Record +): Promise { + const pkgPath = join(projectRoot, "package.json"); + + let existing: Record = {}; + try { + const raw = await readFile(pkgPath, "utf-8"); + existing = JSON.parse(raw); + } catch { + // No existing package.json — start fresh + } + + const currentSkills = (existing.agentskills as Record) ?? {}; + const merged = { ...currentSkills, ...agentskills }; + + const pkg = { ...existing, agentskills: merged }; + await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8"); } async function writeClaudeSettings( config: LogicalConfig, projectRoot: string ): Promise { - // Collect all MCP servers: explicit ones + agentskills if skills exist + // Collect all MCP servers: explicit ones + skills-server if skills exist const allServers: McpServerEntry[] = [...config.mcp_servers]; if (config.skills.length > 0) { allServers.push({ ref: "agentskills", command: "npx", - args: ["-y", "@anthropic-ai/agentskills-mcp-server"], + args: ["-y", "@codemcp/skills-server"], env: {} }); } diff --git a/packages/core/src/catalog/catalog.spec.ts b/packages/core/src/catalog/catalog.spec.ts index fa499ee..f7f1751 100644 --- a/packages/core/src/catalog/catalog.spec.ts +++ b/packages/core/src/catalog/catalog.spec.ts @@ -52,7 +52,7 @@ describe("catalog", () => { expect(conventions!.required).toBe(false); }); - it("has tanstack option with skills for architecture, design, code, testing", () => { + it("has tanstack option with skills for architecture, design, code, testing, and playwright", () => { const catalog = getDefaultCatalog(); const conventions = getFacet(catalog, "conventions")!; const tanstack = getOption(conventions, "tanstack"); @@ -71,6 +71,16 @@ describe("catalog", () => { expect(names).toContain("tanstack-design"); expect(names).toContain("tanstack-code"); expect(names).toContain("tanstack-testing"); + expect(names).toContain("playwright-cli"); + + // playwright-cli should be an external skill (has source, no body) + const playwright = skills.find( + (s: Record) => s.name === "playwright-cli" + ) as Record; + expect(playwright.source).toBe( + "microsoft/playwright-cli/skills/playwright-cli" + ); + expect(playwright).not.toHaveProperty("body"); }); it("has conventional-commits option with a single skill", () => { diff --git a/packages/core/src/catalog/facets/conventions.ts b/packages/core/src/catalog/facets/conventions.ts index fee13ed..a1c38cc 100644 --- a/packages/core/src/catalog/facets/conventions.ts +++ b/packages/core/src/catalog/facets/conventions.ts @@ -107,6 +107,10 @@ export const conventionsFacet: Facet = { "- Assert on visible UI state, not internal query cache state", "- Use `waitFor` for async query resolution in component tests" ].join("\n") + }, + { + name: "playwright-cli", + source: "microsoft/playwright-cli/skills/playwright-cli" } ] } @@ -114,7 +118,7 @@ export const conventionsFacet: Facet = { { writer: "instruction", config: { - text: "This project follows TanStack conventions. Use use_skill() to access the tanstack-architecture, tanstack-design, tanstack-code, and tanstack-testing skills before making changes." + text: "This project follows TanStack conventions. Use use_skill() to access the tanstack-architecture, tanstack-design, tanstack-code, tanstack-testing, and playwright-cli skills before making changes." } } ] diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 08b6890..b89aac1 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -9,7 +9,9 @@ export { type McpServerEntry, type CliAction, type KnowledgeSource, - type SkillDefinition + type SkillDefinition, + type InlineSkill, + type ExternalSkill } from "./types.js"; export { type ResolutionContext, type ResolvedFacet } from "./types.js"; export { type UserConfig, type LockFile } from "./types.js"; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index d1de8a0..a33bc67 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -36,12 +36,19 @@ export type ProvisionWriter = // --- LogicalConfig types --- -export interface SkillDefinition { +export interface InlineSkill { name: string; description: string; body: string; } +export interface ExternalSkill { + name: string; + source: string; +} + +export type SkillDefinition = InlineSkill | ExternalSkill; + export interface LogicalConfig { mcp_servers: McpServerEntry[]; instructions: string[]; diff --git a/packages/core/src/writers/skills.spec.ts b/packages/core/src/writers/skills.spec.ts index 46a5433..e969d37 100644 --- a/packages/core/src/writers/skills.spec.ts +++ b/packages/core/src/writers/skills.spec.ts @@ -62,6 +62,48 @@ describe("skillsWriter", () => { emptyContext ); - expect(result.skills![0].body).toBe(body); + expect(result.skills![0]).toMatchObject({ body }); + }); + + it("returns external skills with source reference", async () => { + const result = await skillsWriter.write( + { + skills: [ + { + name: "playwright-cli", + source: "microsoft/playwright-cli/skills/playwright-cli" + } + ] + }, + emptyContext + ); + + expect(result.skills).toHaveLength(1); + expect(result.skills![0]).toEqual({ + name: "playwright-cli", + source: "microsoft/playwright-cli/skills/playwright-cli" + }); + }); + + it("handles mixed inline and external skills", async () => { + const result = await skillsWriter.write( + { + skills: [ + { name: "my-skill", description: "Inline", body: "Do stuff." }, + { name: "ext-skill", source: "org/repo/skills/ext" } + ] + }, + emptyContext + ); + + expect(result.skills).toHaveLength(2); + expect(result.skills![0]).toMatchObject({ + name: "my-skill", + body: "Do stuff." + }); + expect(result.skills![1]).toMatchObject({ + name: "ext-skill", + source: "org/repo/skills/ext" + }); }); }); From 3799093233051bc23a1cc1dbbdb7d1834da99701 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 06:16:56 +0000 Subject: [PATCH 32/60] refactor: use @codemcp/skills programmatic API for skill installation Instead of manually writing package.json agentskills entries, use the runAdd API from @codemcp/skills to install skills via the file protocol. - Agent writer writes SKILL.md to .ade/skills// for inline skills - CLI calls runAdd() after agent writer to install via local path - External skills installed via runAdd() with source reference - runAdd creates skills-lock.json and installs to .agentskills/skills/ - Keep @codemcp/skills-server MCP server name - Integration tests use real @codemcp/skills implementation (no mocks) https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/cli/package.json | 3 +- packages/cli/src/codemcp-skills.d.ts | 21 + .../commands/conventions.integration.spec.ts | 123 +- packages/cli/src/commands/install.spec.ts | 4 + packages/cli/src/commands/install.ts | 3 + packages/cli/src/commands/setup.spec.ts | 4 + packages/cli/src/commands/setup.ts | 3 + packages/cli/src/skills-installer.ts | 54 + packages/core/src/agents/claude-code.spec.ts | 98 +- packages/core/src/agents/claude-code.ts | 60 +- pnpm-lock.yaml | 1182 ++++++++++++++++- 11 files changed, 1350 insertions(+), 205 deletions(-) create mode 100644 packages/cli/src/codemcp-skills.d.ts create mode 100644 packages/cli/src/skills-installer.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 1874ac5..2b7d1a1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -23,7 +23,8 @@ }, "dependencies": { "@ade/core": "workspace:*", - "@clack/prompts": "^1.1.0" + "@clack/prompts": "^1.1.0", + "@codemcp/skills": "^2.1.1" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^8.21.0", diff --git a/packages/cli/src/codemcp-skills.d.ts b/packages/cli/src/codemcp-skills.d.ts new file mode 100644 index 0000000..f7e35a9 --- /dev/null +++ b/packages/cli/src/codemcp-skills.d.ts @@ -0,0 +1,21 @@ +declare module "@codemcp/skills/api" { + export interface AddOptions { + global?: boolean; + agent?: string[]; + yes?: boolean; + skill?: string[]; + list?: boolean; + all?: boolean; + fullDepth?: boolean; + copy?: boolean; + } + + export function runAdd(args: string[], options?: AddOptions): Promise; + + export function runInstallFromLock(args: string[]): Promise; + + export function parseAddOptions(args: string[]): { + source: string[]; + options: AddOptions; + }; +} diff --git a/packages/cli/src/commands/conventions.integration.spec.ts b/packages/cli/src/commands/conventions.integration.spec.ts index e2e9cc3..2fc4f86 100644 --- a/packages/cli/src/commands/conventions.integration.spec.ts +++ b/packages/cli/src/commands/conventions.integration.spec.ts @@ -31,52 +31,61 @@ describe("conventions facet integration", () => { await rm(dir, { recursive: true, force: true }); }); - it("writes inline SKILL.md files and registers all skills in package.json for tanstack", async () => { - const catalog = getDefaultCatalog(); - - vi.mocked(clack.select).mockResolvedValueOnce("codemcp-workflows"); - vi.mocked(clack.multiselect).mockResolvedValueOnce(["tanstack"]); - - await runSetup(dir, catalog); - - // Inline skills should have SKILL.md in .ade/catalog/skills/ - for (const skill of [ - "tanstack-architecture", - "tanstack-design", - "tanstack-code", - "tanstack-testing" - ]) { - const skillMd = await readFile( - join(dir, ".ade", "catalog", "skills", skill, "SKILL.md"), - "utf-8" + it( + "writes SKILL.md files and installs inline skills for tanstack", + { timeout: 60_000 }, + async () => { + const catalog = getDefaultCatalog(); + + vi.mocked(clack.select).mockResolvedValueOnce("codemcp-workflows"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["tanstack"]); + + await runSetup(dir, catalog); + + // Inline skills should have SKILL.md in .ade/skills/ (staging area) + for (const skill of [ + "tanstack-architecture", + "tanstack-design", + "tanstack-code", + "tanstack-testing" + ]) { + const skillMd = await readFile( + join(dir, ".ade", "skills", skill, "SKILL.md"), + "utf-8" + ); + expect(skillMd).toContain(`name: ${skill}`); + expect(skillMd).toContain("---"); + } + + // Inline skills should also be installed to .agentskills/skills/ by runAdd + for (const skill of [ + "tanstack-architecture", + "tanstack-design", + "tanstack-code", + "tanstack-testing" + ]) { + const installed = await readFile( + join(dir, ".agentskills", "skills", skill, "SKILL.md"), + "utf-8" + ); + expect(installed).toContain(`name: ${skill}`); + } + + // skills-lock.json should be created by runAdd + const lockRaw = await readFile(join(dir, "skills-lock.json"), "utf-8"); + const skillsLock = JSON.parse(lockRaw); + expect(skillsLock.skills).toBeDefined(); + + // skills-server MCP server should be in settings.json + const settings = JSON.parse( + await readFile(join(dir, ".claude", "settings.json"), "utf-8") ); - expect(skillMd).toContain(`name: ${skill}`); - expect(skillMd).toContain("---"); + expect(settings.mcpServers["agentskills"]).toMatchObject({ + command: "npx", + args: ["-y", "@codemcp/skills-server"] + }); } - - // External skill (playwright) should NOT have a local SKILL.md - await expect( - access(join(dir, ".ade", "catalog", "skills", "playwright-cli")) - ).rejects.toThrow(); - - // All skills should be registered in package.json - const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf-8")); - expect(pkg.agentskills["tanstack-architecture"]).toBe( - "file:./.ade/catalog/skills/tanstack-architecture" - ); - expect(pkg.agentskills["playwright-cli"]).toBe( - "microsoft/playwright-cli/skills/playwright-cli" - ); - - // skills-server MCP server should be in settings.json - const settings = JSON.parse( - await readFile(join(dir, ".claude", "settings.json"), "utf-8") - ); - expect(settings.mcpServers["agentskills"]).toMatchObject({ - command: "npx", - args: ["-y", "@codemcp/skills-server"] - }); - }); + ); it("writes skills for multiple selected conventions", async () => { const catalog = getDefaultCatalog(); @@ -89,32 +98,28 @@ describe("conventions facet integration", () => { await runSetup(dir, catalog); - // Both inline skills should exist in .ade/catalog/skills/ + // Both inline skills should exist in .ade/skills/ (staging) const commits = await readFile( - join( - dir, - ".ade", - "catalog", - "skills", - "conventional-commits", - "SKILL.md" - ), + join(dir, ".ade", "skills", "conventional-commits", "SKILL.md"), "utf-8" ); expect(commits).toContain("name: conventional-commits"); expect(commits).toContain("Conventional Commits"); const tdd = await readFile( - join(dir, ".ade", "catalog", "skills", "tdd-london", "SKILL.md"), + join(dir, ".ade", "skills", "tdd-london", "SKILL.md"), "utf-8" ); expect(tdd).toContain("name: tdd-london"); expect(tdd).toContain("London"); - // Both registered in package.json - const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf-8")); - expect(pkg.agentskills["conventional-commits"]).toContain("file:"); - expect(pkg.agentskills["tdd-london"]).toContain("file:"); + // Both should be installed to .agentskills/skills/ + await expect( + access(join(dir, ".agentskills", "skills", "conventional-commits")) + ).resolves.toBeUndefined(); + await expect( + access(join(dir, ".agentskills", "skills", "tdd-london")) + ).resolves.toBeUndefined(); // config.yaml should have array of choices const config = await readUserConfig(dir); @@ -137,7 +142,7 @@ describe("conventions facet integration", () => { await runSetup(dir, catalog); const adr = await readFile( - join(dir, ".ade", "catalog", "skills", "adr-nygard", "SKILL.md"), + join(dir, ".ade", "skills", "adr-nygard", "SKILL.md"), "utf-8" ); expect(adr).toContain("name: adr-nygard"); diff --git a/packages/cli/src/commands/install.spec.ts b/packages/cli/src/commands/install.spec.ts index 8e2f926..f03949a 100644 --- a/packages/cli/src/commands/install.spec.ts +++ b/packages/cli/src/commands/install.spec.ts @@ -9,6 +9,10 @@ vi.mock("@clack/prompts", () => ({ log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() } })); +vi.mock("@codemcp/skills/api", () => ({ + runAdd: vi.fn() +})); + vi.mock("@ade/core", async (importOriginal) => { const actual = (await importOriginal()) as typeof import("@ade/core"); return { diff --git a/packages/cli/src/commands/install.ts b/packages/cli/src/commands/install.ts index 20ae60d..73d8df6 100644 --- a/packages/cli/src/commands/install.ts +++ b/packages/cli/src/commands/install.ts @@ -8,6 +8,7 @@ import { getDefaultCatalog, type LockFile } from "@ade/core"; +import { installSkills } from "../skills-installer.js"; export async function runInstall( projectRoot: string, @@ -42,5 +43,7 @@ export async function runInstall( await agentWriter.install(logicalConfig, projectRoot); + await installSkills(logicalConfig.skills, projectRoot); + clack.outro("Install complete!"); } diff --git a/packages/cli/src/commands/setup.spec.ts b/packages/cli/src/commands/setup.spec.ts index e630f99..e3dec6f 100644 --- a/packages/cli/src/commands/setup.spec.ts +++ b/packages/cli/src/commands/setup.spec.ts @@ -14,6 +14,10 @@ vi.mock("@clack/prompts", () => ({ spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) })); +vi.mock("@codemcp/skills/api", () => ({ + runAdd: vi.fn() +})); + vi.mock("@ade/core", async (importOriginal) => { const actual = (await importOriginal()) as typeof import("@ade/core"); return { diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 1160638..9a52752 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -9,6 +9,7 @@ import { createDefaultRegistry, getAgentWriter } from "@ade/core"; +import { installSkills } from "../skills-installer.js"; export async function runSetup( projectRoot: string, @@ -59,6 +60,8 @@ export async function runSetup( await agentWriter.install(logicalConfig, projectRoot); } + await installSkills(logicalConfig.skills, projectRoot); + clack.outro("Setup complete!"); } diff --git a/packages/cli/src/skills-installer.ts b/packages/cli/src/skills-installer.ts new file mode 100644 index 0000000..93257f4 --- /dev/null +++ b/packages/cli/src/skills-installer.ts @@ -0,0 +1,54 @@ +import { join } from "node:path"; +import type { SkillDefinition, InlineSkill } from "@ade/core"; +import { runAdd } from "@codemcp/skills/api"; + +function isInlineSkill(skill: SkillDefinition): skill is InlineSkill { + return "body" in skill; +} + +/** + * Install skills using the @codemcp/skills programmatic API. + * + * Inline skills are expected to already exist as SKILL.md files under + * `/.ade/skills//` (written by the agent writer). + * This function calls `runAdd` with the local path for inline skills + * and the remote source for external skills. + * + * Note: `runAdd` uses `process.cwd()` to determine the install destination. + * This function changes cwd to `projectRoot` before calling `runAdd`. + */ +export async function installSkills( + skills: SkillDefinition[], + projectRoot: string +): Promise { + if (skills.length === 0) return; + + const originalCwd = process.cwd(); + process.chdir(projectRoot); + + try { + for (const skill of skills) { + const source = isInlineSkill(skill) + ? join(projectRoot, ".ade", "skills", skill.name) + : skill.source; + + try { + await runAdd([source], { yes: true, all: true }); + } catch (err) { + // runAdd may throw on network errors for external skills. + // Log and continue — inline skills should always succeed. + console.warn( + `Warning: failed to install skill "${skill.name}" from ${source}:`, + err instanceof Error ? err.message : err + ); + } + } + } finally { + // Restore cwd only if the original directory still exists + try { + process.chdir(originalCwd); + } catch { + // Original cwd may have been removed (e.g. in tests) + } + } +} diff --git a/packages/core/src/agents/claude-code.spec.ts b/packages/core/src/agents/claude-code.spec.ts index 24e3bfd..b021e76 100644 --- a/packages/core/src/agents/claude-code.spec.ts +++ b/packages/core/src/agents/claude-code.spec.ts @@ -127,7 +127,7 @@ describe("claudeCodeWriter", () => { // ── Skills: inline ──────────────────────────────────────────────────── - it("writes inline SKILL.md files to .ade/catalog/skills//", async () => { + it("writes inline SKILL.md files to .ade/skills//", async () => { const config: LogicalConfig = { mcp_servers: [], instructions: [], @@ -145,14 +145,7 @@ describe("claudeCodeWriter", () => { await claudeCodeWriter.install(config, dir); const skillMd = await readFile( - join( - dir, - ".ade", - "catalog", - "skills", - "tanstack-architecture", - "SKILL.md" - ), + join(dir, ".ade", "skills", "tanstack-architecture", "SKILL.md"), "utf-8" ); expect(skillMd).toContain("name: tanstack-architecture"); @@ -161,53 +154,34 @@ describe("claudeCodeWriter", () => { expect(skillMd).toContain("Use file-based routing."); }); - it("registers inline skills in package.json as file: refs", async () => { + it("writes multiple inline SKILL.md files", async () => { const config: LogicalConfig = { mcp_servers: [], instructions: [], cli_actions: [], knowledge_sources: [], skills: [ - { - name: "tanstack-code", - description: "Code conventions", - body: "# Code\nStuff." - } + { name: "skill-a", description: "First skill", body: "Body A" }, + { name: "skill-b", description: "Second skill", body: "Body B" } ] }; await claudeCodeWriter.install(config, dir); - const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf-8")); - expect(pkg.agentskills["tanstack-code"]).toBe( - "file:./.ade/catalog/skills/tanstack-code" + const a = await readFile( + join(dir, ".ade", "skills", "skill-a", "SKILL.md"), + "utf-8" ); + const b = await readFile( + join(dir, ".ade", "skills", "skill-b", "SKILL.md"), + "utf-8" + ); + expect(a).toContain("name: skill-a"); + expect(b).toContain("name: skill-b"); }); // ── Skills: external ────────────────────────────────────────────────── - it("registers external skills in package.json by source", async () => { - const config: LogicalConfig = { - mcp_servers: [], - instructions: [], - cli_actions: [], - knowledge_sources: [], - skills: [ - { - name: "playwright-cli", - source: "microsoft/playwright-cli/skills/playwright-cli" - } - ] - }; - - await claudeCodeWriter.install(config, dir); - - const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf-8")); - expect(pkg.agentskills["playwright-cli"]).toBe( - "microsoft/playwright-cli/skills/playwright-cli" - ); - }); - it("does not write SKILL.md for external skills", async () => { const config: LogicalConfig = { mcp_servers: [], @@ -225,13 +199,13 @@ describe("claudeCodeWriter", () => { await claudeCodeWriter.install(config, dir); await expect( - access(join(dir, ".ade", "catalog", "skills", "playwright-cli")) + access(join(dir, ".ade", "skills", "playwright-cli")) ).rejects.toThrow(); }); // ── Skills: mixed ───────────────────────────────────────────────────── - it("handles mixed inline and external skills", async () => { + it("writes only inline skills in mixed config", async () => { const config: LogicalConfig = { mcp_servers: [], instructions: [], @@ -247,17 +221,15 @@ describe("claudeCodeWriter", () => { // Inline skill has SKILL.md const skillMd = await readFile( - join(dir, ".ade", "catalog", "skills", "my-conv", "SKILL.md"), + join(dir, ".ade", "skills", "my-conv", "SKILL.md"), "utf-8" ); expect(skillMd).toContain("name: my-conv"); - // Both registered in package.json - const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf-8")); - expect(pkg.agentskills["my-conv"]).toBe( - "file:./.ade/catalog/skills/my-conv" - ); - expect(pkg.agentskills["ext-skill"]).toBe("org/repo/skills/ext"); + // External skill has no local files + await expect( + access(join(dir, ".ade", "skills", "ext-skill")) + ).rejects.toThrow(); }); // ── Skills: MCP server ──────────────────────────────────────────────── @@ -281,7 +253,7 @@ describe("claudeCodeWriter", () => { }); }); - it("skips skills when none present", async () => { + it("skips .ade/skills when no skills present", async () => { const config: LogicalConfig = { mcp_servers: [], instructions: ["hello"], @@ -294,30 +266,4 @@ describe("claudeCodeWriter", () => { await expect(access(join(dir, ".ade"))).rejects.toThrow(); }); - - it("preserves existing package.json fields when adding skills", async () => { - const { writeFile } = await import("node:fs/promises"); - await writeFile( - join(dir, "package.json"), - JSON.stringify({ name: "my-project", version: "1.0.0" }), - "utf-8" - ); - - const config: LogicalConfig = { - mcp_servers: [], - instructions: [], - cli_actions: [], - knowledge_sources: [], - skills: [{ name: "my-skill", description: "A skill", body: "Body." }] - }; - - await claudeCodeWriter.install(config, dir); - - const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf-8")); - expect(pkg.name).toBe("my-project"); - expect(pkg.version).toBe("1.0.0"); - expect(pkg.agentskills["my-skill"]).toBe( - "file:./.ade/catalog/skills/my-skill" - ); - }); }); diff --git a/packages/core/src/agents/claude-code.ts b/packages/core/src/agents/claude-code.ts index fc7e941..47faa4b 100644 --- a/packages/core/src/agents/claude-code.ts +++ b/packages/core/src/agents/claude-code.ts @@ -42,60 +42,22 @@ async function writeSkills( ): Promise { if (config.skills.length === 0) return; - const agentskills: Record = {}; - for (const skill of config.skills) { - if (isInlineSkill(skill)) { - // Write inline skill to catalog staging area - const skillDir = join( - projectRoot, - ".ade", - "catalog", - "skills", - skill.name - ); - await mkdir(skillDir, { recursive: true }); - - const frontmatter = [ - "---", - `name: ${skill.name}`, - `description: ${skill.description}`, - "---" - ].join("\n"); - - const content = `${frontmatter}\n\n${skill.body}\n`; - await writeFile(join(skillDir, "SKILL.md"), content, "utf-8"); - - agentskills[skill.name] = `file:./.ade/catalog/skills/${skill.name}`; - } else { - // External skill — just register the source reference - agentskills[skill.name] = skill.source; - } - } + if (!isInlineSkill(skill)) continue; - // Register skills in package.json - await updatePackageJson(projectRoot, agentskills); -} + const skillDir = join(projectRoot, ".ade", "skills", skill.name); + await mkdir(skillDir, { recursive: true }); -async function updatePackageJson( - projectRoot: string, - agentskills: Record -): Promise { - const pkgPath = join(projectRoot, "package.json"); + const frontmatter = [ + "---", + `name: ${skill.name}`, + `description: ${skill.description}`, + "---" + ].join("\n"); - let existing: Record = {}; - try { - const raw = await readFile(pkgPath, "utf-8"); - existing = JSON.parse(raw); - } catch { - // No existing package.json — start fresh + const content = `${frontmatter}\n\n${skill.body}\n`; + await writeFile(join(skillDir, "SKILL.md"), content, "utf-8"); } - - const currentSkills = (existing.agentskills as Record) ?? {}; - const merged = { ...currentSkills, ...agentskills }; - - const pkg = { ...existing, agentskills: merged }; - await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8"); } async function writeClaudeSettings( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8ab063..33d94c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,6 +101,9 @@ importers: "@clack/prompts": specifier: ^1.1.0 version: 1.1.0 + "@codemcp/skills": + specifier: ^2.1.1 + version: 2.1.1 devDependencies: "@typescript-eslint/eslint-plugin": specifier: ^8.21.0 @@ -392,6 +395,14 @@ packages: integrity: sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g== } + "@codemcp/skills@2.1.1": + resolution: + { + integrity: sha512-aWFLefeFsI8dZVWzWXjAAVTsmVi3GJHRl/pqw5Sk5VxO3gfOB9Qh9AgwIPi/hyq5YwTc1vao6b2+rF7u5RhKPw== + } + engines: { node: ">=18" } + hasBin: true + "@docsearch/css@3.9.0": resolution: { @@ -937,6 +948,13 @@ packages: } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + "@gar/promise-retry@1.0.2": + resolution: + { + integrity: sha512-Lm/ZLhDZcBECta3TmCQSngiQykFdfw+QtI1/GYMsZd4l3nG+P8WLB16XuS7WaBGLQ+9E+cOcWQsth9cayuGt8g== + } + engines: { node: ^20.17.0 || >=22.9.0 } + "@humanfs/core@0.19.1": resolution: { @@ -990,6 +1008,13 @@ packages: } engines: { node: ">=12" } + "@isaacs/fs-minipass@4.0.1": + resolution: + { + integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w== + } + engines: { node: ">=18.0.0" } + "@istanbuljs/schema@0.1.3": resolution: { @@ -1022,6 +1047,18 @@ packages: integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== } + "@kwsites/file-exists@1.1.1": + resolution: + { + integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw== + } + + "@kwsites/promise-deferred@1.1.1": + resolution: + { + integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw== + } + "@mermaid-js/mermaid-mindmap@9.3.0": resolution: { @@ -1055,6 +1092,70 @@ packages: } engines: { node: ">= 8" } + "@npmcli/agent@4.0.0": + resolution: + { + integrity: sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + "@npmcli/fs@5.0.0": + resolution: + { + integrity: sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + "@npmcli/git@7.0.2": + resolution: + { + integrity: sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + "@npmcli/installed-package-contents@4.0.0": + resolution: + { + integrity: sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA== + } + engines: { node: ^20.17.0 || >=22.9.0 } + hasBin: true + + "@npmcli/node-gyp@5.0.0": + resolution: + { + integrity: sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + "@npmcli/package-json@7.0.5": + resolution: + { + integrity: sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + "@npmcli/promise-spawn@9.0.1": + resolution: + { + integrity: sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + "@npmcli/redact@4.0.0": + resolution: + { + integrity: sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + "@npmcli/run-script@10.0.4": + resolution: + { + integrity: sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg== + } + engines: { node: ^20.17.0 || >=22.9.0 } + "@pkgjs/parseargs@0.11.0": resolution: { @@ -1310,6 +1411,48 @@ packages: integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg== } + "@sigstore/bundle@4.0.0": + resolution: + { + integrity: sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + "@sigstore/core@3.1.0": + resolution: + { + integrity: sha512-o5cw1QYhNQ9IroioJxpzexmPjfCe7gzafd2RY3qnMpxr4ZEja+Jad/U8sgFpaue6bOaF+z7RVkyKVV44FN+N8A== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + "@sigstore/protobuf-specs@0.5.0": + resolution: + { + integrity: sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA== + } + engines: { node: ^18.17.0 || >=20.5.0 } + + "@sigstore/sign@4.1.0": + resolution: + { + integrity: sha512-Vx1RmLxLGnSUqx/o5/VsCjkuN5L7y+vxEEwawvc7u+6WtX2W4GNa7b9HEjmcRWohw/d6BpATXmvOwc78m+Swdg== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + "@sigstore/tuf@4.0.1": + resolution: + { + integrity: sha512-OPZBg8y5Vc9yZjmWCHrlWPMBqW5yd8+wFNl+thMdtcWz3vjVSoJQutF8YkrzI0SLGnkuFof4HSsWUhXrf219Lw== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + "@sigstore/verify@3.1.0": + resolution: + { + integrity: sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag== + } + engines: { node: ^20.17.0 || >=22.9.0 } + "@swc/core-darwin-arm64@1.15.11": resolution: { @@ -1436,6 +1579,20 @@ packages: integrity: sha512-ec4tjL2Rr0pkZ5hww65c+EEPYwxOi4Ryv+0MtjeaSQRJyq322Q27eOQiFbuNgw2hpL4hB1/W/HBGk3VKS43osg== } + "@tufjs/canonical-json@2.0.0": + resolution: + { + integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA== + } + engines: { node: ^16.14.0 || >=18.0.0 } + + "@tufjs/models@4.1.0": + resolution: + { + integrity: sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww== + } + engines: { node: ^20.17.0 || >=22.9.0 } + "@types/chai@5.2.3": resolution: { @@ -2086,6 +2243,13 @@ packages: integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w== } + abbrev@4.0.0: + resolution: + { + integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA== + } + engines: { node: ^20.17.0 || >=22.9.0 } + acorn-jsx@5.3.2: resolution: { @@ -2102,12 +2266,25 @@ packages: engines: { node: ">=0.4.0" } hasBin: true + agent-base@7.1.4: + resolution: + { + integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== + } + engines: { node: ">= 14" } + ajv@6.12.6: resolution: { integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== } + ajv@8.18.0: + resolution: + { + integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A== + } + algoliasearch@5.49.0: resolution: { @@ -2157,6 +2334,12 @@ packages: } engines: { node: ">= 8" } + argparse@1.0.10: + resolution: + { + integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + } + argparse@2.0.1: resolution: { @@ -2235,6 +2418,13 @@ packages: } engines: { node: ">=8" } + cacache@20.0.3: + resolution: + { + integrity: sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw== + } + engines: { node: ^20.17.0 || >=22.9.0 } + callsites@3.1.0: resolution: { @@ -2309,6 +2499,13 @@ packages: } engines: { node: ">= 8.10.0" } + chownr@3.0.0: + resolution: + { + integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g== + } + engines: { node: ">=18" } + cli-cursor@5.0.0: resolution: { @@ -2797,6 +2994,13 @@ packages: } engines: { node: ">=0.12" } + env-paths@2.2.1: + resolution: + { + integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + } + engines: { node: ">=6" } + environment@1.1.0: resolution: { @@ -2804,6 +3008,12 @@ packages: } engines: { node: ">=18" } + err-code@2.0.3: + resolution: + { + integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + } + es-module-lexer@1.7.0: resolution: { @@ -2890,6 +3100,14 @@ packages: } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + esprima@4.0.1: + resolution: + { + integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + } + engines: { node: ">=4" } + hasBin: true + esquery@1.7.0: resolution: { @@ -2950,12 +3168,25 @@ packages: } engines: { node: ">=12.0.0" } + exponential-backoff@3.1.3: + resolution: + { + integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA== + } + exsolve@1.0.8: resolution: { integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA== } + extend-shallow@2.0.1: + resolution: + { + integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== + } + engines: { node: ">=0.10.0" } + fast-deep-equal@3.1.3: resolution: { @@ -2981,6 +3212,12 @@ packages: integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== } + fast-uri@3.1.0: + resolution: + { + integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== + } + fastq@1.20.1: resolution: { @@ -3046,6 +3283,13 @@ packages: } engines: { node: ">=14" } + fs-minipass@3.0.3: + resolution: + { + integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw== + } + engines: { node: ^14.17.0 || ^16.13.0 || >=18.0.0 } + fsevents@2.3.3: resolution: { @@ -3111,12 +3355,25 @@ packages: } engines: { node: ">=18" } + graceful-fs@4.2.11: + resolution: + { + integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + } + graphemer@1.4.0: resolution: { integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== } + gray-matter@4.0.3: + resolution: + { + integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q== + } + engines: { node: ">=6.0" } + hachure-fill@0.5.2: resolution: { @@ -3155,6 +3412,13 @@ packages: integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ== } + hosted-git-info@9.0.2: + resolution: + { + integrity: sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg== + } + engines: { node: ^20.17.0 || >=22.9.0 } + html-escaper@2.0.2: resolution: { @@ -3167,6 +3431,26 @@ packages: integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== } + http-cache-semantics@4.2.0: + resolution: + { + integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ== + } + + http-proxy-agent@7.0.2: + resolution: + { + integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + } + engines: { node: ">= 14" } + + https-proxy-agent@7.0.6: + resolution: + { + integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== + } + engines: { node: ">= 14" } + human-signals@5.0.0: resolution: { @@ -3189,12 +3473,26 @@ packages: } engines: { node: ">=0.10.0" } + iconv-lite@0.7.2: + resolution: + { + integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw== + } + engines: { node: ">=0.10.0" } + ignore-by-default@1.0.1: resolution: { integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== } + ignore-walk@8.0.0: + resolution: + { + integrity: sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A== + } + engines: { node: ^20.17.0 || >=22.9.0 } + ignore@5.3.2: resolution: { @@ -3223,6 +3521,13 @@ packages: } engines: { node: ">=0.8.19" } + ini@6.0.0: + resolution: + { + integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ== + } + engines: { node: ^20.17.0 || >=22.9.0 } + internmap@1.0.1: resolution: { @@ -3236,6 +3541,13 @@ packages: } engines: { node: ">=12" } + ip-address@10.1.0: + resolution: + { + integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q== + } + engines: { node: ">= 12" } + is-binary-path@2.1.0: resolution: { @@ -3243,6 +3555,13 @@ packages: } engines: { node: ">=8" } + is-extendable@0.1.1: + resolution: + { + integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== + } + engines: { node: ">=0.10.0" } + is-extglob@2.1.1: resolution: { @@ -3305,6 +3624,13 @@ packages: integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== } + isexe@4.0.0: + resolution: + { + integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw== + } + engines: { node: ">=20" } + istanbul-lib-coverage@3.2.2: resolution: { @@ -3351,6 +3677,13 @@ packages: integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ== } + js-yaml@3.14.2: + resolution: + { + integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg== + } + hasBin: true + js-yaml@4.1.1: resolution: { @@ -3364,18 +3697,38 @@ packages: integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== } + json-parse-even-better-errors@5.0.0: + resolution: + { + integrity: sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ== + } + engines: { node: ^20.17.0 || >=22.9.0 } + json-schema-traverse@0.4.1: resolution: { integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== } + json-schema-traverse@1.0.0: + resolution: + { + integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + } + json-stable-stringify-without-jsonify@1.0.1: resolution: { integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== } + jsonparse@1.3.1: + resolution: + { + integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== + } + engines: { "0": node >= 0.2.0 } + katex@0.16.28: resolution: { @@ -3395,6 +3748,13 @@ packages: integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw== } + kind-of@6.0.3: + resolution: + { + integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + } + engines: { node: ">=0.10.0" } + kolorist@1.8.0: resolution: { @@ -3526,6 +3886,13 @@ packages: } engines: { node: ">=10" } + make-fetch-happen@15.0.4: + resolution: + { + integrity: sha512-vM2sG+wbVeVGYcCm16mM3d5fuem9oC28n436HjsGO3LcxoTI8LNVa4rwZDn3f76+cWyT4GGJDxjTYU1I2nr6zw== + } + engines: { node: ^20.17.0 || >=22.9.0 } + mark.js@8.11.1: resolution: { @@ -3636,58 +4003,122 @@ packages: } engines: { node: ">=16 || 14 >=14.17" } - minipass@7.1.3: + minipass-collect@2.0.1: resolution: { - integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A== + integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw== } engines: { node: ">=16 || 14 >=14.17" } - minisearch@7.2.0: + minipass-fetch@5.0.2: resolution: { - integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg== + integrity: sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ== } + engines: { node: ^20.17.0 || >=22.9.0 } - mitt@3.0.1: + minipass-flush@1.0.5: resolution: { - integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== + integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== } + engines: { node: ">= 8" } - mlly@1.8.0: + minipass-pipeline@1.2.4: resolution: { - integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g== + integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== } + engines: { node: ">=8" } - ms@2.1.3: + minipass-sized@2.0.0: resolution: { - integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + integrity: sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA== } + engines: { node: ">=8" } - nanoid@3.3.11: + minipass@3.3.6: resolution: { - integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== } - engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } - hasBin: true + engines: { node: ">=8" } - natural-compare@1.4.0: + minipass@7.1.3: resolution: { - integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A== } + engines: { node: ">=16 || 14 >=14.17" } - nodemon@3.1.11: + minisearch@7.2.0: resolution: { - integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g== + integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg== } - engines: { node: ">=10" } - hasBin: true + + minizlib@3.1.0: + resolution: + { + integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw== + } + engines: { node: ">= 18" } + + mitt@3.0.1: + resolution: + { + integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== + } + + mlly@1.8.0: + resolution: + { + integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g== + } + + ms@2.1.3: + resolution: + { + integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + } + + nanoid@3.3.11: + resolution: + { + integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + } + engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } + hasBin: true + + natural-compare@1.4.0: + resolution: + { + integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + } + + negotiator@1.0.0: + resolution: + { + integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== + } + engines: { node: ">= 0.6" } + + node-gyp@12.2.0: + resolution: + { + integrity: sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ== + } + engines: { node: ^20.17.0 || >=22.9.0 } + hasBin: true + + nodemon@3.1.11: + resolution: + { + integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g== + } + engines: { node: ">=10" } + hasBin: true non-layered-tidy-tree-layout@2.0.2: resolution: @@ -3695,6 +4126,14 @@ packages: integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw== } + nopt@9.0.0: + resolution: + { + integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw== + } + engines: { node: ^20.17.0 || >=22.9.0 } + hasBin: true + normalize-path@3.0.0: resolution: { @@ -3702,6 +4141,55 @@ packages: } engines: { node: ">=0.10.0" } + npm-bundled@5.0.0: + resolution: + { + integrity: sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + npm-install-checks@8.0.0: + resolution: + { + integrity: sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + npm-normalize-package-bin@5.0.0: + resolution: + { + integrity: sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + npm-package-arg@13.0.2: + resolution: + { + integrity: sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + npm-packlist@10.0.4: + resolution: + { + integrity: sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + npm-pick-manifest@11.0.3: + resolution: + { + integrity: sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + npm-registry-fetch@19.1.1: + resolution: + { + integrity: sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw== + } + engines: { node: ^20.17.0 || >=22.9.0 } + npm-run-path@5.3.0: resolution: { @@ -3750,6 +4238,13 @@ packages: } engines: { node: ">=10" } + p-map@7.0.4: + resolution: + { + integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ== + } + engines: { node: ">=18" } + package-json-from-dist@1.0.1: resolution: { @@ -3762,6 +4257,14 @@ packages: integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA== } + pacote@21.3.1: + resolution: + { + integrity: sha512-O0EDXi85LF4AzdjG74GUwEArhdvawi/YOHcsW6IijKNj7wm8IvEWNF5GnfuxNpQ/ZpO3L37+v8hqdVh8GgWYhg== + } + engines: { node: ^20.17.0 || >=22.9.0 } + hasBin: true + parent-module@1.0.1: resolution: { @@ -3909,6 +4412,20 @@ packages: engines: { node: ">=14" } hasBin: true + proc-log@6.1.0: + resolution: + { + integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + promise-retry@2.0.1: + resolution: + { + integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + } + engines: { node: ">=10" } + property-information@7.1.0: resolution: { @@ -3965,6 +4482,13 @@ packages: integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg== } + require-from-string@2.0.2: + resolution: + { + integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + } + engines: { node: ">=0.10.0" } + resolve-from@4.0.0: resolution: { @@ -3979,6 +4503,20 @@ packages: } engines: { node: ">=18" } + retry@0.12.0: + resolution: + { + integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + } + engines: { node: ">= 4" } + + retry@0.13.1: + resolution: + { + integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + } + engines: { node: ">= 4" } + reusify@1.1.0: resolution: { @@ -4044,6 +4582,13 @@ packages: integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ== } + section-matter@1.0.0: + resolution: + { + integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA== + } + engines: { node: ">=4" } + semver@7.7.4: resolution: { @@ -4085,6 +4630,19 @@ packages: } engines: { node: ">=14" } + sigstore@4.1.0: + resolution: + { + integrity: sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + simple-git@3.33.0: + resolution: + { + integrity: sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng== + } + simple-update-notifier@2.0.0: resolution: { @@ -4112,6 +4670,27 @@ packages: } engines: { node: ">=18" } + smart-buffer@4.2.0: + resolution: + { + integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + } + engines: { node: ">= 6.0.0", npm: ">= 3.0.0" } + + socks-proxy-agent@8.0.5: + resolution: + { + integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw== + } + engines: { node: ">= 14" } + + socks@2.8.7: + resolution: + { + integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A== + } + engines: { node: ">= 10.0.0", npm: ">= 3.0.0" } + source-map-js@1.2.1: resolution: { @@ -4125,6 +4704,24 @@ packages: integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== } + spdx-exceptions@2.5.0: + resolution: + { + integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== + } + + spdx-expression-parse@4.0.0: + resolution: + { + integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ== + } + + spdx-license-ids@3.0.23: + resolution: + { + integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw== + } + speakingurl@14.0.1: resolution: { @@ -4132,6 +4729,19 @@ packages: } engines: { node: ">=0.10.0" } + sprintf-js@1.0.3: + resolution: + { + integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + } + + ssri@13.0.1: + resolution: + { + integrity: sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ== + } + engines: { node: ^20.17.0 || >=22.9.0 } + stackback@0.0.2: resolution: { @@ -4192,6 +4802,13 @@ packages: } engines: { node: ">=12" } + strip-bom-string@1.0.0: + resolution: + { + integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g== + } + engines: { node: ">=0.10.0" } + strip-final-newline@3.0.0: resolution: { @@ -4245,6 +4862,13 @@ packages: integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg== } + tar@7.5.11: + resolution: + { + integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ== + } + engines: { node: ">=18" } + test-exclude@7.0.1: resolution: { @@ -4335,6 +4959,13 @@ packages: } engines: { node: ">=6.10" } + tuf-js@4.1.0: + resolution: + { + integrity: sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ== + } + engines: { node: ^20.17.0 || >=22.9.0 } + turbo-darwin-64@2.8.10: resolution: { @@ -4433,6 +5064,20 @@ packages: integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== } + unique-filename@5.0.0: + resolution: + { + integrity: sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg== + } + engines: { node: ^20.17.0 || >=22.9.0 } + + unique-slug@6.0.0: + resolution: + { + integrity: sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw== + } + engines: { node: ^20.17.0 || >=22.9.0 } + unist-util-is@6.0.1: resolution: { @@ -4476,6 +5121,13 @@ packages: } hasBin: true + validate-npm-package-name@7.0.2: + resolution: + { + integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A== + } + engines: { node: ^20.17.0 || >=22.9.0 } + vfile-message@4.0.3: resolution: { @@ -4685,6 +5337,14 @@ packages: engines: { node: ">= 8" } hasBin: true + which@6.0.1: + resolution: + { + integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg== + } + engines: { node: ^20.17.0 || >=22.9.0 } + hasBin: true + why-is-node-running@2.3.0: resolution: { @@ -4721,6 +5381,19 @@ packages: } engines: { node: ">=18" } + yallist@4.0.0: + resolution: + { + integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + } + + yallist@5.0.0: + resolution: + { + integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw== + } + engines: { node: ">=18" } + yaml@2.8.2: resolution: { @@ -4913,6 +5586,16 @@ snapshots: "@clack/core": 1.1.0 sisteransi: 1.0.5 + "@codemcp/skills@2.1.1": + dependencies: + ajv: 8.18.0 + gray-matter: 4.0.3 + js-yaml: 4.1.1 + pacote: 21.3.1 + simple-git: 3.33.0 + transitivePeerDependencies: + - supports-color + "@docsearch/css@3.9.0": {} "@docsearch/js@3.9.0(@algolia/client-search@5.49.0)(search-insights@2.17.3)": @@ -5132,6 +5815,10 @@ snapshots: "@eslint/core": 0.17.0 levn: 0.4.1 + "@gar/promise-retry@1.0.2": + dependencies: + retry: 0.13.1 + "@humanfs/core@0.19.1": {} "@humanfs/node@0.16.7": @@ -5171,6 +5858,10 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + "@isaacs/fs-minipass@4.0.1": + dependencies: + minipass: 7.1.3 + "@istanbuljs/schema@0.1.3": {} "@jridgewell/gen-mapping@0.3.13": @@ -5187,6 +5878,14 @@ snapshots: "@jridgewell/resolve-uri": 3.1.2 "@jridgewell/sourcemap-codec": 1.5.5 + "@kwsites/file-exists@1.1.1": + dependencies: + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + "@kwsites/promise-deferred@1.1.1": {} + "@mermaid-js/mermaid-mindmap@9.3.0": dependencies: "@braintree/sanitize-url": 6.0.4 @@ -5214,6 +5913,64 @@ snapshots: "@nodelib/fs.scandir": 2.1.5 fastq: 1.20.1 + "@npmcli/agent@4.0.0": + dependencies: + agent-base: 7.1.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 11.2.6 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + "@npmcli/fs@5.0.0": + dependencies: + semver: 7.7.4 + + "@npmcli/git@7.0.2": + dependencies: + "@gar/promise-retry": 1.0.2 + "@npmcli/promise-spawn": 9.0.1 + ini: 6.0.0 + lru-cache: 11.2.6 + npm-pick-manifest: 11.0.3 + proc-log: 6.1.0 + semver: 7.7.4 + which: 6.0.1 + + "@npmcli/installed-package-contents@4.0.0": + dependencies: + npm-bundled: 5.0.0 + npm-normalize-package-bin: 5.0.0 + + "@npmcli/node-gyp@5.0.0": {} + + "@npmcli/package-json@7.0.5": + dependencies: + "@npmcli/git": 7.0.2 + glob: 13.0.5 + hosted-git-info: 9.0.2 + json-parse-even-better-errors: 5.0.0 + proc-log: 6.1.0 + semver: 7.7.4 + spdx-expression-parse: 4.0.0 + + "@npmcli/promise-spawn@9.0.1": + dependencies: + which: 6.0.1 + + "@npmcli/redact@4.0.0": {} + + "@npmcli/run-script@10.0.4": + dependencies: + "@npmcli/node-gyp": 5.0.0 + "@npmcli/package-json": 7.0.5 + "@npmcli/promise-spawn": 9.0.1 + node-gyp: 12.2.0 + proc-log: 6.1.0 + transitivePeerDependencies: + - supports-color + "@pkgjs/parseargs@0.11.0": optional: true @@ -5332,6 +6089,38 @@ snapshots: "@shikijs/vscode-textmate@10.0.2": {} + "@sigstore/bundle@4.0.0": + dependencies: + "@sigstore/protobuf-specs": 0.5.0 + + "@sigstore/core@3.1.0": {} + + "@sigstore/protobuf-specs@0.5.0": {} + + "@sigstore/sign@4.1.0": + dependencies: + "@sigstore/bundle": 4.0.0 + "@sigstore/core": 3.1.0 + "@sigstore/protobuf-specs": 0.5.0 + make-fetch-happen: 15.0.4 + proc-log: 6.1.0 + promise-retry: 2.0.1 + transitivePeerDependencies: + - supports-color + + "@sigstore/tuf@4.0.1": + dependencies: + "@sigstore/protobuf-specs": 0.5.0 + tuf-js: 4.1.0 + transitivePeerDependencies: + - supports-color + + "@sigstore/verify@3.1.0": + dependencies: + "@sigstore/bundle": 4.0.0 + "@sigstore/core": 3.1.0 + "@sigstore/protobuf-specs": 0.5.0 + "@swc/core-darwin-arm64@1.15.11": optional: true @@ -5388,6 +6177,13 @@ snapshots: "@tsconfig/strictest@2.0.5": {} + "@tufjs/canonical-json@2.0.0": {} + + "@tufjs/models@4.1.0": + dependencies: + "@tufjs/canonical-json": 2.0.0 + minimatch: 10.2.1 + "@types/chai@5.2.3": dependencies: "@types/deep-eql": 4.0.2 @@ -5883,12 +6679,16 @@ snapshots: transitivePeerDependencies: - typescript + abbrev@4.0.0: {} + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 acorn@8.16.0: {} + agent-base@7.1.4: {} + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -5896,6 +6696,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + algoliasearch@5.49.0: dependencies: "@algolia/abtesting": 1.15.0 @@ -5932,6 +6739,10 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} assertion-error@2.0.1: {} @@ -5969,6 +6780,20 @@ snapshots: cac@6.7.14: {} + cacache@20.0.3: + dependencies: + "@npmcli/fs": 5.0.0 + fs-minipass: 3.0.3 + glob: 13.0.5 + lru-cache: 11.2.6 + minipass: 7.1.3 + minipass-collect: 2.0.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + p-map: 7.0.4 + ssri: 13.0.1 + unique-filename: 5.0.0 + callsites@3.1.0: {} ccount@2.0.1: {} @@ -6020,6 +6845,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chownr@3.0.0: {} + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -6297,8 +7124,12 @@ snapshots: entities@7.0.1: {} + env-paths@2.2.1: {} + environment@1.1.0: {} + err-code@2.0.3: {} + es-module-lexer@1.7.0: {} esbuild@0.21.5: @@ -6418,6 +7249,8 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 4.2.1 + esprima@4.0.1: {} + esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -6452,8 +7285,14 @@ snapshots: expect-type@1.3.0: {} + exponential-backoff@3.1.3: {} + exsolve@1.0.8: {} + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -6468,6 +7307,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.1.0: {} + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -6505,6 +7346,10 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + fs-minipass@3.0.3: + dependencies: + minipass: 7.1.3 + fsevents@2.3.3: optional: true @@ -6539,8 +7384,17 @@ snapshots: globals@15.15.0: {} + graceful-fs@4.2.11: {} + graphemer@1.4.0: {} + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.2 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + hachure-fill@0.5.2: {} has-flag@3.0.0: {} @@ -6567,10 +7421,30 @@ snapshots: hookable@5.5.3: {} + hosted-git-info@9.0.2: + dependencies: + lru-cache: 11.2.6 + html-escaper@2.0.2: {} html-void-elements@3.0.0: {} + http-cache-semantics@4.2.0: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + human-signals@5.0.0: {} husky@9.1.7: {} @@ -6579,8 +7453,17 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + optional: true + ignore-by-default@1.0.1: {} + ignore-walk@8.0.0: + dependencies: + minimatch: 10.2.1 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -6592,14 +7475,20 @@ snapshots: imurmurhash@0.1.4: {} + ini@6.0.0: {} + internmap@1.0.1: {} internmap@2.0.3: {} + ip-address@10.1.0: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 + is-extendable@0.1.1: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -6622,6 +7511,8 @@ snapshots: isexe@2.0.0: {} + isexe@4.0.0: {} + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -6653,16 +7544,27 @@ snapshots: js-tokens@9.0.1: {} + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@4.1.1: dependencies: argparse: 2.0.1 json-buffer@3.0.1: {} + json-parse-even-better-errors@5.0.0: {} + json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} + jsonparse@1.3.1: {} + katex@0.16.28: dependencies: commander: 8.3.0 @@ -6673,6 +7575,8 @@ snapshots: khroma@2.1.0: {} + kind-of@6.0.3: {} + kolorist@1.8.0: {} langium@3.0.0: @@ -6762,6 +7666,22 @@ snapshots: dependencies: semver: 7.7.4 + make-fetch-happen@15.0.4: + dependencies: + "@gar/promise-retry": 1.0.2 + "@npmcli/agent": 4.0.0 + cacache: 20.0.3 + http-cache-semantics: 4.2.0 + minipass: 7.1.3 + minipass-fetch: 5.0.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 1.0.0 + proc-log: 6.1.0 + ssri: 13.0.1 + transitivePeerDependencies: + - supports-color + mark.js@8.11.1: {} marked@13.0.3: {} @@ -6845,10 +7765,42 @@ snapshots: dependencies: brace-expansion: 2.0.2 + minipass-collect@2.0.1: + dependencies: + minipass: 7.1.3 + + minipass-fetch@5.0.2: + dependencies: + minipass: 7.1.3 + minipass-sized: 2.0.0 + minizlib: 3.1.0 + optionalDependencies: + iconv-lite: 0.7.2 + + minipass-flush@1.0.5: + dependencies: + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@2.0.0: + dependencies: + minipass: 7.1.3 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + minipass@7.1.3: {} minisearch@7.2.0: {} + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + mitt@3.0.1: {} mlly@1.8.0: @@ -6864,6 +7816,23 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.0.0: {} + + node-gyp@12.2.0: + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.3 + graceful-fs: 4.2.11 + make-fetch-happen: 15.0.4 + nopt: 9.0.0 + proc-log: 6.1.0 + semver: 7.7.4 + tar: 7.5.11 + tinyglobby: 0.2.15 + which: 6.0.1 + transitivePeerDependencies: + - supports-color + nodemon@3.1.11: dependencies: chokidar: 3.6.0 @@ -6880,8 +7849,54 @@ snapshots: non-layered-tidy-tree-layout@2.0.2: optional: true + nopt@9.0.0: + dependencies: + abbrev: 4.0.0 + normalize-path@3.0.0: {} + npm-bundled@5.0.0: + dependencies: + npm-normalize-package-bin: 5.0.0 + + npm-install-checks@8.0.0: + dependencies: + semver: 7.7.4 + + npm-normalize-package-bin@5.0.0: {} + + npm-package-arg@13.0.2: + dependencies: + hosted-git-info: 9.0.2 + proc-log: 6.1.0 + semver: 7.7.4 + validate-npm-package-name: 7.0.2 + + npm-packlist@10.0.4: + dependencies: + ignore-walk: 8.0.0 + proc-log: 6.1.0 + + npm-pick-manifest@11.0.3: + dependencies: + npm-install-checks: 8.0.0 + npm-normalize-package-bin: 5.0.0 + npm-package-arg: 13.0.2 + semver: 7.7.4 + + npm-registry-fetch@19.1.1: + dependencies: + "@npmcli/redact": 4.0.0 + jsonparse: 1.3.1 + make-fetch-happen: 15.0.4 + minipass: 7.1.3 + minipass-fetch: 5.0.2 + minizlib: 3.1.0 + npm-package-arg: 13.0.2 + proc-log: 6.1.0 + transitivePeerDependencies: + - supports-color + npm-run-path@5.3.0: dependencies: path-key: 4.0.0 @@ -6917,10 +7932,34 @@ snapshots: dependencies: p-limit: 3.1.0 + p-map@7.0.4: {} + package-json-from-dist@1.0.1: {} package-manager-detector@1.6.0: {} + pacote@21.3.1: + dependencies: + "@npmcli/git": 7.0.2 + "@npmcli/installed-package-contents": 4.0.0 + "@npmcli/package-json": 7.0.5 + "@npmcli/promise-spawn": 9.0.1 + "@npmcli/run-script": 10.0.4 + cacache: 20.0.3 + fs-minipass: 3.0.3 + minipass: 7.1.3 + npm-package-arg: 13.0.2 + npm-packlist: 10.0.4 + npm-pick-manifest: 11.0.3 + npm-registry-fetch: 19.1.1 + proc-log: 6.1.0 + promise-retry: 2.0.1 + sigstore: 4.1.0 + ssri: 13.0.1 + tar: 7.5.11 + transitivePeerDependencies: + - supports-color + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -6988,6 +8027,13 @@ snapshots: prettier@3.8.1: {} + proc-log@6.1.0: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + property-information@7.1.0: {} pstree.remy@1.1.8: {} @@ -7012,6 +8058,8 @@ snapshots: dependencies: regex-utilities: 2.3.0 + require-from-string@2.0.2: {} + resolve-from@4.0.0: {} restore-cursor@5.1.0: @@ -7019,6 +8067,10 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retry@0.12.0: {} + + retry@0.13.1: {} + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -7078,6 +8130,11 @@ snapshots: search-insights@2.17.3: {} + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + semver@7.7.4: {} shebang-command@2.0.0: @@ -7101,6 +8158,25 @@ snapshots: signal-exit@4.1.0: {} + sigstore@4.1.0: + dependencies: + "@sigstore/bundle": 4.0.0 + "@sigstore/core": 3.1.0 + "@sigstore/protobuf-specs": 0.5.0 + "@sigstore/sign": 4.1.0 + "@sigstore/tuf": 4.0.1 + "@sigstore/verify": 3.1.0 + transitivePeerDependencies: + - supports-color + + simple-git@3.33.0: + dependencies: + "@kwsites/file-exists": 1.1.1 + "@kwsites/promise-deferred": 1.1.1 + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + simple-update-notifier@2.0.0: dependencies: semver: 7.7.4 @@ -7117,12 +8193,42 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.0(supports-color@5.5.0) + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + source-map-js@1.2.1: {} space-separated-tokens@2.0.2: {} + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@4.0.0: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.23 + + spdx-license-ids@3.0.23: {} + speakingurl@14.0.1: {} + sprintf-js@1.0.3: {} + + ssri@13.0.1: + dependencies: + minipass: 7.1.3 + stackback@0.0.2: {} std-env@3.10.0: {} @@ -7160,6 +8266,8 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-bom-string@1.0.0: {} + strip-final-newline@3.0.0: {} strip-json-comments@3.1.1: {} @@ -7184,6 +8292,14 @@ snapshots: tabbable@6.4.0: {} + tar@7.5.11: + dependencies: + "@isaacs/fs-minipass": 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + test-exclude@7.0.1: dependencies: "@istanbuljs/schema": 0.1.3 @@ -7221,6 +8337,14 @@ snapshots: ts-dedent@2.2.0: {} + tuf-js@4.1.0: + dependencies: + "@tufjs/models": 4.1.0 + debug: 4.4.3 + make-fetch-happen: 15.0.4 + transitivePeerDependencies: + - supports-color + turbo-darwin-64@2.8.10: optional: true @@ -7270,6 +8394,14 @@ snapshots: undici-types@6.21.0: {} + unique-filename@5.0.0: + dependencies: + unique-slug: 6.0.0 + + unique-slug@6.0.0: + dependencies: + imurmurhash: 0.1.4 + unist-util-is@6.0.1: dependencies: "@types/unist": 3.0.3 @@ -7299,6 +8431,8 @@ snapshots: uuid@9.0.1: {} + validate-npm-package-name@7.0.2: {} + vfile-message@4.0.3: dependencies: "@types/unist": 3.0.3 @@ -7480,6 +8614,10 @@ snapshots: dependencies: isexe: 2.0.0 + which@6.0.1: + dependencies: + isexe: 4.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -7505,6 +8643,10 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.2 + yallist@4.0.0: {} + + yallist@5.0.0: {} + yaml@2.8.2: {} yocto-queue@0.1.0: {} From 89190e3ceeb6a0f00e5046aa31b5212324e652dc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 06:24:38 +0000 Subject: [PATCH 33/60] fix: remove unused readUserConfig import to fix lint error https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/cli/src/commands/install.integration.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/install.integration.spec.ts b/packages/cli/src/commands/install.integration.spec.ts index b7b73bb..0b0dbce 100644 --- a/packages/cli/src/commands/install.integration.spec.ts +++ b/packages/cli/src/commands/install.integration.spec.ts @@ -18,7 +18,7 @@ vi.mock("@clack/prompts", () => ({ import * as clack from "@clack/prompts"; import { runSetup } from "./setup.js"; import { runInstall } from "./install.js"; -import { readUserConfig, readLockFile } from "@ade/core"; +import { readLockFile } from "@ade/core"; import { getDefaultCatalog } from "../../../core/src/catalog/index.js"; describe("install integration (real temp dir)", () => { From 8fe850ea70145bc66aaceb344ad6e8e26e24edd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20J=C3=A4gle?= Date: Mon, 16 Mar 2026 07:55:21 +0100 Subject: [PATCH 34/60] docs: rename cli docs --- docs/{PRD.md => CLI-PRD.md} | 0 docs/{DESIGN.md => CLI-design.md} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename docs/{PRD.md => CLI-PRD.md} (100%) rename docs/{DESIGN.md => CLI-design.md} (100%) diff --git a/docs/PRD.md b/docs/CLI-PRD.md similarity index 100% rename from docs/PRD.md rename to docs/CLI-PRD.md diff --git a/docs/DESIGN.md b/docs/CLI-design.md similarity index 100% rename from docs/DESIGN.md rename to docs/CLI-design.md From 082f996cbaccae9a83c6557bb2ee6f28137cb540 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 12:07:50 +0000 Subject: [PATCH 35/60] refactor: use @codemcp/skills@2.3.0 types instead of local .d.ts Version 2.3.0 of @codemcp/skills now ships type declarations for its ./api export, so the hand-written codemcp-skills.d.ts is no longer needed. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/cli/package.json | 2 +- packages/cli/src/codemcp-skills.d.ts | 21 --------------------- pnpm-lock.yaml | 10 +++++----- 3 files changed, 6 insertions(+), 27 deletions(-) delete mode 100644 packages/cli/src/codemcp-skills.d.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 2b7d1a1..010efb6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -24,7 +24,7 @@ "dependencies": { "@ade/core": "workspace:*", "@clack/prompts": "^1.1.0", - "@codemcp/skills": "^2.1.1" + "@codemcp/skills": "^2.3.0" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^8.21.0", diff --git a/packages/cli/src/codemcp-skills.d.ts b/packages/cli/src/codemcp-skills.d.ts deleted file mode 100644 index f7e35a9..0000000 --- a/packages/cli/src/codemcp-skills.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -declare module "@codemcp/skills/api" { - export interface AddOptions { - global?: boolean; - agent?: string[]; - yes?: boolean; - skill?: string[]; - list?: boolean; - all?: boolean; - fullDepth?: boolean; - copy?: boolean; - } - - export function runAdd(args: string[], options?: AddOptions): Promise; - - export function runInstallFromLock(args: string[]): Promise; - - export function parseAddOptions(args: string[]): { - source: string[]; - options: AddOptions; - }; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33d94c4..769ebd7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,8 +102,8 @@ importers: specifier: ^1.1.0 version: 1.1.0 "@codemcp/skills": - specifier: ^2.1.1 - version: 2.1.1 + specifier: ^2.3.0 + version: 2.3.0 devDependencies: "@typescript-eslint/eslint-plugin": specifier: ^8.21.0 @@ -395,10 +395,10 @@ packages: integrity: sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g== } - "@codemcp/skills@2.1.1": + "@codemcp/skills@2.3.0": resolution: { - integrity: sha512-aWFLefeFsI8dZVWzWXjAAVTsmVi3GJHRl/pqw5Sk5VxO3gfOB9Qh9AgwIPi/hyq5YwTc1vao6b2+rF7u5RhKPw== + integrity: sha512-5tc5i0FtWeOFfCGvhBmqX83mTQehTFKW5/EynH0VxByXjGZ4lmPFsM4WXutqe2vj7DxqAbw/XjlPIffSh859cQ== } engines: { node: ">=18" } hasBin: true @@ -5586,7 +5586,7 @@ snapshots: "@clack/core": 1.1.0 sisteransi: 1.0.5 - "@codemcp/skills@2.1.1": + "@codemcp/skills@2.3.0": dependencies: ajv: 8.18.0 gray-matter: 4.0.3 From 62a2a8bb2932d4bdeeaa8125848365ee42d4a9ad Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 13:40:54 +0000 Subject: [PATCH 36/60] refactor: split conventions facet into architecture and practices The single "conventions" facet conflated two fundamentally different kinds of choices. Architecture (e.g. TanStack) is a constraining, single-select stack decision. Practices (TDD, ADR, Conventional Commits) are composable, multi-select disciplines orthogonal to any stack. - Add architecture facet (single-select): stack/framework conventions - Add practices facet (multi-select): composable development disciplines - Remove conventions facet (replaced by the two above) - Update CLI setup flow, tests, and README documentation https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- README.md | 79 +++++++---- .../commands/conventions.integration.spec.ts | 95 ++++++++++--- .../src/commands/install.integration.spec.ts | 12 +- .../src/commands/setup.integration.spec.ts | 12 +- packages/core/src/catalog/catalog.spec.ts | 51 ++++--- .../core/src/catalog/facets/architecture.ts | 128 ++++++++++++++++++ .../facets/{conventions.ts => practices.ts} | 125 +---------------- packages/core/src/catalog/index.ts | 5 +- 8 files changed, 317 insertions(+), 190 deletions(-) create mode 100644 packages/core/src/catalog/facets/architecture.ts rename packages/core/src/catalog/facets/{conventions.ts => practices.ts} (55%) diff --git a/README.md b/README.md index 2054d90..37cc4b4 100644 --- a/README.md +++ b/README.md @@ -39,15 +39,15 @@ And then there is **documentation**: the reference material you consult while implementing — API details, library behavior, system specifications. You do not memorize it. You look it up at the moment you need it. -Three distinct types of information. Three different acquisition modes. Three -different roles in the act of engineering. +Four distinct types of information. Different acquisition modes. Different roles +in the act of engineering. ## What ADE is **ADE is an information architecture for agentic development.** It provides a rigid, technology-agnostic structure that organizes the information -a coding agent needs into three explicit layers — each mapped to a concrete, +a coding agent needs into explicit layers — each mapped to a concrete, agent-native artifact type — and a mechanism to compose them. ```mermaid @@ -62,16 +62,22 @@ flowchart TD P["Universal engineering workflows

loaded at every session. e. g. via AGENTS.md"] end - subgraph Conventions ["Conventions · Skills"] - S["Project-specific standards
selected per team or project

Provided on demand"] + subgraph Architecture ["Architecture · Skills (single-select)"] + A["Stack & framework conventions
shapes project structure, patterns, libraries

e. g. TanStack, Next.js, FastAPI"] + end + + subgraph Practices ["Practices · Skills (multi-select)"] + PR["Composable development disciplines
mix and match regardless of stack

e. g. TDD, ADR, Conventional Commits"] end subgraph Documentation ["Documentation · Text files"] D["Reference knowledge

read on demand
APIs · libraries · system context"] end - Process -->|"invokes — e. g. "use your design skill""| Conventions - Conventions -->|"points to — e. g. "check .docs/tanstack""| Documentation + Process -->|"invokes — e. g. "use your design skill""| Architecture + Process -->|"invokes — e. g. "use your TDD skill""| Practices + Architecture -->|"points to — e. g. "check .docs/tanstack""| Documentation + Practices -->|"points to — e. g. "check docs/adr/""| Documentation ``` ### Why this is needed @@ -87,7 +93,7 @@ They mix process instructions with coding conventions and documentation fragment in a single flat file. Rule files and skills improve reusability but still lack a coherent taxonomy. -ADE brings structure to this space. By separating the three layers explicitly and +ADE brings structure to this space. By separating the layers explicitly and binding each to a specific artifact type, it makes information easier to find, easier to maintain, and — critically — easier for agents to apply in the right context at the right moment. @@ -97,32 +103,42 @@ team, regardless of who is running them. ## How the layers compose -The three layers are not independent — they reference each other in a deliberate -direction. Process invokes conventions. Conventions point to documentation. +The layers are not independent — they reference each other in a deliberate +direction. Process invokes architecture and practice skills. Skills point to +documentation. **Process enforces workflows and delegates to skills:** > _"When in the plan phase, use your `design` skill."_ The agent system prompt defines and enforces the workflow. At the right step, it -delegates to a skill that encodes the team"s specific approach — keeping process +delegates to a skill that encodes the team's specific approach — keeping process universal and conventions local. -**Skills reference documentation on demand:** +**Architecture skills define the stack:** > _"We are using React with TanStack Query for backend interactions. Check_ > _`.docs/tanstack` when implementing data fetching. Check `.docs/components` for_ > _details on available reusable components."_ -The skill encodes the convention — which libraries, which patterns. It surfaces -the exact documentation needed at the moment it is relevant, rather than loading -everything upfront. +Architecture is a constraining choice — you pick one stack, and it shapes your +project structure, patterns, and libraries. This is a single-select decision: +TanStack or Next.js, not both. + +**Practice skills are composable disciplines:** + +> _"Use London-style TDD. Follow the Red-Green-Refactor cycle. Write ADRs for_ +> _significant decisions. Use Conventional Commits for all commit messages."_ + +Practices are orthogonal to architecture. You can combine TDD, ADR, and +Conventional Commits freely — they apply regardless of what stack you build with. +This is a multi-select decision. This composability is what makes ADE scale. Process is written once and shared -across every project. Skills are curated per team or per project context — ADE -provides a mechanism to select and compose skill sets, so the right conventions -are available for the right context. Documentation lives where it belongs — in -the codebase — and is surfaced precisely when needed. +across every project. Architecture and practice skills are curated per team or +per project context — ADE provides a mechanism to select and compose them, so +the right conventions are available for the right context. Documentation lives +where it belongs — in the codebase — and is surfaced precisely when needed. **Sounds intuitive?** Hopefully, it does. Because **this framework only works, if you as human join the team**. @@ -138,17 +154,24 @@ _does_ work. Every task type has a defined process. The agent follows it. This is the layer that transfers across every project without modification. It encodes the professional engineering mindset as an explicit, shareable artifact. -### Skill sets — selectable and composable +### Architecture — stack conventions (single-select) + +Architecture skills encode stack-specific knowledge: project structure, framework +patterns, and library conventions. You pick one architecture that shapes your +entire codebase — TanStack, Next.js, FastAPI, etc. This is a constraining choice +that determines how your project is organized. + +### Practices — development disciplines (multi-select) -Skills encode project-specific knowledge as reusable, invocable artifacts. They -capture technology choices, architectural patterns, and design decisions in a form -the agent applies on demand. +Practice skills encode composable development disciplines that apply regardless +of your stack: TDD, Architecture Decision Records, Conventional Commits, and +more. You can mix and match freely — they are orthogonal to each other and to +your architecture choice. -ADE provides a mechanism to select and share **skill sets** — curated collections -of skills that match a team"s context. A frontend team, a backend team, and a -platform team each activate the skill set appropriate to their work. Skills can -be shared across projects, versioned, and evolved independently of the process -layer. +ADE provides a mechanism to select and share both architecture and practice +skills. A frontend team, a backend team, and a platform team each activate the +combination appropriate to their work. Skills can be shared across projects, +versioned, and evolved independently of the process layer. ### Documentation sharing diff --git a/packages/cli/src/commands/conventions.integration.spec.ts b/packages/cli/src/commands/conventions.integration.spec.ts index 2fc4f86..5f42067 100644 --- a/packages/cli/src/commands/conventions.integration.spec.ts +++ b/packages/cli/src/commands/conventions.integration.spec.ts @@ -19,7 +19,7 @@ import { runSetup } from "./setup.js"; import { readUserConfig, readLockFile } from "@ade/core"; import { getDefaultCatalog } from "../../../core/src/catalog/index.js"; -describe("conventions facet integration", () => { +describe("architecture and practices facets integration", () => { let dir: string; beforeEach(async () => { @@ -32,13 +32,16 @@ describe("conventions facet integration", () => { }); it( - "writes SKILL.md files and installs inline skills for tanstack", + "writes SKILL.md files and installs inline skills for tanstack architecture", { timeout: 60_000 }, async () => { const catalog = getDefaultCatalog(); - vi.mocked(clack.select).mockResolvedValueOnce("codemcp-workflows"); - vi.mocked(clack.multiselect).mockResolvedValueOnce(["tanstack"]); + // Facet order: process (select), architecture (select), practices (multiselect) + vi.mocked(clack.select) + .mockResolvedValueOnce("codemcp-workflows") // process + .mockResolvedValueOnce("tanstack"); // architecture + vi.mocked(clack.multiselect).mockResolvedValueOnce([]); // practices: none await runSetup(dir, catalog); @@ -87,10 +90,13 @@ describe("conventions facet integration", () => { } ); - it("writes skills for multiple selected conventions", async () => { + it("writes skills for multiple selected practices", async () => { const catalog = getDefaultCatalog(); - vi.mocked(clack.select).mockResolvedValueOnce("native-agents-md"); + // Facet order: process (select), architecture (select), practices (multiselect) + vi.mocked(clack.select) + .mockResolvedValueOnce("native-agents-md") // process + .mockResolvedValueOnce("__skip__"); // architecture: skip vi.mocked(clack.multiselect).mockResolvedValueOnce([ "conventional-commits", "tdd-london" @@ -121,9 +127,9 @@ describe("conventions facet integration", () => { access(join(dir, ".agentskills", "skills", "tdd-london")) ).resolves.toBeUndefined(); - // config.yaml should have array of choices + // config.yaml should have array of choices under practices const config = await readUserConfig(dir); - expect(config!.choices.conventions).toEqual([ + expect(config!.choices.practices).toEqual([ "conventional-commits", "tdd-london" ]); @@ -136,7 +142,10 @@ describe("conventions facet integration", () => { it("writes ADR skill with template content", async () => { const catalog = getDefaultCatalog(); - vi.mocked(clack.select).mockResolvedValueOnce("native-agents-md"); + // Facet order: process (select), architecture (select), practices (multiselect) + vi.mocked(clack.select) + .mockResolvedValueOnce("native-agents-md") // process + .mockResolvedValueOnce("__skip__"); // architecture: skip vi.mocked(clack.multiselect).mockResolvedValueOnce(["adr-nygard"]); await runSetup(dir, catalog); @@ -151,26 +160,33 @@ describe("conventions facet integration", () => { expect(adr).toContain("## Consequences"); }); - it("skips conventions when none selected", async () => { + it("skips both architecture and practices when none selected", async () => { const catalog = getDefaultCatalog(); - vi.mocked(clack.select).mockResolvedValueOnce("native-agents-md"); - vi.mocked(clack.multiselect).mockResolvedValueOnce([]); + // Facet order: process (select), architecture (select), practices (multiselect) + vi.mocked(clack.select) + .mockResolvedValueOnce("native-agents-md") // process + .mockResolvedValueOnce("__skip__"); // architecture: skip + vi.mocked(clack.multiselect).mockResolvedValueOnce([]); // practices: none await runSetup(dir, catalog); // No .ade directory should exist await expect(access(join(dir, ".ade"))).rejects.toThrow(); - // config.yaml should not have conventions key + // config.yaml should not have architecture or practices keys const config = await readUserConfig(dir); - expect(config!.choices).not.toHaveProperty("conventions"); + expect(config!.choices).not.toHaveProperty("architecture"); + expect(config!.choices).not.toHaveProperty("practices"); }); - it("includes convention instructions in AGENTS.md", async () => { + it("includes practice instructions in AGENTS.md", async () => { const catalog = getDefaultCatalog(); - vi.mocked(clack.select).mockResolvedValueOnce("native-agents-md"); + // Facet order: process (select), architecture (select), practices (multiselect) + vi.mocked(clack.select) + .mockResolvedValueOnce("native-agents-md") // process + .mockResolvedValueOnce("__skip__"); // architecture: skip vi.mocked(clack.multiselect).mockResolvedValueOnce(["tdd-london"]); await runSetup(dir, catalog); @@ -179,4 +195,51 @@ describe("conventions facet integration", () => { expect(agentsMd).toContain("tdd-london"); expect(agentsMd).toContain("use_skill()"); }); + + it( + "combines architecture and practices selections", + { timeout: 60_000 }, + async () => { + const catalog = getDefaultCatalog(); + + // Facet order: process (select), architecture (select), practices (multiselect) + vi.mocked(clack.select) + .mockResolvedValueOnce("codemcp-workflows") // process + .mockResolvedValueOnce("tanstack"); // architecture + vi.mocked(clack.multiselect).mockResolvedValueOnce([ + "tdd-london", + "conventional-commits" + ]); + + await runSetup(dir, catalog); + + // Architecture skills should exist + const archSkill = await readFile( + join(dir, ".ade", "skills", "tanstack-architecture", "SKILL.md"), + "utf-8" + ); + expect(archSkill).toContain("name: tanstack-architecture"); + + // Practice skills should exist + const tddSkill = await readFile( + join(dir, ".ade", "skills", "tdd-london", "SKILL.md"), + "utf-8" + ); + expect(tddSkill).toContain("name: tdd-london"); + + const commitsSkill = await readFile( + join(dir, ".ade", "skills", "conventional-commits", "SKILL.md"), + "utf-8" + ); + expect(commitsSkill).toContain("name: conventional-commits"); + + // config.yaml should have both architecture and practices + const config = await readUserConfig(dir); + expect(config!.choices.architecture).toBe("tanstack"); + expect(config!.choices.practices).toEqual([ + "tdd-london", + "conventional-commits" + ]); + } + ); }); diff --git a/packages/cli/src/commands/install.integration.spec.ts b/packages/cli/src/commands/install.integration.spec.ts index 0b0dbce..82b4bec 100644 --- a/packages/cli/src/commands/install.integration.spec.ts +++ b/packages/cli/src/commands/install.integration.spec.ts @@ -37,7 +37,9 @@ describe("install integration (real temp dir)", () => { const catalog = getDefaultCatalog(); // Step 1: Run setup to create config.yaml - vi.mocked(clack.select).mockResolvedValueOnce("codemcp-workflows"); + vi.mocked(clack.select) + .mockResolvedValueOnce("codemcp-workflows") // process + .mockResolvedValueOnce("__skip__"); // architecture await runSetup(dir, catalog); // Step 2: Delete agent output files to simulate a fresh clone @@ -64,7 +66,9 @@ describe("install integration (real temp dir)", () => { const catalog = getDefaultCatalog(); // Setup first - vi.mocked(clack.select).mockResolvedValueOnce("codemcp-workflows"); + vi.mocked(clack.select) + .mockResolvedValueOnce("codemcp-workflows") // process + .mockResolvedValueOnce("__skip__"); // architecture await runSetup(dir, catalog); const lockBefore = await readLockFile(dir); @@ -91,7 +95,9 @@ describe("install integration (real temp dir)", () => { const catalog = getDefaultCatalog(); // Setup with native-agents-md - vi.mocked(clack.select).mockResolvedValueOnce("native-agents-md"); + vi.mocked(clack.select) + .mockResolvedValueOnce("native-agents-md") // process + .mockResolvedValueOnce("__skip__"); // architecture await runSetup(dir, catalog); // Delete agent output diff --git a/packages/cli/src/commands/setup.integration.spec.ts b/packages/cli/src/commands/setup.integration.spec.ts index cdcf67f..2af8264 100644 --- a/packages/cli/src/commands/setup.integration.spec.ts +++ b/packages/cli/src/commands/setup.integration.spec.ts @@ -35,7 +35,9 @@ describe("setup integration (real temp dir)", () => { it("writes config.yaml and config.lock.yaml for codemcp-workflows", async () => { const catalog = getDefaultCatalog(); - vi.mocked(clack.select).mockResolvedValueOnce("codemcp-workflows"); + vi.mocked(clack.select) + .mockResolvedValueOnce("codemcp-workflows") // process + .mockResolvedValueOnce("__skip__"); // architecture await runSetup(dir, catalog); @@ -75,7 +77,9 @@ describe("setup integration (real temp dir)", () => { it("writes config.yaml, lock, and AGENTS.md for native-agents-md", async () => { const catalog = getDefaultCatalog(); - vi.mocked(clack.select).mockResolvedValueOnce("native-agents-md"); + vi.mocked(clack.select) + .mockResolvedValueOnce("native-agents-md") // process + .mockResolvedValueOnce("__skip__"); // architecture await runSetup(dir, catalog); @@ -111,7 +115,9 @@ describe("setup integration (real temp dir)", () => { it("produces valid YAML that roundtrips through read", async () => { const catalog = getDefaultCatalog(); - vi.mocked(clack.select).mockResolvedValueOnce("codemcp-workflows"); + vi.mocked(clack.select) + .mockResolvedValueOnce("codemcp-workflows") // process + .mockResolvedValueOnce("__skip__"); // architecture await runSetup(dir, catalog); diff --git a/packages/core/src/catalog/catalog.spec.ts b/packages/core/src/catalog/catalog.spec.ts index f7f1751..1729e1b 100644 --- a/packages/core/src/catalog/catalog.spec.ts +++ b/packages/core/src/catalog/catalog.spec.ts @@ -44,18 +44,24 @@ describe("catalog", () => { }); }); - describe("conventions facet", () => { + describe("architecture facet", () => { it("exists in the default catalog", () => { const catalog = getDefaultCatalog(); - const conventions = getFacet(catalog, "conventions"); - expect(conventions).toBeDefined(); - expect(conventions!.required).toBe(false); + const architecture = getFacet(catalog, "architecture"); + expect(architecture).toBeDefined(); + expect(architecture!.required).toBe(false); + }); + + it("is single-select", () => { + const catalog = getDefaultCatalog(); + const architecture = getFacet(catalog, "architecture")!; + expect(architecture.multiSelect).toBe(false); }); it("has tanstack option with skills for architecture, design, code, testing, and playwright", () => { const catalog = getDefaultCatalog(); - const conventions = getFacet(catalog, "conventions")!; - const tanstack = getOption(conventions, "tanstack"); + const architecture = getFacet(catalog, "architecture")!; + const tanstack = getOption(architecture, "tanstack"); expect(tanstack).toBeDefined(); const skillsProvisions = tanstack!.recipe.filter( @@ -82,11 +88,26 @@ describe("catalog", () => { ); expect(playwright).not.toHaveProperty("body"); }); + }); + + describe("practices facet", () => { + it("exists in the default catalog", () => { + const catalog = getDefaultCatalog(); + const practices = getFacet(catalog, "practices"); + expect(practices).toBeDefined(); + expect(practices!.required).toBe(false); + }); + + it("is multi-select", () => { + const catalog = getDefaultCatalog(); + const practices = getFacet(catalog, "practices")!; + expect(practices.multiSelect).toBe(true); + }); it("has conventional-commits option with a single skill", () => { const catalog = getDefaultCatalog(); - const conventions = getFacet(catalog, "conventions")!; - const option = getOption(conventions, "conventional-commits"); + const practices = getFacet(catalog, "practices")!; + const option = getOption(practices, "conventional-commits"); expect(option).toBeDefined(); const skills = ( @@ -100,25 +121,19 @@ describe("catalog", () => { it("has tdd-london option with a single skill", () => { const catalog = getDefaultCatalog(); - const conventions = getFacet(catalog, "conventions")!; - const option = getOption(conventions, "tdd-london"); + const practices = getFacet(catalog, "practices")!; + const option = getOption(practices, "tdd-london"); expect(option).toBeDefined(); }); it("has adr-nygard option with a single skill", () => { const catalog = getDefaultCatalog(); - const conventions = getFacet(catalog, "conventions")!; - const option = getOption(conventions, "adr-nygard"); + const practices = getFacet(catalog, "practices")!; + const option = getOption(practices, "adr-nygard"); expect(option).toBeDefined(); }); - - it("is multi-select", () => { - const catalog = getDefaultCatalog(); - const conventions = getFacet(catalog, "conventions")!; - expect(conventions.multiSelect).toBe(true); - }); }); describe("catalog + registry integration", () => { diff --git a/packages/core/src/catalog/facets/architecture.ts b/packages/core/src/catalog/facets/architecture.ts new file mode 100644 index 0000000..6e88033 --- /dev/null +++ b/packages/core/src/catalog/facets/architecture.ts @@ -0,0 +1,128 @@ +import type { Facet } from "../../types.js"; + +export const architectureFacet: Facet = { + id: "architecture", + label: "Architecture", + description: + "Stack and framework conventions that shape your project structure", + required: false, + multiSelect: false, + options: [ + { + id: "tanstack", + label: "TanStack", + description: + "Full-stack conventions for TanStack (Router, Query, Form, Table)", + recipe: [ + { + writer: "skills", + config: { + skills: [ + { + name: "tanstack-architecture", + description: + "Architecture conventions for TanStack applications", + body: [ + "# TanStack Architecture Conventions", + "", + "## Project Structure", + "- Use file-based routing with TanStack Router (`routes/` directory)", + "- Colocate route components with their loaders and actions", + "- Organize by feature, not by type (e.g. `features/auth/`, not `components/auth/`)", + "", + "## Data Flow", + "- Use TanStack Query for all server state management", + "- Use TanStack Router loaders for route-level data requirements", + "- Keep client state minimal — prefer server state via Query", + "- Use `queryOptions()` factory pattern for reusable query definitions", + "", + "## Module Boundaries", + "- Each feature exports a public API via `index.ts`", + "- Features must not import from other features' internals", + "- Shared code goes in `lib/` or `shared/`" + ].join("\n") + }, + { + name: "tanstack-design", + description: "Design patterns for TanStack applications", + body: [ + "# TanStack Design Patterns", + "", + "## Query Patterns", + "- Define query options as standalone functions: `export const userQueryOptions = (id: string) => queryOptions({ queryKey: ['user', id], queryFn: () => fetchUser(id) })`", + "- Use `useSuspenseQuery` in route components paired with `loader` for prefetching", + "- Use `useMutation` with `onSettled` for cache invalidation", + "", + "## Router Patterns", + "- Define routes using `createFileRoute` for type-safe file-based routing", + "- Use `beforeLoad` for auth guards and redirects", + "- Use search params validation with `zodSearchValidator` for type-safe URL state", + "", + "## Form Patterns", + "- Use TanStack Form with Zod validators for form state and validation", + "- Prefer field-level validation over form-level where possible", + "- Connect form submission to `useMutation` for server sync" + ].join("\n") + }, + { + name: "tanstack-code", + description: "Code style conventions for TanStack applications", + body: [ + "# TanStack Code Conventions", + "", + "## TypeScript", + "- Enable strict mode in tsconfig", + "- Infer types from TanStack APIs rather than writing manual type annotations", + "- Use `satisfies` operator for type-safe object literals", + "", + "## Naming", + "- Query keys: `['entity', ...params]` (e.g. `['user', userId]`)", + "- Query option factories: `entityQueryOptions` (e.g. `userQueryOptions`)", + "- Route files: `$param` for dynamic segments (e.g. `users/$userId.tsx`)", + "- Loaders: export as named `loader` from route file", + "", + "## Imports", + "- Import from `@tanstack/react-query`, `@tanstack/react-router`, etc.", + "- Never import internal modules from TanStack packages", + "- Use path aliases for project imports (`@/features/...`)" + ].join("\n") + }, + { + name: "tanstack-testing", + description: "Testing conventions for TanStack applications", + body: [ + "# TanStack Testing Conventions", + "", + "## Query Testing", + "- Wrap components in `QueryClientProvider` with a fresh `QueryClient` per test", + "- Use `@testing-library/react` with `renderHook` for testing custom query hooks", + "- Mock at the network level with MSW, not at the query level", + "", + "## Router Testing", + "- Use `createMemoryHistory` and `createRouter` for route testing", + "- Test route loaders independently as plain async functions", + "- Test search param validation with unit tests on the validator schema", + "", + "## Integration Tests", + "- Test full user flows through route transitions", + "- Assert on visible UI state, not internal query cache state", + "- Use `waitFor` for async query resolution in component tests" + ].join("\n") + }, + { + name: "playwright-cli", + source: "microsoft/playwright-cli/skills/playwright-cli" + } + ] + } + }, + { + writer: "instruction", + config: { + text: "This project follows TanStack conventions. Use use_skill() to access the tanstack-architecture, tanstack-design, tanstack-code, tanstack-testing, and playwright-cli skills before making changes." + } + } + ] + } + ] +}; diff --git a/packages/core/src/catalog/facets/conventions.ts b/packages/core/src/catalog/facets/practices.ts similarity index 55% rename from packages/core/src/catalog/facets/conventions.ts rename to packages/core/src/catalog/facets/practices.ts index a1c38cc..09f230e 100644 --- a/packages/core/src/catalog/facets/conventions.ts +++ b/packages/core/src/catalog/facets/practices.ts @@ -1,128 +1,13 @@ import type { Facet } from "../../types.js"; -export const conventionsFacet: Facet = { - id: "conventions", - label: "Conventions", - description: "Team conventions your AI agent should follow", +export const practicesFacet: Facet = { + id: "practices", + label: "Practices", + description: + "Composable development practices — mix and match regardless of stack", required: false, multiSelect: true, options: [ - { - id: "tanstack", - label: "TanStack", - description: - "Full-stack conventions for TanStack (Router, Query, Form, Table)", - recipe: [ - { - writer: "skills", - config: { - skills: [ - { - name: "tanstack-architecture", - description: - "Architecture conventions for TanStack applications", - body: [ - "# TanStack Architecture Conventions", - "", - "## Project Structure", - "- Use file-based routing with TanStack Router (`routes/` directory)", - "- Colocate route components with their loaders and actions", - "- Organize by feature, not by type (e.g. `features/auth/`, not `components/auth/`)", - "", - "## Data Flow", - "- Use TanStack Query for all server state management", - "- Use TanStack Router loaders for route-level data requirements", - "- Keep client state minimal — prefer server state via Query", - "- Use `queryOptions()` factory pattern for reusable query definitions", - "", - "## Module Boundaries", - "- Each feature exports a public API via `index.ts`", - "- Features must not import from other features' internals", - "- Shared code goes in `lib/` or `shared/`" - ].join("\n") - }, - { - name: "tanstack-design", - description: "Design patterns for TanStack applications", - body: [ - "# TanStack Design Patterns", - "", - "## Query Patterns", - "- Define query options as standalone functions: `export const userQueryOptions = (id: string) => queryOptions({ queryKey: ['user', id], queryFn: () => fetchUser(id) })`", - "- Use `useSuspenseQuery` in route components paired with `loader` for prefetching", - "- Use `useMutation` with `onSettled` for cache invalidation", - "", - "## Router Patterns", - "- Define routes using `createFileRoute` for type-safe file-based routing", - "- Use `beforeLoad` for auth guards and redirects", - "- Use search params validation with `zodSearchValidator` for type-safe URL state", - "", - "## Form Patterns", - "- Use TanStack Form with Zod validators for form state and validation", - "- Prefer field-level validation over form-level where possible", - "- Connect form submission to `useMutation` for server sync" - ].join("\n") - }, - { - name: "tanstack-code", - description: "Code style conventions for TanStack applications", - body: [ - "# TanStack Code Conventions", - "", - "## TypeScript", - "- Enable strict mode in tsconfig", - "- Infer types from TanStack APIs rather than writing manual type annotations", - "- Use `satisfies` operator for type-safe object literals", - "", - "## Naming", - "- Query keys: `['entity', ...params]` (e.g. `['user', userId]`)", - "- Query option factories: `entityQueryOptions` (e.g. `userQueryOptions`)", - "- Route files: `$param` for dynamic segments (e.g. `users/$userId.tsx`)", - "- Loaders: export as named `loader` from route file", - "", - "## Imports", - "- Import from `@tanstack/react-query`, `@tanstack/react-router`, etc.", - "- Never import internal modules from TanStack packages", - "- Use path aliases for project imports (`@/features/...`)" - ].join("\n") - }, - { - name: "tanstack-testing", - description: "Testing conventions for TanStack applications", - body: [ - "# TanStack Testing Conventions", - "", - "## Query Testing", - "- Wrap components in `QueryClientProvider` with a fresh `QueryClient` per test", - "- Use `@testing-library/react` with `renderHook` for testing custom query hooks", - "- Mock at the network level with MSW, not at the query level", - "", - "## Router Testing", - "- Use `createMemoryHistory` and `createRouter` for route testing", - "- Test route loaders independently as plain async functions", - "- Test search param validation with unit tests on the validator schema", - "", - "## Integration Tests", - "- Test full user flows through route transitions", - "- Assert on visible UI state, not internal query cache state", - "- Use `waitFor` for async query resolution in component tests" - ].join("\n") - }, - { - name: "playwright-cli", - source: "microsoft/playwright-cli/skills/playwright-cli" - } - ] - } - }, - { - writer: "instruction", - config: { - text: "This project follows TanStack conventions. Use use_skill() to access the tanstack-architecture, tanstack-design, tanstack-code, tanstack-testing, and playwright-cli skills before making changes." - } - } - ] - }, { id: "conventional-commits", label: "Conventional Commits", diff --git a/packages/core/src/catalog/index.ts b/packages/core/src/catalog/index.ts index 0c4cfb8..6d17d81 100644 --- a/packages/core/src/catalog/index.ts +++ b/packages/core/src/catalog/index.ts @@ -1,10 +1,11 @@ import type { Catalog, Facet, Option } from "../types.js"; import { processFacet } from "./facets/process.js"; -import { conventionsFacet } from "./facets/conventions.js"; +import { architectureFacet } from "./facets/architecture.js"; +import { practicesFacet } from "./facets/practices.js"; export function getDefaultCatalog(): Catalog { return { - facets: [processFacet, conventionsFacet] + facets: [processFacet, architectureFacet, practicesFacet] }; } From f4ab1869c86934fea9d37f78c4301da0019b02dd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 16:13:08 +0000 Subject: [PATCH 37/60] docs: keep conventions as the conceptual layer with architecture/practices subtypes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three-layer model (Process → Conventions → Documentation) stays intact. Architecture and Practices are now presented as two sub-categories within the Conventions layer, not as replacements for it. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- README.md | 97 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 53 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 37cc4b4..daa81cb 100644 --- a/README.md +++ b/README.md @@ -39,15 +39,15 @@ And then there is **documentation**: the reference material you consult while implementing — API details, library behavior, system specifications. You do not memorize it. You look it up at the moment you need it. -Four distinct types of information. Different acquisition modes. Different roles -in the act of engineering. +Three distinct types of information. Three different acquisition modes. Three +different roles in the act of engineering. ## What ADE is **ADE is an information architecture for agentic development.** It provides a rigid, technology-agnostic structure that organizes the information -a coding agent needs into explicit layers — each mapped to a concrete, +a coding agent needs into three explicit layers — each mapped to a concrete, agent-native artifact type — and a mechanism to compose them. ```mermaid @@ -62,20 +62,20 @@ flowchart TD P["Universal engineering workflows

loaded at every session. e. g. via AGENTS.md"] end - subgraph Architecture ["Architecture · Skills (single-select)"] - A["Stack & framework conventions
shapes project structure, patterns, libraries

e. g. TanStack, Next.js, FastAPI"] - end - - subgraph Practices ["Practices · Skills (multi-select)"] - PR["Composable development disciplines
mix and match regardless of stack

e. g. TDD, ADR, Conventional Commits"] + subgraph Conventions ["Conventions · Skills"] + subgraph Architecture ["Architecture (single-select)"] + A["Stack & framework conventions
shapes project structure, patterns, libraries

e. g. TanStack, Next.js, FastAPI"] + end + subgraph Practices ["Practices (multi-select)"] + PR["Composable development disciplines
mix and match regardless of stack

e. g. TDD, ADR, Conventional Commits"] + end end subgraph Documentation ["Documentation · Text files"] D["Reference knowledge

read on demand
APIs · libraries · system context"] end - Process -->|"invokes — e. g. "use your design skill""| Architecture - Process -->|"invokes — e. g. "use your TDD skill""| Practices + Process -->|"invokes — e. g. "use your design skill""| Conventions Architecture -->|"points to — e. g. "check .docs/tanstack""| Documentation Practices -->|"points to — e. g. "check docs/adr/""| Documentation ``` @@ -93,7 +93,7 @@ They mix process instructions with coding conventions and documentation fragment in a single flat file. Rule files and skills improve reusability but still lack a coherent taxonomy. -ADE brings structure to this space. By separating the layers explicitly and +ADE brings structure to this space. By separating the three layers explicitly and binding each to a specific artifact type, it makes information easier to find, easier to maintain, and — critically — easier for agents to apply in the right context at the right moment. @@ -103,9 +103,8 @@ team, regardless of who is running them. ## How the layers compose -The layers are not independent — they reference each other in a deliberate -direction. Process invokes architecture and practice skills. Skills point to -documentation. +The three layers are not independent — they reference each other in a deliberate +direction. Process invokes conventions. Conventions point to documentation. **Process enforces workflows and delegates to skills:** @@ -115,30 +114,37 @@ The agent system prompt defines and enforces the workflow. At the right step, it delegates to a skill that encodes the team's specific approach — keeping process universal and conventions local. -**Architecture skills define the stack:** +**Conventions come in two flavours — architecture and practices:** + +The conventions layer is split into two complementary sub-categories that reflect +how teams actually think about project-level decisions: -> _"We are using React with TanStack Query for backend interactions. Check_ -> _`.docs/tanstack` when implementing data fetching. Check `.docs/components` for_ -> _details on available reusable components."_ +- **Architecture** (single-select) — Stack and framework conventions that shape + your project structure, patterns, and libraries. You pick one architecture + (e.g. TanStack, Next.js, FastAPI) and it constrains everything downstream. -Architecture is a constraining choice — you pick one stack, and it shapes your -project structure, patterns, and libraries. This is a single-select decision: -TanStack or Next.js, not both. + > _"We are using React with TanStack Query for backend interactions. Check_ + > _`.docs/tanstack` when implementing data fetching."_ -**Practice skills are composable disciplines:** +- **Practices** (multi-select) — Composable development disciplines that apply + regardless of your stack. You can combine TDD, ADR, and Conventional Commits + freely — they are orthogonal to each other and to your architecture choice. -> _"Use London-style TDD. Follow the Red-Green-Refactor cycle. Write ADRs for_ -> _significant decisions. Use Conventional Commits for all commit messages."_ + > _"Use London-style TDD. Follow the Red-Green-Refactor cycle. Write ADRs for_ + > _significant decisions."_ -Practices are orthogonal to architecture. You can combine TDD, ADR, and -Conventional Commits freely — they apply regardless of what stack you build with. -This is a multi-select decision. +**Skills reference documentation on demand:** + +The skill encodes the convention — which libraries, which patterns. It surfaces +the exact documentation needed at the moment it is relevant, rather than loading +everything upfront. This composability is what makes ADE scale. Process is written once and shared -across every project. Architecture and practice skills are curated per team or -per project context — ADE provides a mechanism to select and compose them, so -the right conventions are available for the right context. Documentation lives -where it belongs — in the codebase — and is surfaced precisely when needed. +across every project. Convention skills — both architecture and practices — are +curated per team or per project context. ADE provides a mechanism to select and +compose them, so the right conventions are available for the right context. +Documentation lives where it belongs — in the codebase — and is surfaced +precisely when needed. **Sounds intuitive?** Hopefully, it does. Because **this framework only works, if you as human join the team**. @@ -154,22 +160,25 @@ _does_ work. Every task type has a defined process. The agent follows it. This is the layer that transfers across every project without modification. It encodes the professional engineering mindset as an explicit, shareable artifact. -### Architecture — stack conventions (single-select) +### Skill sets — selectable and composable + +Skills encode project-specific knowledge as reusable, invocable artifacts. They +capture technology choices, architectural patterns, and design decisions in a form +the agent applies on demand. -Architecture skills encode stack-specific knowledge: project structure, framework -patterns, and library conventions. You pick one architecture that shapes your -entire codebase — TanStack, Next.js, FastAPI, etc. This is a constraining choice -that determines how your project is organized. +ADE provides a mechanism to select and share **skill sets** — curated collections +of skills that match a team's context. Within the conventions layer, skills are +organized into two sub-categories: -### Practices — development disciplines (multi-select) +- **Architecture** (single-select) — Stack and framework conventions that shape + your project structure. You pick one (e.g. TanStack, Next.js, FastAPI) and it + constrains patterns, libraries, and project organization. -Practice skills encode composable development disciplines that apply regardless -of your stack: TDD, Architecture Decision Records, Conventional Commits, and -more. You can mix and match freely — they are orthogonal to each other and to -your architecture choice. +- **Practices** (multi-select) — Composable development disciplines that apply + regardless of your stack. TDD, ADR, Conventional Commits — mix and match + freely. -ADE provides a mechanism to select and share both architecture and practice -skills. A frontend team, a backend team, and a platform team each activate the +A frontend team, a backend team, and a platform team each activate the combination appropriate to their work. Skills can be shared across projects, versioned, and evolved independently of the process layer. From c07b793b569b6ff1015159952525d469742e8cc5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 16:48:52 +0000 Subject: [PATCH 38/60] feat: add docsets as weak entity on Option for documentation layer Docsets are derived from upstream selections (architecture, practices) rather than being a standalone facet. Each Option can declare recommended docsets which the resolver collects, deduplicates, and maps to knowledge_sources. The TUI presents them as a confirmation step (opt-out). - Add DocsetDef type on Option, excluded_docsets on UserConfig - Resolver collects docsets into knowledge_sources with dedup and exclusion - Add collectDocsets() helper for TUI pre-resolution - Enrich tanstack option with Router/Query/Form/Table docsets - Enrich conventional-commits with spec docset - Setup TUI presents docset multiselect after facet selection - Update design doc with docsets model and design decision #7 https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- docs/CLI-design.md | 33 +++ packages/cli/src/commands/setup.spec.ts | 97 +++++++- packages/cli/src/commands/setup.ts | 36 ++- packages/core/src/catalog/catalog.spec.ts | 38 +++ .../core/src/catalog/facets/architecture.ts | 26 ++ packages/core/src/catalog/facets/practices.ts | 8 + packages/core/src/index.ts | 5 +- packages/core/src/resolver.spec.ts | 227 +++++++++++++++++- packages/core/src/resolver.ts | 56 ++++- packages/core/src/types.ts | 9 + 10 files changed, 528 insertions(+), 7 deletions(-) diff --git a/docs/CLI-design.md b/docs/CLI-design.md index e83d765..1defcb3 100644 --- a/docs/CLI-design.md +++ b/docs/CLI-design.md @@ -103,6 +103,10 @@ read config.yaml → for each provision, invoke the writer with (config, context) → each writer returns a LogicalConfig fragment → record facet as resolved + → collect docsets from all selected options + → deduplicate docsets by id (first wins) + → filter out docsets listed in excluded_docsets + → map enabled docsets to knowledge_sources entries → merge custom section from config.yaml → merge all fragments into one LogicalConfig → write config.lock.yaml (serialized LogicalConfig) @@ -178,6 +182,23 @@ interface Option { label: string; // e.g. "CodeMCP Workflows" description: string; recipe: Provision[]; // multiple provisions per option is common + docsets?: DocsetDef[]; // recommended documentation for this option +} + +// Documentation as a weak entity on Option. Docsets are derived from +// upstream selections — picking "TanStack" in architecture implies +// TanStack docs, picking "GitHub Actions CI/CD" in practices implies +// GH Actions docs. The TUI presents all implied docsets as pre-selected +// defaults and allows the user to deselect. This is opt-out, not opt-in. +// +// The resolver collects docsets from all selected options, deduplicates +// by id, filters by excluded_docsets from UserConfig, and maps enabled +// docsets directly to knowledge_sources in LogicalConfig. +interface DocsetDef { + id: string; // unique key for dedup, e.g. "tanstack-query-docs" + label: string; // display name, e.g. "TanStack Query Reference" + origin: string; // URL, path, or package ref + description: string; // shown in TUI } // A recipe typically contains multiple provisions for different writers. @@ -239,6 +260,7 @@ interface KnowledgeSource { // config.yaml — mostly CLI-managed, agent-agnostic interface UserConfig { choices: Record; // single-select: string, multi-select: string[] + excluded_docsets?: string[]; // docset IDs the user opted out of custom?: { // user-managed section mcp_servers?: McpServerEntry[]; @@ -605,3 +627,14 @@ export const frameworksFacet: Facet = { `config.yaml` is user-managed. The rest is CLI-managed. This eliminates merge conflicts: the CLI never touches `custom`, and users never touch the rest. Agent writers merge both sections when generating output. + +7. **Docsets are a weak entity on Option, not a separate facet.** Documentation + sources are always implied by an upstream selection (architecture or + practices). Making documentation a standalone facet would create a hollow + indirection whose options just mirror upstream choices 1:1. Instead, each + `Option` declares its recommended `docsets[]`. The resolver collects and + deduplicates them; the TUI presents them as a confirmation step (opt-out, + not opt-in). Users who want arbitrary docs not tied to a catalog option + use `custom.knowledge_sources` instead. Config stores `excluded_docsets` + (what the user opted out of) rather than selected docsets, keeping the + common case (accept all recommendations) zero-config. diff --git a/packages/cli/src/commands/setup.spec.ts b/packages/cli/src/commands/setup.spec.ts index e3dec6f..ebdc53e 100644 --- a/packages/cli/src/commands/setup.spec.ts +++ b/packages/cli/src/commands/setup.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import type { Catalog, LogicalConfig } from "@ade/core"; +import type { Catalog, LogicalConfig, DocsetDef } from "@ade/core"; // ── Mocks ──────────────────────────────────────────────────────────────────── @@ -34,7 +34,8 @@ vi.mock("@ade/core", async (importOriginal) => { getAgentWriter: vi.fn().mockReturnValue({ id: "claude-code", install: vi.fn().mockResolvedValue(undefined) - }) + }), + collectDocsets: actual.collectDocsets }; }); @@ -89,6 +90,39 @@ const testCatalog: Catalog = { ] }; +const docsetCatalog: Catalog = { + facets: [ + { + id: "arch", + label: "Architecture", + description: "Stack", + required: true, + options: [ + { + id: "react", + label: "React", + description: "React framework", + recipe: [], + docsets: [ + { + id: "react-docs", + label: "React Reference", + origin: "https://react.dev/reference", + description: "Official React docs" + }, + { + id: "react-tutorial", + label: "React Tutorial", + origin: "https://react.dev/learn", + description: "React learn guide" + } + ] + } + ] + } + ] +}; + // ── Tests ──────────────────────────────────────────────────────────────────── describe("runSetup", () => { @@ -177,6 +211,65 @@ describe("runSetup", () => { expect(clack.cancel).toHaveBeenCalled(); }); + describe("docset confirmation step", () => { + it("presents implied docsets as a multiselect after facet selection", async () => { + vi.mocked(clack.select).mockResolvedValueOnce("react"); + // User accepts all docsets (returns all ids) + vi.mocked(clack.multiselect).mockResolvedValueOnce([ + "react-docs", + "react-tutorial" + ]); + + await runSetup("/tmp/test-project", docsetCatalog); + + // multiselect should have been called for docsets + expect(clack.multiselect).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining("Documentation") + }) + ); + }); + + it("stores deselected docsets as excluded_docsets in user config", async () => { + vi.mocked(clack.select).mockResolvedValueOnce("react"); + // User deselects react-tutorial, keeps only react-docs + vi.mocked(clack.multiselect).mockResolvedValueOnce(["react-docs"]); + + await runSetup("/tmp/test-project", docsetCatalog); + + expect(writeUserConfig).toHaveBeenCalledWith( + "/tmp/test-project", + expect.objectContaining({ + excluded_docsets: ["react-tutorial"] + }) + ); + }); + + it("does not set excluded_docsets when all docsets are accepted", async () => { + vi.mocked(clack.select).mockResolvedValueOnce("react"); + vi.mocked(clack.multiselect).mockResolvedValueOnce([ + "react-docs", + "react-tutorial" + ]); + + await runSetup("/tmp/test-project", docsetCatalog); + + const configArg = vi.mocked(writeUserConfig).mock.calls[0][1]; + expect(configArg.excluded_docsets).toBeUndefined(); + }); + + it("skips docset prompt when no options have docsets", async () => { + vi.mocked(clack.select) + .mockResolvedValueOnce("workflow-a") + .mockResolvedValueOnce("vitest"); + + await runSetup("/tmp/test-project", testCatalog); + + // multiselect should NOT have been called (no docsets in testCatalog) + expect(clack.multiselect).not.toHaveBeenCalled(); + }); + }); + it("calls intro and outro from @clack/prompts", async () => { vi.mocked(clack.select) .mockResolvedValueOnce("workflow-a") diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 9a52752..9e12df4 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -6,6 +6,7 @@ import { writeUserConfig, writeLockFile, resolve, + collectDocsets, createDefaultRegistry, getAgentWriter } from "@ade/core"; @@ -41,7 +42,40 @@ export async function runSetup( } } - const userConfig: UserConfig = { choices }; + // Docset confirmation step: collect implied docsets, let user deselect + const impliedDocsets = collectDocsets(choices, catalog); + let excludedDocsets: string[] | undefined; + + if (impliedDocsets.length > 0) { + const selected = await clack.multiselect({ + message: "Documentation — deselect any you don't need", + options: impliedDocsets.map((d) => ({ + value: d.id, + label: d.label, + hint: d.description + })), + initialValues: impliedDocsets.map((d) => d.id), + required: false + }); + + if (typeof selected === "symbol") { + clack.cancel("Setup cancelled."); + return; + } + + const selectedSet = new Set(selected as string[]); + const excluded = impliedDocsets + .filter((d) => !selectedSet.has(d.id)) + .map((d) => d.id); + if (excluded.length > 0) { + excludedDocsets = excluded; + } + } + + const userConfig: UserConfig = { + choices, + ...(excludedDocsets && { excluded_docsets: excludedDocsets }) + }; const registry = createDefaultRegistry(); const logicalConfig = await resolve(userConfig, catalog, registry); diff --git a/packages/core/src/catalog/catalog.spec.ts b/packages/core/src/catalog/catalog.spec.ts index 1729e1b..9fbaaf1 100644 --- a/packages/core/src/catalog/catalog.spec.ts +++ b/packages/core/src/catalog/catalog.spec.ts @@ -90,6 +90,34 @@ describe("catalog", () => { }); }); + describe("architecture facet docsets", () => { + it("tanstack option declares docsets for Router, Query, Form, and Table", () => { + const catalog = getDefaultCatalog(); + const architecture = getFacet(catalog, "architecture")!; + const tanstack = getOption(architecture, "tanstack")!; + + expect(tanstack.docsets).toBeDefined(); + const ids = tanstack.docsets!.map((d) => d.id); + expect(ids).toContain("tanstack-router-docs"); + expect(ids).toContain("tanstack-query-docs"); + expect(ids).toContain("tanstack-form-docs"); + expect(ids).toContain("tanstack-table-docs"); + }); + + it("each docset has required fields", () => { + const catalog = getDefaultCatalog(); + const architecture = getFacet(catalog, "architecture")!; + const tanstack = getOption(architecture, "tanstack")!; + + for (const docset of tanstack.docsets!) { + expect(docset.id).toBeTruthy(); + expect(docset.label).toBeTruthy(); + expect(docset.origin).toMatch(/^https:\/\//); + expect(docset.description).toBeTruthy(); + } + }); + }); + describe("practices facet", () => { it("exists in the default catalog", () => { const catalog = getDefaultCatalog(); @@ -119,6 +147,16 @@ describe("catalog", () => { expect(skills[0].name).toBe("conventional-commits"); }); + it("conventional-commits option declares the spec docset", () => { + const catalog = getDefaultCatalog(); + const practices = getFacet(catalog, "practices")!; + const option = getOption(practices, "conventional-commits")!; + + expect(option.docsets).toBeDefined(); + expect(option.docsets).toHaveLength(1); + expect(option.docsets![0].id).toBe("conventional-commits-spec"); + }); + it("has tdd-london option with a single skill", () => { const catalog = getDefaultCatalog(); const practices = getFacet(catalog, "practices")!; diff --git a/packages/core/src/catalog/facets/architecture.ts b/packages/core/src/catalog/facets/architecture.ts index 6e88033..b73504b 100644 --- a/packages/core/src/catalog/facets/architecture.ts +++ b/packages/core/src/catalog/facets/architecture.ts @@ -122,6 +122,32 @@ export const architectureFacet: Facet = { text: "This project follows TanStack conventions. Use use_skill() to access the tanstack-architecture, tanstack-design, tanstack-code, tanstack-testing, and playwright-cli skills before making changes." } } + ], + docsets: [ + { + id: "tanstack-router-docs", + label: "TanStack Router", + origin: "https://tanstack.com/router/latest/docs", + description: "File-based routing, loaders, and search params" + }, + { + id: "tanstack-query-docs", + label: "TanStack Query", + origin: "https://tanstack.com/query/latest/docs", + description: "Server state management, caching, and mutations" + }, + { + id: "tanstack-form-docs", + label: "TanStack Form", + origin: "https://tanstack.com/form/latest/docs", + description: "Type-safe form state and validation" + }, + { + id: "tanstack-table-docs", + label: "TanStack Table", + origin: "https://tanstack.com/table/latest/docs", + description: "Headless table and datagrid utilities" + } ] } ] diff --git a/packages/core/src/catalog/facets/practices.ts b/packages/core/src/catalog/facets/practices.ts index 09f230e..fac7b1d 100644 --- a/packages/core/src/catalog/facets/practices.ts +++ b/packages/core/src/catalog/facets/practices.ts @@ -62,6 +62,14 @@ export const practicesFacet: Facet = { text: "Use the conventional-commits skill (via use_skill()) when writing commit messages." } } + ], + docsets: [ + { + id: "conventional-commits-spec", + label: "Conventional Commits Spec", + origin: "https://www.conventionalcommits.org/en/v1.0.0/", + description: "The Conventional Commits specification" + } ] }, { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b89aac1..2d496d4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,7 +2,8 @@ export { type Catalog, type Facet, type Option, - type Provision + type Provision, + type DocsetDef } from "./types.js"; export { type LogicalConfig, @@ -35,7 +36,7 @@ export { getAgentWriter, createDefaultRegistry } from "./registry.js"; -export { resolve } from "./resolver.js"; +export { resolve, collectDocsets } from "./resolver.js"; export { getDefaultCatalog, getFacet, getOption } from "./catalog/index.js"; export { claudeCodeWriter } from "./agents/claude-code.js"; export { skillsWriter } from "./writers/skills.js"; diff --git a/packages/core/src/resolver.spec.ts b/packages/core/src/resolver.spec.ts index 71038a0..4ad3537 100644 --- a/packages/core/src/resolver.spec.ts +++ b/packages/core/src/resolver.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { resolve } from "./resolver.js"; +import { resolve, collectDocsets } from "./resolver.js"; import { getDefaultCatalog } from "./catalog/index.js"; import { createRegistry, registerProvisionWriter } from "./registry.js"; import { instructionWriter } from "./writers/instruction.js"; @@ -193,6 +193,231 @@ describe("resolve", () => { }); }); + describe("docset collection", () => { + it("collects docsets from selected options into knowledge_sources", async () => { + const docsetCatalog: Catalog = { + facets: [ + { + id: "arch", + label: "Architecture", + description: "Stack", + required: false, + options: [ + { + id: "react", + label: "React", + description: "React framework", + recipe: [], + docsets: [ + { + id: "react-docs", + label: "React Reference", + origin: "https://react.dev/reference", + description: "Official React documentation" + } + ] + } + ] + } + ] + }; + + const userConfig: UserConfig = { choices: { arch: "react" } }; + const result = await resolve(userConfig, docsetCatalog, registry); + + expect(result.knowledge_sources).toHaveLength(1); + expect(result.knowledge_sources[0]).toEqual({ + name: "react-docs", + origin: "https://react.dev/reference", + description: "Official React documentation" + }); + }); + + it("deduplicates docsets by id across multiple options", async () => { + const docsetCatalog: Catalog = { + facets: [ + { + id: "stack", + label: "Stack", + description: "Tech stack", + required: false, + multiSelect: true, + options: [ + { + id: "react", + label: "React", + description: "React", + recipe: [], + docsets: [ + { + id: "react-docs", + label: "React Reference", + origin: "https://react.dev/reference", + description: "React docs" + } + ] + }, + { + id: "nextjs", + label: "Next.js", + description: "Next.js", + recipe: [], + docsets: [ + { + id: "react-docs", + label: "React Reference", + origin: "https://react.dev/reference", + description: "React docs" + }, + { + id: "nextjs-docs", + label: "Next.js Docs", + origin: "https://nextjs.org/docs", + description: "Next.js docs" + } + ] + } + ] + } + ] + }; + + const userConfig: UserConfig = { + choices: { stack: ["react", "nextjs"] } + }; + const result = await resolve(userConfig, docsetCatalog, registry); + + expect(result.knowledge_sources).toHaveLength(2); + const ids = result.knowledge_sources.map((ks) => ks.name); + expect(ids).toContain("react-docs"); + expect(ids).toContain("nextjs-docs"); + }); + + it("filters out excluded_docsets", async () => { + const docsetCatalog: Catalog = { + facets: [ + { + id: "arch", + label: "Architecture", + description: "Stack", + required: false, + options: [ + { + id: "react", + label: "React", + description: "React", + recipe: [], + docsets: [ + { + id: "react-docs", + label: "React Reference", + origin: "https://react.dev/reference", + description: "React docs" + }, + { + id: "react-tutorial", + label: "React Tutorial", + origin: "https://react.dev/learn", + description: "React tutorial" + } + ] + } + ] + } + ] + }; + + const userConfig: UserConfig = { + choices: { arch: "react" }, + excluded_docsets: ["react-tutorial"] + }; + const result = await resolve(userConfig, docsetCatalog, registry); + + expect(result.knowledge_sources).toHaveLength(1); + expect(result.knowledge_sources[0].name).toBe("react-docs"); + }); + + it("produces no knowledge_sources when option has no docsets", async () => { + const userConfig: UserConfig = { + choices: { process: "native-agents-md" } + }; + const result = await resolve(userConfig, catalog, registry); + + expect(result.knowledge_sources).toEqual([]); + }); + }); + + describe("collectDocsets", () => { + it("returns deduplicated docsets for given choices", () => { + const docsetCatalog: Catalog = { + facets: [ + { + id: "stack", + label: "Stack", + description: "Stack", + required: false, + multiSelect: true, + options: [ + { + id: "a", + label: "A", + description: "A", + recipe: [], + docsets: [ + { + id: "shared", + label: "Shared", + origin: "https://x", + description: "shared" + }, + { + id: "a-only", + label: "A Only", + origin: "https://a", + description: "a" + } + ] + }, + { + id: "b", + label: "B", + description: "B", + recipe: [], + docsets: [ + { + id: "shared", + label: "Shared", + origin: "https://x", + description: "shared" + }, + { + id: "b-only", + label: "B Only", + origin: "https://b", + description: "b" + } + ] + } + ] + } + ] + }; + + const result = collectDocsets({ stack: ["a", "b"] }, docsetCatalog); + + expect(result).toHaveLength(3); + const ids = result.map((d) => d.id); + expect(ids).toContain("shared"); + expect(ids).toContain("a-only"); + expect(ids).toContain("b-only"); + }); + + it("returns empty array when no options have docsets", () => { + const result = collectDocsets({ process: "native-agents-md" }, catalog); + expect(result).toEqual([]); + }); + }); + describe("MCP server dedup by ref", () => { it("deduplicates mcp_servers by ref, keeping the last one", async () => { // Create a custom registry with a writer that produces duplicate refs diff --git a/packages/core/src/resolver.ts b/packages/core/src/resolver.ts index 502c8b5..5814a67 100644 --- a/packages/core/src/resolver.ts +++ b/packages/core/src/resolver.ts @@ -4,7 +4,8 @@ import type { WriterRegistry, LogicalConfig, McpServerEntry, - ResolutionContext + ResolutionContext, + DocsetDef } from "./types.js"; import { getFacet, getOption } from "./catalog/index.js"; import { getProvisionWriter } from "./registry.js"; @@ -67,6 +68,33 @@ export async function resolve( } } + // Collect docsets from all selected options, dedup by id, filter exclusions + const seenDocsets = new Map(); + for (const [facetId, optionId] of Object.entries(userConfig.choices)) { + const facet = getFacet(catalog, facetId); + if (!facet) continue; + const selectedIds = Array.isArray(optionId) ? optionId : [optionId]; + for (const selectedId of selectedIds) { + const option = getOption(facet, selectedId); + if (!option?.docsets) continue; + for (const docset of option.docsets) { + if (!seenDocsets.has(docset.id)) { + seenDocsets.set(docset.id, docset); + } + } + } + } + + const excludedSet = new Set(userConfig.excluded_docsets ?? []); + for (const [id, docset] of seenDocsets) { + if (excludedSet.has(id)) continue; + result.knowledge_sources.push({ + name: docset.id, + origin: docset.origin, + description: docset.description + }); + } + // Merge custom section if (userConfig.custom) { if (userConfig.custom.instructions) { @@ -86,3 +114,29 @@ export async function resolve( return result; } + +/** + * Collect all unique docsets implied by the given choices. + * Used by the TUI to present docsets for confirmation before resolution. + */ +export function collectDocsets( + choices: Record, + catalog: Catalog +): DocsetDef[] { + const seen = new Map(); + for (const [facetId, optionId] of Object.entries(choices)) { + const facet = getFacet(catalog, facetId); + if (!facet) continue; + const selectedIds = Array.isArray(optionId) ? optionId : [optionId]; + for (const selectedId of selectedIds) { + const option = getOption(facet, selectedId); + if (!option?.docsets) continue; + for (const docset of option.docsets) { + if (!seen.has(docset.id)) { + seen.set(docset.id, docset); + } + } + } + } + return Array.from(seen.values()); +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index a33bc67..70a592e 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -19,6 +19,14 @@ export interface Option { label: string; description: string; recipe: Provision[]; + docsets?: DocsetDef[]; +} + +export interface DocsetDef { + id: string; + label: string; + origin: string; + description: string; } export interface Provision { @@ -91,6 +99,7 @@ export interface ResolvedFacet { export interface UserConfig { choices: Record; + excluded_docsets?: string[]; custom?: { mcp_servers?: McpServerEntry[]; instructions?: string[]; From ddd4e07775bc7f7d077001a0c191401a84659c27 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 17:07:33 +0000 Subject: [PATCH 39/60] feat: implement knowledge writer and installer for documentation layer - Implement knowledgeWriter provision writer (produces knowledge_sources) - Replace knowledge stub in registry with real writer - Resolver auto-adds @codemcp/knowledge-server MCP entry when knowledge_sources are present - Add knowledge-installer in CLI: calls createDocset + initDocset from @codemcp/knowledge programmatic API - Wire installKnowledge into setup and install commands - Add type declarations for @codemcp/knowledge API - Add @codemcp/knowledge@2.1.0 dependency to @ade/cli https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- .gitignore | 1 + packages/cli/package.json | 1 + packages/cli/src/commands/install.ts | 2 + packages/cli/src/commands/setup.ts | 2 + packages/cli/src/knowledge-api.d.ts | 52 ++ packages/cli/src/knowledge-installer.spec.ts | 111 +++ packages/cli/src/knowledge-installer.ts | 54 ++ packages/core/src/index.ts | 1 + packages/core/src/registry.ts | 5 +- packages/core/src/resolver.spec.ts | 51 ++ packages/core/src/resolver.ts | 10 + packages/core/src/writers/knowledge.spec.ts | 26 + packages/core/src/writers/knowledge.ts | 15 + pnpm-lock.yaml | 901 ++++++++++++++++++- 14 files changed, 1230 insertions(+), 2 deletions(-) create mode 100644 packages/cli/src/knowledge-api.d.ts create mode 100644 packages/cli/src/knowledge-installer.spec.ts create mode 100644 packages/cli/src/knowledge-installer.ts create mode 100644 packages/core/src/writers/knowledge.spec.ts create mode 100644 packages/core/src/writers/knowledge.ts diff --git a/.gitignore b/.gitignore index de62c3b..8a87119 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ dist # Typescript *.tsbuildinfo *.d.ts +!*-api.d.ts # Turborepo .turbo diff --git a/packages/cli/package.json b/packages/cli/package.json index 010efb6..3576ee2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -27,6 +27,7 @@ "@codemcp/skills": "^2.3.0" }, "devDependencies": { + "@codemcp/knowledge": "2.1.0", "@typescript-eslint/eslint-plugin": "^8.21.0", "@typescript-eslint/parser": "^8.21.0", "eslint": "^9.18.0", diff --git a/packages/cli/src/commands/install.ts b/packages/cli/src/commands/install.ts index 73d8df6..c9aacc4 100644 --- a/packages/cli/src/commands/install.ts +++ b/packages/cli/src/commands/install.ts @@ -9,6 +9,7 @@ import { type LockFile } from "@ade/core"; import { installSkills } from "../skills-installer.js"; +import { installKnowledge } from "../knowledge-installer.js"; export async function runInstall( projectRoot: string, @@ -44,6 +45,7 @@ export async function runInstall( await agentWriter.install(logicalConfig, projectRoot); await installSkills(logicalConfig.skills, projectRoot); + await installKnowledge(logicalConfig.knowledge_sources, projectRoot); clack.outro("Install complete!"); } diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 9e12df4..8c9e0c6 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -11,6 +11,7 @@ import { getAgentWriter } from "@ade/core"; import { installSkills } from "../skills-installer.js"; +import { installKnowledge } from "../knowledge-installer.js"; export async function runSetup( projectRoot: string, @@ -95,6 +96,7 @@ export async function runSetup( } await installSkills(logicalConfig.skills, projectRoot); + await installKnowledge(logicalConfig.knowledge_sources, projectRoot); clack.outro("Setup complete!"); } diff --git a/packages/cli/src/knowledge-api.d.ts b/packages/cli/src/knowledge-api.d.ts new file mode 100644 index 0000000..821e02d --- /dev/null +++ b/packages/cli/src/knowledge-api.d.ts @@ -0,0 +1,52 @@ +declare module "@codemcp/knowledge/packages/cli/dist/exports.js" { + interface CreateDocsetParams { + id: string; + name: string; + description?: string; + preset: "git-repo" | "local-folder" | "archive"; + url?: string; + branch?: string; + paths?: string[]; + path?: string; + } + + interface CreateDocsetOptions { + cwd?: string; + } + + interface CreateDocsetResult { + docset: unknown; + configPath: string; + configCreated: boolean; + } + + interface InitDocsetParams { + docsetId: string; + force?: boolean; + discoverPaths?: boolean; + cwd?: string; + onSourceProgress?: (progress: unknown) => void; + } + + interface InitDocsetResult { + alreadyInitialized: boolean; + discoveredPaths?: string[]; + } + + export function createDocset( + params: CreateDocsetParams, + options?: CreateDocsetOptions + ): Promise; + + export function initDocset( + params: InitDocsetParams + ): Promise; + + export function refreshDocsets(params?: { + docsetId?: string; + force?: boolean; + cwd?: string; + }): Promise; + + export function getStatus(params?: { cwd?: string }): Promise; +} diff --git a/packages/cli/src/knowledge-installer.spec.ts b/packages/cli/src/knowledge-installer.spec.ts new file mode 100644 index 0000000..c00bb32 --- /dev/null +++ b/packages/cli/src/knowledge-installer.spec.ts @@ -0,0 +1,111 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { KnowledgeSource } from "@ade/core"; + +vi.mock("@codemcp/knowledge/packages/cli/dist/exports.js", () => ({ + createDocset: vi.fn().mockResolvedValue({ + docset: {}, + configPath: ".knowledge/config.yaml", + configCreated: false + }), + initDocset: vi.fn().mockResolvedValue({ alreadyInitialized: false }) +})); + +import { + createDocset, + initDocset +} from "@codemcp/knowledge/packages/cli/dist/exports.js"; +import { installKnowledge } from "./knowledge-installer.js"; + +describe("installKnowledge", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("does nothing when knowledge_sources is empty", async () => { + await installKnowledge([], "/tmp/project"); + + expect(createDocset).not.toHaveBeenCalled(); + expect(initDocset).not.toHaveBeenCalled(); + }); + + it("calls createDocset for each knowledge source", async () => { + const sources: KnowledgeSource[] = [ + { + name: "react-docs", + origin: "https://github.com/facebook/react.git", + description: "React documentation" + }, + { + name: "tanstack-query-docs", + origin: "https://github.com/TanStack/query.git", + description: "TanStack Query docs" + } + ]; + + await installKnowledge(sources, "/tmp/project"); + + expect(createDocset).toHaveBeenCalledTimes(2); + expect(createDocset).toHaveBeenCalledWith( + expect.objectContaining({ + id: "react-docs", + name: "React documentation", + preset: "git-repo", + url: "https://github.com/facebook/react.git" + }), + expect.objectContaining({ cwd: "/tmp/project" }) + ); + }); + + it("calls initDocset for each knowledge source after creation", async () => { + const sources: KnowledgeSource[] = [ + { + name: "react-docs", + origin: "https://github.com/facebook/react.git", + description: "React documentation" + } + ]; + + await installKnowledge(sources, "/tmp/project"); + + expect(initDocset).toHaveBeenCalledTimes(1); + expect(initDocset).toHaveBeenCalledWith( + expect.objectContaining({ + docsetId: "react-docs", + cwd: "/tmp/project" + }) + ); + }); + + it("continues with remaining sources when one fails", async () => { + vi.mocked(createDocset) + .mockRejectedValueOnce(new Error("already exists")) + .mockResolvedValueOnce({ + docset: {}, + configPath: ".knowledge/config.yaml", + configCreated: false + }); + + const sources: KnowledgeSource[] = [ + { + name: "failing", + origin: "https://github.com/fail/fail.git", + description: "Will fail" + }, + { + name: "succeeding", + origin: "https://github.com/ok/ok.git", + description: "Will succeed" + } + ]; + + await installKnowledge(sources, "/tmp/project"); + + // Should have attempted both + expect(createDocset).toHaveBeenCalledTimes(2); + // initDocset only called for the successful one + expect(initDocset).toHaveBeenCalledTimes(1); + expect(initDocset).toHaveBeenCalledWith( + expect.objectContaining({ docsetId: "succeeding" }) + ); + }); +}); diff --git a/packages/cli/src/knowledge-installer.ts b/packages/cli/src/knowledge-installer.ts new file mode 100644 index 0000000..bfabd80 --- /dev/null +++ b/packages/cli/src/knowledge-installer.ts @@ -0,0 +1,54 @@ +import type { KnowledgeSource } from "@ade/core"; +import { + createDocset, + initDocset +} from "@codemcp/knowledge/packages/cli/dist/exports.js"; + +/** + * Install knowledge sources using the @codemcp/knowledge programmatic API. + * + * For each knowledge source: + * 1. Creates a docset config entry via `createDocset` + * 2. Initializes (downloads) the docset via `initDocset` + * + * Errors on individual sources are logged and skipped so that one failure + * doesn't block the rest. + */ +export async function installKnowledge( + sources: KnowledgeSource[], + projectRoot: string +): Promise { + if (sources.length === 0) return; + + for (const source of sources) { + try { + await createDocset( + { + id: source.name, + name: source.description, + preset: "git-repo" as const, + url: source.origin + }, + { cwd: projectRoot } + ); + } catch (err) { + console.warn( + `Warning: failed to create docset "${source.name}":`, + err instanceof Error ? err.message : err + ); + continue; + } + + try { + await initDocset({ + docsetId: source.name, + cwd: projectRoot + }); + } catch (err) { + console.warn( + `Warning: failed to initialize docset "${source.name}":`, + err instanceof Error ? err.message : err + ); + } + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2d496d4..ce48910 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -40,3 +40,4 @@ export { resolve, collectDocsets } from "./resolver.js"; export { getDefaultCatalog, getFacet, getOption } from "./catalog/index.js"; export { claudeCodeWriter } from "./agents/claude-code.js"; export { skillsWriter } from "./writers/skills.js"; +export { knowledgeWriter } from "./writers/knowledge.js"; diff --git a/packages/core/src/registry.ts b/packages/core/src/registry.ts index bb5ece3..d062009 100644 --- a/packages/core/src/registry.ts +++ b/packages/core/src/registry.ts @@ -6,6 +6,7 @@ import type { import { instructionWriter } from "./writers/instruction.js"; import { workflowsWriter } from "./writers/workflows.js"; import { skillsWriter } from "./writers/skills.js"; +import { knowledgeWriter } from "./writers/knowledge.js"; import { claudeCodeWriter } from "./agents/claude-code.js"; export function createRegistry(): WriterRegistry { @@ -50,8 +51,10 @@ export function createDefaultRegistry(): WriterRegistry { registerProvisionWriter(registry, workflowsWriter); registerProvisionWriter(registry, skillsWriter); + registerProvisionWriter(registry, knowledgeWriter); + // Stub writers for types not yet implemented - for (const id of ["knowledge", "mcp-server", "installable"]) { + for (const id of ["mcp-server", "installable"]) { registerProvisionWriter(registry, { id, write: async () => ({}) diff --git a/packages/core/src/resolver.spec.ts b/packages/core/src/resolver.spec.ts index 4ad3537..2e8d640 100644 --- a/packages/core/src/resolver.spec.ts +++ b/packages/core/src/resolver.spec.ts @@ -337,6 +337,57 @@ describe("resolve", () => { expect(result.knowledge_sources[0].name).toBe("react-docs"); }); + it("adds knowledge-server MCP entry when knowledge_sources are present", async () => { + const docsetCatalog: Catalog = { + facets: [ + { + id: "arch", + label: "Architecture", + description: "Stack", + required: false, + options: [ + { + id: "react", + label: "React", + description: "React", + recipe: [], + docsets: [ + { + id: "react-docs", + label: "React Reference", + origin: "https://react.dev/reference", + description: "React docs" + } + ] + } + ] + } + ] + }; + + const userConfig: UserConfig = { choices: { arch: "react" } }; + const result = await resolve(userConfig, docsetCatalog, registry); + + const knowledgeServer = result.mcp_servers.find( + (s) => s.ref === "@codemcp/knowledge-server" + ); + expect(knowledgeServer).toBeDefined(); + expect(knowledgeServer!.command).toBe("npx"); + expect(knowledgeServer!.args).toContain("@codemcp/knowledge-server"); + }); + + it("does not add knowledge-server MCP entry when no knowledge_sources", async () => { + const userConfig: UserConfig = { + choices: { process: "native-agents-md" } + }; + const result = await resolve(userConfig, catalog, registry); + + const knowledgeServer = result.mcp_servers.find( + (s) => s.ref === "@codemcp/knowledge-server" + ); + expect(knowledgeServer).toBeUndefined(); + }); + it("produces no knowledge_sources when option has no docsets", async () => { const userConfig: UserConfig = { choices: { process: "native-agents-md" } diff --git a/packages/core/src/resolver.ts b/packages/core/src/resolver.ts index 5814a67..7ca30fa 100644 --- a/packages/core/src/resolver.ts +++ b/packages/core/src/resolver.ts @@ -95,6 +95,16 @@ export async function resolve( }); } + // Add knowledge-server MCP entry if any knowledge_sources were collected + if (result.knowledge_sources.length > 0) { + result.mcp_servers.push({ + ref: "@codemcp/knowledge-server", + command: "npx", + args: ["-y", "@codemcp/knowledge-server"], + env: {} + }); + } + // Merge custom section if (userConfig.custom) { if (userConfig.custom.instructions) { diff --git a/packages/core/src/writers/knowledge.spec.ts b/packages/core/src/writers/knowledge.spec.ts new file mode 100644 index 0000000..63cd009 --- /dev/null +++ b/packages/core/src/writers/knowledge.spec.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from "vitest"; +import { knowledgeWriter } from "./knowledge.js"; + +describe("knowledgeWriter", () => { + it("has id 'knowledge'", () => { + expect(knowledgeWriter.id).toBe("knowledge"); + }); + + it("produces a knowledge_sources entry from config", async () => { + const result = await knowledgeWriter.write( + { + name: "react-docs", + origin: "https://react.dev/reference", + description: "Official React documentation" + }, + { resolved: {} } + ); + + expect(result.knowledge_sources).toHaveLength(1); + expect(result.knowledge_sources![0]).toEqual({ + name: "react-docs", + origin: "https://react.dev/reference", + description: "Official React documentation" + }); + }); +}); diff --git a/packages/core/src/writers/knowledge.ts b/packages/core/src/writers/knowledge.ts new file mode 100644 index 0000000..04fa488 --- /dev/null +++ b/packages/core/src/writers/knowledge.ts @@ -0,0 +1,15 @@ +import type { ProvisionWriterDef } from "../types.js"; + +export const knowledgeWriter: ProvisionWriterDef = { + id: "knowledge", + async write(config) { + const { name, origin, description } = config as { + name: string; + origin: string; + description: string; + }; + return { + knowledge_sources: [{ name, origin, description }] + }; + } +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 769ebd7..8f2f223 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,6 +105,9 @@ importers: specifier: ^2.3.0 version: 2.3.0 devDependencies: + "@codemcp/knowledge": + specifier: 2.1.0 + version: 2.1.0(zod@4.3.6) "@typescript-eslint/eslint-plugin": specifier: ^8.21.0 version: 8.56.0(@typescript-eslint/parser@8.56.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) @@ -395,6 +398,14 @@ packages: integrity: sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g== } + "@codemcp/knowledge@2.1.0": + resolution: + { + integrity: sha512-n+p0oFHHkYY7RUNtoPgh3/SuLHMafwFbZyeKMoBIDfS01atJJRJpLlpri5rpc0a7khEGocvU1/WI3P0OC7TgvA== + } + engines: { node: ">=20.0.0", pnpm: ">=9.0.0" } + hasBin: true + "@codemcp/skills@2.3.0": resolution: { @@ -955,6 +966,15 @@ packages: } engines: { node: ^20.17.0 || >=22.9.0 } + "@hono/node-server@1.19.11": + resolution: + { + integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g== + } + engines: { node: ">=18.14.1" } + peerDependencies: + hono: ^4 + "@humanfs/core@0.19.1": resolution: { @@ -1071,6 +1091,19 @@ packages: integrity: sha512-HsvL6zgE5sUPGgkIDlmAWR1HTNHz2Iy11BAWPTa4Jjabkpguy4Ze2gzfLrg6pdRuBvFwgUYyxiaNqZwrEEXepA== } + "@modelcontextprotocol/sdk@1.27.1": + resolution: + { + integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA== + } + engines: { node: ">=18" } + peerDependencies: + "@cfworker/json-schema": ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + "@cfworker/json-schema": + optional: true + "@nodelib/fs.scandir@2.1.5": resolution: { @@ -2250,6 +2283,13 @@ packages: } engines: { node: ^20.17.0 || >=22.9.0 } + accepts@2.0.0: + resolution: + { + integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== + } + engines: { node: ">= 0.6" } + acorn-jsx@5.3.2: resolution: { @@ -2266,6 +2306,13 @@ packages: engines: { node: ">=0.4.0" } hasBin: true + adm-zip@0.5.16: + resolution: + { + integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ== + } + engines: { node: ">=12.0" } + agent-base@7.1.4: resolution: { @@ -2273,6 +2320,17 @@ packages: } engines: { node: ">= 14" } + ajv-formats@3.0.1: + resolution: + { + integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ== + } + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: { @@ -2385,6 +2443,13 @@ packages: integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw== } + body-parser@2.2.2: + resolution: + { + integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA== + } + engines: { node: ">=18" } + brace-expansion@1.1.12: resolution: { @@ -2411,6 +2476,13 @@ packages: } engines: { node: ">=8" } + bytes@3.1.2: + resolution: + { + integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + } + engines: { node: ">= 0.8" } + cac@6.7.14: resolution: { @@ -2425,6 +2497,20 @@ packages: } engines: { node: ^20.17.0 || >=22.9.0 } + call-bind-apply-helpers@1.0.2: + resolution: + { + integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + } + engines: { node: ">= 0.4" } + + call-bound@1.0.4: + resolution: + { + integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + } + engines: { node: ">= 0.4" } + callsites@3.1.0: resolution: { @@ -2545,6 +2631,13 @@ packages: integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== } + commander@12.1.0: + resolution: + { + integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + } + engines: { node: ">=18" } + commander@13.1.0: resolution: { @@ -2584,6 +2677,34 @@ packages: integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ== } + content-disposition@1.0.1: + resolution: + { + integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q== + } + engines: { node: ">=18" } + + content-type@1.0.5: + resolution: + { + integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + } + engines: { node: ">= 0.6" } + + cookie-signature@1.2.2: + resolution: + { + integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== + } + engines: { node: ">=6.6.0" } + + cookie@0.7.2: + resolution: + { + integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + } + engines: { node: ">= 0.6" } + copy-anything@4.0.5: resolution: { @@ -2591,6 +2712,13 @@ packages: } engines: { node: ">=18" } + cors@2.8.6: + resolution: + { + integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw== + } + engines: { node: ">= 0.10" } + cose-base@1.0.3: resolution: { @@ -2938,6 +3066,13 @@ packages: integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw== } + depd@2.0.0: + resolution: + { + integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + } + engines: { node: ">= 0.8" } + dequal@2.0.3: resolution: { @@ -2957,12 +3092,25 @@ packages: integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q== } + dunder-proto@1.0.1: + resolution: + { + integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + } + engines: { node: ">= 0.4" } + eastasianwidth@0.2.0: resolution: { integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== } + ee-first@1.1.1: + resolution: + { + integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + } + emoji-regex-xs@1.0.0: resolution: { @@ -2987,6 +3135,13 @@ packages: integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== } + encodeurl@2.0.0: + resolution: + { + integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + } + engines: { node: ">= 0.8" } + entities@7.0.1: resolution: { @@ -3014,12 +3169,33 @@ packages: integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== } + es-define-property@1.0.1: + resolution: + { + integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + } + engines: { node: ">= 0.4" } + + es-errors@1.3.0: + resolution: + { + integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + } + engines: { node: ">= 0.4" } + es-module-lexer@1.7.0: resolution: { integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== } + es-object-atoms@1.1.1: + resolution: + { + integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + } + engines: { node: ">= 0.4" } + esbuild@0.21.5: resolution: { @@ -3036,6 +3212,12 @@ packages: engines: { node: ">=18" } hasBin: true + escape-html@1.0.3: + resolution: + { + integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + } + escape-string-regexp@4.0.0: resolution: { @@ -3148,12 +3330,33 @@ packages: } engines: { node: ">=0.10.0" } + etag@1.8.1: + resolution: + { + integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + } + engines: { node: ">= 0.6" } + eventemitter3@5.0.4: resolution: { integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw== } + eventsource-parser@3.0.6: + resolution: + { + integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg== + } + engines: { node: ">=18.0.0" } + + eventsource@3.0.7: + resolution: + { + integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA== + } + engines: { node: ">=18.0.0" } + execa@8.0.1: resolution: { @@ -3174,6 +3377,22 @@ packages: integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA== } + express-rate-limit@8.3.1: + resolution: + { + integrity: sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw== + } + engines: { node: ">= 16" } + peerDependencies: + express: ">= 4.11" + + express@5.2.1: + resolution: + { + integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw== + } + engines: { node: ">= 18" } + exsolve@1.0.8: resolution: { @@ -3250,6 +3469,13 @@ packages: } engines: { node: ">=8" } + finalhandler@2.1.1: + resolution: + { + integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA== + } + engines: { node: ">= 18.0.0" } + find-up@5.0.0: resolution: { @@ -3283,6 +3509,20 @@ packages: } engines: { node: ">=14" } + forwarded@0.2.0: + resolution: + { + integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + } + engines: { node: ">= 0.6" } + + fresh@2.0.0: + resolution: + { + integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== + } + engines: { node: ">= 0.8" } + fs-minipass@3.0.3: resolution: { @@ -3298,6 +3538,12 @@ packages: engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } os: [darwin] + function-bind@1.1.2: + resolution: + { + integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + } + get-east-asian-width@1.5.0: resolution: { @@ -3305,6 +3551,20 @@ packages: } engines: { node: ">=18" } + get-intrinsic@1.3.0: + resolution: + { + integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + } + engines: { node: ">= 0.4" } + + get-proto@1.0.1: + resolution: + { + integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + } + engines: { node: ">= 0.4" } + get-stream@8.0.1: resolution: { @@ -3355,6 +3615,13 @@ packages: } engines: { node: ">=18" } + gopd@1.2.0: + resolution: + { + integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + } + engines: { node: ">= 0.4" } + graceful-fs@4.2.11: resolution: { @@ -3394,6 +3661,20 @@ packages: } engines: { node: ">=8" } + has-symbols@1.1.0: + resolution: + { + integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + } + engines: { node: ">= 0.4" } + + hasown@2.0.2: + resolution: + { + integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + } + engines: { node: ">= 0.4" } + hast-util-to-html@9.0.5: resolution: { @@ -3406,6 +3687,13 @@ packages: integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== } + hono@4.12.8: + resolution: + { + integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A== + } + engines: { node: ">=16.9.0" } + hookable@5.5.3: resolution: { @@ -3437,6 +3725,13 @@ packages: integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ== } + http-errors@2.0.1: + resolution: + { + integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== + } + engines: { node: ">= 0.8" } + http-proxy-agent@7.0.2: resolution: { @@ -3521,6 +3816,12 @@ packages: } engines: { node: ">=0.8.19" } + inherits@2.0.4: + resolution: + { + integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + } + ini@6.0.0: resolution: { @@ -3548,6 +3849,13 @@ packages: } engines: { node: ">= 12" } + ipaddr.js@1.9.1: + resolution: + { + integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + } + engines: { node: ">= 0.10" } + is-binary-path@2.1.0: resolution: { @@ -3604,6 +3912,12 @@ packages: } engines: { node: ">=0.12.0" } + is-promise@4.0.0: + resolution: + { + integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== + } + is-stream@3.0.0: resolution: { @@ -3665,6 +3979,12 @@ packages: integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== } + jose@6.2.1: + resolution: + { + integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw== + } + js-tokens@10.0.0: resolution: { @@ -3716,6 +4036,12 @@ packages: integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== } + json-schema-typed@8.0.2: + resolution: + { + integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA== + } + json-stable-stringify-without-jsonify@1.0.1: resolution: { @@ -3907,12 +4233,33 @@ packages: engines: { node: ">= 18" } hasBin: true + math-intrinsics@1.1.0: + resolution: + { + integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + } + engines: { node: ">= 0.4" } + mdast-util-to-hast@13.2.1: resolution: { integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA== } + media-typer@1.1.0: + resolution: + { + integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== + } + engines: { node: ">= 0.8" } + + merge-descriptors@2.0.0: + resolution: + { + integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== + } + engines: { node: ">=18" } + merge-stream@2.0.0: resolution: { @@ -3969,6 +4316,20 @@ packages: } engines: { node: ">=8.6" } + mime-db@1.54.0: + resolution: + { + integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + } + engines: { node: ">= 0.6" } + + mime-types@3.0.2: + resolution: + { + integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A== + } + engines: { node: ">=18" } + mimic-fn@4.0.0: resolution: { @@ -4197,6 +4558,33 @@ packages: } engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + object-assign@4.1.1: + resolution: + { + integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + } + engines: { node: ">=0.10.0" } + + object-inspect@1.13.4: + resolution: + { + integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + } + engines: { node: ">= 0.4" } + + on-finished@2.4.1: + resolution: + { + integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + } + engines: { node: ">= 0.8" } + + once@1.4.0: + resolution: + { + integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + } + onetime@6.0.0: resolution: { @@ -4272,6 +4660,13 @@ packages: } engines: { node: ">=6" } + parseurl@1.3.3: + resolution: + { + integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + } + engines: { node: ">= 0.8" } + path-data-parser@0.1.0: resolution: { @@ -4313,6 +4708,12 @@ packages: } engines: { node: 20 || >=22 } + path-to-regexp@8.3.0: + resolution: + { + integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA== + } + pathe@2.0.3: resolution: { @@ -4360,6 +4761,13 @@ packages: engines: { node: ">=0.10" } hasBin: true + pkce-challenge@5.0.1: + resolution: + { + integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ== + } + engines: { node: ">=16.20.0" } + pkg-types@1.3.1: resolution: { @@ -4432,6 +4840,13 @@ packages: integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ== } + proxy-addr@2.0.7: + resolution: + { + integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + } + engines: { node: ">= 0.10" } + pstree.remy@1.1.8: resolution: { @@ -4445,6 +4860,13 @@ packages: } engines: { node: ">=6" } + qs@6.15.0: + resolution: + { + integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ== + } + engines: { node: ">=0.6" } + quansync@0.2.11: resolution: { @@ -4457,6 +4879,20 @@ packages: integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== } + range-parser@1.2.1: + resolution: + { + integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + } + engines: { node: ">= 0.6" } + + raw-body@3.0.2: + resolution: + { + integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA== + } + engines: { node: ">= 0.10" } + readdirp@3.6.0: resolution: { @@ -4558,6 +4994,13 @@ packages: integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ== } + router@2.2.0: + resolution: + { + integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ== + } + engines: { node: ">= 18" } + run-parallel@1.2.0: resolution: { @@ -4597,6 +5040,26 @@ packages: engines: { node: ">=10" } hasBin: true + send@1.2.1: + resolution: + { + integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ== + } + engines: { node: ">= 18" } + + serve-static@2.2.1: + resolution: + { + integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw== + } + engines: { node: ">= 18" } + + setprototypeof@1.2.0: + resolution: + { + integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + } + shebang-command@2.0.0: resolution: { @@ -4617,6 +5080,34 @@ packages: integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ== } + side-channel-list@1.0.0: + resolution: + { + integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + } + engines: { node: ">= 0.4" } + + side-channel-map@1.0.1: + resolution: + { + integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + } + engines: { node: ">= 0.4" } + + side-channel-weakmap@1.0.2: + resolution: + { + integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + } + engines: { node: ">= 0.4" } + + side-channel@1.1.0: + resolution: + { + integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + } + engines: { node: ">= 0.4" } + siginfo@2.0.0: resolution: { @@ -4748,6 +5239,13 @@ packages: integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== } + statuses@2.0.2: + resolution: + { + integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + } + engines: { node: ">= 0.8" } + std-env@3.10.0: resolution: { @@ -4930,6 +5428,13 @@ packages: } engines: { node: ">=8.0" } + toidentifier@1.0.1: + resolution: + { + integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + } + engines: { node: ">=0.6" } + touch@3.1.1: resolution: { @@ -5028,6 +5533,13 @@ packages: } engines: { node: ">= 0.8.0" } + type-is@2.0.1: + resolution: + { + integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw== + } + engines: { node: ">= 0.6" } + typescript-eslint@8.21.0: resolution: { @@ -5108,6 +5620,13 @@ packages: integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg== } + unpipe@1.0.0: + resolution: + { + integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + } + engines: { node: ">= 0.8" } + uri-js@4.4.1: resolution: { @@ -5128,6 +5647,13 @@ packages: } engines: { node: ^20.17.0 || >=22.9.0 } + vary@1.1.2: + resolution: + { + integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + } + engines: { node: ">= 0.8" } + vfile-message@4.0.3: resolution: { @@ -5381,6 +5907,12 @@ packages: } engines: { node: ">=18" } + wrappy@1.0.2: + resolution: + { + integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + } + yallist@4.0.0: resolution: { @@ -5409,6 +5941,20 @@ packages: } engines: { node: ">=10" } + zod-to-json-schema@3.25.1: + resolution: + { + integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA== + } + peerDependencies: + zod: ^3.25 || ^4 + + zod@4.3.6: + resolution: + { + integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg== + } + zwitch@2.0.4: resolution: { @@ -5586,6 +6132,16 @@ snapshots: "@clack/core": 1.1.0 sisteransi: 1.0.5 + "@codemcp/knowledge@2.1.0(zod@4.3.6)": + dependencies: + "@modelcontextprotocol/sdk": 1.27.1(zod@4.3.6) + adm-zip: 0.5.16 + commander: 12.1.0 + transitivePeerDependencies: + - "@cfworker/json-schema" + - supports-color + - zod + "@codemcp/skills@2.3.0": dependencies: ajv: 8.18.0 @@ -5819,6 +6375,10 @@ snapshots: dependencies: retry: 0.13.1 + "@hono/node-server@1.19.11(hono@4.12.8)": + dependencies: + hono: 4.12.8 + "@humanfs/core@0.19.1": {} "@humanfs/node@0.16.7": @@ -5901,6 +6461,28 @@ snapshots: dependencies: langium: 3.0.0 + "@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)": + dependencies: + "@hono/node-server": 1.19.11(hono@4.12.8) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.3.1(express@5.2.1) + hono: 4.12.8 + jose: 6.2.1 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - supports-color + "@nodelib/fs.scandir@2.1.5": dependencies: "@nodelib/fs.stat": 2.0.5 @@ -6681,14 +7263,25 @@ snapshots: abbrev@4.0.0: {} + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 acorn@8.16.0: {} + adm-zip@0.5.16: {} + agent-base@7.1.4: {} + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -6761,6 +7354,20 @@ snapshots: birpc@2.9.0: {} + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -6778,6 +7385,8 @@ snapshots: dependencies: fill-range: 7.1.1 + bytes@3.1.2: {} + cac@6.7.14: {} cacache@20.0.3: @@ -6794,6 +7403,16 @@ snapshots: ssri: 13.0.1 unique-filename: 5.0.0 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} ccount@2.0.1: {} @@ -6866,6 +7485,8 @@ snapshots: comma-separated-tokens@2.0.3: {} + commander@12.1.0: {} + commander@13.1.0: {} commander@7.2.0: {} @@ -6878,10 +7499,23 @@ snapshots: confbox@0.2.4: {} + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + copy-anything@4.0.5: dependencies: is-what: 5.5.0 + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cose-base@1.0.3: dependencies: layout-base: 1.0.2 @@ -7102,6 +7736,8 @@ snapshots: dependencies: robust-predicates: 3.0.2 + depd@2.0.0: {} + dequal@2.0.3: {} devlop@1.1.0: @@ -7112,8 +7748,16 @@ snapshots: optionalDependencies: "@types/trusted-types": 2.0.7 + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + eastasianwidth@0.2.0: {} + ee-first@1.1.1: {} + emoji-regex-xs@1.0.0: {} emoji-regex@10.6.0: {} @@ -7122,6 +7766,8 @@ snapshots: emoji-regex@9.2.2: {} + encodeurl@2.0.0: {} + entities@7.0.1: {} env-paths@2.2.1: {} @@ -7130,8 +7776,16 @@ snapshots: err-code@2.0.3: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + esbuild@0.21.5: optionalDependencies: "@esbuild/aix-ppc64": 0.21.5 @@ -7187,6 +7841,8 @@ snapshots: "@esbuild/win32-ia32": 0.27.3 "@esbuild/win32-x64": 0.27.3 + escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} eslint-config-prettier@10.1.8(eslint@9.39.2): @@ -7269,8 +7925,16 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + eventemitter3@5.0.4: {} + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + execa@8.0.1: dependencies: cross-spawn: 7.0.6 @@ -7287,6 +7951,44 @@ snapshots: exponential-backoff@3.1.3: {} + express-rate-limit@8.3.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.0(supports-color@5.5.0) + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + exsolve@1.0.8: {} extend-shallow@2.0.1: @@ -7325,6 +8027,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.0(supports-color@5.5.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -7346,6 +8059,10 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + forwarded@0.2.0: {} + + fresh@2.0.0: {} + fs-minipass@3.0.3: dependencies: minipass: 7.1.3 @@ -7353,8 +8070,28 @@ snapshots: fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + get-east-asian-width@1.5.0: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@8.0.1: {} glob-parent@5.1.2: @@ -7384,6 +8121,8 @@ snapshots: globals@15.15.0: {} + gopd@1.2.0: {} + graceful-fs@4.2.11: {} graphemer@1.4.0: {} @@ -7401,6 +8140,12 @@ snapshots: has-flag@4.0.0: {} + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + hast-util-to-html@9.0.5: dependencies: "@types/hast": 3.0.4 @@ -7419,6 +8164,8 @@ snapshots: dependencies: "@types/hast": 3.0.4 + hono@4.12.8: {} + hookable@5.5.3: {} hosted-git-info@9.0.2: @@ -7431,6 +8178,14 @@ snapshots: http-cache-semantics@4.2.0: {} + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -7456,7 +8211,6 @@ snapshots: iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 - optional: true ignore-by-default@1.0.1: {} @@ -7475,6 +8229,8 @@ snapshots: imurmurhash@0.1.4: {} + inherits@2.0.4: {} + ini@6.0.0: {} internmap@1.0.1: {} @@ -7483,6 +8239,8 @@ snapshots: ip-address@10.1.0: {} + ipaddr.js@1.9.1: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -7505,6 +8263,8 @@ snapshots: is-number@7.0.0: {} + is-promise@4.0.0: {} + is-stream@3.0.0: {} is-what@5.5.0: {} @@ -7540,6 +8300,8 @@ snapshots: optionalDependencies: "@pkgjs/parseargs": 0.11.0 + jose@6.2.1: {} + js-tokens@10.0.0: {} js-tokens@9.0.1: {} @@ -7561,6 +8323,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} jsonparse@1.3.1: {} @@ -7686,6 +8450,8 @@ snapshots: marked@13.0.3: {} + math-intrinsics@1.1.0: {} + mdast-util-to-hast@13.2.1: dependencies: "@types/hast": 3.0.4 @@ -7698,6 +8464,10 @@ snapshots: unist-util-visit: 5.1.0 vfile: 6.0.3 + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -7749,6 +8519,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mimic-fn@4.0.0: {} mimic-function@5.0.1: {} @@ -7901,6 +8677,18 @@ snapshots: dependencies: path-key: 4.0.0 + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + onetime@6.0.0: dependencies: mimic-fn: 4.0.0 @@ -7964,6 +8752,8 @@ snapshots: dependencies: callsites: 3.1.0 + parseurl@1.3.3: {} + path-data-parser@0.1.0: {} path-exists@4.0.0: {} @@ -7982,6 +8772,8 @@ snapshots: lru-cache: 11.2.6 minipass: 7.1.3 + path-to-regexp@8.3.0: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -7996,6 +8788,8 @@ snapshots: pidtree@0.6.0: {} + pkce-challenge@5.0.1: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -8036,14 +8830,32 @@ snapshots: property-information@7.1.0: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + pstree.remy@1.1.8: {} punycode@2.3.1: {} + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + quansync@0.2.11: {} queue-microtask@1.2.3: {} + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -8120,6 +8932,16 @@ snapshots: points-on-curve: 0.2.0 points-on-path: 0.2.1 + router@2.2.0: + dependencies: + debug: 4.4.0(supports-color@5.5.0) + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -8137,6 +8959,33 @@ snapshots: semver@7.7.4: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -8154,6 +9003,34 @@ snapshots: "@shikijs/vscode-textmate": 10.0.2 "@types/hast": 3.0.4 + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -8231,6 +9108,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} string-argv@0.3.2: {} @@ -8327,6 +9206,8 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + touch@3.1.1: {} trim-lines@3.0.1: {} @@ -8376,6 +9257,12 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typescript-eslint@8.21.0(eslint@9.39.2)(typescript@5.9.3): dependencies: "@typescript-eslint/eslint-plugin": 8.21.0(@typescript-eslint/parser@8.21.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) @@ -8425,6 +9312,8 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 + unpipe@1.0.0: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -8433,6 +9322,8 @@ snapshots: validate-npm-package-name@7.0.2: {} + vary@1.1.2: {} + vfile-message@4.0.3: dependencies: "@types/unist": 3.0.3 @@ -8643,6 +9534,8 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.2 + wrappy@1.0.2: {} + yallist@4.0.0: {} yallist@5.0.0: {} @@ -8651,4 +9544,10 @@ snapshots: yocto-queue@0.1.0: {} + zod-to-json-schema@3.25.1(zod@4.3.6): + dependencies: + zod: 4.3.6 + + zod@4.3.6: {} + zwitch@2.0.4: {} From 38c0b0d41914e753bf270b7596989cfba8e58715 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 17:30:30 +0000 Subject: [PATCH 40/60] refactor: use @codemcp/knowledge's own type declarations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove hand-written knowledge-api.d.ts — the package ships exports.d.ts with full typed API at packages/cli/dist/exports.d.ts. Revert the .gitignore !*-api.d.ts exception since it's no longer needed. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- .gitignore | 1 - packages/cli/src/knowledge-api.d.ts | 52 ----------------------------- 2 files changed, 53 deletions(-) delete mode 100644 packages/cli/src/knowledge-api.d.ts diff --git a/.gitignore b/.gitignore index 8a87119..de62c3b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ dist # Typescript *.tsbuildinfo *.d.ts -!*-api.d.ts # Turborepo .turbo diff --git a/packages/cli/src/knowledge-api.d.ts b/packages/cli/src/knowledge-api.d.ts deleted file mode 100644 index 821e02d..0000000 --- a/packages/cli/src/knowledge-api.d.ts +++ /dev/null @@ -1,52 +0,0 @@ -declare module "@codemcp/knowledge/packages/cli/dist/exports.js" { - interface CreateDocsetParams { - id: string; - name: string; - description?: string; - preset: "git-repo" | "local-folder" | "archive"; - url?: string; - branch?: string; - paths?: string[]; - path?: string; - } - - interface CreateDocsetOptions { - cwd?: string; - } - - interface CreateDocsetResult { - docset: unknown; - configPath: string; - configCreated: boolean; - } - - interface InitDocsetParams { - docsetId: string; - force?: boolean; - discoverPaths?: boolean; - cwd?: string; - onSourceProgress?: (progress: unknown) => void; - } - - interface InitDocsetResult { - alreadyInitialized: boolean; - discoveredPaths?: string[]; - } - - export function createDocset( - params: CreateDocsetParams, - options?: CreateDocsetOptions - ): Promise; - - export function initDocset( - params: InitDocsetParams - ): Promise; - - export function refreshDocsets(params?: { - docsetId?: string; - force?: boolean; - cwd?: string; - }): Promise; - - export function getStatus(params?: { cwd?: string }): Promise; -} From a3838f956fc96dd9f7e58b34bd8b578b381569d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 21:01:08 +0000 Subject: [PATCH 41/60] fix: use .git URLs for docset origins and add knowledge integration tests - Fix all docset origins to use valid .git repository URLs required by createDocset's git-repo preset validation - Add knowledge integration tests verifying createDocset + initDocset calls, docset exclusion, and MCP server entry in settings.json - Fix existing tanstack integration tests to mock the docset confirmation multiselect - Remove unused DocsetDef import (lint fix) https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- .../commands/conventions.integration.spec.ts | 11 +- .../commands/knowledge.integration.spec.ts | 167 ++++++++++++++++++ packages/cli/src/commands/setup.spec.ts | 6 +- .../core/src/catalog/facets/architecture.ts | 8 +- packages/core/src/catalog/facets/practices.ts | 3 +- packages/core/src/resolver.spec.ts | 14 +- packages/core/src/writers/knowledge.spec.ts | 4 +- 7 files changed, 191 insertions(+), 22 deletions(-) create mode 100644 packages/cli/src/commands/knowledge.integration.spec.ts diff --git a/packages/cli/src/commands/conventions.integration.spec.ts b/packages/cli/src/commands/conventions.integration.spec.ts index 5f42067..4c88350 100644 --- a/packages/cli/src/commands/conventions.integration.spec.ts +++ b/packages/cli/src/commands/conventions.integration.spec.ts @@ -41,7 +41,9 @@ describe("architecture and practices facets integration", () => { vi.mocked(clack.select) .mockResolvedValueOnce("codemcp-workflows") // process .mockResolvedValueOnce("tanstack"); // architecture - vi.mocked(clack.multiselect).mockResolvedValueOnce([]); // practices: none + vi.mocked(clack.multiselect) + .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce([]); // docsets: deselect all await runSetup(dir, catalog); @@ -206,10 +208,9 @@ describe("architecture and practices facets integration", () => { vi.mocked(clack.select) .mockResolvedValueOnce("codemcp-workflows") // process .mockResolvedValueOnce("tanstack"); // architecture - vi.mocked(clack.multiselect).mockResolvedValueOnce([ - "tdd-london", - "conventional-commits" - ]); + vi.mocked(clack.multiselect) + .mockResolvedValueOnce(["tdd-london", "conventional-commits"]) // practices + .mockResolvedValueOnce([]); // docsets: deselect all await runSetup(dir, catalog); diff --git a/packages/cli/src/commands/knowledge.integration.spec.ts b/packages/cli/src/commands/knowledge.integration.spec.ts new file mode 100644 index 0000000..97d2b31 --- /dev/null +++ b/packages/cli/src/commands/knowledge.integration.spec.ts @@ -0,0 +1,167 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +vi.mock("@clack/prompts", () => ({ + intro: vi.fn(), + outro: vi.fn(), + select: vi.fn(), + multiselect: vi.fn(), + confirm: vi.fn(), + isCancel: vi.fn().mockReturnValue(false), + cancel: vi.fn(), + spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) +})); + +vi.mock("@codemcp/knowledge/packages/cli/dist/exports.js", () => ({ + createDocset: vi.fn().mockResolvedValue({ + docset: {}, + configPath: ".knowledge/config.yaml", + configCreated: false + }), + initDocset: vi.fn().mockResolvedValue({ alreadyInitialized: false }) +})); + +import * as clack from "@clack/prompts"; +import { + createDocset, + initDocset +} from "@codemcp/knowledge/packages/cli/dist/exports.js"; +import { runSetup } from "./setup.js"; +import { readLockFile } from "@ade/core"; +import { getDefaultCatalog } from "../../../core/src/catalog/index.js"; + +describe("knowledge integration", () => { + let dir: string; + + beforeEach(async () => { + vi.clearAllMocks(); + dir = await mkdtemp(join(tmpdir(), "ade-knowledge-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it( + "creates and initializes docsets when tanstack is selected", + { timeout: 60_000 }, + async () => { + const catalog = getDefaultCatalog(); + + // Facet order: process (select), architecture (select) + vi.mocked(clack.select) + .mockResolvedValueOnce("codemcp-workflows") // process + .mockResolvedValueOnce("tanstack"); // architecture + + // multiselect order: practices, then docsets confirmation + vi.mocked(clack.multiselect) + .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce([ + // docsets: accept all 4 + "tanstack-router-docs", + "tanstack-query-docs", + "tanstack-form-docs", + "tanstack-table-docs" + ]); + + await runSetup(dir, catalog); + + // createDocset should be called for each of the 4 TanStack docsets + expect(createDocset).toHaveBeenCalledTimes(4); + + expect(createDocset).toHaveBeenCalledWith( + expect.objectContaining({ + id: "tanstack-router-docs", + preset: "git-repo", + url: "https://github.com/TanStack/router.git" + }), + expect.objectContaining({ cwd: dir }) + ); + + expect(createDocset).toHaveBeenCalledWith( + expect.objectContaining({ + id: "tanstack-query-docs", + preset: "git-repo", + url: "https://github.com/TanStack/query.git" + }), + expect.objectContaining({ cwd: dir }) + ); + + // initDocset should be called for each docset after creation + expect(initDocset).toHaveBeenCalledTimes(4); + expect(initDocset).toHaveBeenCalledWith( + expect.objectContaining({ + docsetId: "tanstack-router-docs", + cwd: dir + }) + ); + + // Lock file should contain knowledge_sources + const lock = await readLockFile(dir); + expect(lock!.logical_config.knowledge_sources).toHaveLength(4); + expect(lock!.logical_config.knowledge_sources.map((s) => s.name)).toEqual( + expect.arrayContaining([ + "tanstack-router-docs", + "tanstack-query-docs", + "tanstack-form-docs", + "tanstack-table-docs" + ]) + ); + + // MCP server entry for knowledge-server should be in settings.json + const settings = JSON.parse( + await readFile(join(dir, ".claude", "settings.json"), "utf-8") + ); + expect(settings.mcpServers["@codemcp/knowledge-server"]).toMatchObject({ + command: "npx", + args: ["-y", "@codemcp/knowledge-server"] + }); + } + ); + + it( + "excludes deselected docsets from knowledge installation", + { timeout: 60_000 }, + async () => { + const catalog = getDefaultCatalog(); + + vi.mocked(clack.select) + .mockResolvedValueOnce("codemcp-workflows") // process + .mockResolvedValueOnce("tanstack"); // architecture + + // multiselect order: practices, then docsets (only keep router + query) + vi.mocked(clack.multiselect) + .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce(["tanstack-router-docs", "tanstack-query-docs"]); + + await runSetup(dir, catalog); + + // Only 2 docsets should be created + expect(createDocset).toHaveBeenCalledTimes(2); + expect(initDocset).toHaveBeenCalledTimes(2); + + // Lock file should only have the 2 selected sources + const lock = await readLockFile(dir); + expect(lock!.logical_config.knowledge_sources).toHaveLength(2); + expect(lock!.logical_config.knowledge_sources.map((s) => s.name)).toEqual( + expect.arrayContaining(["tanstack-router-docs", "tanstack-query-docs"]) + ); + } + ); + + it("skips knowledge installation when no docsets are implied", async () => { + const catalog = getDefaultCatalog(); + + vi.mocked(clack.select) + .mockResolvedValueOnce("native-agents-md") // process + .mockResolvedValueOnce("__skip__"); // architecture: skip + vi.mocked(clack.multiselect).mockResolvedValueOnce(["tdd-london"]); // practices: no docsets + + await runSetup(dir, catalog); + + expect(createDocset).not.toHaveBeenCalled(); + expect(initDocset).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/cli/src/commands/setup.spec.ts b/packages/cli/src/commands/setup.spec.ts index ebdc53e..101b288 100644 --- a/packages/cli/src/commands/setup.spec.ts +++ b/packages/cli/src/commands/setup.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import type { Catalog, LogicalConfig, DocsetDef } from "@ade/core"; +import type { Catalog, LogicalConfig } from "@ade/core"; // ── Mocks ──────────────────────────────────────────────────────────────────── @@ -107,13 +107,13 @@ const docsetCatalog: Catalog = { { id: "react-docs", label: "React Reference", - origin: "https://react.dev/reference", + origin: "https://github.com/facebook/react.git", description: "Official React docs" }, { id: "react-tutorial", label: "React Tutorial", - origin: "https://react.dev/learn", + origin: "https://github.com/reactjs/react.dev.git", description: "React learn guide" } ] diff --git a/packages/core/src/catalog/facets/architecture.ts b/packages/core/src/catalog/facets/architecture.ts index b73504b..daba41d 100644 --- a/packages/core/src/catalog/facets/architecture.ts +++ b/packages/core/src/catalog/facets/architecture.ts @@ -127,25 +127,25 @@ export const architectureFacet: Facet = { { id: "tanstack-router-docs", label: "TanStack Router", - origin: "https://tanstack.com/router/latest/docs", + origin: "https://github.com/TanStack/router.git", description: "File-based routing, loaders, and search params" }, { id: "tanstack-query-docs", label: "TanStack Query", - origin: "https://tanstack.com/query/latest/docs", + origin: "https://github.com/TanStack/query.git", description: "Server state management, caching, and mutations" }, { id: "tanstack-form-docs", label: "TanStack Form", - origin: "https://tanstack.com/form/latest/docs", + origin: "https://github.com/TanStack/form.git", description: "Type-safe form state and validation" }, { id: "tanstack-table-docs", label: "TanStack Table", - origin: "https://tanstack.com/table/latest/docs", + origin: "https://github.com/TanStack/table.git", description: "Headless table and datagrid utilities" } ] diff --git a/packages/core/src/catalog/facets/practices.ts b/packages/core/src/catalog/facets/practices.ts index fac7b1d..2e85987 100644 --- a/packages/core/src/catalog/facets/practices.ts +++ b/packages/core/src/catalog/facets/practices.ts @@ -67,7 +67,8 @@ export const practicesFacet: Facet = { { id: "conventional-commits-spec", label: "Conventional Commits Spec", - origin: "https://www.conventionalcommits.org/en/v1.0.0/", + origin: + "https://github.com/conventional-commits/conventionalcommits.org.git", description: "The Conventional Commits specification" } ] diff --git a/packages/core/src/resolver.spec.ts b/packages/core/src/resolver.spec.ts index 2e8d640..d3db8d0 100644 --- a/packages/core/src/resolver.spec.ts +++ b/packages/core/src/resolver.spec.ts @@ -212,7 +212,7 @@ describe("resolve", () => { { id: "react-docs", label: "React Reference", - origin: "https://react.dev/reference", + origin: "https://github.com/facebook/react.git", description: "Official React documentation" } ] @@ -228,7 +228,7 @@ describe("resolve", () => { expect(result.knowledge_sources).toHaveLength(1); expect(result.knowledge_sources[0]).toEqual({ name: "react-docs", - origin: "https://react.dev/reference", + origin: "https://github.com/facebook/react.git", description: "Official React documentation" }); }); @@ -252,7 +252,7 @@ describe("resolve", () => { { id: "react-docs", label: "React Reference", - origin: "https://react.dev/reference", + origin: "https://github.com/facebook/react.git", description: "React docs" } ] @@ -266,7 +266,7 @@ describe("resolve", () => { { id: "react-docs", label: "React Reference", - origin: "https://react.dev/reference", + origin: "https://github.com/facebook/react.git", description: "React docs" }, { @@ -311,13 +311,13 @@ describe("resolve", () => { { id: "react-docs", label: "React Reference", - origin: "https://react.dev/reference", + origin: "https://github.com/facebook/react.git", description: "React docs" }, { id: "react-tutorial", label: "React Tutorial", - origin: "https://react.dev/learn", + origin: "https://github.com/reactjs/react.dev.git", description: "React tutorial" } ] @@ -355,7 +355,7 @@ describe("resolve", () => { { id: "react-docs", label: "React Reference", - origin: "https://react.dev/reference", + origin: "https://github.com/facebook/react.git", description: "React docs" } ] diff --git a/packages/core/src/writers/knowledge.spec.ts b/packages/core/src/writers/knowledge.spec.ts index 63cd009..5a46fe7 100644 --- a/packages/core/src/writers/knowledge.spec.ts +++ b/packages/core/src/writers/knowledge.spec.ts @@ -10,7 +10,7 @@ describe("knowledgeWriter", () => { const result = await knowledgeWriter.write( { name: "react-docs", - origin: "https://react.dev/reference", + origin: "https://github.com/facebook/react.git", description: "Official React documentation" }, { resolved: {} } @@ -19,7 +19,7 @@ describe("knowledgeWriter", () => { expect(result.knowledge_sources).toHaveLength(1); expect(result.knowledge_sources![0]).toEqual({ name: "react-docs", - origin: "https://react.dev/reference", + origin: "https://github.com/facebook/react.git", description: "Official React documentation" }); }); From 355c22004eab48dc23bcf0420f612338420eadcc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 21:34:24 +0000 Subject: [PATCH 42/60] docs: align PRD and design doc with current implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace 4-facet model (process/conventions/documentation/frameworks) with actual 3-facet model (process/architecture/practices) - Document docsets as weak entity on Option with opt-out confirmation - Update agent writer: OpenCode → Claude Code with actual output files - Update package structure to reflect existing files - Update registry example to match createDefaultRegistry() - Update knowledge writer example with .git URLs and createDocset API - Update skills writer example with inline/external skill definitions - Add knowledge-installer and skills-installer to CLI structure - Remove references to unimplemented commands (add/remove/status) from package structure (kept in CLI Commands as planned) https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- docs/CLI-PRD.md | 142 ++++++++++++++------------ docs/CLI-design.md | 249 +++++++++++++++++++++++++-------------------- 2 files changed, 211 insertions(+), 180 deletions(-) diff --git a/docs/CLI-PRD.md b/docs/CLI-PRD.md index 85b8503..1fde443 100644 --- a/docs/CLI-PRD.md +++ b/docs/CLI-PRD.md @@ -1,8 +1,8 @@ # ADE CLI — Product Requirements Document -> **Scope.** This document covers the **ADE CLI** (`packages/ade`) — the +> **Scope.** This document covers the **ADE CLI** (`packages/cli`) — the > setup and configuration tool. It does not cover the broader ADE information -> architecture (process, conventions, documentation layers) or the runtime +> architecture (process, practices, documentation layers) or the runtime > MCP servers. For the overall ADE vision, see the project README. ## Problem @@ -13,7 +13,7 @@ Teams manually maintain these per-agent config files, leading to drift, duplication, and onboarding friction. Adding a new MCP server or skill means editing multiple agent-specific files by hand. -ADE's information architecture (process, conventions, documentation) is +ADE's information architecture (process, practices, documentation) is agent-agnostic, but the last mile — getting it into an agent's config — is not. ## Goal @@ -33,24 +33,15 @@ agent-specific configuration for whichever coding agent they use. ### Facet A user-facing configuration question representing a single concern (e.g. -"Which workflow framework?" or "Which testing convention?"). Each facet offers +"Which workflow framework?" or "Which architecture stack?"). Each facet offers a set of options, exactly one of which is selected (or multiple, if the facet allows multi-select). Facets can be skippable (no selection = no provisions from that facet). -Facets may **depend on other facets**. When a facet declares dependencies, -its provision writers receive the resolved options from those facets as -context. This allows provisions to adapt their output based on sibling -selections. For example, the testing facet may depend on the workflow facet -so that its skills writer knows which workflow-specific test conventions to -install. The resolver processes facets in dependency order. - -When a user selects a facet whose dependencies are not yet satisfied (e.g. -via `ade add`), the CLI prompts for the missing dependent facets first. - ### Option -One possible answer to a facet. Each option carries a recipe. +One possible answer to a facet. Each option carries a recipe and optionally +a list of recommended docsets. ### Recipe @@ -58,12 +49,24 @@ A list of provisions that an option brings into the project. A recipe is never referenced directly by the user — it is the payload behind an option. A single option often produces **multiple provisions targeting different -writers**. For example, the "codemcp" workflow option's recipe contains both +writers**. For example, the "codemcp-workflows" option's recipe contains both a `workflows` provision (registers the MCP server) and an `instruction` provision (adds workflow guidance to the agent's instructions). This is how one logical concept (e.g. "use codemcp workflows") materializes as both runtime config and agent instructions. +### Docset + +Documentation sources recommended by an option. Docsets are a **weak entity +on Option** — they are always implied by an upstream selection (e.g. picking +"TanStack" implies TanStack Router/Query/Form/Table docs). The TUI presents +all implied docsets as pre-selected defaults and allows the user to deselect +(opt-out, not opt-in). The resolver collects docsets from all selected +options, deduplicates by id, filters by `excluded_docsets`, and maps them to +`knowledge_sources` in LogicalConfig. When any knowledge sources are present, +the resolver automatically adds a `@codemcp/knowledge-server` MCP server +entry. + ### Provision An atomic unit of configuration. Each provision names a **writer** and @@ -72,19 +75,18 @@ carries writer-specific config. Provision types: | Writer | What it produces | | ------------- | ------------------------------------------------------------ | | `workflows` | MCP server entry for `@codemcp/workflows-server` | -| `skills` | Invokes `@codemcp/skills` to install skills | -| `knowledge` | Invokes `@codemcp/knowledge` CLI to set up knowledge sources | +| `skills` | Skill definitions (inline or external) for `@codemcp/skills` | +| `knowledge` | Knowledge source entry for `@codemcp/knowledge` | | `mcp-server` | Generic MCP server entry (command + args + env) | | `instruction` | Raw instruction text for the agent | | `installable` | CLI tool or dependency to be installed | ### KnowledgeSource -Describes the origin of documentation content (e.g. a URL, a local path, or -a package reference). Multiple knowledge sources may be combined into a single -docset when the `@codemcp/knowledge-server` MCP is the selected option for the -documentation facet. The knowledge CLI (`@codemcp/knowledge`) manages the -physical docset artifacts; ADE only tracks the sources. +Describes the origin of documentation content (a git repository URL ending +in `.git`). The `@codemcp/knowledge` package manages the physical docset +artifacts via its programmatic API (`createDocset` + `initDocset`); ADE +tracks the sources in LogicalConfig. ### LogicalConfig (intermediate representation) @@ -94,7 +96,7 @@ resolution step and the agent writers: ``` mcp_servers: [{ref, command, args, env}] instructions: [string] -cli_actions: [{command, args}] +skills: [SkillDefinition] knowledge_sources: [{name, origin, description}] ``` @@ -106,11 +108,9 @@ its writer needs updating. Supported agents (v1): -| Agent | Output files | -| -------- | ---------------------------- | -| OpenCode | TBD — opencode config format | - -Future agents: Claude Code, Copilot, Kiro. +| Agent | Output files | +| ----------- | ------------------------------------------------- | +| Claude Code | `.claude/settings.json`, `AGENTS.md`, skill files | ## User-Facing Files @@ -121,12 +121,13 @@ users may add manual entries in the `custom` section. ```yaml choices: - process: codemcp-workflows # facet_id: option_id (single-select) - conventions: codemcp-skills - documentation: knowledge-mcp - frameworks: # multi-select facet: list of option_ids - - react - - node-express + process: codemcp-workflows # single-select facet + architecture: tanstack # single-select facet + practices: # multi-select facet + - conventional-commits + - tdd-london +excluded_docsets: # docsets the user opted out of + - tanstack-table-docs custom: # user-managed section (not touched by CLI) mcp_servers: - ref: custom-server @@ -136,11 +137,6 @@ custom: # user-managed section (not touched by CLI) - "Always use pnpm, never npm." ``` -The target agent is **not** stored in `config.yaml`. It is specified at -generation time via `--agent` flag (e.g. `ade install --agent opencode`). -There is no auto-detection. This keeps the config agent-agnostic — the same -`config.yaml` can generate output for any supported agent. - The `custom` section is the only part users edit by hand. All other sections are maintained exclusively through CLI commands, which simplifies merge conflicts and keeps the file structure predictable. @@ -154,12 +150,12 @@ when a facet selection or catalog version is updated. ## CLI Commands ``` -ade setup Interactive TUI: select agent, walk through facets, - write config.yaml + config.lock.yaml + agent files. - Agent selection is a setup-time choice, not stored in config. +ade setup Interactive TUI: walk through facets, confirm docsets, + write config.yaml + config.lock.yaml + agent files, + install skills and knowledge sources. ade install Re-resolve config.yaml → config.lock.yaml → agent files. - Non-interactive. Idempotent. Requires --agent flag. + Non-interactive. Idempotent. ade add Add or change a single facet selection interactively. @@ -178,7 +174,7 @@ naturally with the ADE package. ## V1 Catalog -Four facets ship in v1: +Three facets ship in v1: ### 1. Process Guidance (`process`) @@ -189,36 +185,41 @@ How the agent receives workflow and process instructions. | `codemcp-workflows` | Uses `@codemcp/workflows-server` MCP for structured EPCC workflows | | `native-agents-md` | Uses `AGENTS.md` with inline EPCC instructions (no MCP dependency) | -### 2. Conventions (`conventions`) +### 2. Architecture (`architecture`) + +Stack and framework conventions that shape the project structure. -How project-specific skills and standards are delivered. +| Option | Description | +| ---------- | ---------------------------------------------------------------- | +| `tanstack` | Full-stack conventions for TanStack (Router, Query, Form, Table) | -| Option | Description | -| ---------------- | ----------------------------------------------------- | -| `codemcp-skills` | Uses `@codemcp/skills` MCP for dynamic skill delivery | -| `native-skills` | Installs skills as static files in the project | +Each architecture option carries inline skills (conventions, design patterns, +code style, testing) and recommended docsets (git repos for each library's +documentation). -### 3. Documentation (`documentation`) +### 3. Practices (`practices`) — multi-select -How reference documentation is made available to the agent. +Composable development practices. Multiple selections allowed. -| Option | Description | -| --------------- | --------------------------------------------------------- | -| `knowledge-mcp` | Uses `@codemcp/knowledge-server` MCP with managed docsets | -| `web-search` | Relies on agent's built-in web search capability | +| Option | Description | +| ---------------------- | ------------------------------------------------------------------ | +| `conventional-commits` | Structured commit messages following the Conventional Commits spec | +| `tdd-london` | London-school (mockist) Test-Driven Development | +| `adr-nygard` | Architecture Decision Records following Nygard's template | -### 4. Development Frameworks (`frameworks`) — multi-select +Practices with associated documentation (e.g. Conventional Commits) carry +docsets that are collected alongside architecture docsets. -Which tech stacks the project uses. Multiple selections allowed. -Provisions install framework-specific knowledge sources, skills, and -instructions. +### Documentation Layer (derived) -| Option | Description | -| -------------- | ------------------------- | -| `react` | React frontend framework | -| `vue` | Vue.js frontend framework | -| `java-spring` | Java Spring Boot backend | -| `node-express` | Node.js Express backend | +Documentation is **not** a standalone facet. Instead, each option in +architecture and practices declares recommended `docsets[]`. The setup TUI +collects all implied docsets and presents them as an opt-out confirmation +step. Accepted docsets become `knowledge_sources` in LogicalConfig, which +triggers: + +1. Automatic addition of the `@codemcp/knowledge-server` MCP server entry +2. Installation via `@codemcp/knowledge` API (`createDocset` + `initDocset`) ## Non-Goals (initial release) @@ -245,3 +246,10 @@ instructions. 5. **User edits are confined to `custom`.** The rest of `config.yaml` is CLI-managed, eliminating merge conflicts in the structured sections. + +6. **Docsets are a weak entity on Option, not a separate facet.** Documentation + sources are always implied by an upstream selection. Making documentation a + standalone facet would create a hollow indirection whose options just mirror + upstream choices 1:1. Config stores `excluded_docsets` (what the user opted + out of) rather than selected docsets, keeping the common case (accept all + recommendations) zero-config. diff --git a/docs/CLI-design.md b/docs/CLI-design.md index 1defcb3..93e7e7f 100644 --- a/docs/CLI-design.md +++ b/docs/CLI-design.md @@ -23,21 +23,18 @@ core/src/ resolver.ts # config + catalog → LogicalConfig registry.ts # writer registry (provision + agent) catalog/ - index.ts # catalog registry, exports all facets + index.ts # catalog assembly, exports all facets facets/ - process.ts - conventions.ts - documentation.ts - frameworks.ts + process.ts # workflow delivery method + architecture.ts # stack-specific conventions (e.g. TanStack) + practices.ts # composable practices (commits, TDD, ADR) writers/ # built-in provision writers workflows.ts skills.ts knowledge.ts - mcp-server.ts instruction.ts - installable.ts agents/ # built-in agent writers - opencode.ts + claude-code.ts # AGENTS.md, .claude/settings.json, skill files ``` ### `@ade/cli` (`packages/cli`) @@ -47,15 +44,12 @@ lives in core; CLI commands are thin handlers that parse args and delegate. ``` cli/src/ - index.ts # entry point, arg parser, command routing + index.ts # entry point, arg parser, command routing + skills-installer.ts # calls @codemcp/skills API to install skills + knowledge-installer.ts # calls @codemcp/knowledge API to install docsets commands/ - setup.ts # interactive TUI setup - install.ts # resolve + generate (idempotent) - add.ts # modify single facet - remove.ts # remove facet selection - status.ts # show current state - tui/ - prompts.ts # interactive facet selection UI + setup.ts # interactive TUI setup + install.ts # resolve + generate (idempotent) ``` `@ade/cli` depends on `@ade/core`. Nothing depends on `@ade/cli`. @@ -63,28 +57,28 @@ cli/src/ ## Architecture Overview ``` -┌─────────────────────────────────────────────────────────────┐ -│ @ade/cli │ -│ ade setup · ade install · ade add · ade remove · ade status │ -│ TUI prompts │ -└──────────────────────────┬──────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────┐ +│ @ade/cli │ +│ ade setup · ade install │ +│ TUI prompts · skills-installer · knowledge-installer │ +└──────────────────────────┬───────────────────────────────────┘ │ delegates to -┌──────────────────────────▼──────────────────────────────────┐ -│ @ade/core │ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌────────────────────────┐ │ -│ │ Catalog │──▶│ Resolver │──▶│ Writer Registry │ │ -│ │ (facets) │ │ │ │ │ │ -│ └──────────┘ └────┬─────┘ │ provision: Map │ │ -│ │ │ agents: Map │ │ -│ ▼ └───────────┬────────────┘ │ -│ ┌──────────────┐ │ │ -│ │ LogicalConfig│◀────────────┘ │ -│ └──────┬───────┘ merge fragments │ -│ │ │ -│ ▼ │ -│ agent-specific files │ -└─────────────────────────────────────────────────────────────┘ +┌──────────────────────────▼───────────────────────────────────┐ +│ @ade/core │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌────────────────────────┐ │ +│ │ Catalog │──▶│ Resolver │──▶│ Writer Registry │ │ +│ │ (facets) │ │ │ │ │ │ +│ └──────────┘ └────┬─────┘ │ provision: Map │ │ +│ │ │ agents: Map │ │ +│ ▼ └───────────┬────────────┘ │ +│ ┌──────────────┐ │ │ +│ │ LogicalConfig│◀────────────┘ │ +│ └──────┬───────┘ merge fragments │ +│ │ │ +│ ▼ │ +│ agent-specific files │ +└──────────────────────────────────────────────────────────────┘ ``` ## Data Flow @@ -132,24 +126,25 @@ not stored in `config.yaml`. There is no auto-detection. This keeps the config agent-agnostic — the same choices can produce output for any supported agent. -### 3. Package API calls from provision writers +### 3. Package API calls from CLI installers -Some provisions (notably `skills` and `knowledge`) delegate to sibling -packages. ADE imports them as TypeScript dependencies rather than shelling -out, giving type safety and avoiding CLI flag contracts. +Skills and knowledge installation delegates to sibling packages. ADE imports +them as TypeScript dependencies rather than shelling out, giving type safety +and avoiding CLI flag contracts. ``` -provision {writer: "skills", config: {name: "design", version: "1.0"}} - → import { install } from "@codemcp/skills" - → install({name: "design", version: "1.0"}) - → skills package manages its own files - → may return a LogicalConfig fragment (e.g. MCP server entry) - -provision {writer: "knowledge", config: {name: "tanstack", origin: "https://..."}} - → import { addSource } from "@codemcp/knowledge" - → addSource({name: "tanstack", origin: "https://..."}) - → knowledge package manages docset artifacts - → LogicalConfig gets a knowledge_sources entry +skills-installer: + → import { runAdd } from "@codemcp/skills/api" + → for each skill: runAdd([source], { yes: true, all: true }) + → skills package writes SKILL.md files and skills-lock.json + +knowledge-installer: + → import { createDocset, initDocset } + from "@codemcp/knowledge/packages/cli/dist/exports.js" + → for each knowledge_source: + createDocset({ id, name, preset: "git-repo", url: origin }, { cwd }) + initDocset({ docsetId: id, cwd }) + → knowledge package manages .knowledge/ directory and docset artifacts ``` Where direct import is impractical (e.g. the dependency isn't TypeScript or @@ -363,18 +358,16 @@ writers. The CLI calls this at startup. A future plugin would call ```typescript function createDefaultRegistry(): WriterRegistry { - const provisions = new Map(); - provisions.set("workflows", workflowsWriter); - provisions.set("skills", skillsWriter); - provisions.set("knowledge", knowledgeWriter); - provisions.set("mcp-server", mcpServerWriter); - provisions.set("instruction", instructionWriter); - provisions.set("installable", installableWriter); - - const agents = new Map(); - agents.set("opencode", opencodeWriter); - - return { provisions, agents }; + const registry = createRegistry(); + + registerProvisionWriter(registry, instructionWriter); + registerProvisionWriter(registry, workflowsWriter); + registerProvisionWriter(registry, skillsWriter); + registerProvisionWriter(registry, knowledgeWriter); + + registerAgentWriter(registry, claudeCodeWriter); + + return registry; } ``` @@ -397,34 +390,23 @@ For reference, the typed configs used internally by built-in writers: ```typescript interface WorkflowsConfig { package: string; + ref?: string; env?: Record; } interface SkillsConfig { - name: string; - version?: string; + skills: SkillDefinition[]; } interface KnowledgeConfig { name: string; - origin: string; -} - -interface McpServerConfig { - ref: string; - command: string; - args: string[]; - env?: Record; + origin: string; // must be a valid .git URL + description: string; } interface InstructionConfig { text: string; } - -interface InstallableConfig { - command: string; - check?: string; -} ``` These are not exported as part of the public contract. They are @@ -436,12 +418,16 @@ Each agent writer implements `AgentWriterDef`. The writer has full ownership of how to translate LogicalConfig into agent-specific files. It reads existing files when needed to perform incremental updates. -### OpenCode Writer (v1) +### Claude Code Writer (v1) + +Produces agent-specific config files for Claude Code: -Produces agent-specific config files for OpenCode. Exact output format TBD -based on OpenCode's config specification. +- **`AGENTS.md`** — ADE-managed section with resolved instructions +- **`.claude/settings.json`** — MCP server entries (merged with existing) +- **`.ade/skills//SKILL.md`** — Inline skill files (staging area for + `@codemcp/skills` installation) -Future agent writers: Claude Code, Copilot, Kiro. +Future agent writers: OpenCode, Copilot, Kiro. ### ADE-Managed Section Delimiters @@ -480,22 +466,32 @@ Produces: one `McpServerEntry` with `command: "npx"`, ### `skills` writer ```typescript -{ name: "design", version: "1.0" } +{ + skills: [ + { name: "tanstack-architecture", description: "...", body: "..." }, + { + name: "playwright-cli", + source: "microsoft/playwright-cli/skills/playwright-cli" + } + ]; +} ``` -Calls `@codemcp/skills` API to install. May also produce an `McpServerEntry` -if the skills MCP server needs to be registered. +Passes skill definitions (inline or external) through to LogicalConfig. +Inline skills include a `body` field; external skills reference a `source`. +The actual installation (writing SKILL.md files and calling `@codemcp/skills` +API) is handled by the agent writer and CLI's `skills-installer`. ### `knowledge` writer ```typescript -{ name: "tanstack", origin: "https://tanstack.com/query/latest/docs" } +{ name: "tanstack-query-docs", origin: "https://github.com/TanStack/query.git", description: "Server state management" } ``` -Calls `@codemcp/knowledge` API to add the source. Produces a -`KnowledgeSource` entry so agent writers can reference it. The knowledge -package manages the physical docset artifacts; multiple sources may be -combined into one docset by `@codemcp/knowledge-server` at runtime. +Produces a `KnowledgeSource` entry in LogicalConfig. The actual installation +(calling `@codemcp/knowledge` API) is handled by the CLI's +`knowledge-installer`. Origins must be valid `.git` URLs for the `git-repo` +preset. ### `mcp-server` writer @@ -566,31 +562,57 @@ export const processFacet: Facet = { ] }; -// catalog/facets/frameworks.ts -export const frameworksFacet: Facet = { - id: "frameworks", - label: "Development Frameworks", - description: "Which tech stacks the project uses", +// catalog/facets/architecture.ts — options carry skills + docsets +export const architectureFacet: Facet = { + id: "architecture", + label: "Architecture", + description: + "Stack and framework conventions that shape your project structure", required: false, - multiSelect: true, - dependsOn: ["conventions"], // skills may vary by framework options: [ { - id: "react", - label: "React", - description: "React frontend framework", + id: "tanstack", + label: "TanStack", + description: + "Full-stack conventions for TanStack (Router, Query, Form, Table)", recipe: [ { - writer: "knowledge", - config: { name: "react", origin: "https://react.dev/reference" } + writer: "skills", + config: { + skills: [ + { + name: "tanstack-architecture", + description: "...", + body: "..." + }, + { + name: "playwright-cli", + source: "microsoft/playwright-cli/skills/playwright-cli" + } + ] + } }, { writer: "instruction", - config: { text: "This project uses React..." } + config: { text: "This project follows TanStack conventions..." } + } + ], + docsets: [ + { + id: "tanstack-router-docs", + label: "TanStack Router", + origin: "https://github.com/TanStack/router.git", + description: "File-based routing, loaders, and search params" + }, + { + id: "tanstack-query-docs", + label: "TanStack Query", + origin: "https://github.com/TanStack/query.git", + description: "Server state management, caching, and mutations" } + // ... form, table ] } - // ... vue, java-spring, node-express ] }; ``` @@ -618,10 +640,11 @@ export const frameworksFacet: Facet = { Kept inside core for now; extractable to a separate package later along the `Catalog` interface seam. -5. **Direct package imports over CLI subprocesses.** Provision writers for - `skills` and `knowledge` import `@codemcp/skills` and `@codemcp/knowledge` - as TypeScript dependencies and call their APIs. CLI subprocess invocation - is the fallback for non-TypeScript or cross-runtime cases. +5. **Direct package imports over CLI subprocesses.** The CLI's installers + import `@codemcp/skills` and `@codemcp/knowledge` as TypeScript + dependencies and call their programmatic APIs (`runAdd`, `createDocset`, + `initDocset`). CLI subprocess invocation is the fallback for + non-TypeScript or cross-runtime cases. 6. **`custom` section isolates user edits.** Only the `custom` block in `config.yaml` is user-managed. The rest is CLI-managed. This eliminates @@ -634,7 +657,7 @@ export const frameworksFacet: Facet = { indirection whose options just mirror upstream choices 1:1. Instead, each `Option` declares its recommended `docsets[]`. The resolver collects and deduplicates them; the TUI presents them as a confirmation step (opt-out, - not opt-in). Users who want arbitrary docs not tied to a catalog option - use `custom.knowledge_sources` instead. Config stores `excluded_docsets` - (what the user opted out of) rather than selected docsets, keeping the - common case (accept all recommendations) zero-config. + not opt-in). Config stores `excluded_docsets` (what the user opted out of) + rather than selected docsets, keeping the common case (accept all + recommendations) zero-config. When knowledge sources are present, the + resolver automatically adds a `@codemcp/knowledge-server` MCP server entry. From a5456f55bd2146886d9d7f94334362639625394e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 21:57:45 +0000 Subject: [PATCH 43/60] refactor: install reads lock file only, setup re-runs with defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit install command: - Reads config.lock.yaml directly instead of re-resolving from config.yaml - No longer writes the lock file — treats it as immutable input - Fails with clear message when lock file is missing setup command: - Reads existing config.yaml and pre-selects previous choices as defaults (initialValue for select, initialValues for multiselect) - Warns when a previous selection references an option no longer in catalog - Stale options are silently dropped from initialValue (not pre-selected) Removed from docs and CLI help: - ade add / ade remove / ade status commands (re-run setup instead) - dependsOn facet dependencies (not implemented, not needed) - installable writer and CliAction type (vestigial) - Updated LogicalConfig to show skills[] instead of cli_actions[] https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- docs/CLI-PRD.md | 16 +-- docs/CLI-design.md | 87 ++++++--------- .../src/commands/install.integration.spec.ts | 28 +++-- packages/cli/src/commands/install.spec.ts | 102 ++++++++--------- packages/cli/src/commands/install.ts | 29 +---- packages/cli/src/commands/setup.spec.ts | 104 +++++++++++++++++- packages/cli/src/commands/setup.ts | 76 ++++++++++--- packages/cli/src/index.ts | 4 +- 8 files changed, 277 insertions(+), 169 deletions(-) diff --git a/docs/CLI-PRD.md b/docs/CLI-PRD.md index 1fde443..977eef9 100644 --- a/docs/CLI-PRD.md +++ b/docs/CLI-PRD.md @@ -77,9 +77,7 @@ carries writer-specific config. Provision types: | `workflows` | MCP server entry for `@codemcp/workflows-server` | | `skills` | Skill definitions (inline or external) for `@codemcp/skills` | | `knowledge` | Knowledge source entry for `@codemcp/knowledge` | -| `mcp-server` | Generic MCP server entry (command + args + env) | | `instruction` | Raw instruction text for the agent | -| `installable` | CLI tool or dependency to be installed | ### KnowledgeSource @@ -153,15 +151,13 @@ when a facet selection or catalog version is updated. ade setup Interactive TUI: walk through facets, confirm docsets, write config.yaml + config.lock.yaml + agent files, install skills and knowledge sources. + Re-running setup on an existing project pre-selects + previous choices as defaults. Warns if a previous + selection references an option no longer in the catalog. -ade install Re-resolve config.yaml → config.lock.yaml → agent files. - Non-interactive. Idempotent. - -ade add Add or change a single facet selection interactively. - -ade remove Remove a facet selection. - -ade status Show current selections and what would change on install. +ade install Apply config.lock.yaml → agent files + skills + knowledge. + Non-interactive. Idempotent. Does not re-resolve — uses + the lock file as-is. ``` ## Catalog diff --git a/docs/CLI-design.md b/docs/CLI-design.md index 93e7e7f..81a981a 100644 --- a/docs/CLI-design.md +++ b/docs/CLI-design.md @@ -83,44 +83,48 @@ cli/src/ ## Data Flow -### 1. Resolution: config.yaml → LogicalConfig +### 1. Setup: TUI → config.yaml + config.lock.yaml + agent files ``` -read config.yaml - → topologically sort facets by dependsOn - → for each facet (in dependency order): - → if facet has unmet dependencies (via `ade add`), prompt for them first - → look up selected option(s) in catalog - (single-select: one option; multi-select: list of options) - → build ResolutionContext from already-resolved dependent facets - → for each selected option, collect all provisions from its recipe - → for each provision, invoke the writer with (config, context) - → each writer returns a LogicalConfig fragment - → record facet as resolved +read existing config.yaml (if any) for default selections + → walk facets interactively: + → pre-select previous choice as default (if still valid) + → warn if previous choice references a stale option + → collect new user choices → collect docsets from all selected options - → deduplicate docsets by id (first wins) - → filter out docsets listed in excluded_docsets - → map enabled docsets to knowledge_sources entries - → merge custom section from config.yaml - → merge all fragments into one LogicalConfig - → write config.lock.yaml (serialized LogicalConfig) + → present docset confirmation (opt-out multiselect) + → resolve choices + catalog → LogicalConfig + → write config.yaml (user choices) + → write config.lock.yaml (resolved LogicalConfig snapshot) + → run agent writer (generate AGENTS.md, settings.json, etc.) + → install skills via @codemcp/skills API + → install knowledge via @codemcp/knowledge API ``` +Resolution expands each selected option's recipe provisions into +LogicalConfig fragments, deduplicates docsets by id, filters by +`excluded_docsets`, maps enabled docsets to `knowledge_sources`, adds the +`@codemcp/knowledge-server` MCP entry if knowledge sources are present, +merges the custom section, and deduplicates MCP servers by ref. + For **multi-select facets**, each selected option's recipe is resolved -independently and their LogicalConfig fragments are merged. This means -selecting both `react` and `node-express` in the frameworks facet produces -the union of both recipes' provisions. +independently and their LogicalConfig fragments are merged. -### 2. Generation: LogicalConfig → agent files +### 2. Install: config.lock.yaml → agent files (idempotent) ``` -read config.lock.yaml (or use in-memory LogicalConfig) - → select agent writer from --agent flag (no auto-detection) - → writer reads current agent files (if any) for merge/update - → writer produces updated agent-specific files - → write files to disk +read config.lock.yaml + → select agent writer (default: claude-code) + → apply logical_config from lock file (no re-resolution) + → run agent writer + → install skills + → install knowledge ``` +`ade install` does **not** re-resolve from `config.yaml`. It treats the +lock file as the source of truth, like `npm ci` treats `package-lock.json`. +To change selections, re-run `ade setup`. + The target agent is a **generation-time parameter** (`--agent` flag), not stored in `config.yaml`. There is no auto-detection. This keeps the config agent-agnostic — the same choices can produce output for any @@ -168,7 +172,6 @@ interface Facet { description: string; required: boolean; // false = skippable multiSelect?: boolean; // true = user can pick multiple options - dependsOn?: string[]; // facet IDs this facet depends on options: Option[]; } @@ -207,15 +210,10 @@ interface Provision { config: Record; // writer-specific, validated at boundary } -// Passed to provision writers so they can adapt based on sibling selections. -// Only contains resolved options from facets declared in dependsOn. +// Passed to provision writers for future cross-facet context. +// Currently passed as { resolved: {} }. interface ResolutionContext { - resolved: Record; // facet_id → resolved info -} - -interface ResolvedFacet { - optionId: string; - option: Option; + resolved: Record; } ``` @@ -225,7 +223,7 @@ interface ResolvedFacet { interface LogicalConfig { mcp_servers: McpServerEntry[]; instructions: string[]; - cli_actions: CliAction[]; + skills: SkillDefinition[]; knowledge_sources: KnowledgeSource[]; } @@ -236,12 +234,6 @@ interface McpServerEntry { env: Record; } -interface CliAction { - command: string; - args: string[]; - phase: "setup" | "install"; // when to run -} - interface KnowledgeSource { name: string; // e.g. "tanstack" origin: string; // URL, path, or package ref @@ -511,15 +503,6 @@ Pass-through: produces one `McpServerEntry` directly. Produces: one `instructions` entry. -### `installable` writer - -```typescript -{ command: "pnpm", check: "pnpm --version" } -``` - -Produces: one `CliAction` for validation/installation of a CLI tool or -dependency. - ## V1 Catalog (TypeScript) Example of how the catalog is defined in code: diff --git a/packages/cli/src/commands/install.integration.spec.ts b/packages/cli/src/commands/install.integration.spec.ts index 82b4bec..2fffe29 100644 --- a/packages/cli/src/commands/install.integration.spec.ts +++ b/packages/cli/src/commands/install.integration.spec.ts @@ -18,7 +18,6 @@ vi.mock("@clack/prompts", () => ({ import * as clack from "@clack/prompts"; import { runSetup } from "./setup.js"; import { runInstall } from "./install.js"; -import { readLockFile } from "@ade/core"; import { getDefaultCatalog } from "../../../core/src/catalog/index.js"; describe("install integration (real temp dir)", () => { @@ -33,10 +32,10 @@ describe("install integration (real temp dir)", () => { await rm(dir, { recursive: true, force: true }); }); - it("re-resolves from existing config.yaml and regenerates agent files", async () => { + it("applies lock file to regenerate agent files without re-resolving", async () => { const catalog = getDefaultCatalog(); - // Step 1: Run setup to create config.yaml + // Step 1: Run setup to create config.yaml + config.lock.yaml vi.mocked(clack.select) .mockResolvedValueOnce("codemcp-workflows") // process .mockResolvedValueOnce("__skip__"); // architecture @@ -46,7 +45,7 @@ describe("install integration (real temp dir)", () => { await rm(join(dir, "AGENTS.md")); await rm(join(dir, ".claude"), { recursive: true, force: true }); - // Step 3: Run install — should regenerate from config.yaml + // Step 3: Run install — should regenerate from config.lock.yaml await runInstall(dir, "claude-code"); // Agent files should be back @@ -62,7 +61,7 @@ describe("install integration (real temp dir)", () => { }); }); - it("updates lock file on install", async () => { + it("does not modify the lock file", async () => { const catalog = getDefaultCatalog(); // Setup first @@ -71,23 +70,22 @@ describe("install integration (real temp dir)", () => { .mockResolvedValueOnce("__skip__"); // architecture await runSetup(dir, catalog); - const lockBefore = await readLockFile(dir); - - // Small delay so timestamp differs - await new Promise((r) => setTimeout(r, 10)); + const lockRawBefore = await readFile( + join(dir, "config.lock.yaml"), + "utf-8" + ); // Re-install await runInstall(dir, "claude-code"); - const lockAfter = await readLockFile(dir); - expect(lockAfter).not.toBeNull(); - expect(lockAfter!.generated_at).not.toBe(lockBefore!.generated_at); - expect(lockAfter!.logical_config).toEqual(lockBefore!.logical_config); + const lockRawAfter = await readFile(join(dir, "config.lock.yaml"), "utf-8"); + // Lock file should be byte-identical (install doesn't rewrite it) + expect(lockRawAfter).toBe(lockRawBefore); }); - it("fails when no config.yaml exists", async () => { + it("fails when no config.lock.yaml exists", async () => { await expect(runInstall(dir, "claude-code")).rejects.toThrow( - /config\.yaml not found/i + /config\.lock\.yaml not found/i ); }); diff --git a/packages/cli/src/commands/install.spec.ts b/packages/cli/src/commands/install.spec.ts index f03949a..617445c 100644 --- a/packages/cli/src/commands/install.spec.ts +++ b/packages/cli/src/commands/install.spec.ts @@ -13,19 +13,19 @@ vi.mock("@codemcp/skills/api", () => ({ runAdd: vi.fn() })); +const mockLogical: LogicalConfig = { + mcp_servers: [], + instructions: ["test instruction"], + cli_actions: [], + knowledge_sources: [], + skills: [] +}; + vi.mock("@ade/core", async (importOriginal) => { const actual = (await importOriginal()) as typeof import("@ade/core"); return { ...actual, - readUserConfig: vi.fn(), - writeLockFile: vi.fn().mockResolvedValue(undefined), - resolve: vi.fn().mockResolvedValue({ - mcp_servers: [], - instructions: ["test instruction"], - cli_actions: [], - knowledge_sources: [], - skills: [] - } satisfies LogicalConfig), + readLockFile: vi.fn(), getAgentWriter: vi.fn().mockReturnValue({ id: "claude-code", install: vi.fn().mockResolvedValue(undefined) @@ -34,12 +34,7 @@ vi.mock("@ade/core", async (importOriginal) => { }); import * as clack from "@clack/prompts"; -import { - readUserConfig, - writeLockFile, - resolve, - getAgentWriter -} from "@ade/core"; +import { readLockFile, getAgentWriter } from "@ade/core"; import { runInstall } from "./install.js"; // ── Tests ──────────────────────────────────────────────────────────────────── @@ -49,23 +44,21 @@ describe("runInstall", () => { vi.clearAllMocks(); }); - it("reads config.yaml and resolves to logical config", async () => { - vi.mocked(readUserConfig).mockResolvedValueOnce({ - choices: { process: "codemcp-workflows" } + it("reads config.lock.yaml and applies logical config", async () => { + vi.mocked(readLockFile).mockResolvedValueOnce({ + version: 1, + generated_at: "2024-01-01T00:00:00.000Z", + choices: { process: "codemcp-workflows" }, + logical_config: mockLogical }); await runInstall("/tmp/project", "claude-code"); - expect(readUserConfig).toHaveBeenCalledWith("/tmp/project"); - expect(resolve).toHaveBeenCalledOnce(); - const resolveArgs = vi.mocked(resolve).mock.calls[0]; - expect(resolveArgs[0]).toMatchObject({ - choices: { process: "codemcp-workflows" } - }); + expect(readLockFile).toHaveBeenCalledWith("/tmp/project"); }); - it("writes lock file with resolved config", async () => { - const mockLogical: LogicalConfig = { + it("does not re-resolve — uses lock file logical_config directly", async () => { + const lockedConfig: LogicalConfig = { mcp_servers: [ { ref: "workflows", @@ -79,31 +72,35 @@ describe("runInstall", () => { skills: [], knowledge_sources: [] }; - vi.mocked(readUserConfig).mockResolvedValueOnce({ - choices: { process: "codemcp-workflows" } + vi.mocked(readLockFile).mockResolvedValueOnce({ + version: 1, + generated_at: "2024-01-01T00:00:00.000Z", + choices: { process: "codemcp-workflows" }, + logical_config: lockedConfig + }); + + const mockInstall = vi.fn().mockResolvedValue(undefined); + vi.mocked(getAgentWriter).mockReturnValueOnce({ + id: "claude-code", + install: mockInstall }); - vi.mocked(resolve).mockResolvedValueOnce(mockLogical); await runInstall("/tmp/project", "claude-code"); - expect(writeLockFile).toHaveBeenCalledWith( - "/tmp/project", - expect.objectContaining({ - version: 1, - choices: { process: "codemcp-workflows" }, - logical_config: mockLogical - }) - ); + expect(mockInstall).toHaveBeenCalledWith(lockedConfig, "/tmp/project"); }); - it("calls agent writer install with resolved config", async () => { + it("calls agent writer install with lock file config", async () => { const mockInstall = vi.fn().mockResolvedValue(undefined); vi.mocked(getAgentWriter).mockReturnValueOnce({ id: "claude-code", install: mockInstall }); - vi.mocked(readUserConfig).mockResolvedValueOnce({ - choices: { process: "codemcp-workflows" } + vi.mocked(readLockFile).mockResolvedValueOnce({ + version: 1, + generated_at: "2024-01-01T00:00:00.000Z", + choices: { process: "codemcp-workflows" }, + logical_config: mockLogical }); await runInstall("/tmp/project", "claude-code"); @@ -112,23 +109,23 @@ describe("runInstall", () => { expect.anything(), "claude-code" ); - expect(mockInstall).toHaveBeenCalledWith( - expect.objectContaining({ instructions: expect.any(Array) }), - "/tmp/project" - ); + expect(mockInstall).toHaveBeenCalledWith(mockLogical, "/tmp/project"); }); - it("throws when config.yaml is missing", async () => { - vi.mocked(readUserConfig).mockResolvedValueOnce(null); + it("throws when config.lock.yaml is missing", async () => { + vi.mocked(readLockFile).mockResolvedValueOnce(null); await expect(runInstall("/tmp/project", "claude-code")).rejects.toThrow( - /config\.yaml not found/i + /config\.lock\.yaml not found/i ); }); it("throws when agent writer is unknown", async () => { - vi.mocked(readUserConfig).mockResolvedValueOnce({ - choices: { process: "codemcp-workflows" } + vi.mocked(readLockFile).mockResolvedValueOnce({ + version: 1, + generated_at: "2024-01-01T00:00:00.000Z", + choices: { process: "codemcp-workflows" }, + logical_config: mockLogical }); vi.mocked(getAgentWriter).mockReturnValueOnce(undefined); @@ -138,8 +135,11 @@ describe("runInstall", () => { }); it("shows intro and outro messages", async () => { - vi.mocked(readUserConfig).mockResolvedValueOnce({ - choices: { process: "codemcp-workflows" } + vi.mocked(readLockFile).mockResolvedValueOnce({ + version: 1, + generated_at: "2024-01-01T00:00:00.000Z", + choices: { process: "codemcp-workflows" }, + logical_config: mockLogical }); await runInstall("/tmp/project", "claude-code"); diff --git a/packages/cli/src/commands/install.ts b/packages/cli/src/commands/install.ts index c9aacc4..7808e5a 100644 --- a/packages/cli/src/commands/install.ts +++ b/packages/cli/src/commands/install.ts @@ -1,13 +1,5 @@ import * as clack from "@clack/prompts"; -import { - readUserConfig, - writeLockFile, - resolve, - createDefaultRegistry, - getAgentWriter, - getDefaultCatalog, - type LockFile -} from "@ade/core"; +import { readLockFile, createDefaultRegistry, getAgentWriter } from "@ade/core"; import { installSkills } from "../skills-installer.js"; import { installKnowledge } from "../knowledge-installer.js"; @@ -17,30 +9,19 @@ export async function runInstall( ): Promise { clack.intro("ade install"); - const userConfig = await readUserConfig(projectRoot); - if (!userConfig) { - throw new Error( - "config.yaml not found. Run `ade setup` first to create one." - ); + const lockFile = await readLockFile(projectRoot); + if (!lockFile) { + throw new Error("config.lock.yaml not found. Run `ade setup` first."); } const registry = createDefaultRegistry(); - const catalog = getDefaultCatalog(); const agentWriter = getAgentWriter(registry, agent); if (!agentWriter) { throw new Error(`Unknown agent "${agent}". Available: claude-code`); } - const logicalConfig = await resolve(userConfig, catalog, registry); - - const lockFile: LockFile = { - version: 1, - generated_at: new Date().toISOString(), - choices: userConfig.choices, - logical_config: logicalConfig - }; - await writeLockFile(projectRoot, lockFile); + const logicalConfig = lockFile.logical_config; await agentWriter.install(logicalConfig, projectRoot); diff --git a/packages/cli/src/commands/setup.spec.ts b/packages/cli/src/commands/setup.spec.ts index 101b288..ce60b14 100644 --- a/packages/cli/src/commands/setup.spec.ts +++ b/packages/cli/src/commands/setup.spec.ts @@ -11,6 +11,7 @@ vi.mock("@clack/prompts", () => ({ confirm: vi.fn(), isCancel: vi.fn().mockReturnValue(false), cancel: vi.fn(), + log: { warn: vi.fn(), info: vi.fn() }, spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) })); @@ -22,6 +23,7 @@ vi.mock("@ade/core", async (importOriginal) => { const actual = (await importOriginal()) as typeof import("@ade/core"); return { ...actual, + readUserConfig: vi.fn().mockResolvedValue(null), writeUserConfig: vi.fn().mockResolvedValue(undefined), writeLockFile: vi.fn().mockResolvedValue(undefined), resolve: vi.fn().mockResolvedValue({ @@ -40,7 +42,12 @@ vi.mock("@ade/core", async (importOriginal) => { }); import * as clack from "@clack/prompts"; -import { writeUserConfig, writeLockFile, resolve } from "@ade/core"; +import { + readUserConfig, + writeUserConfig, + writeLockFile, + resolve +} from "@ade/core"; import { runSetup } from "./setup.js"; // ── Test catalog fixture ───────────────────────────────────────────────────── @@ -280,4 +287,99 @@ describe("runSetup", () => { expect(clack.intro).toHaveBeenCalled(); expect(clack.outro).toHaveBeenCalled(); }); + + describe("re-run with existing config", () => { + it("passes existing single-select choice as initialValue", async () => { + vi.mocked(readUserConfig).mockResolvedValueOnce({ + choices: { process: "workflow-b", testing: "jest" } + }); + + vi.mocked(clack.select) + .mockResolvedValueOnce("workflow-b") + .mockResolvedValueOnce("jest"); + + await runSetup("/tmp/test-project", testCatalog); + + // First select (process) should receive initialValue "workflow-b" + expect(clack.select).toHaveBeenCalledWith( + expect.objectContaining({ initialValue: "workflow-b" }) + ); + // Second select (testing) should receive initialValue "jest" + expect(clack.select).toHaveBeenCalledWith( + expect.objectContaining({ initialValue: "jest" }) + ); + }); + + it("passes existing multi-select choices as initialValues", async () => { + const multiCatalog: Catalog = { + facets: [ + { + id: "practices", + label: "Practices", + description: "Dev practices", + required: false, + multiSelect: true, + options: [ + { + id: "tdd", + label: "TDD", + description: "Test-driven dev", + recipe: [] + }, + { + id: "adr", + label: "ADR", + description: "Architecture decisions", + recipe: [] + } + ] + } + ] + }; + + vi.mocked(readUserConfig).mockResolvedValueOnce({ + choices: { practices: ["tdd", "adr"] } + }); + + vi.mocked(clack.multiselect).mockResolvedValueOnce(["tdd", "adr"]); + + await runSetup("/tmp/test-project", multiCatalog); + + expect(clack.multiselect).toHaveBeenCalledWith( + expect.objectContaining({ initialValues: ["tdd", "adr"] }) + ); + }); + + it("warns when existing choice references a stale option", async () => { + vi.mocked(readUserConfig).mockResolvedValueOnce({ + choices: { process: "workflow-a", testing: "mocha" } // "mocha" doesn't exist + }); + + vi.mocked(clack.select) + .mockResolvedValueOnce("workflow-a") + .mockResolvedValueOnce("vitest"); + + await runSetup("/tmp/test-project", testCatalog); + + expect(clack.log.warn).toHaveBeenCalledWith( + expect.stringContaining("mocha") + ); + }); + + it("does not set initialValue for stale option", async () => { + vi.mocked(readUserConfig).mockResolvedValueOnce({ + choices: { process: "deleted-option" } + }); + + vi.mocked(clack.select) + .mockResolvedValueOnce("workflow-a") + .mockResolvedValueOnce("vitest"); + + await runSetup("/tmp/test-project", testCatalog); + + // First select (process) should NOT have initialValue set + const firstCall = vi.mocked(clack.select).mock.calls[0][0]; + expect(firstCall).not.toHaveProperty("initialValue"); + }); + }); }); diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 8c9e0c6..f097719 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -1,14 +1,18 @@ import * as clack from "@clack/prompts"; import { type Catalog, + type Facet, type UserConfig, type LockFile, + readUserConfig, writeUserConfig, writeLockFile, resolve, collectDocsets, createDefaultRegistry, - getAgentWriter + getAgentWriter, + getFacet, + getOption } from "@ade/core"; import { installSkills } from "../skills-installer.js"; import { installKnowledge } from "../knowledge-installer.js"; @@ -19,11 +23,29 @@ export async function runSetup( ): Promise { clack.intro("ade setup"); + const existingConfig = await readUserConfig(projectRoot); + const existingChoices = existingConfig?.choices ?? {}; + + // Warn about stale choices that reference options no longer in the catalog + for (const [facetId, value] of Object.entries(existingChoices)) { + const facet = getFacet(catalog, facetId); + if (!facet) continue; + + const ids = Array.isArray(value) ? value : [value]; + for (const optionId of ids) { + if (!getOption(facet, optionId)) { + clack.log.warn( + `Previously selected option "${optionId}" is no longer available in facet "${facet.label}".` + ); + } + } + } + const choices: Record = {}; for (const facet of catalog.facets) { if (facet.multiSelect) { - const selected = await promptMultiSelect(facet); + const selected = await promptMultiSelect(facet, existingChoices); if (typeof selected === "symbol") { clack.cancel("Setup cancelled."); return; @@ -32,7 +54,7 @@ export async function runSetup( choices[facet.id] = selected; } } else { - const selected = await promptSelect(facet); + const selected = await promptSelect(facet, existingChoices); if (typeof selected === "symbol") { clack.cancel("Setup cancelled."); return; @@ -101,11 +123,31 @@ export async function runSetup( clack.outro("Setup complete!"); } -function promptSelect(facet: { - label: string; - required: boolean; - options: { id: string; label: string; description: string }[]; -}) { +function getValidInitialValue( + facet: Facet, + existingChoices: Record +): string | undefined { + const value = existingChoices[facet.id]; + if (typeof value !== "string") return undefined; + // Only set initialValue if the option still exists in the catalog + return facet.options.some((o) => o.id === value) ? value : undefined; +} + +function getValidInitialValues( + facet: Facet, + existingChoices: Record +): string[] | undefined { + const value = existingChoices[facet.id]; + if (!Array.isArray(value)) return undefined; + // Only include options that still exist in the catalog + const valid = value.filter((v) => facet.options.some((o) => o.id === v)); + return valid.length > 0 ? valid : undefined; +} + +function promptSelect( + facet: Facet, + existingChoices: Record +) { const options = facet.options.map((o) => ({ value: o.id, label: o.label, @@ -116,25 +158,31 @@ function promptSelect(facet: { options.push({ value: "__skip__", label: "Skip", hint: "" }); } + const initialValue = getValidInitialValue(facet, existingChoices); + return clack.select({ message: facet.label, - options + options, + ...(initialValue !== undefined && { initialValue }) }); } -function promptMultiSelect(facet: { - label: string; - options: { id: string; label: string; description: string }[]; -}) { +function promptMultiSelect( + facet: Facet, + existingChoices: Record +) { const options = facet.options.map((o) => ({ value: o.id, label: o.label, hint: o.description })); + const initialValues = getValidInitialValues(facet, existingChoices); + return clack.multiselect({ message: facet.label, options, - required: false + required: false, + ...(initialValues !== undefined && { initialValues }) }); } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 60ff8c0..8830b19 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -27,10 +27,10 @@ if (command === "setup") { console.log(); console.log("Commands:"); console.log( - " setup [dir] Configure your AI agent (default: current dir)" + " setup [dir] Interactive setup wizard (re-run to change selections)" ); console.log( - " install [dir] Re-resolve config and regenerate agent files" + " install [dir] Apply lock file to generate agent files (idempotent)" ); console.log(); console.log("Options:"); From c855e2ab72bf1c0409c6f2c7ffd55b2b01ba6f75 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 05:28:44 +0000 Subject: [PATCH 44/60] feat: add multi-harness support with @ade/harnesses package Create a new published package @ade/harnesses with writers for 6 coding agent harnesses: Claude Code, Cursor, GitHub Copilot, Windsurf, Cline, and Roo Code. Each writer generates the appropriate MCP config and instruction files for its platform. Key changes: - New @ade/harnesses package with HarnessWriter interface extending AgentWriterDef with label/description metadata - Claude Code: .claude/settings.json + AGENTS.md (moved from core) - Cursor: .cursor/mcp.json + .cursor/rules/ade.mdc - GitHub Copilot: .vscode/mcp.json + .github/copilot-instructions.md + dedicated .github/agents/ade.agent.md agent definition - Windsurf: .windsurf/mcp.json + .windsurfrules - Cline: .cline/mcp.json + .clinerules - Roo Code: .roo/mcp.json + .roorules - Setup wizard now includes harness multi-select as final step - Install command supports --harness flag (comma-separated) - UserConfig and LockFile store selected harnesses - Legacy --agent flag preserved as alias https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/cli/package.json | 1 + .../commands/conventions.integration.spec.ts | 26 ++-- .../src/commands/install.integration.spec.ts | 17 ++- packages/cli/src/commands/install.spec.ts | 112 ++++++++------- packages/cli/src/commands/install.ts | 31 +++-- .../commands/knowledge.integration.spec.ts | 10 +- .../src/commands/setup.integration.spec.ts | 11 +- packages/cli/src/commands/setup.spec.ts | 67 ++++++--- packages/cli/src/commands/setup.ts | 44 +++++- packages/cli/src/index.ts | 31 ++++- packages/core/src/index.ts | 1 + packages/core/src/registry.spec.ts | 6 +- packages/core/src/registry.ts | 3 - packages/core/src/types.ts | 2 + packages/harnesses/package.json | 33 +++++ packages/harnesses/src/index.spec.ts | 39 ++++++ packages/harnesses/src/index.ts | 36 +++++ packages/harnesses/src/types.ts | 12 ++ .../harnesses/src/writers/claude-code.spec.ts | 125 +++++++++++++++++ packages/harnesses/src/writers/claude-code.ts | 110 +++++++++++++++ packages/harnesses/src/writers/cline.spec.ts | 64 +++++++++ packages/harnesses/src/writers/cline.ts | 71 ++++++++++ .../harnesses/src/writers/copilot.spec.ts | 98 +++++++++++++ packages/harnesses/src/writers/copilot.ts | 129 ++++++++++++++++++ packages/harnesses/src/writers/cursor.spec.ts | 101 ++++++++++++++ packages/harnesses/src/writers/cursor.ts | 82 +++++++++++ .../harnesses/src/writers/roo-code.spec.ts | 64 +++++++++ packages/harnesses/src/writers/roo-code.ts | 71 ++++++++++ .../harnesses/src/writers/windsurf.spec.ts | 65 +++++++++ packages/harnesses/src/writers/windsurf.ts | 75 ++++++++++ packages/harnesses/tsconfig.build.json | 8 ++ packages/harnesses/tsconfig.json | 7 + packages/harnesses/vitest.config.ts | 5 + pnpm-lock.yaml | 31 +++++ tsconfig.json | 3 +- 35 files changed, 1482 insertions(+), 109 deletions(-) create mode 100644 packages/harnesses/package.json create mode 100644 packages/harnesses/src/index.spec.ts create mode 100644 packages/harnesses/src/index.ts create mode 100644 packages/harnesses/src/types.ts create mode 100644 packages/harnesses/src/writers/claude-code.spec.ts create mode 100644 packages/harnesses/src/writers/claude-code.ts create mode 100644 packages/harnesses/src/writers/cline.spec.ts create mode 100644 packages/harnesses/src/writers/cline.ts create mode 100644 packages/harnesses/src/writers/copilot.spec.ts create mode 100644 packages/harnesses/src/writers/copilot.ts create mode 100644 packages/harnesses/src/writers/cursor.spec.ts create mode 100644 packages/harnesses/src/writers/cursor.ts create mode 100644 packages/harnesses/src/writers/roo-code.spec.ts create mode 100644 packages/harnesses/src/writers/roo-code.ts create mode 100644 packages/harnesses/src/writers/windsurf.spec.ts create mode 100644 packages/harnesses/src/writers/windsurf.ts create mode 100644 packages/harnesses/tsconfig.build.json create mode 100644 packages/harnesses/tsconfig.json create mode 100644 packages/harnesses/vitest.config.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 3576ee2..42a6e60 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "@ade/core": "workspace:*", + "@ade/harnesses": "workspace:*", "@clack/prompts": "^1.1.0", "@codemcp/skills": "^2.3.0" }, diff --git a/packages/cli/src/commands/conventions.integration.spec.ts b/packages/cli/src/commands/conventions.integration.spec.ts index 4c88350..f4d655a 100644 --- a/packages/cli/src/commands/conventions.integration.spec.ts +++ b/packages/cli/src/commands/conventions.integration.spec.ts @@ -43,7 +43,8 @@ describe("architecture and practices facets integration", () => { .mockResolvedValueOnce("tanstack"); // architecture vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none - .mockResolvedValueOnce([]); // docsets: deselect all + .mockResolvedValueOnce([]) // docsets: deselect all + .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -99,10 +100,10 @@ describe("architecture and practices facets integration", () => { vi.mocked(clack.select) .mockResolvedValueOnce("native-agents-md") // process .mockResolvedValueOnce("__skip__"); // architecture: skip - vi.mocked(clack.multiselect).mockResolvedValueOnce([ - "conventional-commits", - "tdd-london" - ]); + vi.mocked(clack.multiselect) + .mockResolvedValueOnce(["conventional-commits", "tdd-london"]) // practices + .mockResolvedValueOnce([]) // docsets: deselect all (conventional-commits has docset) + .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -148,7 +149,9 @@ describe("architecture and practices facets integration", () => { vi.mocked(clack.select) .mockResolvedValueOnce("native-agents-md") // process .mockResolvedValueOnce("__skip__"); // architecture: skip - vi.mocked(clack.multiselect).mockResolvedValueOnce(["adr-nygard"]); + vi.mocked(clack.multiselect) + .mockResolvedValueOnce(["adr-nygard"]) + .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -169,7 +172,9 @@ describe("architecture and practices facets integration", () => { vi.mocked(clack.select) .mockResolvedValueOnce("native-agents-md") // process .mockResolvedValueOnce("__skip__"); // architecture: skip - vi.mocked(clack.multiselect).mockResolvedValueOnce([]); // practices: none + vi.mocked(clack.multiselect) + .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -189,7 +194,9 @@ describe("architecture and practices facets integration", () => { vi.mocked(clack.select) .mockResolvedValueOnce("native-agents-md") // process .mockResolvedValueOnce("__skip__"); // architecture: skip - vi.mocked(clack.multiselect).mockResolvedValueOnce(["tdd-london"]); + vi.mocked(clack.multiselect) + .mockResolvedValueOnce(["tdd-london"]) + .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -210,7 +217,8 @@ describe("architecture and practices facets integration", () => { .mockResolvedValueOnce("tanstack"); // architecture vi.mocked(clack.multiselect) .mockResolvedValueOnce(["tdd-london", "conventional-commits"]) // practices - .mockResolvedValueOnce([]); // docsets: deselect all + .mockResolvedValueOnce([]) // docsets: deselect all + .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); diff --git a/packages/cli/src/commands/install.integration.spec.ts b/packages/cli/src/commands/install.integration.spec.ts index 2fffe29..dbeff28 100644 --- a/packages/cli/src/commands/install.integration.spec.ts +++ b/packages/cli/src/commands/install.integration.spec.ts @@ -9,7 +9,7 @@ vi.mock("@clack/prompts", () => ({ outro: vi.fn(), log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, select: vi.fn(), - multiselect: vi.fn().mockResolvedValue([]), + multiselect: vi.fn(), isCancel: vi.fn().mockReturnValue(false), cancel: vi.fn(), spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) @@ -39,6 +39,9 @@ describe("install integration (real temp dir)", () => { vi.mocked(clack.select) .mockResolvedValueOnce("codemcp-workflows") // process .mockResolvedValueOnce("__skip__"); // architecture + vi.mocked(clack.multiselect) + .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); // Step 2: Delete agent output files to simulate a fresh clone @@ -46,7 +49,7 @@ describe("install integration (real temp dir)", () => { await rm(join(dir, ".claude"), { recursive: true, force: true }); // Step 3: Run install — should regenerate from config.lock.yaml - await runInstall(dir, "claude-code"); + await runInstall(dir, ["claude-code"]); // Agent files should be back const agentsMd = await readFile(join(dir, "AGENTS.md"), "utf-8"); @@ -68,6 +71,9 @@ describe("install integration (real temp dir)", () => { vi.mocked(clack.select) .mockResolvedValueOnce("codemcp-workflows") // process .mockResolvedValueOnce("__skip__"); // architecture + vi.mocked(clack.multiselect) + .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); const lockRawBefore = await readFile( @@ -76,7 +82,7 @@ describe("install integration (real temp dir)", () => { ); // Re-install - await runInstall(dir, "claude-code"); + await runInstall(dir, ["claude-code"]); const lockRawAfter = await readFile(join(dir, "config.lock.yaml"), "utf-8"); // Lock file should be byte-identical (install doesn't rewrite it) @@ -96,13 +102,16 @@ describe("install integration (real temp dir)", () => { vi.mocked(clack.select) .mockResolvedValueOnce("native-agents-md") // process .mockResolvedValueOnce("__skip__"); // architecture + vi.mocked(clack.multiselect) + .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); // Delete agent output await rm(join(dir, "AGENTS.md")); // Re-install - await runInstall(dir, "claude-code"); + await runInstall(dir, ["claude-code"]); const agentsMd = await readFile(join(dir, "AGENTS.md"), "utf-8"); expect(agentsMd).toContain("AGENTS.md"); diff --git a/packages/cli/src/commands/install.spec.ts b/packages/cli/src/commands/install.spec.ts index 617445c..8c63ad2 100644 --- a/packages/cli/src/commands/install.spec.ts +++ b/packages/cli/src/commands/install.spec.ts @@ -25,23 +25,53 @@ vi.mock("@ade/core", async (importOriginal) => { const actual = (await importOriginal()) as typeof import("@ade/core"); return { ...actual, - readLockFile: vi.fn(), - getAgentWriter: vi.fn().mockReturnValue({ - id: "claude-code", - install: vi.fn().mockResolvedValue(undefined) - }) + readLockFile: vi.fn() }; }); +const mockInstall = vi.fn().mockResolvedValue(undefined); + +vi.mock("@ade/harnesses", () => ({ + getHarnessWriter: vi.fn().mockImplementation((id: string) => { + if (id === "claude-code" || id === "cursor") { + return { id, install: mockInstall }; + } + return undefined; + }), + getHarnessIds: vi + .fn() + .mockReturnValue([ + "claude-code", + "cursor", + "copilot", + "windsurf", + "cline", + "roo-code" + ]) +})); + import * as clack from "@clack/prompts"; -import { readLockFile, getAgentWriter } from "@ade/core"; +import { readLockFile } from "@ade/core"; import { runInstall } from "./install.js"; // ── Tests ──────────────────────────────────────────────────────────────────── describe("runInstall", () => { - beforeEach(() => { + beforeEach(async () => { vi.clearAllMocks(); + // Re-set the default implementation after clearAllMocks + const { getHarnessWriter } = await import("@ade/harnesses"); + vi.mocked(getHarnessWriter).mockImplementation((id: string) => { + if (id === "claude-code" || id === "cursor") { + return { + id, + label: id, + description: "test", + install: mockInstall + }; + } + return undefined; + }); }); it("reads config.lock.yaml and applies logical config", async () => { @@ -52,85 +82,71 @@ describe("runInstall", () => { logical_config: mockLogical }); - await runInstall("/tmp/project", "claude-code"); + await runInstall("/tmp/project"); expect(readLockFile).toHaveBeenCalledWith("/tmp/project"); }); - it("does not re-resolve — uses lock file logical_config directly", async () => { - const lockedConfig: LogicalConfig = { - mcp_servers: [ - { - ref: "workflows", - command: "npx", - args: ["@codemcp/workflows-server@latest"], - env: {} - } - ], - instructions: ["do stuff"], - cli_actions: [], - skills: [], - knowledge_sources: [] - }; + it("defaults to claude-code harness when none specified", async () => { vi.mocked(readLockFile).mockResolvedValueOnce({ version: 1, generated_at: "2024-01-01T00:00:00.000Z", choices: { process: "codemcp-workflows" }, - logical_config: lockedConfig + logical_config: mockLogical }); - const mockInstall = vi.fn().mockResolvedValue(undefined); - vi.mocked(getAgentWriter).mockReturnValueOnce({ - id: "claude-code", - install: mockInstall + await runInstall("/tmp/project"); + + expect(mockInstall).toHaveBeenCalledWith(mockLogical, "/tmp/project"); + }); + + it("uses harnesses from lock file when present", async () => { + vi.mocked(readLockFile).mockResolvedValueOnce({ + version: 1, + generated_at: "2024-01-01T00:00:00.000Z", + choices: { process: "codemcp-workflows" }, + harnesses: ["claude-code", "cursor"], + logical_config: mockLogical }); - await runInstall("/tmp/project", "claude-code"); + await runInstall("/tmp/project"); - expect(mockInstall).toHaveBeenCalledWith(lockedConfig, "/tmp/project"); + expect(mockInstall).toHaveBeenCalledTimes(2); }); - it("calls agent writer install with lock file config", async () => { - const mockInstall = vi.fn().mockResolvedValue(undefined); - vi.mocked(getAgentWriter).mockReturnValueOnce({ - id: "claude-code", - install: mockInstall - }); + it("uses explicit harness ids when provided", async () => { vi.mocked(readLockFile).mockResolvedValueOnce({ version: 1, generated_at: "2024-01-01T00:00:00.000Z", choices: { process: "codemcp-workflows" }, + harnesses: ["claude-code"], logical_config: mockLogical }); - await runInstall("/tmp/project", "claude-code"); + await runInstall("/tmp/project", ["cursor"]); - expect(getAgentWriter).toHaveBeenCalledWith( - expect.anything(), - "claude-code" - ); - expect(mockInstall).toHaveBeenCalledWith(mockLogical, "/tmp/project"); + // Explicit takes priority over lock file + expect(mockInstall).toHaveBeenCalledTimes(1); }); it("throws when config.lock.yaml is missing", async () => { vi.mocked(readLockFile).mockResolvedValueOnce(null); - await expect(runInstall("/tmp/project", "claude-code")).rejects.toThrow( + await expect(runInstall("/tmp/project")).rejects.toThrow( /config\.lock\.yaml not found/i ); }); - it("throws when agent writer is unknown", async () => { + it("throws when harness id is unknown", async () => { vi.mocked(readLockFile).mockResolvedValueOnce({ version: 1, generated_at: "2024-01-01T00:00:00.000Z", choices: { process: "codemcp-workflows" }, logical_config: mockLogical }); - vi.mocked(getAgentWriter).mockReturnValueOnce(undefined); - await expect(runInstall("/tmp/project", "unknown-agent")).rejects.toThrow( - /unknown agent/i + await expect(runInstall("/tmp/project", ["unknown-agent"])).rejects.toThrow( + /unknown harness/i ); }); @@ -142,7 +158,7 @@ describe("runInstall", () => { logical_config: mockLogical }); - await runInstall("/tmp/project", "claude-code"); + await runInstall("/tmp/project"); expect(clack.intro).toHaveBeenCalled(); expect(clack.outro).toHaveBeenCalled(); diff --git a/packages/cli/src/commands/install.ts b/packages/cli/src/commands/install.ts index 7808e5a..3588591 100644 --- a/packages/cli/src/commands/install.ts +++ b/packages/cli/src/commands/install.ts @@ -1,11 +1,12 @@ import * as clack from "@clack/prompts"; -import { readLockFile, createDefaultRegistry, getAgentWriter } from "@ade/core"; +import { readLockFile } from "@ade/core"; +import { getHarnessWriter, getHarnessIds } from "@ade/harnesses"; import { installSkills } from "../skills-installer.js"; import { installKnowledge } from "../knowledge-installer.js"; export async function runInstall( projectRoot: string, - agent: string + harnessIds?: string[] ): Promise { clack.intro("ade install"); @@ -14,16 +15,30 @@ export async function runInstall( throw new Error("config.lock.yaml not found. Run `ade setup` first."); } - const registry = createDefaultRegistry(); - - const agentWriter = getAgentWriter(registry, agent); - if (!agentWriter) { - throw new Error(`Unknown agent "${agent}". Available: claude-code`); + // Determine which harnesses to install for: + // 1. --harness flag (comma-separated) + // 2. harnesses saved in the lock file + // 3. legacy --agent flag (mapped to harness) + // 4. default: claude-code + const ids = harnessIds ?? lockFile.harnesses ?? ["claude-code"]; + + const validIds = getHarnessIds(); + for (const id of ids) { + if (!validIds.includes(id)) { + throw new Error( + `Unknown harness "${id}". Available: ${validIds.join(", ")}` + ); + } } const logicalConfig = lockFile.logical_config; - await agentWriter.install(logicalConfig, projectRoot); + for (const id of ids) { + const writer = getHarnessWriter(id); + if (writer) { + await writer.install(logicalConfig, projectRoot); + } + } await installSkills(logicalConfig.skills, projectRoot); await installKnowledge(logicalConfig.knowledge_sources, projectRoot); diff --git a/packages/cli/src/commands/knowledge.integration.spec.ts b/packages/cli/src/commands/knowledge.integration.spec.ts index 97d2b31..9d2bfc9 100644 --- a/packages/cli/src/commands/knowledge.integration.spec.ts +++ b/packages/cli/src/commands/knowledge.integration.spec.ts @@ -64,7 +64,8 @@ describe("knowledge integration", () => { "tanstack-query-docs", "tanstack-form-docs", "tanstack-table-docs" - ]); + ]) + .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -134,7 +135,8 @@ describe("knowledge integration", () => { // multiselect order: practices, then docsets (only keep router + query) vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none - .mockResolvedValueOnce(["tanstack-router-docs", "tanstack-query-docs"]); + .mockResolvedValueOnce(["tanstack-router-docs", "tanstack-query-docs"]) + .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -157,7 +159,9 @@ describe("knowledge integration", () => { vi.mocked(clack.select) .mockResolvedValueOnce("native-agents-md") // process .mockResolvedValueOnce("__skip__"); // architecture: skip - vi.mocked(clack.multiselect).mockResolvedValueOnce(["tdd-london"]); // practices: no docsets + vi.mocked(clack.multiselect) + .mockResolvedValueOnce(["tdd-london"]) // practices: no docsets + .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); diff --git a/packages/cli/src/commands/setup.integration.spec.ts b/packages/cli/src/commands/setup.integration.spec.ts index 2af8264..c5566a5 100644 --- a/packages/cli/src/commands/setup.integration.spec.ts +++ b/packages/cli/src/commands/setup.integration.spec.ts @@ -8,7 +8,7 @@ vi.mock("@clack/prompts", () => ({ intro: vi.fn(), outro: vi.fn(), select: vi.fn(), - multiselect: vi.fn().mockResolvedValue([]), + multiselect: vi.fn(), confirm: vi.fn(), isCancel: vi.fn().mockReturnValue(false), cancel: vi.fn(), @@ -38,6 +38,9 @@ describe("setup integration (real temp dir)", () => { vi.mocked(clack.select) .mockResolvedValueOnce("codemcp-workflows") // process .mockResolvedValueOnce("__skip__"); // architecture + vi.mocked(clack.multiselect) + .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -80,6 +83,9 @@ describe("setup integration (real temp dir)", () => { vi.mocked(clack.select) .mockResolvedValueOnce("native-agents-md") // process .mockResolvedValueOnce("__skip__"); // architecture + vi.mocked(clack.multiselect) + .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); @@ -118,6 +124,9 @@ describe("setup integration (real temp dir)", () => { vi.mocked(clack.select) .mockResolvedValueOnce("codemcp-workflows") // process .mockResolvedValueOnce("__skip__"); // architecture + vi.mocked(clack.multiselect) + .mockResolvedValueOnce([]) // practices: none + .mockResolvedValueOnce(["claude-code"]); // harnesses await runSetup(dir, catalog); diff --git a/packages/cli/src/commands/setup.spec.ts b/packages/cli/src/commands/setup.spec.ts index ce60b14..3397177 100644 --- a/packages/cli/src/commands/setup.spec.ts +++ b/packages/cli/src/commands/setup.spec.ts @@ -33,14 +33,28 @@ vi.mock("@ade/core", async (importOriginal) => { knowledge_sources: [], skills: [] } satisfies LogicalConfig), - getAgentWriter: vi.fn().mockReturnValue({ - id: "claude-code", - install: vi.fn().mockResolvedValue(undefined) - }), collectDocsets: actual.collectDocsets }; }); +vi.mock("@ade/harnesses", () => ({ + allHarnessWriters: [ + { + id: "claude-code", + label: "Claude Code", + description: "test", + install: vi.fn().mockResolvedValue(undefined) + } + ], + getHarnessWriter: vi.fn().mockReturnValue({ + id: "claude-code", + label: "Claude Code", + description: "test", + install: vi.fn().mockResolvedValue(undefined) + }), + getHarnessIds: vi.fn().mockReturnValue(["claude-code"]) +})); + import * as clack from "@clack/prompts"; import { readUserConfig, @@ -142,6 +156,8 @@ describe("runSetup", () => { vi.mocked(clack.select) .mockResolvedValueOnce("workflow-a") .mockResolvedValueOnce("vitest"); + // Harness multiselect + vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", testCatalog); @@ -169,6 +185,7 @@ describe("runSetup", () => { vi.mocked(clack.select) .mockResolvedValueOnce("workflow-a") .mockResolvedValueOnce("vitest"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", testCatalog); @@ -194,6 +211,7 @@ describe("runSetup", () => { vi.mocked(clack.select) .mockResolvedValueOnce("workflow-a") .mockResolvedValueOnce("__skip__"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", testCatalog); @@ -221,11 +239,10 @@ describe("runSetup", () => { describe("docset confirmation step", () => { it("presents implied docsets as a multiselect after facet selection", async () => { vi.mocked(clack.select).mockResolvedValueOnce("react"); - // User accepts all docsets (returns all ids) - vi.mocked(clack.multiselect).mockResolvedValueOnce([ - "react-docs", - "react-tutorial" - ]); + // User accepts all docsets (returns all ids), then harness selection + vi.mocked(clack.multiselect) + .mockResolvedValueOnce(["react-docs", "react-tutorial"]) + .mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", docsetCatalog); @@ -239,8 +256,10 @@ describe("runSetup", () => { it("stores deselected docsets as excluded_docsets in user config", async () => { vi.mocked(clack.select).mockResolvedValueOnce("react"); - // User deselects react-tutorial, keeps only react-docs - vi.mocked(clack.multiselect).mockResolvedValueOnce(["react-docs"]); + // User deselects react-tutorial, keeps only react-docs; then harness + vi.mocked(clack.multiselect) + .mockResolvedValueOnce(["react-docs"]) + .mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", docsetCatalog); @@ -254,10 +273,9 @@ describe("runSetup", () => { it("does not set excluded_docsets when all docsets are accepted", async () => { vi.mocked(clack.select).mockResolvedValueOnce("react"); - vi.mocked(clack.multiselect).mockResolvedValueOnce([ - "react-docs", - "react-tutorial" - ]); + vi.mocked(clack.multiselect) + .mockResolvedValueOnce(["react-docs", "react-tutorial"]) + .mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", docsetCatalog); @@ -269,11 +287,18 @@ describe("runSetup", () => { vi.mocked(clack.select) .mockResolvedValueOnce("workflow-a") .mockResolvedValueOnce("vitest"); + // Only the harness multiselect should be called (no docsets in testCatalog) + vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", testCatalog); - // multiselect should NOT have been called (no docsets in testCatalog) - expect(clack.multiselect).not.toHaveBeenCalled(); + // multiselect should have been called exactly once (for harnesses only) + expect(clack.multiselect).toHaveBeenCalledTimes(1); + expect(clack.multiselect).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining("Harnesses") + }) + ); }); }); @@ -281,6 +306,7 @@ describe("runSetup", () => { vi.mocked(clack.select) .mockResolvedValueOnce("workflow-a") .mockResolvedValueOnce("vitest"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", testCatalog); @@ -297,6 +323,7 @@ describe("runSetup", () => { vi.mocked(clack.select) .mockResolvedValueOnce("workflow-b") .mockResolvedValueOnce("jest"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", testCatalog); @@ -341,7 +368,9 @@ describe("runSetup", () => { choices: { practices: ["tdd", "adr"] } }); - vi.mocked(clack.multiselect).mockResolvedValueOnce(["tdd", "adr"]); + vi.mocked(clack.multiselect) + .mockResolvedValueOnce(["tdd", "adr"]) + .mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", multiCatalog); @@ -358,6 +387,7 @@ describe("runSetup", () => { vi.mocked(clack.select) .mockResolvedValueOnce("workflow-a") .mockResolvedValueOnce("vitest"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", testCatalog); @@ -374,6 +404,7 @@ describe("runSetup", () => { vi.mocked(clack.select) .mockResolvedValueOnce("workflow-a") .mockResolvedValueOnce("vitest"); + vi.mocked(clack.multiselect).mockResolvedValueOnce(["claude-code"]); await runSetup("/tmp/test-project", testCatalog); diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index f097719..9c979c4 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -10,10 +10,10 @@ import { resolve, collectDocsets, createDefaultRegistry, - getAgentWriter, getFacet, getOption } from "@ade/core"; +import { allHarnessWriters, getHarnessWriter } from "@ade/harnesses"; import { installSkills } from "../skills-installer.js"; import { installKnowledge } from "../knowledge-installer.js"; @@ -95,9 +95,39 @@ export async function runSetup( } } + // Harness selection — multi-select from all available harnesses + const existingHarnesses = existingConfig?.harnesses; + const harnessOptions = allHarnessWriters.map((w) => ({ + value: w.id, + label: w.label, + hint: w.description + })); + + const validExistingHarnesses = existingHarnesses?.filter((h) => + allHarnessWriters.some((w) => w.id === h) + ); + + const selectedHarnesses = await clack.multiselect({ + message: "Harnesses — which coding agents should receive config?", + options: harnessOptions, + initialValues: + validExistingHarnesses && validExistingHarnesses.length > 0 + ? validExistingHarnesses + : ["claude-code"], + required: false + }); + + if (typeof selectedHarnesses === "symbol") { + clack.cancel("Setup cancelled."); + return; + } + + const harnesses = selectedHarnesses as string[]; + const userConfig: UserConfig = { choices, - ...(excludedDocsets && { excluded_docsets: excludedDocsets }) + ...(excludedDocsets && { excluded_docsets: excludedDocsets }), + ...(harnesses.length > 0 && { harnesses }) }; const registry = createDefaultRegistry(); const logicalConfig = await resolve(userConfig, catalog, registry); @@ -108,13 +138,17 @@ export async function runSetup( version: 1, generated_at: new Date().toISOString(), choices: userConfig.choices, + ...(harnesses.length > 0 && { harnesses }), logical_config: logicalConfig }; await writeLockFile(projectRoot, lockFile); - const agentWriter = getAgentWriter(registry, "claude-code"); - if (agentWriter) { - await agentWriter.install(logicalConfig, projectRoot); + // Install to all selected harnesses + for (const harnessId of harnesses) { + const writer = getHarnessWriter(harnessId); + if (writer) { + await writer.install(logicalConfig, projectRoot); + } } await installSkills(logicalConfig.skills, projectRoot); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 8830b19..a26f06b 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -4,6 +4,7 @@ import { version } from "./version.js"; import { runSetup } from "./commands/setup.js"; import { runInstall } from "./commands/install.js"; import { getDefaultCatalog } from "@ade/core"; +import { getHarnessIds } from "@ade/harnesses"; const args = process.argv.slice(2); const command = args[0]; @@ -14,13 +15,30 @@ if (command === "setup") { await runSetup(projectRoot, catalog); } else if (command === "install") { const projectRoot = args[1] ?? process.cwd(); - const agent = args.includes("--agent") - ? args[args.indexOf("--agent") + 1] - : "claude-code"; - await runInstall(projectRoot, agent); + + let harnessIds: string[] | undefined; + + // Support --harness flag (comma-separated) + if (args.includes("--harness")) { + const val = args[args.indexOf("--harness") + 1]; + if (val) { + harnessIds = val.split(",").map((s) => s.trim()); + } + } + + // Legacy --agent flag maps to single harness + if (!harnessIds && args.includes("--agent")) { + const val = args[args.indexOf("--agent") + 1]; + if (val) { + harnessIds = [val]; + } + } + + await runInstall(projectRoot, harnessIds); } else if (command === "--version" || command === "-v") { console.log(version); } else { + const allIds = getHarnessIds(); console.log(`ade v${version}`); console.log(); console.log("Usage: ade [options]"); @@ -34,7 +52,10 @@ if (command === "setup") { ); console.log(); console.log("Options:"); - console.log(" --agent Agent writer to use (default: claude-code)"); + console.log( + ` --harness Comma-separated harnesses (${allIds.join(", ")})` + ); + console.log(" --agent Legacy alias for --harness (single value)"); console.log(" -v, --version Show version"); process.exitCode = command ? 1 : 0; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ce48910..f84bac7 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -38,6 +38,7 @@ export { } from "./registry.js"; export { resolve, collectDocsets } from "./resolver.js"; export { getDefaultCatalog, getFacet, getOption } from "./catalog/index.js"; +/** @deprecated Use @ade/harnesses package instead */ export { claudeCodeWriter } from "./agents/claude-code.js"; export { skillsWriter } from "./writers/skills.js"; export { knowledgeWriter } from "./writers/knowledge.js"; diff --git a/packages/core/src/registry.spec.ts b/packages/core/src/registry.spec.ts index 4a5800a..4ff05bc 100644 --- a/packages/core/src/registry.spec.ts +++ b/packages/core/src/registry.spec.ts @@ -132,11 +132,9 @@ describe("registry", () => { expect(registry.provisions.size).toBe(6); }); - it("has the 'claude-code' agent writer registered", () => { + it("has no agent writers by default (moved to @ade/harnesses)", () => { const registry = createDefaultRegistry(); - const agent = getAgentWriter(registry, "claude-code"); - expect(agent).toBeDefined(); - expect(agent!.id).toBe("claude-code"); + expect(registry.agents.size).toBe(0); }); }); }); diff --git a/packages/core/src/registry.ts b/packages/core/src/registry.ts index d062009..90d7ace 100644 --- a/packages/core/src/registry.ts +++ b/packages/core/src/registry.ts @@ -7,7 +7,6 @@ import { instructionWriter } from "./writers/instruction.js"; import { workflowsWriter } from "./writers/workflows.js"; import { skillsWriter } from "./writers/skills.js"; import { knowledgeWriter } from "./writers/knowledge.js"; -import { claudeCodeWriter } from "./agents/claude-code.js"; export function createRegistry(): WriterRegistry { return { @@ -61,7 +60,5 @@ export function createDefaultRegistry(): WriterRegistry { }); } - registerAgentWriter(registry, claudeCodeWriter); - return registry; } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 70a592e..b6b8c47 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -100,6 +100,7 @@ export interface ResolvedFacet { export interface UserConfig { choices: Record; excluded_docsets?: string[]; + harnesses?: string[]; custom?: { mcp_servers?: McpServerEntry[]; instructions?: string[]; @@ -110,6 +111,7 @@ export interface LockFile { version: 1; generated_at: string; choices: Record; + harnesses?: string[]; logical_config: LogicalConfig; } diff --git a/packages/harnesses/package.json b/packages/harnesses/package.json new file mode 100644 index 0000000..f5f5595 --- /dev/null +++ b/packages/harnesses/package.json @@ -0,0 +1,33 @@ +{ + "name": "@ade/harnesses", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "clean:build": "rimraf ./dist", + "dev": "nodemon", + "lint": "eslint .", + "lint:fix": "eslint --fix .", + "format": "prettier --check .", + "format:fix": "prettier --write .", + "test": "vitest --run", + "test:watch": "vitest", + "typecheck": "tsc" + }, + "dependencies": { + "@ade/core": "workspace:*" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^8.21.0", + "@typescript-eslint/parser": "^8.21.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "prettier": "^3.4.2", + "rimraf": "^6.0.1", + "typescript": "^5.7.3" + } +} diff --git a/packages/harnesses/src/index.spec.ts b/packages/harnesses/src/index.spec.ts new file mode 100644 index 0000000..afb4233 --- /dev/null +++ b/packages/harnesses/src/index.spec.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from "vitest"; +import { allHarnessWriters, getHarnessWriter, getHarnessIds } from "./index.js"; + +describe("harness registry", () => { + it("exports all harness writers", () => { + expect(allHarnessWriters).toHaveLength(6); + const ids = allHarnessWriters.map((w) => w.id); + expect(ids).toContain("claude-code"); + expect(ids).toContain("cursor"); + expect(ids).toContain("copilot"); + expect(ids).toContain("windsurf"); + expect(ids).toContain("cline"); + expect(ids).toContain("roo-code"); + }); + + it("looks up harness by id", () => { + expect(getHarnessWriter("cursor")?.label).toBe("Cursor"); + expect(getHarnessWriter("nonexistent")).toBeUndefined(); + }); + + it("returns all harness ids", () => { + const ids = getHarnessIds(); + expect(ids).toEqual([ + "claude-code", + "cursor", + "copilot", + "windsurf", + "cline", + "roo-code" + ]); + }); + + it("all writers have label and description", () => { + for (const w of allHarnessWriters) { + expect(w.label).toBeTruthy(); + expect(w.description).toBeTruthy(); + } + }); +}); diff --git a/packages/harnesses/src/index.ts b/packages/harnesses/src/index.ts new file mode 100644 index 0000000..46be492 --- /dev/null +++ b/packages/harnesses/src/index.ts @@ -0,0 +1,36 @@ +export type { HarnessWriter } from "./types.js"; + +export { claudeCodeWriter } from "./writers/claude-code.js"; +export { cursorWriter } from "./writers/cursor.js"; +export { copilotWriter } from "./writers/copilot.js"; +export { windsurfWriter } from "./writers/windsurf.js"; +export { clineWriter } from "./writers/cline.js"; +export { rooCodeWriter } from "./writers/roo-code.js"; + +import type { HarnessWriter } from "./types.js"; +import { claudeCodeWriter } from "./writers/claude-code.js"; +import { cursorWriter } from "./writers/cursor.js"; +import { copilotWriter } from "./writers/copilot.js"; +import { windsurfWriter } from "./writers/windsurf.js"; +import { clineWriter } from "./writers/cline.js"; +import { rooCodeWriter } from "./writers/roo-code.js"; + +/** All built-in harness writers, ordered for wizard display. */ +export const allHarnessWriters: HarnessWriter[] = [ + claudeCodeWriter, + cursorWriter, + copilotWriter, + windsurfWriter, + clineWriter, + rooCodeWriter +]; + +/** Look up a harness writer by id. */ +export function getHarnessWriter(id: string): HarnessWriter | undefined { + return allHarnessWriters.find((w) => w.id === id); +} + +/** All valid harness IDs. */ +export function getHarnessIds(): string[] { + return allHarnessWriters.map((w) => w.id); +} diff --git a/packages/harnesses/src/types.ts b/packages/harnesses/src/types.ts new file mode 100644 index 0000000..7291d9f --- /dev/null +++ b/packages/harnesses/src/types.ts @@ -0,0 +1,12 @@ +import type { AgentWriterDef } from "@ade/core"; + +/** + * A harness writer extends AgentWriterDef with metadata for display in the + * setup wizard and CLI help. + */ +export interface HarnessWriter extends AgentWriterDef { + /** Human-readable label for the wizard (e.g. "Claude Code") */ + label: string; + /** Short description shown as hint in the wizard */ + description: string; +} diff --git a/packages/harnesses/src/writers/claude-code.spec.ts b/packages/harnesses/src/writers/claude-code.spec.ts new file mode 100644 index 0000000..125786b --- /dev/null +++ b/packages/harnesses/src/writers/claude-code.spec.ts @@ -0,0 +1,125 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm, readFile, access } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { LogicalConfig } from "@ade/core"; +import { claudeCodeWriter } from "./claude-code.js"; + +describe("claudeCodeWriter", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "ade-harness-cc-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("has correct metadata", () => { + expect(claudeCodeWriter.id).toBe("claude-code"); + expect(claudeCodeWriter.label).toBe("Claude Code"); + expect(claudeCodeWriter.description).toBeTruthy(); + }); + + it("writes AGENTS.md with instructions", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: ["Use workflow files.", "Follow conventions."], + cli_actions: [], + knowledge_sources: [], + skills: [] + }; + + await claudeCodeWriter.install(config, dir); + + const content = await readFile(join(dir, "AGENTS.md"), "utf-8"); + expect(content).toContain("# AGENTS"); + expect(content).toContain("Use workflow files."); + expect(content).toContain("Follow conventions."); + }); + + it("writes .claude/settings.json with MCP servers", async () => { + const config: LogicalConfig = { + mcp_servers: [ + { + ref: "@codemcp/workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: {} + } + ], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [] + }; + + await claudeCodeWriter.install(config, dir); + + const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8"); + const settings = JSON.parse(raw); + expect(settings.mcpServers["@codemcp/workflows"]).toEqual({ + command: "npx", + args: ["-y", "@codemcp/workflows"] + }); + }); + + it("adds skills-server when skills are present", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [{ name: "my-skill", description: "A skill", body: "Do stuff." }] + }; + + await claudeCodeWriter.install(config, dir); + + const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8"); + const settings = JSON.parse(raw); + expect(settings.mcpServers["agentskills"]).toEqual({ + command: "npx", + args: ["-y", "@codemcp/skills-server"] + }); + }); + + it("writes inline SKILL.md files", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [ + { + name: "tanstack-architecture", + description: "TanStack architecture conventions", + body: "# Architecture\n\nUse file-based routing." + } + ] + }; + + await claudeCodeWriter.install(config, dir); + + const skillMd = await readFile( + join(dir, ".ade", "skills", "tanstack-architecture", "SKILL.md"), + "utf-8" + ); + expect(skillMd).toContain("name: tanstack-architecture"); + expect(skillMd).toContain("# Architecture"); + }); + + it("skips AGENTS.md when no instructions", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [] + }; + + await claudeCodeWriter.install(config, dir); + + await expect(readFile(join(dir, "AGENTS.md"), "utf-8")).rejects.toThrow(); + }); +}); diff --git a/packages/harnesses/src/writers/claude-code.ts b/packages/harnesses/src/writers/claude-code.ts new file mode 100644 index 0000000..931d7fd --- /dev/null +++ b/packages/harnesses/src/writers/claude-code.ts @@ -0,0 +1,110 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { LogicalConfig, McpServerEntry, InlineSkill } from "@ade/core"; +import type { HarnessWriter } from "../types.js"; + +function isInlineSkill( + skill: LogicalConfig["skills"][number] +): skill is InlineSkill { + return "body" in skill; +} + +export const claudeCodeWriter: HarnessWriter = { + id: "claude-code", + label: "Claude Code", + description: "Anthropic's CLI agent — .claude/settings.json + AGENTS.md", + async install(config: LogicalConfig, projectRoot: string) { + await writeAgentsMd(config, projectRoot); + await writeSkills(config, projectRoot); + await writeClaudeSettings(config, projectRoot); + } +}; + +async function writeAgentsMd( + config: LogicalConfig, + projectRoot: string +): Promise { + if (config.instructions.length === 0) return; + + const lines = ["# AGENTS", ""]; + for (const instruction of config.instructions) { + lines.push(instruction, ""); + } + + await writeFile(join(projectRoot, "AGENTS.md"), lines.join("\n"), "utf-8"); +} + +async function writeSkills( + config: LogicalConfig, + projectRoot: string +): Promise { + if (config.skills.length === 0) return; + + for (const skill of config.skills) { + if (!isInlineSkill(skill)) continue; + + const skillDir = join(projectRoot, ".ade", "skills", skill.name); + await mkdir(skillDir, { recursive: true }); + + const frontmatter = [ + "---", + `name: ${skill.name}`, + `description: ${skill.description}`, + "---" + ].join("\n"); + + const content = `${frontmatter}\n\n${skill.body}\n`; + await writeFile(join(skillDir, "SKILL.md"), content, "utf-8"); + } +} + +async function writeClaudeSettings( + config: LogicalConfig, + projectRoot: string +): Promise { + const allServers: McpServerEntry[] = [...config.mcp_servers]; + + if (config.skills.length > 0) { + allServers.push({ + ref: "agentskills", + command: "npx", + args: ["-y", "@codemcp/skills-server"], + env: {} + }); + } + + if (allServers.length === 0) return; + + const claudeDir = join(projectRoot, ".claude"); + await mkdir(claudeDir, { recursive: true }); + + const settingsPath = join(claudeDir, "settings.json"); + + let existing: Record = {}; + try { + const raw = await readFile(settingsPath, "utf-8"); + existing = JSON.parse(raw); + } catch { + // No existing file — start fresh + } + + const mcpServers: Record< + string, + { command: string; args: string[]; env?: Record } + > = (existing.mcpServers as typeof mcpServers) ?? {}; + + for (const server of allServers) { + mcpServers[server.ref] = { + command: server.command, + args: server.args, + ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) + }; + } + + const settings = { ...existing, mcpServers }; + await writeFile( + settingsPath, + JSON.stringify(settings, null, 2) + "\n", + "utf-8" + ); +} diff --git a/packages/harnesses/src/writers/cline.spec.ts b/packages/harnesses/src/writers/cline.spec.ts new file mode 100644 index 0000000..036b90b --- /dev/null +++ b/packages/harnesses/src/writers/cline.spec.ts @@ -0,0 +1,64 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { LogicalConfig } from "@ade/core"; +import { clineWriter } from "./cline.js"; + +describe("clineWriter", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "ade-harness-cline-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("has correct metadata", () => { + expect(clineWriter.id).toBe("cline"); + expect(clineWriter.label).toBe("Cline"); + }); + + it("writes .cline/mcp.json with MCP servers", async () => { + const config: LogicalConfig = { + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: {} + } + ], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [] + }; + + await clineWriter.install(config, dir); + + const raw = await readFile(join(dir, ".cline", "mcp.json"), "utf-8"); + const parsed = JSON.parse(raw); + expect(parsed.mcpServers["workflows"]).toEqual({ + command: "npx", + args: ["-y", "@codemcp/workflows"] + }); + }); + + it("writes .clinerules with instructions", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: ["Follow TDD."], + cli_actions: [], + knowledge_sources: [], + skills: [] + }; + + await clineWriter.install(config, dir); + + const content = await readFile(join(dir, ".clinerules"), "utf-8"); + expect(content).toContain("Follow TDD."); + }); +}); diff --git a/packages/harnesses/src/writers/cline.ts b/packages/harnesses/src/writers/cline.ts new file mode 100644 index 0000000..74ec579 --- /dev/null +++ b/packages/harnesses/src/writers/cline.ts @@ -0,0 +1,71 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { LogicalConfig, McpServerEntry } from "@ade/core"; +import type { HarnessWriter } from "../types.js"; + +export const clineWriter: HarnessWriter = { + id: "cline", + label: "Cline", + description: "VS Code AI agent — .cline/mcp.json + .clinerules", + async install(config: LogicalConfig, projectRoot: string) { + await writeMcpJson(config, projectRoot); + await writeRules(config, projectRoot); + } +}; + +async function writeMcpJson( + config: LogicalConfig, + projectRoot: string +): Promise { + const allServers: McpServerEntry[] = [...config.mcp_servers]; + + if (config.skills.length > 0) { + allServers.push({ + ref: "agentskills", + command: "npx", + args: ["-y", "@codemcp/skills-server"], + env: {} + }); + } + + if (allServers.length === 0) return; + + const clineDir = join(projectRoot, ".cline"); + await mkdir(clineDir, { recursive: true }); + + const mcpPath = join(clineDir, "mcp.json"); + + let existing: Record = {}; + try { + const raw = await readFile(mcpPath, "utf-8"); + existing = JSON.parse(raw); + } catch { + // Start fresh + } + + const mcpServers: Record< + string, + { command: string; args: string[]; env?: Record } + > = (existing.mcpServers as typeof mcpServers) ?? {}; + + for (const server of allServers) { + mcpServers[server.ref] = { + command: server.command, + args: server.args, + ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) + }; + } + + const result = { ...existing, mcpServers }; + await writeFile(mcpPath, JSON.stringify(result, null, 2) + "\n", "utf-8"); +} + +async function writeRules( + config: LogicalConfig, + projectRoot: string +): Promise { + if (config.instructions.length === 0) return; + + const lines = config.instructions.flatMap((i) => [i, ""]); + await writeFile(join(projectRoot, ".clinerules"), lines.join("\n"), "utf-8"); +} diff --git a/packages/harnesses/src/writers/copilot.spec.ts b/packages/harnesses/src/writers/copilot.spec.ts new file mode 100644 index 0000000..45b0a25 --- /dev/null +++ b/packages/harnesses/src/writers/copilot.spec.ts @@ -0,0 +1,98 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { LogicalConfig } from "@ade/core"; +import { copilotWriter } from "./copilot.js"; + +describe("copilotWriter", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "ade-harness-copilot-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("has correct metadata", () => { + expect(copilotWriter.id).toBe("copilot"); + expect(copilotWriter.label).toBe("GitHub Copilot"); + }); + + it("writes .vscode/mcp.json with 'servers' key and type field", async () => { + const config: LogicalConfig = { + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: {} + } + ], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [] + }; + + await copilotWriter.install(config, dir); + + const raw = await readFile(join(dir, ".vscode", "mcp.json"), "utf-8"); + const parsed = JSON.parse(raw); + // Copilot uses "servers", not "mcpServers" + expect(parsed.servers["workflows"]).toEqual({ + type: "stdio", + command: "npx", + args: ["-y", "@codemcp/workflows"] + }); + }); + + it("writes .github/copilot-instructions.md with instructions", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: ["Follow TDD.", "Use ADRs."], + cli_actions: [], + knowledge_sources: [], + skills: [] + }; + + await copilotWriter.install(config, dir); + + const content = await readFile( + join(dir, ".github", "copilot-instructions.md"), + "utf-8" + ); + expect(content).toContain("Follow TDD."); + expect(content).toContain("Use ADRs."); + }); + + it("writes dedicated .github/agents/ade.agent.md with agent definition", async () => { + const config: LogicalConfig = { + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: {} + } + ], + instructions: ["Follow TDD."], + cli_actions: [], + knowledge_sources: [], + skills: [] + }; + + await copilotWriter.install(config, dir); + + const content = await readFile( + join(dir, ".github", "agents", "ade.agent.md"), + "utf-8" + ); + expect(content).toContain("name: ade"); + expect(content).toContain("tools:"); + expect(content).toContain(" - workflows"); + expect(content).toContain("Follow TDD."); + }); +}); diff --git a/packages/harnesses/src/writers/copilot.ts b/packages/harnesses/src/writers/copilot.ts new file mode 100644 index 0000000..9ca7ce2 --- /dev/null +++ b/packages/harnesses/src/writers/copilot.ts @@ -0,0 +1,129 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { LogicalConfig, McpServerEntry } from "@ade/core"; +import type { HarnessWriter } from "../types.js"; + +export const copilotWriter: HarnessWriter = { + id: "copilot", + label: "GitHub Copilot", + description: + "VS Code Copilot — .vscode/mcp.json + .github/copilot-instructions.md", + async install(config: LogicalConfig, projectRoot: string) { + await writeVsCodeMcp(config, projectRoot); + await writeCopilotInstructions(config, projectRoot); + await writeCopilotAgent(config, projectRoot); + } +}; + +async function writeVsCodeMcp( + config: LogicalConfig, + projectRoot: string +): Promise { + const allServers: McpServerEntry[] = [...config.mcp_servers]; + + if (config.skills.length > 0) { + allServers.push({ + ref: "agentskills", + command: "npx", + args: ["-y", "@codemcp/skills-server"], + env: {} + }); + } + + if (allServers.length === 0) return; + + const vscodeDir = join(projectRoot, ".vscode"); + await mkdir(vscodeDir, { recursive: true }); + + const mcpPath = join(vscodeDir, "mcp.json"); + + let existing: Record = {}; + try { + const raw = await readFile(mcpPath, "utf-8"); + existing = JSON.parse(raw); + } catch { + // Start fresh + } + + // Copilot uses "servers" key, not "mcpServers" + const servers: Record< + string, + { + type: string; + command: string; + args: string[]; + env?: Record; + } + > = (existing.servers as typeof servers) ?? {}; + + for (const server of allServers) { + servers[server.ref] = { + type: "stdio", + command: server.command, + args: server.args, + ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) + }; + } + + const result = { ...existing, servers }; + await writeFile(mcpPath, JSON.stringify(result, null, 2) + "\n", "utf-8"); +} + +async function writeCopilotInstructions( + config: LogicalConfig, + projectRoot: string +): Promise { + if (config.instructions.length === 0) return; + + const githubDir = join(projectRoot, ".github"); + await mkdir(githubDir, { recursive: true }); + + const lines = config.instructions.flatMap((i) => [i, ""]); + await writeFile( + join(githubDir, "copilot-instructions.md"), + lines.join("\n"), + "utf-8" + ); +} + +/** + * Write a dedicated ADE agent definition that combines instructions and + * references configured MCP servers. + */ +async function writeCopilotAgent( + config: LogicalConfig, + projectRoot: string +): Promise { + const allServers: McpServerEntry[] = [...config.mcp_servers]; + if (config.skills.length > 0) { + allServers.push({ + ref: "agentskills", + command: "npx", + args: ["-y", "@codemcp/skills-server"], + env: {} + }); + } + + if (config.instructions.length === 0 && allServers.length === 0) return; + + const agentsDir = join(projectRoot, ".github", "agents"); + await mkdir(agentsDir, { recursive: true }); + + const frontmatter: string[] = [ + "---", + "name: ade", + "description: ADE — Agentic Development Environment agent with project conventions and tools" + ]; + + if (allServers.length > 0) { + frontmatter.push("tools:", ...allServers.map((s) => ` - ${s.ref}`)); + } + + frontmatter.push("---"); + + const body = + config.instructions.length > 0 ? config.instructions.join("\n\n") : ""; + + const content = frontmatter.join("\n") + "\n\n" + body + "\n"; + await writeFile(join(agentsDir, "ade.agent.md"), content, "utf-8"); +} diff --git a/packages/harnesses/src/writers/cursor.spec.ts b/packages/harnesses/src/writers/cursor.spec.ts new file mode 100644 index 0000000..db8997d --- /dev/null +++ b/packages/harnesses/src/writers/cursor.spec.ts @@ -0,0 +1,101 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { LogicalConfig } from "@ade/core"; +import { cursorWriter } from "./cursor.js"; + +describe("cursorWriter", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "ade-harness-cursor-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("has correct metadata", () => { + expect(cursorWriter.id).toBe("cursor"); + expect(cursorWriter.label).toBe("Cursor"); + }); + + it("writes .cursor/mcp.json with MCP servers", async () => { + const config: LogicalConfig = { + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: {} + } + ], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [] + }; + + await cursorWriter.install(config, dir); + + const raw = await readFile(join(dir, ".cursor", "mcp.json"), "utf-8"); + const parsed = JSON.parse(raw); + expect(parsed.mcpServers["workflows"]).toEqual({ + command: "npx", + args: ["-y", "@codemcp/workflows"] + }); + }); + + it("writes .cursor/rules/ade.mdc with instructions", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: ["Follow TDD.", "Use conventional commits."], + cli_actions: [], + knowledge_sources: [], + skills: [] + }; + + await cursorWriter.install(config, dir); + + const content = await readFile( + join(dir, ".cursor", "rules", "ade.mdc"), + "utf-8" + ); + expect(content).toContain("description: ADE project conventions"); + expect(content).toContain("Follow TDD."); + expect(content).toContain("Use conventional commits."); + }); + + it("adds skills-server when skills are present", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [{ name: "my-skill", description: "A skill", body: "content" }] + }; + + await cursorWriter.install(config, dir); + + const raw = await readFile(join(dir, ".cursor", "mcp.json"), "utf-8"); + const parsed = JSON.parse(raw); + expect(parsed.mcpServers["agentskills"]).toBeDefined(); + }); + + it("skips mcp.json when no servers and no skills", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: ["hello"], + cli_actions: [], + knowledge_sources: [], + skills: [] + }; + + await cursorWriter.install(config, dir); + + await expect( + readFile(join(dir, ".cursor", "mcp.json"), "utf-8") + ).rejects.toThrow(); + }); +}); diff --git a/packages/harnesses/src/writers/cursor.ts b/packages/harnesses/src/writers/cursor.ts new file mode 100644 index 0000000..30f76cb --- /dev/null +++ b/packages/harnesses/src/writers/cursor.ts @@ -0,0 +1,82 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { LogicalConfig, McpServerEntry } from "@ade/core"; +import type { HarnessWriter } from "../types.js"; + +export const cursorWriter: HarnessWriter = { + id: "cursor", + label: "Cursor", + description: "AI code editor — .cursor/mcp.json + .cursor/rules/", + async install(config: LogicalConfig, projectRoot: string) { + await writeMcpJson(config, projectRoot); + await writeRules(config, projectRoot); + } +}; + +async function writeMcpJson( + config: LogicalConfig, + projectRoot: string +): Promise { + const allServers: McpServerEntry[] = [...config.mcp_servers]; + + if (config.skills.length > 0) { + allServers.push({ + ref: "agentskills", + command: "npx", + args: ["-y", "@codemcp/skills-server"], + env: {} + }); + } + + if (allServers.length === 0) return; + + const cursorDir = join(projectRoot, ".cursor"); + await mkdir(cursorDir, { recursive: true }); + + const mcpPath = join(cursorDir, "mcp.json"); + + let existing: Record = {}; + try { + const raw = await readFile(mcpPath, "utf-8"); + existing = JSON.parse(raw); + } catch { + // Start fresh + } + + const mcpServers: Record< + string, + { command: string; args: string[]; env?: Record } + > = (existing.mcpServers as typeof mcpServers) ?? {}; + + for (const server of allServers) { + mcpServers[server.ref] = { + command: server.command, + args: server.args, + ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) + }; + } + + const result = { ...existing, mcpServers }; + await writeFile(mcpPath, JSON.stringify(result, null, 2) + "\n", "utf-8"); +} + +async function writeRules( + config: LogicalConfig, + projectRoot: string +): Promise { + if (config.instructions.length === 0) return; + + const rulesDir = join(projectRoot, ".cursor", "rules"); + await mkdir(rulesDir, { recursive: true }); + + const content = [ + "---", + "description: ADE project conventions", + "globs: *", + "---", + "", + ...config.instructions.flatMap((i) => [i, ""]) + ].join("\n"); + + await writeFile(join(rulesDir, "ade.mdc"), content, "utf-8"); +} diff --git a/packages/harnesses/src/writers/roo-code.spec.ts b/packages/harnesses/src/writers/roo-code.spec.ts new file mode 100644 index 0000000..637a858 --- /dev/null +++ b/packages/harnesses/src/writers/roo-code.spec.ts @@ -0,0 +1,64 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { LogicalConfig } from "@ade/core"; +import { rooCodeWriter } from "./roo-code.js"; + +describe("rooCodeWriter", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "ade-harness-roo-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("has correct metadata", () => { + expect(rooCodeWriter.id).toBe("roo-code"); + expect(rooCodeWriter.label).toBe("Roo Code"); + }); + + it("writes .roo/mcp.json with MCP servers", async () => { + const config: LogicalConfig = { + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: {} + } + ], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [] + }; + + await rooCodeWriter.install(config, dir); + + const raw = await readFile(join(dir, ".roo", "mcp.json"), "utf-8"); + const parsed = JSON.parse(raw); + expect(parsed.mcpServers["workflows"]).toEqual({ + command: "npx", + args: ["-y", "@codemcp/workflows"] + }); + }); + + it("writes .roorules with instructions", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: ["Follow TDD."], + cli_actions: [], + knowledge_sources: [], + skills: [] + }; + + await rooCodeWriter.install(config, dir); + + const content = await readFile(join(dir, ".roorules"), "utf-8"); + expect(content).toContain("Follow TDD."); + }); +}); diff --git a/packages/harnesses/src/writers/roo-code.ts b/packages/harnesses/src/writers/roo-code.ts new file mode 100644 index 0000000..95b709a --- /dev/null +++ b/packages/harnesses/src/writers/roo-code.ts @@ -0,0 +1,71 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { LogicalConfig, McpServerEntry } from "@ade/core"; +import type { HarnessWriter } from "../types.js"; + +export const rooCodeWriter: HarnessWriter = { + id: "roo-code", + label: "Roo Code", + description: "AI coding agent — .roo/mcp.json + .roorules", + async install(config: LogicalConfig, projectRoot: string) { + await writeMcpJson(config, projectRoot); + await writeRules(config, projectRoot); + } +}; + +async function writeMcpJson( + config: LogicalConfig, + projectRoot: string +): Promise { + const allServers: McpServerEntry[] = [...config.mcp_servers]; + + if (config.skills.length > 0) { + allServers.push({ + ref: "agentskills", + command: "npx", + args: ["-y", "@codemcp/skills-server"], + env: {} + }); + } + + if (allServers.length === 0) return; + + const rooDir = join(projectRoot, ".roo"); + await mkdir(rooDir, { recursive: true }); + + const mcpPath = join(rooDir, "mcp.json"); + + let existing: Record = {}; + try { + const raw = await readFile(mcpPath, "utf-8"); + existing = JSON.parse(raw); + } catch { + // Start fresh + } + + const mcpServers: Record< + string, + { command: string; args: string[]; env?: Record } + > = (existing.mcpServers as typeof mcpServers) ?? {}; + + for (const server of allServers) { + mcpServers[server.ref] = { + command: server.command, + args: server.args, + ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) + }; + } + + const result = { ...existing, mcpServers }; + await writeFile(mcpPath, JSON.stringify(result, null, 2) + "\n", "utf-8"); +} + +async function writeRules( + config: LogicalConfig, + projectRoot: string +): Promise { + if (config.instructions.length === 0) return; + + const lines = config.instructions.flatMap((i) => [i, ""]); + await writeFile(join(projectRoot, ".roorules"), lines.join("\n"), "utf-8"); +} diff --git a/packages/harnesses/src/writers/windsurf.spec.ts b/packages/harnesses/src/writers/windsurf.spec.ts new file mode 100644 index 0000000..1827f24 --- /dev/null +++ b/packages/harnesses/src/writers/windsurf.spec.ts @@ -0,0 +1,65 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtemp, rm, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { LogicalConfig } from "@ade/core"; +import { windsurfWriter } from "./windsurf.js"; + +describe("windsurfWriter", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "ade-harness-windsurf-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("has correct metadata", () => { + expect(windsurfWriter.id).toBe("windsurf"); + expect(windsurfWriter.label).toBe("Windsurf"); + }); + + it("writes .windsurf/mcp.json with MCP servers", async () => { + const config: LogicalConfig = { + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: { API_KEY: "test" } + } + ], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [] + }; + + await windsurfWriter.install(config, dir); + + const raw = await readFile(join(dir, ".windsurf", "mcp.json"), "utf-8"); + const parsed = JSON.parse(raw); + expect(parsed.mcpServers["workflows"]).toEqual({ + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: { API_KEY: "test" } + }); + }); + + it("writes .windsurfrules with instructions", async () => { + const config: LogicalConfig = { + mcp_servers: [], + instructions: ["Follow TDD."], + cli_actions: [], + knowledge_sources: [], + skills: [] + }; + + await windsurfWriter.install(config, dir); + + const content = await readFile(join(dir, ".windsurfrules"), "utf-8"); + expect(content).toContain("Follow TDD."); + }); +}); diff --git a/packages/harnesses/src/writers/windsurf.ts b/packages/harnesses/src/writers/windsurf.ts new file mode 100644 index 0000000..40082aa --- /dev/null +++ b/packages/harnesses/src/writers/windsurf.ts @@ -0,0 +1,75 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { LogicalConfig, McpServerEntry } from "@ade/core"; +import type { HarnessWriter } from "../types.js"; + +export const windsurfWriter: HarnessWriter = { + id: "windsurf", + label: "Windsurf", + description: "Codeium's AI IDE — .windsurf/mcp.json + .windsurfrules", + async install(config: LogicalConfig, projectRoot: string) { + await writeMcpJson(config, projectRoot); + await writeRules(config, projectRoot); + } +}; + +async function writeMcpJson( + config: LogicalConfig, + projectRoot: string +): Promise { + const allServers: McpServerEntry[] = [...config.mcp_servers]; + + if (config.skills.length > 0) { + allServers.push({ + ref: "agentskills", + command: "npx", + args: ["-y", "@codemcp/skills-server"], + env: {} + }); + } + + if (allServers.length === 0) return; + + const windsurfDir = join(projectRoot, ".windsurf"); + await mkdir(windsurfDir, { recursive: true }); + + const mcpPath = join(windsurfDir, "mcp.json"); + + let existing: Record = {}; + try { + const raw = await readFile(mcpPath, "utf-8"); + existing = JSON.parse(raw); + } catch { + // Start fresh + } + + const mcpServers: Record< + string, + { command: string; args: string[]; env?: Record } + > = (existing.mcpServers as typeof mcpServers) ?? {}; + + for (const server of allServers) { + mcpServers[server.ref] = { + command: server.command, + args: server.args, + ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) + }; + } + + const result = { ...existing, mcpServers }; + await writeFile(mcpPath, JSON.stringify(result, null, 2) + "\n", "utf-8"); +} + +async function writeRules( + config: LogicalConfig, + projectRoot: string +): Promise { + if (config.instructions.length === 0) return; + + const lines = config.instructions.flatMap((i) => [i, ""]); + await writeFile( + join(projectRoot, ".windsurfrules"), + lines.join("\n"), + "utf-8" + ); +} diff --git a/packages/harnesses/tsconfig.build.json b/packages/harnesses/tsconfig.build.json new file mode 100644 index 0000000..7cbd949 --- /dev/null +++ b/packages/harnesses/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["**/*.spec.ts"] +} diff --git a/packages/harnesses/tsconfig.json b/packages/harnesses/tsconfig.json new file mode 100644 index 0000000..c17b099 --- /dev/null +++ b/packages/harnesses/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "." + }, + "include": ["src/**/*"] +} diff --git a/packages/harnesses/vitest.config.ts b/packages/harnesses/vitest.config.ts new file mode 100644 index 0000000..7b62873 --- /dev/null +++ b/packages/harnesses/vitest.config.ts @@ -0,0 +1,5 @@ +// @ts-check +/** @type {import("vitest.config.ts").defineConfig} */ + +const baseConfig = await import("../../vitest.config.js"); +export default baseConfig.default; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f2f223..c979928 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,6 +98,9 @@ importers: "@ade/core": specifier: workspace:* version: link:../core + "@ade/harnesses": + specifier: workspace:* + version: link:../harnesses "@clack/prompts": specifier: ^1.1.0 version: 1.1.0 @@ -158,6 +161,34 @@ importers: specifier: ^5.7.3 version: 5.9.3 + packages/harnesses: + dependencies: + "@ade/core": + specifier: workspace:* + version: link:../core + devDependencies: + "@typescript-eslint/eslint-plugin": + specifier: ^8.21.0 + version: 8.56.0(@typescript-eslint/parser@8.56.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) + "@typescript-eslint/parser": + specifier: ^8.21.0 + version: 8.56.0(eslint@9.39.2)(typescript@5.9.3) + eslint: + specifier: ^9.18.0 + version: 9.39.2 + eslint-config-prettier: + specifier: ^10.0.1 + version: 10.1.8(eslint@9.39.2) + prettier: + specifier: ^3.4.2 + version: 3.8.1 + rimraf: + specifier: ^6.0.1 + version: 6.1.3 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + packages: "@algolia/abtesting@1.15.0": resolution: diff --git a/tsconfig.json b/tsconfig.json index 0cf3371..bf218bf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,8 @@ "baseUrl": ".", "paths": { "@ade/core/*": ["packages/core/src/*"], - "@ade/cli/*": ["packages/cli/src/*"] + "@ade/cli/*": ["packages/cli/src/*"], + "@ade/harnesses/*": ["packages/harnesses/src/*"] } } } From e2715c43f2c97c59b63a0ef16372aad735c24cdf Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 05:29:55 +0000 Subject: [PATCH 45/60] fix: resolve lint errors in @ade/harnesses package Add eslint and vitest tsconfig, remove unused import. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/harnesses/eslint.config.mjs | 40 +++++++++++++++++++ .../harnesses/src/writers/claude-code.spec.ts | 2 +- packages/harnesses/tsconfig.vitest.json | 7 ++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 packages/harnesses/eslint.config.mjs create mode 100644 packages/harnesses/tsconfig.vitest.json diff --git a/packages/harnesses/eslint.config.mjs b/packages/harnesses/eslint.config.mjs new file mode 100644 index 0000000..1483555 --- /dev/null +++ b/packages/harnesses/eslint.config.mjs @@ -0,0 +1,40 @@ +import js from "@eslint/js"; +import { parser, configs } from "typescript-eslint"; +import prettier from "eslint-config-prettier"; + +export default [ + js.configs.recommended, + ...configs.recommended, + prettier, + { + // Config for TypeScript files + files: ["**/*.{ts,tsx}"], + languageOptions: { + parser, + parserOptions: { + project: ["./tsconfig.json", "./tsconfig.vitest.json"] + } + } + }, + { + // Config for JavaScript files - no TypeScript parsing + files: ["**/*.{js,jsx}"], + ...js.configs.recommended + }, + { + // Relaxed rules for test files + files: ["**/*.test.ts", "**/*.spec.ts"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": "off" + } + }, + { + ignores: [ + "**/node_modules/**", + "**/dist/**", + ".pnpm-store/**", + "pnpm-lock.yaml" + ] + } +]; diff --git a/packages/harnesses/src/writers/claude-code.spec.ts b/packages/harnesses/src/writers/claude-code.spec.ts index 125786b..02cf174 100644 --- a/packages/harnesses/src/writers/claude-code.spec.ts +++ b/packages/harnesses/src/writers/claude-code.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { mkdtemp, rm, readFile, access } from "node:fs/promises"; +import { mkdtemp, rm, readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { LogicalConfig } from "@ade/core"; diff --git a/packages/harnesses/tsconfig.vitest.json b/packages/harnesses/tsconfig.vitest.json new file mode 100644 index 0000000..f8add23 --- /dev/null +++ b/packages/harnesses/tsconfig.vitest.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "moduleResolution": "bundler" + }, + "include": ["vitest.config.ts"] +} From 62cce32207ef1b3a6e65c8da7cc98271846bb710 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 05:30:29 +0000 Subject: [PATCH 46/60] fix: add .prettierignore to @ade/harnesses to exclude dist https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/harnesses/.prettierignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/harnesses/.prettierignore diff --git a/packages/harnesses/.prettierignore b/packages/harnesses/.prettierignore new file mode 100644 index 0000000..1521c8b --- /dev/null +++ b/packages/harnesses/.prettierignore @@ -0,0 +1 @@ +dist From 7f09d01eef11752f6046252b0fec65b92541653f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 05:31:12 +0000 Subject: [PATCH 47/60] fix: correct type error in install integration test https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/cli/src/commands/install.integration.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/install.integration.spec.ts b/packages/cli/src/commands/install.integration.spec.ts index dbeff28..ae8f0d5 100644 --- a/packages/cli/src/commands/install.integration.spec.ts +++ b/packages/cli/src/commands/install.integration.spec.ts @@ -90,7 +90,7 @@ describe("install integration (real temp dir)", () => { }); it("fails when no config.lock.yaml exists", async () => { - await expect(runInstall(dir, "claude-code")).rejects.toThrow( + await expect(runInstall(dir, ["claude-code"])).rejects.toThrow( /config\.lock\.yaml not found/i ); }); From 4039fc4b3bb1ec1a1d0bdf98c688b90621faae89 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 10:37:37 +0000 Subject: [PATCH 48/60] feat: add Kiro and OpenCode harness writers with tool permissions Add harness writers for Kiro (.kiro/agents/ade.json) and OpenCode (opencode.json + .opencode/agents/ade.md). Add allowedTools field to McpServerEntry type and wire alwaysAllow tool permissions into Cline, Roo Code, and Windsurf writers (defaulting to wildcard). Remove legacy --agent CLI flag. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/cli/src/commands/install.ts | 3 +- packages/cli/src/index.ts | 9 -- packages/core/src/types.ts | 5 + packages/harnesses/src/index.spec.ts | 8 +- packages/harnesses/src/index.ts | 8 +- packages/harnesses/src/writers/cline.spec.ts | 3 +- packages/harnesses/src/writers/cline.ts | 11 +- packages/harnesses/src/writers/kiro.ts | 93 ++++++++++++++ packages/harnesses/src/writers/opencode.ts | 121 ++++++++++++++++++ .../harnesses/src/writers/roo-code.spec.ts | 3 +- packages/harnesses/src/writers/roo-code.ts | 11 +- .../harnesses/src/writers/windsurf.spec.ts | 3 +- packages/harnesses/src/writers/windsurf.ts | 11 +- 13 files changed, 266 insertions(+), 23 deletions(-) create mode 100644 packages/harnesses/src/writers/kiro.ts create mode 100644 packages/harnesses/src/writers/opencode.ts diff --git a/packages/cli/src/commands/install.ts b/packages/cli/src/commands/install.ts index 3588591..d8ca13e 100644 --- a/packages/cli/src/commands/install.ts +++ b/packages/cli/src/commands/install.ts @@ -18,8 +18,7 @@ export async function runInstall( // Determine which harnesses to install for: // 1. --harness flag (comma-separated) // 2. harnesses saved in the lock file - // 3. legacy --agent flag (mapped to harness) - // 4. default: claude-code + // 3. default: claude-code const ids = harnessIds ?? lockFile.harnesses ?? ["claude-code"]; const validIds = getHarnessIds(); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index a26f06b..e49ffa1 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -26,14 +26,6 @@ if (command === "setup") { } } - // Legacy --agent flag maps to single harness - if (!harnessIds && args.includes("--agent")) { - const val = args[args.indexOf("--agent") + 1]; - if (val) { - harnessIds = [val]; - } - } - await runInstall(projectRoot, harnessIds); } else if (command === "--version" || command === "-v") { console.log(version); @@ -55,7 +47,6 @@ if (command === "setup") { console.log( ` --harness Comma-separated harnesses (${allIds.join(", ")})` ); - console.log(" --agent Legacy alias for --harness (single value)"); console.log(" -v, --version Show version"); process.exitCode = command ? 1 : 0; } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index b6b8c47..d79513f 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -70,6 +70,11 @@ export interface McpServerEntry { command: string; args: string[]; env: Record; + /** + * Tool names the agent is pre-approved to use from this server. + * Defaults to `["*"]` (all tools) when not specified. + */ + allowedTools?: string[]; } export interface CliAction { diff --git a/packages/harnesses/src/index.spec.ts b/packages/harnesses/src/index.spec.ts index afb4233..aefc104 100644 --- a/packages/harnesses/src/index.spec.ts +++ b/packages/harnesses/src/index.spec.ts @@ -3,7 +3,7 @@ import { allHarnessWriters, getHarnessWriter, getHarnessIds } from "./index.js"; describe("harness registry", () => { it("exports all harness writers", () => { - expect(allHarnessWriters).toHaveLength(6); + expect(allHarnessWriters).toHaveLength(8); const ids = allHarnessWriters.map((w) => w.id); expect(ids).toContain("claude-code"); expect(ids).toContain("cursor"); @@ -11,6 +11,8 @@ describe("harness registry", () => { expect(ids).toContain("windsurf"); expect(ids).toContain("cline"); expect(ids).toContain("roo-code"); + expect(ids).toContain("kiro"); + expect(ids).toContain("opencode"); }); it("looks up harness by id", () => { @@ -26,7 +28,9 @@ describe("harness registry", () => { "copilot", "windsurf", "cline", - "roo-code" + "roo-code", + "kiro", + "opencode" ]); }); diff --git a/packages/harnesses/src/index.ts b/packages/harnesses/src/index.ts index 46be492..f6d3393 100644 --- a/packages/harnesses/src/index.ts +++ b/packages/harnesses/src/index.ts @@ -6,6 +6,8 @@ export { copilotWriter } from "./writers/copilot.js"; export { windsurfWriter } from "./writers/windsurf.js"; export { clineWriter } from "./writers/cline.js"; export { rooCodeWriter } from "./writers/roo-code.js"; +export { kiroWriter } from "./writers/kiro.js"; +export { opencodeWriter } from "./writers/opencode.js"; import type { HarnessWriter } from "./types.js"; import { claudeCodeWriter } from "./writers/claude-code.js"; @@ -14,6 +16,8 @@ import { copilotWriter } from "./writers/copilot.js"; import { windsurfWriter } from "./writers/windsurf.js"; import { clineWriter } from "./writers/cline.js"; import { rooCodeWriter } from "./writers/roo-code.js"; +import { kiroWriter } from "./writers/kiro.js"; +import { opencodeWriter } from "./writers/opencode.js"; /** All built-in harness writers, ordered for wizard display. */ export const allHarnessWriters: HarnessWriter[] = [ @@ -22,7 +26,9 @@ export const allHarnessWriters: HarnessWriter[] = [ copilotWriter, windsurfWriter, clineWriter, - rooCodeWriter + rooCodeWriter, + kiroWriter, + opencodeWriter ]; /** Look up a harness writer by id. */ diff --git a/packages/harnesses/src/writers/cline.spec.ts b/packages/harnesses/src/writers/cline.spec.ts index 036b90b..8b1828c 100644 --- a/packages/harnesses/src/writers/cline.spec.ts +++ b/packages/harnesses/src/writers/cline.spec.ts @@ -43,7 +43,8 @@ describe("clineWriter", () => { const parsed = JSON.parse(raw); expect(parsed.mcpServers["workflows"]).toEqual({ command: "npx", - args: ["-y", "@codemcp/workflows"] + args: ["-y", "@codemcp/workflows"], + alwaysAllow: ["*"] }); }); diff --git a/packages/harnesses/src/writers/cline.ts b/packages/harnesses/src/writers/cline.ts index 74ec579..576f272 100644 --- a/packages/harnesses/src/writers/cline.ts +++ b/packages/harnesses/src/writers/cline.ts @@ -45,14 +45,21 @@ async function writeMcpJson( const mcpServers: Record< string, - { command: string; args: string[]; env?: Record } + { + command: string; + args: string[]; + env?: Record; + alwaysAllow?: string[]; + } > = (existing.mcpServers as typeof mcpServers) ?? {}; for (const server of allServers) { + const allowed = server.allowedTools ?? ["*"]; mcpServers[server.ref] = { command: server.command, args: server.args, - ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) + ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}), + alwaysAllow: allowed }; } diff --git a/packages/harnesses/src/writers/kiro.ts b/packages/harnesses/src/writers/kiro.ts new file mode 100644 index 0000000..2167e3b --- /dev/null +++ b/packages/harnesses/src/writers/kiro.ts @@ -0,0 +1,93 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { LogicalConfig, McpServerEntry } from "@ade/core"; +import type { HarnessWriter } from "../types.js"; + +export const kiroWriter: HarnessWriter = { + id: "kiro", + label: "Kiro", + description: "AWS AI IDE — .kiro/agents/ade.json", + async install(config: LogicalConfig, projectRoot: string) { + await writeAgentJson(config, projectRoot); + } +}; + +async function writeAgentJson( + config: LogicalConfig, + projectRoot: string +): Promise { + const allServers: McpServerEntry[] = [...config.mcp_servers]; + + if (config.skills.length > 0) { + allServers.push({ + ref: "agentskills", + command: "npx", + args: ["-y", "@codemcp/skills-server"], + env: {} + }); + } + + if (allServers.length === 0 && config.instructions.length === 0) return; + + const agentsDir = join(projectRoot, ".kiro", "agents"); + await mkdir(agentsDir, { recursive: true }); + + const agentPath = join(agentsDir, "ade.json"); + + const mcpServers: Record< + string, + { command: string; args: string[]; env?: Record } + > = {}; + + for (const server of allServers) { + mcpServers[server.ref] = { + command: server.command, + args: server.args, + ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) + }; + } + + // Kiro tools: built-in tools + @server references + const tools: string[] = [ + "execute_bash", + "fs_read", + "fs_write", + "knowledge", + "thinking" + ]; + for (const name of Object.keys(mcpServers)) { + tools.push(`@${name}`); + } + + // Kiro allowedTools: grant wildcard access to each MCP server + const allowedTools: string[] = []; + for (const server of allServers) { + const explicit = server.allowedTools; + if (explicit && !explicit.includes("*")) { + for (const tool of explicit) { + allowedTools.push(`@${server.ref}/${tool}`); + } + } else { + allowedTools.push(`@${server.ref}/*`); + } + } + + const prompt = + config.instructions.length > 0 + ? config.instructions.join("\n\n") + : "ADE — Agentic Development Environment agent"; + + const agentConfig = { + name: "ade", + prompt, + mcpServers, + tools, + allowedTools + }; + + await writeFile( + agentPath, + JSON.stringify(agentConfig, null, 2) + "\n", + "utf-8" + ); +} diff --git a/packages/harnesses/src/writers/opencode.ts b/packages/harnesses/src/writers/opencode.ts new file mode 100644 index 0000000..797a94f --- /dev/null +++ b/packages/harnesses/src/writers/opencode.ts @@ -0,0 +1,121 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { LogicalConfig, McpServerEntry } from "@ade/core"; +import type { HarnessWriter } from "../types.js"; + +export const opencodeWriter: HarnessWriter = { + id: "opencode", + label: "OpenCode", + description: "Terminal AI agent — opencode.json + .opencode/agents/", + async install(config: LogicalConfig, projectRoot: string) { + await writeOpenCodeJson(config, projectRoot); + await writeAgentMd(config, projectRoot); + } +}; + +async function writeOpenCodeJson( + config: LogicalConfig, + projectRoot: string +): Promise { + const allServers: McpServerEntry[] = [...config.mcp_servers]; + + if (config.skills.length > 0) { + allServers.push({ + ref: "agentskills", + command: "npx", + args: ["-y", "@codemcp/skills-server"], + env: {} + }); + } + + if (allServers.length === 0) return; + + const configPath = join(projectRoot, "opencode.json"); + + let existing: Record = {}; + try { + const raw = await readFile(configPath, "utf-8"); + existing = JSON.parse(raw); + } catch { + // Start fresh + } + + const mcpServers: Record< + string, + { command: string[]; env?: Record } + > = (existing.mcp_servers as typeof mcpServers) ?? {}; + + for (const server of allServers) { + mcpServers[server.ref] = { + command: [server.command, ...server.args], + ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) + }; + } + + const result = { + $schema: "https://opencode.ai/config.json", + ...existing, + mcp_servers: mcpServers + }; + + await writeFile(configPath, JSON.stringify(result, null, 2) + "\n", "utf-8"); +} + +async function writeAgentMd( + config: LogicalConfig, + projectRoot: string +): Promise { + const allServers: McpServerEntry[] = [...config.mcp_servers]; + if (config.skills.length > 0) { + allServers.push({ + ref: "agentskills", + command: "npx", + args: ["-y", "@codemcp/skills-server"], + env: {} + }); + } + + if (config.instructions.length === 0 && allServers.length === 0) return; + + const agentsDir = join(projectRoot, ".opencode", "agents"); + await mkdir(agentsDir, { recursive: true }); + + const frontmatter: string[] = [ + "---", + "name: ade", + "description: ADE — Agentic Development Environment agent" + ]; + + // Tool permissions + frontmatter.push("tools:"); + frontmatter.push(" read: true"); + frontmatter.push(" edit: approve"); + frontmatter.push(" bash: approve"); + + // MCP server references + if (allServers.length > 0) { + frontmatter.push("mcp_servers:"); + for (const server of allServers) { + frontmatter.push(` ${server.ref}:`); + frontmatter.push( + ` command: [${[server.command, ...server.args].map((a) => `"${a}"`).join(", ")}]` + ); + if (Object.keys(server.env).length > 0) { + frontmatter.push(" env:"); + for (const [k, v] of Object.entries(server.env)) { + frontmatter.push(` ${k}: "${v}"`); + } + } + } + } + + frontmatter.push("---"); + + const body = + config.instructions.length > 0 + ? config.instructions.join("\n\n") + : "ADE — Agentic Development Environment agent with project conventions and tools."; + + const content = frontmatter.join("\n") + "\n\n" + body + "\n"; + await writeFile(join(agentsDir, "ade.md"), content, "utf-8"); +} diff --git a/packages/harnesses/src/writers/roo-code.spec.ts b/packages/harnesses/src/writers/roo-code.spec.ts index 637a858..c9dee42 100644 --- a/packages/harnesses/src/writers/roo-code.spec.ts +++ b/packages/harnesses/src/writers/roo-code.spec.ts @@ -43,7 +43,8 @@ describe("rooCodeWriter", () => { const parsed = JSON.parse(raw); expect(parsed.mcpServers["workflows"]).toEqual({ command: "npx", - args: ["-y", "@codemcp/workflows"] + args: ["-y", "@codemcp/workflows"], + alwaysAllow: ["*"] }); }); diff --git a/packages/harnesses/src/writers/roo-code.ts b/packages/harnesses/src/writers/roo-code.ts index 95b709a..eab8c92 100644 --- a/packages/harnesses/src/writers/roo-code.ts +++ b/packages/harnesses/src/writers/roo-code.ts @@ -45,14 +45,21 @@ async function writeMcpJson( const mcpServers: Record< string, - { command: string; args: string[]; env?: Record } + { + command: string; + args: string[]; + env?: Record; + alwaysAllow?: string[]; + } > = (existing.mcpServers as typeof mcpServers) ?? {}; for (const server of allServers) { + const allowed = server.allowedTools ?? ["*"]; mcpServers[server.ref] = { command: server.command, args: server.args, - ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) + ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}), + alwaysAllow: allowed }; } diff --git a/packages/harnesses/src/writers/windsurf.spec.ts b/packages/harnesses/src/writers/windsurf.spec.ts index 1827f24..2c72620 100644 --- a/packages/harnesses/src/writers/windsurf.spec.ts +++ b/packages/harnesses/src/writers/windsurf.spec.ts @@ -44,7 +44,8 @@ describe("windsurfWriter", () => { expect(parsed.mcpServers["workflows"]).toEqual({ command: "npx", args: ["-y", "@codemcp/workflows"], - env: { API_KEY: "test" } + env: { API_KEY: "test" }, + alwaysAllow: ["*"] }); }); diff --git a/packages/harnesses/src/writers/windsurf.ts b/packages/harnesses/src/writers/windsurf.ts index 40082aa..c6b66a2 100644 --- a/packages/harnesses/src/writers/windsurf.ts +++ b/packages/harnesses/src/writers/windsurf.ts @@ -45,14 +45,21 @@ async function writeMcpJson( const mcpServers: Record< string, - { command: string; args: string[]; env?: Record } + { + command: string; + args: string[]; + env?: Record; + alwaysAllow?: string[]; + } > = (existing.mcpServers as typeof mcpServers) ?? {}; for (const server of allServers) { + const allowed = server.allowedTools ?? ["*"]; mcpServers[server.ref] = { command: server.command, args: server.args, - ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) + ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}), + alwaysAllow: allowed }; } From 4ae91cf6ced369a1173201640e6aff0d43357e6e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 10:38:15 +0000 Subject: [PATCH 49/60] fix: remove unused readFile import in kiro writer https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/harnesses/src/writers/kiro.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/harnesses/src/writers/kiro.ts b/packages/harnesses/src/writers/kiro.ts index 2167e3b..693324c 100644 --- a/packages/harnesses/src/writers/kiro.ts +++ b/packages/harnesses/src/writers/kiro.ts @@ -1,4 +1,4 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import type { LogicalConfig, McpServerEntry } from "@ade/core"; import type { HarnessWriter } from "../types.js"; From d0c5910d9a693dc8dc8eed0f8b360228e9bac1b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 12:20:04 +0000 Subject: [PATCH 50/60] feat: add universal harness, custom agents, and improve tool fidelity - Add "universal" harness writer (AGENTS.md + .mcp.json) as cross-tool standard, set as default instead of claude-code - Update Claude Code writer to generate .claude/agents/ade.md custom agent definition, .mcp.json for MCP servers, and .claude/settings.json with MCP tool permission allow-rules - Update Copilot writer to include built-in tools + MCP wildcards in .github/agents/ade.agent.md tools list - Fix OpenCode writer to use "mcp" top-level key (not "mcp_servers") with type:"local" per upstream reference implementation - Update all integration tests for new file locations https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- .../commands/conventions.integration.spec.ts | 19 +- .../src/commands/install.integration.spec.ts | 24 +-- packages/cli/src/commands/install.spec.ts | 11 +- packages/cli/src/commands/install.ts | 4 +- .../commands/knowledge.integration.spec.ts | 8 +- .../src/commands/setup.integration.spec.ts | 26 +-- packages/cli/src/commands/setup.ts | 2 +- packages/harnesses/src/index.spec.ts | 4 +- packages/harnesses/src/index.ts | 3 + .../harnesses/src/writers/claude-code.spec.ts | 70 +++++--- packages/harnesses/src/writers/claude-code.ts | 163 ++++++++++++++---- .../harnesses/src/writers/copilot.spec.ts | 3 +- packages/harnesses/src/writers/copilot.ts | 14 +- packages/harnesses/src/writers/opencode.ts | 11 +- packages/harnesses/src/writers/universal.ts | 78 +++++++++ 15 files changed, 330 insertions(+), 110 deletions(-) create mode 100644 packages/harnesses/src/writers/universal.ts diff --git a/packages/cli/src/commands/conventions.integration.spec.ts b/packages/cli/src/commands/conventions.integration.spec.ts index f4d655a..2a660bd 100644 --- a/packages/cli/src/commands/conventions.integration.spec.ts +++ b/packages/cli/src/commands/conventions.integration.spec.ts @@ -82,11 +82,11 @@ describe("architecture and practices facets integration", () => { const skillsLock = JSON.parse(lockRaw); expect(skillsLock.skills).toBeDefined(); - // skills-server MCP server should be in settings.json - const settings = JSON.parse( - await readFile(join(dir, ".claude", "settings.json"), "utf-8") + // skills-server MCP server should be in .mcp.json + const mcpJson = JSON.parse( + await readFile(join(dir, ".mcp.json"), "utf-8") ); - expect(settings.mcpServers["agentskills"]).toMatchObject({ + expect(mcpJson.mcpServers["agentskills"]).toMatchObject({ command: "npx", args: ["-y", "@codemcp/skills-server"] }); @@ -187,7 +187,7 @@ describe("architecture and practices facets integration", () => { expect(config!.choices).not.toHaveProperty("practices"); }); - it("includes practice instructions in AGENTS.md", async () => { + it("includes practice instructions in custom agent", async () => { const catalog = getDefaultCatalog(); // Facet order: process (select), architecture (select), practices (multiselect) @@ -200,9 +200,12 @@ describe("architecture and practices facets integration", () => { await runSetup(dir, catalog); - const agentsMd = await readFile(join(dir, "AGENTS.md"), "utf-8"); - expect(agentsMd).toContain("tdd-london"); - expect(agentsMd).toContain("use_skill()"); + const agentMd = await readFile( + join(dir, ".claude", "agents", "ade.md"), + "utf-8" + ); + expect(agentMd).toContain("tdd-london"); + expect(agentMd).toContain("use_skill()"); }); it( diff --git a/packages/cli/src/commands/install.integration.spec.ts b/packages/cli/src/commands/install.integration.spec.ts index ae8f0d5..9eceb85 100644 --- a/packages/cli/src/commands/install.integration.spec.ts +++ b/packages/cli/src/commands/install.integration.spec.ts @@ -45,20 +45,21 @@ describe("install integration (real temp dir)", () => { await runSetup(dir, catalog); // Step 2: Delete agent output files to simulate a fresh clone - await rm(join(dir, "AGENTS.md")); + await rm(join(dir, ".mcp.json")); await rm(join(dir, ".claude"), { recursive: true, force: true }); // Step 3: Run install — should regenerate from config.lock.yaml await runInstall(dir, ["claude-code"]); // Agent files should be back - const agentsMd = await readFile(join(dir, "AGENTS.md"), "utf-8"); - expect(agentsMd).toContain("Call whats_next()"); - - const settings = JSON.parse( - await readFile(join(dir, ".claude", "settings.json"), "utf-8") + const agentMd = await readFile( + join(dir, ".claude", "agents", "ade.md"), + "utf-8" ); - expect(settings.mcpServers["workflows"]).toMatchObject({ + expect(agentMd).toContain("Call whats_next()"); + + const mcpJson = JSON.parse(await readFile(join(dir, ".mcp.json"), "utf-8")); + expect(mcpJson.mcpServers["workflows"]).toMatchObject({ command: "npx", args: ["@codemcp/workflows-server@latest"] }); @@ -108,12 +109,15 @@ describe("install integration (real temp dir)", () => { await runSetup(dir, catalog); // Delete agent output - await rm(join(dir, "AGENTS.md")); + await rm(join(dir, ".claude"), { recursive: true, force: true }); // Re-install await runInstall(dir, ["claude-code"]); - const agentsMd = await readFile(join(dir, "AGENTS.md"), "utf-8"); - expect(agentsMd).toContain("AGENTS.md"); + const agentMd = await readFile( + join(dir, ".claude", "agents", "ade.md"), + "utf-8" + ); + expect(agentMd).toContain("AGENTS.md"); }); }); diff --git a/packages/cli/src/commands/install.spec.ts b/packages/cli/src/commands/install.spec.ts index 8c63ad2..3093751 100644 --- a/packages/cli/src/commands/install.spec.ts +++ b/packages/cli/src/commands/install.spec.ts @@ -33,7 +33,7 @@ const mockInstall = vi.fn().mockResolvedValue(undefined); vi.mock("@ade/harnesses", () => ({ getHarnessWriter: vi.fn().mockImplementation((id: string) => { - if (id === "claude-code" || id === "cursor") { + if (id === "universal" || id === "claude-code" || id === "cursor") { return { id, install: mockInstall }; } return undefined; @@ -41,12 +41,15 @@ vi.mock("@ade/harnesses", () => ({ getHarnessIds: vi .fn() .mockReturnValue([ + "universal", "claude-code", "cursor", "copilot", "windsurf", "cline", - "roo-code" + "roo-code", + "kiro", + "opencode" ]) })); @@ -62,7 +65,7 @@ describe("runInstall", () => { // Re-set the default implementation after clearAllMocks const { getHarnessWriter } = await import("@ade/harnesses"); vi.mocked(getHarnessWriter).mockImplementation((id: string) => { - if (id === "claude-code" || id === "cursor") { + if (id === "universal" || id === "claude-code" || id === "cursor") { return { id, label: id, @@ -87,7 +90,7 @@ describe("runInstall", () => { expect(readLockFile).toHaveBeenCalledWith("/tmp/project"); }); - it("defaults to claude-code harness when none specified", async () => { + it("defaults to universal harness when none specified", async () => { vi.mocked(readLockFile).mockResolvedValueOnce({ version: 1, generated_at: "2024-01-01T00:00:00.000Z", diff --git a/packages/cli/src/commands/install.ts b/packages/cli/src/commands/install.ts index d8ca13e..17ac0ff 100644 --- a/packages/cli/src/commands/install.ts +++ b/packages/cli/src/commands/install.ts @@ -18,8 +18,8 @@ export async function runInstall( // Determine which harnesses to install for: // 1. --harness flag (comma-separated) // 2. harnesses saved in the lock file - // 3. default: claude-code - const ids = harnessIds ?? lockFile.harnesses ?? ["claude-code"]; + // 3. default: universal + const ids = harnessIds ?? lockFile.harnesses ?? ["universal"]; const validIds = getHarnessIds(); for (const id of ids) { diff --git a/packages/cli/src/commands/knowledge.integration.spec.ts b/packages/cli/src/commands/knowledge.integration.spec.ts index 9d2bfc9..d491aa6 100644 --- a/packages/cli/src/commands/knowledge.integration.spec.ts +++ b/packages/cli/src/commands/knowledge.integration.spec.ts @@ -111,11 +111,11 @@ describe("knowledge integration", () => { ]) ); - // MCP server entry for knowledge-server should be in settings.json - const settings = JSON.parse( - await readFile(join(dir, ".claude", "settings.json"), "utf-8") + // MCP server entry for knowledge-server should be in .mcp.json + const mcpJson = JSON.parse( + await readFile(join(dir, ".mcp.json"), "utf-8") ); - expect(settings.mcpServers["@codemcp/knowledge-server"]).toMatchObject({ + expect(mcpJson.mcpServers["@codemcp/knowledge-server"]).toMatchObject({ command: "npx", args: ["-y", "@codemcp/knowledge-server"] }); diff --git a/packages/cli/src/commands/setup.integration.spec.ts b/packages/cli/src/commands/setup.integration.spec.ts index c5566a5..6e77cf2 100644 --- a/packages/cli/src/commands/setup.integration.spec.ts +++ b/packages/cli/src/commands/setup.integration.spec.ts @@ -62,19 +62,20 @@ describe("setup integration (real temp dir)", () => { expect(lc.mcp_servers[0].ref).toBe("workflows"); expect(lc.instructions.length).toBeGreaterThan(0); - // ── Agent output: .claude/settings.json ────────────────────────────── + // ── Agent output: .mcp.json ───────────────────────────────────────── const { readFile } = await import("node:fs/promises"); - const settings = JSON.parse( - await readFile(join(dir, ".claude", "settings.json"), "utf-8") - ); - expect(settings.mcpServers["workflows"]).toMatchObject({ + const mcpJson = JSON.parse(await readFile(join(dir, ".mcp.json"), "utf-8")); + expect(mcpJson.mcpServers["workflows"]).toMatchObject({ command: "npx", args: ["@codemcp/workflows-server@latest"] }); - // ── Agent output: AGENTS.md ───────────────────────────────────────── - const agentsMd = await readFile(join(dir, "AGENTS.md"), "utf-8"); - expect(agentsMd).toContain("Call whats_next()"); + // ── Agent output: .claude/agents/ade.md ──────────────────────────── + const agentMd = await readFile( + join(dir, ".claude", "agents", "ade.md"), + "utf-8" + ); + expect(agentMd).toContain("Call whats_next()"); }); it("writes config.yaml, lock, and AGENTS.md for native-agents-md", async () => { @@ -96,10 +97,13 @@ describe("setup integration (real temp dir)", () => { expect(lock!.choices).toEqual({ process: "native-agents-md" }); expect(lock!.logical_config.instructions.length).toBeGreaterThan(0); - // Agent output: AGENTS.md is written with instruction text + // Agent output: .claude/agents/ade.md is written with instruction text const { readFile } = await import("node:fs/promises"); - const agentsMd = await readFile(join(dir, "AGENTS.md"), "utf-8"); - expect(agentsMd).toContain("AGENTS.md"); + const agentMd = await readFile( + join(dir, ".claude", "agents", "ade.md"), + "utf-8" + ); + expect(agentMd).toContain("AGENTS.md"); }); it("does not write any files when user cancels", async () => { diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 9c979c4..3e4dccc 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -113,7 +113,7 @@ export async function runSetup( initialValues: validExistingHarnesses && validExistingHarnesses.length > 0 ? validExistingHarnesses - : ["claude-code"], + : ["universal"], required: false }); diff --git a/packages/harnesses/src/index.spec.ts b/packages/harnesses/src/index.spec.ts index aefc104..e3970fe 100644 --- a/packages/harnesses/src/index.spec.ts +++ b/packages/harnesses/src/index.spec.ts @@ -3,8 +3,9 @@ import { allHarnessWriters, getHarnessWriter, getHarnessIds } from "./index.js"; describe("harness registry", () => { it("exports all harness writers", () => { - expect(allHarnessWriters).toHaveLength(8); + expect(allHarnessWriters).toHaveLength(9); const ids = allHarnessWriters.map((w) => w.id); + expect(ids).toContain("universal"); expect(ids).toContain("claude-code"); expect(ids).toContain("cursor"); expect(ids).toContain("copilot"); @@ -23,6 +24,7 @@ describe("harness registry", () => { it("returns all harness ids", () => { const ids = getHarnessIds(); expect(ids).toEqual([ + "universal", "claude-code", "cursor", "copilot", diff --git a/packages/harnesses/src/index.ts b/packages/harnesses/src/index.ts index f6d3393..d46526b 100644 --- a/packages/harnesses/src/index.ts +++ b/packages/harnesses/src/index.ts @@ -1,5 +1,6 @@ export type { HarnessWriter } from "./types.js"; +export { universalWriter } from "./writers/universal.js"; export { claudeCodeWriter } from "./writers/claude-code.js"; export { cursorWriter } from "./writers/cursor.js"; export { copilotWriter } from "./writers/copilot.js"; @@ -10,6 +11,7 @@ export { kiroWriter } from "./writers/kiro.js"; export { opencodeWriter } from "./writers/opencode.js"; import type { HarnessWriter } from "./types.js"; +import { universalWriter } from "./writers/universal.js"; import { claudeCodeWriter } from "./writers/claude-code.js"; import { cursorWriter } from "./writers/cursor.js"; import { copilotWriter } from "./writers/copilot.js"; @@ -21,6 +23,7 @@ import { opencodeWriter } from "./writers/opencode.js"; /** All built-in harness writers, ordered for wizard display. */ export const allHarnessWriters: HarnessWriter[] = [ + universalWriter, claudeCodeWriter, cursorWriter, copilotWriter, diff --git a/packages/harnesses/src/writers/claude-code.spec.ts b/packages/harnesses/src/writers/claude-code.spec.ts index 02cf174..dcac396 100644 --- a/packages/harnesses/src/writers/claude-code.spec.ts +++ b/packages/harnesses/src/writers/claude-code.spec.ts @@ -22,9 +22,16 @@ describe("claudeCodeWriter", () => { expect(claudeCodeWriter.description).toBeTruthy(); }); - it("writes AGENTS.md with instructions", async () => { + it("writes .claude/agents/ade.md custom agent", async () => { const config: LogicalConfig = { - mcp_servers: [], + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: {} + } + ], instructions: ["Use workflow files.", "Follow conventions."], cli_actions: [], knowledge_sources: [], @@ -33,13 +40,17 @@ describe("claudeCodeWriter", () => { await claudeCodeWriter.install(config, dir); - const content = await readFile(join(dir, "AGENTS.md"), "utf-8"); - expect(content).toContain("# AGENTS"); + const content = await readFile( + join(dir, ".claude", "agents", "ade.md"), + "utf-8" + ); + expect(content).toContain("name: ade"); + expect(content).toContain("description:"); expect(content).toContain("Use workflow files."); expect(content).toContain("Follow conventions."); }); - it("writes .claude/settings.json with MCP servers", async () => { + it("writes .mcp.json with MCP servers", async () => { const config: LogicalConfig = { mcp_servers: [ { @@ -57,14 +68,37 @@ describe("claudeCodeWriter", () => { await claudeCodeWriter.install(config, dir); - const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8"); - const settings = JSON.parse(raw); - expect(settings.mcpServers["@codemcp/workflows"]).toEqual({ + const raw = await readFile(join(dir, ".mcp.json"), "utf-8"); + const parsed = JSON.parse(raw); + expect(parsed.mcpServers["@codemcp/workflows"]).toEqual({ command: "npx", args: ["-y", "@codemcp/workflows"] }); }); + it("writes .claude/settings.json with MCP tool permissions", async () => { + const config: LogicalConfig = { + mcp_servers: [ + { + ref: "workflows", + command: "npx", + args: ["-y", "@codemcp/workflows"], + env: {} + } + ], + instructions: [], + cli_actions: [], + knowledge_sources: [], + skills: [] + }; + + await claudeCodeWriter.install(config, dir); + + const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8"); + const settings = JSON.parse(raw); + expect(settings.permissions.allow).toContain("MCP(workflows:*)"); + }); + it("adds skills-server when skills are present", async () => { const config: LogicalConfig = { mcp_servers: [], @@ -76,9 +110,9 @@ describe("claudeCodeWriter", () => { await claudeCodeWriter.install(config, dir); - const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8"); - const settings = JSON.parse(raw); - expect(settings.mcpServers["agentskills"]).toEqual({ + const raw = await readFile(join(dir, ".mcp.json"), "utf-8"); + const parsed = JSON.parse(raw); + expect(parsed.mcpServers["agentskills"]).toEqual({ command: "npx", args: ["-y", "@codemcp/skills-server"] }); @@ -108,18 +142,4 @@ describe("claudeCodeWriter", () => { expect(skillMd).toContain("name: tanstack-architecture"); expect(skillMd).toContain("# Architecture"); }); - - it("skips AGENTS.md when no instructions", async () => { - const config: LogicalConfig = { - mcp_servers: [], - instructions: [], - cli_actions: [], - knowledge_sources: [], - skills: [] - }; - - await claudeCodeWriter.install(config, dir); - - await expect(readFile(join(dir, "AGENTS.md"), "utf-8")).rejects.toThrow(); - }); }); diff --git a/packages/harnesses/src/writers/claude-code.ts b/packages/harnesses/src/writers/claude-code.ts index 931d7fd..4e5bb08 100644 --- a/packages/harnesses/src/writers/claude-code.ts +++ b/packages/harnesses/src/writers/claude-code.ts @@ -12,52 +12,107 @@ function isInlineSkill( export const claudeCodeWriter: HarnessWriter = { id: "claude-code", label: "Claude Code", - description: "Anthropic's CLI agent — .claude/settings.json + AGENTS.md", + description: + "Anthropic's CLI agent — .claude/agents/ade.md + .mcp.json + .claude/settings.json", async install(config: LogicalConfig, projectRoot: string) { - await writeAgentsMd(config, projectRoot); - await writeSkills(config, projectRoot); + await writeCustomAgent(config, projectRoot); + await writeMcpJson(config, projectRoot); await writeClaudeSettings(config, projectRoot); + await writeSkills(config, projectRoot); } }; -async function writeAgentsMd( +/** + * Write .claude/agents/ade.md — the preferred custom agent definition + * that combines instructions and MCP tool references. + */ +async function writeCustomAgent( config: LogicalConfig, projectRoot: string ): Promise { - if (config.instructions.length === 0) return; - - const lines = ["# AGENTS", ""]; - for (const instruction of config.instructions) { - lines.push(instruction, ""); + const allServers: McpServerEntry[] = [...config.mcp_servers]; + if (config.skills.length > 0) { + allServers.push({ + ref: "agentskills", + command: "npx", + args: ["-y", "@codemcp/skills-server"], + env: {} + }); } - await writeFile(join(projectRoot, "AGENTS.md"), lines.join("\n"), "utf-8"); + if (config.instructions.length === 0 && allServers.length === 0) return; + + const agentsDir = join(projectRoot, ".claude", "agents"); + await mkdir(agentsDir, { recursive: true }); + + const frontmatter: string[] = [ + "---", + "name: ade", + "description: ADE — Agentic Development Environment agent with project conventions and tools" + ]; + + frontmatter.push("---"); + + const body = + config.instructions.length > 0 + ? config.instructions.join("\n\n") + : "ADE — Agentic Development Environment agent."; + + const content = frontmatter.join("\n") + "\n\n" + body + "\n"; + await writeFile(join(agentsDir, "ade.md"), content, "utf-8"); } -async function writeSkills( +/** + * Write .mcp.json — the standard MCP config at project root. + * Claude Code reads this natively. + */ +async function writeMcpJson( config: LogicalConfig, projectRoot: string ): Promise { - if (config.skills.length === 0) return; + const allServers: McpServerEntry[] = [...config.mcp_servers]; - for (const skill of config.skills) { - if (!isInlineSkill(skill)) continue; + if (config.skills.length > 0) { + allServers.push({ + ref: "agentskills", + command: "npx", + args: ["-y", "@codemcp/skills-server"], + env: {} + }); + } - const skillDir = join(projectRoot, ".ade", "skills", skill.name); - await mkdir(skillDir, { recursive: true }); + if (allServers.length === 0) return; - const frontmatter = [ - "---", - `name: ${skill.name}`, - `description: ${skill.description}`, - "---" - ].join("\n"); + const mcpPath = join(projectRoot, ".mcp.json"); - const content = `${frontmatter}\n\n${skill.body}\n`; - await writeFile(join(skillDir, "SKILL.md"), content, "utf-8"); + let existing: Record = {}; + try { + const raw = await readFile(mcpPath, "utf-8"); + existing = JSON.parse(raw); + } catch { + // Start fresh } + + const mcpServers: Record< + string, + { command: string; args: string[]; env?: Record } + > = (existing.mcpServers as typeof mcpServers) ?? {}; + + for (const server of allServers) { + mcpServers[server.ref] = { + command: server.command, + args: server.args, + ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) + }; + } + + const result = { ...existing, mcpServers }; + await writeFile(mcpPath, JSON.stringify(result, null, 2) + "\n", "utf-8"); } +/** + * Write .claude/settings.json — permissions for MCP tools. + */ async function writeClaudeSettings( config: LogicalConfig, projectRoot: string @@ -88,23 +143,61 @@ async function writeClaudeSettings( // No existing file — start fresh } - const mcpServers: Record< - string, - { command: string; args: string[]; env?: Record } - > = (existing.mcpServers as typeof mcpServers) ?? {}; - + // Build permission allow-list for MCP tools + const allowRules: string[] = []; for (const server of allServers) { - mcpServers[server.ref] = { - command: server.command, - args: server.args, - ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) - }; + const allowed = server.allowedTools ?? ["*"]; + if (allowed.includes("*")) { + allowRules.push(`MCP(${server.ref}:*)`); + } else { + for (const tool of allowed) { + allowRules.push(`MCP(${server.ref}:${tool})`); + } + } } - const settings = { ...existing, mcpServers }; + const existingPermissions = + (existing.permissions as Record) ?? {}; + const existingAllow = (existingPermissions.allow as string[]) ?? []; + + // Merge: keep existing rules, add new ones + const mergedAllow = [...new Set([...existingAllow, ...allowRules])]; + + const settings = { + ...existing, + permissions: { + ...existingPermissions, + allow: mergedAllow + } + }; + await writeFile( settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8" ); } + +async function writeSkills( + config: LogicalConfig, + projectRoot: string +): Promise { + if (config.skills.length === 0) return; + + for (const skill of config.skills) { + if (!isInlineSkill(skill)) continue; + + const skillDir = join(projectRoot, ".ade", "skills", skill.name); + await mkdir(skillDir, { recursive: true }); + + const frontmatter = [ + "---", + `name: ${skill.name}`, + `description: ${skill.description}`, + "---" + ].join("\n"); + + const content = `${frontmatter}\n\n${skill.body}\n`; + await writeFile(join(skillDir, "SKILL.md"), content, "utf-8"); + } +} diff --git a/packages/harnesses/src/writers/copilot.spec.ts b/packages/harnesses/src/writers/copilot.spec.ts index 45b0a25..838c73c 100644 --- a/packages/harnesses/src/writers/copilot.spec.ts +++ b/packages/harnesses/src/writers/copilot.spec.ts @@ -92,7 +92,8 @@ describe("copilotWriter", () => { ); expect(content).toContain("name: ade"); expect(content).toContain("tools:"); - expect(content).toContain(" - workflows"); + expect(content).toContain(" - workflows/*"); + expect(content).toContain(" - edit"); expect(content).toContain("Follow TDD."); }); }); diff --git a/packages/harnesses/src/writers/copilot.ts b/packages/harnesses/src/writers/copilot.ts index 9ca7ce2..c1b95ba 100644 --- a/packages/harnesses/src/writers/copilot.ts +++ b/packages/harnesses/src/writers/copilot.ts @@ -115,9 +115,17 @@ async function writeCopilotAgent( "description: ADE — Agentic Development Environment agent with project conventions and tools" ]; - if (allServers.length > 0) { - frontmatter.push("tools:", ...allServers.map((s) => ` - ${s.ref}`)); - } + // Built-in tools + MCP server wildcards (server/* grants all tools) + const tools = [ + "edit", + "search", + "runCommands", + "runTasks", + "fetch", + "githubRepo", + ...allServers.map((s) => `${s.ref}/*`) + ]; + frontmatter.push("tools:", ...tools.map((t) => ` - ${t}`)); frontmatter.push("---"); diff --git a/packages/harnesses/src/writers/opencode.ts b/packages/harnesses/src/writers/opencode.ts index 797a94f..5831d73 100644 --- a/packages/harnesses/src/writers/opencode.ts +++ b/packages/harnesses/src/writers/opencode.ts @@ -40,13 +40,14 @@ async function writeOpenCodeJson( // Start fresh } - const mcpServers: Record< + const mcp: Record< string, - { command: string[]; env?: Record } - > = (existing.mcp_servers as typeof mcpServers) ?? {}; + { type: string; command: string[]; env?: Record } + > = (existing.mcp as typeof mcp) ?? {}; for (const server of allServers) { - mcpServers[server.ref] = { + mcp[server.ref] = { + type: "local", command: [server.command, ...server.args], ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) }; @@ -55,7 +56,7 @@ async function writeOpenCodeJson( const result = { $schema: "https://opencode.ai/config.json", ...existing, - mcp_servers: mcpServers + mcp }; await writeFile(configPath, JSON.stringify(result, null, 2) + "\n", "utf-8"); diff --git a/packages/harnesses/src/writers/universal.ts b/packages/harnesses/src/writers/universal.ts new file mode 100644 index 0000000..4a33538 --- /dev/null +++ b/packages/harnesses/src/writers/universal.ts @@ -0,0 +1,78 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { LogicalConfig, McpServerEntry } from "@ade/core"; +import type { HarnessWriter } from "../types.js"; + +/** + * Universal harness — generates the cross-tool standard files: + * AGENTS.md (instructions readable by all agents) + * .mcp.json (MCP server config readable by Claude Code and others) + */ +export const universalWriter: HarnessWriter = { + id: "universal", + label: "Universal (AGENTS.md + .mcp.json)", + description: + "Cross-tool standard — AGENTS.md + .mcp.json (works with any agent)", + async install(config: LogicalConfig, projectRoot: string) { + await writeAgentsMd(config, projectRoot); + await writeMcpJson(config, projectRoot); + } +}; + +async function writeAgentsMd( + config: LogicalConfig, + projectRoot: string +): Promise { + if (config.instructions.length === 0) return; + + const lines = ["# AGENTS", ""]; + for (const instruction of config.instructions) { + lines.push(instruction, ""); + } + + await writeFile(join(projectRoot, "AGENTS.md"), lines.join("\n"), "utf-8"); +} + +async function writeMcpJson( + config: LogicalConfig, + projectRoot: string +): Promise { + const allServers: McpServerEntry[] = [...config.mcp_servers]; + + if (config.skills.length > 0) { + allServers.push({ + ref: "agentskills", + command: "npx", + args: ["-y", "@codemcp/skills-server"], + env: {} + }); + } + + if (allServers.length === 0) return; + + const mcpPath = join(projectRoot, ".mcp.json"); + + let existing: Record = {}; + try { + const raw = await readFile(mcpPath, "utf-8"); + existing = JSON.parse(raw); + } catch { + // Start fresh + } + + const mcpServers: Record< + string, + { command: string; args: string[]; env?: Record } + > = (existing.mcpServers as typeof mcpServers) ?? {}; + + for (const server of allServers) { + mcpServers[server.ref] = { + command: server.command, + args: server.args, + ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) + }; + } + + const result = { ...existing, mcpServers }; + await writeFile(mcpPath, JSON.stringify(result, null, 2) + "\n", "utf-8"); +} From ad23cfb1cf77ebf49605ae777b825b239ccc7bea Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 12:20:35 +0000 Subject: [PATCH 51/60] fix: remove unused mkdir import in universal writer https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/harnesses/src/writers/universal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/harnesses/src/writers/universal.ts b/packages/harnesses/src/writers/universal.ts index 4a33538..9e06601 100644 --- a/packages/harnesses/src/writers/universal.ts +++ b/packages/harnesses/src/writers/universal.ts @@ -1,4 +1,4 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import type { LogicalConfig, McpServerEntry } from "@ade/core"; import type { HarnessWriter } from "../types.js"; From 13888c6ece3e1f96534957c3fac4df337d0a7219 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 12:38:34 +0000 Subject: [PATCH 52/60] fix: update Copilot writer description to cover VS Code + CLI The .github/agents/*.agent.md format is read by both VS Code Copilot and GitHub Copilot CLI. Update the description to reflect this. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/harnesses/src/writers/copilot.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/harnesses/src/writers/copilot.ts b/packages/harnesses/src/writers/copilot.ts index c1b95ba..2ed33c5 100644 --- a/packages/harnesses/src/writers/copilot.ts +++ b/packages/harnesses/src/writers/copilot.ts @@ -6,8 +6,7 @@ import type { HarnessWriter } from "../types.js"; export const copilotWriter: HarnessWriter = { id: "copilot", label: "GitHub Copilot", - description: - "VS Code Copilot — .vscode/mcp.json + .github/copilot-instructions.md", + description: "VS Code + CLI — .vscode/mcp.json + .github/agents/ade.agent.md", async install(config: LogicalConfig, projectRoot: string) { await writeVsCodeMcp(config, projectRoot); await writeCopilotInstructions(config, projectRoot); From fb954f23f51988c6a78d3d1a3b5942c857bd8604 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 12:54:10 +0000 Subject: [PATCH 53/60] refactor: clean up facet separation, knowledge init, and copilot output - Remove copilot-instructions.md generation; .github/agents/ade.agent.md is the preferred custom agent format for both VS Code and CLI - Fix knowledge-server MCP ref from "@codemcp/knowledge-server" to "knowledge" (short ref, consistent with other servers) - Remove instruction provisions from architecture and practices facets; these facets now produce only skills and MCP servers, not system prompt instructions. Only the process facet generates instructions. - Replace automatic knowledge init during setup/install with a post-setup hint directing users to run `npx @codemcp/knowledge init` separately https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- .../commands/conventions.integration.spec.ts | 18 +++-- packages/cli/src/commands/install.ts | 8 ++- .../commands/knowledge.integration.spec.ts | 72 ++++--------------- packages/cli/src/commands/setup.ts | 8 ++- .../core/src/catalog/facets/architecture.ts | 6 -- packages/core/src/catalog/facets/practices.ts | 18 ----- packages/core/src/resolver.spec.ts | 4 +- packages/core/src/resolver.ts | 2 +- .../harnesses/src/writers/copilot.spec.ts | 13 ++-- packages/harnesses/src/writers/copilot.ts | 21 +----- 10 files changed, 49 insertions(+), 121 deletions(-) diff --git a/packages/cli/src/commands/conventions.integration.spec.ts b/packages/cli/src/commands/conventions.integration.spec.ts index 2a660bd..258d0f2 100644 --- a/packages/cli/src/commands/conventions.integration.spec.ts +++ b/packages/cli/src/commands/conventions.integration.spec.ts @@ -187,7 +187,7 @@ describe("architecture and practices facets integration", () => { expect(config!.choices).not.toHaveProperty("practices"); }); - it("includes practice instructions in custom agent", async () => { + it("exposes practices as skills, not instructions", async () => { const catalog = getDefaultCatalog(); // Facet order: process (select), architecture (select), practices (multiselect) @@ -200,12 +200,20 @@ describe("architecture and practices facets integration", () => { await runSetup(dir, catalog); - const agentMd = await readFile( - join(dir, ".claude", "agents", "ade.md"), + // Practice produces a skill, not an instruction + const skillMd = await readFile( + join(dir, ".ade", "skills", "tdd-london", "SKILL.md"), "utf-8" ); - expect(agentMd).toContain("tdd-london"); - expect(agentMd).toContain("use_skill()"); + expect(skillMd).toContain("name: tdd-london"); + + // Lock file should have skill but no practice-specific instructions + const lock = await readLockFile(dir); + expect(lock!.logical_config.skills.length).toBeGreaterThanOrEqual(1); + // Only process-facet instructions should be present (from native-agents-md) + for (const instruction of lock!.logical_config.instructions) { + expect(instruction).not.toContain("tdd-london"); + } }); it( diff --git a/packages/cli/src/commands/install.ts b/packages/cli/src/commands/install.ts index 17ac0ff..850a9d9 100644 --- a/packages/cli/src/commands/install.ts +++ b/packages/cli/src/commands/install.ts @@ -2,7 +2,6 @@ import * as clack from "@clack/prompts"; import { readLockFile } from "@ade/core"; import { getHarnessWriter, getHarnessIds } from "@ade/harnesses"; import { installSkills } from "../skills-installer.js"; -import { installKnowledge } from "../knowledge-installer.js"; export async function runInstall( projectRoot: string, @@ -40,7 +39,12 @@ export async function runInstall( } await installSkills(logicalConfig.skills, projectRoot); - await installKnowledge(logicalConfig.knowledge_sources, projectRoot); + + if (logicalConfig.knowledge_sources.length > 0) { + clack.log.info( + "Knowledge sources configured. Initialize them separately:\n npx @codemcp/knowledge init" + ); + } clack.outro("Install complete!"); } diff --git a/packages/cli/src/commands/knowledge.integration.spec.ts b/packages/cli/src/commands/knowledge.integration.spec.ts index d491aa6..a1a4680 100644 --- a/packages/cli/src/commands/knowledge.integration.spec.ts +++ b/packages/cli/src/commands/knowledge.integration.spec.ts @@ -11,23 +11,11 @@ vi.mock("@clack/prompts", () => ({ confirm: vi.fn(), isCancel: vi.fn().mockReturnValue(false), cancel: vi.fn(), + log: { info: vi.fn(), warn: vi.fn() }, spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) })); -vi.mock("@codemcp/knowledge/packages/cli/dist/exports.js", () => ({ - createDocset: vi.fn().mockResolvedValue({ - docset: {}, - configPath: ".knowledge/config.yaml", - configCreated: false - }), - initDocset: vi.fn().mockResolvedValue({ alreadyInitialized: false }) -})); - import * as clack from "@clack/prompts"; -import { - createDocset, - initDocset -} from "@codemcp/knowledge/packages/cli/dist/exports.js"; import { runSetup } from "./setup.js"; import { readLockFile } from "@ade/core"; import { getDefaultCatalog } from "../../../core/src/catalog/index.js"; @@ -45,21 +33,18 @@ describe("knowledge integration", () => { }); it( - "creates and initializes docsets when tanstack is selected", + "records knowledge sources in lock file when tanstack is selected", { timeout: 60_000 }, async () => { const catalog = getDefaultCatalog(); - // Facet order: process (select), architecture (select) vi.mocked(clack.select) .mockResolvedValueOnce("codemcp-workflows") // process .mockResolvedValueOnce("tanstack"); // architecture - // multiselect order: practices, then docsets confirmation vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none .mockResolvedValueOnce([ - // docsets: accept all 4 "tanstack-router-docs", "tanstack-query-docs", "tanstack-form-docs", @@ -69,36 +54,6 @@ describe("knowledge integration", () => { await runSetup(dir, catalog); - // createDocset should be called for each of the 4 TanStack docsets - expect(createDocset).toHaveBeenCalledTimes(4); - - expect(createDocset).toHaveBeenCalledWith( - expect.objectContaining({ - id: "tanstack-router-docs", - preset: "git-repo", - url: "https://github.com/TanStack/router.git" - }), - expect.objectContaining({ cwd: dir }) - ); - - expect(createDocset).toHaveBeenCalledWith( - expect.objectContaining({ - id: "tanstack-query-docs", - preset: "git-repo", - url: "https://github.com/TanStack/query.git" - }), - expect.objectContaining({ cwd: dir }) - ); - - // initDocset should be called for each docset after creation - expect(initDocset).toHaveBeenCalledTimes(4); - expect(initDocset).toHaveBeenCalledWith( - expect.objectContaining({ - docsetId: "tanstack-router-docs", - cwd: dir - }) - ); - // Lock file should contain knowledge_sources const lock = await readLockFile(dir); expect(lock!.logical_config.knowledge_sources).toHaveLength(4); @@ -111,19 +66,24 @@ describe("knowledge integration", () => { ]) ); - // MCP server entry for knowledge-server should be in .mcp.json + // MCP server entry for knowledge should be in .mcp.json const mcpJson = JSON.parse( await readFile(join(dir, ".mcp.json"), "utf-8") ); - expect(mcpJson.mcpServers["@codemcp/knowledge-server"]).toMatchObject({ + expect(mcpJson.mcpServers["knowledge"]).toMatchObject({ command: "npx", args: ["-y", "@codemcp/knowledge-server"] }); + + // Knowledge init is deferred — user should see a hint + expect(clack.log.info).toHaveBeenCalledWith( + expect.stringContaining("npx @codemcp/knowledge init") + ); } ); it( - "excludes deselected docsets from knowledge installation", + "excludes deselected docsets from lock file", { timeout: 60_000 }, async () => { const catalog = getDefaultCatalog(); @@ -132,7 +92,6 @@ describe("knowledge integration", () => { .mockResolvedValueOnce("codemcp-workflows") // process .mockResolvedValueOnce("tanstack"); // architecture - // multiselect order: practices, then docsets (only keep router + query) vi.mocked(clack.multiselect) .mockResolvedValueOnce([]) // practices: none .mockResolvedValueOnce(["tanstack-router-docs", "tanstack-query-docs"]) @@ -140,10 +99,6 @@ describe("knowledge integration", () => { await runSetup(dir, catalog); - // Only 2 docsets should be created - expect(createDocset).toHaveBeenCalledTimes(2); - expect(initDocset).toHaveBeenCalledTimes(2); - // Lock file should only have the 2 selected sources const lock = await readLockFile(dir); expect(lock!.logical_config.knowledge_sources).toHaveLength(2); @@ -153,7 +108,7 @@ describe("knowledge integration", () => { } ); - it("skips knowledge installation when no docsets are implied", async () => { + it("does not show knowledge hint when no docsets are implied", async () => { const catalog = getDefaultCatalog(); vi.mocked(clack.select) @@ -165,7 +120,8 @@ describe("knowledge integration", () => { await runSetup(dir, catalog); - expect(createDocset).not.toHaveBeenCalled(); - expect(initDocset).not.toHaveBeenCalled(); + expect(clack.log.info).not.toHaveBeenCalledWith( + expect.stringContaining("npx @codemcp/knowledge init") + ); }); }); diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 3e4dccc..f85e900 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -15,7 +15,6 @@ import { } from "@ade/core"; import { allHarnessWriters, getHarnessWriter } from "@ade/harnesses"; import { installSkills } from "../skills-installer.js"; -import { installKnowledge } from "../knowledge-installer.js"; export async function runSetup( projectRoot: string, @@ -152,7 +151,12 @@ export async function runSetup( } await installSkills(logicalConfig.skills, projectRoot); - await installKnowledge(logicalConfig.knowledge_sources, projectRoot); + + if (logicalConfig.knowledge_sources.length > 0) { + clack.log.info( + "Knowledge sources selected. Initialize them separately:\n npx @codemcp/knowledge init" + ); + } clack.outro("Setup complete!"); } diff --git a/packages/core/src/catalog/facets/architecture.ts b/packages/core/src/catalog/facets/architecture.ts index daba41d..5462250 100644 --- a/packages/core/src/catalog/facets/architecture.ts +++ b/packages/core/src/catalog/facets/architecture.ts @@ -115,12 +115,6 @@ export const architectureFacet: Facet = { } ] } - }, - { - writer: "instruction", - config: { - text: "This project follows TanStack conventions. Use use_skill() to access the tanstack-architecture, tanstack-design, tanstack-code, tanstack-testing, and playwright-cli skills before making changes." - } } ], docsets: [ diff --git a/packages/core/src/catalog/facets/practices.ts b/packages/core/src/catalog/facets/practices.ts index 2e85987..51d1ef9 100644 --- a/packages/core/src/catalog/facets/practices.ts +++ b/packages/core/src/catalog/facets/practices.ts @@ -55,12 +55,6 @@ export const practicesFacet: Facet = { } ] } - }, - { - writer: "instruction", - config: { - text: "Use the conventional-commits skill (via use_skill()) when writing commit messages." - } } ], docsets: [ @@ -117,12 +111,6 @@ export const practicesFacet: Facet = { } ] } - }, - { - writer: "instruction", - config: { - text: "This project uses London-style TDD. Use the tdd-london skill (via use_skill()) before writing tests. Always follow the Red-Green-Refactor cycle." - } } ] }, @@ -178,12 +166,6 @@ export const practicesFacet: Facet = { } ] } - }, - { - writer: "instruction", - config: { - text: "This project uses Architecture Decision Records. Use the adr-nygard skill (via use_skill()) when making or documenting architectural decisions. Store ADRs in docs/adr/." - } } ] } diff --git a/packages/core/src/resolver.spec.ts b/packages/core/src/resolver.spec.ts index d3db8d0..ab7698c 100644 --- a/packages/core/src/resolver.spec.ts +++ b/packages/core/src/resolver.spec.ts @@ -369,7 +369,7 @@ describe("resolve", () => { const result = await resolve(userConfig, docsetCatalog, registry); const knowledgeServer = result.mcp_servers.find( - (s) => s.ref === "@codemcp/knowledge-server" + (s) => s.ref === "knowledge" ); expect(knowledgeServer).toBeDefined(); expect(knowledgeServer!.command).toBe("npx"); @@ -383,7 +383,7 @@ describe("resolve", () => { const result = await resolve(userConfig, catalog, registry); const knowledgeServer = result.mcp_servers.find( - (s) => s.ref === "@codemcp/knowledge-server" + (s) => s.ref === "knowledge" ); expect(knowledgeServer).toBeUndefined(); }); diff --git a/packages/core/src/resolver.ts b/packages/core/src/resolver.ts index 7ca30fa..ed980c7 100644 --- a/packages/core/src/resolver.ts +++ b/packages/core/src/resolver.ts @@ -98,7 +98,7 @@ export async function resolve( // Add knowledge-server MCP entry if any knowledge_sources were collected if (result.knowledge_sources.length > 0) { result.mcp_servers.push({ - ref: "@codemcp/knowledge-server", + ref: "knowledge", command: "npx", args: ["-y", "@codemcp/knowledge-server"], env: {} diff --git a/packages/harnesses/src/writers/copilot.spec.ts b/packages/harnesses/src/writers/copilot.spec.ts index 838c73c..1076e7a 100644 --- a/packages/harnesses/src/writers/copilot.spec.ts +++ b/packages/harnesses/src/writers/copilot.spec.ts @@ -49,10 +49,10 @@ describe("copilotWriter", () => { }); }); - it("writes .github/copilot-instructions.md with instructions", async () => { + it("does not write copilot-instructions.md (prefers agent definition)", async () => { const config: LogicalConfig = { mcp_servers: [], - instructions: ["Follow TDD.", "Use ADRs."], + instructions: ["Follow TDD."], cli_actions: [], knowledge_sources: [], skills: [] @@ -60,12 +60,9 @@ describe("copilotWriter", () => { await copilotWriter.install(config, dir); - const content = await readFile( - join(dir, ".github", "copilot-instructions.md"), - "utf-8" - ); - expect(content).toContain("Follow TDD."); - expect(content).toContain("Use ADRs."); + await expect( + readFile(join(dir, ".github", "copilot-instructions.md"), "utf-8") + ).rejects.toThrow(); }); it("writes dedicated .github/agents/ade.agent.md with agent definition", async () => { diff --git a/packages/harnesses/src/writers/copilot.ts b/packages/harnesses/src/writers/copilot.ts index 2ed33c5..49b78e3 100644 --- a/packages/harnesses/src/writers/copilot.ts +++ b/packages/harnesses/src/writers/copilot.ts @@ -9,7 +9,6 @@ export const copilotWriter: HarnessWriter = { description: "VS Code + CLI — .vscode/mcp.json + .github/agents/ade.agent.md", async install(config: LogicalConfig, projectRoot: string) { await writeVsCodeMcp(config, projectRoot); - await writeCopilotInstructions(config, projectRoot); await writeCopilotAgent(config, projectRoot); } }; @@ -68,26 +67,10 @@ async function writeVsCodeMcp( await writeFile(mcpPath, JSON.stringify(result, null, 2) + "\n", "utf-8"); } -async function writeCopilotInstructions( - config: LogicalConfig, - projectRoot: string -): Promise { - if (config.instructions.length === 0) return; - - const githubDir = join(projectRoot, ".github"); - await mkdir(githubDir, { recursive: true }); - - const lines = config.instructions.flatMap((i) => [i, ""]); - await writeFile( - join(githubDir, "copilot-instructions.md"), - lines.join("\n"), - "utf-8" - ); -} - /** * Write a dedicated ADE agent definition that combines instructions and - * references configured MCP servers. + * references configured MCP servers. Read by both VS Code Copilot and + * GitHub Copilot CLI. */ async function writeCopilotAgent( config: LogicalConfig, From 49b72b1e0848224d328c9d90b8d9b4b3663dfa80 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 17:08:53 +0000 Subject: [PATCH 54/60] Centralize agentskills MCP entry in resolver, remove duplication from 9 harness writers The resolver now auto-adds the agentskills (skills-server) MCP entry when skills are present, matching the existing pattern for knowledge-server. This removes identical conditional logic from all 9 harness writers (-131 lines, +61 lines net). https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/core/src/resolver.spec.ts | 20 ++++++++++++ packages/core/src/resolver.ts | 10 ++++++ .../harnesses/src/writers/claude-code.spec.ts | 11 +++++-- packages/harnesses/src/writers/claude-code.ts | 32 ++----------------- packages/harnesses/src/writers/cline.ts | 11 +------ packages/harnesses/src/writers/copilot.ts | 21 ++---------- packages/harnesses/src/writers/cursor.spec.ts | 11 +++++-- packages/harnesses/src/writers/cursor.ts | 11 +------ packages/harnesses/src/writers/kiro.ts | 11 +------ packages/harnesses/src/writers/opencode.ts | 21 ++---------- packages/harnesses/src/writers/roo-code.ts | 11 +------ packages/harnesses/src/writers/universal.ts | 11 +------ packages/harnesses/src/writers/windsurf.ts | 11 +------ 13 files changed, 61 insertions(+), 131 deletions(-) diff --git a/packages/core/src/resolver.spec.ts b/packages/core/src/resolver.spec.ts index ab7698c..5970e85 100644 --- a/packages/core/src/resolver.spec.ts +++ b/packages/core/src/resolver.spec.ts @@ -190,6 +190,26 @@ describe("resolve", () => { expect(result.skills).toHaveLength(1); expect(result.skills[0].name).toBe("test-skill"); + + // Resolver should auto-add agentskills MCP entry when skills are present + const agentskills = result.mcp_servers.find( + (s) => s.ref === "agentskills" + ); + expect(agentskills).toBeDefined(); + expect(agentskills!.command).toBe("npx"); + expect(agentskills!.args).toContain("@codemcp/skills-server"); + }); + + it("does not add agentskills MCP entry when no skills", async () => { + const userConfig: UserConfig = { + choices: { process: "native-agents-md" } + }; + const result = await resolve(userConfig, catalog, registry); + + const agentskills = result.mcp_servers.find( + (s) => s.ref === "agentskills" + ); + expect(agentskills).toBeUndefined(); }); }); diff --git a/packages/core/src/resolver.ts b/packages/core/src/resolver.ts index ed980c7..4f42ba4 100644 --- a/packages/core/src/resolver.ts +++ b/packages/core/src/resolver.ts @@ -105,6 +105,16 @@ export async function resolve( }); } + // Add skills-server MCP entry if any skills were collected + if (result.skills.length > 0) { + result.mcp_servers.push({ + ref: "agentskills", + command: "npx", + args: ["-y", "@codemcp/skills-server"], + env: {} + }); + } + // Merge custom section if (userConfig.custom) { if (userConfig.custom.instructions) { diff --git a/packages/harnesses/src/writers/claude-code.spec.ts b/packages/harnesses/src/writers/claude-code.spec.ts index dcac396..b2e0b6c 100644 --- a/packages/harnesses/src/writers/claude-code.spec.ts +++ b/packages/harnesses/src/writers/claude-code.spec.ts @@ -99,9 +99,16 @@ describe("claudeCodeWriter", () => { expect(settings.permissions.allow).toContain("MCP(workflows:*)"); }); - it("adds skills-server when skills are present", async () => { + it("includes agentskills server from mcp_servers", async () => { const config: LogicalConfig = { - mcp_servers: [], + mcp_servers: [ + { + ref: "agentskills", + command: "npx", + args: ["-y", "@codemcp/skills-server"], + env: {} + } + ], instructions: [], cli_actions: [], knowledge_sources: [], diff --git a/packages/harnesses/src/writers/claude-code.ts b/packages/harnesses/src/writers/claude-code.ts index 4e5bb08..cb56fa6 100644 --- a/packages/harnesses/src/writers/claude-code.ts +++ b/packages/harnesses/src/writers/claude-code.ts @@ -30,15 +30,7 @@ async function writeCustomAgent( config: LogicalConfig, projectRoot: string ): Promise { - const allServers: McpServerEntry[] = [...config.mcp_servers]; - if (config.skills.length > 0) { - allServers.push({ - ref: "agentskills", - command: "npx", - args: ["-y", "@codemcp/skills-server"], - env: {} - }); - } + const allServers: McpServerEntry[] = config.mcp_servers; if (config.instructions.length === 0 && allServers.length === 0) return; @@ -70,16 +62,7 @@ async function writeMcpJson( config: LogicalConfig, projectRoot: string ): Promise { - const allServers: McpServerEntry[] = [...config.mcp_servers]; - - if (config.skills.length > 0) { - allServers.push({ - ref: "agentskills", - command: "npx", - args: ["-y", "@codemcp/skills-server"], - env: {} - }); - } + const allServers: McpServerEntry[] = config.mcp_servers; if (allServers.length === 0) return; @@ -117,16 +100,7 @@ async function writeClaudeSettings( config: LogicalConfig, projectRoot: string ): Promise { - const allServers: McpServerEntry[] = [...config.mcp_servers]; - - if (config.skills.length > 0) { - allServers.push({ - ref: "agentskills", - command: "npx", - args: ["-y", "@codemcp/skills-server"], - env: {} - }); - } + const allServers: McpServerEntry[] = config.mcp_servers; if (allServers.length === 0) return; diff --git a/packages/harnesses/src/writers/cline.ts b/packages/harnesses/src/writers/cline.ts index 576f272..767cd6a 100644 --- a/packages/harnesses/src/writers/cline.ts +++ b/packages/harnesses/src/writers/cline.ts @@ -17,16 +17,7 @@ async function writeMcpJson( config: LogicalConfig, projectRoot: string ): Promise { - const allServers: McpServerEntry[] = [...config.mcp_servers]; - - if (config.skills.length > 0) { - allServers.push({ - ref: "agentskills", - command: "npx", - args: ["-y", "@codemcp/skills-server"], - env: {} - }); - } + const allServers: McpServerEntry[] = config.mcp_servers; if (allServers.length === 0) return; diff --git a/packages/harnesses/src/writers/copilot.ts b/packages/harnesses/src/writers/copilot.ts index 49b78e3..2200427 100644 --- a/packages/harnesses/src/writers/copilot.ts +++ b/packages/harnesses/src/writers/copilot.ts @@ -17,16 +17,7 @@ async function writeVsCodeMcp( config: LogicalConfig, projectRoot: string ): Promise { - const allServers: McpServerEntry[] = [...config.mcp_servers]; - - if (config.skills.length > 0) { - allServers.push({ - ref: "agentskills", - command: "npx", - args: ["-y", "@codemcp/skills-server"], - env: {} - }); - } + const allServers: McpServerEntry[] = config.mcp_servers; if (allServers.length === 0) return; @@ -76,15 +67,7 @@ async function writeCopilotAgent( config: LogicalConfig, projectRoot: string ): Promise { - const allServers: McpServerEntry[] = [...config.mcp_servers]; - if (config.skills.length > 0) { - allServers.push({ - ref: "agentskills", - command: "npx", - args: ["-y", "@codemcp/skills-server"], - env: {} - }); - } + const allServers: McpServerEntry[] = config.mcp_servers; if (config.instructions.length === 0 && allServers.length === 0) return; diff --git a/packages/harnesses/src/writers/cursor.spec.ts b/packages/harnesses/src/writers/cursor.spec.ts index db8997d..6e8995f 100644 --- a/packages/harnesses/src/writers/cursor.spec.ts +++ b/packages/harnesses/src/writers/cursor.spec.ts @@ -67,9 +67,16 @@ describe("cursorWriter", () => { expect(content).toContain("Use conventional commits."); }); - it("adds skills-server when skills are present", async () => { + it("includes agentskills server from mcp_servers", async () => { const config: LogicalConfig = { - mcp_servers: [], + mcp_servers: [ + { + ref: "agentskills", + command: "npx", + args: ["-y", "@codemcp/skills-server"], + env: {} + } + ], instructions: [], cli_actions: [], knowledge_sources: [], diff --git a/packages/harnesses/src/writers/cursor.ts b/packages/harnesses/src/writers/cursor.ts index 30f76cb..54327aa 100644 --- a/packages/harnesses/src/writers/cursor.ts +++ b/packages/harnesses/src/writers/cursor.ts @@ -17,16 +17,7 @@ async function writeMcpJson( config: LogicalConfig, projectRoot: string ): Promise { - const allServers: McpServerEntry[] = [...config.mcp_servers]; - - if (config.skills.length > 0) { - allServers.push({ - ref: "agentskills", - command: "npx", - args: ["-y", "@codemcp/skills-server"], - env: {} - }); - } + const allServers: McpServerEntry[] = config.mcp_servers; if (allServers.length === 0) return; diff --git a/packages/harnesses/src/writers/kiro.ts b/packages/harnesses/src/writers/kiro.ts index 693324c..b8e9cde 100644 --- a/packages/harnesses/src/writers/kiro.ts +++ b/packages/harnesses/src/writers/kiro.ts @@ -16,16 +16,7 @@ async function writeAgentJson( config: LogicalConfig, projectRoot: string ): Promise { - const allServers: McpServerEntry[] = [...config.mcp_servers]; - - if (config.skills.length > 0) { - allServers.push({ - ref: "agentskills", - command: "npx", - args: ["-y", "@codemcp/skills-server"], - env: {} - }); - } + const allServers: McpServerEntry[] = config.mcp_servers; if (allServers.length === 0 && config.instructions.length === 0) return; diff --git a/packages/harnesses/src/writers/opencode.ts b/packages/harnesses/src/writers/opencode.ts index 5831d73..772ae1b 100644 --- a/packages/harnesses/src/writers/opencode.ts +++ b/packages/harnesses/src/writers/opencode.ts @@ -17,16 +17,7 @@ async function writeOpenCodeJson( config: LogicalConfig, projectRoot: string ): Promise { - const allServers: McpServerEntry[] = [...config.mcp_servers]; - - if (config.skills.length > 0) { - allServers.push({ - ref: "agentskills", - command: "npx", - args: ["-y", "@codemcp/skills-server"], - env: {} - }); - } + const allServers: McpServerEntry[] = config.mcp_servers; if (allServers.length === 0) return; @@ -66,15 +57,7 @@ async function writeAgentMd( config: LogicalConfig, projectRoot: string ): Promise { - const allServers: McpServerEntry[] = [...config.mcp_servers]; - if (config.skills.length > 0) { - allServers.push({ - ref: "agentskills", - command: "npx", - args: ["-y", "@codemcp/skills-server"], - env: {} - }); - } + const allServers: McpServerEntry[] = config.mcp_servers; if (config.instructions.length === 0 && allServers.length === 0) return; diff --git a/packages/harnesses/src/writers/roo-code.ts b/packages/harnesses/src/writers/roo-code.ts index eab8c92..b314beb 100644 --- a/packages/harnesses/src/writers/roo-code.ts +++ b/packages/harnesses/src/writers/roo-code.ts @@ -17,16 +17,7 @@ async function writeMcpJson( config: LogicalConfig, projectRoot: string ): Promise { - const allServers: McpServerEntry[] = [...config.mcp_servers]; - - if (config.skills.length > 0) { - allServers.push({ - ref: "agentskills", - command: "npx", - args: ["-y", "@codemcp/skills-server"], - env: {} - }); - } + const allServers: McpServerEntry[] = config.mcp_servers; if (allServers.length === 0) return; diff --git a/packages/harnesses/src/writers/universal.ts b/packages/harnesses/src/writers/universal.ts index 9e06601..0ea55fe 100644 --- a/packages/harnesses/src/writers/universal.ts +++ b/packages/harnesses/src/writers/universal.ts @@ -37,16 +37,7 @@ async function writeMcpJson( config: LogicalConfig, projectRoot: string ): Promise { - const allServers: McpServerEntry[] = [...config.mcp_servers]; - - if (config.skills.length > 0) { - allServers.push({ - ref: "agentskills", - command: "npx", - args: ["-y", "@codemcp/skills-server"], - env: {} - }); - } + const allServers: McpServerEntry[] = config.mcp_servers; if (allServers.length === 0) return; diff --git a/packages/harnesses/src/writers/windsurf.ts b/packages/harnesses/src/writers/windsurf.ts index c6b66a2..175199e 100644 --- a/packages/harnesses/src/writers/windsurf.ts +++ b/packages/harnesses/src/writers/windsurf.ts @@ -17,16 +17,7 @@ async function writeMcpJson( config: LogicalConfig, projectRoot: string ): Promise { - const allServers: McpServerEntry[] = [...config.mcp_servers]; - - if (config.skills.length > 0) { - allServers.push({ - ref: "agentskills", - command: "npx", - args: ["-y", "@codemcp/skills-server"], - env: {} - }); - } + const allServers: McpServerEntry[] = config.mcp_servers; if (allServers.length === 0) return; From f103c02d8b2da4d93f66d7c250c72bb6f29f65de Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 17:16:08 +0000 Subject: [PATCH 55/60] Replace hardcoded agent list in README with link to harness writers source The supported agents list evolves frequently. Point to the source of truth instead of maintaining a static list in prose. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index daa81cb..3d30c67 100644 --- a/README.md +++ b/README.md @@ -194,14 +194,16 @@ reference knowledge. ### Coding agent agnostic setup tooling -ADE will :soon: include a simple cli to setup your coding agent. All configuration -can be placed into your repo, so that you can check it in. - -We're using STDIO based MCP-servers to expose process guidance, conventions and docs -to coding agents. There are other proprietary ways to do this, but by using the -well-established Model Context Protocol which is optimized for discoverability, we -make sure that you get a similar experience, no matter whether you are using Claude -Code, Copilot or Kiro. +ADE includes a CLI (`ade setup`) that generates the correct configuration for +whichever coding agent you use. All configuration is placed into your repo so +you can check it in. + +We use STDIO-based MCP servers to expose process guidance, conventions, and docs +to coding agents. By using the Model Context Protocol — optimized for +discoverability — you get a consistent experience regardless of your agent. + +The CLI supports a growing list of agents. See the +[harness writers source](packages/harnesses/src/writers) for the current set. ## Core principles From 5b866a00a8bb197953901339ecb0781f93f5b02c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 18:46:46 +0000 Subject: [PATCH 56/60] Refactor harness writers for DRY, move skills-installer, remove deprecated core agent - Extract shared utilities (readJsonOrEmpty, writeJson, writeMcpServers, writeRulesFile, writeAgentMd, writeInlineSkills) into harnesses/src/util.ts - Rewrite all 9 harness writers to use shared utilities (~700 lines removed) - Move skills-installer from CLI to harnesses package (domain responsibility) - Remove deprecated core/src/agents/claude-code.ts (replaced by harnesses) - Remove stale @codemcp/skills/api mocks from CLI tests https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- packages/cli/package.json | 3 +- packages/cli/src/commands/install.spec.ts | 7 +- packages/cli/src/commands/install.ts | 3 +- packages/cli/src/commands/setup.spec.ts | 7 +- packages/cli/src/commands/setup.ts | 7 +- packages/core/src/agents/claude-code.spec.ts | 269 ------------------ packages/core/src/agents/claude-code.ts | 114 -------- packages/core/src/index.ts | 2 - packages/harnesses/package.json | 3 +- packages/harnesses/src/index.ts | 1 + .../src/skills-installer.ts | 0 packages/harnesses/src/util.ts | 188 ++++++++++++ packages/harnesses/src/writers/claude-code.ts | 171 ++--------- packages/harnesses/src/writers/cline.ts | 67 +---- packages/harnesses/src/writers/copilot.ts | 114 ++------ packages/harnesses/src/writers/cursor.ts | 85 ++---- packages/harnesses/src/writers/kiro.ts | 109 +++---- packages/harnesses/src/writers/opencode.ts | 134 +++------ packages/harnesses/src/writers/roo-code.ts | 67 +---- packages/harnesses/src/writers/universal.ts | 77 ++--- packages/harnesses/src/writers/windsurf.ts | 74 +---- pnpm-lock.yaml | 6 +- 22 files changed, 402 insertions(+), 1106 deletions(-) delete mode 100644 packages/core/src/agents/claude-code.spec.ts delete mode 100644 packages/core/src/agents/claude-code.ts rename packages/{cli => harnesses}/src/skills-installer.ts (100%) create mode 100644 packages/harnesses/src/util.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 42a6e60..634fca9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -24,8 +24,7 @@ "dependencies": { "@ade/core": "workspace:*", "@ade/harnesses": "workspace:*", - "@clack/prompts": "^1.1.0", - "@codemcp/skills": "^2.3.0" + "@clack/prompts": "^1.1.0" }, "devDependencies": { "@codemcp/knowledge": "2.1.0", diff --git a/packages/cli/src/commands/install.spec.ts b/packages/cli/src/commands/install.spec.ts index 3093751..d0aeb74 100644 --- a/packages/cli/src/commands/install.spec.ts +++ b/packages/cli/src/commands/install.spec.ts @@ -9,10 +9,6 @@ vi.mock("@clack/prompts", () => ({ log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() } })); -vi.mock("@codemcp/skills/api", () => ({ - runAdd: vi.fn() -})); - const mockLogical: LogicalConfig = { mcp_servers: [], instructions: ["test instruction"], @@ -50,7 +46,8 @@ vi.mock("@ade/harnesses", () => ({ "roo-code", "kiro", "opencode" - ]) + ]), + installSkills: vi.fn().mockResolvedValue(undefined) })); import * as clack from "@clack/prompts"; diff --git a/packages/cli/src/commands/install.ts b/packages/cli/src/commands/install.ts index 850a9d9..49e797a 100644 --- a/packages/cli/src/commands/install.ts +++ b/packages/cli/src/commands/install.ts @@ -1,7 +1,6 @@ import * as clack from "@clack/prompts"; import { readLockFile } from "@ade/core"; -import { getHarnessWriter, getHarnessIds } from "@ade/harnesses"; -import { installSkills } from "../skills-installer.js"; +import { getHarnessWriter, getHarnessIds, installSkills } from "@ade/harnesses"; export async function runInstall( projectRoot: string, diff --git a/packages/cli/src/commands/setup.spec.ts b/packages/cli/src/commands/setup.spec.ts index 3397177..1c3380d 100644 --- a/packages/cli/src/commands/setup.spec.ts +++ b/packages/cli/src/commands/setup.spec.ts @@ -15,10 +15,6 @@ vi.mock("@clack/prompts", () => ({ spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() }) })); -vi.mock("@codemcp/skills/api", () => ({ - runAdd: vi.fn() -})); - vi.mock("@ade/core", async (importOriginal) => { const actual = (await importOriginal()) as typeof import("@ade/core"); return { @@ -52,7 +48,8 @@ vi.mock("@ade/harnesses", () => ({ description: "test", install: vi.fn().mockResolvedValue(undefined) }), - getHarnessIds: vi.fn().mockReturnValue(["claude-code"]) + getHarnessIds: vi.fn().mockReturnValue(["claude-code"]), + installSkills: vi.fn().mockResolvedValue(undefined) })); import * as clack from "@clack/prompts"; diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index f85e900..bd464e7 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -13,8 +13,11 @@ import { getFacet, getOption } from "@ade/core"; -import { allHarnessWriters, getHarnessWriter } from "@ade/harnesses"; -import { installSkills } from "../skills-installer.js"; +import { + allHarnessWriters, + getHarnessWriter, + installSkills +} from "@ade/harnesses"; export async function runSetup( projectRoot: string, diff --git a/packages/core/src/agents/claude-code.spec.ts b/packages/core/src/agents/claude-code.spec.ts deleted file mode 100644 index b021e76..0000000 --- a/packages/core/src/agents/claude-code.spec.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { mkdtemp, rm, readFile, access } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import type { LogicalConfig } from "../types.js"; -import { claudeCodeWriter } from "./claude-code.js"; - -describe("claudeCodeWriter", () => { - let dir: string; - - beforeEach(async () => { - dir = await mkdtemp(join(tmpdir(), "ade-agent-")); - }); - - afterEach(async () => { - await rm(dir, { recursive: true, force: true }); - }); - - it("writes AGENTS.md with instructions", async () => { - const config: LogicalConfig = { - mcp_servers: [], - instructions: ["Use workflow files.", "Follow conventions."], - cli_actions: [], - knowledge_sources: [], - skills: [] - }; - - await claudeCodeWriter.install(config, dir); - - const content = await readFile(join(dir, "AGENTS.md"), "utf-8"); - expect(content).toContain("# AGENTS"); - expect(content).toContain("Use workflow files."); - expect(content).toContain("Follow conventions."); - }); - - it("writes .claude/settings.json with MCP servers", async () => { - const config: LogicalConfig = { - mcp_servers: [ - { - ref: "@codemcp/workflows", - command: "npx", - args: ["-y", "@codemcp/workflows"], - env: {} - } - ], - instructions: [], - cli_actions: [], - knowledge_sources: [], - skills: [] - }; - - await claudeCodeWriter.install(config, dir); - - const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8"); - const settings = JSON.parse(raw); - expect(settings.mcpServers["@codemcp/workflows"]).toEqual({ - command: "npx", - args: ["-y", "@codemcp/workflows"] - }); - }); - - it("preserves existing settings.json keys", async () => { - const { mkdir, writeFile } = await import("node:fs/promises"); - await mkdir(join(dir, ".claude"), { recursive: true }); - await writeFile( - join(dir, ".claude", "settings.json"), - JSON.stringify({ customKey: true }), - "utf-8" - ); - - const config: LogicalConfig = { - mcp_servers: [ - { - ref: "my-server", - command: "node", - args: ["server.js"], - env: { API_KEY: "secret" } - } - ], - instructions: [], - cli_actions: [], - knowledge_sources: [], - skills: [] - }; - - await claudeCodeWriter.install(config, dir); - - const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8"); - const settings = JSON.parse(raw); - expect(settings.customKey).toBe(true); - expect(settings.mcpServers["my-server"]).toEqual({ - command: "node", - args: ["server.js"], - env: { API_KEY: "secret" } - }); - }); - - it("skips AGENTS.md when no instructions", async () => { - const config: LogicalConfig = { - mcp_servers: [], - instructions: [], - cli_actions: [], - knowledge_sources: [], - skills: [] - }; - - await claudeCodeWriter.install(config, dir); - - await expect(readFile(join(dir, "AGENTS.md"), "utf-8")).rejects.toThrow(); - }); - - it("skips settings.json when no MCP servers and no skills", async () => { - const config: LogicalConfig = { - mcp_servers: [], - instructions: ["hello"], - cli_actions: [], - knowledge_sources: [], - skills: [] - }; - - await claudeCodeWriter.install(config, dir); - - await expect( - readFile(join(dir, ".claude", "settings.json"), "utf-8") - ).rejects.toThrow(); - }); - - // ── Skills: inline ──────────────────────────────────────────────────── - - it("writes inline SKILL.md files to .ade/skills//", async () => { - const config: LogicalConfig = { - mcp_servers: [], - instructions: [], - cli_actions: [], - knowledge_sources: [], - skills: [ - { - name: "tanstack-architecture", - description: "TanStack architecture conventions", - body: "# Architecture\n\nUse file-based routing." - } - ] - }; - - await claudeCodeWriter.install(config, dir); - - const skillMd = await readFile( - join(dir, ".ade", "skills", "tanstack-architecture", "SKILL.md"), - "utf-8" - ); - expect(skillMd).toContain("name: tanstack-architecture"); - expect(skillMd).toContain("description: TanStack architecture conventions"); - expect(skillMd).toContain("# Architecture"); - expect(skillMd).toContain("Use file-based routing."); - }); - - it("writes multiple inline SKILL.md files", async () => { - const config: LogicalConfig = { - mcp_servers: [], - instructions: [], - cli_actions: [], - knowledge_sources: [], - skills: [ - { name: "skill-a", description: "First skill", body: "Body A" }, - { name: "skill-b", description: "Second skill", body: "Body B" } - ] - }; - - await claudeCodeWriter.install(config, dir); - - const a = await readFile( - join(dir, ".ade", "skills", "skill-a", "SKILL.md"), - "utf-8" - ); - const b = await readFile( - join(dir, ".ade", "skills", "skill-b", "SKILL.md"), - "utf-8" - ); - expect(a).toContain("name: skill-a"); - expect(b).toContain("name: skill-b"); - }); - - // ── Skills: external ────────────────────────────────────────────────── - - it("does not write SKILL.md for external skills", async () => { - const config: LogicalConfig = { - mcp_servers: [], - instructions: [], - cli_actions: [], - knowledge_sources: [], - skills: [ - { - name: "playwright-cli", - source: "microsoft/playwright-cli/skills/playwright-cli" - } - ] - }; - - await claudeCodeWriter.install(config, dir); - - await expect( - access(join(dir, ".ade", "skills", "playwright-cli")) - ).rejects.toThrow(); - }); - - // ── Skills: mixed ───────────────────────────────────────────────────── - - it("writes only inline skills in mixed config", async () => { - const config: LogicalConfig = { - mcp_servers: [], - instructions: [], - cli_actions: [], - knowledge_sources: [], - skills: [ - { name: "my-conv", description: "Inline", body: "Do stuff." }, - { name: "ext-skill", source: "org/repo/skills/ext" } - ] - }; - - await claudeCodeWriter.install(config, dir); - - // Inline skill has SKILL.md - const skillMd = await readFile( - join(dir, ".ade", "skills", "my-conv", "SKILL.md"), - "utf-8" - ); - expect(skillMd).toContain("name: my-conv"); - - // External skill has no local files - await expect( - access(join(dir, ".ade", "skills", "ext-skill")) - ).rejects.toThrow(); - }); - - // ── Skills: MCP server ──────────────────────────────────────────────── - - it("adds skills-server MCP server when skills are present", async () => { - const config: LogicalConfig = { - mcp_servers: [], - instructions: [], - cli_actions: [], - knowledge_sources: [], - skills: [{ name: "my-skill", description: "A skill", body: "Do stuff." }] - }; - - await claudeCodeWriter.install(config, dir); - - const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8"); - const settings = JSON.parse(raw); - expect(settings.mcpServers["agentskills"]).toEqual({ - command: "npx", - args: ["-y", "@codemcp/skills-server"] - }); - }); - - it("skips .ade/skills when no skills present", async () => { - const config: LogicalConfig = { - mcp_servers: [], - instructions: ["hello"], - cli_actions: [], - knowledge_sources: [], - skills: [] - }; - - await claudeCodeWriter.install(config, dir); - - await expect(access(join(dir, ".ade"))).rejects.toThrow(); - }); -}); diff --git a/packages/core/src/agents/claude-code.ts b/packages/core/src/agents/claude-code.ts deleted file mode 100644 index 47faa4b..0000000 --- a/packages/core/src/agents/claude-code.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { join } from "node:path"; -import type { - AgentWriterDef, - LogicalConfig, - McpServerEntry, - InlineSkill -} from "../types.js"; - -function isInlineSkill( - skill: LogicalConfig["skills"][number] -): skill is InlineSkill { - return "body" in skill; -} - -export const claudeCodeWriter: AgentWriterDef = { - id: "claude-code", - async install(config: LogicalConfig, projectRoot: string) { - await writeAgentsMd(config, projectRoot); - await writeSkills(config, projectRoot); - await writeClaudeSettings(config, projectRoot); - } -}; - -async function writeAgentsMd( - config: LogicalConfig, - projectRoot: string -): Promise { - if (config.instructions.length === 0) return; - - const lines = ["# AGENTS", ""]; - for (const instruction of config.instructions) { - lines.push(instruction, ""); - } - - await writeFile(join(projectRoot, "AGENTS.md"), lines.join("\n"), "utf-8"); -} - -async function writeSkills( - config: LogicalConfig, - projectRoot: string -): Promise { - if (config.skills.length === 0) return; - - for (const skill of config.skills) { - if (!isInlineSkill(skill)) continue; - - const skillDir = join(projectRoot, ".ade", "skills", skill.name); - await mkdir(skillDir, { recursive: true }); - - const frontmatter = [ - "---", - `name: ${skill.name}`, - `description: ${skill.description}`, - "---" - ].join("\n"); - - const content = `${frontmatter}\n\n${skill.body}\n`; - await writeFile(join(skillDir, "SKILL.md"), content, "utf-8"); - } -} - -async function writeClaudeSettings( - config: LogicalConfig, - projectRoot: string -): Promise { - // Collect all MCP servers: explicit ones + skills-server if skills exist - const allServers: McpServerEntry[] = [...config.mcp_servers]; - - if (config.skills.length > 0) { - allServers.push({ - ref: "agentskills", - command: "npx", - args: ["-y", "@codemcp/skills-server"], - env: {} - }); - } - - if (allServers.length === 0) return; - - const claudeDir = join(projectRoot, ".claude"); - await mkdir(claudeDir, { recursive: true }); - - const settingsPath = join(claudeDir, "settings.json"); - - // Read existing settings to avoid clobbering user data - let existing: Record = {}; - try { - const raw = await readFile(settingsPath, "utf-8"); - existing = JSON.parse(raw); - } catch { - // No existing file — start fresh - } - - const mcpServers: Record< - string, - { command: string; args: string[]; env?: Record } - > = (existing.mcpServers as typeof mcpServers) ?? {}; - - for (const server of allServers) { - mcpServers[server.ref] = { - command: server.command, - args: server.args, - ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) - }; - } - - const settings = { ...existing, mcpServers }; - await writeFile( - settingsPath, - JSON.stringify(settings, null, 2) + "\n", - "utf-8" - ); -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f84bac7..29ccfea 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -38,7 +38,5 @@ export { } from "./registry.js"; export { resolve, collectDocsets } from "./resolver.js"; export { getDefaultCatalog, getFacet, getOption } from "./catalog/index.js"; -/** @deprecated Use @ade/harnesses package instead */ -export { claudeCodeWriter } from "./agents/claude-code.js"; export { skillsWriter } from "./writers/skills.js"; export { knowledgeWriter } from "./writers/knowledge.js"; diff --git a/packages/harnesses/package.json b/packages/harnesses/package.json index f5f5595..4f13825 100644 --- a/packages/harnesses/package.json +++ b/packages/harnesses/package.json @@ -19,7 +19,8 @@ "typecheck": "tsc" }, "dependencies": { - "@ade/core": "workspace:*" + "@ade/core": "workspace:*", + "@codemcp/skills": "^2.3.0" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^8.21.0", diff --git a/packages/harnesses/src/index.ts b/packages/harnesses/src/index.ts index d46526b..9087d1e 100644 --- a/packages/harnesses/src/index.ts +++ b/packages/harnesses/src/index.ts @@ -1,4 +1,5 @@ export type { HarnessWriter } from "./types.js"; +export { installSkills } from "./skills-installer.js"; export { universalWriter } from "./writers/universal.js"; export { claudeCodeWriter } from "./writers/claude-code.js"; diff --git a/packages/cli/src/skills-installer.ts b/packages/harnesses/src/skills-installer.ts similarity index 100% rename from packages/cli/src/skills-installer.ts rename to packages/harnesses/src/skills-installer.ts diff --git a/packages/harnesses/src/util.ts b/packages/harnesses/src/util.ts new file mode 100644 index 0000000..39a2ee3 --- /dev/null +++ b/packages/harnesses/src/util.ts @@ -0,0 +1,188 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import type { LogicalConfig, McpServerEntry } from "@ade/core"; + +// --------------------------------------------------------------------------- +// JSON helpers +// --------------------------------------------------------------------------- + +/** Read a JSON file, returning `{}` if missing or unparseable. */ +export async function readJsonOrEmpty( + path: string +): Promise> { + try { + return JSON.parse(await readFile(path, "utf-8")); + } catch { + return {}; + } +} + +/** Write a JSON object with trailing newline. Creates parent dirs. */ +export async function writeJson(path: string, data: unknown): Promise { + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, JSON.stringify(data, null, 2) + "\n", "utf-8"); +} + +// --------------------------------------------------------------------------- +// Server entry transform — each harness overrides only what differs +// --------------------------------------------------------------------------- + +/** Minimal MCP entry: command + args + optional env. */ +function baseEntry(server: McpServerEntry) { + return { + command: server.command, + args: server.args, + ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) + }; +} + +export type ServerTransform = ( + server: McpServerEntry +) => Record; + +/** Standard mcpServers entry (cursor, universal, claude-code). */ +export const standardEntry: ServerTransform = baseEntry; + +/** Adds `type: "stdio"` (copilot). */ +export const stdioEntry: ServerTransform = (s) => ({ + type: "stdio", + ...baseEntry(s) +}); + +/** Adds `alwaysAllow` (cline, roo-code, windsurf). */ +export const alwaysAllowEntry: ServerTransform = (s) => ({ + ...baseEntry(s), + alwaysAllow: s.allowedTools ?? ["*"] +}); + +// --------------------------------------------------------------------------- +// MCP JSON writer — covers 7 of 9 harnesses +// --------------------------------------------------------------------------- + +interface WriteMcpServersOpts { + /** Full path to the JSON file. */ + path: string; + /** Key in the JSON that holds the server map. Default: `"mcpServers"`. */ + key?: string; + /** Transform each McpServerEntry into the harness-specific shape. */ + transform?: ServerTransform; + /** Extra top-level fields to merge (e.g. `$schema`). */ + defaults?: Record; +} + +/** + * Merge MCP server entries into an existing JSON config file. + * Creates the file (and parent dirs) if missing. + */ +export async function writeMcpServers( + servers: McpServerEntry[], + opts: WriteMcpServersOpts +): Promise { + if (servers.length === 0) return; + + const key = opts.key ?? "mcpServers"; + const transform = opts.transform ?? standardEntry; + + const existing = await readJsonOrEmpty(opts.path); + const map = (existing[key] as Record) ?? {}; + + for (const server of servers) { + map[server.ref] = transform(server); + } + + const result = { ...(opts.defaults ?? {}), ...existing, [key]: map }; + await writeJson(opts.path, result); +} + +// --------------------------------------------------------------------------- +// Instructions → flat rules file (windsurf, cline, roo-code) +// --------------------------------------------------------------------------- + +/** + * Write instructions as a plain text rules file. + * Skips if no instructions. + */ +export async function writeRulesFile( + instructions: string[], + path: string +): Promise { + if (instructions.length === 0) return; + const lines = instructions.flatMap((i) => [i, ""]); + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, lines.join("\n"), "utf-8"); +} + +// --------------------------------------------------------------------------- +// Instructions → agent markdown with YAML frontmatter +// --------------------------------------------------------------------------- + +interface AgentMdOpts { + /** Full path to the .md file. */ + path: string; + /** Extra YAML frontmatter lines (after name/description, before `---`). */ + extraFrontmatter?: string[]; + /** Fallback body when instructions are empty. */ + fallbackBody?: string; +} + +/** + * Write an agent markdown file with YAML frontmatter. + * Shared by claude-code, copilot, and opencode. + */ +export async function writeAgentMd( + config: LogicalConfig, + opts: AgentMdOpts +): Promise { + if (config.instructions.length === 0 && config.mcp_servers.length === 0) + return; + + const fm: string[] = [ + "---", + "name: ade", + "description: ADE — Agentic Development Environment agent with project conventions and tools" + ]; + + if (opts.extraFrontmatter) { + fm.push(...opts.extraFrontmatter); + } + + fm.push("---"); + + const body = + config.instructions.length > 0 + ? config.instructions.join("\n\n") + : (opts.fallbackBody ?? ""); + + const content = fm.join("\n") + "\n\n" + body + "\n"; + await mkdir(dirname(opts.path), { recursive: true }); + await writeFile(opts.path, content, "utf-8"); +} + +// --------------------------------------------------------------------------- +// Inline skill SKILL.md writer (used by claude-code) +// --------------------------------------------------------------------------- + +export async function writeInlineSkills( + config: LogicalConfig, + projectRoot: string +): Promise { + for (const skill of config.skills) { + if (!("body" in skill)) continue; + + const skillDir = join(projectRoot, ".ade", "skills", skill.name); + await mkdir(skillDir, { recursive: true }); + + const frontmatter = [ + "---", + `name: ${skill.name}`, + `description: ${skill.description}`, + "---" + ].join("\n"); + + await writeFile( + join(skillDir, "SKILL.md"), + `${frontmatter}\n\n${skill.body}\n`, + "utf-8" + ); + } +} diff --git a/packages/harnesses/src/writers/claude-code.ts b/packages/harnesses/src/writers/claude-code.ts index cb56fa6..0052734 100644 --- a/packages/harnesses/src/writers/claude-code.ts +++ b/packages/harnesses/src/writers/claude-code.ts @@ -1,13 +1,13 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import type { LogicalConfig, McpServerEntry, InlineSkill } from "@ade/core"; +import type { LogicalConfig } from "@ade/core"; import type { HarnessWriter } from "../types.js"; - -function isInlineSkill( - skill: LogicalConfig["skills"][number] -): skill is InlineSkill { - return "body" in skill; -} +import { + readJsonOrEmpty, + writeJson, + writeMcpServers, + writeAgentMd, + writeInlineSkills +} from "../util.js"; export const claudeCodeWriter: HarnessWriter = { id: "claude-code", @@ -15,111 +15,32 @@ export const claudeCodeWriter: HarnessWriter = { description: "Anthropic's CLI agent — .claude/agents/ade.md + .mcp.json + .claude/settings.json", async install(config: LogicalConfig, projectRoot: string) { - await writeCustomAgent(config, projectRoot); - await writeMcpJson(config, projectRoot); - await writeClaudeSettings(config, projectRoot); - await writeSkills(config, projectRoot); - } -}; - -/** - * Write .claude/agents/ade.md — the preferred custom agent definition - * that combines instructions and MCP tool references. - */ -async function writeCustomAgent( - config: LogicalConfig, - projectRoot: string -): Promise { - const allServers: McpServerEntry[] = config.mcp_servers; - - if (config.instructions.length === 0 && allServers.length === 0) return; - - const agentsDir = join(projectRoot, ".claude", "agents"); - await mkdir(agentsDir, { recursive: true }); - - const frontmatter: string[] = [ - "---", - "name: ade", - "description: ADE — Agentic Development Environment agent with project conventions and tools" - ]; + await writeAgentMd(config, { + path: join(projectRoot, ".claude", "agents", "ade.md"), + fallbackBody: "ADE — Agentic Development Environment agent." + }); - frontmatter.push("---"); - - const body = - config.instructions.length > 0 - ? config.instructions.join("\n\n") - : "ADE — Agentic Development Environment agent."; - - const content = frontmatter.join("\n") + "\n\n" + body + "\n"; - await writeFile(join(agentsDir, "ade.md"), content, "utf-8"); -} - -/** - * Write .mcp.json — the standard MCP config at project root. - * Claude Code reads this natively. - */ -async function writeMcpJson( - config: LogicalConfig, - projectRoot: string -): Promise { - const allServers: McpServerEntry[] = config.mcp_servers; - - if (allServers.length === 0) return; - - const mcpPath = join(projectRoot, ".mcp.json"); - - let existing: Record = {}; - try { - const raw = await readFile(mcpPath, "utf-8"); - existing = JSON.parse(raw); - } catch { - // Start fresh - } + await writeMcpServers(config.mcp_servers, { + path: join(projectRoot, ".mcp.json") + }); - const mcpServers: Record< - string, - { command: string; args: string[]; env?: Record } - > = (existing.mcpServers as typeof mcpServers) ?? {}; - - for (const server of allServers) { - mcpServers[server.ref] = { - command: server.command, - args: server.args, - ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) - }; + await writeClaudeSettings(config, projectRoot); + await writeInlineSkills(config, projectRoot); } +}; - const result = { ...existing, mcpServers }; - await writeFile(mcpPath, JSON.stringify(result, null, 2) + "\n", "utf-8"); -} - -/** - * Write .claude/settings.json — permissions for MCP tools. - */ async function writeClaudeSettings( config: LogicalConfig, projectRoot: string ): Promise { - const allServers: McpServerEntry[] = config.mcp_servers; - - if (allServers.length === 0) return; + const servers = config.mcp_servers; + if (servers.length === 0) return; - const claudeDir = join(projectRoot, ".claude"); - await mkdir(claudeDir, { recursive: true }); + const settingsPath = join(projectRoot, ".claude", "settings.json"); + const existing = await readJsonOrEmpty(settingsPath); - const settingsPath = join(claudeDir, "settings.json"); - - let existing: Record = {}; - try { - const raw = await readFile(settingsPath, "utf-8"); - existing = JSON.parse(raw); - } catch { - // No existing file — start fresh - } - - // Build permission allow-list for MCP tools const allowRules: string[] = []; - for (const server of allServers) { + for (const server of servers) { const allowed = server.allowedTools ?? ["*"]; if (allowed.includes("*")) { allowRules.push(`MCP(${server.ref}:*)`); @@ -130,48 +51,12 @@ async function writeClaudeSettings( } } - const existingPermissions = - (existing.permissions as Record) ?? {}; - const existingAllow = (existingPermissions.allow as string[]) ?? []; - - // Merge: keep existing rules, add new ones + const existingPerms = (existing.permissions as Record) ?? {}; + const existingAllow = (existingPerms.allow as string[]) ?? []; const mergedAllow = [...new Set([...existingAllow, ...allowRules])]; - const settings = { + await writeJson(settingsPath, { ...existing, - permissions: { - ...existingPermissions, - allow: mergedAllow - } - }; - - await writeFile( - settingsPath, - JSON.stringify(settings, null, 2) + "\n", - "utf-8" - ); -} - -async function writeSkills( - config: LogicalConfig, - projectRoot: string -): Promise { - if (config.skills.length === 0) return; - - for (const skill of config.skills) { - if (!isInlineSkill(skill)) continue; - - const skillDir = join(projectRoot, ".ade", "skills", skill.name); - await mkdir(skillDir, { recursive: true }); - - const frontmatter = [ - "---", - `name: ${skill.name}`, - `description: ${skill.description}`, - "---" - ].join("\n"); - - const content = `${frontmatter}\n\n${skill.body}\n`; - await writeFile(join(skillDir, "SKILL.md"), content, "utf-8"); - } + permissions: { ...existingPerms, allow: mergedAllow } + }); } diff --git a/packages/harnesses/src/writers/cline.ts b/packages/harnesses/src/writers/cline.ts index 767cd6a..3c89388 100644 --- a/packages/harnesses/src/writers/cline.ts +++ b/packages/harnesses/src/writers/cline.ts @@ -1,69 +1,18 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import type { LogicalConfig, McpServerEntry } from "@ade/core"; +import type { LogicalConfig } from "@ade/core"; import type { HarnessWriter } from "../types.js"; +import { writeMcpServers, alwaysAllowEntry, writeRulesFile } from "../util.js"; export const clineWriter: HarnessWriter = { id: "cline", label: "Cline", description: "VS Code AI agent — .cline/mcp.json + .clinerules", async install(config: LogicalConfig, projectRoot: string) { - await writeMcpJson(config, projectRoot); - await writeRules(config, projectRoot); - } -}; - -async function writeMcpJson( - config: LogicalConfig, - projectRoot: string -): Promise { - const allServers: McpServerEntry[] = config.mcp_servers; - - if (allServers.length === 0) return; + await writeMcpServers(config.mcp_servers, { + path: join(projectRoot, ".cline", "mcp.json"), + transform: alwaysAllowEntry + }); - const clineDir = join(projectRoot, ".cline"); - await mkdir(clineDir, { recursive: true }); - - const mcpPath = join(clineDir, "mcp.json"); - - let existing: Record = {}; - try { - const raw = await readFile(mcpPath, "utf-8"); - existing = JSON.parse(raw); - } catch { - // Start fresh + await writeRulesFile(config.instructions, join(projectRoot, ".clinerules")); } - - const mcpServers: Record< - string, - { - command: string; - args: string[]; - env?: Record; - alwaysAllow?: string[]; - } - > = (existing.mcpServers as typeof mcpServers) ?? {}; - - for (const server of allServers) { - const allowed = server.allowedTools ?? ["*"]; - mcpServers[server.ref] = { - command: server.command, - args: server.args, - ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}), - alwaysAllow: allowed - }; - } - - const result = { ...existing, mcpServers }; - await writeFile(mcpPath, JSON.stringify(result, null, 2) + "\n", "utf-8"); -} - -async function writeRules( - config: LogicalConfig, - projectRoot: string -): Promise { - if (config.instructions.length === 0) return; - - const lines = config.instructions.flatMap((i) => [i, ""]); - await writeFile(join(projectRoot, ".clinerules"), lines.join("\n"), "utf-8"); -} +}; diff --git a/packages/harnesses/src/writers/copilot.ts b/packages/harnesses/src/writers/copilot.ts index 2200427..17e61b6 100644 --- a/packages/harnesses/src/writers/copilot.ts +++ b/packages/harnesses/src/writers/copilot.ts @@ -1,102 +1,32 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import type { LogicalConfig, McpServerEntry } from "@ade/core"; +import type { LogicalConfig } from "@ade/core"; import type { HarnessWriter } from "../types.js"; +import { writeMcpServers, stdioEntry, writeAgentMd } from "../util.js"; export const copilotWriter: HarnessWriter = { id: "copilot", label: "GitHub Copilot", description: "VS Code + CLI — .vscode/mcp.json + .github/agents/ade.agent.md", async install(config: LogicalConfig, projectRoot: string) { - await writeVsCodeMcp(config, projectRoot); - await writeCopilotAgent(config, projectRoot); + await writeMcpServers(config.mcp_servers, { + path: join(projectRoot, ".vscode", "mcp.json"), + key: "servers", + transform: stdioEntry + }); + + const tools = [ + "edit", + "search", + "runCommands", + "runTasks", + "fetch", + "githubRepo", + ...config.mcp_servers.map((s) => `${s.ref}/*`) + ]; + + await writeAgentMd(config, { + path: join(projectRoot, ".github", "agents", "ade.agent.md"), + extraFrontmatter: ["tools:", ...tools.map((t) => ` - ${t}`)] + }); } }; - -async function writeVsCodeMcp( - config: LogicalConfig, - projectRoot: string -): Promise { - const allServers: McpServerEntry[] = config.mcp_servers; - - if (allServers.length === 0) return; - - const vscodeDir = join(projectRoot, ".vscode"); - await mkdir(vscodeDir, { recursive: true }); - - const mcpPath = join(vscodeDir, "mcp.json"); - - let existing: Record = {}; - try { - const raw = await readFile(mcpPath, "utf-8"); - existing = JSON.parse(raw); - } catch { - // Start fresh - } - - // Copilot uses "servers" key, not "mcpServers" - const servers: Record< - string, - { - type: string; - command: string; - args: string[]; - env?: Record; - } - > = (existing.servers as typeof servers) ?? {}; - - for (const server of allServers) { - servers[server.ref] = { - type: "stdio", - command: server.command, - args: server.args, - ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) - }; - } - - const result = { ...existing, servers }; - await writeFile(mcpPath, JSON.stringify(result, null, 2) + "\n", "utf-8"); -} - -/** - * Write a dedicated ADE agent definition that combines instructions and - * references configured MCP servers. Read by both VS Code Copilot and - * GitHub Copilot CLI. - */ -async function writeCopilotAgent( - config: LogicalConfig, - projectRoot: string -): Promise { - const allServers: McpServerEntry[] = config.mcp_servers; - - if (config.instructions.length === 0 && allServers.length === 0) return; - - const agentsDir = join(projectRoot, ".github", "agents"); - await mkdir(agentsDir, { recursive: true }); - - const frontmatter: string[] = [ - "---", - "name: ade", - "description: ADE — Agentic Development Environment agent with project conventions and tools" - ]; - - // Built-in tools + MCP server wildcards (server/* grants all tools) - const tools = [ - "edit", - "search", - "runCommands", - "runTasks", - "fetch", - "githubRepo", - ...allServers.map((s) => `${s.ref}/*`) - ]; - frontmatter.push("tools:", ...tools.map((t) => ` - ${t}`)); - - frontmatter.push("---"); - - const body = - config.instructions.length > 0 ? config.instructions.join("\n\n") : ""; - - const content = frontmatter.join("\n") + "\n\n" + body + "\n"; - await writeFile(join(agentsDir, "ade.agent.md"), content, "utf-8"); -} diff --git a/packages/harnesses/src/writers/cursor.ts b/packages/harnesses/src/writers/cursor.ts index 54327aa..321cbac 100644 --- a/packages/harnesses/src/writers/cursor.ts +++ b/packages/harnesses/src/writers/cursor.ts @@ -1,73 +1,32 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import type { LogicalConfig, McpServerEntry } from "@ade/core"; +import type { LogicalConfig } from "@ade/core"; import type { HarnessWriter } from "../types.js"; +import { writeMcpServers } from "../util.js"; export const cursorWriter: HarnessWriter = { id: "cursor", label: "Cursor", description: "AI code editor — .cursor/mcp.json + .cursor/rules/", async install(config: LogicalConfig, projectRoot: string) { - await writeMcpJson(config, projectRoot); - await writeRules(config, projectRoot); + await writeMcpServers(config.mcp_servers, { + path: join(projectRoot, ".cursor", "mcp.json") + }); + + if (config.instructions.length > 0) { + const rulesDir = join(projectRoot, ".cursor", "rules"); + await mkdir(rulesDir, { recursive: true }); + + const content = [ + "---", + "description: ADE project conventions", + "globs: *", + "---", + "", + ...config.instructions.flatMap((i) => [i, ""]) + ].join("\n"); + + await writeFile(join(rulesDir, "ade.mdc"), content, "utf-8"); + } } }; - -async function writeMcpJson( - config: LogicalConfig, - projectRoot: string -): Promise { - const allServers: McpServerEntry[] = config.mcp_servers; - - if (allServers.length === 0) return; - - const cursorDir = join(projectRoot, ".cursor"); - await mkdir(cursorDir, { recursive: true }); - - const mcpPath = join(cursorDir, "mcp.json"); - - let existing: Record = {}; - try { - const raw = await readFile(mcpPath, "utf-8"); - existing = JSON.parse(raw); - } catch { - // Start fresh - } - - const mcpServers: Record< - string, - { command: string; args: string[]; env?: Record } - > = (existing.mcpServers as typeof mcpServers) ?? {}; - - for (const server of allServers) { - mcpServers[server.ref] = { - command: server.command, - args: server.args, - ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) - }; - } - - const result = { ...existing, mcpServers }; - await writeFile(mcpPath, JSON.stringify(result, null, 2) + "\n", "utf-8"); -} - -async function writeRules( - config: LogicalConfig, - projectRoot: string -): Promise { - if (config.instructions.length === 0) return; - - const rulesDir = join(projectRoot, ".cursor", "rules"); - await mkdir(rulesDir, { recursive: true }); - - const content = [ - "---", - "description: ADE project conventions", - "globs: *", - "---", - "", - ...config.instructions.flatMap((i) => [i, ""]) - ].join("\n"); - - await writeFile(join(rulesDir, "ade.mdc"), content, "utf-8"); -} diff --git a/packages/harnesses/src/writers/kiro.ts b/packages/harnesses/src/writers/kiro.ts index b8e9cde..8f00c73 100644 --- a/packages/harnesses/src/writers/kiro.ts +++ b/packages/harnesses/src/writers/kiro.ts @@ -1,84 +1,51 @@ -import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import type { LogicalConfig, McpServerEntry } from "@ade/core"; +import type { LogicalConfig } from "@ade/core"; import type { HarnessWriter } from "../types.js"; +import { standardEntry, writeJson } from "../util.js"; export const kiroWriter: HarnessWriter = { id: "kiro", label: "Kiro", description: "AWS AI IDE — .kiro/agents/ade.json", async install(config: LogicalConfig, projectRoot: string) { - await writeAgentJson(config, projectRoot); - } -}; - -async function writeAgentJson( - config: LogicalConfig, - projectRoot: string -): Promise { - const allServers: McpServerEntry[] = config.mcp_servers; - - if (allServers.length === 0 && config.instructions.length === 0) return; - - const agentsDir = join(projectRoot, ".kiro", "agents"); - await mkdir(agentsDir, { recursive: true }); - - const agentPath = join(agentsDir, "ade.json"); - - const mcpServers: Record< - string, - { command: string; args: string[]; env?: Record } - > = {}; - - for (const server of allServers) { - mcpServers[server.ref] = { - command: server.command, - args: server.args, - ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) - }; - } + const servers = config.mcp_servers; + if (servers.length === 0 && config.instructions.length === 0) return; - // Kiro tools: built-in tools + @server references - const tools: string[] = [ - "execute_bash", - "fs_read", - "fs_write", - "knowledge", - "thinking" - ]; - for (const name of Object.keys(mcpServers)) { - tools.push(`@${name}`); - } + const mcpServers: Record = {}; + for (const s of servers) { + mcpServers[s.ref] = standardEntry(s); + } - // Kiro allowedTools: grant wildcard access to each MCP server - const allowedTools: string[] = []; - for (const server of allServers) { - const explicit = server.allowedTools; - if (explicit && !explicit.includes("*")) { - for (const tool of explicit) { - allowedTools.push(`@${server.ref}/${tool}`); + const tools: string[] = [ + "execute_bash", + "fs_read", + "fs_write", + "knowledge", + "thinking", + ...Object.keys(mcpServers).map((n) => `@${n}`) + ]; + + const allowedTools: string[] = []; + for (const s of servers) { + const explicit = s.allowedTools; + if (explicit && !explicit.includes("*")) { + for (const tool of explicit) { + allowedTools.push(`@${s.ref}/${tool}`); + } + } else { + allowedTools.push(`@${s.ref}/*`); } - } else { - allowedTools.push(`@${server.ref}/*`); } - } - const prompt = - config.instructions.length > 0 - ? config.instructions.join("\n\n") - : "ADE — Agentic Development Environment agent"; - - const agentConfig = { - name: "ade", - prompt, - mcpServers, - tools, - allowedTools - }; - - await writeFile( - agentPath, - JSON.stringify(agentConfig, null, 2) + "\n", - "utf-8" - ); -} + await writeJson(join(projectRoot, ".kiro", "agents", "ade.json"), { + name: "ade", + prompt: + config.instructions.length > 0 + ? config.instructions.join("\n\n") + : "ADE — Agentic Development Environment agent", + mcpServers, + tools, + allowedTools + }); + } +}; diff --git a/packages/harnesses/src/writers/opencode.ts b/packages/harnesses/src/writers/opencode.ts index 772ae1b..a25e311 100644 --- a/packages/harnesses/src/writers/opencode.ts +++ b/packages/harnesses/src/writers/opencode.ts @@ -1,105 +1,53 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import type { LogicalConfig, McpServerEntry } from "@ade/core"; +import type { LogicalConfig } from "@ade/core"; import type { HarnessWriter } from "../types.js"; +import { writeMcpServers, writeAgentMd } from "../util.js"; export const opencodeWriter: HarnessWriter = { id: "opencode", label: "OpenCode", description: "Terminal AI agent — opencode.json + .opencode/agents/", async install(config: LogicalConfig, projectRoot: string) { - await writeOpenCodeJson(config, projectRoot); - await writeAgentMd(config, projectRoot); - } -}; - -async function writeOpenCodeJson( - config: LogicalConfig, - projectRoot: string -): Promise { - const allServers: McpServerEntry[] = config.mcp_servers; - - if (allServers.length === 0) return; - - const configPath = join(projectRoot, "opencode.json"); - - let existing: Record = {}; - try { - const raw = await readFile(configPath, "utf-8"); - existing = JSON.parse(raw); - } catch { - // Start fresh - } - - const mcp: Record< - string, - { type: string; command: string[]; env?: Record } - > = (existing.mcp as typeof mcp) ?? {}; - - for (const server of allServers) { - mcp[server.ref] = { - type: "local", - command: [server.command, ...server.args], - ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) - }; - } - - const result = { - $schema: "https://opencode.ai/config.json", - ...existing, - mcp - }; - - await writeFile(configPath, JSON.stringify(result, null, 2) + "\n", "utf-8"); -} - -async function writeAgentMd( - config: LogicalConfig, - projectRoot: string -): Promise { - const allServers: McpServerEntry[] = config.mcp_servers; - - if (config.instructions.length === 0 && allServers.length === 0) return; - - const agentsDir = join(projectRoot, ".opencode", "agents"); - await mkdir(agentsDir, { recursive: true }); - - const frontmatter: string[] = [ - "---", - "name: ade", - "description: ADE — Agentic Development Environment agent" - ]; - - // Tool permissions - frontmatter.push("tools:"); - frontmatter.push(" read: true"); - frontmatter.push(" edit: approve"); - frontmatter.push(" bash: approve"); - - // MCP server references - if (allServers.length > 0) { - frontmatter.push("mcp_servers:"); - for (const server of allServers) { - frontmatter.push(` ${server.ref}:`); - frontmatter.push( - ` command: [${[server.command, ...server.args].map((a) => `"${a}"`).join(", ")}]` - ); - if (Object.keys(server.env).length > 0) { - frontmatter.push(" env:"); - for (const [k, v] of Object.entries(server.env)) { - frontmatter.push(` ${k}: "${v}"`); + await writeMcpServers(config.mcp_servers, { + path: join(projectRoot, "opencode.json"), + key: "mcp", + transform: (s) => ({ + type: "local", + command: [s.command, ...s.args], + ...(Object.keys(s.env).length > 0 ? { env: s.env } : {}) + }), + defaults: { $schema: "https://opencode.ai/config.json" } + }); + + const servers = config.mcp_servers; + const extraFm: string[] = [ + "tools:", + " read: true", + " edit: approve", + " bash: approve" + ]; + + if (servers.length > 0) { + extraFm.push("mcp_servers:"); + for (const s of servers) { + extraFm.push(` ${s.ref}:`); + extraFm.push( + ` command: [${[s.command, ...s.args].map((a) => `"${a}"`).join(", ")}]` + ); + if (Object.keys(s.env).length > 0) { + extraFm.push(" env:"); + for (const [k, v] of Object.entries(s.env)) { + extraFm.push(` ${k}: "${v}"`); + } } } } - } - - frontmatter.push("---"); - - const body = - config.instructions.length > 0 - ? config.instructions.join("\n\n") - : "ADE — Agentic Development Environment agent with project conventions and tools."; - const content = frontmatter.join("\n") + "\n\n" + body + "\n"; - await writeFile(join(agentsDir, "ade.md"), content, "utf-8"); -} + await writeAgentMd(config, { + path: join(projectRoot, ".opencode", "agents", "ade.md"), + extraFrontmatter: extraFm, + fallbackBody: + "ADE — Agentic Development Environment agent with project conventions and tools." + }); + } +}; diff --git a/packages/harnesses/src/writers/roo-code.ts b/packages/harnesses/src/writers/roo-code.ts index b314beb..a1d008d 100644 --- a/packages/harnesses/src/writers/roo-code.ts +++ b/packages/harnesses/src/writers/roo-code.ts @@ -1,69 +1,18 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import type { LogicalConfig, McpServerEntry } from "@ade/core"; +import type { LogicalConfig } from "@ade/core"; import type { HarnessWriter } from "../types.js"; +import { writeMcpServers, alwaysAllowEntry, writeRulesFile } from "../util.js"; export const rooCodeWriter: HarnessWriter = { id: "roo-code", label: "Roo Code", description: "AI coding agent — .roo/mcp.json + .roorules", async install(config: LogicalConfig, projectRoot: string) { - await writeMcpJson(config, projectRoot); - await writeRules(config, projectRoot); - } -}; - -async function writeMcpJson( - config: LogicalConfig, - projectRoot: string -): Promise { - const allServers: McpServerEntry[] = config.mcp_servers; - - if (allServers.length === 0) return; + await writeMcpServers(config.mcp_servers, { + path: join(projectRoot, ".roo", "mcp.json"), + transform: alwaysAllowEntry + }); - const rooDir = join(projectRoot, ".roo"); - await mkdir(rooDir, { recursive: true }); - - const mcpPath = join(rooDir, "mcp.json"); - - let existing: Record = {}; - try { - const raw = await readFile(mcpPath, "utf-8"); - existing = JSON.parse(raw); - } catch { - // Start fresh + await writeRulesFile(config.instructions, join(projectRoot, ".roorules")); } - - const mcpServers: Record< - string, - { - command: string; - args: string[]; - env?: Record; - alwaysAllow?: string[]; - } - > = (existing.mcpServers as typeof mcpServers) ?? {}; - - for (const server of allServers) { - const allowed = server.allowedTools ?? ["*"]; - mcpServers[server.ref] = { - command: server.command, - args: server.args, - ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}), - alwaysAllow: allowed - }; - } - - const result = { ...existing, mcpServers }; - await writeFile(mcpPath, JSON.stringify(result, null, 2) + "\n", "utf-8"); -} - -async function writeRules( - config: LogicalConfig, - projectRoot: string -): Promise { - if (config.instructions.length === 0) return; - - const lines = config.instructions.flatMap((i) => [i, ""]); - await writeFile(join(projectRoot, ".roorules"), lines.join("\n"), "utf-8"); -} +}; diff --git a/packages/harnesses/src/writers/universal.ts b/packages/harnesses/src/writers/universal.ts index 0ea55fe..29ab3d4 100644 --- a/packages/harnesses/src/writers/universal.ts +++ b/packages/harnesses/src/writers/universal.ts @@ -1,69 +1,30 @@ -import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import type { LogicalConfig, McpServerEntry } from "@ade/core"; +import { writeFile } from "node:fs/promises"; +import type { LogicalConfig } from "@ade/core"; import type { HarnessWriter } from "../types.js"; +import { writeMcpServers } from "../util.js"; -/** - * Universal harness — generates the cross-tool standard files: - * AGENTS.md (instructions readable by all agents) - * .mcp.json (MCP server config readable by Claude Code and others) - */ export const universalWriter: HarnessWriter = { id: "universal", label: "Universal (AGENTS.md + .mcp.json)", description: "Cross-tool standard — AGENTS.md + .mcp.json (works with any agent)", async install(config: LogicalConfig, projectRoot: string) { - await writeAgentsMd(config, projectRoot); - await writeMcpJson(config, projectRoot); + if (config.instructions.length > 0) { + const lines = [ + "# AGENTS", + "", + ...config.instructions.flatMap((i) => [i, ""]) + ]; + await writeFile( + join(projectRoot, "AGENTS.md"), + lines.join("\n"), + "utf-8" + ); + } + + await writeMcpServers(config.mcp_servers, { + path: join(projectRoot, ".mcp.json") + }); } }; - -async function writeAgentsMd( - config: LogicalConfig, - projectRoot: string -): Promise { - if (config.instructions.length === 0) return; - - const lines = ["# AGENTS", ""]; - for (const instruction of config.instructions) { - lines.push(instruction, ""); - } - - await writeFile(join(projectRoot, "AGENTS.md"), lines.join("\n"), "utf-8"); -} - -async function writeMcpJson( - config: LogicalConfig, - projectRoot: string -): Promise { - const allServers: McpServerEntry[] = config.mcp_servers; - - if (allServers.length === 0) return; - - const mcpPath = join(projectRoot, ".mcp.json"); - - let existing: Record = {}; - try { - const raw = await readFile(mcpPath, "utf-8"); - existing = JSON.parse(raw); - } catch { - // Start fresh - } - - const mcpServers: Record< - string, - { command: string; args: string[]; env?: Record } - > = (existing.mcpServers as typeof mcpServers) ?? {}; - - for (const server of allServers) { - mcpServers[server.ref] = { - command: server.command, - args: server.args, - ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}) - }; - } - - const result = { ...existing, mcpServers }; - await writeFile(mcpPath, JSON.stringify(result, null, 2) + "\n", "utf-8"); -} diff --git a/packages/harnesses/src/writers/windsurf.ts b/packages/harnesses/src/writers/windsurf.ts index 175199e..e03d298 100644 --- a/packages/harnesses/src/writers/windsurf.ts +++ b/packages/harnesses/src/writers/windsurf.ts @@ -1,73 +1,21 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import type { LogicalConfig, McpServerEntry } from "@ade/core"; +import type { LogicalConfig } from "@ade/core"; import type { HarnessWriter } from "../types.js"; +import { writeMcpServers, alwaysAllowEntry, writeRulesFile } from "../util.js"; export const windsurfWriter: HarnessWriter = { id: "windsurf", label: "Windsurf", description: "Codeium's AI IDE — .windsurf/mcp.json + .windsurfrules", async install(config: LogicalConfig, projectRoot: string) { - await writeMcpJson(config, projectRoot); - await writeRules(config, projectRoot); + await writeMcpServers(config.mcp_servers, { + path: join(projectRoot, ".windsurf", "mcp.json"), + transform: alwaysAllowEntry + }); + + await writeRulesFile( + config.instructions, + join(projectRoot, ".windsurfrules") + ); } }; - -async function writeMcpJson( - config: LogicalConfig, - projectRoot: string -): Promise { - const allServers: McpServerEntry[] = config.mcp_servers; - - if (allServers.length === 0) return; - - const windsurfDir = join(projectRoot, ".windsurf"); - await mkdir(windsurfDir, { recursive: true }); - - const mcpPath = join(windsurfDir, "mcp.json"); - - let existing: Record = {}; - try { - const raw = await readFile(mcpPath, "utf-8"); - existing = JSON.parse(raw); - } catch { - // Start fresh - } - - const mcpServers: Record< - string, - { - command: string; - args: string[]; - env?: Record; - alwaysAllow?: string[]; - } - > = (existing.mcpServers as typeof mcpServers) ?? {}; - - for (const server of allServers) { - const allowed = server.allowedTools ?? ["*"]; - mcpServers[server.ref] = { - command: server.command, - args: server.args, - ...(Object.keys(server.env).length > 0 ? { env: server.env } : {}), - alwaysAllow: allowed - }; - } - - const result = { ...existing, mcpServers }; - await writeFile(mcpPath, JSON.stringify(result, null, 2) + "\n", "utf-8"); -} - -async function writeRules( - config: LogicalConfig, - projectRoot: string -): Promise { - if (config.instructions.length === 0) return; - - const lines = config.instructions.flatMap((i) => [i, ""]); - await writeFile( - join(projectRoot, ".windsurfrules"), - lines.join("\n"), - "utf-8" - ); -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c979928..0ec574d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,9 +104,6 @@ importers: "@clack/prompts": specifier: ^1.1.0 version: 1.1.0 - "@codemcp/skills": - specifier: ^2.3.0 - version: 2.3.0 devDependencies: "@codemcp/knowledge": specifier: 2.1.0 @@ -166,6 +163,9 @@ importers: "@ade/core": specifier: workspace:* version: link:../core + "@codemcp/skills": + specifier: ^2.3.0 + version: 2.3.0 devDependencies: "@typescript-eslint/eslint-plugin": specifier: ^8.21.0 From a17072c68fca447bdc6965efba68445be678db8f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 21:16:44 +0000 Subject: [PATCH 57/60] docs: position ADE within harness engineering landscape Incorporate concepts from HumanLayer's "Skill Issue" article to connect ADE's information architecture to the broader harness engineering practice. - Reference harness engineering in "Why this is needed" and cite ETH Zurich agentfile study - Add "Where ADE fits" section mapping ADE's three layers to information levers and acknowledging complementary runtime levers (sub-agents, hooks, back-pressure) - Add "bias towards shipping" guidance in Customization - Add Further Reading section linking the article https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- README.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3d30c67..c7eb99d 100644 --- a/README.md +++ b/README.md @@ -83,15 +83,24 @@ flowchart TD ### Why this is needed There is no secret ingredient to interacting with coding agents. We just have to -instruct them properly. But this "properly" can be achieve in many ways. -Where do we put write down this steering? In the `AGENTS.md`? In Skills? Move it to a prompt, -potentially exposed by an MCP server? And how do we share it across team mates? -It has become good practice to check this into the repo, but honestly: +instruct them properly. The emerging practice of +[harness engineering](https://www.humanlayer.dev/blog/skill-issue-harness-engineering-for-coding-agents) +— leveraging agent configuration points to improve output quality and +reliability — has shown that most agent failures are configuration problems, not +capability problems. But _how_ do you configure well? + +Where do you write down the steering? In the `AGENTS.md`? In skills? Move it to +a prompt, potentially exposed by an MCP server? And how do you share it across +team mates? It has become good practice to check this into the repo, but +honestly: Most `AGENTS.md` files are snowflakes: ad-hoc, project-specific, unstructured. They mix process instructions with coding conventions and documentation fragments in a single flat file. Rule files and skills improve reusability but still lack a -coherent taxonomy. +coherent taxonomy. The ETH Zurich study on agentfiles confirmed what +practitioners already knew: LLM-generated ones hurt performance, bloated ones +waste instruction budget, and codebase overviews add nothing — agents discover +repository structure on their own. ADE brings structure to this space. By separating the three layers explicitly and binding each to a specific artifact type, it makes information easier to find, @@ -205,6 +214,43 @@ discoverability — you get a consistent experience regardless of your agent. The CLI supports a growing list of agents. See the [harness writers source](packages/harnesses/src/writers) for the current set. +## Where ADE fits in harness engineering + +A coding agent's harness has many configuration levers. ADE addresses the +**information levers** — the ones that determine _what the agent knows_: + +| Harness lever | ADE layer | Artifact | +| ---------------------------- | ----------------- | --------------------------------- | +| System prompt / agentfile | **Process** | `AGENTS.md` | +| Skills / instruction modules | **Conventions** | Skills (architecture + practices) | +| Reference knowledge | **Documentation** | Text files, read on demand | + +Practitioners have identified additional **runtime levers** that complement the +information architecture: + +- **Sub-agents** — context firewalls that encapsulate discrete tasks in isolated + context windows, preventing intermediate noise from accumulating in the parent + thread. This keeps the orchestrating agent in the "smart zone" and enables + coherent work across many sessions. + +- **Hooks** — user-defined scripts triggered at lifecycle events (tool calls, + stop events) that add deterministic control flow: auto-approving or denying + dangerous commands, surfacing build errors before the agent finishes, or + notifying the team on completion. + +- **Back-pressure** — verification mechanisms (typechecks, tests, coverage + gates) that let the agent check its own work. The likelihood of successfully + solving a problem with a coding agent is strongly correlated with the agent's + ability to verify its output. Context-efficient verification — where success is + silent and only failures surface — keeps the context window clean. + +ADE focuses on the information side because that is where most teams struggle +first: without a coherent taxonomy, every project re-invents its agentfile from +scratch. The runtime levers are powerful complements — and ADE's process layer +can reference them (e.g. _"delegate research to a sub-agent"_, _"verify with the +build hook before committing"_) — but they are orthogonal to the information +architecture itself. + ## Core principles **Shared context over personal configuration.** @@ -227,9 +273,19 @@ stack. What transfers across projects is the structure itself. ## Customization -All artifacts, that are produced by the CLI, are by default adaptable: You can provide -own workflows, your own skills, your own docs. It should work out of the box. -If this is still too opinionated for you, you can swap out each layer. +All artifacts produced by the CLI are adaptable: you can provide your own +workflows, your own skills, your own docs. It should work out of the box. If +this is still too opinionated for you, you can swap out each layer. + +Bias towards shipping. Start simple and add configuration only when the agent +actually fails — then engineer a solution so it does not fail that way again. +The goal is not the ideal harness; it is shipping high-quality code faster. + +After all: there is no secret ingredient. It is only about getting relevant +information into the conversation context. + +## Further reading -After all: there is no secret ingredient, it only about getting relevant information -into the conversation context. +- [Skill Issue: Harness Engineering for Coding Agents](https://www.humanlayer.dev/blog/skill-issue-harness-engineering-for-coding-agents) + — HumanLayer's practical guide to harness engineering, covering skills, + sub-agents, hooks, and back-pressure mechanisms. From a84fd71b38d441540159dd925538134912401e82 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 21:18:50 +0000 Subject: [PATCH 58/60] docs: update tagline to reference harness engineering https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c7eb99d..62dfb25 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ADE — Agentic Development Environment -> A technology-agnostic information architecture for coding agents that enables -> consistent, professional-grade agentic engineering at team scale. +> A structured information architecture for harness engineering — organizing what +> coding agents know into composable, team-shared layers. ## The alignment problem From 5d423d5c0d708af5734875fc58d21d7448de36c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 21:52:39 +0000 Subject: [PATCH 59/60] feat: add extending-catalog skill for ADE contributors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Teaches Claude how to add new options, practices, facets, and provision writers to the ADE catalog — following the exact code paths and conventions used in the project. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- .claude/skills/extending-catalog/SKILL.md | 126 ++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 .claude/skills/extending-catalog/SKILL.md diff --git a/.claude/skills/extending-catalog/SKILL.md b/.claude/skills/extending-catalog/SKILL.md new file mode 100644 index 0000000..d64b346 --- /dev/null +++ b/.claude/skills/extending-catalog/SKILL.md @@ -0,0 +1,126 @@ +--- +name: extending-catalog +description: Extends the ADE catalog with new facets, options, or practices. Use when adding a new architecture (e.g. Next.js), a new practice (e.g. Trunk-Based Development), or a new facet to the catalog. +--- + +# Extending the ADE catalog + +The catalog lives in `packages/core/src/catalog/`. Each facet is a separate file under `facets/`. + +## Adding an option to an existing facet + +Single-file change. Edit the facet file and append an `Option` to the `options` array. + +**Architecture option** — `packages/core/src/catalog/facets/architecture.ts`: + +```ts +{ + id: "nextjs", + label: "Next.js", + description: "Full-stack conventions for Next.js App Router", + recipe: [ + { + writer: "skills", + config: { + skills: [ + { + name: "nextjs-architecture", + description: "Architecture conventions for Next.js applications", + body: "# Next.js Architecture\n\n## Routing\n- Use App Router..." + } + ] + } + } + ], + docsets: [ + { + id: "nextjs-docs", + label: "Next.js", + origin: "https://github.com/vercel/next.js.git", + description: "Next.js framework documentation" + } + ] +} +``` + +**Practice option** — `packages/core/src/catalog/facets/practices.ts`: + +Same shape, but `docsets` is optional. Practices must work independently of architecture choices. + +### Option checklist + +- `id`: kebab-case, unique within the facet +- `recipe`: array of provisions — each uses an existing writer (`skills`, `workflows`, `instruction`, `mcp-server`, `knowledge`, `installable`) +- `docsets`: optional, only add git repos that contain genuinely useful reference docs +- Inline skill `body`: keep concise — Claude already knows common frameworks +- For third-party skills, use `ExternalSkill` format: `{ name: "x", source: "org/repo/skills/x" }` + +## Adding a new facet + +Three touches. + +**1. Create the facet file** — e.g. `packages/core/src/catalog/facets/runtime.ts`: + +```ts +import type { Facet } from "../../types.js"; + +export const runtimeFacet: Facet = { + id: "runtime", + label: "Runtime", + description: "Runtime configuration for agent execution", + required: false, + multiSelect: true, + options: [ + // options here + ] +}; +``` + +**2. Register it** — `packages/core/src/catalog/index.ts`: + +```ts +import { runtimeFacet } from "./facets/runtime.js"; + +export function getDefaultCatalog(): Catalog { + return { + facets: [processFacet, architectureFacet, practicesFacet, runtimeFacet] + }; +} +``` + +**3. Done** — the CLI setup command iterates `catalog.facets`, so a new facet gets its own selection step automatically. + +### Facet checklist + +- `id`: kebab-case, globally unique +- `required`: true only if every project must choose an option +- `multiSelect`: true when options are composable (like practices), false when mutually exclusive (like architecture) +- `dependsOn`: optional array of facet IDs that must be resolved first + +## Adding a new provision writer + +Only needed when existing writers (`workflows`, `skills`, `knowledge`, `mcp-server`, `instruction`, `installable`) cannot express the output. + +1. Add the writer name to the `ProvisionWriter` union in `packages/core/src/types.ts` +2. Implement `ProvisionWriterDef` — a `write(config, context)` returning `Partial` +3. Register it in the `WriterRegistry` +4. If the output doesn't fit existing `LogicalConfig` fields, extend the interface and update merge logic in `packages/core/src/resolver.ts` +5. Update every harness writer in `packages/harnesses/src/writers/` to emit the new config field + +This is the most expensive extension — avoid it unless strictly necessary. + +## Key types + +All in `packages/core/src/types.ts`: + +- `Catalog` → `{ facets: Facet[] }` +- `Facet` → `{ id, label, description, required, multiSelect?, dependsOn?, options }` +- `Option` → `{ id, label, description, recipe, docsets? }` +- `Provision` → `{ writer: ProvisionWriter, config }` +- `ProvisionWriter` → `"workflows" | "skills" | "knowledge" | "mcp-server" | "instruction" | "installable"` + +## Resolution flow + +`UserConfig.choices` → `resolve()` iterates facets → matches options → runs each provision's writer → merges into `LogicalConfig` → harness writers emit agent-specific files. + +See `packages/core/src/resolver.ts` for the full implementation. From 8617db209310c76f5d0bc2f52c1a11e38b63af56 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 21:56:14 +0000 Subject: [PATCH 60/60] refactor: make extending-catalog skill more concise Remove code samples and type summaries that can be read from source. Keep only conceptual guidance and file locations. https://claude.ai/code/session_01GCqwdfAMznLZZ97tYfJXRa --- .claude/skills/extending-catalog/SKILL.md | 129 ++++------------------ 1 file changed, 22 insertions(+), 107 deletions(-) diff --git a/.claude/skills/extending-catalog/SKILL.md b/.claude/skills/extending-catalog/SKILL.md index d64b346..31b2bfc 100644 --- a/.claude/skills/extending-catalog/SKILL.md +++ b/.claude/skills/extending-catalog/SKILL.md @@ -5,122 +5,37 @@ description: Extends the ADE catalog with new facets, options, or practices. Use # Extending the ADE catalog -The catalog lives in `packages/core/src/catalog/`. Each facet is a separate file under `facets/`. +Catalog: `packages/core/src/catalog/`. Facets: one file each under `facets/`. Types: `packages/core/src/types.ts`. -## Adding an option to an existing facet +## Adding an option -Single-file change. Edit the facet file and append an `Option` to the `options` array. +Single-file change. Append an `Option` to the facet's `options` array. Read the existing facet file for the shape — follow the pattern already there. -**Architecture option** — `packages/core/src/catalog/facets/architecture.ts`: +- Architecture options go in `facets/architecture.ts` (single-select) +- Practice options go in `facets/practices.ts` (multi-select, stack-independent) +- Each option's `recipe` uses existing writers: `skills`, `workflows`, `instruction`, `mcp-server`, `knowledge`, `installable` +- Inline skill bodies should be concise — only add context Claude doesn't already have +- For third-party skills use `ExternalSkill`: `{ name, source }` instead of `{ name, description, body }` +- `docsets` are optional — only add repos with genuinely useful reference docs -```ts -{ - id: "nextjs", - label: "Next.js", - description: "Full-stack conventions for Next.js App Router", - recipe: [ - { - writer: "skills", - config: { - skills: [ - { - name: "nextjs-architecture", - description: "Architecture conventions for Next.js applications", - body: "# Next.js Architecture\n\n## Routing\n- Use App Router..." - } - ] - } - } - ], - docsets: [ - { - id: "nextjs-docs", - label: "Next.js", - origin: "https://github.com/vercel/next.js.git", - description: "Next.js framework documentation" - } - ] -} -``` +## Adding a facet -**Practice option** — `packages/core/src/catalog/facets/practices.ts`: +1. Create `facets/.ts` — export a `Facet` object. Read an existing facet for the shape +2. Register it in `packages/core/src/catalog/index.ts` — add to the `facets` array +3. The CLI auto-discovers facets from the array — no UI changes needed -Same shape, but `docsets` is optional. Practices must work independently of architecture choices. +Key decisions: `required` (must every project choose?), `multiSelect` (composable or mutually exclusive?), `dependsOn` (resolved after which facets?). -### Option checklist +## Adding a provision writer -- `id`: kebab-case, unique within the facet -- `recipe`: array of provisions — each uses an existing writer (`skills`, `workflows`, `instruction`, `mcp-server`, `knowledge`, `installable`) -- `docsets`: optional, only add git repos that contain genuinely useful reference docs -- Inline skill `body`: keep concise — Claude already knows common frameworks -- For third-party skills, use `ExternalSkill` format: `{ name: "x", source: "org/repo/skills/x" }` +Expensive — touches types, registry, resolver, and every harness writer. Avoid unless existing writers cannot express the output. -## Adding a new facet - -Three touches. - -**1. Create the facet file** — e.g. `packages/core/src/catalog/facets/runtime.ts`: - -```ts -import type { Facet } from "../../types.js"; - -export const runtimeFacet: Facet = { - id: "runtime", - label: "Runtime", - description: "Runtime configuration for agent execution", - required: false, - multiSelect: true, - options: [ - // options here - ] -}; -``` - -**2. Register it** — `packages/core/src/catalog/index.ts`: - -```ts -import { runtimeFacet } from "./facets/runtime.js"; - -export function getDefaultCatalog(): Catalog { - return { - facets: [processFacet, architectureFacet, practicesFacet, runtimeFacet] - }; -} -``` - -**3. Done** — the CLI setup command iterates `catalog.facets`, so a new facet gets its own selection step automatically. - -### Facet checklist - -- `id`: kebab-case, globally unique -- `required`: true only if every project must choose an option -- `multiSelect`: true when options are composable (like practices), false when mutually exclusive (like architecture) -- `dependsOn`: optional array of facet IDs that must be resolved first - -## Adding a new provision writer - -Only needed when existing writers (`workflows`, `skills`, `knowledge`, `mcp-server`, `instruction`, `installable`) cannot express the output. - -1. Add the writer name to the `ProvisionWriter` union in `packages/core/src/types.ts` -2. Implement `ProvisionWriterDef` — a `write(config, context)` returning `Partial` -3. Register it in the `WriterRegistry` -4. If the output doesn't fit existing `LogicalConfig` fields, extend the interface and update merge logic in `packages/core/src/resolver.ts` -5. Update every harness writer in `packages/harnesses/src/writers/` to emit the new config field - -This is the most expensive extension — avoid it unless strictly necessary. - -## Key types - -All in `packages/core/src/types.ts`: - -- `Catalog` → `{ facets: Facet[] }` -- `Facet` → `{ id, label, description, required, multiSelect?, dependsOn?, options }` -- `Option` → `{ id, label, description, recipe, docsets? }` -- `Provision` → `{ writer: ProvisionWriter, config }` -- `ProvisionWriter` → `"workflows" | "skills" | "knowledge" | "mcp-server" | "instruction" | "installable"` +1. Extend `ProvisionWriter` union in `types.ts` +2. Implement `ProvisionWriterDef` with `write(config, context) → Partial` +3. Register in `WriterRegistry` +4. If needed, extend `LogicalConfig` and update merge logic in `resolver.ts` +5. Update every harness writer in `packages/harnesses/src/writers/` ## Resolution flow -`UserConfig.choices` → `resolve()` iterates facets → matches options → runs each provision's writer → merges into `LogicalConfig` → harness writers emit agent-specific files. - -See `packages/core/src/resolver.ts` for the full implementation. +`UserConfig.choices` → `resolve()` iterates facets → matches options → runs each provision's writer → merges into `LogicalConfig` → harness writers emit agent-specific files. See `packages/core/src/resolver.ts`.