From 3fd688ac374f25ca04be2fabdfbd922f880cfe71 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Sat, 28 Mar 2026 18:38:25 +0800 Subject: [PATCH] feat(core): add 12 custom parsers for non-code file types Add regex/parser-based analyzers for: Markdown, YAML, JSON, TOML, Env, Dockerfile, SQL, GraphQL, Protobuf, Terraform, Makefile, Shell. Each implements AnalyzerPlugin with analyzeFile() and optional extractReferences(). Uses `yaml` npm package for YAML parsing, built-in JSON.parse for JSON, regex for all others. Add registerAllParsers() helper to register all parsers at once. Add comprehensive test suite with 35 tests covering all parsers. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../packages/core/package.json | 1 + .../core/src/__tests__/parsers.test.ts | 386 ++++++++++++++++++ .../src/plugins/parsers/dockerfile-parser.ts | 68 +++ .../core/src/plugins/parsers/env-parser.ts | 36 ++ .../src/plugins/parsers/graphql-parser.ts | 118 ++++++ .../core/src/plugins/parsers/index.ts | 44 ++ .../core/src/plugins/parsers/json-parser.ts | 65 +++ .../src/plugins/parsers/makefile-parser.ts | 43 ++ .../src/plugins/parsers/markdown-parser.ts | 56 +++ .../src/plugins/parsers/protobuf-parser.ts | 133 ++++++ .../core/src/plugins/parsers/shell-parser.ts | 70 ++++ .../core/src/plugins/parsers/sql-parser.ts | 98 +++++ .../src/plugins/parsers/terraform-parser.ts | 122 ++++++ .../core/src/plugins/parsers/toml-parser.ts | 41 ++ .../core/src/plugins/parsers/yaml-parser.ts | 66 +++ understand-anything-plugin/pnpm-lock.yaml | 98 ++--- 16 files changed, 1388 insertions(+), 57 deletions(-) create mode 100644 understand-anything-plugin/packages/core/src/__tests__/parsers.test.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/dockerfile-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/env-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/graphql-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/index.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/json-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/makefile-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/markdown-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/protobuf-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/shell-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/sql-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/terraform-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/toml-parser.ts create mode 100644 understand-anything-plugin/packages/core/src/plugins/parsers/yaml-parser.ts diff --git a/understand-anything-plugin/packages/core/package.json b/understand-anything-plugin/packages/core/package.json index dc27f16..457aae8 100644 --- a/understand-anything-plugin/packages/core/package.json +++ b/understand-anything-plugin/packages/core/package.json @@ -41,6 +41,7 @@ "tree-sitter-javascript": "^0.25.0", "tree-sitter-typescript": "^0.23.2", "web-tree-sitter": "^0.26.6", + "yaml": "^2.8.3", "zod": "^4.3.6" } } diff --git a/understand-anything-plugin/packages/core/src/__tests__/parsers.test.ts b/understand-anything-plugin/packages/core/src/__tests__/parsers.test.ts new file mode 100644 index 0000000..13dca7e --- /dev/null +++ b/understand-anything-plugin/packages/core/src/__tests__/parsers.test.ts @@ -0,0 +1,386 @@ +import { describe, it, expect } from "vitest"; +import { MarkdownParser } from "../plugins/parsers/markdown-parser.js"; +import { YAMLConfigParser } from "../plugins/parsers/yaml-parser.js"; +import { JSONConfigParser } from "../plugins/parsers/json-parser.js"; +import { TOMLParser } from "../plugins/parsers/toml-parser.js"; +import { EnvParser } from "../plugins/parsers/env-parser.js"; +import { DockerfileParser } from "../plugins/parsers/dockerfile-parser.js"; +import { SQLParser } from "../plugins/parsers/sql-parser.js"; +import { GraphQLParser } from "../plugins/parsers/graphql-parser.js"; +import { ProtobufParser } from "../plugins/parsers/protobuf-parser.js"; +import { TerraformParser } from "../plugins/parsers/terraform-parser.js"; +import { MakefileParser } from "../plugins/parsers/makefile-parser.js"; +import { ShellParser } from "../plugins/parsers/shell-parser.js"; +import { registerAllParsers } from "../plugins/parsers/index.js"; +import { PluginRegistry } from "../plugins/registry.js"; + +describe("MarkdownParser", () => { + const parser = new MarkdownParser(); + + it("extracts heading sections", () => { + const content = "# Title\n\nIntro\n\n## Section A\n\nContent A\n\n### Subsection\n\nContent B"; + const result = parser.analyzeFile("README.md", content); + expect(result.sections).toHaveLength(3); + expect(result.sections![0]).toMatchObject({ name: "Title", level: 1 }); + expect(result.sections![1]).toMatchObject({ name: "Section A", level: 2 }); + expect(result.sections![2]).toMatchObject({ name: "Subsection", level: 3 }); + }); + + it("extracts YAML front matter as imports", () => { + const content = "---\ntitle: Test\ntags: [a, b]\n---\n# Content"; + const result = parser.analyzeFile("post.md", content); + expect(result.imports).toHaveLength(0); + }); + + it("extracts file references", () => { + const content = "See [guide](./docs/guide.md) and ![img](./assets/logo.png)"; + const refs = parser.extractReferences!("README.md", content); + expect(refs).toHaveLength(2); + expect(refs[0]).toMatchObject({ target: "./docs/guide.md", referenceType: "file" }); + expect(refs[1]).toMatchObject({ target: "./assets/logo.png", referenceType: "image" }); + }); + + it("skips external URLs in references", () => { + const content = "[link](https://example.com) and [local](./file.md)"; + const refs = parser.extractReferences!("README.md", content); + expect(refs).toHaveLength(1); + expect(refs[0].target).toBe("./file.md"); + }); + + it("returns empty sections for empty content", () => { + const result = parser.analyzeFile("empty.md", ""); + expect(result.sections).toHaveLength(0); + }); +}); + +describe("YAMLConfigParser", () => { + const parser = new YAMLConfigParser(); + + it("extracts top-level key sections", () => { + const content = "name: my-app\nversion: 1.0\nservices:\n web:\n image: node\n db:\n image: postgres"; + const result = parser.analyzeFile("config.yaml", content); + expect(result.sections).toBeDefined(); + expect(result.sections!.length).toBeGreaterThanOrEqual(3); + expect(result.sections!.map(s => s.name)).toContain("name"); + expect(result.sections!.map(s => s.name)).toContain("services"); + }); + + it("handles invalid YAML gracefully", () => { + const content = "invalid: yaml: content: [[["; + const result = parser.analyzeFile("broken.yaml", content); + expect(result.sections).toBeDefined(); + }); +}); + +describe("JSONConfigParser", () => { + const parser = new JSONConfigParser(); + + it("extracts top-level key sections", () => { + const content = '{\n "name": "my-app",\n "version": "1.0",\n "dependencies": {}\n}'; + const result = parser.analyzeFile("package.json", content); + expect(result.sections).toBeDefined(); + expect(result.sections!.map(s => s.name)).toContain("name"); + expect(result.sections!.map(s => s.name)).toContain("dependencies"); + }); + + it("extracts $ref references", () => { + const content = '{\n "$ref": "./common.json#/defs/User"\n}'; + const refs = parser.extractReferences!("schema.json", content); + expect(refs).toHaveLength(1); + expect(refs[0]).toMatchObject({ target: "./common.json#/defs/User", referenceType: "schema" }); + }); + + it("skips internal $ref references", () => { + const content = '{\n "$ref": "#/definitions/User"\n}'; + const refs = parser.extractReferences!("schema.json", content); + expect(refs).toHaveLength(0); + }); + + it("handles invalid JSON gracefully", () => { + const content = "not json at all"; + const result = parser.analyzeFile("broken.json", content); + expect(result.sections).toHaveLength(0); + }); +}); + +describe("TOMLParser", () => { + const parser = new TOMLParser(); + + it("extracts section headers", () => { + const content = "[package]\nname = \"my-app\"\n\n[dependencies]\nfoo = \"1.0\"\n\n[[bin]]\nname = \"cli\""; + const result = parser.analyzeFile("Cargo.toml", content); + expect(result.sections).toBeDefined(); + expect(result.sections!.length).toBe(3); + expect(result.sections![0].name).toBe("package"); + expect(result.sections![1].name).toBe("dependencies"); + expect(result.sections![2].name).toBe("[[bin]]"); + }); +}); + +describe("EnvParser", () => { + const parser = new EnvParser(); + + it("extracts variable names", () => { + const content = "# Database config\nDB_HOST=localhost\nDB_PORT=5432\n\n# API\nAPI_KEY=secret123"; + const result = parser.analyzeFile(".env", content); + expect(result.definitions).toBeDefined(); + expect(result.definitions!).toHaveLength(3); + expect(result.definitions!.map(d => d.name)).toEqual(["DB_HOST", "DB_PORT", "API_KEY"]); + }); + + it("skips comments and empty lines", () => { + const content = "# comment\n\nVAR=value"; + const result = parser.analyzeFile(".env", content); + expect(result.definitions!).toHaveLength(1); + }); +}); + +describe("DockerfileParser", () => { + const parser = new DockerfileParser(); + + it("extracts FROM stages", () => { + const content = "FROM node:22-slim AS builder\nRUN npm install\n\nFROM node:22-slim AS runner\nCOPY --from=builder /app /app\nEXPOSE 3000"; + const result = parser.analyzeFile("Dockerfile", content); + expect(result.services).toBeDefined(); + expect(result.services!).toHaveLength(2); + expect(result.services![0]).toMatchObject({ name: "builder", image: "node:22-slim" }); + expect(result.services![1]).toMatchObject({ name: "runner", image: "node:22-slim" }); + }); + + it("extracts EXPOSE ports", () => { + const content = "FROM node:22\nEXPOSE 3000 8080\nCMD [\"node\", \"server.js\"]"; + const result = parser.analyzeFile("Dockerfile", content); + expect(result.services![0].ports).toContain(3000); + expect(result.services![0].ports).toContain(8080); + }); + + it("extracts steps", () => { + const content = "FROM node:22\nWORKDIR /app\nCOPY . .\nRUN npm install\nCMD [\"node\", \"start\"]"; + const result = parser.analyzeFile("Dockerfile", content); + expect(result.steps).toBeDefined(); + expect(result.steps!.length).toBe(5); + }); +}); + +describe("SQLParser", () => { + const parser = new SQLParser(); + + it("extracts CREATE TABLE definitions with columns", () => { + const content = `CREATE TABLE users ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + email TEXT UNIQUE +); + +CREATE TABLE posts ( + id INTEGER PRIMARY KEY, + user_id INTEGER, + title TEXT, + FOREIGN KEY (user_id) REFERENCES users(id) +);`; + const result = parser.analyzeFile("schema.sql", content); + expect(result.definitions).toBeDefined(); + expect(result.definitions!).toHaveLength(2); + expect(result.definitions![0]).toMatchObject({ name: "users", kind: "table" }); + expect(result.definitions![0].fields).toContain("id"); + expect(result.definitions![0].fields).toContain("name"); + expect(result.definitions![0].fields).toContain("email"); + expect(result.definitions![1]).toMatchObject({ name: "posts", kind: "table" }); + }); + + it("extracts CREATE VIEW", () => { + const content = "CREATE VIEW active_users AS SELECT * FROM users WHERE active = true;"; + const result = parser.analyzeFile("views.sql", content); + expect(result.definitions!.some(d => d.name === "active_users" && d.kind === "view")).toBe(true); + }); + + it("extracts CREATE INDEX", () => { + const content = "CREATE UNIQUE INDEX idx_users_email ON users(email);"; + const result = parser.analyzeFile("indexes.sql", content); + expect(result.definitions!.some(d => d.name === "idx_users_email" && d.kind === "index")).toBe(true); + }); +}); + +describe("GraphQLParser", () => { + const parser = new GraphQLParser(); + + it("extracts type definitions", () => { + const content = `type User { + id: ID! + name: String! + email: String! +} + +type Post { + id: ID! + title: String! + author: User! +}`; + const result = parser.analyzeFile("schema.graphql", content); + expect(result.definitions).toBeDefined(); + expect(result.definitions!).toHaveLength(2); + expect(result.definitions![0]).toMatchObject({ name: "User", kind: "type" }); + expect(result.definitions![0].fields).toContain("id"); + expect(result.definitions![0].fields).toContain("name"); + expect(result.definitions![1]).toMatchObject({ name: "Post", kind: "type" }); + }); + + it("extracts Query/Mutation endpoints", () => { + const content = `type Query { + users: [User!]! + user(id: ID!): User +} + +type Mutation { + createUser(name: String!): User! +}`; + const result = parser.analyzeFile("schema.graphql", content); + expect(result.endpoints).toBeDefined(); + expect(result.endpoints!.length).toBeGreaterThanOrEqual(3); + expect(result.endpoints!.some(e => e.method === "Query" && e.path === "users")).toBe(true); + expect(result.endpoints!.some(e => e.method === "Mutation" && e.path === "createUser")).toBe(true); + }); + + it("extracts enum definitions", () => { + const content = "enum Role {\n ADMIN\n USER\n GUEST\n}"; + const result = parser.analyzeFile("schema.graphql", content); + expect(result.definitions!.some(d => d.name === "Role" && d.kind === "enum")).toBe(true); + }); +}); + +describe("ProtobufParser", () => { + const parser = new ProtobufParser(); + + it("extracts message definitions with fields", () => { + const content = `message User { + string name = 1; + int32 age = 2; + repeated string emails = 3; +}`; + const result = parser.analyzeFile("user.proto", content); + expect(result.definitions).toBeDefined(); + expect(result.definitions!).toHaveLength(1); + expect(result.definitions![0]).toMatchObject({ name: "User", kind: "message" }); + expect(result.definitions![0].fields).toContain("name"); + expect(result.definitions![0].fields).toContain("age"); + expect(result.definitions![0].fields).toContain("emails"); + }); + + it("extracts enum definitions", () => { + const content = "enum Status {\n UNKNOWN = 0;\n ACTIVE = 1;\n INACTIVE = 2;\n}"; + const result = parser.analyzeFile("status.proto", content); + expect(result.definitions!.some(d => d.name === "Status" && d.kind === "enum")).toBe(true); + expect(result.definitions![0].fields).toContain("UNKNOWN"); + expect(result.definitions![0].fields).toContain("ACTIVE"); + }); + + it("extracts service RPC methods", () => { + const content = `service UserService { + rpc GetUser (GetUserRequest) returns (User); + rpc CreateUser (CreateUserRequest) returns (User); +}`; + const result = parser.analyzeFile("service.proto", content); + expect(result.endpoints).toBeDefined(); + expect(result.endpoints!).toHaveLength(2); + expect(result.endpoints![0]).toMatchObject({ method: "rpc", path: "UserService.GetUser" }); + expect(result.endpoints![1]).toMatchObject({ method: "rpc", path: "UserService.CreateUser" }); + }); +}); + +describe("TerraformParser", () => { + const parser = new TerraformParser(); + + it("extracts resource blocks", () => { + const content = `resource "aws_s3_bucket" "main" { + bucket = "my-bucket" +} + +resource "aws_iam_role" "lambda" { + name = "lambda-role" +}`; + const result = parser.analyzeFile("main.tf", content); + expect(result.resources).toBeDefined(); + expect(result.resources!).toHaveLength(2); + expect(result.resources![0]).toMatchObject({ name: "aws_s3_bucket.main", kind: "aws_s3_bucket" }); + expect(result.resources![1]).toMatchObject({ name: "aws_iam_role.lambda", kind: "aws_iam_role" }); + }); + + it("extracts data blocks", () => { + const content = 'data "aws_ami" "ubuntu" {\n most_recent = true\n}'; + const result = parser.analyzeFile("data.tf", content); + expect(result.resources!.some(r => r.name === "data.aws_ami.ubuntu")).toBe(true); + }); + + it("extracts module blocks", () => { + const content = 'module "vpc" {\n source = "./modules/vpc"\n}'; + const result = parser.analyzeFile("modules.tf", content); + expect(result.resources!.some(r => r.name === "module.vpc" && r.kind === "module")).toBe(true); + }); + + it("extracts variables and outputs", () => { + const content = 'variable "region" {\n default = "us-east-1"\n}\n\noutput "bucket_arn" {\n value = aws_s3_bucket.main.arn\n}'; + const result = parser.analyzeFile("variables.tf", content); + expect(result.definitions).toBeDefined(); + expect(result.definitions!.some(d => d.name === "region" && d.kind === "variable")).toBe(true); + expect(result.definitions!.some(d => d.name === "bucket_arn" && d.kind === "output")).toBe(true); + }); +}); + +describe("MakefileParser", () => { + const parser = new MakefileParser(); + + it("extracts make targets", () => { + const content = "build:\n\tgo build -o bin/app\n\ntest:\n\tgo test ./...\n\nclean:\n\trm -rf bin/"; + const result = parser.analyzeFile("Makefile", content); + expect(result.steps).toBeDefined(); + expect(result.steps!).toHaveLength(3); + expect(result.steps!.map(s => s.name)).toEqual(["build", "test", "clean"]); + }); + + it("does not confuse variable assignments with targets", () => { + const content = "CC := gcc\nCFLAGS := -Wall\n\nbuild:\n\t$(CC) $(CFLAGS) main.c"; + const result = parser.analyzeFile("Makefile", content); + expect(result.steps!).toHaveLength(1); + expect(result.steps![0].name).toBe("build"); + }); +}); + +describe("ShellParser", () => { + const parser = new ShellParser(); + + it("extracts function definitions", () => { + const content = "#!/bin/bash\n\ngreet() {\n echo \"Hello $1\"\n}\n\nfunction cleanup {\n rm -rf tmp/\n}"; + const result = parser.analyzeFile("script.sh", content); + expect(result.functions).toHaveLength(2); + expect(result.functions[0].name).toBe("greet"); + expect(result.functions[1].name).toBe("cleanup"); + }); + + it("extracts source references", () => { + const content = "#!/bin/bash\nsource ./lib/utils.sh\n. ./lib/config.sh"; + const refs = parser.extractReferences!("script.sh", content); + expect(refs).toHaveLength(2); + expect(refs[0]).toMatchObject({ target: "./lib/utils.sh", referenceType: "file" }); + expect(refs[1]).toMatchObject({ target: "./lib/config.sh", referenceType: "file" }); + }); +}); + +describe("registerAllParsers", () => { + it("registers all 12 parsers with a PluginRegistry", () => { + const registry = new PluginRegistry(); + registerAllParsers(registry); + expect(registry.getPlugins()).toHaveLength(12); + expect(registry.getSupportedLanguages()).toContain("markdown"); + expect(registry.getSupportedLanguages()).toContain("yaml"); + expect(registry.getSupportedLanguages()).toContain("json"); + expect(registry.getSupportedLanguages()).toContain("toml"); + expect(registry.getSupportedLanguages()).toContain("env"); + expect(registry.getSupportedLanguages()).toContain("dockerfile"); + expect(registry.getSupportedLanguages()).toContain("sql"); + expect(registry.getSupportedLanguages()).toContain("graphql"); + expect(registry.getSupportedLanguages()).toContain("protobuf"); + expect(registry.getSupportedLanguages()).toContain("terraform"); + expect(registry.getSupportedLanguages()).toContain("makefile"); + expect(registry.getSupportedLanguages()).toContain("shell"); + }); +}); diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/dockerfile-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/dockerfile-parser.ts new file mode 100644 index 0000000..a70726b --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/dockerfile-parser.ts @@ -0,0 +1,68 @@ +import type { AnalyzerPlugin, StructuralAnalysis, ServiceInfo, StepInfo } from "../../types.js"; + +export class DockerfileParser implements AnalyzerPlugin { + name = "dockerfile-parser"; + languages = ["dockerfile"]; + + analyzeFile(_filePath: string, content: string): StructuralAnalysis { + const services = this.extractStages(content); + const steps = this.extractSteps(content); + return { + functions: [], + classes: [], + imports: [], + exports: [], + services, + steps, + }; + } + + private extractStages(content: string): ServiceInfo[] { + const stages: ServiceInfo[] = []; + const lines = content.split("\n"); + const ports: number[] = []; + + // Collect all EXPOSE ports + for (const line of lines) { + const exposeMatch = line.match(/^EXPOSE\s+(.+)/i); + if (exposeMatch) { + const portValues = exposeMatch[1].split(/\s+/); + for (const p of portValues) { + const num = parseInt(p, 10); + if (!isNaN(num)) ports.push(num); + } + } + } + + // Extract FROM stages + for (const line of lines) { + const fromMatch = line.match(/^FROM\s+(\S+)(?:\s+[Aa][Ss]\s+(\S+))?/i); + if (fromMatch) { + const image = fromMatch[1]; + const name = fromMatch[2] ?? image.split(":")[0].split("/").pop() ?? image; + stages.push({ + name, + image, + ports: stages.length === 0 ? ports : [], // Assign ports to first stage only as default + }); + } + } + + return stages; + } + + private extractSteps(content: string): StepInfo[] { + const steps: StepInfo[] = []; + const lines = content.split("\n"); + for (let i = 0; i < lines.length; i++) { + const match = lines[i].match(/^(FROM|RUN|COPY|ADD|WORKDIR|CMD|ENTRYPOINT|ENV|ARG|EXPOSE|VOLUME|USER|HEALTHCHECK)\s/i); + if (match) { + steps.push({ + name: `${match[1].toUpperCase()} ${lines[i].slice(match[1].length + 1).trim().slice(0, 60)}`, + lineRange: [i + 1, i + 1], + }); + } + } + return steps; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/env-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/env-parser.ts new file mode 100644 index 0000000..a2f22a3 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/env-parser.ts @@ -0,0 +1,36 @@ +import type { AnalyzerPlugin, StructuralAnalysis, DefinitionInfo } from "../../types.js"; + +export class EnvParser implements AnalyzerPlugin { + name = "env-parser"; + languages = ["env"]; + + analyzeFile(_filePath: string, content: string): StructuralAnalysis { + const definitions = this.extractVariables(content); + return { + functions: [], + classes: [], + imports: [], + exports: [], + definitions, + }; + } + + private extractVariables(content: string): DefinitionInfo[] { + const definitions: DefinitionInfo[] = []; + const lines = content.split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (line.startsWith("#") || line === "") continue; + const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/); + if (match) { + definitions.push({ + name: match[1], + kind: "variable", + lineRange: [i + 1, i + 1], + fields: [], + }); + } + } + return definitions; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/graphql-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/graphql-parser.ts new file mode 100644 index 0000000..184b6b2 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/graphql-parser.ts @@ -0,0 +1,118 @@ +import type { AnalyzerPlugin, StructuralAnalysis, DefinitionInfo, EndpointInfo } from "../../types.js"; + +export class GraphQLParser implements AnalyzerPlugin { + name = "graphql-parser"; + languages = ["graphql"]; + + analyzeFile(_filePath: string, content: string): StructuralAnalysis { + const definitions = this.extractDefinitions(content); + const endpoints = this.extractEndpoints(content); + return { + functions: [], + classes: [], + imports: [], + exports: [], + definitions, + endpoints, + }; + } + + private extractDefinitions(content: string): DefinitionInfo[] { + const definitions: DefinitionInfo[] = []; + const lines = content.split("\n"); + + // Match type, input, enum, interface, union, scalar definitions + const typeRegex = /^(type|input|enum|interface|union|scalar)\s+(\w+)/gm; + let match; + while ((match = typeRegex.exec(content)) !== null) { + const kind = match[1]; + const name = match[2]; + if (name === "Query" || name === "Mutation" || name === "Subscription") continue; + const startLine = content.slice(0, match.index).split("\n").length; + + // Extract fields (for type/input/interface/enum) + const fields = this.extractFields(content, match.index); + + // Find closing brace + const afterMatch = content.slice(match.index); + const closeBrace = afterMatch.indexOf("}"); + const endLine = closeBrace !== -1 + ? content.slice(0, match.index + closeBrace + 1).split("\n").length + : startLine; + + definitions.push({ + name, + kind, + lineRange: [startLine, endLine], + fields, + }); + } + + return definitions; + } + + private extractEndpoints(content: string): EndpointInfo[] { + const endpoints: EndpointInfo[] = []; + + // Find Query, Mutation, Subscription blocks and extract their fields + const blockRegex = /^(type)\s+(Query|Mutation|Subscription)\s*\{/gm; + let match; + while ((match = blockRegex.exec(content)) !== null) { + const method = match[2]; // Query, Mutation, Subscription + const startIdx = match.index + match[0].length; + + // Find closing brace + let depth = 1; + let i = startIdx; + while (i < content.length && depth > 0) { + if (content[i] === "{") depth++; + if (content[i] === "}") depth--; + i++; + } + + const blockContent = content.slice(startIdx, i - 1); + const blockLines = blockContent.split("\n"); + const blockStartLine = content.slice(0, startIdx).split("\n").length; + + for (let j = 0; j < blockLines.length; j++) { + const fieldMatch = blockLines[j].trim().match(/^(\w+)/); + if (fieldMatch && fieldMatch[1]) { + const lineNum = blockStartLine + j; + endpoints.push({ + method, + path: fieldMatch[1], + lineRange: [lineNum, lineNum], + }); + } + } + } + + return endpoints; + } + + private extractFields(content: string, startIdx: number): string[] { + const fields: string[] = []; + const afterType = content.slice(startIdx); + const openBrace = afterType.indexOf("{"); + if (openBrace === -1) return fields; + + let depth = 1; + let i = openBrace + 1; + while (i < afterType.length && depth > 0) { + if (afterType[i] === "{") depth++; + if (afterType[i] === "}") depth--; + i++; + } + + const body = afterType.slice(openBrace + 1, i - 1); + const lines = body.split("\n"); + for (const line of lines) { + const fieldMatch = line.trim().match(/^(\w+)/); + if (fieldMatch) { + fields.push(fieldMatch[1]); + } + } + + return fields; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/index.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/index.ts new file mode 100644 index 0000000..5832091 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/index.ts @@ -0,0 +1,44 @@ +export { MarkdownParser } from "./markdown-parser.js"; +export { YAMLConfigParser } from "./yaml-parser.js"; +export { JSONConfigParser } from "./json-parser.js"; +export { TOMLParser } from "./toml-parser.js"; +export { EnvParser } from "./env-parser.js"; +export { DockerfileParser } from "./dockerfile-parser.js"; +export { SQLParser } from "./sql-parser.js"; +export { GraphQLParser } from "./graphql-parser.js"; +export { ProtobufParser } from "./protobuf-parser.js"; +export { TerraformParser } from "./terraform-parser.js"; +export { MakefileParser } from "./makefile-parser.js"; +export { ShellParser } from "./shell-parser.js"; + +import type { PluginRegistry } from "../registry.js"; +import { MarkdownParser } from "./markdown-parser.js"; +import { YAMLConfigParser } from "./yaml-parser.js"; +import { JSONConfigParser } from "./json-parser.js"; +import { TOMLParser } from "./toml-parser.js"; +import { EnvParser } from "./env-parser.js"; +import { DockerfileParser } from "./dockerfile-parser.js"; +import { SQLParser } from "./sql-parser.js"; +import { GraphQLParser } from "./graphql-parser.js"; +import { ProtobufParser } from "./protobuf-parser.js"; +import { TerraformParser } from "./terraform-parser.js"; +import { MakefileParser } from "./makefile-parser.js"; +import { ShellParser } from "./shell-parser.js"; + +/** + * Register all built-in non-code parsers with a PluginRegistry. + */ +export function registerAllParsers(registry: PluginRegistry): void { + registry.register(new MarkdownParser()); + registry.register(new YAMLConfigParser()); + registry.register(new JSONConfigParser()); + registry.register(new TOMLParser()); + registry.register(new EnvParser()); + registry.register(new DockerfileParser()); + registry.register(new SQLParser()); + registry.register(new GraphQLParser()); + registry.register(new ProtobufParser()); + registry.register(new TerraformParser()); + registry.register(new MakefileParser()); + registry.register(new ShellParser()); +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/json-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/json-parser.ts new file mode 100644 index 0000000..baf74a5 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/json-parser.ts @@ -0,0 +1,65 @@ +import type { AnalyzerPlugin, StructuralAnalysis, SectionInfo, ReferenceResolution } from "../../types.js"; + +export class JSONConfigParser implements AnalyzerPlugin { + name = "json-config-parser"; + languages = ["json"]; + + analyzeFile(_filePath: string, content: string): StructuralAnalysis { + const sections = this.extractSections(content); + return { + functions: [], + classes: [], + imports: [], + exports: [], + sections, + }; + } + + extractReferences(filePath: string, content: string): ReferenceResolution[] { + const refs: ReferenceResolution[] = []; + // Match $ref values (JSON Schema / OpenAPI) + const refRegex = /"\$ref"\s*:\s*"([^"]+)"/g; + let match; + while ((match = refRegex.exec(content)) !== null) { + const target = match[1]; + if (target.startsWith("#")) continue; // Skip internal refs + const line = content.slice(0, match.index).split("\n").length; + refs.push({ + source: filePath, + target, + referenceType: "schema", + line, + }); + } + return refs; + } + + private extractSections(content: string): SectionInfo[] { + const sections: SectionInfo[] = []; + try { + const doc = JSON.parse(content); + if (doc && typeof doc === "object" && !Array.isArray(doc)) { + const lines = content.split("\n"); + for (const key of Object.keys(doc)) { + const escapedKey = JSON.stringify(key); + const lineIdx = lines.findIndex((l) => l.includes(escapedKey)); + if (lineIdx !== -1) { + sections.push({ + name: key, + level: 1, + lineRange: [lineIdx + 1, lineIdx + 1], + }); + } + } + // Fix lineRange end + for (let i = 0; i < sections.length; i++) { + const next = sections[i + 1]; + sections[i].lineRange[1] = next ? next.lineRange[0] - 1 : lines.length; + } + } + } catch { + // JSON parse failed — skip + } + return sections; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/makefile-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/makefile-parser.ts new file mode 100644 index 0000000..4c1b9bf --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/makefile-parser.ts @@ -0,0 +1,43 @@ +import type { AnalyzerPlugin, StructuralAnalysis, StepInfo } from "../../types.js"; + +export class MakefileParser implements AnalyzerPlugin { + name = "makefile-parser"; + languages = ["makefile"]; + + analyzeFile(_filePath: string, content: string): StructuralAnalysis { + const steps = this.extractTargets(content); + return { + functions: [], + classes: [], + imports: [], + exports: [], + steps, + }; + } + + private extractTargets(content: string): StepInfo[] { + const targets: StepInfo[] = []; + const lines = content.split("\n"); + for (let i = 0; i < lines.length; i++) { + // Match target: dependencies (not variable assignments or comments) + const match = lines[i].match(/^([a-zA-Z_][\w.-]*)(?:\s+.*)?:/); + if (match && !lines[i].includes(":=") && !lines[i].includes("?=")) { + // Find end of target (next non-indented non-empty line or EOF) + let endLine = i + 1; + while (endLine < lines.length) { + const nextLine = lines[endLine]; + if (nextLine === "" || nextLine.startsWith("\t") || nextLine.startsWith(" ")) { + endLine++; + } else { + break; + } + } + targets.push({ + name: match[1], + lineRange: [i + 1, endLine], + }); + } + } + return targets; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/markdown-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/markdown-parser.ts new file mode 100644 index 0000000..f0192f9 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/markdown-parser.ts @@ -0,0 +1,56 @@ +import type { AnalyzerPlugin, StructuralAnalysis, ReferenceResolution, SectionInfo } from "../../types.js"; + +export class MarkdownParser implements AnalyzerPlugin { + name = "markdown-parser"; + languages = ["markdown"]; + + analyzeFile(_filePath: string, content: string): StructuralAnalysis { + const sections = this.extractSections(content); + return { + functions: [], + classes: [], + imports: [], + exports: [], + sections, + }; + } + + extractReferences(filePath: string, content: string): ReferenceResolution[] { + const refs: ReferenceResolution[] = []; + const linkRegex = /!?\[([^\]]*)\]\(([^)]+)\)/g; + let match; + while ((match = linkRegex.exec(content)) !== null) { + const target = match[2]; + if (target.startsWith("http")) continue; // Skip external URLs + const line = content.slice(0, match.index).split("\n").length; + refs.push({ + source: filePath, + target, + referenceType: match[0].startsWith("!") ? "image" : "file", + line, + }); + } + return refs; + } + + private extractSections(content: string): SectionInfo[] { + const sections: SectionInfo[] = []; + const lines = content.split("\n"); + for (let i = 0; i < lines.length; i++) { + const match = lines[i].match(/^(#{1,6})\s+(.+)/); + if (match) { + sections.push({ + name: match[2].trim(), + level: match[1].length, + lineRange: [i + 1, i + 1], + }); + } + } + // Fix lineRange end for each section (extends to next heading or EOF) + for (let i = 0; i < sections.length; i++) { + const next = sections[i + 1]; + sections[i].lineRange[1] = next ? next.lineRange[0] - 1 : lines.length; + } + return sections; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/protobuf-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/protobuf-parser.ts new file mode 100644 index 0000000..6c49886 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/protobuf-parser.ts @@ -0,0 +1,133 @@ +import type { AnalyzerPlugin, StructuralAnalysis, DefinitionInfo, EndpointInfo } from "../../types.js"; + +export class ProtobufParser implements AnalyzerPlugin { + name = "protobuf-parser"; + languages = ["protobuf"]; + + analyzeFile(_filePath: string, content: string): StructuralAnalysis { + const definitions = this.extractDefinitions(content); + const endpoints = this.extractServiceMethods(content); + return { + functions: [], + classes: [], + imports: [], + exports: [], + definitions, + endpoints, + }; + } + + private extractDefinitions(content: string): DefinitionInfo[] { + const definitions: DefinitionInfo[] = []; + + // Match message definitions + const messageRegex = /^message\s+(\w+)\s*\{/gm; + let match; + while ((match = messageRegex.exec(content)) !== null) { + const startLine = content.slice(0, match.index).split("\n").length; + const fields = this.extractMessageFields(content, match.index); + const afterMatch = content.slice(match.index); + const closeBrace = this.findClosingBrace(afterMatch); + const endLine = content.slice(0, match.index + closeBrace + 1).split("\n").length; + + definitions.push({ + name: match[1], + kind: "message", + lineRange: [startLine, endLine], + fields, + }); + } + + // Match enum definitions + const enumRegex = /^enum\s+(\w+)\s*\{/gm; + while ((match = enumRegex.exec(content)) !== null) { + const startLine = content.slice(0, match.index).split("\n").length; + const fields = this.extractEnumValues(content, match.index); + const afterMatch = content.slice(match.index); + const closeBrace = this.findClosingBrace(afterMatch); + const endLine = content.slice(0, match.index + closeBrace + 1).split("\n").length; + + definitions.push({ + name: match[1], + kind: "enum", + lineRange: [startLine, endLine], + fields, + }); + } + + return definitions; + } + + private extractServiceMethods(content: string): EndpointInfo[] { + const endpoints: EndpointInfo[] = []; + const serviceRegex = /^service\s+(\w+)\s*\{/gm; + let match; + while ((match = serviceRegex.exec(content)) !== null) { + const serviceName = match[1]; + const startIdx = match.index + match[0].length; + const afterService = content.slice(match.index); + const closeBrace = this.findClosingBrace(afterService); + const body = afterService.slice(match[0].length, closeBrace); + + const rpcRegex = /rpc\s+(\w+)\s*\(/g; + let rpcMatch; + while ((rpcMatch = rpcRegex.exec(body)) !== null) { + const lineNum = content.slice(0, startIdx + rpcMatch.index).split("\n").length; + endpoints.push({ + method: "rpc", + path: `${serviceName}.${rpcMatch[1]}`, + lineRange: [lineNum, lineNum], + }); + } + } + return endpoints; + } + + private extractMessageFields(content: string, startIdx: number): string[] { + const fields: string[] = []; + const afterMsg = content.slice(startIdx); + const openBrace = afterMsg.indexOf("{"); + if (openBrace === -1) return fields; + + const closeBrace = this.findClosingBrace(afterMsg); + const body = afterMsg.slice(openBrace + 1, closeBrace); + + const fieldRegex = /^\s*(?:repeated\s+|optional\s+|required\s+|map<[^>]+>\s+)?\w+\s+(\w+)\s*=/gm; + let match; + while ((match = fieldRegex.exec(body)) !== null) { + fields.push(match[1]); + } + + return fields; + } + + private extractEnumValues(content: string, startIdx: number): string[] { + const values: string[] = []; + const afterEnum = content.slice(startIdx); + const openBrace = afterEnum.indexOf("{"); + if (openBrace === -1) return values; + + const closeBrace = this.findClosingBrace(afterEnum); + const body = afterEnum.slice(openBrace + 1, closeBrace); + + const valueRegex = /^\s*(\w+)\s*=/gm; + let match; + while ((match = valueRegex.exec(body)) !== null) { + values.push(match[1]); + } + + return values; + } + + private findClosingBrace(content: string): number { + let depth = 0; + for (let i = 0; i < content.length; i++) { + if (content[i] === "{") depth++; + if (content[i] === "}") { + depth--; + if (depth === 0) return i; + } + } + return content.length; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/shell-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/shell-parser.ts new file mode 100644 index 0000000..09f6c30 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/shell-parser.ts @@ -0,0 +1,70 @@ +import type { AnalyzerPlugin, StructuralAnalysis, ReferenceResolution } from "../../types.js"; + +export class ShellParser implements AnalyzerPlugin { + name = "shell-parser"; + languages = ["shell"]; + + analyzeFile(_filePath: string, content: string): StructuralAnalysis { + const functions = this.extractFunctions(content); + return { + functions, + classes: [], + imports: [], + exports: [], + }; + } + + extractReferences(filePath: string, content: string): ReferenceResolution[] { + const refs: ReferenceResolution[] = []; + const lines = content.split("\n"); + for (let i = 0; i < lines.length; i++) { + // Match source/. commands + const sourceMatch = lines[i].match(/^\s*(?:source|\.)[ \t]+["']?([^"'\s]+)["']?/); + if (sourceMatch) { + refs.push({ + source: filePath, + target: sourceMatch[1], + referenceType: "file", + line: i + 1, + }); + } + } + return refs; + } + + private extractFunctions(content: string): Array<{ name: string; lineRange: [number, number]; params: string[] }> { + const functions: Array<{ name: string; lineRange: [number, number]; params: string[] }> = []; + const lines = content.split("\n"); + + for (let i = 0; i < lines.length; i++) { + // Match function name() { or function name { + const match = lines[i].match(/^(?:function\s+)?(\w+)\s*\(\s*\)\s*\{?/) || + lines[i].match(/^function\s+(\w+)\s*\{?/); + if (match) { + const name = match[1]; + // Find closing brace + let endLine = i; + if (lines[i].includes("{")) { + let depth = 0; + for (let j = i; j < lines.length; j++) { + for (const ch of lines[j]) { + if (ch === "{") depth++; + if (ch === "}") depth--; + } + if (depth === 0) { + endLine = j; + break; + } + } + } + functions.push({ + name, + lineRange: [i + 1, endLine + 1], + params: [], + }); + } + } + + return functions; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/sql-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/sql-parser.ts new file mode 100644 index 0000000..01240b1 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/sql-parser.ts @@ -0,0 +1,98 @@ +import type { AnalyzerPlugin, StructuralAnalysis, DefinitionInfo } from "../../types.js"; + +export class SQLParser implements AnalyzerPlugin { + name = "sql-parser"; + languages = ["sql"]; + + analyzeFile(_filePath: string, content: string): StructuralAnalysis { + const definitions = this.extractDefinitions(content); + return { + functions: [], + classes: [], + imports: [], + exports: [], + definitions, + }; + } + + private extractDefinitions(content: string): DefinitionInfo[] { + const definitions: DefinitionInfo[] = []; + const lines = content.split("\n"); + + // Match CREATE TABLE statements + const tableRegex = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:`|")?(\w+)(?:`|")?/gi; + let match; + while ((match = tableRegex.exec(content)) !== null) { + const tableName = match[1]; + const startLine = content.slice(0, match.index).split("\n").length; + + // Extract columns (simplified: look for column names in parenthesized block) + const fields = this.extractColumns(content, match.index); + + // Find the end of the CREATE TABLE statement + const afterMatch = content.slice(match.index); + const endParen = afterMatch.indexOf(");"); + const endLine = endParen !== -1 + ? content.slice(0, match.index + endParen + 2).split("\n").length + : startLine + 5; + + definitions.push({ + name: tableName, + kind: "table", + lineRange: [startLine, endLine], + fields, + }); + } + + // Match CREATE VIEW + const viewRegex = /CREATE\s+(?:OR\s+REPLACE\s+)?VIEW\s+(?:`|")?(\w+)(?:`|")?/gi; + while ((match = viewRegex.exec(content)) !== null) { + const startLine = content.slice(0, match.index).split("\n").length; + definitions.push({ + name: match[1], + kind: "view", + lineRange: [startLine, startLine], + fields: [], + }); + } + + // Match CREATE INDEX + const indexRegex = /CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:`|")?(\w+)(?:`|")?/gi; + while ((match = indexRegex.exec(content)) !== null) { + const startLine = content.slice(0, match.index).split("\n").length; + definitions.push({ + name: match[1], + kind: "index", + lineRange: [startLine, startLine], + fields: [], + }); + } + + return definitions; + } + + private extractColumns(content: string, startIdx: number): string[] { + const fields: string[] = []; + const afterCreate = content.slice(startIdx); + const openParen = afterCreate.indexOf("("); + if (openParen === -1) return fields; + + const closeParen = afterCreate.indexOf(");", openParen); + if (closeParen === -1) return fields; + + const body = afterCreate.slice(openParen + 1, closeParen); + const lines = body.split(","); + + for (const line of lines) { + const trimmed = line.trim(); + // Skip constraints + if (/^(PRIMARY|FOREIGN|UNIQUE|CHECK|CONSTRAINT|INDEX|KEY)/i.test(trimmed)) continue; + const colMatch = trimmed.match(/^(?:`|")?(\w+)(?:`|")?\s+/); + if (colMatch) { + fields.push(colMatch[1]); + } + } + + return fields; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/terraform-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/terraform-parser.ts new file mode 100644 index 0000000..0fd44bf --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/terraform-parser.ts @@ -0,0 +1,122 @@ +import type { AnalyzerPlugin, StructuralAnalysis, ResourceInfo, DefinitionInfo } from "../../types.js"; + +export class TerraformParser implements AnalyzerPlugin { + name = "terraform-parser"; + languages = ["terraform"]; + + analyzeFile(_filePath: string, content: string): StructuralAnalysis { + const resources = this.extractResources(content); + const definitions = this.extractVariablesAndOutputs(content); + return { + functions: [], + classes: [], + imports: [], + exports: [], + resources, + definitions, + }; + } + + private extractResources(content: string): ResourceInfo[] { + const resources: ResourceInfo[] = []; + + // Match resource blocks: resource "type" "name" { + const resourceRegex = /^resource\s+"([^"]+)"\s+"([^"]+)"\s*\{/gm; + let match; + while ((match = resourceRegex.exec(content)) !== null) { + const startLine = content.slice(0, match.index).split("\n").length; + const afterMatch = content.slice(match.index); + const closeBrace = this.findClosingBrace(afterMatch); + const endLine = content.slice(0, match.index + closeBrace + 1).split("\n").length; + + resources.push({ + name: `${match[1]}.${match[2]}`, + kind: match[1], + lineRange: [startLine, endLine], + }); + } + + // Match data blocks: data "type" "name" { + const dataRegex = /^data\s+"([^"]+)"\s+"([^"]+)"\s*\{/gm; + while ((match = dataRegex.exec(content)) !== null) { + const startLine = content.slice(0, match.index).split("\n").length; + const afterMatch = content.slice(match.index); + const closeBrace = this.findClosingBrace(afterMatch); + const endLine = content.slice(0, match.index + closeBrace + 1).split("\n").length; + + resources.push({ + name: `data.${match[1]}.${match[2]}`, + kind: `data.${match[1]}`, + lineRange: [startLine, endLine], + }); + } + + // Match module blocks: module "name" { + const moduleRegex = /^module\s+"([^"]+)"\s*\{/gm; + while ((match = moduleRegex.exec(content)) !== null) { + const startLine = content.slice(0, match.index).split("\n").length; + const afterMatch = content.slice(match.index); + const closeBrace = this.findClosingBrace(afterMatch); + const endLine = content.slice(0, match.index + closeBrace + 1).split("\n").length; + + resources.push({ + name: `module.${match[1]}`, + kind: "module", + lineRange: [startLine, endLine], + }); + } + + return resources; + } + + private extractVariablesAndOutputs(content: string): DefinitionInfo[] { + const definitions: DefinitionInfo[] = []; + + // Match variable blocks + const varRegex = /^variable\s+"([^"]+)"\s*\{/gm; + let match; + while ((match = varRegex.exec(content)) !== null) { + const startLine = content.slice(0, match.index).split("\n").length; + const afterMatch = content.slice(match.index); + const closeBrace = this.findClosingBrace(afterMatch); + const endLine = content.slice(0, match.index + closeBrace + 1).split("\n").length; + + definitions.push({ + name: match[1], + kind: "variable", + lineRange: [startLine, endLine], + fields: [], + }); + } + + // Match output blocks + const outputRegex = /^output\s+"([^"]+)"\s*\{/gm; + while ((match = outputRegex.exec(content)) !== null) { + const startLine = content.slice(0, match.index).split("\n").length; + const afterMatch = content.slice(match.index); + const closeBrace = this.findClosingBrace(afterMatch); + const endLine = content.slice(0, match.index + closeBrace + 1).split("\n").length; + + definitions.push({ + name: match[1], + kind: "output", + lineRange: [startLine, endLine], + fields: [], + }); + } + + return definitions; + } + + private findClosingBrace(content: string): number { + let depth = 0; + for (let i = 0; i < content.length; i++) { + if (content[i] === "{") depth++; + if (content[i] === "}") { + depth--; + if (depth === 0) return i; + } + } + return content.length; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/toml-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/toml-parser.ts new file mode 100644 index 0000000..114db91 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/toml-parser.ts @@ -0,0 +1,41 @@ +import type { AnalyzerPlugin, StructuralAnalysis, SectionInfo } from "../../types.js"; + +export class TOMLParser implements AnalyzerPlugin { + name = "toml-parser"; + languages = ["toml"]; + + analyzeFile(_filePath: string, content: string): StructuralAnalysis { + const sections = this.extractSections(content); + return { + functions: [], + classes: [], + imports: [], + exports: [], + sections, + }; + } + + private extractSections(content: string): SectionInfo[] { + const sections: SectionInfo[] = []; + const lines = content.split("\n"); + for (let i = 0; i < lines.length; i++) { + // Match [section] and [[array-of-tables]] headers + const match = lines[i].match(/^\s*\[(\[?)([^\]]+)\]?\]/); + if (match) { + const isArray = match[1] === "["; + const name = match[2].trim(); + sections.push({ + name: isArray ? `[[${name}]]` : name, + level: name.split(".").length, + lineRange: [i + 1, i + 1], + }); + } + } + // Fix lineRange end + for (let i = 0; i < sections.length; i++) { + const next = sections[i + 1]; + sections[i].lineRange[1] = next ? next.lineRange[0] - 1 : lines.length; + } + return sections; + } +} diff --git a/understand-anything-plugin/packages/core/src/plugins/parsers/yaml-parser.ts b/understand-anything-plugin/packages/core/src/plugins/parsers/yaml-parser.ts new file mode 100644 index 0000000..7f9dfd6 --- /dev/null +++ b/understand-anything-plugin/packages/core/src/plugins/parsers/yaml-parser.ts @@ -0,0 +1,66 @@ +import type { AnalyzerPlugin, StructuralAnalysis, SectionInfo } from "../../types.js"; +import { parse as parseYAML } from "yaml"; + +export class YAMLConfigParser implements AnalyzerPlugin { + name = "yaml-config-parser"; + languages = ["yaml"]; + + analyzeFile(_filePath: string, content: string): StructuralAnalysis { + const sections = this.extractSections(content); + return { + functions: [], + classes: [], + imports: [], + exports: [], + sections, + }; + } + + private extractSections(content: string): SectionInfo[] { + const sections: SectionInfo[] = []; + try { + const doc = parseYAML(content); + if (doc && typeof doc === "object" && !Array.isArray(doc)) { + const lines = content.split("\n"); + for (const key of Object.keys(doc)) { + // Find the line where this top-level key appears + const lineIdx = lines.findIndex((l) => l.match(new RegExp(`^${this.escapeRegex(key)}\\s*:`))); + if (lineIdx !== -1) { + sections.push({ + name: key, + level: 1, + lineRange: [lineIdx + 1, lineIdx + 1], + }); + } + } + // Fix lineRange end + for (let i = 0; i < sections.length; i++) { + const next = sections[i + 1]; + sections[i].lineRange[1] = next ? next.lineRange[0] - 1 : lines.length; + } + } + } catch { + // If YAML parsing fails, fall back to regex + const lines = content.split("\n"); + for (let i = 0; i < lines.length; i++) { + const match = lines[i].match(/^(\w[\w-]*)\s*:/); + if (match) { + sections.push({ + name: match[1], + level: 1, + lineRange: [i + 1, i + 1], + }); + } + } + for (let i = 0; i < sections.length; i++) { + const next = sections[i + 1]; + sections[i].lineRange[1] = next ? next.lineRange[0] - 1 : lines.length; + } + } + return sections; + } + + private escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + } +} diff --git a/understand-anything-plugin/pnpm-lock.yaml b/understand-anything-plugin/pnpm-lock.yaml index c3844a3..d2f0f61 100644 --- a/understand-anything-plugin/pnpm-lock.yaml +++ b/understand-anything-plugin/pnpm-lock.yaml @@ -20,19 +20,23 @@ importers: version: 5.9.3 vitest: specifier: ^3.1.0 - version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0) + version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) packages/core: + dependencies: + yaml: + specifier: ^2.8.3 + version: 2.8.3 devDependencies: '@types/node': specifier: ^25.5.0 version: 25.5.0 '@vitest/coverage-v8': specifier: 3.2.4 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) vitest: specifier: ^3.1.0 - version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0) + version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) packages/dashboard: dependencies: @@ -66,7 +70,7 @@ importers: devDependencies: '@tailwindcss/vite': specifier: ^4.0.0 - version: 4.2.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)) + version: 4.2.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) '@types/react': specifier: ^19.0.0 version: 19.2.14 @@ -75,7 +79,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^4.3.0 - version: 4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)) + version: 4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) tailwindcss: specifier: ^4.0.0 version: 4.2.2 @@ -84,7 +88,7 @@ importers: version: 5.9.3 vite: specifier: ^6.0.0 - version: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0) + version: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) packages: @@ -406,79 +410,66 @@ packages: resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.0': resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.0': resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.0': resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.0': resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.0': resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.0': resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.0': resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.0': resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.0': resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.0': resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.0': resolution: {integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.0': resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.60.0': resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==} @@ -548,28 +539,24 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.2': resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.2': resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.2': resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.2': resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} @@ -1092,28 +1079,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -1579,6 +1562,11 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + zustand@4.5.7: resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} engines: {node: '>=12.7.0'} @@ -1991,12 +1979,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 - '@tailwindcss/vite@4.2.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0))': + '@tailwindcss/vite@4.2.2(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': dependencies: '@tailwindcss/node': 4.2.2 '@tailwindcss/oxide': 4.2.2 tailwindcss: 4.2.2 - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) '@types/babel__core@7.20.5': dependencies: @@ -2089,7 +2077,7 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -2097,11 +2085,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -2116,7 +2104,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.2 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0) + vitest: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -2128,21 +2116,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0))': + '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0) - - '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) '@vitest/pretty-format@3.2.4': dependencies: @@ -3076,13 +3056,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-node@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0): + vite-node@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -3097,13 +3077,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0): + vite-node@3.2.4(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -3118,7 +3098,7 @@ snapshots: - tsx - yaml - vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0): + vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -3131,8 +3111,9 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.32.0 + yaml: 2.8.3 - vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0): + vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -3145,12 +3126,13 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.32.0 + yaml: 2.8.3 - vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0): + vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)) + '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -3168,8 +3150,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0) - vite-node: 3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + vite-node: 3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.13 @@ -3188,11 +3170,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0): + vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)) + '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -3210,8 +3192,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0) - vite-node: 3.2.4(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + vite-node: 3.2.4(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.13 @@ -3253,6 +3235,8 @@ snapshots: yallist@3.1.1: {} + yaml@2.8.3: {} + zustand@4.5.7(@types/react@19.2.14)(react@19.2.4): dependencies: use-sync-external-store: 1.6.0(react@19.2.4)