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) <noreply@anthropic.com>
This commit is contained in:
Lum1104
2026-03-28 18:38:25 +08:00
Unverified
parent c76b0a1f2d
commit 3fd688ac37
16 changed files with 1388 additions and 57 deletions
@@ -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"
}
}
@@ -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");
});
});
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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());
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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, "\\$&");
}
}
+41 -57
View File
@@ -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)