diff --git a/.gitignore b/.gitignore index c9ef675..0b420f6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ artifacts .codeflow-store .codeflow-store-test .codeflow-sandboxes +.test-store *.log *.tsbuildinfo .env.local diff --git a/docs/PACKAGE_DECOMPOSITION.md b/docs/PACKAGE_DECOMPOSITION.md index 8353079..fbe3b68 100644 --- a/docs/PACKAGE_DECOMPOSITION.md +++ b/docs/PACKAGE_DECOMPOSITION.md @@ -12,6 +12,21 @@ coderag → @abhinav2203/codeflow-core ``` +### `codeflow-core` Source Files (not yet isolated — extracted from `src/lib/blueprint/`) + +``` +src/lib/blueprint/ + - schema.ts ← BlueprintGraph, BlueprintNode, BlueprintEdge, all type definitions + - repo.ts + - repo.test.ts + - utils.ts ← slugify, createNodeId, mergeFields, dedupeEdges, toPosixPath, etc. + - store-paths.ts ← getStoreRoot, sessionDirForProject, latestSessionPath, etc. + - export.ts + - export.test.ts +``` + +> **Note:** `codeflow-core` is the foundation. `schema.ts` defines the entire type system used by every other package. `repo.ts` uses ts-morph for TypeScript repo analysis. `export.ts` handles blueprint artifact export. These are extracted first before any other package work begins. + ## Proposed New Packages | Package | Description | @@ -230,7 +245,11 @@ src/lib/server/terminal-sessions.ts → move to @abhinav2203/codeflow-core "./checkpoint": { "types": "./dist/checkpoint.d.ts", "default": "./dist/checkpoint.js" }, "./approval": { "types": "./dist/approval.d.ts", "default": "./dist/approval.js" }, "./run": { "types": "./dist/run.d.ts", "default": "./dist/run.js" }, - "./risk": { "types": "./dist/risk.d.ts", "default": "./dist/risk.js" } + "./risk": { "types": "./dist/risk.d.ts", "default": "./dist/risk.js" }, + "./observability": { "types": "./dist/observability.d.ts", "default": "./dist/observability.js" }, + "./branch": { "types": "./dist/branch.d.ts", "default": "./dist/branch.js" }, + "./session": { "types": "./dist/session.d.ts", "default": "./dist/session.js" }, + "./store": { "types": "./dist/store.d.ts", "default": "./dist/store.js" } }, "bin": { "codeflow-store": "./dist/bin/cli.js" @@ -357,6 +376,7 @@ FROM: src/lib/blueprint/ - build.ts - build.test.ts - file-tree.ts (used by build for file scanning) + - typescript-workspace.ts (used by build.ts for reverse-mode ts-morph analysis) ``` **API routes to wire:** @@ -365,6 +385,7 @@ FROM: src/lib/blueprint/ FROM: src/app/api/blueprint/route.ts FROM: src/app/api/blueprint/route.test.ts FROM: src/app/api/generate-blueprint/route.ts +FROM: src/app/api/generate-blueprint/route.test.ts ``` **`package.json` fields:** @@ -374,7 +395,8 @@ FROM: src/app/api/generate-blueprint/route.ts "name": "@abhinav2203/codeflow-prd", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, - "./build": { "types": "./dist/build.d.ts", "default": "./dist/build.js" } + "./build": { "types": "./dist/build.d.ts", "default": "./dist/build.js" }, + "./typescript-workspace": { "types": "./dist/typescript-workspace.d.ts", "default": "./dist/typescript-workspace.js" } }, "bin": { "codeflow-prd": "./dist/bin/cli.js" @@ -387,7 +409,7 @@ FROM: src/app/api/generate-blueprint/route.ts ``` **Developer prompt:** -> "Extract the PRD ingestion layer. Move `src/lib/blueprint/{prd,prd.test,build,build.test,file-tree}.ts` and `src/app/api/blueprint/route.ts`, `src/app/api/generate-blueprint/route.ts` into `packages/codeflow-prd/src/`. The PRD parser extracts screens, APIs, classes, functions, modules, and workflows (with `->` syntax) from markdown. The build step turns parsed PRD into a BlueprintGraph. Publish as `@abhinav2203/codeflow-prd`." +> "Extract the PRD ingestion layer. Move `src/lib/blueprint/{prd,prd.test,build,build.test,file-tree,typescript-workspace}.ts` and `src/app/api/blueprint/route.ts`, `src/app/api/generate-blueprint/route.ts` into `packages/codeflow-prd/src/`. The PRD parser extracts screens, APIs, classes, functions, modules, and workflows (with `->` syntax) from markdown. The build step turns parsed PRD into a BlueprintGraph. Publish as `@abhinav2203/codeflow-prd`." --- @@ -409,6 +431,8 @@ FROM: src/lib/blueprint/ - metrics.test.ts - refactor.ts - refactor.test.ts + - conflicts.ts ← detectGraphConflicts: repo vs blueprint conflict analysis (imports analyzeTypeScriptRepo from repo.ts in codeflow-core) + - conflicts.test.ts ``` **API routes to wire:** @@ -452,7 +476,7 @@ FROM: src/app/api/conflicts/route.test.ts ``` **Developer prompt:** -> "Extract the graph analysis layer. Move all `src/lib/blueprint/{cycles,smells,metrics,refactor}*.ts` files and their corresponding `src/app/api/analysis/{cycles,metrics,smells}/route.ts`, `src/app/api/refactor/{detect,heal}/route.ts`, and `src/app/api/conflicts/route.ts` (with all tests) into `packages/codeflow-analysis/src/`. Each sub-module exposes a focused analysis function. Publish as `@abhinav2203/codeflow-analysis`." +> "Extract the graph analysis layer. Move all `src/lib/blueprint/{cycles,smells,metrics,refactor,conflicts}*.ts` files and their corresponding `src/app/api/analysis/{cycles,metrics,smells}/route.ts`, `src/app/api/refactor/{detect,heal}/route.ts`, and `src/app/api/conflicts/route.ts` (with all tests) into `packages/codeflow-analysis/src/`. Each sub-module exposes a focused analysis function. Publish as `@abhinav2203/codeflow-analysis`." --- @@ -477,6 +501,8 @@ FROM: src/lib/blueprint/ FROM: src/app/api/generate-blueprint/route.ts (NVIDIA AI generation endpoint) ``` +> **Note:** `generate-blueprint` is a **single shared route** that dispatches to either PRD build (reverse mode) or AI generation (nvidia.ts) based on request parameters. Both `codeflow-prd` and `codeflow-ai` contribute to this route's implementation. + **`package.json` fields:** ```json @@ -519,13 +545,15 @@ FROM: src/lib/blueprint/ - phases.test.ts - execute.ts - execute.test.ts - - vcr.ts + - vcr.ts ← VCR recording/replay of trace spans - vcr.test.ts - runtime-contracts.ts - runtime-tests.ts - runtime-tests.test.ts - runtime-workspace.ts - sandbox.ts + - mermaid.ts ← toMermaid / toMermaidClassDiagram (used by export/mermaid API route) + - mermaid.test.ts ``` **API routes to wire:** @@ -552,7 +580,8 @@ FROM: src/app/api/code-completions/route.ts "./execute": { "types": "./dist/execute.d.ts", "default": "./dist/execute.js" }, "./vcr": { "types": "./dist/vcr.d.ts", "default": "./dist/vcr.js" }, "./runtime-tests": { "types": "./dist/runtime-tests.d.ts", "default": "./dist/runtime-tests.js" }, - "./mermaid": { "types": "./dist/mermaid.d.ts", "default": "./dist/mermaid.js" } + "./mermaid": { "types": "./dist/mermaid.d.ts", "default": "./dist/mermaid.js" }, + "./sandbox": { "types": "./dist/sandbox.d.ts", "default": "./dist/sandbox.js" } }, "bin": { "codeflow-execution": "./dist/bin/cli.js" @@ -566,7 +595,7 @@ FROM: src/app/api/code-completions/route.ts ``` **Developer prompt:** -> "Extract the execution engine. Move `src/lib/blueprint/{runner,plan,phases,execute,vcr,runtime-contracts,runtime-tests,runtime-workspace,sandbox}*.ts` (all files with these prefixes, with tests) and `src/app/api/executions/run/route.ts`, `src/app/api/vcr/route.ts`, `src/app/api/export/mermaid/route.ts`, `src/app/api/code-completions/route.ts` into `packages/codeflow-execution/src/`. The runner orchestrates task plans with phases. VCR records trace spans for replay. Publish as `@abhinav2203/codeflow-execution`." +> "Extract the execution engine. Move `src/lib/blueprint/{runner,plan,phases,execute,vcr,runtime-contracts,runtime-tests,runtime-workspace,sandbox,mermaid}*.ts` (all files with these prefixes, with tests) and `src/app/api/executions/run/route.ts`, `src/app/api/vcr/route.ts`, `src/app/api/export/mermaid/route.ts`, `src/app/api/code-completions/route.ts` into `packages/codeflow-execution/src/`. The runner orchestrates task plans with phases. VCR records trace spans for replay. Mermaid exports generate diagrams from blueprints. Publish as `@abhinav2203/codeflow-execution`." --- @@ -590,6 +619,7 @@ FROM: src/lib/opencode/ - config.test.ts - modelFetcher.ts - modelFetcher.test.ts + - types.ts ← OpencodeProvider, OpencodeConfig, McpServerConfig types - api-key-validator.tsx FROM: src/lib/server/ @@ -703,6 +733,8 @@ FROM: src/lib/blueprint/ FROM: src/app/api/ghost-nodes/route.ts ``` +> **Note:** `heatmap.ts` is shared between `codeflow-evolution` and `codeflow-canvas`. Both packages copy this file (it's not a separate package). The heatmap CLI in `codeflow-evolution` and the heatmap overlay in `codeflow-canvas` both use this same file. + **API routes to wire:** ```text @@ -782,6 +814,8 @@ FROM: src/app/api/observability/ingest/route.ts (trace overlay data) FROM: src/app/api/observability/latest/route.ts ``` +> **Note:** Observability data storage routes (`observability/ingest`, `observability/latest`) persist to `codeflow-store`. The `observability.ts` lib file (display/compute logic) lives in `codeflow-canvas` alongside traces and heatmap for graph overlay rendering. + **`package.json` fields:** ```json @@ -793,7 +827,8 @@ FROM: src/app/api/observability/latest/route.ts "./edit": { "types": "./dist/edit.d.ts", "default": "./dist/edit.js" }, "./traces": { "types": "./dist/traces.d.ts", "default": "./dist/traces.js" }, "./editor": { "types": "./dist/editor.d.ts", "default": "./dist/editor.js" }, - "./heatmap": { "types": "./dist/heatmap.d.ts", "default": "./dist/heatmap.js" } + "./heatmap": { "types": "./dist/heatmap.d.ts", "default": "./dist/heatmap.js" }, + "./observability": { "types": "./dist/observability.d.ts", "default": "./dist/observability.js" } }, "bin": { "codeflow-canvas": "./dist/bin/cli.js" @@ -865,7 +900,22 @@ FROM: src/app/api/digital-twin/simulate/route.test.ts --- -## Shared / Copy-Once Utilities +## Unaccounted API Routes + +The following API routes exist in `src/app/api/` but are NOT assigned to any package in this decomposition. They may belong to an existing package, a future package, or may need to be reassigned: + +| Route | Likely Owner | Notes | +|-------|-------------|-------| +| `src/app/api/coderag/route.ts` | `coderag` (existing) | RAG embedding + retrieval | +| `src/app/api/files/get/route.ts` | TBD | File retrieval | +| `src/app/api/files/list/route.ts` | TBD | File listing | +| `src/app/api/files/post/route.ts` | TBD | File upload | +| `src/app/api/terminal/sessions/route.ts` | TBD | Terminal session management | +| `src/app/api/terminal/sessions/[sessionId]/route.ts` | TBD | Individual terminal session | + +> **Action needed:** Assign these routes to appropriate packages before extraction begins. `coderag` is an existing package and should take its own route. The file and terminal routes may belong to `codeflow-store` or a new `codeflow-fs` package. + +--- These files are used by multiple packages. They should be moved to `@abhinav2203/codeflow-core` (or a dedicated utility package) to avoid code duplication and logic drift. Each consuming package should import them as a normal package dependency rather than copying the source: @@ -895,11 +945,11 @@ codeflow-versioning: src/app/api/branches/{route,[id]/route,diff/route}.ts codeflow-prd: - src/lib/blueprint/{prd,prd.test,build,build.test,file-tree}.ts + src/lib/blueprint/{prd,prd.test,build,build.test,file-tree,typescript-workspace}.ts src/app/api/{blueprint,generate-blueprint}/route.ts codeflow-analysis: - src/lib/blueprint/{cycles,smells,metrics,refactor}*.ts + src/lib/blueprint/{cycles,smells,metrics,refactor,conflicts}*.ts src/app/api/analysis/{cycles,metrics,smells}/route.ts src/app/api/{refactor/{detect,heal},conflicts}/route.ts @@ -908,7 +958,7 @@ codeflow-ai: src/app/api/generate-blueprint/route.ts codeflow-execution: - src/lib/blueprint/{runner,plan,phases,execute,vcr,runtime-contracts,runtime-tests,runtime-workspace,sandbox}*.ts + src/lib/blueprint/{runner,plan,phases,execute,vcr,runtime-contracts,runtime-tests,runtime-workspace,sandbox,mermaid}*.ts src/app/api/{executions/run,vcr,export/mermaid,code-completions}/route.ts codeflow-opencode: diff --git a/docs/superpowers/plans/2026-04-21-codeflow-mcp-decomposition.md b/docs/superpowers/plans/2026-04-21-codeflow-mcp-decomposition.md new file mode 100644 index 0000000..d50fccb --- /dev/null +++ b/docs/superpowers/plans/2026-04-21-codeflow-mcp-decomposition.md @@ -0,0 +1,281 @@ +# codeflow-mcp Package Decomposition Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Extract `src/lib/blueprint/mcp.ts`, its test, and the two MCP API routes into a standalone npm package `@abhinav2203/codeflow-mcp` that works in isolation — no monorepo, no Next.js app required. + +**Architecture:** The package exposes an MCP client library (`listMcpTools`, `invokeMcpTool`) and an MCP server that wraps CodeFlow blueprint operations. API routes in the Next.js app are replaced with thin re-exports from the package. During development, packages use `workspace:*` ranges; once published to npm, these resolve to published semver. + +**Tech Stack:** TypeScript, Node.js, `zod`, `vitest`, MCP JSON-RPC protocol + +--- + +## Step 0 — Scaffold Package Skeleton + +- [ ] **Step 0.1: Create directory structure** + +```bash +mkdir -p packages/codeflow-mcp/src/{bin,invoke,tools} +mkdir -p packages/codeflow-mcp/test-fixtures +``` + +- [ ] **Step 0.2: Create `packages/codeflow-mcp/package.json`** + +```json +{ + "name": "@abhinav2203/codeflow-mcp", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, + "./invoke": { "types": "./dist/invoke.d.ts", "default": "./dist/invoke.js" }, + "./tools": { "types": "./dist/tools.d.ts", "default": "./dist/tools.js" } + }, + "bin": { + "codeflow-mcp": "./dist/bin/cli.js" + }, + "scripts": { + "check": "tsc --noEmit", + "test": "vitest run", + "build": "tsc && node scripts/wrap-cli.mjs", + "clean": "rm -rf dist" + }, + "dependencies": { + "@abhinav2203/codeflow-core": "workspace:*", + "zod": "^3.0.0" + }, + "devDependencies": { + "typescript": "^5.0.0", + "vitest": "^1.0.0" + } +} +``` + +- [ ] **Step 0.3: Create `packages/codeflow-mcp/tsconfig.json`** + +```json +{ + "extends": '../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src"] +} +``` + +- [ ] **Step 0.4: Create `packages/codeflow-mcp/vitest.config.ts`** + +```typescript +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"] + } +}); +``` + +- [ ] **Step 0.5: Create `scripts/wrap-cli.mjs`** (wraps the TS compile step for the CLI bin — the bin entry must be a .js file) + +```javascript +import { writeFileSync } from "fs"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const srcDir = join(__dirname, "../dist"); +const distBin = join(srcDir, "bin"); + +// The CLI is a thin wrapper that loads the compiled module +writeFileSync(join(distBin, "cli.js"), `#!/usr/bin/env node +import { main } from "../invoke/index.js"; +main(); +`); +``` + +- [ ] **Step 0.6: Run `npm install` in the package** + +Run: `cd packages/codeflow-mcp && npm install` +Expected: Dependencies resolved without errors + +--- + +## Step 1 — Move core `mcp.ts` logic + +- [ ] **Step 1.1: Create `packages/codeflow-mcp/src/index.ts`** — copy `src/lib/blueprint/mcp.ts` content, but: + - Remove `@/lib/blueprint/schema` import — import `McpTool`, `McpToolResult` types from `@abhinav2203/codeflow-core` + - Keep all functions: `sendJsonRpc`, `listMcpTools`, `invokeMcpTool`, `extractTextFromMcpResult` + +- [ ] **Step 1.2: Run check** + +Run: `cd packages/codeflow-mcp && npm run check` +Expected: No TypeScript errors (types resolve via workspace `codeflow-core`) + +- [ ] **Step 1.3: Run tests** + +Run: `cd packages/codeflow-mcp && npm run test` +Expected: All tests pass + +- [ ] **Step 1.4: Commit** + +```bash +cd packages/codeflow-mcp +git add src/index.ts package.json tsconfig.json vitest.config.ts scripts/ +git commit -m "feat(mcp): move core MCP client library to package" +``` + +--- + +## Step 2 — Move API routes as package exports + +- [ ] **Step 2.1: Create `packages/codeflow-mcp/src/invoke.ts`** — copy `src/app/api/mcp/invoke/route.ts` + - Change import `from "@/lib/blueprint/mcp"` → `from "@abhinav2203/codeflow-mcp"` + - Change import `from "next/server"` → `from "next"; import type { NextResponse } from "next"` + - Keep all SSRF validation, header filtering, error handling + +- [ ] **Step 2.2: Create `packages/codeflow-mcp/src/invoke.test.ts`** — copy `src/app/api/mcp/invoke/route.test.ts` + - Change import `from "@/app/api/mcp/invoke/route"` → `from "./invoke"` + +- [ ] **Step 2.3: Create `packages/codeflow-mcp/src/tools.ts`** — copy `src/app/api/mcp/tools/route.ts` + - Change import `from "@/lib/blueprint/mcp"` → `from "@abhinav2203/codeflow-mcp"` + - Change import `from "next/server"` → `from "next"; import type { NextResponse } from "next"` + +- [ ] **Step 2.4: Create `packages/codeflow-mcp/src/tools.test.ts`** — copy `src/app/api/mcp/tools/route.test.ts` + - Change import `from "@/app/api/mcp/tools/route"` → `from "./tools"` + +- [ ] **Step 2.5: Create `packages/codeflow-mcp/src/index.test.ts`** — copy `src/lib/blueprint/mcp.test.ts` + - Change import `from "@/lib/blueprint/mcp"` → `from "./index"` + - Change import `from "@/lib/blueprint/schema"` → `from "@abhinav2203/codeflow-core"` + +- [ ] **Step 2.6: Run check and tests** + +Run: `cd packages/codeflow-mcp && npm run check && npm run test` +Expected: Both pass + +- [ ] **Step 2.7: Commit** + +```bash +cd packages/codeflow-mcp +git add src/invoke.ts src/invoke.test.ts src/tools.ts src/tools.test.ts src/index.test.ts +git commit -m "feat(mcp): move API routes as package sub-exports" +``` + +--- + +## Step 3 — Wire Next.js app to import from package + +- [ ] **Step 3.1: Replace `src/app/api/mcp/invoke/route.ts`** with: + +```typescript +// Re-export from package — implementation lives in the package now +export { POST as invokeRoute } from "@abhinav2203/codeflow-mcp/invoke"; +``` + +- [ ] **Step 3.2: Replace `src/app/api/mcp/tools/route.ts`** with: + +```typescript +export { POST as toolsRoute } from "@abhinav2203/codeflow-mcp/tools"; +``` + +- [ ] **Step 3.3: Update test files** — update the test imports in both route.test.ts files so they still work with the Next.js app. + +For `src/app/api/mcp/invoke/route.test.ts`, the test imports `POST` from the route. Since we've replaced the route with a re-export, we need to make sure the route still exports `POST`: + +```typescript +// In src/app/api/mcp/invoke/route.ts — replace with: +import { POST } from "@abhinav2203/codeflow-mcp/invoke"; +export { POST }; +``` + +```typescript +// In src/app/api/mcp/tools/route.ts — replace with: +import { POST } from "@abhinav2203/codeflow-mcp/tools"; +export { POST }; +``` + +- [ ] **Step 3.4: Run full app type check** + +Run: `cd /Users/abhinavnehra/git/CodeFlow && npm run check` +Expected: No TypeScript errors + +- [ ] **Step 3.5: Commit** + +```bash +cd /Users/abhinavnehra/git/CodeFlow +git add src/app/api/mcp/invoke/route.ts src/app/api/mcp/tools/route.ts +git commit -m "feat(mcp): wire API routes to import from @abhinav2203/codeflow-mcp" +``` + +--- + +## Step 4 — Add CLI bin and test-fixtures for isolation testing + +- [ ] **Step 4.1: Create `packages/codeflow-mcp/src/bin/cli.ts`** + +```typescript +#!/usr/bin/env node +import { listMcpTools, invokeMcpTool } from "../index.js"; + +const [cmd, ...args] = process.argv.slice(2); + +if (cmd === "tool" && args[0] === "list") { + const serverUrl = args[1] ?? "http://localhost:3001/mcp"; + const tools = await listMcpTools(serverUrl); + console.json({ tools }); +} else if (cmd === "tool" && args[0] === "invoke") { + const toolName = args[1]; + const serverUrl = args[2] ?? "http://localhost:3001/mcp"; + const rawArgs = args[3] ?? "{}"; + const result = await invokeMcpTool(serverUrl, toolName, JSON.parse(rawArgs)); + console.json({ result }); +} else { + console.log("Usage: codeflow-mcp tool list \n codeflow-mcp tool invoke "); +} +``` + +- [ ] **Step 4.2: Create `test-fixtures/minimal-blueprint.json`** + +A minimal BlueprintGraph JSON for CLI testing. + +- [ ] **Step 4.3: Run isolation test** + +Run: `cd packages/codeflow-mcp && npm run build && node dist/bin/cli.js tool list http://localhost:9999/mcp` +Expected: Returns 400 with connection error (server not running — this proves the CLI runs and makes the HTTP call) + +- [ ] **Step 4.4: Commit** + +```bash +cd packages/codeflow-mcp +git add src/bin/cli.ts test-fixtures/ +git commit -m "feat(mcp): add CLI bin for isolation testing" +``` + +--- + +## Step 5 — Final verification + +- [ ] **Step 5.1: Run all package checks** + +Run: `cd packages/codeflow-mcp && npm run check && npm run test && npm run build` +Expected: `tsc --noEmit` passes, `vitest run` passes, build produces `dist/` with all entry points + +- [ ] **Step 5.2: Verify app still works** + +Run: `cd /Users/abhinavnehra/git/CodeFlow && npm run check` +Expected: App type-checks with the rewired routes + +--- + +## Summary of all changes + +| File | Action | +|------|--------| +| `packages/codeflow-mcp/` | Created — all package source lives here | +| `src/lib/blueprint/mcp.ts` | Stays (used by workspace dependency) | +| `src/app/api/mcp/invoke/route.ts` | Replaced with re-export from package | +| `src/app/api/mcp/tools/route.ts` | Replaced with re-export from package | diff --git a/package-lock.json b/package-lock.json index 893cdd1..80e318d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codeflow", - "version": "0.1.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codeflow", - "version": "0.1.0", + "version": "1.0.0", "dependencies": { "@abhinav2203/coderag": "^0.2.1", "@monaco-editor/react": "^4.7.0", @@ -16,7 +16,6 @@ "cross-spawn": "^7.0.6", "framer-motion": "^12.38.0", "monaco-editor": "^0.55.1", - "next": "^16.1.6", "opencode-ai": "^1.3.13", "react": "^19.2.4", "react-dom": "^19.2.4", @@ -38,6 +37,7 @@ "eslint": "^9.39.4", "eslint-config-next": "^16.2.1", "jsdom": "^28.1.0", + "next": "^16.2.4", "tinyexec": "^1.0.2", "tsx": "^4.21.0", "vitest": "^4.1.0" @@ -585,6 +585,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1336,6 +1337,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -1349,6 +1351,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1371,6 +1374,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1393,6 +1397,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1409,6 +1414,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1425,6 +1431,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1441,6 +1448,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1457,6 +1465,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1473,6 +1482,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1489,6 +1499,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1505,6 +1516,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1521,6 +1533,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1537,6 +1550,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1553,6 +1567,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1575,6 +1590,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1597,6 +1613,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1619,6 +1636,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1641,6 +1659,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1663,6 +1682,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1685,6 +1705,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1707,6 +1728,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1729,6 +1751,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -1748,6 +1771,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1767,6 +1791,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1786,6 +1811,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2131,9 +2157,10 @@ } }, "node_modules/@next/env": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.1.tgz", - "integrity": "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.4.tgz", + "integrity": "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==", + "dev": true, "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -2147,12 +2174,13 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.1.tgz", - "integrity": "sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.4.tgz", + "integrity": "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2163,12 +2191,13 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.1.tgz", - "integrity": "sha512-/vrcE6iQSJq3uL3VGVHiXeaKbn8Es10DGTGRJnRZlkNQQk3kaNtAJg8Y6xuAlrx/6INKVjkfi5rY0iEXorZ6uA==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz", + "integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2179,12 +2208,13 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.1.tgz", - "integrity": "sha512-uLn+0BK+C31LTVbQ/QU+UaVrV0rRSJQ8RfniQAHPghDdgE+SlroYqcmFnO5iNjNfVWCyKZHYrs3Nl0mUzWxbBw==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz", + "integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2195,12 +2225,13 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.1.tgz", - "integrity": "sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz", + "integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2211,12 +2242,13 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.1.tgz", - "integrity": "sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz", + "integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2227,12 +2259,13 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.1.tgz", - "integrity": "sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz", + "integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2243,12 +2276,13 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.1.tgz", - "integrity": "sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz", + "integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2259,12 +2293,13 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.1.tgz", - "integrity": "sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz", + "integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2346,7 +2381,7 @@ "version": "1.59.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { @@ -3038,7 +3073,6 @@ "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -4239,6 +4273,7 @@ "version": "2.10.7", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.7.tgz", "integrity": "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw==", + "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -4448,6 +4483,7 @@ "version": "1.0.30001778", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -4530,6 +4566,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "dev": true, "license": "MIT" }, "node_modules/clsx": { @@ -5039,7 +5076,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -8018,6 +8055,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -8065,12 +8103,13 @@ } }, "node_modules/next": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.1.tgz", - "integrity": "sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.4.tgz", + "integrity": "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==", + "dev": true, "license": "MIT", "dependencies": { - "@next/env": "16.2.1", + "@next/env": "16.2.4", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -8084,14 +8123,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.1", - "@next/swc-darwin-x64": "16.2.1", - "@next/swc-linux-arm64-gnu": "16.2.1", - "@next/swc-linux-arm64-musl": "16.2.1", - "@next/swc-linux-x64-gnu": "16.2.1", - "@next/swc-linux-x64-musl": "16.2.1", - "@next/swc-win32-arm64-msvc": "16.2.1", - "@next/swc-win32-x64-msvc": "16.2.1", + "@next/swc-darwin-arm64": "16.2.4", + "@next/swc-darwin-x64": "16.2.4", + "@next/swc-linux-arm64-gnu": "16.2.4", + "@next/swc-linux-arm64-musl": "16.2.4", + "@next/swc-linux-x64-gnu": "16.2.4", + "@next/swc-linux-x64-musl": "16.2.4", + "@next/swc-win32-arm64-msvc": "16.2.4", + "@next/swc-win32-x64-msvc": "16.2.4", "sharp": "^0.34.5" }, "peerDependencies": { @@ -8630,6 +8669,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -8658,7 +8698,7 @@ "version": "1.59.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.59.1" @@ -8677,7 +8717,7 @@ "version": "1.59.1", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -8715,6 +8755,7 @@ "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -9259,7 +9300,7 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -9372,6 +9413,7 @@ "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "optional": true, @@ -9517,6 +9559,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -9751,6 +9794,7 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "dev": true, "license": "MIT", "dependencies": { "client-only": "0.0.1" diff --git a/package.json b/package.json index c9f859b..e0727b6 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "cross-spawn": "^7.0.6", "framer-motion": "^12.38.0", "monaco-editor": "^0.55.1", - "next": "^16.1.6", + "next": "^16.2.4", "opencode-ai": "^1.3.13", "react": "^19.2.4", "react-dom": "^19.2.4", diff --git a/packages/codeflow-mcp/dist/bin/cli.d.ts b/packages/codeflow-mcp/dist/bin/cli.d.ts new file mode 100644 index 0000000..6ae5e30 --- /dev/null +++ b/packages/codeflow-mcp/dist/bin/cli.d.ts @@ -0,0 +1,12 @@ +#!/usr/bin/env node +/** + * codeflow-mcp CLI + * + * Usage: + * codeflow-mcp stdio # Start MCP server over stdio (Claude Code, Cursor) + * codeflow-mcp server start # Start MCP server over HTTP+SSE + * codeflow-mcp tool list # Query tools from a remote MCP server + * codeflow-mcp tool invoke [args-json] + */ +export {}; +//# sourceMappingURL=cli.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/bin/cli.d.ts.map b/packages/codeflow-mcp/dist/bin/cli.d.ts.map new file mode 100644 index 0000000..72f8c90 --- /dev/null +++ b/packages/codeflow-mcp/dist/bin/cli.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/bin/cli.ts"],"names":[],"mappings":";AAEA;;;;;;;;GAQG"} \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/bin/cli.js b/packages/codeflow-mcp/dist/bin/cli.js new file mode 100644 index 0000000..72f9d96 --- /dev/null +++ b/packages/codeflow-mcp/dist/bin/cli.js @@ -0,0 +1,103 @@ +#!/usr/bin/env node +/** + * codeflow-mcp CLI + * + * Usage: + * codeflow-mcp stdio # Start MCP server over stdio (Claude Code, Cursor) + * codeflow-mcp server start # Start MCP server over HTTP+SSE + * codeflow-mcp tool list # Query tools from a remote MCP server + * codeflow-mcp tool invoke [args-json] + */ +import { startStdioServer, createHttpServer } from "../invoke/index.js"; +function jsonRpcError(id, code, message) { + return { jsonrpc: "2.0", id: id, error: { code, message } }; +} +function jsonRpcResult(id, result) { + return { jsonrpc: "2.0", id: id, result }; +} +// ─── Tool query client ──────────────────────────────────────────────────────── +async function queryRemote(serverUrl, request) { + const res = await fetch(serverUrl, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(request), + }); + return res.json(); +} +// ─── CLI dispatch ───────────────────────────────────────────────────────────── +const [cmd, subcmd, ...args] = process.argv.slice(2); +if (cmd === "stdio") { + // MCP over stdio — keeps process alive, reads/writes JSON-RPC lines + startStdioServer().catch((err) => { + console.error("[codeflow-mcp] stdio server error:", err); + process.exit(1); + }); +} +else if (cmd === "server" && subcmd === "start") { + // HTTP + SSE server + let port = 3100; + let host = "localhost"; + const remaining = args.slice(0); + for (let i = 0; i < remaining.length; i++) { + if (remaining[i] === "--port" && i + 1 < remaining.length) { + port = parseInt(remaining[i + 1], 10); + } + else if (remaining[i] === "--host" && i + 1 < remaining.length) { + host = remaining[i + 1]; + } + } + createHttpServer(port, host); +} +else if (cmd === "tool" && subcmd === "list") { + const serverUrl = args[0] ?? "http://localhost:3100"; + const data = await queryRemote(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "tools/list", + params: {}, + }); + if (data.error) { + console.error(`Error: ${data.error.message}`); + process.exit(1); + } + console.log(JSON.stringify(data.result, null, 2)); +} +else if (cmd === "tool" && subcmd === "invoke") { + const [toolName, serverUrl = "http://localhost:3100", argsJson = "{}"] = args; + const data = await queryRemote(serverUrl, { + jsonrpc: "2.0", + id: 2, + method: "tools/call", + params: { name: toolName, arguments: JSON.parse(argsJson) }, + }); + if (data.error) { + console.error(`Error: ${data.error.message}`); + process.exit(1); + } + console.log(JSON.stringify(data.result, null, 2)); +} +else { + console.log(`Usage: + codeflow-mcp stdio # Start MCP server over stdio (Claude Code, Cursor) + codeflow-mcp server start [--port 3100] [--host localhost] + codeflow-mcp tool list + codeflow-mcp tool invoke [args-json] + +Transports: + stdio — Claude Code CLI, Cursor, any stdio MCP client (Recommended for local dev) + HTTP — Web clients, Claude Desktop, any HTTP MCP client + SSE — Claude Desktop streaming responses + +Examples: + # Start as MCP server for Claude Code CLI + codeflow-mcp stdio + + # Start as HTTP server with SSE + codeflow-mcp server start --port 3100 + + # Query remote server + codeflow-mcp tool list http://localhost:3100 + codeflow-mcp tool invoke test_tool http://localhost:3100 '{}' +`); +} +//# sourceMappingURL=cli.js.map \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/bin/cli.js.map b/packages/codeflow-mcp/dist/bin/cli.js.map new file mode 100644 index 0000000..25ec229 --- /dev/null +++ b/packages/codeflow-mcp/dist/bin/cli.js.map @@ -0,0 +1 @@ +{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/bin/cli.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/bin/cli.test.d.ts b/packages/codeflow-mcp/dist/bin/cli.test.d.ts new file mode 100644 index 0000000..9e8ffec --- /dev/null +++ b/packages/codeflow-mcp/dist/bin/cli.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=cli.test.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/bin/cli.test.d.ts.map b/packages/codeflow-mcp/dist/bin/cli.test.d.ts.map new file mode 100644 index 0000000..06d1e79 --- /dev/null +++ b/packages/codeflow-mcp/dist/bin/cli.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"cli.test.d.ts","sourceRoot":"","sources":["../../src/bin/cli.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/bin/cli.test.js b/packages/codeflow-mcp/dist/bin/cli.test.js new file mode 100644 index 0000000..a5af7bc --- /dev/null +++ b/packages/codeflow-mcp/dist/bin/cli.test.js @@ -0,0 +1,70 @@ +import { describe, expect, it } from "vitest"; +import { spawn } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +const __dirname = dirname(fileURLToPath(import.meta.url)); +const CLI_PATH = join(__dirname, "../../dist/bin/cli.js"); +describe("CLI binary", () => { + it("invoking without args prints usage to stdout", async () => { + const child = spawn("node", [CLI_PATH], { + env: { ...process.env, NODE_OPTIONS: "" }, + }); + const stdout = await new Promise((resolve, reject) => { + let data = ""; + child.stdout?.on("data", (chunk) => { data += chunk.toString(); }); + child.stderr?.on("data", (chunk) => { data += chunk.toString(); }); + child.on("close", (code) => { + resolve(data); + }); + child.on("error", reject); + setTimeout(() => { child.kill(); reject(new Error("timeout")); }, 5_000); + }); + expect(stdout).toContain("codeflow-mcp stdio"); + expect(stdout).toContain("Usage:"); + }); + it("stdio mode: initialize JSON-RPC yields correct response", async () => { + const child = spawn("node", [CLI_PATH, "stdio"], { + env: { ...process.env, NODE_OPTIONS: "" }, + stdio: ["pipe", "pipe", "pipe"], + }); + const stdoutData = await new Promise((resolve, reject) => { + let data = ""; + child.stdout?.on("data", (chunk) => { data += chunk.toString(); }); + child.on("close", () => { resolve(data); }); + child.on("error", reject); + const initMessage = JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "test", version: "1.0" } }, + }) + "\n"; + child.stdin?.write(initMessage); + child.stdin?.end(); + setTimeout(() => { child.kill(); reject(new Error("timeout")); }, 5_000); + }); + const lines = stdoutData.split("\n").filter(Boolean); + expect(lines.length).toBeGreaterThan(0); + const response = JSON.parse(lines[0]); + expect(response.jsonrpc).toBe("2.0"); + expect(response.id).toBe(1); + expect(response.result).toHaveProperty("protocolVersion", "2024-11-05"); + expect(response.result).toHaveProperty("capabilities"); + expect(response.result).toHaveProperty("serverInfo"); + expect(response.result.serverInfo.name).toBe("codeflow-mcp"); + }); + it("unknown subcommand prints usage without crashing", async () => { + const child = spawn("node", [CLI_PATH, "unknown-cmd"], { + env: { ...process.env, NODE_OPTIONS: "" }, + }); + const stdout = await new Promise((resolve, reject) => { + let data = ""; + child.stdout?.on("data", (chunk) => { data += chunk.toString(); }); + child.stderr?.on("data", (chunk) => { data += chunk.toString(); }); + child.on("close", () => { resolve(data); }); + child.on("error", reject); + setTimeout(() => { child.kill(); reject(new Error("timeout")); }, 5_000); + }); + expect(stdout).toContain("Usage:"); + }); +}); +//# sourceMappingURL=cli.test.js.map \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/bin/cli.test.js.map b/packages/codeflow-mcp/dist/bin/cli.test.js.map new file mode 100644 index 0000000..5c75678 --- /dev/null +++ b/packages/codeflow-mcp/dist/bin/cli.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"cli.test.js","sourceRoot":"","sources":["../../src/bin/cli.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/index.d.ts b/packages/codeflow-mcp/dist/index.d.ts new file mode 100644 index 0000000..361e817 --- /dev/null +++ b/packages/codeflow-mcp/dist/index.d.ts @@ -0,0 +1,14 @@ +import type { McpTool, McpToolResult } from "@abhinav2203/codeflow-core/schema"; +/** + * Discover the tools exposed by an MCP server via the `tools/list` JSON-RPC method. + */ +export declare const listMcpTools: (serverUrl: string, headers?: Record) => Promise; +/** + * Invoke a named tool on an MCP server via the `tools/call` JSON-RPC method. + */ +export declare const invokeMcpTool: (serverUrl: string, toolName: string, args: Record, headers?: Record) => Promise; +/** + * Extract a plain-text summary from an MCP tool result's content array. + */ +export declare const extractTextFromMcpResult: (result: McpToolResult) => string; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/index.d.ts.map b/packages/codeflow-mcp/dist/index.d.ts.map new file mode 100644 index 0000000..85f243b --- /dev/null +++ b/packages/codeflow-mcp/dist/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAkEhF;;GAEG;AACH,eAAO,MAAM,YAAY,GACvB,WAAW,MAAM,EACjB,UAAU,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC/B,OAAO,CAAC,OAAO,EAAE,CAGnB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,GACxB,WAAW,MAAM,EACjB,UAAU,MAAM,EAChB,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,UAAU,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC/B,OAAO,CAAC,aAAa,CASvB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,wBAAwB,GAAI,QAAQ,aAAa,KAAG,MAIlD,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/index.js b/packages/codeflow-mcp/dist/index.js new file mode 100644 index 0000000..ee4c46d --- /dev/null +++ b/packages/codeflow-mcp/dist/index.js @@ -0,0 +1,55 @@ +const sendJsonRpc = async (serverUrl, method, params, id, headers, timeoutMs = 10_000) => { + const request = { jsonrpc: "2.0", id, method, params }; + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + try { + const response = await fetch(serverUrl, { + method: "POST", + headers: { "content-type": "application/json", ...headers }, + body: JSON.stringify(request), + signal: controller.signal + }); + if (!response.ok) { + throw new Error(`MCP server responded with ${response.status} ${response.statusText}`); + } + const json = (await response.json()); + if (json.error) { + throw new Error(`MCP error ${json.error.code}: ${json.error.message}`); + } + if (json.result === undefined) { + throw new Error("MCP server returned an empty result"); + } + return json.result; + } + catch (error) { + if (error instanceof Error && error.name === "AbortError") { + throw new Error(`MCP request timed out after ${timeoutMs}ms`); + } + throw error; + } + finally { + clearTimeout(timeoutId); + } +}; +/** + * Discover the tools exposed by an MCP server via the `tools/list` JSON-RPC method. + */ +export const listMcpTools = async (serverUrl, headers) => { + const result = await sendJsonRpc(serverUrl, "tools/list", {}, 1, headers); + return result.tools ?? []; +}; +/** + * Invoke a named tool on an MCP server via the `tools/call` JSON-RPC method. + */ +export const invokeMcpTool = async (serverUrl, toolName, args, headers) => { + const result = await sendJsonRpc(serverUrl, "tools/call", { name: toolName, arguments: args }, 2, headers); + return result; +}; +/** + * Extract a plain-text summary from an MCP tool result's content array. + */ +export const extractTextFromMcpResult = (result) => result.content + .filter((item) => item.type === "text" && item.text) + .map((item) => item.text) + .join("\n"); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/index.js.map b/packages/codeflow-mcp/dist/index.js.map new file mode 100644 index 0000000..ea07a97 --- /dev/null +++ b/packages/codeflow-mcp/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/index.test.d.ts b/packages/codeflow-mcp/dist/index.test.d.ts new file mode 100644 index 0000000..121d59b --- /dev/null +++ b/packages/codeflow-mcp/dist/index.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=index.test.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/index.test.d.ts.map b/packages/codeflow-mcp/dist/index.test.d.ts.map new file mode 100644 index 0000000..b5774e1 --- /dev/null +++ b/packages/codeflow-mcp/dist/index.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/index.test.js b/packages/codeflow-mcp/dist/index.test.js new file mode 100644 index 0000000..c567e9b --- /dev/null +++ b/packages/codeflow-mcp/dist/index.test.js @@ -0,0 +1,97 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { extractTextFromMcpResult, invokeMcpTool, listMcpTools } from "./index.js"; +const makeFetchMock = (responseBody, ok = true, status = 200) => vi.fn().mockResolvedValue({ + ok, + status, + statusText: ok ? "OK" : "Bad Request", + json: async () => responseBody +}); +afterEach(() => vi.unstubAllGlobals()); +describe("listMcpTools", () => { + it("returns the tools array from a valid tools/list response", async () => { + const tools = [ + { name: "search_github", description: "Search GitHub", inputSchema: { type: "object" } }, + { name: "send_slack", description: "Send a Slack message" } + ]; + vi.stubGlobal("fetch", makeFetchMock({ jsonrpc: "2.0", id: 1, result: { tools } })); + const result = await listMcpTools("http://localhost:3001/mcp"); + expect(result).toHaveLength(2); + expect(result[0]?.name).toBe("search_github"); + expect(result[1]?.name).toBe("send_slack"); + }); + it("forwards custom headers to the MCP server", async () => { + const fetchMock = makeFetchMock({ jsonrpc: "2.0", id: 1, result: { tools: [] } }); + vi.stubGlobal("fetch", fetchMock); + await listMcpTools("http://mcp.example.com/mcp", { Authorization: "Bearer tok" }); + const [, options] = fetchMock.mock.calls[0]; + expect(options.headers["Authorization"]).toBe("Bearer tok"); + }); + it("throws when the server returns a non-ok HTTP status", async () => { + vi.stubGlobal("fetch", makeFetchMock(null, false, 503)); + await expect(listMcpTools("http://mcp.example.com/mcp")).rejects.toThrow("503"); + }); + it("throws when the JSON-RPC response contains an error", async () => { + vi.stubGlobal("fetch", makeFetchMock({ jsonrpc: "2.0", id: 1, error: { code: -32601, message: "Method not found" } })); + await expect(listMcpTools("http://mcp.example.com/mcp")).rejects.toThrow("Method not found"); + }); + it("returns an empty array when the result contains no tools field", async () => { + vi.stubGlobal("fetch", makeFetchMock({ jsonrpc: "2.0", id: 1, result: {} })); + const result = await listMcpTools("http://mcp.example.com/mcp"); + expect(result).toEqual([]); + }); +}); +describe("invokeMcpTool", () => { + it("returns the content from a valid tools/call response", async () => { + const content = [{ type: "text", text: "Found 3 results." }]; + vi.stubGlobal("fetch", makeFetchMock({ jsonrpc: "2.0", id: 2, result: { content } })); + const result = await invokeMcpTool("http://localhost:3001/mcp", "search_github", { + query: "typescript MCP" + }); + expect(result.content).toHaveLength(1); + expect(result.content[0]?.text).toBe("Found 3 results."); + }); + it("sends the tool name and arguments in the JSON-RPC params", async () => { + const fetchMock = makeFetchMock({ + jsonrpc: "2.0", + id: 2, + result: { content: [] } + }); + vi.stubGlobal("fetch", fetchMock); + await invokeMcpTool("http://mcp.example.com/mcp", "send_slack", { channel: "#dev", text: "hello" }); + const [, options] = fetchMock.mock.calls[0]; + const body = JSON.parse(options.body); + expect(body.method).toBe("tools/call"); + expect(body.params.name).toBe("send_slack"); + expect(body.params.arguments).toEqual({ channel: "#dev", text: "hello" }); + }); + it("propagates isError flag from the MCP response", async () => { + vi.stubGlobal("fetch", makeFetchMock({ + jsonrpc: "2.0", + id: 2, + result: { content: [{ type: "text", text: "Permission denied" }], isError: true } + })); + const result = await invokeMcpTool("http://mcp.example.com/mcp", "restricted_tool", {}); + expect(result.isError).toBe(true); + }); + it("throws when the JSON-RPC response contains an error", async () => { + vi.stubGlobal("fetch", makeFetchMock({ jsonrpc: "2.0", id: 2, error: { code: -32602, message: "Invalid params" } })); + await expect(invokeMcpTool("http://mcp.example.com/mcp", "search_github", {})).rejects.toThrow("Invalid params"); + }); +}); +describe("extractTextFromMcpResult", () => { + it("joins text-type content items", () => { + const result = { + content: [ + { type: "text", text: "Line one" }, + { type: "image" }, + { type: "text", text: "Line two" } + ] + }; + expect(extractTextFromMcpResult(result)).toBe("Line one\nLine two"); + }); + it("returns empty string when there is no text content", () => { + const result = { content: [{ type: "image" }] }; + expect(extractTextFromMcpResult(result)).toBe(""); + }); +}); +//# sourceMappingURL=index.test.js.map \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/index.test.js.map b/packages/codeflow-mcp/dist/index.test.js.map new file mode 100644 index 0000000..7d34952 --- /dev/null +++ b/packages/codeflow-mcp/dist/index.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/invoke/index.d.ts b/packages/codeflow-mcp/dist/invoke/index.d.ts new file mode 100644 index 0000000..9aa23d0 --- /dev/null +++ b/packages/codeflow-mcp/dist/invoke/index.d.ts @@ -0,0 +1,65 @@ +/** + * codeflow-mcp MCP server + * + * Implements the MCP spec with three transports: + * - stdio: for Claude Code CLI, Cursor, local tools + * - HTTP: for web clients, Claude Desktop + * - SSE: for streaming responses (Claude Desktop, Cursor) + * + * The server is transport-agnostic — the same handler logic runs regardless + * of how the client connects. Each transport implements the same JSON-RPC + * protocol over its channel. + */ +import { type Server } from "node:http"; +import { TOOLS } from "../tools/index.js"; +interface ToolResult { + content: Array<{ + type: "text"; + text: string; + } | { + type: "image"; + data: string; + mimeType: string; + }>; + isError?: boolean; +} +interface JsonRpcRequest { + jsonrpc: "2.0"; + id: number | string | null; + method: string; + params?: Record; +} +interface JsonRpcResponse { + jsonrpc: "2.0"; + id: number | string | null; + result?: unknown; + error?: { + code: number; + message: string; + }; +} +declare function jsonRpcError(id: unknown, code: number, message: string): JsonRpcResponse; +declare function jsonRpcResult(id: unknown, result: unknown): JsonRpcResponse; +declare function handleJsonRpc(req: JsonRpcRequest): Promise; +/** + * MCP over stdio — the standard MCP transport for local tools and AI IDEs. + * + * Protocol: + * - Client sends JSON-RPC messages (one per line, \n-delimited) + * - Server sends JSON-RPC responses (one per line, \n-delimited) + * - Connection stays open until client sends "terminate" or closes stdin + * + * This is how Claude Code CLI, Cursor, and Claude Desktop connect. + */ +export declare function startStdioServer(): Promise; +/** + * Start HTTP server with both plain JSON-RPC and SSE streaming endpoints. + * + * Endpoints: + * POST / — JSON-RPC (request/response, compatible with all HTTP MCP clients) + * GET /sse — SSE stream for streaming responses (Claude Desktop, Cursor) + */ +export declare function createHttpServer(port?: number, host?: string): Server; +export { TOOLS, handleJsonRpc, jsonRpcError, jsonRpcResult }; +export type { ToolResult, JsonRpcRequest, JsonRpcResponse }; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/invoke/index.d.ts.map b/packages/codeflow-mcp/dist/invoke/index.d.ts.map new file mode 100644 index 0000000..ddf6107 --- /dev/null +++ b/packages/codeflow-mcp/dist/invoke/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/invoke/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAGtD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAI1C,UAAU,UAAU;IAClB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnG,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAiCD,UAAU,cAAc;IACtB,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,UAAU,eAAe;IACvB,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED,iBAAS,YAAY,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,eAAe,CAEjF;AAED,iBAAS,aAAa,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,eAAe,CAEpE;AAID,iBAAe,aAAa,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CAkC1E;AAID;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAwDtD;AA4BD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,SAAO,EAAE,IAAI,SAAc,GAAG,MAAM,CAsFxE;AAID,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,CAAC;AAC7D,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,eAAe,EAAE,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/invoke/index.js b/packages/codeflow-mcp/dist/invoke/index.js new file mode 100644 index 0000000..7c39bdd --- /dev/null +++ b/packages/codeflow-mcp/dist/invoke/index.js @@ -0,0 +1,239 @@ +/** + * codeflow-mcp MCP server + * + * Implements the MCP spec with three transports: + * - stdio: for Claude Code CLI, Cursor, local tools + * - HTTP: for web clients, Claude Desktop + * - SSE: for streaming responses (Claude Desktop, Cursor) + * + * The server is transport-agnostic — the same handler logic runs regardless + * of how the client connects. Each transport implements the same JSON-RPC + * protocol over its channel. + */ +import { createServer } from "node:http"; +import { TOOLS } from "../tools/index.js"; +const TOOL_HANDLERS = { + async test_tool(_args) { + return { + content: [ + { + type: "text", + text: [ + " ∧_∧", + " (。・ω・。)", + " /> <\", + " /< > \", + " | ∨ | |", + "", + " ┌──┐", + " │CF│", + " └──┘", + "", + "🐾 CodeFlow MCP server is alive!", + ].join("\n"), + }, + ], + }; + }, +}; +function jsonRpcError(id, code, message) { + return { jsonrpc: "2.0", id: id, error: { code, message } }; +} +function jsonRpcResult(id, result) { + return { jsonrpc: "2.0", id: id, result }; +} +// ─── Request handler (transport-agnostic) ───────────────────────────────────── +async function handleJsonRpc(req) { + const { method, params, id } = req; + if (method === "tools/list") { + return jsonRpcResult(id, { tools: TOOLS }); + } + if (method === "initialize") { + return jsonRpcResult(id, { + protocolVersion: "2024-11-05", + capabilities: { tools: {} }, + serverInfo: { name: "codeflow-mcp", version: "0.1.0" }, + }); + } + if (method === "tools/call") { + const name = params?.["name"]; + const args = params?.["arguments"] ?? {}; + if (!name) + return jsonRpcError(id, -32602, "Missing tool name"); + const handler = TOOL_HANDLERS[name]; + if (!handler) + return jsonRpcError(id, -32602, `Unknown tool: ${name}`); + try { + const result = await handler(args); + return jsonRpcResult(id, result); + } + catch (err) { + return jsonRpcError(id, -32603, err instanceof Error ? err.message : "Tool execution failed"); + } + } + return jsonRpcError(id, -32601, `Method not found: ${method}`); +} +// ─── stdio transport ───────────────────────────────────────────────────────── +/** + * MCP over stdio — the standard MCP transport for local tools and AI IDEs. + * + * Protocol: + * - Client sends JSON-RPC messages (one per line, \n-delimited) + * - Server sends JSON-RPC responses (one per line, \n-delimited) + * - Connection stays open until client sends "terminate" or closes stdin + * + * This is how Claude Code CLI, Cursor, and Claude Desktop connect. + */ +export async function startStdioServer() { + let buffer = ""; + process.stdin.setEncoding("utf-8"); + process.stdin.on("data", async (chunk) => { + buffer += chunk; + // Process all complete JSON-RPC messages (newline-delimited) + while (buffer.includes("\n")) { + const newlineIndex = buffer.indexOf("\n"); + const line = buffer.slice(0, newlineIndex).trim(); + buffer = buffer.slice(newlineIndex + 1); + if (!line) + continue; + try { + const request = JSON.parse(line); + // Handle MCP spec messages + if (request.method === "initialize") { + const response = await handleJsonRpc(request); + process.stdout.write(JSON.stringify(response) + "\n"); + // Send notification that we're ready + process.stdout.write(JSON.stringify(jsonRpcResult(null, {})) + "\n"); + continue; + } + if (request.method === "notifications/initialized") { + // Client is ready — no response needed + continue; + } + if (request.method === "terminate" || request?.method === "exit") { + process.exit(0); + } + const response = await handleJsonRpc(request); + process.stdout.write(JSON.stringify(response) + "\n"); + } + catch { + const err = { + jsonrpc: "2.0", + id: null, + error: { code: -32700, message: "Parse error" }, + }; + process.stdout.write(JSON.stringify(err) + "\n"); + } + } + }); + process.stdin.on("end", () => { + process.exit(0); + }); + // Keep the process alive + return new Promise(() => { }); +} +// ─── HTTP transport ──────────────────────────────────────────────────────────── +function buildCorsHeaders() { + return { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, authorization, x-api-key, x-request-id", + }; +} +async function parseBody(req) { + let body = ""; + for await (const chunk of req) { + body += chunk; + } + return body; +} +function sendJson(res, data, cors = true) { + res.writeHead(200, { + "Content-Type": "application/json", + ...(cors ? buildCorsHeaders() : {}), + }); + res.end(JSON.stringify(data)); +} +/** + * Start HTTP server with both plain JSON-RPC and SSE streaming endpoints. + * + * Endpoints: + * POST / — JSON-RPC (request/response, compatible with all HTTP MCP clients) + * GET /sse — SSE stream for streaming responses (Claude Desktop, Cursor) + */ +export function createHttpServer(port = 3100, host = "localhost") { + const server = createServer(async (req, res) => { + const url = new URL(req.url ?? "/", `http://${req.headers.host}`); + // CORS preflight + if (req.method === "OPTIONS") { + res.writeHead(204, buildCorsHeaders()); + res.end(); + return; + } + // ── SSE endpoint ────────────────────────────────────────────────────────── + if (url.pathname === "/sse" && req.method === "GET") { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + ...buildCorsHeaders(), + }); + // Send initial connection event + res.write("event: connected\ndata: {}\n\n"); + // Keep-alive ping every 30s + const pingInterval = setInterval(() => { + res.write("event: ping\ndata: {}\n\n"); + }, 30_000); + req.on("close", () => { + clearInterval(pingInterval); + }); + // For SSE, we don't process requests through this connection + // The client reconnects to POST / for actual RPC calls + return; + } + // ── JSON-RPC POST endpoint ─────────────────────────────────────────────── + if (req.method === "POST") { + const body = await parseBody(req); + let request; + try { + request = JSON.parse(body); + } + catch { + sendJson(res, { + jsonrpc: "2.0", + id: null, + error: { code: -32700, message: "Parse error" }, + }); + return; + } + const response = await handleJsonRpc(request); + sendJson(res, response); + return; + } + // ── GET / — MCP protocol handshake / tooling info ────────────────────── + if (req.method === "GET" && url.pathname === "/") { + res.writeHead(200, { "Content-Type": "application/json", ...buildCorsHeaders() }); + res.end(JSON.stringify({ + name: "codeflow-mcp", + version: "0.1.0", + protocolVersion: "2024-11-05", + capabilities: { tools: {} }, + transports: ["stdio", "http", "sse"], + })); + return; + } + // 404 + res.writeHead(404); + res.end(); + }); + server.listen(port, host, () => { + console.log(`[codeflow-mcp] MCP server running`); + console.log(`[codeflow-mcp] HTTP: http://${host}:${port}/`); + console.log(`[codeflow-mcp] SSE: http://${host}:${port}/sse`); + console.log(`[codeflow-mcp] Tools: ${TOOLS.map((t) => t.name).join(", ")}`); + }); + return server; +} +// ─── Exports ───────────────────────────────────────────────────────────────── +export { TOOLS, handleJsonRpc, jsonRpcError, jsonRpcResult }; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/invoke/index.js.map b/packages/codeflow-mcp/dist/invoke/index.js.map new file mode 100644 index 0000000..c3472d6 --- /dev/null +++ b/packages/codeflow-mcp/dist/invoke/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/invoke/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/invoke/index.test.d.ts b/packages/codeflow-mcp/dist/invoke/index.test.d.ts new file mode 100644 index 0000000..121d59b --- /dev/null +++ b/packages/codeflow-mcp/dist/invoke/index.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=index.test.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/invoke/index.test.d.ts.map b/packages/codeflow-mcp/dist/invoke/index.test.d.ts.map new file mode 100644 index 0000000..44f560b --- /dev/null +++ b/packages/codeflow-mcp/dist/invoke/index.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/invoke/index.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/invoke/index.test.js b/packages/codeflow-mcp/dist/invoke/index.test.js new file mode 100644 index 0000000..dc015b1 --- /dev/null +++ b/packages/codeflow-mcp/dist/invoke/index.test.js @@ -0,0 +1,186 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { handleJsonRpc, jsonRpcError, jsonRpcResult, TOOLS } from "./index.js"; +afterEach(() => vi.restoreAllMocks()); +describe("handleJsonRpc", () => { + describe("tools/list", () => { + it("returns the TOOLS registry as JSON-RPC result", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 1, + method: "tools/list", + params: {}, + }); + expect(response.jsonrpc).toBe("2.0"); + expect(response.id).toBe(1); + expect(response.result).toEqual({ tools: TOOLS }); + }); + it("returns all fields for each tool (name, description, inputSchema)", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: null, + method: "tools/list", + params: {}, + }); + const tools = response.result.tools; + expect(tools.length).toBeGreaterThan(0); + for (const tool of tools) { + expect(tool).toHaveProperty("name"); + expect(tool).toHaveProperty("description"); + expect(tool).toHaveProperty("inputSchema"); + } + }); + }); + describe("initialize", () => { + it("returns protocol version, capabilities, and server info", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 2, + method: "initialize", + params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "test", version: "1.0" } }, + }); + expect(response.result).toMatchObject({ + protocolVersion: "2024-11-05", + capabilities: { tools: {} }, + serverInfo: { name: "codeflow-mcp", version: "0.1.0" }, + }); + }); + it("echoes the id as-is (numeric, string, null)", async () => { + for (const id of [1, "abc", null]) { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id, + method: "initialize", + params: {}, + }); + expect(response.id).toBe(id); + } + }); + }); + describe("tools/call", () => { + it("calls test_tool handler and returns ASCII art content", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 3, + method: "tools/call", + params: { name: "test_tool", arguments: {} }, + }); + expect(response.result).toHaveProperty("content"); + const content = response.result.content; + expect(content[0]?.type).toBe("text"); + expect(content[0]?.text).toContain("CF"); + }); + it("returns error when name param is missing", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 4, + method: "tools/call", + params: { arguments: {} }, + }); + expect(response.error).toEqual({ code: -32602, message: "Missing tool name" }); + }); + it("returns error when name param is empty string", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 5, + method: "tools/call", + params: { name: "", arguments: {} }, + }); + expect(response.error).toEqual({ code: -32602, message: "Missing tool name" }); + }); + it("returns error for unknown tool name", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 6, + method: "tools/call", + params: { name: "nonexistent_tool", arguments: {} }, + }); + expect(response.error).toEqual({ code: -32602, message: "Unknown tool: nonexistent_tool" }); + }); + it("passes arguments to the tool handler", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 7, + method: "tools/call", + params: { name: "test_tool", arguments: { mockArg: "value" } }, + }); + // test_tool ignores args but we verify the handler was called + expect(response.result).toHaveProperty("content"); + }); + it("returns -32602 for unknown tool name (not -32603)", async () => { + // Handler exists but name is unknown — verify the correct error code + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 8, + method: "tools/call", + params: { name: "nonexistent_tool", arguments: {} }, + }); + expect(response.error?.code).toBe(-32602); + }); + }); + describe("method not found", () => { + it("returns -32601 for unknown methods", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 9, + method: "tools/delete", + params: {}, + }); + expect(response.error).toEqual({ code: -32601, message: "Method not found: tools/delete" }); + }); + it("returns -32601 for empty method string", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 10, + method: "", + params: {}, + }); + expect(response.error).toEqual({ code: -32601, message: "Method not found: " }); + }); + }); +}); +describe("jsonRpcError", () => { + it("formats error response with jsonrpc, id, and error object", () => { + const err = jsonRpcError(42, -32602, "Invalid params"); + expect(err).toEqual({ + jsonrpc: "2.0", + id: 42, + error: { code: -32602, message: "Invalid params" }, + }); + }); + it("works with string id", () => { + const err = jsonRpcError("req-1", -32700, "Parse error"); + expect(err.error.code).toBe(-32700); + expect(err.id).toBe("req-1"); + }); + it("works with null id", () => { + const err = jsonRpcError(null, -32601, "Method not found"); + expect(err.id).toBe(null); + }); +}); +describe("jsonRpcResult", () => { + it("formats success response with jsonrpc, id, and result", () => { + const result = jsonRpcResult(1, { tools: [] }); + expect(result).toEqual({ + jsonrpc: "2.0", + id: 1, + result: { tools: [] }, + }); + }); +}); +describe("TOOLS registry", () => { + it("contains test_tool with valid MCP tool shape", () => { + const testTool = TOOLS.find((t) => t.name === "test_tool"); + expect(testTool).toBeDefined(); + expect(testTool?.description).toBeTruthy(); + expect(testTool?.inputSchema).toEqual({ type: "object", properties: {}, required: [] }); + }); + it("each tool has name, description, and inputSchema", () => { + for (const tool of TOOLS) { + expect(typeof tool.name).toBe("string"); + expect(tool.name.length).toBeGreaterThan(0); + expect(typeof tool.description).toBe("string"); + expect(typeof tool.inputSchema).toBe("object"); + } + }); +}); +//# sourceMappingURL=index.test.js.map \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/invoke/index.test.js.map b/packages/codeflow-mcp/dist/invoke/index.test.js.map new file mode 100644 index 0000000..182665b --- /dev/null +++ b/packages/codeflow-mcp/dist/invoke/index.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../../src/invoke/index.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/tools/index.d.ts b/packages/codeflow-mcp/dist/tools/index.d.ts new file mode 100644 index 0000000..58add2d --- /dev/null +++ b/packages/codeflow-mcp/dist/tools/index.d.ts @@ -0,0 +1,3 @@ +import type { McpTool } from "@abhinav2203/codeflow-core/schema"; +export declare const TOOLS: McpTool[]; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/tools/index.d.ts.map b/packages/codeflow-mcp/dist/tools/index.d.ts.map new file mode 100644 index 0000000..a3f32df --- /dev/null +++ b/packages/codeflow-mcp/dist/tools/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mCAAmC,CAAC;AAEjE,eAAO,MAAM,KAAK,EAAE,OAAO,EAM1B,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/tools/index.js b/packages/codeflow-mcp/dist/tools/index.js new file mode 100644 index 0000000..b079aa7 --- /dev/null +++ b/packages/codeflow-mcp/dist/tools/index.js @@ -0,0 +1,8 @@ +export const TOOLS = [ + { + name: "test_tool", + description: "Prints a paw and 'CF' in ASCII art. Use to verify the MCP server is working.", + inputSchema: { type: "object", properties: {}, required: [] }, + }, +]; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/tools/index.js.map b/packages/codeflow-mcp/dist/tools/index.js.map new file mode 100644 index 0000000..8532a6e --- /dev/null +++ b/packages/codeflow-mcp/dist/tools/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,KAAK,GAAc;IAC9B;QACE,IAAI,EAAE,WAAW;QACjB,WAAW,EAAE,8EAA8E;QAC3F,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;KAC9D;CACF,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/tools/index.test.d.ts b/packages/codeflow-mcp/dist/tools/index.test.d.ts new file mode 100644 index 0000000..121d59b --- /dev/null +++ b/packages/codeflow-mcp/dist/tools/index.test.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=index.test.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/tools/index.test.d.ts.map b/packages/codeflow-mcp/dist/tools/index.test.d.ts.map new file mode 100644 index 0000000..cf73ae9 --- /dev/null +++ b/packages/codeflow-mcp/dist/tools/index.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/tools/index.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/tools/index.test.js b/packages/codeflow-mcp/dist/tools/index.test.js new file mode 100644 index 0000000..bd9afab --- /dev/null +++ b/packages/codeflow-mcp/dist/tools/index.test.js @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import { TOOLS } from "./index.js"; +describe("TOOLS registry", () => { + it("TOOLS array has exactly 1 tool", () => { + expect(TOOLS).toHaveLength(1); + }); + it("the tool is named test_tool", () => { + expect(TOOLS[0]?.name).toBe("test_tool"); + }); + it("test_tool has correct description", () => { + expect(TOOLS[0]?.description).toBe("Prints a paw and 'CF' in ASCII art. Use to verify the MCP server is working."); + }); + it("test_tool has correct inputSchema", () => { + expect(TOOLS[0]?.inputSchema).toEqual({ type: "object", properties: {}, required: [] }); + }); + it("each tool entry is a valid McpTool shape (name is string)", () => { + for (const tool of TOOLS) { + expect(typeof tool.name).toBe("string"); + } + }); + it("each tool entry has description as string", () => { + for (const tool of TOOLS) { + expect(typeof tool.description).toBe("string"); + } + }); + it("each tool entry has inputSchema as object", () => { + for (const tool of TOOLS) { + expect(tool.inputSchema).toBeInstanceOf(Object); + } + }); + it("each tool entry is a valid McpTool with all required fields", () => { + const isValidMcpTool = (t) => typeof t === "object" && + t !== null && + typeof t.name === "string" && + typeof t.description === "string" && + typeof t.inputSchema === "object"; + for (const tool of TOOLS) { + expect(isValidMcpTool(tool)).toBe(true); + } + }); +}); +//# sourceMappingURL=index.test.js.map \ No newline at end of file diff --git a/packages/codeflow-mcp/dist/tools/index.test.js.map b/packages/codeflow-mcp/dist/tools/index.test.js.map new file mode 100644 index 0000000..ab0d762 --- /dev/null +++ b/packages/codeflow-mcp/dist/tools/index.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../../src/tools/index.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,IAAI,CAChC,8EAA8E,CAC/E,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;IAC1F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,CAAC,OAAO,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACjD,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QAClD,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;QACrE,MAAM,cAAc,GAAG,CAAC,CAAU,EAAgB,EAAE,CAClD,OAAO,CAAC,KAAK,QAAQ;YACrB,CAAC,KAAK,IAAI;YACV,OAAQ,CAAa,CAAC,IAAI,KAAK,QAAQ;YACvC,OAAQ,CAAa,CAAC,WAAW,KAAK,QAAQ;YAC9C,OAAQ,CAAa,CAAC,WAAW,KAAK,QAAQ,CAAC;QAEjD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-mcp/package.json b/packages/codeflow-mcp/package.json new file mode 100644 index 0000000..c93b0d4 --- /dev/null +++ b/packages/codeflow-mcp/package.json @@ -0,0 +1,40 @@ +{ + "name": "@abhinav2203/codeflow-mcp", + "version": "0.1.0", + "description": "MCP server configuration and tool registry for CodeFlow blueprint operations.", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./invoke": { + "types": "./dist/invoke/index.d.ts", + "default": "./dist/invoke/index.js" + }, + "./tools": { + "types": "./dist/tools/index.d.ts", + "default": "./dist/tools/index.js" + } + }, + "bin": { + "codeflow-mcp": "./dist/bin/cli.js" + }, + "scripts": { + "check": "tsc --noEmit", + "test": "vitest run", + "build": "tsc --outDir dist --declaration --declarationMap && node scripts/wrap-cli.mjs", + "clean": "rm -rf dist" + }, + "dependencies": { + "@abhinav2203/codeflow-core": "workspace:*", + "zod": "^3.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.7.0", + "vitest": "^3.0.0" + } +} diff --git a/packages/codeflow-mcp/scripts/wrap-cli.mjs b/packages/codeflow-mcp/scripts/wrap-cli.mjs new file mode 100644 index 0000000..365a266 --- /dev/null +++ b/packages/codeflow-mcp/scripts/wrap-cli.mjs @@ -0,0 +1,9 @@ +import { mkdir } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const distBin = join(__dirname, "../dist/bin"); + +// Ensure the bin directory exists (tsc doesn't create nested dirs by default) +await mkdir(distBin, { recursive: true }); diff --git a/packages/codeflow-mcp/src/bin/cli.test.ts b/packages/codeflow-mcp/src/bin/cli.test.ts new file mode 100644 index 0000000..7434ff8 --- /dev/null +++ b/packages/codeflow-mcp/src/bin/cli.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from "vitest"; +import { spawn } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const CLI_PATH = join(__dirname, "../../dist/bin/cli.js"); + +describe("CLI binary", () => { + it("invoking without args prints usage to stdout", async () => { + const child = spawn("node", [CLI_PATH], { + env: { ...process.env, NODE_OPTIONS: "" }, + }); + + const stdout = await new Promise((resolve, reject) => { + let data = ""; + child.stdout?.on("data", (chunk) => { data += chunk.toString(); }); + child.stderr?.on("data", (chunk) => { data += chunk.toString(); }); + child.on("close", (code) => { + resolve(data); + }); + child.on("error", reject); + setTimeout(() => { child.kill(); reject(new Error("timeout")); }, 5_000); + }); + + expect(stdout).toContain("codeflow-mcp stdio"); + expect(stdout).toContain("Usage:"); + }); + + it("stdio mode: initialize JSON-RPC yields correct response", async () => { + const child = spawn("node", [CLI_PATH, "stdio"], { + env: { ...process.env, NODE_OPTIONS: "" }, + stdio: ["pipe", "pipe", "pipe"], + }); + + const stdoutData = await new Promise((resolve, reject) => { + let data = ""; + child.stdout?.on("data", (chunk) => { data += chunk.toString(); }); + child.on("close", () => { resolve(data); }); + child.on("error", reject); + + const initMessage = JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "test", version: "1.0" } }, + }) + "\n"; + + child.stdin?.write(initMessage); + child.stdin?.end(); + + setTimeout(() => { child.kill(); reject(new Error("timeout")); }, 5_000); + }); + + const lines = stdoutData.split("\n").filter(Boolean); + expect(lines.length).toBeGreaterThan(0); + + const response = JSON.parse(lines[0]); + expect(response.jsonrpc).toBe("2.0"); + expect(response.id).toBe(1); + expect(response.result).toHaveProperty("protocolVersion", "2024-11-05"); + expect(response.result).toHaveProperty("capabilities"); + expect(response.result).toHaveProperty("serverInfo"); + expect(response.result.serverInfo.name).toBe("codeflow-mcp"); + }); + + it("unknown subcommand prints usage without crashing", async () => { + const child = spawn("node", [CLI_PATH, "unknown-cmd"], { + env: { ...process.env, NODE_OPTIONS: "" }, + }); + + const stdout = await new Promise((resolve, reject) => { + let data = ""; + child.stdout?.on("data", (chunk) => { data += chunk.toString(); }); + child.stderr?.on("data", (chunk) => { data += chunk.toString(); }); + child.on("close", () => { resolve(data); }); + child.on("error", reject); + setTimeout(() => { child.kill(); reject(new Error("timeout")); }, 5_000); + }); + + expect(stdout).toContain("Usage:"); + }); +}); \ No newline at end of file diff --git a/packages/codeflow-mcp/src/bin/cli.ts b/packages/codeflow-mcp/src/bin/cli.ts new file mode 100644 index 0000000..5d384da --- /dev/null +++ b/packages/codeflow-mcp/src/bin/cli.ts @@ -0,0 +1,123 @@ +#!/usr/bin/env node + +/** + * codeflow-mcp CLI + * + * Usage: + * codeflow-mcp stdio # Start MCP server over stdio (Claude Code, Cursor) + * codeflow-mcp server start # Start MCP server over HTTP+SSE + * codeflow-mcp tool list # Query tools from a remote MCP server + * codeflow-mcp tool invoke [args-json] + */ + +import { createServer } from "node:http"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { startStdioServer, createHttpServer } from "../invoke/index.js"; + +interface JsonRpcRequest { + jsonrpc: "2.0"; + id: number | string | null; + method: string; + params?: Record; +} + +interface JsonRpcResponse { + jsonrpc: "2.0"; + id: number | string | null; + result?: unknown; + error?: { code: number; message: string }; +} + +function jsonRpcError(id: unknown, code: number, message: string): JsonRpcResponse { + return { jsonrpc: "2.0", id: id as string | number | null, error: { code, message } }; +} + +function jsonRpcResult(id: unknown, result: unknown): JsonRpcResponse { + return { jsonrpc: "2.0", id: id as string | number | null, result }; +} + +// ─── Tool query client ──────────────────────────────────────────────────────── + +async function queryRemote(serverUrl: string, request: JsonRpcRequest): Promise { + const res = await fetch(serverUrl, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(request), + }); + return res.json() as Promise; +} + +// ─── CLI dispatch ───────────────────────────────────────────────────────────── + +const [cmd, subcmd, ...args] = process.argv.slice(2); + +if (cmd === "stdio") { + // MCP over stdio — keeps process alive, reads/writes JSON-RPC lines + startStdioServer().catch((err) => { + console.error("[codeflow-mcp] stdio server error:", err); + process.exit(1); + }); +} else if (cmd === "server" && subcmd === "start") { + // HTTP + SSE server + let port = 3100; + let host = "localhost"; + const remaining = args.slice(0); + for (let i = 0; i < remaining.length; i++) { + if (remaining[i] === "--port" && i + 1 < remaining.length) { + port = parseInt(remaining[i + 1], 10); + } else if (remaining[i] === "--host" && i + 1 < remaining.length) { + host = remaining[i + 1]; + } + } + createHttpServer(port, host); +} else if (cmd === "tool" && subcmd === "list") { + const serverUrl = args[0] ?? "http://localhost:3100"; + const data = await queryRemote(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "tools/list", + params: {}, + }); + if (data.error) { + console.error(`Error: ${data.error.message}`); + process.exit(1); + } + console.log(JSON.stringify(data.result, null, 2)); +} else if (cmd === "tool" && subcmd === "invoke") { + const [toolName, serverUrl = "http://localhost:3100", argsJson = "{}"] = args; + const data = await queryRemote(serverUrl, { + jsonrpc: "2.0", + id: 2, + method: "tools/call", + params: { name: toolName, arguments: JSON.parse(argsJson) }, + }); + if (data.error) { + console.error(`Error: ${data.error.message}`); + process.exit(1); + } + console.log(JSON.stringify(data.result, null, 2)); +} else { + console.log(`Usage: + codeflow-mcp stdio # Start MCP server over stdio (Claude Code, Cursor) + codeflow-mcp server start [--port 3100] [--host localhost] + codeflow-mcp tool list + codeflow-mcp tool invoke [args-json] + +Transports: + stdio — Claude Code CLI, Cursor, any stdio MCP client (Recommended for local dev) + HTTP — Web clients, Claude Desktop, any HTTP MCP client + SSE — Claude Desktop streaming responses + +Examples: + # Start as MCP server for Claude Code CLI + codeflow-mcp stdio + + # Start as HTTP server with SSE + codeflow-mcp server start --port 3100 + + # Query remote server + codeflow-mcp tool list http://localhost:3100 + codeflow-mcp tool invoke test_tool http://localhost:3100 '{}' +`); +} \ No newline at end of file diff --git a/packages/codeflow-mcp/src/index.test.ts b/packages/codeflow-mcp/src/index.test.ts new file mode 100644 index 0000000..a6fb831 --- /dev/null +++ b/packages/codeflow-mcp/src/index.test.ts @@ -0,0 +1,149 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { extractTextFromMcpResult, invokeMcpTool, listMcpTools } from "./index.js"; +import type { McpToolResult } from "@abhinav2203/codeflow-core/schema"; + +const makeFetchMock = (responseBody: unknown, ok = true, status = 200) => + vi.fn().mockResolvedValue({ + ok, + status, + statusText: ok ? "OK" : "Bad Request", + json: async () => responseBody + }); + +afterEach(() => vi.unstubAllGlobals()); + +describe("listMcpTools", () => { + it("returns the tools array from a valid tools/list response", async () => { + const tools = [ + { name: "search_github", description: "Search GitHub", inputSchema: { type: "object" } }, + { name: "send_slack", description: "Send a Slack message" } + ]; + vi.stubGlobal( + "fetch", + makeFetchMock({ jsonrpc: "2.0", id: 1, result: { tools } }) + ); + + const result = await listMcpTools("http://localhost:3001/mcp"); + + expect(result).toHaveLength(2); + expect(result[0]?.name).toBe("search_github"); + expect(result[1]?.name).toBe("send_slack"); + }); + + it("forwards custom headers to the MCP server", async () => { + const fetchMock = makeFetchMock({ jsonrpc: "2.0", id: 1, result: { tools: [] } }); + vi.stubGlobal("fetch", fetchMock); + + await listMcpTools("http://mcp.example.com/mcp", { Authorization: "Bearer tok" }); + + const [, options] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect((options.headers as Record)["Authorization"]).toBe("Bearer tok"); + }); + + it("throws when the server returns a non-ok HTTP status", async () => { + vi.stubGlobal("fetch", makeFetchMock(null, false, 503)); + + await expect(listMcpTools("http://mcp.example.com/mcp")).rejects.toThrow("503"); + }); + + it("throws when the JSON-RPC response contains an error", async () => { + vi.stubGlobal( + "fetch", + makeFetchMock({ jsonrpc: "2.0", id: 1, error: { code: -32601, message: "Method not found" } }) + ); + + await expect(listMcpTools("http://mcp.example.com/mcp")).rejects.toThrow("Method not found"); + }); + + it("returns an empty array when the result contains no tools field", async () => { + vi.stubGlobal( + "fetch", + makeFetchMock({ jsonrpc: "2.0", id: 1, result: {} }) + ); + + const result = await listMcpTools("http://mcp.example.com/mcp"); + expect(result).toEqual([]); + }); +}); + +describe("invokeMcpTool", () => { + it("returns the content from a valid tools/call response", async () => { + const content = [{ type: "text", text: "Found 3 results." }]; + vi.stubGlobal( + "fetch", + makeFetchMock({ jsonrpc: "2.0", id: 2, result: { content } }) + ); + + const result = await invokeMcpTool("http://localhost:3001/mcp", "search_github", { + query: "typescript MCP" + }); + + expect(result.content).toHaveLength(1); + expect(result.content[0]?.text).toBe("Found 3 results."); + }); + + it("sends the tool name and arguments in the JSON-RPC params", async () => { + const fetchMock = makeFetchMock({ + jsonrpc: "2.0", + id: 2, + result: { content: [] } + }); + vi.stubGlobal("fetch", fetchMock); + + await invokeMcpTool("http://mcp.example.com/mcp", "send_slack", { channel: "#dev", text: "hello" }); + + const [, options] = fetchMock.mock.calls[0] as [string, RequestInit]; + const body = JSON.parse(options.body as string) as { + method: string; + params: { name: string; arguments: Record }; + }; + + expect(body.method).toBe("tools/call"); + expect(body.params.name).toBe("send_slack"); + expect(body.params.arguments).toEqual({ channel: "#dev", text: "hello" }); + }); + + it("propagates isError flag from the MCP response", async () => { + vi.stubGlobal( + "fetch", + makeFetchMock({ + jsonrpc: "2.0", + id: 2, + result: { content: [{ type: "text", text: "Permission denied" }], isError: true } + }) + ); + + const result = await invokeMcpTool("http://mcp.example.com/mcp", "restricted_tool", {}); + expect(result.isError).toBe(true); + }); + + it("throws when the JSON-RPC response contains an error", async () => { + vi.stubGlobal( + "fetch", + makeFetchMock({ jsonrpc: "2.0", id: 2, error: { code: -32602, message: "Invalid params" } }) + ); + + await expect(invokeMcpTool("http://mcp.example.com/mcp", "search_github", {})).rejects.toThrow( + "Invalid params" + ); + }); +}); + +describe("extractTextFromMcpResult", () => { + it("joins text-type content items", () => { + const result: McpToolResult = { + content: [ + { type: "text", text: "Line one" }, + { type: "image" }, + { type: "text", text: "Line two" } + ] + }; + expect(extractTextFromMcpResult(result)).toBe("Line one\nLine two"); + }); + + it("returns empty string when there is no text content", () => { + const result: McpToolResult = { content: [{ type: "image" }] }; + expect(extractTextFromMcpResult(result)).toBe(""); + }); +}); diff --git a/packages/codeflow-mcp/src/index.ts b/packages/codeflow-mcp/src/index.ts new file mode 100644 index 0000000..c18328f --- /dev/null +++ b/packages/codeflow-mcp/src/index.ts @@ -0,0 +1,104 @@ +import type { McpTool, McpToolResult } from "@abhinav2203/codeflow-core/schema"; + +interface JsonRpcRequest { + jsonrpc: "2.0"; + id: number; + method: string; + params?: Record; +} + +interface JsonRpcResponse { + jsonrpc: "2.0"; + id: number; + result?: T; + error?: { code: number; message: string; data?: unknown }; +} + +interface ToolsListResult { + tools: McpTool[]; +} + +const sendJsonRpc = async ( + serverUrl: string, + method: string, + params: Record, + id: number, + headers?: Record, + timeoutMs: number = 10_000 +): Promise => { + const request: JsonRpcRequest = { jsonrpc: "2.0", id, method, params }; + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await fetch(serverUrl, { + method: "POST", + headers: { "content-type": "application/json", ...headers }, + body: JSON.stringify(request), + signal: controller.signal + }); + + if (!response.ok) { + throw new Error(`MCP server responded with ${response.status} ${response.statusText}`); + } + + const json = (await response.json()) as JsonRpcResponse; + + if (json.error) { + throw new Error(`MCP error ${json.error.code}: ${json.error.message}`); + } + + if (json.result === undefined) { + throw new Error("MCP server returned an empty result"); + } + + return json.result; + } catch (error) { + if (error instanceof Error && error.name === "AbortError") { + throw new Error(`MCP request timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timeoutId); + } +}; + +/** + * Discover the tools exposed by an MCP server via the `tools/list` JSON-RPC method. + */ +export const listMcpTools = async ( + serverUrl: string, + headers?: Record +): Promise => { + const result = await sendJsonRpc(serverUrl, "tools/list", {}, 1, headers); + return result.tools ?? []; +}; + +/** + * Invoke a named tool on an MCP server via the `tools/call` JSON-RPC method. + */ +export const invokeMcpTool = async ( + serverUrl: string, + toolName: string, + args: Record, + headers?: Record +): Promise => { + const result = await sendJsonRpc( + serverUrl, + "tools/call", + { name: toolName, arguments: args }, + 2, + headers + ); + return result; +}; + +/** + * Extract a plain-text summary from an MCP tool result's content array. + */ +export const extractTextFromMcpResult = (result: McpToolResult): string => + result.content + .filter((item) => item.type === "text" && item.text) + .map((item) => item.text as string) + .join("\n"); diff --git a/packages/codeflow-mcp/src/invoke/index.test.ts b/packages/codeflow-mcp/src/invoke/index.test.ts new file mode 100644 index 0000000..6c8db8b --- /dev/null +++ b/packages/codeflow-mcp/src/invoke/index.test.ts @@ -0,0 +1,214 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { handleJsonRpc, jsonRpcError, jsonRpcResult, TOOLS } from "./index.js"; + +afterEach(() => vi.restoreAllMocks()); + +describe("handleJsonRpc", () => { + describe("tools/list", () => { + it("returns the TOOLS registry as JSON-RPC result", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 1, + method: "tools/list", + params: {}, + }); + + expect(response.jsonrpc).toBe("2.0"); + expect(response.id).toBe(1); + expect(response.result).toEqual({ tools: TOOLS }); + }); + + it("returns all fields for each tool (name, description, inputSchema)", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: null, + method: "tools/list", + params: {}, + }); + + const tools = (response.result as { tools: typeof TOOLS }).tools; + expect(tools.length).toBeGreaterThan(0); + for (const tool of tools) { + expect(tool).toHaveProperty("name"); + expect(tool).toHaveProperty("description"); + expect(tool).toHaveProperty("inputSchema"); + } + }); + }); + + describe("initialize", () => { + it("returns protocol version, capabilities, and server info", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 2, + method: "initialize", + params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "test", version: "1.0" } }, + }); + + expect(response.result).toMatchObject({ + protocolVersion: "2024-11-05", + capabilities: { tools: {} }, + serverInfo: { name: "codeflow-mcp", version: "0.1.0" }, + }); + }); + + it("echoes the id as-is (numeric, string, null)", async () => { + for (const id of [1, "abc", null]) { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id, + method: "initialize", + params: {}, + }); + expect(response.id).toBe(id); + } + }); + }); + + describe("tools/call", () => { + it("calls test_tool handler and returns ASCII art content", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 3, + method: "tools/call", + params: { name: "test_tool", arguments: {} }, + }); + + expect(response.result).toHaveProperty("content"); + const content = (response.result as { content: Array<{ type: string; text: string }> }).content; + expect(content[0]?.type).toBe("text"); + expect(content[0]?.text).toContain("CF"); + }); + + it("returns error when name param is missing", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 4, + method: "tools/call", + params: { arguments: {} }, + }); + + expect(response.error).toEqual({ code: -32602, message: "Missing tool name" }); + }); + + it("returns error when name param is empty string", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 5, + method: "tools/call", + params: { name: "", arguments: {} }, + }); + + expect(response.error).toEqual({ code: -32602, message: "Missing tool name" }); + }); + + it("returns error for unknown tool name", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 6, + method: "tools/call", + params: { name: "nonexistent_tool", arguments: {} }, + }); + + expect(response.error).toEqual({ code: -32602, message: "Unknown tool: nonexistent_tool" }); + }); + + it("passes arguments to the tool handler", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 7, + method: "tools/call", + params: { name: "test_tool", arguments: { mockArg: "value" } }, + }); + + // test_tool ignores args but we verify the handler was called + expect(response.result).toHaveProperty("content"); + }); + + it("returns -32602 for unknown tool name (not -32603)", async () => { + // Handler exists but name is unknown — verify the correct error code + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 8, + method: "tools/call", + params: { name: "nonexistent_tool", arguments: {} }, + }); + expect(response.error?.code).toBe(-32602); + }); + }); + + describe("method not found", () => { + it("returns -32601 for unknown methods", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 9, + method: "tools/delete", + params: {}, + }); + + expect(response.error).toEqual({ code: -32601, message: "Method not found: tools/delete" }); + }); + + it("returns -32601 for empty method string", async () => { + const response = await handleJsonRpc({ + jsonrpc: "2.0", + id: 10, + method: "", + params: {}, + }); + + expect(response.error).toEqual({ code: -32601, message: "Method not found: " }); + }); + }); +}); + +describe("jsonRpcError", () => { + it("formats error response with jsonrpc, id, and error object", () => { + const err = jsonRpcError(42, -32602, "Invalid params"); + expect(err).toEqual({ + jsonrpc: "2.0", + id: 42, + error: { code: -32602, message: "Invalid params" }, + }); + }); + + it("works with string id", () => { + const err = jsonRpcError("req-1", -32700, "Parse error"); + expect(err.error!.code).toBe(-32700); + expect(err.id).toBe("req-1"); + }); + + it("works with null id", () => { + const err = jsonRpcError(null, -32601, "Method not found"); + expect(err.id).toBe(null); + }); +}); + +describe("jsonRpcResult", () => { + it("formats success response with jsonrpc, id, and result", () => { + const result = jsonRpcResult(1, { tools: [] }); + expect(result).toEqual({ + jsonrpc: "2.0", + id: 1, + result: { tools: [] }, + }); + }); +}); + +describe("TOOLS registry", () => { + it("contains test_tool with valid MCP tool shape", () => { + const testTool = TOOLS.find((t) => t.name === "test_tool"); + expect(testTool).toBeDefined(); + expect(testTool?.description).toBeTruthy(); + expect(testTool?.inputSchema).toEqual({ type: "object", properties: {}, required: [] }); + }); + + it("each tool has name, description, and inputSchema", () => { + for (const tool of TOOLS) { + expect(typeof tool.name).toBe("string"); + expect(tool.name.length).toBeGreaterThan(0); + expect(typeof tool.description).toBe("string"); + expect(typeof tool.inputSchema).toBe("object"); + } + }); +}); \ No newline at end of file diff --git a/packages/codeflow-mcp/src/invoke/index.ts b/packages/codeflow-mcp/src/invoke/index.ts new file mode 100644 index 0000000..a583020 --- /dev/null +++ b/packages/codeflow-mcp/src/invoke/index.ts @@ -0,0 +1,311 @@ +/** + * codeflow-mcp MCP server + * + * Implements the MCP spec with three transports: + * - stdio: for Claude Code CLI, Cursor, local tools + * - HTTP: for web clients, Claude Desktop + * - SSE: for streaming responses (Claude Desktop, Cursor) + * + * The server is transport-agnostic — the same handler logic runs regardless + * of how the client connects. Each transport implements the same JSON-RPC + * protocol over its channel. + */ + +import { createServer, type Server } from "node:http"; +import type { IncomingMessage, ServerResponse } from "node:http"; + +import { TOOLS } from "../tools/index.js"; + +// ─── Tool registry ──────────────────────────────────────────────────────────── + +interface ToolResult { + content: Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }>; + isError?: boolean; +} + +interface ToolHandler { + (args: Record): ToolResult | Promise; +} + +const TOOL_HANDLERS: Record = { + async test_tool(_args) { + return { + content: [ + { + type: "text", + text: [ + " ∧_∧", + " (。・ω・。)", + " /> <\", + " /< > \", + " | ∨ | |", + "", + " ┌──┐", + " │CF│", + " └──┘", + "", + "🐾 CodeFlow MCP server is alive!", + ].join("\n"), + }, + ], + }; + }, +}; + +// ─── JSON-RPC types ─────────────────────────────────────────────────────────── + +interface JsonRpcRequest { + jsonrpc: "2.0"; + id: number | string | null; + method: string; + params?: Record; +} + +interface JsonRpcResponse { + jsonrpc: "2.0"; + id: number | string | null; + result?: unknown; + error?: { code: number; message: string }; +} + +function jsonRpcError(id: unknown, code: number, message: string): JsonRpcResponse { + return { jsonrpc: "2.0", id: id as string | number | null, error: { code, message } }; +} + +function jsonRpcResult(id: unknown, result: unknown): JsonRpcResponse { + return { jsonrpc: "2.0", id: id as string | number | null, result }; +} + +// ─── Request handler (transport-agnostic) ───────────────────────────────────── + +async function handleJsonRpc(req: JsonRpcRequest): Promise { + const { method, params, id } = req; + + if (method === "tools/list") { + return jsonRpcResult(id, { tools: TOOLS }); + } + + if (method === "initialize") { + return jsonRpcResult(id, { + protocolVersion: "2024-11-05", + capabilities: { tools: {} }, + serverInfo: { name: "codeflow-mcp", version: "0.1.0" }, + }); + } + + if (method === "tools/call") { + const name = (params as Record)?.["name"] as string | undefined; + const args = ((params as Record)?.["arguments"] as Record) ?? {}; + if (!name) return jsonRpcError(id, -32602, "Missing tool name"); + const handler = TOOL_HANDLERS[name]; + if (!handler) return jsonRpcError(id, -32602, `Unknown tool: ${name}`); + try { + const result = await handler(args); + return jsonRpcResult(id, result); + } catch (err) { + return jsonRpcError( + id, + -32603, + err instanceof Error ? err.message : "Tool execution failed" + ); + } + } + + return jsonRpcError(id, -32601, `Method not found: ${method}`); +} + +// ─── stdio transport ───────────────────────────────────────────────────────── + +/** + * MCP over stdio — the standard MCP transport for local tools and AI IDEs. + * + * Protocol: + * - Client sends JSON-RPC messages (one per line, \n-delimited) + * - Server sends JSON-RPC responses (one per line, \n-delimited) + * - Connection stays open until client sends "terminate" or closes stdin + * + * This is how Claude Code CLI, Cursor, and Claude Desktop connect. + */ +export async function startStdioServer(): Promise { + let buffer = ""; + + process.stdin.setEncoding("utf-8"); + + process.stdin.on("data", async (chunk: string) => { + buffer += chunk; + + // Process all complete JSON-RPC messages (newline-delimited) + while (buffer.includes("\n")) { + const newlineIndex = buffer.indexOf("\n"); + const line = buffer.slice(0, newlineIndex).trim(); + buffer = buffer.slice(newlineIndex + 1); + + if (!line) continue; + + try { + const request = JSON.parse(line) as JsonRpcRequest; + + // Handle MCP spec messages + if (request.method === "initialize") { + const response = await handleJsonRpc(request); + process.stdout.write(JSON.stringify(response) + "\n"); + // Send notification that we're ready + process.stdout.write(JSON.stringify(jsonRpcResult(null, {})) + "\n"); + continue; + } + + if (request.method === "notifications/initialized") { + // Client is ready — no response needed + continue; + } + + if (request.method === "terminate" || (request as unknown as { method: string })?.method === "exit") { + process.exit(0); + } + + const response = await handleJsonRpc(request); + process.stdout.write(JSON.stringify(response) + "\n"); + } catch { + const err: JsonRpcResponse = { + jsonrpc: "2.0", + id: null, + error: { code: -32700, message: "Parse error" }, + }; + process.stdout.write(JSON.stringify(err) + "\n"); + } + } + }); + + process.stdin.on("end", () => { + process.exit(0); + }); + + // Keep the process alive + return new Promise(() => {}); +} + +// ─── HTTP transport ──────────────────────────────────────────────────────────── + +function buildCorsHeaders(): Record { + return { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, authorization, x-api-key, x-request-id", + }; +} + +async function parseBody(req: IncomingMessage): Promise { + let body = ""; + for await (const chunk of req) { + body += chunk; + } + return body; +} + +function sendJson(res: ServerResponse, data: JsonRpcResponse, cors = true) { + res.writeHead(200, { + "Content-Type": "application/json", + ...(cors ? buildCorsHeaders() : {}), + }); + res.end(JSON.stringify(data)); +} + +/** + * Start HTTP server with both plain JSON-RPC and SSE streaming endpoints. + * + * Endpoints: + * POST / — JSON-RPC (request/response, compatible with all HTTP MCP clients) + * GET /sse — SSE stream for streaming responses (Claude Desktop, Cursor) + */ +export function createHttpServer(port = 3100, host = "localhost"): Server { + const server = createServer(async (req, res) => { + const url = new URL(req.url ?? "/", `http://${req.headers.host}`); + + // CORS preflight + if (req.method === "OPTIONS") { + res.writeHead(204, buildCorsHeaders()); + res.end(); + return; + } + + // ── SSE endpoint ────────────────────────────────────────────────────────── + if (url.pathname === "/sse" && req.method === "GET") { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + ...buildCorsHeaders(), + }); + + // Send initial connection event + res.write("event: connected\ndata: {}\n\n"); + + // Keep-alive ping every 30s + const pingInterval = setInterval(() => { + res.write("event: ping\ndata: {}\n\n"); + }, 30_000); + + req.on("close", () => { + clearInterval(pingInterval); + }); + + // For SSE, we don't process requests through this connection + // The client reconnects to POST / for actual RPC calls + return; + } + + // ── JSON-RPC POST endpoint ─────────────────────────────────────────────── + if (req.method === "POST") { + const body = await parseBody(req); + + let request: JsonRpcRequest; + try { + request = JSON.parse(body); + } catch { + sendJson(res, { + jsonrpc: "2.0", + id: null, + error: { code: -32700, message: "Parse error" }, + }); + return; + } + + const response = await handleJsonRpc(request); + sendJson(res, response); + return; + } + + // ── GET / — MCP protocol handshake / tooling info ────────────────────── + if (req.method === "GET" && url.pathname === "/") { + res.writeHead(200, { "Content-Type": "application/json", ...buildCorsHeaders() }); + res.end( + JSON.stringify({ + name: "codeflow-mcp", + version: "0.1.0", + protocolVersion: "2024-11-05", + capabilities: { tools: {} }, + transports: ["stdio", "http", "sse"], + }) + ); + return; + } + + // 404 + res.writeHead(404); + res.end(); + }); + + server.listen(port, host, () => { + console.log(`[codeflow-mcp] MCP server running`); + console.log(`[codeflow-mcp] HTTP: http://${host}:${port}/`); + console.log(`[codeflow-mcp] SSE: http://${host}:${port}/sse`); + console.log(`[codeflow-mcp] Tools: ${TOOLS.map((t) => t.name).join(", ")}`); + }); + + return server; +} + +// ─── Exports ───────────────────────────────────────────────────────────────── + +export { TOOLS, handleJsonRpc, jsonRpcError, jsonRpcResult }; +export type { ToolResult, JsonRpcRequest, JsonRpcResponse }; \ No newline at end of file diff --git a/packages/codeflow-mcp/src/tools/index.test.ts b/packages/codeflow-mcp/src/tools/index.test.ts new file mode 100644 index 0000000..40a1e83 --- /dev/null +++ b/packages/codeflow-mcp/src/tools/index.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import type { McpTool } from "@abhinav2203/codeflow-core/schema"; +import { TOOLS } from "./index.js"; + +describe("TOOLS registry", () => { + it("TOOLS array has exactly 1 tool", () => { + expect(TOOLS).toHaveLength(1); + }); + + it("the tool is named test_tool", () => { + expect(TOOLS[0]?.name).toBe("test_tool"); + }); + + it("test_tool has correct description", () => { + expect(TOOLS[0]?.description).toBe( + "Prints a paw and 'CF' in ASCII art. Use to verify the MCP server is working." + ); + }); + + it("test_tool has correct inputSchema", () => { + expect(TOOLS[0]?.inputSchema).toEqual({ type: "object", properties: {}, required: [] }); + }); + + it("each tool entry is a valid McpTool shape (name is string)", () => { + for (const tool of TOOLS) { + expect(typeof tool.name).toBe("string"); + } + }); + + it("each tool entry has description as string", () => { + for (const tool of TOOLS) { + expect(typeof tool.description).toBe("string"); + } + }); + + it("each tool entry has inputSchema as object", () => { + for (const tool of TOOLS) { + expect(tool.inputSchema).toBeInstanceOf(Object); + } + }); + + it("each tool entry is a valid McpTool with all required fields", () => { + const isValidMcpTool = (t: unknown): t is McpTool => + typeof t === "object" && + t !== null && + typeof (t as McpTool).name === "string" && + typeof (t as McpTool).description === "string" && + typeof (t as McpTool).inputSchema === "object"; + + for (const tool of TOOLS) { + expect(isValidMcpTool(tool)).toBe(true); + } + }); +}); \ No newline at end of file diff --git a/packages/codeflow-mcp/src/tools/index.ts b/packages/codeflow-mcp/src/tools/index.ts new file mode 100644 index 0000000..69fedd8 --- /dev/null +++ b/packages/codeflow-mcp/src/tools/index.ts @@ -0,0 +1,9 @@ +import type { McpTool } from "@abhinav2203/codeflow-core/schema"; + +export const TOOLS: McpTool[] = [ + { + name: "test_tool", + description: "Prints a paw and 'CF' in ASCII art. Use to verify the MCP server is working.", + inputSchema: { type: "object", properties: {}, required: [] }, + }, +]; diff --git a/packages/codeflow-mcp/tsconfig.json b/packages/codeflow-mcp/tsconfig.json new file mode 100644 index 0000000..7216a0d --- /dev/null +++ b/packages/codeflow-mcp/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "types": ["node", "vitest/globals"], + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test-fixtures"] +} diff --git a/packages/codeflow-mcp/vitest.config.ts b/packages/codeflow-mcp/vitest.config.ts new file mode 100644 index 0000000..02e4a79 --- /dev/null +++ b/packages/codeflow-mcp/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + setupFiles: [], + include: ["src/**/*.test.ts"] + } +}); diff --git a/packages/codeflow-store/TESTING.md b/packages/codeflow-store/TESTING.md new file mode 100644 index 0000000..7cde5df --- /dev/null +++ b/packages/codeflow-store/TESTING.md @@ -0,0 +1,898 @@ +# Testing `codeflow-store` in Isolation + +This guide tests every sub-module of `codeflow-store` from scratch, using only the package itself — no monorepo, no Next.js app, no other CodeFlow packages. + +--- + +## Prerequisites + +You need `node` and `npm` installed. No other dependencies. + +--- + +## Setup + +```bash +# 1. Navigate to the package +cd /Users/abhinavnehra/git/CodeFlow/packages/codeflow-store + +# 2. Install dependencies (if not already installed) +npm install + +# 3. Build (if dist/ is not present or stale) +npm run build + +# 4. Set an isolated store root so tests don't pollute your real ~/.codeflow-store +export CODEFLOW_STORE_ROOT=/tmp/cf-store-test +rm -rf /tmp/cf-store-test +``` + +All commands below assume `CODEFLOW_STORE_ROOT=/tmp/cf-store-test` is set. Every command writes to `/tmp/cf-store-test/` and nowhere else. + +--- + +## Part 0 — Generate a Blueprint (using `codeflow-core`) + +Most tests below use the pre-built `test-fixtures/blueprint.json` files. But you can also generate a fresh blueprint from scratch using `codeflow-core`'s `buildBlueprintGraph`. + +> **Note:** `codeflow-core` must be built first. From the monorepo root: +> ```bash +> npm run build --workspace=@abhinav2203/codeflow-core +> ``` + +`codeflow-core` exposes `buildBlueprintGraph` from `src/analyzer/build.ts`. It accepts either a **PRD text** (markdown describing what to build) or a **repo path** (reverse-mode: analyzes existing TypeScript code to produce a blueprint). + +### 0.1 — Generate from PRD text + +```bash +node -e " +import { buildBlueprintGraph } from '/Users/abhinavnehra/git/CodeFlow/codeflow-core/src/analyzer/build.js'; + +const graph = await buildBlueprintGraph({ + projectName: 'my-app', + mode: 'essential', + prdText: \` +# Screens +- Login screen +- Dashboard + +# APIs +- GET /users +- POST /orders + +# Modules +- auth.ts: handles authentication + -> calls: validateToken + -> reads-state: session + +# Workflows +Login screen -> Dashboard + \` +}); + +console.log(JSON.stringify(graph, null, 2)); +" > /tmp/my-blueprint.json +``` + +Expected output — a full `BlueprintGraph` with `nodes[]`, `edges[]`, and `workflows[]`: +```json +{ + "projectName": "my-app", + "mode": "essential", + "phase": "spec", + "generatedAt": "", + "nodes": [ + { "id": "n1", "kind": "ui-screen", "name": "Login screen", ... }, + { "id": "n2", "kind": "ui-screen", "name": "Dashboard", ... }, + { "id": "n3", "kind": "module", "name": "auth", "path": "auth.ts", ... } + ], + "edges": [ + { "from": "n1", "to": "n3", "kind": "calls", ... } + ], + "workflows": [ + { "name": "login -> dashboard", "steps": ["Login screen", "Dashboard"] } + ], + "warnings": [] +} +``` + +### 0.2 — Generate from an existing TypeScript repo (reverse mode) + +Point `repoPath` at any TypeScript project: + +```bash +mkdir -p /tmp/test-repo/src +cat << 'EOF' > /tmp/test-repo/src/index.ts +export function authenticate(email: string, password: string) { + return { token: 'fake-token', email }; +} +EOF + +node -e " +import { buildBlueprintGraph } from '/Users/abhinavnehra/git/CodeFlow/codeflow-core/src/analyzer/build.js'; + +const graph = await buildBlueprintGraph({ + projectName: 'reverse-test', + mode: 'essential', + repoPath: '/tmp/test-repo/src' +}); + +console.log(JSON.stringify(graph, null, 2)); +" +``` + +This extracts functions, classes, imports, and call relationships from the AST to build a blueprint automatically. + +### 0.3 — Validate a blueprint schema + +Once you have a `blueprint.json`, validate it matches the expected schema: + +```bash +node -e " +import { blueprintGraphSchema } from '/Users/abhinavnehra/git/CodeFlow/codeflow-core/src/schema/index.js'; +import { readFileSync } from 'node:fs'; + +const raw = JSON.parse(readFileSync('/tmp/my-blueprint.json', 'utf8')); +const result = blueprintGraphSchema.safeParse(raw); + +if (result.success) { + console.log('Blueprint is valid!'); +} else { + console.error('Blueprint is invalid:', result.error); + process.exit(1); +} +" +``` + +### What the pipeline looks like + +``` +PRD text OR TypeScript repo + │ + ▼ +┌──────────────────────────────────┐ +│ parsePrd() ← extracts from markdown +│ OR │ +│ analyzeTypeScriptRepo() ← extracts from AST +└──────────────┬───────────────────┘ + │ + ▼ +┌──────────────────────────────────┐ +│ mergeNodes() ← deduplicates +│ createImplicitWorkflowEdges() ← builds edges from "step1 -> step2" syntax +└──────────────┬───────────────────┘ + │ + ▼ + BlueprintGraph + { nodes[], edges[], workflows[] } +``` + +The two modes can be combined — pass both `prdText` and `repoPath` to merge a PRD's planned structure with the reality of existing code. + +--- + +## Part 1 — Unit Tests + +Run the existing test suite: + +```bash +npm test +``` + +Expected output: +``` + ✓ src/session.test.ts > codeflow-store > session > creates a valid session ID + ✓ src/session.test.ts > codeflow-store > risk > assesses export risk for a minimal blueprint + ✓ src/session.test.ts > codeflow-store > risk > flags yolo mode in risk assessment +``` + +All 3 tests should pass. + +--- + +## Part 2 — Type Checking + +```bash +npm run check +``` + +Expected: no output (TypeScript compiles cleanly with no errors). + +--- + +## Part 3 — Risk Assessment + +Risk is the only sub-module that requires **no file I/O** — it purely computes a score from a blueprint + run plan. This is the easiest thing to test end-to-end. + +### 3.1 — Low risk (minimal blueprint, no existing output) + +```bash +node dist/bin/cli.js risk assess test-fixtures/minimal-blueprint.json +``` + +Expected output: +```json +{ + "fingerprint": "", + "outputDir": "/artifacts/test-project", + "riskReport": { + "score": 0, + "level": "low", + "requiresApproval": false, + "factors": [] + }, + "hasExistingOutput": false +} +``` + +### 3.2 — Medium risk (has existing output directory) + +```bash +# Create the output directory to trigger the "overwrite-existing-output" factor +mkdir -p /tmp/cf-store-test/artifacts/test-project +echo "some existing file" > /tmp/cf-store-test/artifacts/test-project/existing.ts + +# Re-run risk assessment — should now flag existing output +node dist/bin/cli.js risk assess test-fixtures/minimal-blueprint.json +``` + +Expected output — `score` should be `4`, `level` should be `"medium"`, and `factors` should contain: +```json +{ + "code": "overwrite-existing-output", + "message": "Output directory .../artifacts/test-project already contains files.", + "score": 4 +} +``` + +### 3.3 — High risk (yolo mode) + +Yolo mode skips approval gates. Create a blueprint with `mode: "yolo"`: + +```bash +cat << 'EOF' > /tmp/yolo-blueprint.json +{ + "projectName": "yolo-project", + "mode": "yolo", + "generatedAt": "2026-01-01T00:00:00.000Z", + "nodes": [], + "edges": [], + "workflows": [], + "warnings": [] +} +EOF + +node dist/bin/cli.js risk assess /tmp/yolo-blueprint.json +``` + +Expected: `score` includes at least `2` from the `yolo-mode` factor. + +### 3.4 — Risk on a real blueprint from codeflow-core + +Generate a real blueprint using Part 0, then assess its risk: + +```bash +# Generate a blueprint (from Part 0) +node -e " +import { buildBlueprintGraph } from '/Users/abhinavnehra/git/CodeFlow/codeflow-core/src/analyzer/build.js'; +const graph = await buildBlueprintGraph({ + projectName: 'real-app', + mode: 'essential', + prdText: \` +# Screens +- Login +- Dashboard + +# Modules +- auth.ts: handles login with email and password +- db.ts: database connection module +- dashboard.ts: renders dashboard data + +# APIs +- GET /api/users +- POST /api/auth/login + +# Workflows +Login -> Dashboard + \` +}); +console.log(JSON.stringify(graph)); +" > /tmp/real-blueprint.json + +# Assess risk on the real blueprint +node dist/bin/cli.js risk assess /tmp/real-blueprint.json +``` + +Expected: risk factors should include `repo-backed-context` (nodes have source refs), `large-task-set` (if tasks ≥ 20), etc. A small blueprint should score low. + +--- + +## Part 4 — Session Management + +The session store persists a `PersistedSession` (blueprint graph + run plan) to disk. + +### 4.1 — Initialize a new session + +```bash +node dist/bin/cli.js session init "test-project" +``` + +Expected output: +```json +{ + "sessionId": "", + "projectName": "test-project", + "updatedAt": "", + "repoPath": null, + "graph": { + "projectName": "test-project", + "mode": "essential", + "nodes": [], + "edges": [], + "workflows": [], + "warnings": [] + }, + "runPlan": { + "tasks": [], + "batches": [] + }, + "lastRiskReport": null, + "lastExportResult": null, + "lastExecutionReport": null, + "approvalIds": [] +} +``` + +Verify it was written to disk: +```bash +cat /tmp/cf-store-test/sessions/test-project/latest.json +# → should match the output above (same sessionId, etc.) +``` + +### 4.2 — Load the latest session + +```bash +node dist/bin/cli.js session last "test-project" +``` + +Expected: returns the same session as step 4.1 (sessionId, graph, etc.). + +### 4.3 — Session survives a process restart (load from disk) + +```bash +# Run a new node process — no in-memory state carried over +node dist/bin/cli.js session last "test-project" +``` + +Expected: same session data, proving it was persisted to disk and re-loaded correctly. + +### 4.4 — Session with a real repo path + +```bash +mkdir -p /tmp/test-repo +node dist/bin/cli.js session init "repo-project" +# Note: the CLI's "session init" currently creates a blank session. +# To test with a repo path, manually edit the session JSON: +echo '{"sessionId":"test","projectName":"repo-project","repoPath":"/tmp/test-repo","graph":{...}}' > /tmp/cf-store-test/sessions/repo-project/latest.json +``` + +--- + +## Part 5 — Approval Workflow + +The approval store manages export requests that require human sign-off. + +### 5.1 — Create an approval record + +You need to construct the full approval record manually since there's no CLI subcommand for creation (it's done by the export flow): + +```bash +node -e " +const { createApprovalRecord } = await import('./dist/approval/index.js'); + +const record = await createApprovalRecord({ + projectName: 'test-project', + fingerprint: 'abc123fingerprint', + outputDir: '/tmp/test-output', + runPlan: { generatedAt: new Date().toISOString(), tasks: [], batches: [], warnings: [] }, + riskReport: { score: 6, level: 'high', requiresApproval: true, factors: [] } +}); + +console.log(JSON.stringify(record, null, 2)); +" +``` + +Expected output: +```json +{ + "id": "", + "action": "export", + "projectName": "test-project", + "status": "pending", + "fingerprint": "abc123fingerprint", + "requestedAt": "", + "outputDir": "/tmp/test-output", + "runPlan": {...}, + "riskReport": {...} +} +``` + +Verify it was written to disk: +```bash +cat /tmp/cf-store-test/approvals/.json +``` + +### 5.2 — Get an approval record + +```bash +# Replace with the id from step 5.1 +node dist/bin/cli.js approval get +``` + +Expected: returns the same approval record. + +### 5.3 — Approve the record + +```bash +node dist/bin/cli.js approval approve +``` + +Expected output — `status` changes from `"pending"` to `"approved"`, and `approvedAt` is set: +```json +{ + "id": "", + "status": "approved", + "approvedAt": "", + ... +} +``` + +Verify the file was updated on disk: +```bash +cat /tmp/cf-store-test/approvals/.json +# → status should be "approved" +``` + +### 5.4 — Approval for non-existent record + +```bash +node dist/bin/cli.js approval get does-not-exist-id +``` + +Expected: exits with code 1 and error message `Approval does-not-exist-id was not found.` + +--- + +## Part 6 — Checkpoints + +Checkpoints copy an entire project directory to a timestamped location. They let you roll back to a known-good state. + +### 6.1 — Create a checkpoint + +```bash +# First, create a "project" directory with some files +mkdir -p /tmp/my-project/src +echo "console.log('hello')" > /tmp/my-project/src/index.ts +echo "const x = 1" > /tmp/my-project/src/utils.ts + +# Create a checkpoint +CHECKPOINT_ID="checkpoint-$(date +%s)" +node dist/bin/cli.js checkpoint create "$CHECKPOINT_ID" /tmp/my-project +``` + +Expected output: +```json +{ + "checkpointDir": "/tmp/cf-store-test/checkpoints/" +} +``` + +Verify the directory was copied: +```bash +ls /tmp/cf-store-test/checkpoints//src/ +# → index.ts utils.ts +cat /tmp/cf-store-test/checkpoints//src/index.ts +# → console.log('hello') +``` + +### 6.2 — Modify the project, then restore the checkpoint + +```bash +# Modify the project +echo "console.log('modified')" > /tmp/my-project/src/index.ts + +# Verify it's changed +cat /tmp/my-project/src/index.ts +# → console.log('modified') + +# Restore the checkpoint (copy it back) +cp -r /tmp/cf-store-test/checkpoints//* /tmp/my-project/ + +# Verify the original is back +cat /tmp/my-project/src/index.ts +# → console.log('hello') +``` + +--- + +## Part 7 — Run Records + +Run records store the result of every execution of a blueprint. + +### 7.1 — Save a run record + +```bash +node -e " +const { saveRunRecord, createRunId } = await import('./dist/run/index.js'); + +const runId = createRunId(); +const record = { + id: runId, + projectName: 'test-project', + sessionId: 'test-session-123', + status: 'success', + startedAt: new Date().toISOString(), + completedAt: new Date().toISOString(), + tasks: [ + { id: 'task-1', nodeId: 'n1', status: 'success', durationMs: 120 } + ], + batches: [ + { index: 0, taskIds: ['task-1'], status: 'completed' } + ], + artifacts: [], + errors: [] +}; + +await saveRunRecord(record); +console.log('Saved run:', runId); +" +``` + +Verify it was written: +```bash +cat /tmp/cf-store-test/runs/.json +# → should contain the run record +``` + +### 7.2 — List all run records + +```bash +node dist/bin/cli.js run list +``` + +Expected: returns a JSON array of filenames like `[".json", ".json"]`. + +--- + +## Part 8 — Branch Management + +Branches let you maintain multiple named variants of a blueprint simultaneously. + +### 8.1 — Save a named branch + +```bash +node -e " +const { saveBranch } = await import('./dist/branch/index.js'); + +const branch = { + id: 'feature-auth', + projectName: 'test-project', + name: 'feature-auth', + description: 'Authentication module variant', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + graph: { + projectName: 'test-project', + mode: 'essential', + nodes: [ + { id: 'n1', kind: 'screen', summary: 'Login screen' } + ], + edges: [], + workflows: [], + warnings: [] + }, + metadata: {} +}; + +await saveBranch(branch); +console.log('Saved branch'); +" +``` + +Verify it was written: +```bash +cat /tmp/cf-store-test/branches/test-project/feature-auth.json +``` + +### 8.2 — List all branches + +```bash +node dist/bin/cli.js branch list test-project +``` + +Expected: returns an array with the `feature-auth` branch. + +### 8.3 — Load a specific branch + +```bash +node -e " +const { loadBranch } = await import('./dist/branch/index.js'); +const branch = await loadBranch('test-project', 'feature-auth'); +console.log(JSON.stringify(branch, null, 2)); +" +``` + +Expected: returns the branch with `id: "feature-auth"`. + +### 8.4 — Delete a branch + +```bash +node -e " +const { deleteBranch } = await import('./dist/branch/index.js'); +await deleteBranch('test-project', 'feature-auth'); +console.log('Deleted'); +" + +# Verify it's gone +node dist/bin/cli.js branch list test-project +# → should return [] +``` + +--- + +## Part 9 — Observability + +Observability stores merged trace spans and logs from execution runs. + +### 9.1 — Merge spans and logs into a snapshot + +```bash +# Create sample spans +cat << 'EOF' > /tmp/spans.json +[ + { + "name": "auth.validate", + "spanId": "s1", + "traceId": "t1", + "startTime": "2026-01-01T10:00:00.000Z", + "endTime": "2026-01-01T10:00:01.000Z", + "status": "ok", + "attributes": {} + } +] +EOF + +# Create sample logs +cat << 'EOF' > /tmp/logs.json +[ + { "level": "info", "message": "Server started", "timestamp": "2026-01-01T10:00:00.000Z" } +] +EOF + +# Merge into observability snapshot +node dist/bin/cli.js observability merge test-project /tmp/spans.json /tmp/logs.json +``` + +Expected output: +```json +{ + "projectName": "test-project", + "updatedAt": "", + "spans": [...], + "logs": [...] +} +``` + +### 9.2 — Load the observability snapshot + +```bash +node dist/bin/cli.js observability get test-project +``` + +Expected: returns the same snapshot from step 9.1. + +### 9.3 — Merging appends, not replaces + +```bash +# Add more spans +cat << 'EOF' > /tmp/spans2.json +[ + { + "name": "auth.login", + "spanId": "s2", + "traceId": "t1", + "startTime": "2026-01-01T10:00:02.000Z", + "endTime": "2026-01-01T10:00:03.000Z", + "status": "ok", + "attributes": {} + } +] +EOF + +cat << 'EOF' > /tmp/logs2.json +[] +EOF + +node dist/bin/cli.js observability merge test-project /tmp/spans2.json /tmp/logs2.json + +# Reload and verify both spans are present +node dist/bin/cli.js observability get test-project +# → spans should contain BOTH s1 and s2 +``` + +### 9.4 — Observability caps at 500 items + +```bash +# Create 600 spans +node -e " +const { mergeObservabilitySnapshot } = await import('./dist/observability/index.js'); +const spans = Array.from({ length: 600 }, (_, i) => ({ + name: 'span-' + i, + spanId: 's' + i, + traceId: 't1', + startTime: new Date().toISOString(), + endTime: new Date().toISOString(), + status: 'ok', + attributes: {} +})); +await mergeObservabilitySnapshot({ projectName: 'test-project', spans, logs: [] }); +console.log('done'); +" + +node dist/bin/cli.js observability get test-project +# → spans.length should be 500 (not 600) +``` + +--- + +## Part 10 — Store Root Environment Variable + +Verify that `CODEFLOW_STORE_ROOT` is respected and that multiple projects are fully isolated: + +```bash +# Use a fresh store root +export CODEFLOW_STORE_ROOT=/tmp/cf-store-isolated +rm -rf /tmp/cf-store-isolated + +# Create a session for project A +node dist/bin/cli.js session init "project-a" +# → stored in /tmp/cf-store-isolated/sessions/project-a/latest.json + +# Create a session for project B +node dist/bin/cli.js session init "project-b" +# → stored in /tmp/cf-store-isolated/sessions/project-b/latest.json + +# Verify they are separate +ls /tmp/cf-store-isolated/sessions/ +# → project-a project-b + +# Each session should only return its own project +node dist/bin/cli.js session last "project-a" +node dist/bin/cli.js session last "project-b" +# → each returns the correct project, no mixing +``` + +--- + +## Part 11 — Full End-to-End Scenario + +Simulate a real-world workflow using all sub-modules: + +```bash +export CODEFLOW_STORE_ROOT=/tmp/cf-e2e +rm -rf /tmp/cf-e2e + +PROJECT="my-app" +mkdir -p /tmp/$PROJECT/src + +# Step 1 — Initialize session +SESSION=$(node dist/bin/cli.js session init "$PROJECT") +echo "Session created" + +# Step 2 — Do some work (create a checkpoint before) +CHECKPOINT_ID="pre-refactor-$(date +%s)" +cp -r /tmp/$PROJECT /tmp/$PROJECT-backup # simulate the "project dir" for checkpoint +node dist/bin/cli.js checkpoint create "$CHECKPOINT_ID" /tmp/$PROJECT-backup + +# Step 3 — Assess risk +node dist/bin/cli.js risk assess test-fixtures/sample-blueprint.json /tmp/$PROJECT/artifacts + +# Step 4 — Verify no approvals needed for low risk +# (sample-blueprint has no existing output, so risk should be low) + +# Step 5 — Save a run record +node -e " +const { saveRunRecord, createRunId } = await import('./dist/run/index.js'); +await saveRunRecord({ + id: createRunId(), + projectName: '$PROJECT', + sessionId: 'sess-1', + status: 'success', + startedAt: new Date().toISOString(), + completedAt: new Date().toISOString(), + tasks: [{ id: 't1', nodeId: 'n1', status: 'success', durationMs: 50 }], + batches: [{ index: 0, taskIds: ['t1'], status: 'completed' }], + artifacts: [], + errors: [] +}); +" + +# Step 6 — Create a branch +node -e " +const { saveBranch } = await import('./dist/branch/index.js'); +await saveBranch({ + id: 'v2', + projectName: '$PROJECT', + name: 'v2', + description: 'Architecture variant 2', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + graph: { projectName: '$PROJECT', mode: 'essential', nodes: [], edges: [], workflows: [], warnings: [] }, + metadata: {} +}); +" + +# Step 7 — List branches +node dist/bin/cli.js branch list "$PROJECT" +# → should contain v2 + +# Step 8 — Verify all run records +node dist/bin/cli.js run list +# → should have at least one run record + +echo "End-to-end scenario complete!" +``` + +--- + +## File System State After All Tests + +After running all tests with `CODEFLOW_STORE_ROOT=/tmp/cf-store-test`, the directory should look like: + +``` +/tmp/cf-store-test/ +├── sessions/ +│ ├── test-project/latest.json +│ └── repo-project/latest.json +├── approvals/ +│ └── .json +├── checkpoints/ +│ └── / +│ └── src/ +│ ├── index.ts +│ └── utils.ts +├── runs/ +│ └── .json +├── observability/ +│ └── test-project.json +└── branches/ + └── test-project/ + └── feature-auth.json +``` + +--- + +## Troubleshooting + +### "No session found for project: test-project" + +You forgot to set `CODEFLOW_STORE_ROOT`. Sessions are being written to `~/.codeflow-store/` instead of your test dir. Run: +```bash +export CODEFLOW_STORE_ROOT=/tmp/cf-store-test +``` + +### "dist/bin/cli.js: not found" + +Run: +```bash +npm run build +``` + +### TypeScript errors on `npm run check` + +Run build from the monorepo root — `codeflow-store` depends on `@abhinav2203/codeflow-core` which must be built first: +```bash +cd /Users/abhinavnehra/git/CodeFlow +npm run build --workspace=@abhinav2203/codeflow-core +npm run build --workspace=@abhinav2203/codeflow-store +``` diff --git a/packages/codeflow-store/dist/approval/index.d.ts b/packages/codeflow-store/dist/approval/index.d.ts new file mode 100644 index 0000000..0d1d69b --- /dev/null +++ b/packages/codeflow-store/dist/approval/index.d.ts @@ -0,0 +1,12 @@ +import type { ApprovalRecord, RiskReport, RunPlan } from "@abhinav2203/codeflow-core/schema"; +export declare const createApprovalId: () => string; +export declare const createApprovalRecord: ({ projectName, fingerprint, outputDir, runPlan, riskReport }: { + projectName: string; + fingerprint: string; + outputDir: string; + runPlan: RunPlan; + riskReport: RiskReport; +}) => Promise; +export declare const getApprovalRecord: (approvalId: string) => Promise; +export declare const approveRecord: (approvalId: string) => Promise; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/approval/index.d.ts.map b/packages/codeflow-store/dist/approval/index.d.ts.map new file mode 100644 index 0000000..aca6888 --- /dev/null +++ b/packages/codeflow-store/dist/approval/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/approval/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,mCAAmC,CAAC;AAa7F,eAAO,MAAM,gBAAgB,QAAO,MAA6B,CAAC;AAElE,eAAO,MAAM,oBAAoB,GAAU,8DAMxC;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,UAAU,CAAC;CACxB,KAAG,OAAO,CAAC,cAAc,CAezB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAU,YAAY,MAAM,KAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAOzF,CAAC;AAEF,eAAO,MAAM,aAAa,GAAU,YAAY,MAAM,KAAG,OAAO,CAAC,cAAc,CAc9E,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/approval/index.js b/packages/codeflow-store/dist/approval/index.js new file mode 100644 index 0000000..cda73e5 --- /dev/null +++ b/packages/codeflow-store/dist/approval/index.js @@ -0,0 +1,51 @@ +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { approvalPath } from "../shared/utils.js"; +const ensureDir = async (dirPath) => { + await fs.mkdir(dirPath, { recursive: true }); +}; +const writeApprovalFile = async (record) => { + const filePath = approvalPath(record.id); + await ensureDir(path.dirname(filePath)); + await fs.writeFile(filePath, `${JSON.stringify(record, null, 2)}\n`, "utf8"); +}; +export const createApprovalId = () => crypto.randomUUID(); +export const createApprovalRecord = async ({ projectName, fingerprint, outputDir, runPlan, riskReport }) => { + const record = { + id: createApprovalId(), + action: "export", + projectName, + status: "pending", + fingerprint, + requestedAt: new Date().toISOString(), + outputDir, + runPlan, + riskReport + }; + await writeApprovalFile(record); + return record; +}; +export const getApprovalRecord = async (approvalId) => { + try { + const content = await fs.readFile(approvalPath(approvalId), "utf8"); + return JSON.parse(content); + } + catch { + return null; + } +}; +export const approveRecord = async (approvalId) => { + const existing = await getApprovalRecord(approvalId); + if (!existing) { + throw new Error(`Approval ${approvalId} was not found.`); + } + const approved = { + ...existing, + status: "approved", + approvedAt: new Date().toISOString() + }; + await writeApprovalFile(approved); + return approved; +}; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/approval/index.js.map b/packages/codeflow-store/dist/approval/index.js.map new file mode 100644 index 0000000..9dfbef0 --- /dev/null +++ b/packages/codeflow-store/dist/approval/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/approval/index.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,MAAM,SAAS,GAAG,KAAK,EAAE,OAAe,EAAiB,EAAE;IACzD,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAC/C,CAAC,CAAC;AAEF,MAAM,iBAAiB,GAAG,KAAK,EAAE,MAAsB,EAAiB,EAAE;IACxE,MAAM,QAAQ,GAAG,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACzC,MAAM,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IACxC,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC/E,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAW,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;AAElE,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,EAAE,EACzC,WAAW,EACX,WAAW,EACX,SAAS,EACT,OAAO,EACP,UAAU,EAOX,EAA2B,EAAE;IAC5B,MAAM,MAAM,GAAmB;QAC7B,EAAE,EAAE,gBAAgB,EAAE;QACtB,MAAM,EAAE,QAAQ;QAChB,WAAW;QACX,MAAM,EAAE,SAAS;QACjB,WAAW;QACX,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACrC,SAAS;QACT,OAAO;QACP,UAAU;KACX,CAAC;IAEF,MAAM,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAChC,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,EAAE,UAAkB,EAAkC,EAAE;IAC5F,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,YAAY,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,CAAC;QACpE,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAmB,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAAE,UAAkB,EAA2B,EAAE;IACjF,MAAM,QAAQ,GAAG,MAAM,iBAAiB,CAAC,UAAU,CAAC,CAAC;IACrD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,YAAY,UAAU,iBAAiB,CAAC,CAAC;IAC3D,CAAC;IAED,MAAM,QAAQ,GAAmB;QAC/B,GAAG,QAAQ;QACX,MAAM,EAAE,UAAU;QAClB,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACrC,CAAC;IAEF,MAAM,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IAClC,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/bin/cli.d.ts b/packages/codeflow-store/dist/bin/cli.d.ts new file mode 100644 index 0000000..faaadd5 --- /dev/null +++ b/packages/codeflow-store/dist/bin/cli.d.ts @@ -0,0 +1,3 @@ +#!/usr/bin/env node +export {}; +//# sourceMappingURL=cli.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/bin/cli.d.ts.map b/packages/codeflow-store/dist/bin/cli.d.ts.map new file mode 100644 index 0000000..784b943 --- /dev/null +++ b/packages/codeflow-store/dist/bin/cli.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/bin/cli.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/packages/codeflow-store/dist/bin/cli.js b/packages/codeflow-store/dist/bin/cli.js new file mode 100644 index 0000000..e160212 --- /dev/null +++ b/packages/codeflow-store/dist/bin/cli.js @@ -0,0 +1,161 @@ +#!/usr/bin/env node +import { loadLatestSession, upsertSession, createSessionId } from "../session/index.js"; +import { getApprovalRecord, approveRecord } from "../approval/index.js"; +import { createCheckpointIfNeeded } from "../checkpoint/index.js"; +import { assessExportRisk } from "../risk/index.js"; +import { loadBranches } from "../branch/index.js"; +import { loadObservabilitySnapshot, mergeObservabilitySnapshot } from "../observability/index.js"; +const USAGE = `CodeFlow Store CLI + +Usage: + codeflow-store session init Create a new session + codeflow-store session last Load the latest session + codeflow-store checkpoint create + codeflow-store approval approve Approve a pending approval + codeflow-store approval get Get approval record + codeflow-store risk assess [outputDir] Assess export risk + codeflow-store branch list List branches + codeflow-store branch switch [graphJson] + codeflow-store run list List run records + codeflow-store observability get Get observability snapshot + codeflow-store observability merge + codeflow-store --help Show this help`; +const fail = (msg) => { + console.error(msg); + process.exit(1); +}; +const parseJsonFile = async (filePath) => { + const { readFileSync } = await import("node:fs"); + return JSON.parse(readFileSync(filePath, "utf8")); +}; +const main = async () => { + const args = process.argv.slice(2); + if (args[0] === "--help" || args[0] === "-h" || args.length === 0) { + console.log(USAGE); + process.exit(0); + } + const [command, subcommand, ...rest] = args; + switch (`${command} ${subcommand}`) { + case "session init": { + const projectName = rest[0] ?? fail("projectName required"); + const sessionId = createSessionId(); + const graph = { + projectName, + mode: "essential", + generatedAt: new Date().toISOString(), + nodes: [], + edges: [], + workflows: [], + warnings: [] + }; + const runPlan = { + generatedAt: new Date().toISOString(), + tasks: [], + batches: [], + warnings: [] + }; + const session = await upsertSession({ sessionId, graph, runPlan }); + console.log(JSON.stringify(session, null, 2)); + break; + } + case "session last": { + const projectName = rest[0] ?? fail("projectName required"); + const session = await loadLatestSession(projectName); + if (!session) { + fail(`No session found for project: ${projectName}`); + } + console.log(JSON.stringify(session, null, 2)); + break; + } + case "checkpoint create": { + const checkpointId = rest[0] ?? fail("checkpointId required"); + const outputDir = rest[1] ?? fail("outputDir required"); + const runId = rest[2] ?? fail("runId required"); + const dir = await createCheckpointIfNeeded(outputDir, runId); + console.log(JSON.stringify({ checkpointDir: dir }, null, 2)); + break; + } + case "approval approve": { + const approvalId = rest[0] ?? fail("approvalId required"); + const approval = await approveRecord(approvalId); + console.log(JSON.stringify(approval, null, 2)); + break; + } + case "approval get": { + const approvalId = rest[0] ?? fail("approvalId required"); + const approval = await getApprovalRecord(approvalId); + if (!approval) { + fail(`Approval not found: ${approvalId}`); + } + console.log(JSON.stringify(approval, null, 2)); + break; + } + case "risk assess": { + const graphJson = rest[0] ?? fail("graph JSON file required"); + const outputDir = rest[1]; + const graph = await parseJsonFile(graphJson); + const runPlan = { + generatedAt: new Date().toISOString(), + tasks: [], + batches: [], + warnings: [] + }; + const assessment = await assessExportRisk(graph, runPlan, outputDir); + console.log(JSON.stringify(assessment, null, 2)); + break; + } + case "branch list": { + const projectName = rest[0] ?? fail("projectName required"); + const branches = await loadBranches(projectName); + console.log(JSON.stringify(branches, null, 2)); + break; + } + case "branch switch": { + // Note: branch switching is managed via the branches API routes. + // This command is a placeholder for future branch-switch functionality. + console.log(JSON.stringify({ message: "Branch switching is handled via the API. Use POST /api/branches to create/switch branches." }, null, 2)); + break; + } + case "run list": { + const { readdir } = await import("node:fs/promises"); + const { getStoreRoot } = await import("../shared/utils.js"); + const runsDir = getStoreRoot() + "/runs"; + let files = []; + try { + files = await readdir(runsDir); + } + catch { + files = []; + } + console.log(JSON.stringify(files, null, 2)); + break; + } + case "observability get": { + const projectName = rest[0] ?? fail("projectName required"); + const snapshot = await loadObservabilitySnapshot(projectName); + if (!snapshot) { + fail(`No observability snapshot found for project: ${projectName}`); + } + console.log(JSON.stringify(snapshot, null, 2)); + break; + } + case "observability merge": { + const projectName = rest[0] ?? fail("projectName required"); + const spansJson = rest[1] ?? fail("spans JSON file required"); + const logsJson = rest[2] ?? fail("logs JSON file required"); + const spans = await parseJsonFile(spansJson); + const logs = await parseJsonFile(logsJson); + const snapshot = await mergeObservabilitySnapshot({ + projectName, + spans: spans, + logs: logs + }); + console.log(JSON.stringify(snapshot, null, 2)); + break; + } + default: + fail(`Unknown command: ${command} ${subcommand}\n${USAGE}`); + } +}; +main().catch((err) => fail(err instanceof Error ? err.message : String(err))); +//# sourceMappingURL=cli.js.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/bin/cli.js.map b/packages/codeflow-store/dist/bin/cli.js.map new file mode 100644 index 0000000..1920144 --- /dev/null +++ b/packages/codeflow-store/dist/bin/cli.js.map @@ -0,0 +1 @@ +{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/bin/cli.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/branch/index.d.ts b/packages/codeflow-store/dist/branch/index.d.ts new file mode 100644 index 0000000..19be1ad --- /dev/null +++ b/packages/codeflow-store/dist/branch/index.d.ts @@ -0,0 +1,6 @@ +import type { GraphBranch } from "@abhinav2203/codeflow-core/schema"; +export declare const saveBranch: (branch: GraphBranch) => Promise; +export declare const loadBranch: (projectName: string, branchId: string) => Promise; +export declare const loadBranches: (projectName: string) => Promise; +export declare const deleteBranch: (projectName: string, branchId: string) => Promise; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/branch/index.d.ts.map b/packages/codeflow-store/dist/branch/index.d.ts.map new file mode 100644 index 0000000..fbced24 --- /dev/null +++ b/packages/codeflow-store/dist/branch/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/branch/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAOrE,eAAO,MAAM,UAAU,GAAU,QAAQ,WAAW,KAAG,OAAO,CAAC,IAAI,CAIlE,CAAC;AAEF,eAAO,MAAM,UAAU,GACrB,aAAa,MAAM,EACnB,UAAU,MAAM,KACf,OAAO,CAAC,WAAW,GAAG,IAAI,CAO5B,CAAC;AAEF,eAAO,MAAM,YAAY,GAAU,aAAa,MAAM,KAAG,OAAO,CAAC,WAAW,EAAE,CAwB7E,CAAC;AAEF,eAAO,MAAM,YAAY,GAAU,aAAa,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,IAAI,CAMtF,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/branch/index.js b/packages/codeflow-store/dist/branch/index.js new file mode 100644 index 0000000..f9b2665 --- /dev/null +++ b/packages/codeflow-store/dist/branch/index.js @@ -0,0 +1,52 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { branchDirForProject, branchPath } from "../shared/utils.js"; +const ensureDir = async (dirPath) => { + await fs.mkdir(dirPath, { recursive: true }); +}; +export const saveBranch = async (branch) => { + const filePath = branchPath(branch.projectName, branch.id); + await ensureDir(path.dirname(filePath)); + await fs.writeFile(filePath, `${JSON.stringify(branch, null, 2)}\n`, "utf8"); +}; +export const loadBranch = async (projectName, branchId) => { + try { + const content = await fs.readFile(branchPath(projectName, branchId), "utf8"); + return JSON.parse(content); + } + catch { + return null; + } +}; +export const loadBranches = async (projectName) => { + const dir = branchDirForProject(projectName); + try { + const entries = await fs.readdir(dir); + const branches = await Promise.all(entries + .filter((entry) => entry.endsWith(".json")) + .map(async (entry) => { + try { + const content = await fs.readFile(path.join(dir, entry), "utf8"); + return JSON.parse(content); + } + catch { + return null; + } + })); + return branches + .filter((branch) => branch !== null) + .sort((left, right) => left.createdAt.localeCompare(right.createdAt)); + } + catch { + return []; + } +}; +export const deleteBranch = async (projectName, branchId) => { + try { + await fs.unlink(branchPath(projectName, branchId)); + } + catch { + // Ignore already-removed branches. + } +}; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/branch/index.js.map b/packages/codeflow-store/dist/branch/index.js.map new file mode 100644 index 0000000..b175338 --- /dev/null +++ b/packages/codeflow-store/dist/branch/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/branch/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErE,MAAM,SAAS,GAAG,KAAK,EAAE,OAAe,EAAiB,EAAE;IACzD,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAC/C,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAAE,MAAmB,EAAiB,EAAE;IACrE,MAAM,QAAQ,GAAG,UAAU,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IAC3D,MAAM,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IACxC,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC/E,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAC7B,WAAmB,EACnB,QAAgB,EACa,EAAE;IAC/B,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,WAAW,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC;QAC7E,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAgB,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAAE,WAAmB,EAA0B,EAAE;IAChF,MAAM,GAAG,GAAG,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAE7C,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACtC,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,CAChC,OAAO;aACJ,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;aAC1C,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;YACnB,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC;gBACjE,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAgB,CAAC;YAC5C,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC,CAAC,CACL,CAAC;QAEF,OAAO,QAAQ;aACZ,MAAM,CAAC,CAAC,MAAM,EAAyB,EAAE,CAAC,MAAM,KAAK,IAAI,CAAC;aAC1D,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;IAC1E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAAE,WAAmB,EAAE,QAAgB,EAAiB,EAAE;IACzF,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,mCAAmC;IACrC,CAAC;AACH,CAAC,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/checkpoint/index.d.ts b/packages/codeflow-store/dist/checkpoint/index.d.ts new file mode 100644 index 0000000..3123107 --- /dev/null +++ b/packages/codeflow-store/dist/checkpoint/index.d.ts @@ -0,0 +1,2 @@ +export declare const createCheckpointIfNeeded: (targetDir: string, checkpointId: string) => Promise; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/checkpoint/index.d.ts.map b/packages/codeflow-store/dist/checkpoint/index.d.ts.map new file mode 100644 index 0000000..9935eab --- /dev/null +++ b/packages/codeflow-store/dist/checkpoint/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/checkpoint/index.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,wBAAwB,GACnC,WAAW,MAAM,EACjB,cAAc,MAAM,KACnB,OAAO,CAAC,MAAM,GAAG,SAAS,CAuB5B,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/checkpoint/index.js b/packages/codeflow-store/dist/checkpoint/index.js new file mode 100644 index 0000000..048af85 --- /dev/null +++ b/packages/codeflow-store/dist/checkpoint/index.js @@ -0,0 +1,27 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { checkpointPath } from "../shared/utils.js"; +const ensureDir = async (dirPath) => { + await fs.mkdir(dirPath, { recursive: true }); +}; +export const createCheckpointIfNeeded = async (targetDir, checkpointId) => { + const exists = await fs + .stat(targetDir) + .then((stats) => stats.isDirectory()) + .catch(() => false); + if (!exists) { + return undefined; + } + const entries = await fs.readdir(targetDir); + if (entries.length === 0) { + return undefined; + } + const checkpointDir = checkpointPath(checkpointId); + await ensureDir(path.dirname(checkpointDir)); + await fs.cp(targetDir, checkpointDir, { + recursive: true, + force: true + }); + return checkpointDir; +}; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/checkpoint/index.js.map b/packages/codeflow-store/dist/checkpoint/index.js.map new file mode 100644 index 0000000..2933405 --- /dev/null +++ b/packages/codeflow-store/dist/checkpoint/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/checkpoint/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,MAAM,SAAS,GAAG,KAAK,EAAE,OAAe,EAAiB,EAAE;IACzD,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAC/C,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,wBAAwB,GAAG,KAAK,EAC3C,SAAiB,EACjB,YAAoB,EACS,EAAE;IAC/B,MAAM,MAAM,GAAG,MAAM,EAAE;SACpB,IAAI,CAAC,SAAS,CAAC;SACf,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;SACpC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;IAEtB,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAC5C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,aAAa,GAAG,cAAc,CAAC,YAAY,CAAC,CAAC;IACnD,MAAM,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;IAC7C,MAAM,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,aAAa,EAAE;QACpC,SAAS,EAAE,IAAI;QACf,KAAK,EAAE,IAAI;KACZ,CAAC,CAAC;IAEH,OAAO,aAAa,CAAC;AACvB,CAAC,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/observability/index.d.ts b/packages/codeflow-store/dist/observability/index.d.ts new file mode 100644 index 0000000..e853b2b --- /dev/null +++ b/packages/codeflow-store/dist/observability/index.d.ts @@ -0,0 +1,9 @@ +import type { BlueprintGraph, ObservabilitySnapshot } from "@abhinav2203/codeflow-core/schema"; +export declare const loadObservabilitySnapshot: (projectName: string) => Promise; +export declare const mergeObservabilitySnapshot: ({ projectName, spans, logs, graph }: { + projectName: string; + spans: ObservabilitySnapshot["spans"]; + logs: ObservabilitySnapshot["logs"]; + graph?: BlueprintGraph; +}) => Promise; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/observability/index.d.ts.map b/packages/codeflow-store/dist/observability/index.d.ts.map new file mode 100644 index 0000000..8225572 --- /dev/null +++ b/packages/codeflow-store/dist/observability/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/observability/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAiB/F,eAAO,MAAM,yBAAyB,GACpC,aAAa,MAAM,KAClB,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAOtC,CAAC;AAEF,eAAO,MAAM,0BAA0B,GAAU,qCAK9C;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,qBAAqB,CAAC,OAAO,CAAC,CAAC;IACtC,IAAI,EAAE,qBAAqB,CAAC,MAAM,CAAC,CAAC;IACpC,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB,KAAG,OAAO,CAAC,qBAAqB,CAYhC,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/observability/index.js b/packages/codeflow-store/dist/observability/index.js new file mode 100644 index 0000000..f05a730 --- /dev/null +++ b/packages/codeflow-store/dist/observability/index.js @@ -0,0 +1,34 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { blueprintGraphSchema } from "@abhinav2203/codeflow-core/schema"; +import { observabilityPath } from "../shared/utils.js"; +const ensureDir = async (dirPath) => { + await fs.mkdir(dirPath, { recursive: true }); +}; +const writeSnapshotFile = async (projectName, snapshot) => { + const filePath = observabilityPath(projectName); + await ensureDir(path.dirname(filePath)); + await fs.writeFile(filePath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8"); +}; +export const loadObservabilitySnapshot = async (projectName) => { + try { + const content = await fs.readFile(observabilityPath(projectName), "utf8"); + return JSON.parse(content); + } + catch { + return null; + } +}; +export const mergeObservabilitySnapshot = async ({ projectName, spans, logs, graph }) => { + const existing = await loadObservabilitySnapshot(projectName); + const snapshot = { + projectName, + updatedAt: new Date().toISOString(), + spans: [...(existing?.spans ?? []), ...spans].slice(-500), + logs: [...(existing?.logs ?? []), ...logs].slice(-500), + graph: graph ? blueprintGraphSchema.parse(graph) : existing?.graph + }; + await writeSnapshotFile(projectName, snapshot); + return snapshot; +}; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/observability/index.js.map b/packages/codeflow-store/dist/observability/index.js.map new file mode 100644 index 0000000..0f415a2 --- /dev/null +++ b/packages/codeflow-store/dist/observability/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/observability/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,SAAS,GAAG,KAAK,EAAE,OAAe,EAAiB,EAAE;IACzD,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAC/C,CAAC,CAAC;AAEF,MAAM,iBAAiB,GAAG,KAAK,EAC7B,WAAmB,EACnB,QAA+B,EAChB,EAAE;IACjB,MAAM,QAAQ,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;IAChD,MAAM,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IACxC,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AACjF,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,yBAAyB,GAAG,KAAK,EAC5C,WAAmB,EACoB,EAAE;IACzC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,iBAAiB,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,CAAC;QAC1E,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAA0B,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,0BAA0B,GAAG,KAAK,EAAE,EAC/C,WAAW,EACX,KAAK,EACL,IAAI,EACJ,KAAK,EAMN,EAAkC,EAAE;IACnC,MAAM,QAAQ,GAAG,MAAM,yBAAyB,CAAC,WAAW,CAAC,CAAC;IAC9D,MAAM,QAAQ,GAA0B;QACtC,WAAW;QACX,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,KAAK,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAC,EAAE,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC;QACzD,IAAI,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC;QACtD,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,oBAAoB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,KAAK;KACnE,CAAC;IAEF,MAAM,iBAAiB,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAC/C,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/risk/index.d.ts b/packages/codeflow-store/dist/risk/index.d.ts new file mode 100644 index 0000000..6939f8e --- /dev/null +++ b/packages/codeflow-store/dist/risk/index.d.ts @@ -0,0 +1,9 @@ +import type { BlueprintGraph, RiskReport, RunPlan } from "@abhinav2203/codeflow-core/schema"; +export type ExportRiskAssessment = { + fingerprint: string; + outputDir: string; + riskReport: RiskReport; + hasExistingOutput: boolean; +}; +export declare const assessExportRisk: (graph: BlueprintGraph, runPlan: RunPlan, outputDir?: string) => Promise; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/risk/index.d.ts.map b/packages/codeflow-store/dist/risk/index.d.ts.map new file mode 100644 index 0000000..b93b727 --- /dev/null +++ b/packages/codeflow-store/dist/risk/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/risk/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAc,UAAU,EAAE,OAAO,EAAE,MAAM,mCAAmC,CAAC;AASzG,MAAM,MAAM,oBAAoB,GAAG;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,UAAU,CAAC;IACvB,iBAAiB,EAAE,OAAO,CAAC;CAC5B,CAAC;AAmDF,eAAO,MAAM,gBAAgB,GAC3B,OAAO,cAAc,EACrB,SAAS,OAAO,EAChB,YAAY,MAAM,KACjB,OAAO,CAAC,oBAAoB,CAqF9B,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/risk/index.js b/packages/codeflow-store/dist/risk/index.js new file mode 100644 index 0000000..f140704 --- /dev/null +++ b/packages/codeflow-store/dist/risk/index.js @@ -0,0 +1,120 @@ +import { createHash } from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; +const slugify = (value) => value + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, "") + .slice(0, 80) || "node"; +const scoreToLevel = (score) => { + if (score >= 6) { + return "high"; + } + if (score >= 3) { + return "medium"; + } + return "low"; +}; +const createFingerprint = (graph, runPlan, outputDir) => createHash("sha256") + .update(JSON.stringify({ + projectName: graph.projectName, + mode: graph.mode, + outputDir, + nodes: graph.nodes.map((node) => node.id).sort(), + edges: graph.edges.map((edge) => `${edge.kind}:${edge.from}:${edge.to}`).sort(), + tasks: runPlan.tasks.map((task) => `${task.id}:${task.batchIndex}`).sort() +})) + .digest("hex"); +const resolveWorkspaceRoot = () => { + const configuredRoot = process.env.CODEFLOW_WORKSPACE_ROOT?.trim(); + if (configuredRoot) { + return path.resolve(configuredRoot); + } + return path.join(process.cwd(), "artifacts"); +}; +const resolveDefaultOutputDir = (graph) => { + const workspaceRoot = resolveWorkspaceRoot(); + if (process.env.CODEFLOW_WORKSPACE_ROOT?.trim()) { + return path.resolve(workspaceRoot, "artifacts", slugify(graph.projectName)); + } + return path.resolve(workspaceRoot, slugify(graph.projectName)); +}; +const resolveOutputDir = (graph, outputDir) => outputDir && outputDir.trim() + ? path.resolve(outputDir) + : resolveDefaultOutputDir(graph); +export const assessExportRisk = async (graph, runPlan, outputDir) => { + const resolvedOutputDir = resolveOutputDir(graph, outputDir); + const factors = []; + const repoBackedNodeCount = graph.nodes.filter((node) => node.sourceRefs.some((ref) => ref.kind === "repo")).length; + const exists = await fs + .stat(resolvedOutputDir) + .then((stats) => stats.isDirectory()) + .catch(() => false); + const existingEntries = exists ? (await fs.readdir(resolvedOutputDir)).filter(Boolean) : []; + const hasExistingOutput = existingEntries.length > 0; + const workspaceRoot = resolveWorkspaceRoot(); + const defaultOutputDir = resolveDefaultOutputDir(graph); + if (hasExistingOutput) { + factors.push({ + code: "overwrite-existing-output", + message: `Output directory ${resolvedOutputDir} already contains files.`, + score: 4 + }); + } + if (outputDir && path.resolve(outputDir) !== defaultOutputDir) { + factors.push({ + code: "custom-output-dir", + message: `Artifacts will be written to a custom directory: ${resolvedOutputDir}.`, + score: 1 + }); + } + if (!resolvedOutputDir.startsWith(workspaceRoot)) { + factors.push({ + code: "outside-workspace", + message: `Output directory is outside the workspace root: ${resolvedOutputDir}.`, + score: 2 + }); + } + if (repoBackedNodeCount > 0) { + factors.push({ + code: "repo-backed-context", + message: `${repoBackedNodeCount} blueprint nodes were derived from a real repo.`, + score: 1 + }); + } + if (runPlan.tasks.length >= 20) { + factors.push({ + code: "large-task-set", + message: `Execution plan contains ${runPlan.tasks.length} tasks.`, + score: 2 + }); + } + if (runPlan.batches.length >= 6) { + factors.push({ + code: "deep-execution-plan", + message: `Execution plan spans ${runPlan.batches.length} batches.`, + score: 1 + }); + } + if (graph.mode === "yolo") { + factors.push({ + code: "yolo-mode", + message: "Yolo mode bypasses approval gates.", + score: 2 + }); + } + const score = factors.reduce((total, factor) => total + factor.score, 0); + const riskReport = { + score, + level: scoreToLevel(score), + requiresApproval: graph.mode === "essential" && (hasExistingOutput || score >= 4), + factors + }; + return { + fingerprint: createFingerprint(graph, runPlan, resolvedOutputDir), + outputDir: resolvedOutputDir, + riskReport, + hasExistingOutput + }; +}; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/risk/index.js.map b/packages/codeflow-store/dist/risk/index.js.map new file mode 100644 index 0000000..97b5ae5 --- /dev/null +++ b/packages/codeflow-store/dist/risk/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/risk/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/run/index.d.ts b/packages/codeflow-store/dist/run/index.d.ts new file mode 100644 index 0000000..4dcfe72 --- /dev/null +++ b/packages/codeflow-store/dist/run/index.d.ts @@ -0,0 +1,4 @@ +import type { RunRecord } from "@abhinav2203/codeflow-core/schema"; +export declare const createRunId: () => string; +export declare const saveRunRecord: (runRecord: RunRecord) => Promise; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/run/index.d.ts.map b/packages/codeflow-store/dist/run/index.d.ts.map new file mode 100644 index 0000000..7eb0fb1 --- /dev/null +++ b/packages/codeflow-store/dist/run/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/run/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAOnE,eAAO,MAAM,WAAW,QAAO,MAA6B,CAAC;AAE7D,eAAO,MAAM,aAAa,GAAU,WAAW,SAAS,KAAG,OAAO,CAAC,IAAI,CAItE,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/run/index.js b/packages/codeflow-store/dist/run/index.js new file mode 100644 index 0000000..746ed9f --- /dev/null +++ b/packages/codeflow-store/dist/run/index.js @@ -0,0 +1,14 @@ +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { runPath } from "../shared/utils.js"; +const ensureDir = async (dirPath) => { + await fs.mkdir(dirPath, { recursive: true }); +}; +export const createRunId = () => crypto.randomUUID(); +export const saveRunRecord = async (runRecord) => { + const filePath = runPath(runRecord.id); + await ensureDir(path.dirname(filePath)); + await fs.writeFile(filePath, `${JSON.stringify(runRecord, null, 2)}\n`, "utf8"); +}; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/run/index.js.map b/packages/codeflow-store/dist/run/index.js.map new file mode 100644 index 0000000..05bcfe5 --- /dev/null +++ b/packages/codeflow-store/dist/run/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/run/index.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAE7C,MAAM,SAAS,GAAG,KAAK,EAAE,OAAe,EAAiB,EAAE;IACzD,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAC/C,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAG,GAAW,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;AAE7D,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAAE,SAAoB,EAAiB,EAAE;IACzE,MAAM,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACvC,MAAM,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IACxC,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAClF,CAAC,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/session/index.d.ts b/packages/codeflow-store/dist/session/index.d.ts new file mode 100644 index 0000000..4b9b609 --- /dev/null +++ b/packages/codeflow-store/dist/session/index.d.ts @@ -0,0 +1,14 @@ +import type { BlueprintGraph, ExecutionReport, ExportResult, PersistedSession, RiskReport, RunPlan } from "@abhinav2203/codeflow-core/schema"; +export declare const createSessionId: () => string; +export declare const saveSession: (session: PersistedSession) => Promise; +export declare const loadLatestSession: (projectName: string) => Promise; +export declare const upsertSession: ({ graph, runPlan, lastRiskReport, lastExportResult, lastExecutionReport, approvalId, sessionId }: { + graph: BlueprintGraph; + runPlan: RunPlan; + lastRiskReport?: RiskReport; + lastExportResult?: ExportResult; + lastExecutionReport?: ExecutionReport; + approvalId?: string; + sessionId?: string; +}) => Promise; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/session/index.d.ts.map b/packages/codeflow-store/dist/session/index.d.ts.map new file mode 100644 index 0000000..c35af84 --- /dev/null +++ b/packages/codeflow-store/dist/session/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/session/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,UAAU,EACV,OAAO,EACR,MAAM,mCAAmC,CAAC;AAa3C,eAAO,MAAM,eAAe,QAAO,MAA6B,CAAC;AAEjE,eAAO,MAAM,WAAW,GAAU,SAAS,gBAAgB,KAAG,OAAO,CAAC,IAAI,CAIzE,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAU,aAAa,MAAM,KAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAO5F,CAAC;AAEF,eAAO,MAAM,aAAa,GAAU,kGAQjC;IACD,KAAK,EAAE,cAAc,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,UAAU,CAAC;IAC5B,gBAAgB,CAAC,EAAE,YAAY,CAAC;IAChC,mBAAmB,CAAC,EAAE,eAAe,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,KAAG,OAAO,CAAC,gBAAgB,CAmB3B,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/session/index.js b/packages/codeflow-store/dist/session/index.js new file mode 100644 index 0000000..4471295 --- /dev/null +++ b/packages/codeflow-store/dist/session/index.js @@ -0,0 +1,47 @@ +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { persistedSessionSchema } from "@abhinav2203/codeflow-core/schema"; +import { latestSessionPath, sessionDirForProject, sessionHistoryPath } from "../shared/utils.js"; +const ensureDir = async (dirPath) => { + await fs.mkdir(dirPath, { recursive: true }); +}; +const writeSessionFile = async (filePath, session) => { + await ensureDir(path.dirname(filePath)); + await fs.writeFile(filePath, `${JSON.stringify(session, null, 2)}\n`, "utf8"); +}; +export const createSessionId = () => crypto.randomUUID(); +export const saveSession = async (session) => { + await ensureDir(sessionDirForProject(session.projectName)); + await writeSessionFile(latestSessionPath(session.projectName), session); + await writeSessionFile(sessionHistoryPath(session.projectName, session.sessionId), session); +}; +export const loadLatestSession = async (projectName) => { + try { + const content = await fs.readFile(latestSessionPath(projectName), "utf8"); + return persistedSessionSchema.parse(JSON.parse(content)); + } + catch { + return null; + } +}; +export const upsertSession = async ({ graph, runPlan, lastRiskReport, lastExportResult, lastExecutionReport, approvalId, sessionId }) => { + const existing = await loadLatestSession(graph.projectName); + const normalizedGraph = persistedSessionSchema.shape.graph.parse(graph); + const nextSession = persistedSessionSchema.parse({ + sessionId: sessionId ?? existing?.sessionId ?? createSessionId(), + projectName: normalizedGraph.projectName, + updatedAt: new Date().toISOString(), + graph: normalizedGraph, + runPlan, + lastRiskReport: lastRiskReport ?? existing?.lastRiskReport, + lastExportResult: lastExportResult ?? existing?.lastExportResult, + lastExecutionReport: lastExecutionReport ?? existing?.lastExecutionReport, + approvalIds: approvalId + ? [...new Set([...(existing?.approvalIds ?? []), approvalId])] + : (existing?.approvalIds ?? []) + }); + await saveSession(nextSession); + return nextSession; +}; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/session/index.js.map b/packages/codeflow-store/dist/session/index.js.map new file mode 100644 index 0000000..d30a168 --- /dev/null +++ b/packages/codeflow-store/dist/session/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/session/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/shared/file-tree.d.ts b/packages/codeflow-store/dist/shared/file-tree.d.ts new file mode 100644 index 0000000..cd7e83a --- /dev/null +++ b/packages/codeflow-store/dist/shared/file-tree.d.ts @@ -0,0 +1,17 @@ +/** + * Information about a file or directory in the repository. + */ +export interface FileInfo { + path: string; + name: string; + isDirectory: boolean; +} +/** + * Recursively scans a repository path for files matching allowed extensions. + * Returns a flat array of FileInfo objects for all matching files. + * + * @param repoPath - The root path to scan + * @returns Promise resolving to an array of FileInfo objects + */ +export declare function scanRepoFiles(repoPath: string): Promise; +//# sourceMappingURL=file-tree.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/shared/file-tree.d.ts.map b/packages/codeflow-store/dist/shared/file-tree.d.ts.map new file mode 100644 index 0000000..9168abd --- /dev/null +++ b/packages/codeflow-store/dist/shared/file-tree.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"file-tree.d.ts","sourceRoot":"","sources":["../../src/shared/file-tree.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;CACtB;AAYD;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CA6BzE"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/shared/file-tree.js b/packages/codeflow-store/dist/shared/file-tree.js new file mode 100644 index 0000000..842a43f --- /dev/null +++ b/packages/codeflow-store/dist/shared/file-tree.js @@ -0,0 +1,43 @@ +import { readdir, stat } from "node:fs/promises"; +import { join, basename } from "node:path"; +const ALLOWED_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".json", ".md"]); +/** + * Checks if a file has an allowed extension. + */ +function hasAllowedExtension(fileName) { + const extension = fileName.slice(fileName.lastIndexOf(".")).toLowerCase(); + return ALLOWED_EXTENSIONS.has(extension); +} +/** + * Recursively scans a repository path for files matching allowed extensions. + * Returns a flat array of FileInfo objects for all matching files. + * + * @param repoPath - The root path to scan + * @returns Promise resolving to an array of FileInfo objects + */ +export async function scanRepoFiles(repoPath) { + const files = []; + async function scanDirectory(currentPath) { + const entries = await readdir(currentPath, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = join(currentPath, entry.name); + if (entry.isDirectory()) { + await scanDirectory(fullPath); + } + else if (entry.isFile() && hasAllowedExtension(entry.name)) { + files.push({ + path: fullPath, + name: basename(entry.name), + isDirectory: false + }); + } + } + } + const repoStat = await stat(repoPath); + if (!repoStat.isDirectory()) { + throw new Error(`Path is not a directory: ${repoPath}`); + } + await scanDirectory(repoPath); + return files; +} +//# sourceMappingURL=file-tree.js.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/shared/file-tree.js.map b/packages/codeflow-store/dist/shared/file-tree.js.map new file mode 100644 index 0000000..5faa7ba --- /dev/null +++ b/packages/codeflow-store/dist/shared/file-tree.js.map @@ -0,0 +1 @@ +{"version":3,"file":"file-tree.js","sourceRoot":"","sources":["../../src/shared/file-tree.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAW3C,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;AAEnF;;GAEG;AACH,SAAS,mBAAmB,CAAC,QAAgB;IAC3C,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IAC1E,OAAO,kBAAkB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,QAAgB;IAClD,MAAM,KAAK,GAAe,EAAE,CAAC;IAE7B,KAAK,UAAU,aAAa,CAAC,WAAmB;QAC9C,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,WAAW,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAEpE,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAE/C,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;YAChC,CAAC;iBAAM,IAAI,KAAK,CAAC,MAAM,EAAE,IAAI,mBAAmB,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC7D,KAAK,CAAC,IAAI,CAAC;oBACT,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;oBAC1B,WAAW,EAAE,KAAK;iBACnB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtC,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,4BAA4B,QAAQ,EAAE,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;IAE9B,OAAO,KAAK,CAAC;AACf,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/shared/run-command.d.ts b/packages/codeflow-store/dist/shared/run-command.d.ts new file mode 100644 index 0000000..d22ff5a --- /dev/null +++ b/packages/codeflow-store/dist/shared/run-command.d.ts @@ -0,0 +1,17 @@ +export type RunCommandOptions = { + cwd: string; + env?: NodeJS.ProcessEnv; + timeoutMs?: number; + stdoutMaxBytes?: number; + stderrMaxBytes?: number; +}; +export type RunCommandResult = { + exitCode: number | null; + stdout: string; + stderr: string; + timedOut: boolean; + outputCapped: boolean; + signal: NodeJS.Signals | null; +}; +export declare const runCommand: (command: string, args: string[], options: RunCommandOptions) => Promise; +//# sourceMappingURL=run-command.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/shared/run-command.d.ts.map b/packages/codeflow-store/dist/shared/run-command.d.ts.map new file mode 100644 index 0000000..940a283 --- /dev/null +++ b/packages/codeflow-store/dist/shared/run-command.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"run-command.d.ts","sourceRoot":"","sources":["../../src/shared/run-command.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;CAC/B,CAAC;AAoBF,eAAO,MAAM,UAAU,GACrB,SAAS,MAAM,EACf,MAAM,MAAM,EAAE,EACd,SAAS,iBAAiB,KACzB,OAAO,CAAC,gBAAgB,CA2EvB,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/shared/run-command.js b/packages/codeflow-store/dist/shared/run-command.js new file mode 100644 index 0000000..8e33f29 --- /dev/null +++ b/packages/codeflow-store/dist/shared/run-command.js @@ -0,0 +1,79 @@ +import { spawn } from "node:child_process"; +const DEFAULT_TIMEOUT_MS = 20_000; +const DEFAULT_STDOUT_MAX_BYTES = 64 * 1024; +const DEFAULT_STDERR_MAX_BYTES = 128 * 1024; +const appendChunk = (current, chunk, maxBytes) => { + const next = current + chunk.toString("utf8"); + if (Buffer.byteLength(next, "utf8") <= maxBytes) { + return { next, capped: false }; + } + const truncated = Buffer.from(next, "utf8").subarray(0, maxBytes).toString("utf8"); + return { next: truncated, capped: true }; +}; +export const runCommand = (command, args, options) => new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd: options.cwd, + env: options.env, + stdio: ["ignore", "pipe", "pipe"] + }); + let stdout = ""; + let stderr = ""; + let timedOut = false; + let outputCapped = false; + let settled = false; + const settle = (result) => { + if (settled) { + return; + } + settled = true; + resolve(result); + }; + const timeoutId = setTimeout(() => { + timedOut = true; + child.kill("SIGKILL"); + }, options.timeoutMs ?? DEFAULT_TIMEOUT_MS); + child.stdout.on("data", (chunk) => { + const updated = appendChunk(stdout, chunk, options.stdoutMaxBytes ?? DEFAULT_STDOUT_MAX_BYTES); + stdout = updated.next; + if (updated.capped) { + outputCapped = true; + child.kill("SIGKILL"); + } + }); + child.stderr.on("data", (chunk) => { + const updated = appendChunk(stderr, chunk, options.stderrMaxBytes ?? DEFAULT_STDERR_MAX_BYTES); + stderr = updated.next; + if (updated.capped) { + outputCapped = true; + child.kill("SIGKILL"); + } + }); + child.on("error", (error) => { + clearTimeout(timeoutId); + if (settled) { + return; + } + settled = true; + child.kill("SIGKILL"); + reject(error); + }); + child.on("close", (code, signal) => { + clearTimeout(timeoutId); + let finalStderr = stderr; + if (timedOut) { + finalStderr = `${finalStderr}\nCommand timed out after ${options.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms.`.trim(); + } + if (outputCapped) { + finalStderr = `${finalStderr}\nCommand output exceeded the configured cap.`.trim(); + } + settle({ + exitCode: code, + stdout, + stderr: finalStderr, + timedOut, + outputCapped, + signal + }); + }); +}); +//# sourceMappingURL=run-command.js.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/shared/run-command.js.map b/packages/codeflow-store/dist/shared/run-command.js.map new file mode 100644 index 0000000..460f201 --- /dev/null +++ b/packages/codeflow-store/dist/shared/run-command.js.map @@ -0,0 +1 @@ +{"version":3,"file":"run-command.js","sourceRoot":"","sources":["../../src/shared/run-command.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/shared/terminal-sessions.d.ts b/packages/codeflow-store/dist/shared/terminal-sessions.d.ts new file mode 100644 index 0000000..77ba13f --- /dev/null +++ b/packages/codeflow-store/dist/shared/terminal-sessions.d.ts @@ -0,0 +1,28 @@ +export declare const TERMINAL_REPO_PATH_HEADER = "x-codeflow-repo-path"; +export type TerminalSessionStatus = "running" | "exited" | "error"; +export type TerminalSessionSummary = { + id: string; + title: string; + cwd: string; + shell: string; + status: TerminalSessionStatus; + startedAt: string; + lastActivityAt: string; + exitCode: number | null; +}; +export type TerminalSessionSnapshot = TerminalSessionSummary & { + output: string; + truncated: boolean; +}; +export declare const listTerminalSessions: () => TerminalSessionSummary[]; +export declare const getTerminalSession: (id: string) => TerminalSessionSnapshot | null; +export declare const createTerminalSession: (options?: { + cwd?: string; + title?: string; +}) => Promise; +export declare const writeTerminalInput: (id: string, input: string, options?: { + echoInput?: boolean; +}) => Promise; +export declare const closeTerminalSession: (id: string) => boolean; +export declare const shutdownAllTerminalSessions: () => void; +//# sourceMappingURL=terminal-sessions.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/shared/terminal-sessions.d.ts.map b/packages/codeflow-store/dist/shared/terminal-sessions.d.ts.map new file mode 100644 index 0000000..0b60120 --- /dev/null +++ b/packages/codeflow-store/dist/shared/terminal-sessions.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"terminal-sessions.d.ts","sourceRoot":"","sources":["../../src/shared/terminal-sessions.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,yBAAyB,yBAAyB,CAAC;AAEhE,MAAM,MAAM,qBAAqB,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEnE,MAAM,MAAM,sBAAsB,GAAG;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,qBAAqB,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,sBAAsB,GAAG;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACpB,CAAC;AAsGF,eAAO,MAAM,oBAAoB,QAAO,sBAAsB,EAGW,CAAC;AAE1E,eAAO,MAAM,kBAAkB,GAAI,IAAI,MAAM,KAAG,uBAAuB,GAAG,IAGzE,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAU,UAAU;IACpD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,KAAG,OAAO,CAAC,uBAAuB,CAkDlC,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,IAAI,MAAM,EACV,OAAO,MAAM,EACb,UAAU;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,KAChC,OAAO,CAAC,uBAAuB,CA2BjC,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,IAAI,MAAM,KAAG,OAYjD,CAAC;AAEF,eAAO,MAAM,2BAA2B,YAQvC,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/shared/terminal-sessions.js b/packages/codeflow-store/dist/shared/terminal-sessions.js new file mode 100644 index 0000000..068ac50 --- /dev/null +++ b/packages/codeflow-store/dist/shared/terminal-sessions.js @@ -0,0 +1,174 @@ +import { spawn } from "node:child_process"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { randomUUID } from "node:crypto"; +export const TERMINAL_REPO_PATH_HEADER = "x-codeflow-repo-path"; +const DEFAULT_WORKSPACE_ROOT = process.env.CODEFLOW_REPO_ROOT ?? /* turbopackIgnore: true */ process.cwd(); +const OUTPUT_CAP_BYTES = 128 * 1024; +const OUTPUT_TRUNCATION_NOTICE = "[CodeFlow] Older terminal output truncated.\n"; +const sessions = new Map(); +let sessionCounter = 0; +const stripTruncationNotice = (value) => value.startsWith(OUTPUT_TRUNCATION_NOTICE) ? value.slice(OUTPUT_TRUNCATION_NOTICE.length) : value; +const clampOutput = (value) => { + const buffer = Buffer.from(value, "utf8"); + if (buffer.byteLength <= OUTPUT_CAP_BYTES) { + return { output: value, truncated: false }; + } + const noticeBytes = Buffer.byteLength(OUTPUT_TRUNCATION_NOTICE, "utf8"); + const remainingBytes = Math.max(0, OUTPUT_CAP_BYTES - noticeBytes); + const tail = buffer.subarray(Math.max(0, buffer.byteLength - remainingBytes)).toString("utf8"); + return { + output: `${OUTPUT_TRUNCATION_NOTICE}${tail}`, + truncated: true + }; +}; +const appendOutput = (session, chunk) => { + if (!chunk) { + return; + } + const next = `${stripTruncationNotice(session.output)}${chunk}`; + const clamped = clampOutput(next); + session.output = clamped.output; + session.truncated = clamped.truncated; + session.lastActivityAt = new Date().toISOString(); +}; +const toSummary = (session) => ({ + id: session.id, + title: session.title, + cwd: session.cwd, + shell: session.shell, + status: session.status, + startedAt: session.startedAt, + lastActivityAt: session.lastActivityAt, + exitCode: session.exitCode +}); +const toSnapshot = (session) => ({ + ...toSummary(session), + output: session.output, + truncated: session.truncated +}); +const resolveInitialCwd = async (cwd) => { + const resolved = cwd?.trim() ? path.resolve(cwd.trim()) : path.resolve(DEFAULT_WORKSPACE_ROOT); + const stats = await fs.stat(resolved).catch(() => null); + if (!stats?.isDirectory()) { + throw new Error(`Terminal working directory does not exist or is not a directory: ${resolved}`); + } + return resolved; +}; +const getShellPath = () => { + const configuredShell = process.env.CODEFLOW_TERMINAL_SHELL?.trim(); + if (configuredShell) { + return configuredShell; + } + return process.env.SHELL?.trim() || "/bin/sh"; +}; +const recordInput = (session, input) => { + const printable = input + .replace(/\r/g, "") + .split("\n") + .map((line) => line.trimEnd()) + .filter((line) => line.length > 0) + .join("\n"); + if (!printable) { + return; + } + appendOutput(session, `${printable + .split("\n") + .map((line) => `$ ${line}`) + .join("\n")}\n`); +}; +export const listTerminalSessions = () => [...sessions.values()] + .map(toSummary) + .sort((left, right) => right.startedAt.localeCompare(left.startedAt)); +export const getTerminalSession = (id) => { + const session = sessions.get(id); + return session ? toSnapshot(session) : null; +}; +export const createTerminalSession = async (options) => { + const cwd = await resolveInitialCwd(options?.cwd); + const shell = getShellPath(); + const child = spawn(shell, [], { + cwd, + env: { + ...process.env, + TERM: process.env.TERM || "xterm-256color" + }, + stdio: ["pipe", "pipe", "pipe"] + }); + const startedAt = new Date().toISOString(); + sessionCounter += 1; + const session = { + id: randomUUID(), + title: options?.title?.trim() || `Shell ${sessionCounter}`, + cwd, + shell, + status: "running", + startedAt, + lastActivityAt: startedAt, + exitCode: null, + output: "", + truncated: false, + child + }; + child.stdout.on("data", (chunk) => { + appendOutput(session, chunk.toString("utf8")); + }); + child.stderr.on("data", (chunk) => { + appendOutput(session, chunk.toString("utf8")); + }); + child.on("error", (error) => { + session.status = "error"; + session.exitCode = null; + appendOutput(session, `\n[CodeFlow] Terminal process error: ${error.message}\n`); + }); + child.on("close", (code) => { + session.status = session.status === "error" ? "error" : "exited"; + session.exitCode = code; + appendOutput(session, `\n[CodeFlow] Terminal exited with code ${code ?? "unknown"}.\\n`); + }); + sessions.set(session.id, session); + return toSnapshot(session); +}; +export const writeTerminalInput = async (id, input, options) => { + const session = sessions.get(id); + if (!session) { + throw new Error(`Terminal session ${id} was not found.`); + } + if (session.status !== "running") { + throw new Error(`Terminal session ${id} is no longer running.`); + } + if (options?.echoInput ?? true) { + recordInput(session, input); + } + await new Promise((resolve, reject) => { + session.child.stdin.write(input, (error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + session.lastActivityAt = new Date().toISOString(); + return toSnapshot(session); +}; +export const closeTerminalSession = (id) => { + const session = sessions.get(id); + if (!session) { + return false; + } + if (session.status === "running") { + session.child.kill("SIGTERM"); + } + sessions.delete(id); + return true; +}; +export const shutdownAllTerminalSessions = () => { + for (const session of sessions.values()) { + if (session.status === "running") { + session.child.kill("SIGTERM"); + } + } + sessions.clear(); +}; +//# sourceMappingURL=terminal-sessions.js.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/shared/terminal-sessions.js.map b/packages/codeflow-store/dist/shared/terminal-sessions.js.map new file mode 100644 index 0000000..bb1cd04 --- /dev/null +++ b/packages/codeflow-store/dist/shared/terminal-sessions.js.map @@ -0,0 +1 @@ +{"version":3,"file":"terminal-sessions.js","sourceRoot":"","sources":["../../src/shared/terminal-sessions.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/shared/utils.d.ts b/packages/codeflow-store/dist/shared/utils.d.ts new file mode 100644 index 0000000..33ecc5a --- /dev/null +++ b/packages/codeflow-store/dist/shared/utils.d.ts @@ -0,0 +1,11 @@ +export declare const getStoreRoot: () => string; +export declare const sessionDirForProject: (projectName: string) => string; +export declare const latestSessionPath: (projectName: string) => string; +export declare const sessionHistoryPath: (projectName: string, sessionId: string) => string; +export declare const approvalPath: (approvalId: string) => string; +export declare const runPath: (runId: string) => string; +export declare const checkpointPath: (checkpointId: string) => string; +export declare const observabilityPath: (projectName: string) => string; +export declare const branchDirForProject: (projectName: string) => string; +export declare const branchPath: (projectName: string, branchId: string) => string; +//# sourceMappingURL=utils.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/shared/utils.d.ts.map b/packages/codeflow-store/dist/shared/utils.d.ts.map new file mode 100644 index 0000000..27ba87f --- /dev/null +++ b/packages/codeflow-store/dist/shared/utils.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/shared/utils.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,YAAY,QAAO,MAGD,CAAC;AAEhC,eAAO,MAAM,oBAAoB,GAAI,aAAa,MAAM,KAAG,MACE,CAAC;AAE9D,eAAO,MAAM,iBAAiB,GAAI,aAAa,MAAM,KAAG,MACK,CAAC;AAE9D,eAAO,MAAM,kBAAkB,GAAI,aAAa,MAAM,EAAE,WAAW,MAAM,KAAG,MACE,CAAC;AAE/E,eAAO,MAAM,YAAY,GAAI,YAAY,MAAM,KAAG,MACY,CAAC;AAE/D,eAAO,MAAM,OAAO,GAAI,OAAO,MAAM,KAAG,MACY,CAAC;AAErD,eAAO,MAAM,cAAc,GAAI,cAAc,MAAM,KAAG,MACE,CAAC;AAEzD,eAAO,MAAM,iBAAiB,GAAI,aAAa,MAAM,KAAG,MACoB,CAAC;AAE7E,eAAO,MAAM,mBAAmB,GAAI,aAAa,MAAM,KAAG,MACG,CAAC;AAE9D,eAAO,MAAM,UAAU,GAAI,aAAa,MAAM,EAAE,UAAU,MAAM,KAAG,MAQlE,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/shared/utils.js b/packages/codeflow-store/dist/shared/utils.js new file mode 100644 index 0000000..25467f1 --- /dev/null +++ b/packages/codeflow-store/dist/shared/utils.js @@ -0,0 +1,32 @@ +import os from "node:os"; +import path from "node:path"; +const slugify = (value) => value + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, "") + .slice(0, 80) || "node"; +const resolveDefaultStoreRoot = () => { + if (process.env.VITEST || process.env.NODE_ENV === "test") { + return path.join(process.cwd(), ".codeflow-store-test", `worker-${process.env.VITEST_WORKER_ID ?? "0"}`); + } + return path.join(os.homedir(), ".codeflow-store"); +}; +export const getStoreRoot = () => process.env.CODEFLOW_STORE_ROOT + ? path.resolve(process.env.CODEFLOW_STORE_ROOT) + : resolveDefaultStoreRoot(); +export const sessionDirForProject = (projectName) => path.join(getStoreRoot(), "sessions", slugify(projectName)); +export const latestSessionPath = (projectName) => path.join(sessionDirForProject(projectName), "latest.json"); +export const sessionHistoryPath = (projectName, sessionId) => path.join(sessionDirForProject(projectName), "history", `${sessionId}.json`); +export const approvalPath = (approvalId) => path.join(getStoreRoot(), "approvals", `${approvalId}.json`); +export const runPath = (runId) => path.join(getStoreRoot(), "runs", `${runId}.json`); +export const checkpointPath = (checkpointId) => path.join(getStoreRoot(), "checkpoints", checkpointId); +export const observabilityPath = (projectName) => path.join(getStoreRoot(), "observability", `${slugify(projectName)}.json`); +export const branchDirForProject = (projectName) => path.join(getStoreRoot(), "branches", slugify(projectName)); +export const branchPath = (projectName, branchId) => { + const safeBranchId = path.basename(branchId); + if (safeBranchId !== branchId) { + throw new Error("Invalid branch ID"); + } + return path.join(branchDirForProject(projectName), `${safeBranchId}.json`); +}; +//# sourceMappingURL=utils.js.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/shared/utils.js.map b/packages/codeflow-store/dist/shared/utils.js.map new file mode 100644 index 0000000..623da9c --- /dev/null +++ b/packages/codeflow-store/dist/shared/utils.js.map @@ -0,0 +1 @@ +{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/shared/utils.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/store/index.d.ts b/packages/codeflow-store/dist/store/index.d.ts new file mode 100644 index 0000000..a48b5ec --- /dev/null +++ b/packages/codeflow-store/dist/store/index.d.ts @@ -0,0 +1,35 @@ +import type { BlueprintGraph, BlueprintNode } from "@abhinav2203/codeflow-core/schema"; +type GraphStateUpdater = BlueprintGraph | null | ((current: BlueprintGraph | null) => BlueprintGraph | null); +type NodeUpdater = Partial | ((node: BlueprintNode) => BlueprintNode); +export type WorkbenchMode = "graph" | "ide"; +export interface FloatingGraphPanel { + visible: boolean; + x: number; + y: number; + width: number; + height: number; +} +export interface BlueprintStore { + graph: BlueprintGraph | null; + setGraph: (next: GraphStateUpdater) => void; + updateNode: (id: string, patch: NodeUpdater) => void; + openFiles: string[]; + activeFile: string | null; + setOpenFiles: (paths: string[]) => void; + setActiveFile: (path: string | null) => void; + closeFile: (path: string) => void; + repoPath: string | null; + setRepoPath: (path: string | null) => void; + mode: WorkbenchMode; + setMode: (mode: WorkbenchMode) => void; + floatingGraph: FloatingGraphPanel; + setFloatingGraph: (panel: Partial) => void; + selectedNodeId: string | null; + setSelectedNodeId: (id: string | null) => void; + dirtyFiles: Record; + setFileDirty: (path: string, dirty: boolean) => void; + clearFileDirty: (path: string) => void; +} +export declare const useBlueprintStore: import("zustand").UseBoundStore>; +export {}; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/store/index.d.ts.map b/packages/codeflow-store/dist/store/index.d.ts.map new file mode 100644 index 0000000..dfcbfef --- /dev/null +++ b/packages/codeflow-store/dist/store/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAEvF,KAAK,iBAAiB,GAAG,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,KAAK,cAAc,GAAG,IAAI,CAAC,CAAC;AAC7G,KAAK,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,aAAa,KAAK,aAAa,CAAC,CAAC;AAErF,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,KAAK,CAAC;AAE5C,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,cAAc,GAAG,IAAI,CAAC;IAC7B,QAAQ,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC5C,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACrD,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACxC,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7C,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC3C,IAAI,EAAE,aAAa,CAAC;IACpB,OAAO,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAC;IACvC,aAAa,EAAE,kBAAkB,CAAC;IAClC,gBAAgB,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,kBAAkB,CAAC,KAAK,IAAI,CAAC;IAC/D,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,iBAAiB,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC/C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACrD,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC;AAUD,eAAO,MAAM,iBAAiB,6EAsF3B,CAAC"} \ No newline at end of file diff --git a/packages/codeflow-store/dist/store/index.js b/packages/codeflow-store/dist/store/index.js new file mode 100644 index 0000000..45ad4ef --- /dev/null +++ b/packages/codeflow-store/dist/store/index.js @@ -0,0 +1,78 @@ +import { create } from "zustand"; +const resolveGraphUpdate = (current, next) => (typeof next === "function" ? next(current) : next); +const resolveNodeUpdate = (node, patch) => typeof patch === "function" ? patch(node) : { ...node, ...patch }; +export const useBlueprintStore = create((set) => ({ + graph: null, + setGraph: (next) => set((state) => ({ + graph: resolveGraphUpdate(state.graph, next) + })), + updateNode: (id, patch) => set((state) => { + if (!state.graph) { + return state; + } + return { + graph: { + ...state.graph, + nodes: state.graph.nodes.map((node) => node.id === id ? resolveNodeUpdate(node, patch) : node) + } + }; + }), + openFiles: [], + activeFile: null, + setOpenFiles: (paths) => set(() => ({ + openFiles: paths + })), + setActiveFile: (path) => set((state) => ({ + activeFile: path, + floatingGraph: { + ...state.floatingGraph, + visible: path !== null + } + })), + closeFile: (path) => set((state) => { + const nextOpenFiles = state.openFiles.filter((f) => f !== path); + const nextActiveFile = state.activeFile === path + ? nextOpenFiles[nextOpenFiles.length - 1] ?? null + : state.activeFile; + return { + openFiles: nextOpenFiles, + activeFile: nextActiveFile, + floatingGraph: { + ...state.floatingGraph, + visible: nextActiveFile !== null + }, + dirtyFiles: { ...state.dirtyFiles, [path]: false } + }; + }), + repoPath: null, + setRepoPath: (path) => set(() => ({ repoPath: path })), + mode: "ide", + setMode: (mode) => set((state) => ({ + mode, + floatingGraph: { + ...state.floatingGraph, + visible: state.activeFile !== null + } + })), + floatingGraph: { + visible: false, + x: 0, + y: 0, + width: 400, + height: 350 + }, + setFloatingGraph: (panel) => set((state) => ({ + floatingGraph: { ...state.floatingGraph, ...panel } + })), + selectedNodeId: null, + setSelectedNodeId: (id) => set(() => ({ selectedNodeId: id })), + dirtyFiles: {}, + setFileDirty: (path, dirty) => set((state) => ({ + dirtyFiles: { ...state.dirtyFiles, [path]: dirty } + })), + clearFileDirty: (path) => set((state) => { + const { [path]: _, ...rest } = state.dirtyFiles; + return { dirtyFiles: rest }; + }) +})); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/codeflow-store/dist/store/index.js.map b/packages/codeflow-store/dist/store/index.js.map new file mode 100644 index 0000000..5bae873 --- /dev/null +++ b/packages/codeflow-store/dist/store/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/codeflow-store/package-lock.json b/packages/codeflow-store/package-lock.json new file mode 100644 index 0000000..dd7eaae --- /dev/null +++ b/packages/codeflow-store/package-lock.json @@ -0,0 +1,1740 @@ +{ + "name": "@abhinav2203/codeflow-store", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@abhinav2203/codeflow-store", + "version": "0.1.0", + "dependencies": { + "@abhinav2203/codeflow-core": "^0.1.1", + "zustand": "^5.0.0" + }, + "bin": { + "codeflow-store": "dist/bin/cli.js" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.7.0", + "vitest": "^3.0.0" + } + }, + "node_modules/@abhinav2203/codeflow-core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@abhinav2203/codeflow-core/-/codeflow-core-0.1.1.tgz", + "integrity": "sha512-DC1UQuiEwU0eCptVlVP2hiZEH2BqPvg0IhwBYx4yX63RRquzDzoLgOwCXa5pSb0aDx4ESa2K0vVYB/QPtnqrgw==", + "dependencies": { + "ts-morph": "^27.0.2", + "zod": "^4.3.6" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@ts-morph/common": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.28.1.tgz", + "integrity": "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==", + "license": "MIT", + "dependencies": { + "minimatch": "^10.0.1", + "path-browserify": "^1.0.1", + "tinyglobby": "^0.2.14" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ts-morph": { + "version": "27.0.2", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-27.0.2.tgz", + "integrity": "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==", + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.28.1", + "code-block-writer": "^13.0.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "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 + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "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 + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", + "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/packages/codeflow-store/package.json b/packages/codeflow-store/package.json new file mode 100644 index 0000000..79531e3 --- /dev/null +++ b/packages/codeflow-store/package.json @@ -0,0 +1,63 @@ +{ + "name": "@abhinav2203/codeflow-store", + "version": "0.1.0", + "description": "Local session storage, project-scoped state, checkpointing, approvals.", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./checkpoint": { + "types": "./dist/checkpoint/index.d.ts", + "default": "./dist/checkpoint/index.js" + }, + "./approval": { + "types": "./dist/approval/index.d.ts", + "default": "./dist/approval/index.js" + }, + "./run": { + "types": "./dist/run/index.d.ts", + "default": "./dist/run/index.js" + }, + "./risk": { + "types": "./dist/risk/index.d.ts", + "default": "./dist/risk/index.js" + }, + "./observability": { + "types": "./dist/observability/index.d.ts", + "default": "./dist/observability/index.js" + }, + "./branch": { + "types": "./dist/branch/index.d.ts", + "default": "./dist/branch/index.js" + }, + "./session": { + "types": "./dist/session/index.d.ts", + "default": "./dist/session/index.js" + }, + "./store": { + "types": "./dist/store/index.d.ts", + "default": "./dist/store/index.js" + } + }, + "bin": { + "codeflow-store": "./dist/bin/cli.js" + }, + "scripts": { + "check": "tsc --noEmit", + "test": "vitest run", + "build": "tsc --outDir dist --declaration --declarationMap" + }, + "dependencies": { + "@abhinav2203/codeflow-core": "^0.1.1", + "zustand": "^5.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.7.0", + "vitest": "^3.0.0" + } +} diff --git a/packages/codeflow-store/src/approval/approval.test.ts b/packages/codeflow-store/src/approval/approval.test.ts new file mode 100644 index 0000000..b4ccfbd --- /dev/null +++ b/packages/codeflow-store/src/approval/approval.test.ts @@ -0,0 +1,195 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import fs from "node:fs/promises"; +import fsSync from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + createApprovalId, + createApprovalRecord, + getApprovalRecord, + approveRecord +} from "./index.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const STORE_ROOT = path.join(__dirname, "../../.test-store"); +const STORE_ROOT_ENV = { CODEFLOW_STORE_ROOT: STORE_ROOT }; + +const cleanStore = () => { + try { + fsSync.rmSync(STORE_ROOT, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors — best effort + } +}; + +const withEnv = async (fn: () => Promise): Promise => { + const original = process.env.CODEFLOW_STORE_ROOT; + process.env.CODEFLOW_STORE_ROOT = STORE_ROOT; + try { + return await fn(); + } finally { + process.env.CODEFLOW_STORE_ROOT = original ?? ""; + cleanStore(); + } +}; + +describe("approval", () => { + beforeEach(() => { + cleanStore(); + }); + + describe("createApprovalId", () => { + it("returns a valid UUID v4", () => { + const id = createApprovalId(); + expect(typeof id).toBe("string"); + expect(id).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + ); + }); + + it("returns unique IDs each call", () => { + const id1 = createApprovalId(); + const id2 = createApprovalId(); + expect(id1).not.toBe(id2); + }); + }); + + describe("createApprovalRecord", () => { + it("creates a record with status pending", async () => { + await withEnv(async () => { + const record = await createApprovalRecord({ + projectName: "test-project", + fingerprint: "fingerprint-abc", + outputDir: "/tmp/output", + runPlan: { generatedAt: new Date().toISOString(), tasks: [], batches: [], warnings: [] }, + riskReport: { score: 0, level: "low", requiresApproval: false, factors: [] } + }); + + expect(record.status).toBe("pending"); + expect(record.action).toBe("export"); + expect(record.projectName).toBe("test-project"); + expect(record.fingerprint).toBe("fingerprint-abc"); + expect(record.outputDir).toBe("/tmp/output"); + expect(record.requestedAt).toBeDefined(); + expect(record.id).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + ); + }); + }); + + it("writes the record to disk at the correct path", async () => { + await withEnv(async () => { + const record = await createApprovalRecord({ + projectName: "test-project", + fingerprint: "fingerprint-abc", + outputDir: "/tmp/output", + runPlan: { generatedAt: new Date().toISOString(), tasks: [], batches: [], warnings: [] }, + riskReport: { score: 0, level: "low", requiresApproval: false, factors: [] } + }); + + const filePath = path.join( + STORE_ROOT, + "approvals", + `${record.id}.json` + ); + const content = await fs.readFile(filePath, "utf8"); + const parsed = JSON.parse(content); + expect(parsed.id).toBe(record.id); + expect(parsed.status).toBe("pending"); + }); + }); + }); + + describe("getApprovalRecord", () => { + it("returns null for a record that does not exist", async () => { + await withEnv(async () => { + const result = await getApprovalRecord("does-not-exist"); + expect(result).toBeNull(); + }); + }); + + it("returns the record when it exists", async () => { + await withEnv(async () => { + const created = await createApprovalRecord({ + projectName: "test-project", + fingerprint: "fingerprint-abc", + outputDir: "/tmp/output", + runPlan: { generatedAt: new Date().toISOString(), tasks: [], batches: [], warnings: [] }, + riskReport: { score: 0, level: "low", requiresApproval: false, factors: [] } + }); + + const result = await getApprovalRecord(created.id); + expect(result).not.toBeNull(); + expect(result!.id).toBe(created.id); + expect(result!.status).toBe("pending"); + }); + }); + }); + + describe("approveRecord", () => { + it("throws when the record does not exist", async () => { + await withEnv(async () => { + await expect(approveRecord("does-not-exist")).rejects.toThrow( + "Approval does-not-exist was not found." + ); + }); + }); + + it("changes status to approved and sets approvedAt", async () => { + await withEnv(async () => { + const created = await createApprovalRecord({ + projectName: "test-project", + fingerprint: "fingerprint-abc", + outputDir: "/tmp/output", + runPlan: { generatedAt: new Date().toISOString(), tasks: [], batches: [], warnings: [] }, + riskReport: { score: 0, level: "low", requiresApproval: false, factors: [] } + }); + + const approved = await approveRecord(created.id); + expect(approved.status).toBe("approved"); + expect(approved.approvedAt).toBeDefined(); + expect(approved.approvedAt).not.toBeNull(); + }); + }); + + it("preserves all other fields when approving", async () => { + await withEnv(async () => { + const created = await createApprovalRecord({ + projectName: "test-project", + fingerprint: "fingerprint-abc", + outputDir: "/tmp/output", + runPlan: { generatedAt: new Date().toISOString(), tasks: [], batches: [], warnings: [] }, + riskReport: { score: 0, level: "low", requiresApproval: false, factors: [] } + }); + + const approved = await approveRecord(created.id); + expect(approved.id).toBe(created.id); + expect(approved.projectName).toBe("test-project"); + expect(approved.fingerprint).toBe("fingerprint-abc"); + expect(approved.outputDir).toBe("/tmp/output"); + expect(approved.action).toBe("export"); + }); + }); + + it("updates the record on disk after approval", async () => { + await withEnv(async () => { + const created = await createApprovalRecord({ + projectName: "test-project", + fingerprint: "fingerprint-abc", + outputDir: "/tmp/output", + runPlan: { generatedAt: new Date().toISOString(), tasks: [], batches: [], warnings: [] }, + riskReport: { score: 0, level: "low", requiresApproval: false, factors: [] } + }); + + await approveRecord(created.id); + + const filePath = path.join(STORE_ROOT, "approvals", `${created.id}.json`); + const content = await fs.readFile(filePath, "utf8"); + const parsed = JSON.parse(content); + expect(parsed.status).toBe("approved"); + expect(parsed.approvedAt).toBeDefined(); + }); + }); + }); +}); diff --git a/packages/codeflow-store/src/approval/index.ts b/packages/codeflow-store/src/approval/index.ts new file mode 100644 index 0000000..b58ee98 --- /dev/null +++ b/packages/codeflow-store/src/approval/index.ts @@ -0,0 +1,72 @@ +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; + +import type { ApprovalRecord, RiskReport, RunPlan } from "@abhinav2203/codeflow-core/schema"; +import { approvalPath } from "../shared/utils.js"; + +const ensureDir = async (dirPath: string): Promise => { + await fs.mkdir(dirPath, { recursive: true }); +}; + +const writeApprovalFile = async (record: ApprovalRecord): Promise => { + const filePath = approvalPath(record.id); + await ensureDir(path.dirname(filePath)); + await fs.writeFile(filePath, `${JSON.stringify(record, null, 2)}\n`, "utf8"); +}; + +export const createApprovalId = (): string => crypto.randomUUID(); + +export const createApprovalRecord = async ({ + projectName, + fingerprint, + outputDir, + runPlan, + riskReport +}: { + projectName: string; + fingerprint: string; + outputDir: string; + runPlan: RunPlan; + riskReport: RiskReport; +}): Promise => { + const record: ApprovalRecord = { + id: createApprovalId(), + action: "export", + projectName, + status: "pending", + fingerprint, + requestedAt: new Date().toISOString(), + outputDir, + runPlan, + riskReport + }; + + await writeApprovalFile(record); + return record; +}; + +export const getApprovalRecord = async (approvalId: string): Promise => { + try { + const content = await fs.readFile(approvalPath(approvalId), "utf8"); + return JSON.parse(content) as ApprovalRecord; + } catch { + return null; + } +}; + +export const approveRecord = async (approvalId: string): Promise => { + const existing = await getApprovalRecord(approvalId); + if (!existing) { + throw new Error(`Approval ${approvalId} was not found.`); + } + + const approved: ApprovalRecord = { + ...existing, + status: "approved", + approvedAt: new Date().toISOString() + }; + + await writeApprovalFile(approved); + return approved; +}; diff --git a/packages/codeflow-store/src/bin/cli.ts b/packages/codeflow-store/src/bin/cli.ts new file mode 100644 index 0000000..422ea09 --- /dev/null +++ b/packages/codeflow-store/src/bin/cli.ts @@ -0,0 +1,179 @@ +#!/usr/bin/env node +import { loadLatestSession, upsertSession, createSessionId } from "../session/index.js"; +import { createApprovalRecord, getApprovalRecord, approveRecord } from "../approval/index.js"; +import { createCheckpointIfNeeded } from "../checkpoint/index.js"; +import { assessExportRisk } from "../risk/index.js"; +import { loadBranches } from "../branch/index.js"; +import { saveRunRecord, createRunId } from "../run/index.js"; +import { loadObservabilitySnapshot, mergeObservabilitySnapshot } from "../observability/index.js"; +import type { BlueprintGraph, RunPlan } from "@abhinav2203/codeflow-core/schema"; + +const USAGE = `CodeFlow Store CLI + +Usage: + codeflow-store session init Create a new session + codeflow-store session last Load the latest session + codeflow-store checkpoint create + codeflow-store approval approve Approve a pending approval + codeflow-store approval get Get approval record + codeflow-store risk assess [outputDir] Assess export risk + codeflow-store branch list List branches + codeflow-store branch switch [graphJson] + codeflow-store run list List run records + codeflow-store observability get Get observability snapshot + codeflow-store observability merge + codeflow-store --help Show this help`; + +const fail = (msg: string): never => { + console.error(msg); + process.exit(1); +}; + +const parseJsonFile = async (filePath: string): Promise => { + const { readFileSync } = await import("node:fs"); + return JSON.parse(readFileSync(filePath, "utf8")) as T; +}; + +const main = async () => { + const args = process.argv.slice(2); + if (args[0] === "--help" || args[0] === "-h" || args.length === 0) { + console.log(USAGE); + process.exit(0); + } + + const [command, subcommand, ...rest] = args; + + switch (`${command} ${subcommand}`) { + case "session init": { + const projectName = rest[0] ?? fail("projectName required"); + const sessionId = createSessionId(); + const graph: BlueprintGraph = { + projectName, + mode: "essential", + generatedAt: new Date().toISOString(), + nodes: [], + edges: [], + workflows: [], + warnings: [] + }; + const runPlan: RunPlan = { + generatedAt: new Date().toISOString(), + tasks: [], + batches: [], + warnings: [] + }; + const session = await upsertSession({ sessionId, graph, runPlan }); + console.log(JSON.stringify(session, null, 2)); + break; + } + + case "session last": { + const projectName = rest[0] ?? fail("projectName required"); + const session = await loadLatestSession(projectName); + if (!session) { + fail(`No session found for project: ${projectName}`); + } + console.log(JSON.stringify(session, null, 2)); + break; + } + + case "checkpoint create": { + const checkpointId = rest[0] ?? fail("checkpointId required"); + const outputDir = rest[1] ?? fail("outputDir required"); + const runId = rest[2] ?? fail("runId required"); + const dir = await createCheckpointIfNeeded(outputDir, runId); + console.log(JSON.stringify({ checkpointDir: dir }, null, 2)); + break; + } + + case "approval approve": { + const approvalId = rest[0] ?? fail("approvalId required"); + const approval = await approveRecord(approvalId); + console.log(JSON.stringify(approval, null, 2)); + break; + } + + case "approval get": { + const approvalId = rest[0] ?? fail("approvalId required"); + const approval = await getApprovalRecord(approvalId); + if (!approval) { + fail(`Approval not found: ${approvalId}`); + } + console.log(JSON.stringify(approval, null, 2)); + break; + } + + case "risk assess": { + const graphJson = rest[0] ?? fail("graph JSON file required"); + const outputDir = rest[1]; + const graph = await parseJsonFile(graphJson); + const runPlan: RunPlan = { + generatedAt: new Date().toISOString(), + tasks: [], + batches: [], + warnings: [] + }; + const assessment = await assessExportRisk(graph, runPlan, outputDir); + console.log(JSON.stringify(assessment, null, 2)); + break; + } + + case "branch list": { + const projectName = rest[0] ?? fail("projectName required"); + const branches = await loadBranches(projectName); + console.log(JSON.stringify(branches, null, 2)); + break; + } + + case "branch switch": { + // Note: branch switching is managed via the branches API routes. + // This command is a placeholder for future branch-switch functionality. + console.log(JSON.stringify({ message: "Branch switching is handled via the API. Use POST /api/branches to create/switch branches." }, null, 2)); + break; + } + + case "run list": { + const { readdir } = await import("node:fs/promises"); + const { getStoreRoot } = await import("../shared/utils.js"); + const runsDir = getStoreRoot() + "/runs"; + let files: string[] = []; + try { + files = await readdir(runsDir); + } catch { + files = []; + } + console.log(JSON.stringify(files, null, 2)); + break; + } + + case "observability get": { + const projectName = rest[0] ?? fail("projectName required"); + const snapshot = await loadObservabilitySnapshot(projectName); + if (!snapshot) { + fail(`No observability snapshot found for project: ${projectName}`); + } + console.log(JSON.stringify(snapshot, null, 2)); + break; + } + + case "observability merge": { + const projectName = rest[0] ?? fail("projectName required"); + const spansJson = rest[1] ?? fail("spans JSON file required"); + const logsJson = rest[2] ?? fail("logs JSON file required"); + const spans = await parseJsonFile(spansJson); + const logs = await parseJsonFile(logsJson); + const snapshot = await mergeObservabilitySnapshot({ + projectName, + spans: spans as Parameters[0]["spans"], + logs: logs as Parameters[0]["logs"] + }); + console.log(JSON.stringify(snapshot, null, 2)); + break; + } + + default: + fail(`Unknown command: ${command} ${subcommand}\n${USAGE}`); + } +}; + +main().catch((err) => fail(err instanceof Error ? err.message : String(err))); diff --git a/packages/codeflow-store/src/branch/branch.test.ts b/packages/codeflow-store/src/branch/branch.test.ts new file mode 100644 index 0000000..5cb960b --- /dev/null +++ b/packages/codeflow-store/src/branch/branch.test.ts @@ -0,0 +1,231 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import fs from "node:fs/promises"; +import fsSync from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { saveBranch, loadBranch, loadBranches, deleteBranch } from "./index.js"; +import type { GraphBranch } from "@abhinav2203/codeflow-core/schema"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const STORE_ROOT = path.join(__dirname, "../../.test-store"); + +const cleanStore = () => { + try { + fsSync.rmSync(STORE_ROOT, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors — best effort + } +}; + +const withEnv = async (fn: () => Promise): Promise => { + const original = process.env.CODEFLOW_STORE_ROOT; + process.env.CODEFLOW_STORE_ROOT = STORE_ROOT; + try { + return await fn(); + } finally { + process.env.CODEFLOW_STORE_ROOT = original ?? ""; + cleanStore(); + } +}; + +const makeBranch = (overrides: Partial = {}): GraphBranch => ({ + id: "branch-1", + name: "branch-1", + projectName: "test-project", + createdAt: "2026-01-01T00:00:00.000Z", + graph: { + projectName: "test-project", + mode: "essential", + phase: "spec", + generatedAt: "2026-01-01T00:00:00.000Z", + nodes: [], + edges: [], + workflows: [], + warnings: [] + }, + ...overrides +}); + +describe("branch", () => { + beforeEach(() => { + cleanStore(); + }); + + describe("saveBranch", () => { + it("writes a branch file to the correct path", async () => { + await withEnv(async () => { + const branch = makeBranch({ id: "feature-auth", name: "feature-auth" }); + await saveBranch(branch); + + const filePath = path.join( + STORE_ROOT, + "branches", + "test-project", + "feature-auth.json" + ); + const content = await fs.readFile(filePath, "utf8"); + const parsed = JSON.parse(content); + expect(parsed.id).toBe("feature-auth"); + expect(parsed.name).toBe("feature-auth"); + }); + }); + + it("creates parent directories if they do not exist", async () => { + await withEnv(async () => { + const branch = makeBranch({ id: "new-branch", projectName: "new-project" }); + await saveBranch(branch); + + const filePath = path.join( + STORE_ROOT, + "branches", + "new-project", + "new-branch.json" + ); + const stat = await fs.stat(filePath); + expect(stat.isFile()).toBe(true); + }); + }); + }); + + describe("loadBranch", () => { + it("returns null when the branch does not exist", async () => { + await withEnv(async () => { + const result = await loadBranch("test-project", "does-not-exist"); + expect(result).toBeNull(); + }); + }); + + it("returns the branch when it exists", async () => { + await withEnv(async () => { + const branch = makeBranch({ id: "feature-auth", name: "feature-auth" }); + await saveBranch(branch); + + const result = await loadBranch("test-project", "feature-auth"); + expect(result).not.toBeNull(); + expect(result!.id).toBe("feature-auth"); + expect(result!.name).toBe("feature-auth"); + }); + }); + + it("loads the graph correctly", async () => { + await withEnv(async () => { + const branch = makeBranch({ + id: "feature-auth", + graph: { + projectName: "test-project", + mode: "essential", + phase: "spec", + generatedAt: "2026-01-01T00:00:00.000Z", + nodes: [{ + id: "n1", + kind: "module", + name: "auth", + summary: "auth module", + path: "auth.ts", + contract: { summary: "", responsibilities: [], inputs: [], outputs: [], attributes: [], methods: [], sideEffects: [], errors: [], dependencies: [], calls: [], uiAccess: [], backendAccess: [], notes: [] }, + sourceRefs: [{ kind: "repo", path: "src/auth.ts" }], + generatedRefs: [], + traceRefs: [], + status: "spec_only" + }], + edges: [], + workflows: [], + warnings: [] + } + }); + await saveBranch(branch); + + const result = await loadBranch("test-project", "feature-auth"); + expect(result!.graph.nodes).toHaveLength(1); + expect(result!.graph.nodes[0].name).toBe("auth"); + }); + }); + }); + + describe("loadBranches", () => { + it("returns an empty array when no branches exist", async () => { + await withEnv(async () => { + const result = await loadBranches("test-project"); + expect(result).toEqual([]); + }); + }); + + it("returns all branches for a project", async () => { + await withEnv(async () => { + await saveBranch(makeBranch({ id: "branch-a", name: "branch-a", createdAt: "2026-01-01T00:00:00.000Z" })); + await saveBranch(makeBranch({ id: "branch-b", name: "branch-b", createdAt: "2026-01-02T00:00:00.000Z" })); + + const result = await loadBranches("test-project"); + expect(result).toHaveLength(2); + }); + }); + + it("returns branches sorted by createdAt ascending", async () => { + await withEnv(async () => { + await saveBranch(makeBranch({ id: "older", name: "older", createdAt: "2026-01-01T00:00:00.000Z" })); + await saveBranch(makeBranch({ id: "newer", name: "newer", createdAt: "2026-01-03T00:00:00.000Z" })); + await saveBranch(makeBranch({ id: "middle", name: "middle", createdAt: "2026-01-02T00:00:00.000Z" })); + + const result = await loadBranches("test-project"); + expect(result[0].id).toBe("older"); + expect(result[1].id).toBe("middle"); + expect(result[2].id).toBe("newer"); + }); + }); + + it("ignores non-json files in the branch directory", async () => { + await withEnv(async () => { + await saveBranch(makeBranch({ id: "valid-branch", name: "valid-branch" })); + + // Write a non-JSON file into the branch directory + const branchDir = path.join(STORE_ROOT, "branches", "test-project"); + await fs.mkdir(branchDir, { recursive: true }); + await fs.writeFile(path.join(branchDir, "README.txt"), "not a branch"); + + const result = await loadBranches("test-project"); + expect(result).toHaveLength(1); + expect(result[0].id).toBe("valid-branch"); + }); + }); + + it("skips malformed JSON files", async () => { + await withEnv(async () => { + await saveBranch(makeBranch({ id: "good-branch", name: "good-branch" })); + + // Write a malformed JSON file + const branchDir = path.join(STORE_ROOT, "branches", "test-project"); + await fs.writeFile(path.join(branchDir, "malformed.json"), "{ not valid json"); + + const result = await loadBranches("test-project"); + expect(result).toHaveLength(1); + expect(result[0].id).toBe("good-branch"); + }); + }); + }); + + describe("deleteBranch", () => { + it("removes the branch file", async () => { + await withEnv(async () => { + await saveBranch(makeBranch({ id: "to-delete", name: "to-delete" })); + await deleteBranch("test-project", "to-delete"); + + const filePath = path.join( + STORE_ROOT, + "branches", + "test-project", + "to-delete.json" + ); + await expect(fs.access(filePath)).rejects.toThrow(); + }); + }); + + it("does not throw when the branch does not exist", async () => { + await withEnv(async () => { + await expect( + deleteBranch("test-project", "does-not-exist") + ).resolves.toBeUndefined(); + }); + }); + }); +}); diff --git a/packages/codeflow-store/src/branch/index.ts b/packages/codeflow-store/src/branch/index.ts new file mode 100644 index 0000000..98cad0b --- /dev/null +++ b/packages/codeflow-store/src/branch/index.ts @@ -0,0 +1,61 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +import type { GraphBranch } from "@abhinav2203/codeflow-core/schema"; +import { branchDirForProject, branchPath } from "../shared/utils.js"; + +const ensureDir = async (dirPath: string): Promise => { + await fs.mkdir(dirPath, { recursive: true }); +}; + +export const saveBranch = async (branch: GraphBranch): Promise => { + const filePath = branchPath(branch.projectName, branch.id); + await ensureDir(path.dirname(filePath)); + await fs.writeFile(filePath, `${JSON.stringify(branch, null, 2)}\n`, "utf8"); +}; + +export const loadBranch = async ( + projectName: string, + branchId: string +): Promise => { + try { + const content = await fs.readFile(branchPath(projectName, branchId), "utf8"); + return JSON.parse(content) as GraphBranch; + } catch { + return null; + } +}; + +export const loadBranches = async (projectName: string): Promise => { + const dir = branchDirForProject(projectName); + + try { + const entries = await fs.readdir(dir); + const branches = await Promise.all( + entries + .filter((entry) => entry.endsWith(".json")) + .map(async (entry) => { + try { + const content = await fs.readFile(path.join(dir, entry), "utf8"); + return JSON.parse(content) as GraphBranch; + } catch { + return null; + } + }) + ); + + return branches + .filter((branch): branch is GraphBranch => branch !== null) + .sort((left, right) => left.createdAt.localeCompare(right.createdAt)); + } catch { + return []; + } +}; + +export const deleteBranch = async (projectName: string, branchId: string): Promise => { + try { + await fs.unlink(branchPath(projectName, branchId)); + } catch { + // Ignore already-removed branches. + } +}; diff --git a/packages/codeflow-store/src/checkpoint/checkpoint.test.ts b/packages/codeflow-store/src/checkpoint/checkpoint.test.ts new file mode 100644 index 0000000..d8a3711 --- /dev/null +++ b/packages/codeflow-store/src/checkpoint/checkpoint.test.ts @@ -0,0 +1,122 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import fs from "node:fs/promises"; +import fsSync from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { createCheckpointIfNeeded } from "./index.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const STORE_ROOT = path.join(__dirname, "../../.test-store"); + +const cleanStore = () => { + try { + fsSync.rmSync(STORE_ROOT, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors — best effort + } +}; + +const withEnv = async (fn: () => Promise): Promise => { + const original = process.env.CODEFLOW_STORE_ROOT; + process.env.CODEFLOW_STORE_ROOT = STORE_ROOT; + try { + return await fn(); + } finally { + process.env.CODEFLOW_STORE_ROOT = original ?? ""; + cleanStore(); + } +}; + +describe("checkpoint", () => { + beforeEach(() => { + cleanStore(); + }); + + describe("createCheckpointIfNeeded", () => { + it("returns undefined when the target directory does not exist", async () => { + await withEnv(async () => { + const result = await createCheckpointIfNeeded( + "/this/path/does/not/exist", + "checkpoint-1" + ); + expect(result).toBeUndefined(); + }); + }); + + it("returns undefined when the target directory is empty", async () => { + await withEnv(async () => { + const targetDir = path.join(STORE_ROOT, "empty-project"); + await fs.mkdir(targetDir, { recursive: true }); + + const result = await createCheckpointIfNeeded(targetDir, "checkpoint-1"); + expect(result).toBeUndefined(); + }); + }); + + it("copies the directory contents when the target has files", async () => { + await withEnv(async () => { + const targetDir = path.join(STORE_ROOT, "my-project"); + await fs.mkdir(path.join(targetDir, "src"), { recursive: true }); + await fs.writeFile(path.join(targetDir, "src/index.ts"), "console.log('hello')"); + await fs.writeFile(path.join(targetDir, "package.json"), '{"name":"test"}'); + + const result = await createCheckpointIfNeeded(targetDir, "checkpoint-1"); + expect(result).toBeDefined(); + expect(result).toContain("checkpoint-1"); + + // Verify files were copied + const copiedIndex = await fs.readFile( + path.join(result!, "src", "index.ts"), + "utf8" + ); + expect(copiedIndex).toBe("console.log('hello')"); + + const copiedPkg = await fs.readFile( + path.join(result!, "package.json"), + "utf8" + ); + expect(copiedPkg).toBe('{"name":"test"}'); + }); + }); + + it("preserves directory structure in the copy", async () => { + await withEnv(async () => { + const targetDir = path.join(STORE_ROOT, "nested-project"); + await fs.mkdir(path.join(targetDir, "src/lib/utils"), { recursive: true }); + await fs.writeFile( + path.join(targetDir, "src/lib/utils/helper.ts"), + "export const helper = true" + ); + + const result = await createCheckpointIfNeeded(targetDir, "checkpoint-nested"); + + const copiedHelper = await fs.readFile( + path.join(result!, "src", "lib", "utils", "helper.ts"), + "utf8" + ); + expect(copiedHelper).toBe("export const helper = true"); + }); + }); + + it("overwrites existing checkpoint if called again", async () => { + await withEnv(async () => { + const targetDir = path.join(STORE_ROOT, "overwrite-test"); + await fs.mkdir(targetDir, { recursive: true }); + await fs.writeFile(path.join(targetDir, "file.txt"), "original"); + + const result1 = await createCheckpointIfNeeded(targetDir, "checkpoint-overwrite"); + await fs.writeFile(path.join(targetDir, "file.txt"), "modified"); + + const result2 = await createCheckpointIfNeeded(targetDir, "checkpoint-overwrite"); + + // Second call should overwrite with fresh copy + const content = await fs.readFile( + path.join(result2!, "file.txt"), + "utf8" + ); + expect(content).toBe("modified"); + }); + }); + }); +}); diff --git a/packages/codeflow-store/src/checkpoint/index.ts b/packages/codeflow-store/src/checkpoint/index.ts new file mode 100644 index 0000000..6c4c96b --- /dev/null +++ b/packages/codeflow-store/src/checkpoint/index.ts @@ -0,0 +1,36 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +import { checkpointPath } from "../shared/utils.js"; + +const ensureDir = async (dirPath: string): Promise => { + await fs.mkdir(dirPath, { recursive: true }); +}; + +export const createCheckpointIfNeeded = async ( + targetDir: string, + checkpointId: string +): Promise => { + const exists = await fs + .stat(targetDir) + .then((stats) => stats.isDirectory()) + .catch(() => false); + + if (!exists) { + return undefined; + } + + const entries = await fs.readdir(targetDir); + if (entries.length === 0) { + return undefined; + } + + const checkpointDir = checkpointPath(checkpointId); + await ensureDir(path.dirname(checkpointDir)); + await fs.cp(targetDir, checkpointDir, { + recursive: true, + force: true + }); + + return checkpointDir; +}; diff --git a/packages/codeflow-store/src/observability/index.ts b/packages/codeflow-store/src/observability/index.ts new file mode 100644 index 0000000..d8a39e6 --- /dev/null +++ b/packages/codeflow-store/src/observability/index.ts @@ -0,0 +1,54 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +import type { BlueprintGraph, ObservabilitySnapshot } from "@abhinav2203/codeflow-core/schema"; +import { blueprintGraphSchema } from "@abhinav2203/codeflow-core/schema"; +import { observabilityPath } from "../shared/utils.js"; + +const ensureDir = async (dirPath: string): Promise => { + await fs.mkdir(dirPath, { recursive: true }); +}; + +const writeSnapshotFile = async ( + projectName: string, + snapshot: ObservabilitySnapshot +): Promise => { + const filePath = observabilityPath(projectName); + await ensureDir(path.dirname(filePath)); + await fs.writeFile(filePath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8"); +}; + +export const loadObservabilitySnapshot = async ( + projectName: string +): Promise => { + try { + const content = await fs.readFile(observabilityPath(projectName), "utf8"); + return JSON.parse(content) as ObservabilitySnapshot; + } catch { + return null; + } +}; + +export const mergeObservabilitySnapshot = async ({ + projectName, + spans, + logs, + graph +}: { + projectName: string; + spans: ObservabilitySnapshot["spans"]; + logs: ObservabilitySnapshot["logs"]; + graph?: BlueprintGraph; +}): Promise => { + const existing = await loadObservabilitySnapshot(projectName); + const snapshot: ObservabilitySnapshot = { + projectName, + updatedAt: new Date().toISOString(), + spans: [...(existing?.spans ?? []), ...spans].slice(-500), + logs: [...(existing?.logs ?? []), ...logs].slice(-500), + graph: graph ? blueprintGraphSchema.parse(graph) : existing?.graph + }; + + await writeSnapshotFile(projectName, snapshot); + return snapshot; +}; diff --git a/packages/codeflow-store/src/observability/observability.test.ts b/packages/codeflow-store/src/observability/observability.test.ts new file mode 100644 index 0000000..0be5672 --- /dev/null +++ b/packages/codeflow-store/src/observability/observability.test.ts @@ -0,0 +1,267 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import fs from "node:fs/promises"; +import fsSync from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { loadObservabilitySnapshot, mergeObservabilitySnapshot } from "./index.js"; +import type { ObservabilitySnapshot, BlueprintGraph } from "@abhinav2203/codeflow-core/schema"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const STORE_ROOT = path.join(__dirname, "../../.test-store"); + +const cleanStore = () => { + try { + fsSync.rmSync(STORE_ROOT, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors — best effort + } +}; + +const withEnv = async (fn: () => Promise): Promise => { + const original = process.env.CODEFLOW_STORE_ROOT; + process.env.CODEFLOW_STORE_ROOT = STORE_ROOT; + try { + return await fn(); + } finally { + process.env.CODEFLOW_STORE_ROOT = original ?? ""; + cleanStore(); + } +}; + +const makeSpan = (id: string, name: string) => ({ + spanId: id, + traceId: "trace-1", + name, + blueprintNodeId: `node-${id}`, + path: undefined, + status: "success" as const, + durationMs: 100, + runtime: "test", + provenance: "observed" as const, + timestamp: new Date().toISOString() +}); + +const makeLog = (id: string, message: string) => ({ + id: `log-${id}`, + level: "info" as const, + message, + blueprintNodeId: undefined, + path: undefined, + runtime: "test", + timestamp: new Date().toISOString() +}); + +const makeGraph = (): BlueprintGraph => ({ + projectName: "test-project", + mode: "essential", + generatedAt: new Date().toISOString(), + nodes: [], + edges: [], + workflows: [], + warnings: [] +}); + +describe("observability", () => { + beforeEach(() => { + cleanStore(); + }); + + describe("loadObservabilitySnapshot", () => { + it("returns null when no snapshot exists for the project", async () => { + await withEnv(async () => { + const result = await loadObservabilitySnapshot("nonexistent-project"); + expect(result).toBeNull(); + }); + }); + + it("returns the existing snapshot when it exists", async () => { + await withEnv(async () => { + const snapshot: ObservabilitySnapshot = { + projectName: "test-project", + updatedAt: new Date().toISOString(), + spans: [makeSpan("s1", "auth.validate")], + logs: [makeLog("l1", "server started")] + }; + + await mergeObservabilitySnapshot({ + projectName: "test-project", + spans: snapshot.spans, + logs: snapshot.logs + }); + + const result = await loadObservabilitySnapshot("test-project"); + expect(result).not.toBeNull(); + expect(result!.spans).toHaveLength(1); + expect(result!.logs).toHaveLength(1); + expect(result!.spans[0].name).toBe("auth.validate"); + }); + }); + }); + + describe("mergeObservabilitySnapshot", () => { + it("writes a snapshot to disk", async () => { + await withEnv(async () => { + await mergeObservabilitySnapshot({ + projectName: "test-project", + spans: [makeSpan("s1", "auth.login")], + logs: [makeLog("l1", "user logged in")] + }); + + const filePath = path.join( + STORE_ROOT, + "observability", + "test-project.json" + ); + const content = await fs.readFile(filePath, "utf8"); + const parsed = JSON.parse(content); + expect(parsed.projectName).toBe("test-project"); + expect(parsed.spans).toHaveLength(1); + }); + }); + + it("appends spans when merging (does not replace)", async () => { + await withEnv(async () => { + await mergeObservabilitySnapshot({ + projectName: "test-project", + spans: [makeSpan("s1", "auth.login")], + logs: [] + }); + + await mergeObservabilitySnapshot({ + projectName: "test-project", + spans: [makeSpan("s2", "db.query")], + logs: [] + }); + + const result = await loadObservabilitySnapshot("test-project"); + expect(result!.spans).toHaveLength(2); + expect(result!.spans[0].spanId).toBe("s1"); + expect(result!.spans[1].spanId).toBe("s2"); + }); + }); + + it("appends logs when merging (does not replace)", async () => { + await withEnv(async () => { + await mergeObservabilitySnapshot({ + projectName: "test-project", + spans: [], + logs: [makeLog("l1", "server started")] + }); + + await mergeObservabilitySnapshot({ + projectName: "test-project", + spans: [], + logs: [makeLog("l2", "request received")] + }); + + const result = await loadObservabilitySnapshot("test-project"); + expect(result!.logs).toHaveLength(2); + expect(result!.logs[0].message).toBe("server started"); + expect(result!.logs[1].message).toBe("request received"); + }); + }); + + it("caps spans at 500 items (slice -500)", async () => { + await withEnv(async () => { + // Create 600 spans + const manySpans = Array.from({ length: 600 }, (_, i) => + makeSpan(`s${i}`, `span-${i}`) + ); + + await mergeObservabilitySnapshot({ + projectName: "test-project", + spans: manySpans, + logs: [] + }); + + const result = await loadObservabilitySnapshot("test-project"); + expect(result!.spans).toHaveLength(500); + // Last 500 — so s100 through s599 should be present, s0-s99 dropped + expect(result!.spans[0].spanId).toBe("s100"); + expect(result!.spans[499].spanId).toBe("s599"); + }); + }); + + it("caps logs at 500 items", async () => { + await withEnv(async () => { + const manyLogs = Array.from({ length: 600 }, (_, i) => + makeLog(`l${i}`, `log message ${i}`) + ); + + await mergeObservabilitySnapshot({ + projectName: "test-project", + spans: [], + logs: manyLogs + }); + + const result = await loadObservabilitySnapshot("test-project"); + expect(result!.logs).toHaveLength(500); + }); + }); + + it("updates the graph when provided", async () => { + await withEnv(async () => { + const graph = makeGraph(); + + await mergeObservabilitySnapshot({ + projectName: "test-project", + spans: [], + logs: [], + graph + }); + + const result = await loadObservabilitySnapshot("test-project"); + expect(result!.graph).toBeDefined(); + expect(result!.graph!.projectName).toBe("test-project"); + }); + }); + + it("preserves the existing graph when not provided", async () => { + await withEnv(async () => { + const graph = makeGraph(); + await mergeObservabilitySnapshot({ + projectName: "test-project", + spans: [], + logs: [], + graph + }); + + await mergeObservabilitySnapshot({ + projectName: "test-project", + spans: [makeSpan("s1", "auth.login")], + logs: [] + // no graph provided + }); + + const result = await loadObservabilitySnapshot("test-project"); + expect(result!.graph).toBeDefined(); + }); + }); + + it("updates the updatedAt timestamp on merge", async () => { + await withEnv(async () => { + await mergeObservabilitySnapshot({ + projectName: "test-project", + spans: [], + logs: [] + }); + + const first = await loadObservabilitySnapshot("test-project"); + const firstUpdatedAt = first!.updatedAt; + + // Wait a tiny bit to ensure timestamp differs + await new Promise((r) => setTimeout(r, 10)); + + await mergeObservabilitySnapshot({ + projectName: "test-project", + spans: [makeSpan("s1", "auth.login")], + logs: [] + }); + + const second = await loadObservabilitySnapshot("test-project"); + expect(second!.updatedAt).not.toBe(firstUpdatedAt); + }); + }); + }); +}); diff --git a/packages/codeflow-store/src/risk/index.ts b/packages/codeflow-store/src/risk/index.ts new file mode 100644 index 0000000..ec9c8cb --- /dev/null +++ b/packages/codeflow-store/src/risk/index.ts @@ -0,0 +1,160 @@ +import { createHash } from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; + +import type { BlueprintGraph, RiskFactor, RiskReport, RunPlan } from "@abhinav2203/codeflow-core/schema"; + +const slugify = (value: string): string => + value + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, "") + .slice(0, 80) || "node"; + +export type ExportRiskAssessment = { + fingerprint: string; + outputDir: string; + riskReport: RiskReport; + hasExistingOutput: boolean; +}; + +const scoreToLevel = (score: number): RiskReport["level"] => { + if (score >= 6) { + return "high"; + } + + if (score >= 3) { + return "medium"; + } + + return "low"; +}; + +const createFingerprint = (graph: BlueprintGraph, runPlan: RunPlan, outputDir: string): string => + createHash("sha256") + .update( + JSON.stringify({ + projectName: graph.projectName, + mode: graph.mode, + outputDir, + nodes: graph.nodes.map((node) => node.id).sort(), + edges: graph.edges.map((edge) => `${edge.kind}:${edge.from}:${edge.to}`).sort(), + tasks: runPlan.tasks.map((task) => `${task.id}:${task.batchIndex}`).sort() + }) + ) + .digest("hex"); + +const resolveWorkspaceRoot = (): string => { + const configuredRoot = process.env.CODEFLOW_WORKSPACE_ROOT?.trim(); + if (configuredRoot) { + return path.resolve(configuredRoot); + } + + return path.join(process.cwd(), "artifacts"); +}; + +const resolveDefaultOutputDir = (graph: BlueprintGraph): string => { + const workspaceRoot = resolveWorkspaceRoot(); + if (process.env.CODEFLOW_WORKSPACE_ROOT?.trim()) { + return path.resolve(workspaceRoot, "artifacts", slugify(graph.projectName)); + } + + return path.resolve(workspaceRoot, slugify(graph.projectName)); +}; + +const resolveOutputDir = (graph: BlueprintGraph, outputDir?: string): string => + outputDir && outputDir.trim() + ? path.resolve(outputDir) + : resolveDefaultOutputDir(graph); + +export const assessExportRisk = async ( + graph: BlueprintGraph, + runPlan: RunPlan, + outputDir?: string +): Promise => { + const resolvedOutputDir = resolveOutputDir(graph, outputDir); + const factors: RiskFactor[] = []; + const repoBackedNodeCount = graph.nodes.filter((node) => + node.sourceRefs.some((ref) => ref.kind === "repo") + ).length; + const exists = await fs + .stat(resolvedOutputDir) + .then((stats) => stats.isDirectory()) + .catch(() => false); + const existingEntries = exists ? (await fs.readdir(resolvedOutputDir)).filter(Boolean) : []; + const hasExistingOutput = existingEntries.length > 0; + const workspaceRoot = resolveWorkspaceRoot(); + const defaultOutputDir = resolveDefaultOutputDir(graph); + + if (hasExistingOutput) { + factors.push({ + code: "overwrite-existing-output", + message: `Output directory ${resolvedOutputDir} already contains files.`, + score: 4 + }); + } + + if (outputDir && path.resolve(outputDir) !== defaultOutputDir) { + factors.push({ + code: "custom-output-dir", + message: `Artifacts will be written to a custom directory: ${resolvedOutputDir}.`, + score: 1 + }); + } + + const relativeOutputDir = path.relative(workspaceRoot, resolvedOutputDir); + if (relativeOutputDir.startsWith("..") || path.isAbsolute(relativeOutputDir)) { + factors.push({ + code: "outside-workspace", + message: `Output directory is outside the workspace root: ${resolvedOutputDir}.`, + score: 2 + }); + } + + if (repoBackedNodeCount > 0) { + factors.push({ + code: "repo-backed-context", + message: `${repoBackedNodeCount} blueprint nodes were derived from a real repo.`, + score: 1 + }); + } + + if (runPlan.tasks.length >= 20) { + factors.push({ + code: "large-task-set", + message: `Execution plan contains ${runPlan.tasks.length} tasks.`, + score: 2 + }); + } + + if (runPlan.batches.length >= 6) { + factors.push({ + code: "deep-execution-plan", + message: `Execution plan spans ${runPlan.batches.length} batches.`, + score: 1 + }); + } + + if (graph.mode === "yolo") { + factors.push({ + code: "yolo-mode", + message: "Yolo mode bypasses approval gates.", + score: 2 + }); + } + + const score = factors.reduce((total, factor) => total + factor.score, 0); + const riskReport: RiskReport = { + score, + level: scoreToLevel(score), + requiresApproval: graph.mode === "essential" && (hasExistingOutput || score >= 4), + factors + }; + + return { + fingerprint: createFingerprint(graph, runPlan, resolvedOutputDir), + outputDir: resolvedOutputDir, + riskReport, + hasExistingOutput + }; +}; diff --git a/packages/codeflow-store/src/risk/risk.test.ts b/packages/codeflow-store/src/risk/risk.test.ts new file mode 100644 index 0000000..52613f9 --- /dev/null +++ b/packages/codeflow-store/src/risk/risk.test.ts @@ -0,0 +1,312 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import fs from "node:fs/promises"; +import fsSync from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { assessExportRisk } from "./index.js"; +import type { BlueprintGraph, RunPlan } from "@abhinav2203/codeflow-core/schema"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const STORE_ROOT = path.join(__dirname, "../../.test-store"); +// WORKSPACE_ROOT must be .test-store (NOT .test-store/artifacts) so that +// resolveDefaultOutputDir(graph) → /artifacts/ +// which matches where the test creates files. +const WORKSPACE_ROOT = path.join(__dirname, "../../.test-store"); + +const cleanStore = () => { + try { + fsSync.rmSync(STORE_ROOT, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors — best effort + } +}; + +const withEnv = async (fn: () => Promise): Promise => { + const original = { store: process.env.CODEFLOW_STORE_ROOT, workspace: process.env.CODEFLOW_WORKSPACE_ROOT }; + process.env.CODEFLOW_STORE_ROOT = STORE_ROOT; + process.env.CODEFLOW_WORKSPACE_ROOT = WORKSPACE_ROOT; + try { + return await fn(); + } finally { + process.env.CODEFLOW_STORE_ROOT = original.store ?? ""; + process.env.CODEFLOW_WORKSPACE_ROOT = original.workspace ?? ""; + cleanStore(); + } +}; + +const makeGraph = (overrides: Partial = {}): BlueprintGraph => ({ + projectName: "test-project", + mode: "essential", + phase: "spec", + generatedAt: new Date().toISOString(), + nodes: [], + edges: [], + workflows: [], + warnings: [], + ...overrides +}); + +const makeRunPlan = (overrides: Partial = {}): RunPlan => ({ + generatedAt: new Date().toISOString(), + tasks: [], + batches: [], + warnings: [], + ...overrides +}); + +describe("risk", () => { + beforeEach(() => { + cleanStore(); + }); + + describe("assessExportRisk", () => { + it("returns low risk for an empty blueprint with no output dir", async () => { + await withEnv(async () => { + const assessment = await assessExportRisk(makeGraph(), makeRunPlan()); + expect(assessment.riskReport.level).toBe("low"); + expect(assessment.riskReport.score).toBe(0); + expect(assessment.riskReport.factors).toHaveLength(0); + }); + }); + + it("adds factor overwrite-existing-output (+4) when output dir has files", async () => { + await withEnv(async () => { + // Create existing output at /artifacts/ + // (where resolveDefaultOutputDir resolves to when CODEFLOW_WORKSPACE_ROOT is set) + await fs.mkdir(path.join(WORKSPACE_ROOT, "artifacts", "test-project"), { recursive: true }); + await fs.writeFile( + path.join(WORKSPACE_ROOT, "artifacts", "test-project", "existing.ts"), + "console.log('old')" + ); + + const assessment = await assessExportRisk(makeGraph(), makeRunPlan()); + const factor = assessment.riskReport.factors.find( + (f) => f.code === "overwrite-existing-output" + ); + + expect(factor).toBeDefined(); + expect(factor!.score).toBe(4); + expect(assessment.riskReport.score).toBeGreaterThanOrEqual(4); + }); + }); + + it("adds factor custom-output-dir (+1) when output dir is explicitly set", async () => { + await withEnv(async () => { + const assessment = await assessExportRisk( + makeGraph(), + makeRunPlan(), + "/tmp/custom-output" + ); + + const factor = assessment.riskReport.factors.find( + (f) => f.code === "custom-output-dir" + ); + expect(factor).toBeDefined(); + expect(factor!.score).toBe(1); + }); + }); + + it("adds factor outside-workspace (+2) when output is outside workspace root", async () => { + await withEnv(async () => { + const assessment = await assessExportRisk( + makeGraph(), + makeRunPlan(), + "/tmp/totally-unrelated-dir" + ); + + const factor = assessment.riskReport.factors.find( + (f) => f.code === "outside-workspace" + ); + expect(factor).toBeDefined(); + expect(factor!.score).toBe(2); + }); + }); + + it("adds factor repo-backed-context (+1) when nodes have repo sourceRefs", async () => { + await withEnv(async () => { + const graph = makeGraph({ + nodes: [ + { + id: "n1", + kind: "module", + name: "auth", + summary: "auth module", + path: "src/auth.ts", + contract: { summary: "", responsibilities: [], inputs: [], outputs: [], attributes: [], methods: [], sideEffects: [], errors: [], dependencies: [], calls: [], uiAccess: [], backendAccess: [], notes: [] }, + sourceRefs: [{ kind: "repo", path: "src/auth.ts" }], + generatedRefs: [], + traceRefs: [], + status: "spec_only" + } + ] + }); + + const assessment = await assessExportRisk(graph, makeRunPlan()); + const factor = assessment.riskReport.factors.find( + (f) => f.code === "repo-backed-context" + ); + expect(factor).toBeDefined(); + expect(factor!.score).toBe(1); + }); + }); + + it("adds factor large-task-set (+2) when tasks >= 20", async () => { + await withEnv(async () => { + const tasks = Array.from({ length: 20 }, (_, i) => ({ + id: `task-${i}`, + nodeId: `n${i}`, + title: `Task ${i}`, + kind: "module" as const, + dependsOn: [], + batchIndex: 0 + })); + + const runPlan = makeRunPlan({ tasks }); + const assessment = await assessExportRisk(makeGraph(), runPlan); + + const factor = assessment.riskReport.factors.find( + (f) => f.code === "large-task-set" + ); + expect(factor).toBeDefined(); + expect(factor!.score).toBe(2); + }); + }); + + it("adds factor deep-execution-plan (+1) when batches >= 6", async () => { + await withEnv(async () => { + const batches = Array.from({ length: 6 }, (_, i) => ({ + index: i, + taskIds: [`task-${i}`] + })); + const tasks = batches.map((b, i) => ({ + id: `task-${i}`, + nodeId: `n${i}`, + title: `Task ${i}`, + kind: "module" as const, + dependsOn: [], + batchIndex: b.index + })); + + const runPlan = makeRunPlan({ tasks, batches }); + const assessment = await assessExportRisk(makeGraph(), runPlan); + + const factor = assessment.riskReport.factors.find( + (f) => f.code === "deep-execution-plan" + ); + expect(factor).toBeDefined(); + expect(factor!.score).toBe(1); + }); + }); + + it("adds factor yolo-mode (+2) when mode is yolo", async () => { + await withEnv(async () => { + const graph = makeGraph({ mode: "yolo" }); + const assessment = await assessExportRisk(graph, makeRunPlan()); + + const factor = assessment.riskReport.factors.find( + (f) => f.code === "yolo-mode" + ); + expect(factor).toBeDefined(); + expect(factor!.score).toBe(2); + }); + }); + + it("returns correct risk level thresholds", async () => { + await withEnv(async () => { + // Score 0-2 → low + let assessment = await assessExportRisk(makeGraph(), makeRunPlan()); + expect(assessment.riskReport.level).toBe("low"); + + // Score 3-5 → medium (score 4: overwrite-existing-output) + await fs.mkdir(path.join(WORKSPACE_ROOT, "artifacts", "test-project"), { recursive: true }); + await fs.writeFile( + path.join(WORKSPACE_ROOT, "artifacts", "test-project", "file.ts"), + "content" + ); + assessment = await assessExportRisk(makeGraph(), makeRunPlan()); + expect(assessment.riskReport.level).toBe("medium"); + + // Score 6+ → high (score 6: overwrite-existing-output + yolo-mode) + const graph = makeGraph({ mode: "yolo" }); + await fs.writeFile( + path.join(WORKSPACE_ROOT, "artifacts", "test-project", "another.ts"), + "more content" + ); + assessment = await assessExportRisk(graph, makeRunPlan()); + expect(assessment.riskReport.level).toBe("high"); + }); + }); + + it("sets requiresApproval to true when essential mode + existing output", async () => { + await withEnv(async () => { + await fs.mkdir(path.join(WORKSPACE_ROOT, "artifacts", "test-project"), { recursive: true }); + await fs.writeFile( + path.join(WORKSPACE_ROOT, "artifacts", "test-project", "file.ts"), + "existing" + ); + + const assessment = await assessExportRisk(makeGraph({ mode: "essential" }), makeRunPlan()); + expect(assessment.riskReport.requiresApproval).toBe(true); + }); + }); + + it("sets requiresApproval to false when yolo mode", async () => { + await withEnv(async () => { + await fs.mkdir(path.join(WORKSPACE_ROOT, "artifacts", "test-project"), { recursive: true }); + await fs.writeFile( + path.join(WORKSPACE_ROOT, "artifacts", "test-project", "file.ts"), + "existing" + ); + + const assessment = await assessExportRisk(makeGraph({ mode: "yolo" }), makeRunPlan()); + expect(assessment.riskReport.requiresApproval).toBe(false); + }); + }); + + it("produces a consistent fingerprint for the same graph + plan + output", async () => { + await withEnv(async () => { + const graph = makeGraph(); + const plan = makeRunPlan(); + + const a = await assessExportRisk(graph, plan, "/tmp/output"); + const b = await assessExportRisk(graph, plan, "/tmp/output"); + + expect(a.fingerprint).toBe(b.fingerprint); + }); + }); + + it("produces a different fingerprint when graph changes", async () => { + await withEnv(async () => { + const graph1 = makeGraph(); + const graph2 = makeGraph({ projectName: "different-project" }); + const plan = makeRunPlan(); + + const a = await assessExportRisk(graph1, plan); + const b = await assessExportRisk(graph2, plan); + + expect(a.fingerprint).not.toBe(b.fingerprint); + }); + }); + + it("hasExistingOutput is true when output dir has files", async () => { + await withEnv(async () => { + await fs.mkdir(path.join(WORKSPACE_ROOT, "artifacts", "test-project"), { recursive: true }); + await fs.writeFile( + path.join(WORKSPACE_ROOT, "artifacts", "test-project", "file.ts"), + "content" + ); + + const assessment = await assessExportRisk(makeGraph(), makeRunPlan()); + expect(assessment.hasExistingOutput).toBe(true); + }); + }); + + it("hasExistingOutput is false when output dir does not exist", async () => { + await withEnv(async () => { + const assessment = await assessExportRisk(makeGraph(), makeRunPlan()); + expect(assessment.hasExistingOutput).toBe(false); + }); + }); + }); +}); diff --git a/packages/codeflow-store/src/run/index.ts b/packages/codeflow-store/src/run/index.ts new file mode 100644 index 0000000..7c599c8 --- /dev/null +++ b/packages/codeflow-store/src/run/index.ts @@ -0,0 +1,18 @@ +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; + +import type { RunRecord } from "@abhinav2203/codeflow-core/schema"; +import { runPath } from "../shared/utils.js"; + +const ensureDir = async (dirPath: string): Promise => { + await fs.mkdir(dirPath, { recursive: true }); +}; + +export const createRunId = (): string => crypto.randomUUID(); + +export const saveRunRecord = async (runRecord: RunRecord): Promise => { + const filePath = runPath(runRecord.id); + await ensureDir(path.dirname(filePath)); + await fs.writeFile(filePath, `${JSON.stringify(runRecord, null, 2)}\n`, "utf8"); +}; diff --git a/packages/codeflow-store/src/run/run.test.ts b/packages/codeflow-store/src/run/run.test.ts new file mode 100644 index 0000000..bec8327 --- /dev/null +++ b/packages/codeflow-store/src/run/run.test.ts @@ -0,0 +1,138 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import fs from "node:fs/promises"; +import fsSync from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { createRunId, saveRunRecord } from "./index.js"; +import type { RunRecord } from "@abhinav2203/codeflow-core/schema"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const STORE_ROOT = path.join(__dirname, "../../.test-store"); + +const cleanStore = () => { + try { + fsSync.rmSync(STORE_ROOT, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors — best effort + } +}; + +const withEnv = async (fn: () => Promise): Promise => { + const original = process.env.CODEFLOW_STORE_ROOT; + process.env.CODEFLOW_STORE_ROOT = STORE_ROOT; + try { + return await fn(); + } finally { + process.env.CODEFLOW_STORE_ROOT = original ?? ""; + cleanStore(); + } +}; + +const makeRunRecord = (id: string): RunRecord => ({ + id, + projectName: "test-project", + action: "build", + createdAt: new Date().toISOString(), + runPlan: { + generatedAt: new Date().toISOString(), + tasks: [ + { + id: "task-1", + nodeId: "n1", + title: "Implement auth", + kind: "module", + dependsOn: [], + batchIndex: 0 + } + ], + batches: [{ index: 0, taskIds: ["task-1"] }], + warnings: [] + }, + riskReport: undefined, + approvalId: undefined, + executionReport: undefined, + exportResult: undefined +}); + +describe("run", () => { + beforeEach(() => { + cleanStore(); + }); + + describe("createRunId", () => { + it("returns a valid UUID v4", () => { + const id = createRunId(); + expect(typeof id).toBe("string"); + expect(id).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + ); + }); + + it("returns unique IDs each call", () => { + const id1 = createRunId(); + const id2 = createRunId(); + expect(id1).not.toBe(id2); + }); + }); + + describe("saveRunRecord", () => { + it("writes the run record to the correct path", async () => { + await withEnv(async () => { + const record = makeRunRecord("run-123"); + await saveRunRecord(record); + + const filePath = path.join(STORE_ROOT, "runs", "run-123.json"); + const content = await fs.readFile(filePath, "utf8"); + const parsed = JSON.parse(content); + expect(parsed.id).toBe("run-123"); + expect(parsed.projectName).toBe("test-project"); + }); + }); + + it("creates parent directories if they do not exist", async () => { + await withEnv(async () => { + const record = makeRunRecord("new-run-456"); + await saveRunRecord(record); + + const filePath = path.join(STORE_ROOT, "runs", "new-run-456.json"); + const stat = await fs.stat(filePath); + expect(stat.isFile()).toBe(true); + }); + }); + + it("stores the complete run record including tasks and batches", async () => { + await withEnv(async () => { + const record = makeRunRecord("run-full"); + await saveRunRecord(record); + + const filePath = path.join(STORE_ROOT, "runs", "run-full.json"); + const content = await fs.readFile(filePath, "utf8"); + const parsed = JSON.parse(content); + expect(parsed.runPlan.tasks).toHaveLength(1); + expect(parsed.runPlan.batches).toHaveLength(1); + expect(parsed.runPlan.tasks[0].id).toBe("task-1"); + }); + }); + + it("can store a record with optional fields as null", async () => { + await withEnv(async () => { + const record: RunRecord = { + ...makeRunRecord("run-minimal"), + riskReport: undefined, + approvalId: undefined, + executionReport: undefined, + exportResult: undefined + }; + await saveRunRecord(record); + + const result = await fs.readFile( + path.join(STORE_ROOT, "runs", "run-minimal.json"), + "utf8" + ); + const parsed = JSON.parse(result); + expect(parsed.id).toBe("run-minimal"); + }); + }); + }); +}); diff --git a/packages/codeflow-store/src/session.test.ts b/packages/codeflow-store/src/session.test.ts new file mode 100644 index 0000000..27b296f --- /dev/null +++ b/packages/codeflow-store/src/session.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect } from "vitest"; +import { createSessionId } from "./session/index.js"; +import { assessExportRisk } from "./risk/index.js"; +import type { BlueprintGraph, RunPlan } from "@abhinav2203/codeflow-core/schema"; + +describe("codeflow-store", () => { + describe("session", () => { + it("creates a valid session ID", () => { + const id = createSessionId(); + expect(typeof id).toBe("string"); + expect(id.length).toBeGreaterThan(0); + expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); + }); + }); + + describe("risk", () => { + it("assesses export risk for a minimal blueprint", async () => { + const graph: BlueprintGraph = { + projectName: "test-project", + mode: "essential", + generatedAt: new Date().toISOString(), + nodes: [], + edges: [], + workflows: [], + warnings: [] + }; + const runPlan: RunPlan = { + generatedAt: new Date().toISOString(), + tasks: [], + batches: [], + warnings: [] + }; + + const assessment = await assessExportRisk(graph, runPlan); + + expect(assessment).toHaveProperty("fingerprint"); + expect(assessment).toHaveProperty("outputDir"); + expect(assessment).toHaveProperty("riskReport"); + expect(assessment.riskReport).toHaveProperty("score"); + expect(assessment.riskReport).toHaveProperty("level"); + expect(assessment.riskReport.level).toBe("low"); + }); + + it("flags yolo mode in risk assessment", async () => { + const graph: BlueprintGraph = { + projectName: "yolo-test", + mode: "yolo", + generatedAt: new Date().toISOString(), + nodes: [], + edges: [], + workflows: [], + warnings: [] + }; + const runPlan: RunPlan = { + generatedAt: new Date().toISOString(), + tasks: [], + batches: [], + warnings: [] + }; + + const assessment = await assessExportRisk(graph, runPlan); + + const yoloFactor = assessment.riskReport.factors.find((f) => f.code === "yolo-mode"); + expect(yoloFactor).toBeDefined(); + expect(yoloFactor?.score).toBe(2); + }); + }); +}); diff --git a/packages/codeflow-store/src/session/index.ts b/packages/codeflow-store/src/session/index.ts new file mode 100644 index 0000000..ade4db5 --- /dev/null +++ b/packages/codeflow-store/src/session/index.ts @@ -0,0 +1,80 @@ +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; + +import type { + BlueprintGraph, + ExecutionReport, + ExportResult, + PersistedSession, + RiskReport, + RunPlan +} from "@abhinav2203/codeflow-core/schema"; +import { persistedSessionSchema } from "@abhinav2203/codeflow-core/schema"; +import { latestSessionPath, sessionDirForProject, sessionHistoryPath } from "../shared/utils.js"; + +const ensureDir = async (dirPath: string): Promise => { + await fs.mkdir(dirPath, { recursive: true }); +}; + +const writeSessionFile = async (filePath: string, session: PersistedSession): Promise => { + await ensureDir(path.dirname(filePath)); + await fs.writeFile(filePath, `${JSON.stringify(session, null, 2)}\n`, "utf8"); +}; + +export const createSessionId = (): string => crypto.randomUUID(); + +export const saveSession = async (session: PersistedSession): Promise => { + await ensureDir(sessionDirForProject(session.projectName)); + await writeSessionFile(latestSessionPath(session.projectName), session); + await writeSessionFile(sessionHistoryPath(session.projectName, session.sessionId), session); +}; + +export const loadLatestSession = async (projectName: string): Promise => { + try { + const content = await fs.readFile(latestSessionPath(projectName), "utf8"); + return persistedSessionSchema.parse(JSON.parse(content)); + } catch { + return null; + } +}; + +export const upsertSession = async ({ + graph, + runPlan, + repoPath, + lastRiskReport, + lastExportResult, + lastExecutionReport, + approvalId, + sessionId +}: { + graph: BlueprintGraph; + runPlan: RunPlan; + repoPath?: string; + lastRiskReport?: RiskReport; + lastExportResult?: ExportResult; + lastExecutionReport?: ExecutionReport; + approvalId?: string; + sessionId?: string; +}): Promise => { + const existing = await loadLatestSession(graph.projectName); + const normalizedGraph = persistedSessionSchema.shape.graph.parse(graph); + const nextSession = persistedSessionSchema.parse({ + sessionId: sessionId ?? existing?.sessionId ?? createSessionId(), + projectName: normalizedGraph.projectName, + updatedAt: new Date().toISOString(), + repoPath: repoPath?.trim() ? path.resolve(repoPath) : existing?.repoPath, + graph: normalizedGraph, + runPlan, + lastRiskReport: lastRiskReport ?? existing?.lastRiskReport, + lastExportResult: lastExportResult ?? existing?.lastExportResult, + lastExecutionReport: lastExecutionReport ?? existing?.lastExecutionReport, + approvalIds: approvalId + ? [...new Set([...(existing?.approvalIds ?? []), approvalId])] + : (existing?.approvalIds ?? []) + }); + + await saveSession(nextSession); + return nextSession; +}; diff --git a/packages/codeflow-store/src/session/session.test.ts b/packages/codeflow-store/src/session/session.test.ts new file mode 100644 index 0000000..379c69f --- /dev/null +++ b/packages/codeflow-store/src/session/session.test.ts @@ -0,0 +1,283 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import fs from "node:fs/promises"; +import fsSync from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { createSessionId, saveSession, loadLatestSession, upsertSession } from "./index.js"; +import type { BlueprintGraph, RunPlan, PersistedSession } from "@abhinav2203/codeflow-core/schema"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const STORE_ROOT = path.join(__dirname, "../../.test-store"); + +const cleanStore = () => { + try { + fsSync.rmSync(STORE_ROOT, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors — best effort + } +}; + +const withEnv = async (fn: () => Promise): Promise => { + const original = process.env.CODEFLOW_STORE_ROOT; + process.env.CODEFLOW_STORE_ROOT = STORE_ROOT; + try { + return await fn(); + } finally { + process.env.CODEFLOW_STORE_ROOT = original ?? ""; + cleanStore(); + } +}; + +const makeGraph = (overrides: Partial = {}): BlueprintGraph => { + const base: BlueprintGraph = { + projectName: "test-project", + mode: "essential", + phase: "spec", + generatedAt: new Date().toISOString(), + nodes: [], + edges: [], + workflows: [], + warnings: [] + }; + return { ...base, ...overrides } as BlueprintGraph; +}; + +const makeRunPlan = (): RunPlan => ({ + generatedAt: new Date().toISOString(), + tasks: [], + batches: [], + warnings: [] +}); + +const makeSession = (overrides: Partial = {}): PersistedSession => ({ + sessionId: "session-1", + projectName: "test-project", + updatedAt: new Date().toISOString(), + graph: makeGraph(), + runPlan: makeRunPlan(), + lastRiskReport: undefined, + lastExportResult: undefined, + lastExecutionReport: undefined, + approvalIds: [], + ...overrides +} as PersistedSession); + +describe("session", () => { + beforeEach(() => { + cleanStore(); + }); + + describe("createSessionId", () => { + it("returns a valid UUID v4", () => { + const id = createSessionId(); + expect(typeof id).toBe("string"); + expect(id).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + ); + }); + + it("returns unique IDs each call", () => { + const id1 = createSessionId(); + const id2 = createSessionId(); + expect(id1).not.toBe(id2); + }); + }); + + describe("saveSession", () => { + it("writes a latest.json and a history file", async () => { + await withEnv(async () => { + const session = makeSession(); + await saveSession(session); + + const latestPath = path.join( + STORE_ROOT, + "sessions", + "test-project", + "latest.json" + ); + const historyPath = path.join( + STORE_ROOT, + "sessions", + "test-project", + "history", + "session-1.json" + ); + + const latestContent = await fs.readFile(latestPath, "utf8"); + expect(JSON.parse(latestContent).sessionId).toBe("session-1"); + + const historyContent = await fs.readFile(historyPath, "utf8"); + expect(JSON.parse(historyContent).sessionId).toBe("session-1"); + }); + }); + + it("overwrites latest.json when saving again", async () => { + await withEnv(async () => { + const session1 = makeSession({ sessionId: "session-1", updatedAt: "2026-01-01T00:00:00.000Z" }); + await saveSession(session1); + + const session2 = makeSession({ sessionId: "session-2", updatedAt: "2026-01-02T00:00:00.000Z" }); + await saveSession(session2); + + const latestPath = path.join( + STORE_ROOT, + "sessions", + "test-project", + "latest.json" + ); + const latest = JSON.parse(await fs.readFile(latestPath, "utf8")); + expect(latest.sessionId).toBe("session-2"); + }); + }); + }); + + describe("loadLatestSession", () => { + it("returns null when no session exists", async () => { + await withEnv(async () => { + const result = await loadLatestSession("nonexistent"); + expect(result).toBeNull(); + }); + }); + + it("returns the session when it exists", async () => { + await withEnv(async () => { + const session = makeSession(); + await saveSession(session); + + const result = await loadLatestSession("test-project"); + expect(result).not.toBeNull(); + expect(result!.sessionId).toBe("session-1"); + expect(result!.projectName).toBe("test-project"); + }); + }); + + it("returns null when latest.json is malformed", async () => { + await withEnv(async () => { + const latestPath = path.join( + STORE_ROOT, + "sessions", + "test-project", + "latest.json" + ); + await fs.mkdir(path.dirname(latestPath), { recursive: true }); + await fs.writeFile(latestPath, "{ not valid json"); + + const result = await loadLatestSession("test-project"); + expect(result).toBeNull(); + }); + }); + }); + + describe("upsertSession", () => { + it("creates a new session when none exists", async () => { + await withEnv(async () => { + const graph = makeGraph(); + const runPlan = makeRunPlan(); + + const session = await upsertSession({ graph, runPlan }); + + expect(session.sessionId).toBeDefined(); + expect(session.projectName).toBe("test-project"); + expect(session.graph).toBeDefined(); + expect(session.runPlan).toBeDefined(); + }); + }); + + it("preserves existing sessionId on update", async () => { + await withEnv(async () => { + const graph = makeGraph(); + const runPlan = makeRunPlan(); + + const first = await upsertSession({ graph, runPlan, sessionId: "my-session" }); + const second = await upsertSession({ graph, runPlan, sessionId: first.sessionId }); + + expect(second.sessionId).toBe(first.sessionId); + }); + }); + + it("accumulates approvalIds", async () => { + await withEnv(async () => { + const graph = makeGraph(); + const runPlan = makeRunPlan(); + + const first = await upsertSession({ graph, runPlan, approvalId: "approval-1" }); + const second = await upsertSession({ graph, runPlan, approvalId: "approval-2" }); + + expect(first.approvalIds).toContain("approval-1"); + expect(second.approvalIds).toContain("approval-1"); + expect(second.approvalIds).toContain("approval-2"); + expect(second.approvalIds).toHaveLength(2); + }); + }); + + it("does not duplicate approvalIds on multiple upserts", async () => { + await withEnv(async () => { + const graph = makeGraph(); + const runPlan = makeRunPlan(); + + await upsertSession({ graph, runPlan, approvalId: "approval-1" }); + await upsertSession({ graph, runPlan, approvalId: "approval-1" }); + + const latest = await loadLatestSession("test-project"); + expect(latest!.approvalIds).toHaveLength(1); + expect(latest!.approvalIds[0]).toBe("approval-1"); + }); + }); + + it("preserves lastRiskReport from existing session", async () => { + await withEnv(async () => { + const graph = makeGraph(); + const runPlan = makeRunPlan(); + const riskReport = { + score: 5, + level: "medium" as const, + requiresApproval: true, + factors: [] + }; + + await upsertSession({ graph, runPlan }); + await upsertSession({ graph, runPlan, lastRiskReport: riskReport }); + + const latest = await loadLatestSession("test-project"); + expect(latest!.lastRiskReport).toBeDefined(); + expect(latest!.lastRiskReport!.score).toBe(5); + }); + }); + + it("overrides lastRiskReport when explicitly passed", async () => { + await withEnv(async () => { + const graph = makeGraph(); + const runPlan = makeRunPlan(); + + await upsertSession({ + graph, + runPlan, + lastRiskReport: { score: 2, level: "low", requiresApproval: false, factors: [] } + }); + await upsertSession({ + graph, + runPlan, + lastRiskReport: { score: 8, level: "high", requiresApproval: true, factors: [] } + }); + + const latest = await loadLatestSession("test-project"); + expect(latest!.lastRiskReport!.score).toBe(8); + expect(latest!.lastRiskReport!.level).toBe("high"); + }); + }); + + it("sets updatedAt on every upsert", async () => { + await withEnv(async () => { + const graph = makeGraph(); + const runPlan = makeRunPlan(); + + const first = await upsertSession({ graph, runPlan }); + await new Promise((r) => setTimeout(r, 10)); + const second = await upsertSession({ graph, runPlan }); + + expect(second.updatedAt).not.toBe(first.updatedAt); + }); + }); + }); +}); diff --git a/packages/codeflow-store/src/shared/file-tree.ts b/packages/codeflow-store/src/shared/file-tree.ts new file mode 100644 index 0000000..28ba677 --- /dev/null +++ b/packages/codeflow-store/src/shared/file-tree.ts @@ -0,0 +1,59 @@ +import { readdir, stat } from "node:fs/promises"; +import { join, basename } from "node:path"; + +/** + * Information about a file or directory in the repository. + */ +export interface FileInfo { + path: string; + name: string; + isDirectory: boolean; +} + +const ALLOWED_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".json", ".md"]); + +/** + * Checks if a file has an allowed extension. + */ +function hasAllowedExtension(fileName: string): boolean { + const extension = fileName.slice(fileName.lastIndexOf(".")).toLowerCase(); + return ALLOWED_EXTENSIONS.has(extension); +} + +/** + * Recursively scans a repository path for files matching allowed extensions. + * Returns a flat array of FileInfo objects for all matching files. + * + * @param repoPath - The root path to scan + * @returns Promise resolving to an array of FileInfo objects + */ +export async function scanRepoFiles(repoPath: string): Promise { + const files: FileInfo[] = []; + + async function scanDirectory(currentPath: string): Promise { + const entries = await readdir(currentPath, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = join(currentPath, entry.name); + + if (entry.isDirectory()) { + await scanDirectory(fullPath); + } else if (entry.isFile() && hasAllowedExtension(entry.name)) { + files.push({ + path: fullPath, + name: basename(entry.name), + isDirectory: false + }); + } + } + } + + const repoStat = await stat(repoPath); + if (!repoStat.isDirectory()) { + throw new Error(`Path is not a directory: ${repoPath}`); + } + + await scanDirectory(repoPath); + + return files; +} diff --git a/packages/codeflow-store/src/shared/run-command.ts b/packages/codeflow-store/src/shared/run-command.ts new file mode 100644 index 0000000..6e19545 --- /dev/null +++ b/packages/codeflow-store/src/shared/run-command.ts @@ -0,0 +1,117 @@ +import { spawn } from "node:child_process"; + +export type RunCommandOptions = { + cwd: string; + env?: NodeJS.ProcessEnv; + timeoutMs?: number; + stdoutMaxBytes?: number; + stderrMaxBytes?: number; +}; + +export type RunCommandResult = { + exitCode: number | null; + stdout: string; + stderr: string; + timedOut: boolean; + outputCapped: boolean; + signal: NodeJS.Signals | null; +}; + +const DEFAULT_TIMEOUT_MS = 20_000; +const DEFAULT_STDOUT_MAX_BYTES = 64 * 1024; +const DEFAULT_STDERR_MAX_BYTES = 128 * 1024; + +const appendChunk = ( + current: string, + chunk: Buffer, + maxBytes: number +): { next: string; capped: boolean } => { + const next = current + chunk.toString("utf8"); + if (Buffer.byteLength(next, "utf8") <= maxBytes) { + return { next, capped: false }; + } + + const truncated = Buffer.from(next, "utf8").subarray(0, maxBytes).toString("utf8"); + return { next: truncated, capped: true }; +}; + +export const runCommand = ( + command: string, + args: string[], + options: RunCommandOptions +): Promise => + new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd: options.cwd, + env: options.env, + stdio: ["ignore", "pipe", "pipe"] + }); + + let stdout = ""; + let stderr = ""; + let timedOut = false; + let outputCapped = false; + let settled = false; + + const settle = (result: RunCommandResult) => { + if (settled) { + return; + } + settled = true; + resolve(result); + }; + + const timeoutId = setTimeout(() => { + timedOut = true; + child.kill("SIGKILL"); + }, options.timeoutMs ?? DEFAULT_TIMEOUT_MS); + + child.stdout.on("data", (chunk: Buffer) => { + const updated = appendChunk(stdout, chunk, options.stdoutMaxBytes ?? DEFAULT_STDOUT_MAX_BYTES); + stdout = updated.next; + if (updated.capped) { + outputCapped = true; + child.kill("SIGKILL"); + } + }); + + child.stderr.on("data", (chunk: Buffer) => { + const updated = appendChunk(stderr, chunk, options.stderrMaxBytes ?? DEFAULT_STDERR_MAX_BYTES); + stderr = updated.next; + if (updated.capped) { + outputCapped = true; + child.kill("SIGKILL"); + } + }); + + child.on("error", (error) => { + clearTimeout(timeoutId); + if (settled) { + return; + } + settled = true; + child.kill("SIGKILL"); + reject(error); + }); + + child.on("close", (code, signal) => { + clearTimeout(timeoutId); + + let finalStderr = stderr; + if (timedOut) { + finalStderr = `${finalStderr}\nCommand timed out after ${options.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms.`.trim(); + } + if (outputCapped) { + finalStderr = `${finalStderr}\nCommand output exceeded the configured cap.`.trim(); + } + + settle({ + exitCode: code, + stdout, + stderr: finalStderr, + timedOut, + outputCapped, + signal + }); + }); + }); diff --git a/packages/codeflow-store/src/shared/terminal-sessions.ts b/packages/codeflow-store/src/shared/terminal-sessions.ts new file mode 100644 index 0000000..d8a40ef --- /dev/null +++ b/packages/codeflow-store/src/shared/terminal-sessions.ts @@ -0,0 +1,246 @@ +import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { randomUUID } from "node:crypto"; + +export const TERMINAL_REPO_PATH_HEADER = "x-codeflow-repo-path"; + +export type TerminalSessionStatus = "running" | "exited" | "error"; + +export type TerminalSessionSummary = { + id: string; + title: string; + cwd: string; + shell: string; + status: TerminalSessionStatus; + startedAt: string; + lastActivityAt: string; + exitCode: number | null; +}; + +export type TerminalSessionSnapshot = TerminalSessionSummary & { + output: string; + truncated: boolean; +}; + +type InternalTerminalSession = TerminalSessionSnapshot & { + child: ChildProcessWithoutNullStreams; +}; + +const DEFAULT_WORKSPACE_ROOT = + process.env.CODEFLOW_REPO_ROOT ?? /* turbopackIgnore: true */ process.cwd(); +const OUTPUT_CAP_BYTES = 128 * 1024; +const OUTPUT_TRUNCATION_NOTICE = "[CodeFlow] Older terminal output truncated.\n"; + +const sessions = new Map(); +let sessionCounter = 0; + +const stripTruncationNotice = (value: string): string => + value.startsWith(OUTPUT_TRUNCATION_NOTICE) ? value.slice(OUTPUT_TRUNCATION_NOTICE.length) : value; + +const clampOutput = (value: string): { output: string; truncated: boolean } => { + const buffer = Buffer.from(value, "utf8"); + if (buffer.byteLength <= OUTPUT_CAP_BYTES) { + return { output: value, truncated: false }; + } + + const noticeBytes = Buffer.byteLength(OUTPUT_TRUNCATION_NOTICE, "utf8"); + const remainingBytes = Math.max(0, OUTPUT_CAP_BYTES - noticeBytes); + const tail = buffer.subarray(Math.max(0, buffer.byteLength - remainingBytes)).toString("utf8"); + return { + output: `${OUTPUT_TRUNCATION_NOTICE}${tail}`, + truncated: true + }; +}; + +const appendOutput = (session: InternalTerminalSession, chunk: string) => { + if (!chunk) { + return; + } + + const next = `${stripTruncationNotice(session.output)}${chunk}`; + const clamped = clampOutput(next); + session.output = clamped.output; + session.truncated = clamped.truncated; + session.lastActivityAt = new Date().toISOString(); +}; + +const toSummary = (session: InternalTerminalSession): TerminalSessionSummary => ({ + id: session.id, + title: session.title, + cwd: session.cwd, + shell: session.shell, + status: session.status, + startedAt: session.startedAt, + lastActivityAt: session.lastActivityAt, + exitCode: session.exitCode +}); + +const toSnapshot = (session: InternalTerminalSession): TerminalSessionSnapshot => ({ + ...toSummary(session), + output: session.output, + truncated: session.truncated +}); + +const resolveInitialCwd = async (cwd?: string): Promise => { + const resolved = cwd?.trim() ? path.resolve(cwd.trim()) : path.resolve(DEFAULT_WORKSPACE_ROOT); + const stats = await fs.stat(resolved).catch(() => null); + + if (!stats?.isDirectory()) { + throw new Error(`Terminal working directory does not exist or is not a directory: ${resolved}`); + } + + return resolved; +}; + +const getShellPath = (): string => { + const configuredShell = process.env.CODEFLOW_TERMINAL_SHELL?.trim(); + if (configuredShell) { + return configuredShell; + } + + return process.env.SHELL?.trim() || "/bin/sh"; +}; + +const recordInput = (session: InternalTerminalSession, input: string) => { + const printable = input + .replace(/\r/g, "") + .split("\n") + .map((line) => line.trimEnd()) + .filter((line) => line.length > 0) + .join("\n"); + + if (!printable) { + return; + } + + appendOutput( + session, + `${printable + .split("\n") + .map((line) => `$ ${line}`) + .join("\n")}\n` + ); +}; + +export const listTerminalSessions = (): TerminalSessionSummary[] => + [...sessions.values()] + .map(toSummary) + .sort((left, right) => right.startedAt.localeCompare(left.startedAt)); + +export const getTerminalSession = (id: string): TerminalSessionSnapshot | null => { + const session = sessions.get(id); + return session ? toSnapshot(session) : null; +}; + +export const createTerminalSession = async (options?: { + cwd?: string; + title?: string; +}): Promise => { + const cwd = await resolveInitialCwd(options?.cwd); + const shell = getShellPath(); + const child = spawn(shell, [], { + cwd, + env: { + ...process.env, + TERM: process.env.TERM || "xterm-256color" + }, + stdio: ["pipe", "pipe", "pipe"] + }); + const startedAt = new Date().toISOString(); + sessionCounter += 1; + + const session: InternalTerminalSession = { + id: randomUUID(), + title: options?.title?.trim() || `Shell ${sessionCounter}`, + cwd, + shell, + status: "running", + startedAt, + lastActivityAt: startedAt, + exitCode: null, + output: "", + truncated: false, + child + }; + + child.stdout.on("data", (chunk: Buffer) => { + appendOutput(session, chunk.toString("utf8")); + }); + + child.stderr.on("data", (chunk: Buffer) => { + appendOutput(session, chunk.toString("utf8")); + }); + + child.on("error", (error) => { + session.status = "error"; + session.exitCode = null; + appendOutput(session, `\n[CodeFlow] Terminal process error: ${error.message}\n`); + }); + + child.on("close", (code) => { + session.status = session.status === "error" ? "error" : "exited"; + session.exitCode = code; + appendOutput(session, `\n[CodeFlow] Terminal exited with code ${code ?? "unknown"}.\n`); + }); + + sessions.set(session.id, session); + return toSnapshot(session); +}; + +export const writeTerminalInput = async ( + id: string, + input: string, + options?: { echoInput?: boolean } +): Promise => { + const session = sessions.get(id); + if (!session) { + throw new Error(`Terminal session ${id} was not found.`); + } + + if (session.status !== "running") { + throw new Error(`Terminal session ${id} is no longer running.`); + } + + if (options?.echoInput ?? true) { + recordInput(session, input); + } + + await new Promise((resolve, reject) => { + session.child.stdin.write(input, (error) => { + if (error) { + reject(error); + return; + } + + resolve(); + }); + }); + + session.lastActivityAt = new Date().toISOString(); + return toSnapshot(session); +}; + +export const closeTerminalSession = (id: string): boolean => { + const session = sessions.get(id); + if (!session) { + return false; + } + + if (session.status === "running") { + session.child.kill("SIGTERM"); + } + + sessions.delete(id); + return true; +}; + +export const shutdownAllTerminalSessions = () => { + for (const session of sessions.values()) { + if (session.status === "running") { + session.child.kill("SIGTERM"); + } + } + + sessions.clear(); +}; diff --git a/packages/codeflow-store/src/shared/utils.ts b/packages/codeflow-store/src/shared/utils.ts new file mode 100644 index 0000000..af9ea08 --- /dev/null +++ b/packages/codeflow-store/src/shared/utils.ts @@ -0,0 +1,81 @@ +import os from "node:os"; +import path from "node:path"; + +const slugify = (value: string): string => + value + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, "") + .slice(0, 80) || "node"; + +const resolveDefaultStoreRoot = (): string => { + if (process.env.VITEST || process.env.NODE_ENV === "test") { + return path.join( + process.cwd(), + ".codeflow-store-test", + `worker-${process.env.VITEST_WORKER_ID ?? "0"}` + ); + } + + return path.join(os.homedir(), ".codeflow-store"); +}; + +export const getStoreRoot = (): string => + process.env.CODEFLOW_STORE_ROOT + ? path.resolve(process.env.CODEFLOW_STORE_ROOT) + : resolveDefaultStoreRoot(); + +export const sessionDirForProject = (projectName: string): string => + path.join(getStoreRoot(), "sessions", slugify(projectName)); + +export const latestSessionPath = (projectName: string): string => + path.join(sessionDirForProject(projectName), "latest.json"); + +export const sessionHistoryPath = (projectName: string, sessionId: string): string => + path.join(sessionDirForProject(projectName), "history", `${sessionId}.json`); + +export const approvalPath = (approvalId: string): string => { + const safeApprovalId = path.basename(approvalId); + + if (safeApprovalId !== approvalId) { + throw new Error(`Invalid approval ID: must not contain path separators`); + } + + return path.join(getStoreRoot(), "approvals", `${safeApprovalId}.json`); +}; + +export const runPath = (runId: string): string => { + const safeRunId = path.basename(runId); + + if (safeRunId !== runId) { + throw new Error(`Invalid run ID: must not contain path separators`); + } + + return path.join(getStoreRoot(), "runs", `${safeRunId}.json`); +}; + +export const checkpointPath = (checkpointId: string): string => { + const safeCheckpointId = path.basename(checkpointId); + + if (safeCheckpointId !== checkpointId) { + throw new Error(`Invalid checkpoint ID: must not contain path separators`); + } + + return path.join(getStoreRoot(), "checkpoints", safeCheckpointId); +}; + +export const observabilityPath = (projectName: string): string => + path.join(getStoreRoot(), "observability", `${slugify(projectName)}.json`); + +export const branchDirForProject = (projectName: string): string => + path.join(getStoreRoot(), "branches", slugify(projectName)); + +export const branchPath = (projectName: string, branchId: string): string => { + const safeBranchId = path.basename(branchId); + + if (safeBranchId !== branchId) { + throw new Error("Invalid branch ID"); + } + + return path.join(branchDirForProject(projectName), `${safeBranchId}.json`); +}; diff --git a/packages/codeflow-store/src/store/index.ts b/packages/codeflow-store/src/store/index.ts new file mode 100644 index 0000000..24b856d --- /dev/null +++ b/packages/codeflow-store/src/store/index.ts @@ -0,0 +1,134 @@ +import { create } from "zustand"; + +import type { BlueprintGraph, BlueprintNode } from "@abhinav2203/codeflow-core/schema"; + +type GraphStateUpdater = BlueprintGraph | null | ((current: BlueprintGraph | null) => BlueprintGraph | null); +type NodeUpdater = Partial | ((node: BlueprintNode) => BlueprintNode); + +export type WorkbenchMode = "graph" | "ide"; + +export interface FloatingGraphPanel { + visible: boolean; + x: number; + y: number; + width: number; + height: number; +} + +export interface BlueprintStore { + graph: BlueprintGraph | null; + setGraph: (next: GraphStateUpdater) => void; + updateNode: (id: string, patch: NodeUpdater) => void; + openFiles: string[]; + activeFile: string | null; + setOpenFiles: (paths: string[]) => void; + setActiveFile: (path: string | null) => void; + closeFile: (path: string) => void; + repoPath: string | null; + setRepoPath: (path: string | null) => void; + mode: WorkbenchMode; + setMode: (mode: WorkbenchMode) => void; + floatingGraph: FloatingGraphPanel; + setFloatingGraph: (panel: Partial) => void; + selectedNodeId: string | null; + setSelectedNodeId: (id: string | null) => void; + dirtyFiles: Record; + setFileDirty: (path: string, dirty: boolean) => void; + clearFileDirty: (path: string) => void; +} + +const resolveGraphUpdate = ( + current: BlueprintGraph | null, + next: GraphStateUpdater +): BlueprintGraph | null => (typeof next === "function" ? next(current) : next); + +const resolveNodeUpdate = (node: BlueprintNode, patch: NodeUpdater): BlueprintNode => + typeof patch === "function" ? patch(node) : { ...node, ...patch }; + +export const useBlueprintStore = create((set) => ({ + graph: null, + setGraph: (next) => + set((state) => ({ + graph: resolveGraphUpdate(state.graph, next) + })), + updateNode: (id, patch) => + set((state) => { + if (!state.graph) { + return state; + } + return { + graph: { + ...state.graph, + nodes: state.graph.nodes.map((node) => + node.id === id ? resolveNodeUpdate(node, patch) : node + ) + } + }; + }), + openFiles: [], + activeFile: null, + setOpenFiles: (paths) => + set(() => ({ + openFiles: paths + })), + setActiveFile: (path) => + set((state) => ({ + activeFile: path, + floatingGraph: { + ...state.floatingGraph, + visible: path !== null + } + })), + closeFile: (path) => + set((state) => { + const nextOpenFiles = state.openFiles.filter((f) => f !== path); + const nextActiveFile = + state.activeFile === path + ? nextOpenFiles[nextOpenFiles.length - 1] ?? null + : state.activeFile; + + return { + openFiles: nextOpenFiles, + activeFile: nextActiveFile, + floatingGraph: { + ...state.floatingGraph, + visible: nextActiveFile !== null + }, + dirtyFiles: { ...state.dirtyFiles, [path]: false } + }; + }), + repoPath: null, + setRepoPath: (path) => set(() => ({ repoPath: path })), + mode: "ide", + setMode: (mode) => + set((state) => ({ + mode, + floatingGraph: { + ...state.floatingGraph, + visible: state.activeFile !== null + } + })), + floatingGraph: { + visible: false, + x: 0, + y: 0, + width: 400, + height: 350 + }, + setFloatingGraph: (panel) => + set((state) => ({ + floatingGraph: { ...state.floatingGraph, ...panel } + })), + selectedNodeId: null, + setSelectedNodeId: (id) => set(() => ({ selectedNodeId: id })), + dirtyFiles: {}, + setFileDirty: (path, dirty) => + set((state) => ({ + dirtyFiles: { ...state.dirtyFiles, [path]: dirty } + })), + clearFileDirty: (path) => + set((state) => { + const { [path]: _, ...rest } = state.dirtyFiles; + return { dirtyFiles: rest }; + }) +})); diff --git a/packages/codeflow-store/test-fixtures/minimal-blueprint.json b/packages/codeflow-store/test-fixtures/minimal-blueprint.json new file mode 100644 index 0000000..1f899da --- /dev/null +++ b/packages/codeflow-store/test-fixtures/minimal-blueprint.json @@ -0,0 +1,6 @@ +{ + "projectName": "test-project", + "mode": "essential", + "nodes": [], + "edges": [] +} diff --git a/packages/codeflow-store/test-fixtures/sample-blueprint.json b/packages/codeflow-store/test-fixtures/sample-blueprint.json new file mode 100644 index 0000000..a51f37d --- /dev/null +++ b/packages/codeflow-store/test-fixtures/sample-blueprint.json @@ -0,0 +1,28 @@ +{ + "projectName": "sample-blueprint", + "mode": "essential", + "nodes": [ + { + "id": "node-1", + "type": "task", + "label": "Setup", + "sourceRefs": [], + "config": {} + }, + { + "id": "node-2", + "type": "task", + "label": "Build", + "sourceRefs": [], + "config": {} + } + ], + "edges": [ + { + "id": "edge-1", + "kind": "sequence", + "from": "node-1", + "to": "node-2" + } + ] +} diff --git a/packages/codeflow-store/tsconfig.json b/packages/codeflow-store/tsconfig.json new file mode 100644 index 0000000..2797439 --- /dev/null +++ b/packages/codeflow-store/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test-fixtures"] +} diff --git a/packages/codeflow-store/vitest.config.ts b/packages/codeflow-store/vitest.config.ts new file mode 100644 index 0000000..ac31efb --- /dev/null +++ b/packages/codeflow-store/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + exclude: ["node_modules", "dist", "test-fixtures", "**/*.test.ts.skip"], + globals: true, + environment: "node", + pool: "threads", + poolOptions: { + threads: { + singleThread: true + } + } + } +}); diff --git a/src/app/api/approvals/approve/route.ts b/src/app/api/approvals/approve/route.ts index 8b46a95..bde9678 100644 --- a/src/app/api/approvals/approve/route.ts +++ b/src/app/api/approvals/approve/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; -import { approveRecord } from "@/lib/blueprint/approval-store"; +import { approveRecord } from "@abhinav2203/codeflow-store/approval"; import { approvalActionRequestSchema } from "@/lib/blueprint/schema"; export async function POST(request: Request) { diff --git a/src/app/api/export/route.ts b/src/app/api/export/route.ts index 38b7a22..285739e 100644 --- a/src/app/api/export/route.ts +++ b/src/app/api/export/route.ts @@ -4,15 +4,15 @@ import { createExecutionReport } from "@/lib/blueprint/execute"; import { exportBlueprintArtifacts } from "@/lib/blueprint/export"; import { exportBlueprintRequestSchema } from "@/lib/blueprint/schema"; import { createRunPlan } from "@/lib/blueprint/plan"; -import { assessExportRisk } from "@/lib/blueprint/risk"; +import { assessExportRisk } from "@abhinav2203/codeflow-store/risk"; import { createSandboxDir, syncSandboxToTarget, writeDiffManifest } from "@/lib/blueprint/sandbox"; import { createApprovalRecord, getApprovalRecord -} from "@/lib/blueprint/approval-store"; -import { createCheckpointIfNeeded } from "@/lib/blueprint/checkpoint-store"; -import { createRunId, saveRunRecord } from "@/lib/blueprint/run-store"; -import { loadLatestSession, upsertSession } from "@/lib/blueprint/session-store"; +} from "@abhinav2203/codeflow-store/approval"; +import { createCheckpointIfNeeded } from "@abhinav2203/codeflow-store/checkpoint"; +import { createRunId, saveRunRecord } from "@abhinav2203/codeflow-store/run"; +import { loadLatestSession, upsertSession } from "@abhinav2203/codeflow-store/session"; import { initCodeRag } from "@/lib/coderag"; export async function POST(request: Request) { diff --git a/src/app/api/mcp/invoke/route.ts b/src/app/api/mcp/invoke/route.ts index 1bd2db9..fa0a41f 100644 --- a/src/app/api/mcp/invoke/route.ts +++ b/src/app/api/mcp/invoke/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { z } from "zod"; -import { invokeMcpTool } from "@/lib/blueprint/mcp"; +import { invokeMcpTool } from "@abhinav2203/codeflow-mcp"; const serverUrlSchema = z.string().min(1).transform((value, ctx) => { let url: URL; diff --git a/src/app/api/mcp/tools/route.ts b/src/app/api/mcp/tools/route.ts index 5206bcf..abebc74 100644 --- a/src/app/api/mcp/tools/route.ts +++ b/src/app/api/mcp/tools/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { z } from "zod"; -import { listMcpTools } from "@/lib/blueprint/mcp"; +import { listMcpTools } from "@abhinav2203/codeflow-mcp"; const requestSchema = z.object({ serverUrl: z.string().min(1),