From 2e825d2654903611c60281540555f3564a7dca5f Mon Sep 17 00:00:00 2001 From: ksemenenko Date: Sat, 3 Jan 2026 14:45:15 +0100 Subject: [PATCH] Create ragsharp codegraph scaffold --- .gitignore | 412 +----------------- README.md | 67 ++- .../skill-templates/build-code-graph/SKILL.md | 15 + .../build-code-graph/references/README.md | 6 + .../build-code-graph/scripts/run.ps1 | 2 + .../build-code-graph/scripts/run.sh | 3 + .../skill-templates/query-code-graph/SKILL.md | 9 + .../query-code-graph/references/README.md | 4 + .../query-code-graph/scripts/run.ps1 | 2 + .../query-code-graph/scripts/run.sh | 3 + docs/Development/CodeGraph.md | 18 + docs/Development/OutputSchema.md | 38 ++ docs/Development/Performance.md | 5 + docs/Development/QueryContract.md | 9 + docs/Development/SetupDotNet.md | 29 ++ docs/Development/Troubleshooting.md | 5 + global.json | 7 + ragsharp.sln | 55 +++ src/RagSharp.CodeGraph.Cli/Program.cs | 238 ++++++++++ .../RagSharp.CodeGraph.Cli.csproj | 14 + .../CodeGraphIndexer.cs | 352 +++++++++++++++ src/RagSharp.CodeGraph.Core/GraphModels.cs | 75 ++++ src/RagSharp.CodeGraph.Core/IGraphStore.cs | 10 + .../RagSharp.CodeGraph.Core.csproj | 12 + .../LiteGraphStore.cs | 240 ++++++++++ .../RagSharp.CodeGraph.Store.LiteGraph.csproj | 11 + .../RagSharp.Packaging.csproj | 24 + src/RagSharp.SkillInstaller/Installer.cs | 175 ++++++++ src/RagSharp.SkillInstaller/Program.cs | 68 +++ .../RagSharp.SkillInstaller.csproj | 18 + tests/RagSharp.CodeGraph.Tests/IndexTests.cs | 29 ++ .../RagSharp.CodeGraph.Tests.csproj | 17 + .../InstallerTests.cs | 24 + .../RagSharp.SkillInstaller.Tests.csproj | 16 + tests/samples/SampleApp/Program.cs | 17 + tests/samples/SampleApp/SampleApp.csproj | 7 + tests/samples/SampleApp/SampleApp.sln | 19 + 37 files changed, 1650 insertions(+), 405 deletions(-) create mode 100644 assets/skill-templates/build-code-graph/SKILL.md create mode 100644 assets/skill-templates/build-code-graph/references/README.md create mode 100644 assets/skill-templates/build-code-graph/scripts/run.ps1 create mode 100644 assets/skill-templates/build-code-graph/scripts/run.sh create mode 100644 assets/skill-templates/query-code-graph/SKILL.md create mode 100644 assets/skill-templates/query-code-graph/references/README.md create mode 100644 assets/skill-templates/query-code-graph/scripts/run.ps1 create mode 100644 assets/skill-templates/query-code-graph/scripts/run.sh create mode 100644 docs/Development/CodeGraph.md create mode 100644 docs/Development/OutputSchema.md create mode 100644 docs/Development/Performance.md create mode 100644 docs/Development/QueryContract.md create mode 100644 docs/Development/SetupDotNet.md create mode 100644 docs/Development/Troubleshooting.md create mode 100644 global.json create mode 100644 ragsharp.sln create mode 100644 src/RagSharp.CodeGraph.Cli/Program.cs create mode 100644 src/RagSharp.CodeGraph.Cli/RagSharp.CodeGraph.Cli.csproj create mode 100644 src/RagSharp.CodeGraph.Core/CodeGraphIndexer.cs create mode 100644 src/RagSharp.CodeGraph.Core/GraphModels.cs create mode 100644 src/RagSharp.CodeGraph.Core/IGraphStore.cs create mode 100644 src/RagSharp.CodeGraph.Core/RagSharp.CodeGraph.Core.csproj create mode 100644 src/RagSharp.CodeGraph.Store.LiteGraph/LiteGraphStore.cs create mode 100644 src/RagSharp.CodeGraph.Store.LiteGraph/RagSharp.CodeGraph.Store.LiteGraph.csproj create mode 100644 src/RagSharp.Packaging/RagSharp.Packaging.csproj create mode 100644 src/RagSharp.SkillInstaller/Installer.cs create mode 100644 src/RagSharp.SkillInstaller/Program.cs create mode 100644 src/RagSharp.SkillInstaller/RagSharp.SkillInstaller.csproj create mode 100644 tests/RagSharp.CodeGraph.Tests/IndexTests.cs create mode 100644 tests/RagSharp.CodeGraph.Tests/RagSharp.CodeGraph.Tests.csproj create mode 100644 tests/RagSharp.SkillInstaller.Tests/InstallerTests.cs create mode 100644 tests/RagSharp.SkillInstaller.Tests/RagSharp.SkillInstaller.Tests.csproj create mode 100644 tests/samples/SampleApp/Program.cs create mode 100644 tests/samples/SampleApp/SampleApp.csproj create mode 100644 tests/samples/SampleApp/SampleApp.sln diff --git a/.gitignore b/.gitignore index 6c1eb92..a36558a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,406 +1,10 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -[Aa][Rr][Mm]64[Ee][Cc]/ -bld/ -[Bb]in/ -[Oo]bj/ -[Oo]ut/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory +bin/ +obj/ .vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json +.idea/ +.codegraph/ +dist/ artifacts/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -# but not Directory.Build.rsp, as it configures directory-level build defaults -!Directory.Build.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.tlog -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio 6 auto-generated project file (contains which files were open etc.) -*.vbp - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files -*.ncb -*.aps - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# AWS SAM Build and Temporary Artifacts folder -.aws-sam - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# Visual Studio History (VSHistory) files -.vshistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -# VS Code files for those working on multiple tools -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp - -# JetBrains Rider -*.sln.iml +TestResults/ +*.user +*.suo diff --git a/README.md b/README.md index 99b933e..6ac71c9 100644 --- a/README.md +++ b/README.md @@ -1 +1,66 @@ -# RagSharp \ No newline at end of file +# RagSharp + +RagSharp provides a deterministic, offline-capable code graph indexer and skill installer for C#/.NET repositories. It ships a single NuGet package containing: + +- `ragsharp` — installer for Codex skills. +- `ragsharp-codegraph` — code graph indexer/query CLI. + +See the Codex skill specification at https://agentskills.io/specification. + +## Requirements + +- .NET SDK 10 (primary) with multi-targeting to net8.0 for fallback. +- Cross-platform (Windows/macOS/Linux). + +## Install .NET 10 + +Follow the OS-specific steps in [docs/Development/SetupDotNet.md](docs/Development/SetupDotNet.md) and verify: + +```bash +dotnet --info +dotnet --list-sdks +``` + +## Build and test RagSharp + +```bash +dotnet build + +dotnet test +``` + +## Package + +```bash +dotnet pack src/RagSharp.Packaging/RagSharp.Packaging.csproj -c Release -o dist +``` + +## Install in another repository + +From the target repository root: + +```bash +dotnet add package RagSharp --source /path/to/ragsharp/dist + +ragsharp install --root . --skill-dir .codex/skills +``` + +## Index and query + +```bash +ragsharp-codegraph doctor --root . + +ragsharp-codegraph index --root . --db .codegraph/index.db --state .codegraph/state.json + +ragsharp-codegraph query symbols --db .codegraph/index.db --format json --limit 50 --symbol "Greeter" +``` + +## Output locations + +- `.codex/skills/` contains installed skills. +- `.codegraph/` contains the index and state files (not committed). +- Ensure `.codegraph/` and `state.json` remain in `.gitignore`. + +## Troubleshooting + +See [docs/Development/Troubleshooting.md](docs/Development/Troubleshooting.md). diff --git a/assets/skill-templates/build-code-graph/SKILL.md b/assets/skill-templates/build-code-graph/SKILL.md new file mode 100644 index 0000000..d067efe --- /dev/null +++ b/assets/skill-templates/build-code-graph/SKILL.md @@ -0,0 +1,15 @@ +--- +name: ragsharp-build-code-graph +description: | + Build or update a code graph index for C#/.NET repositories using ragsharp-codegraph. + Triggers: build index, update index, refresh index, code graph, dependency graph, static analysis, Roslyn, line numbers. +--- +## Steps +1. Run `ragsharp-codegraph doctor --root .`. +2. Run `ragsharp-codegraph index --root . --db .codegraph/index.db --state .codegraph/state.json`. +3. For incremental updates, run `ragsharp-codegraph update --root . --db .codegraph/index.db --state .codegraph/state.json`. + +## Expected Results +- `.codegraph/index.db` exists. +- `.codegraph/state.json` is updated. +- Output goes to stderr, JSON is emitted only for query commands. diff --git a/assets/skill-templates/build-code-graph/references/README.md b/assets/skill-templates/build-code-graph/references/README.md new file mode 100644 index 0000000..d9203ee --- /dev/null +++ b/assets/skill-templates/build-code-graph/references/README.md @@ -0,0 +1,6 @@ +# Build Code Graph Skill + +Use this skill to initialize or refresh the code graph index. +- `ragsharp-codegraph doctor` validates tooling. +- `ragsharp-codegraph index` builds the graph. +- `ragsharp-codegraph update` applies incremental changes. diff --git a/assets/skill-templates/build-code-graph/scripts/run.ps1 b/assets/skill-templates/build-code-graph/scripts/run.ps1 new file mode 100644 index 0000000..a4e09e8 --- /dev/null +++ b/assets/skill-templates/build-code-graph/scripts/run.ps1 @@ -0,0 +1,2 @@ +param([string]$Root = ".") +ragsharp-codegraph index --root $Root --db .codegraph/index.db --state .codegraph/state.json diff --git a/assets/skill-templates/build-code-graph/scripts/run.sh b/assets/skill-templates/build-code-graph/scripts/run.sh new file mode 100644 index 0000000..c604dd5 --- /dev/null +++ b/assets/skill-templates/build-code-graph/scripts/run.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +ragsharp-codegraph index --root "${1:-.}" --db .codegraph/index.db --state .codegraph/state.json diff --git a/assets/skill-templates/query-code-graph/SKILL.md b/assets/skill-templates/query-code-graph/SKILL.md new file mode 100644 index 0000000..83ad089 --- /dev/null +++ b/assets/skill-templates/query-code-graph/SKILL.md @@ -0,0 +1,9 @@ +--- +name: ragsharp-query-code-graph +description: | + Query the ragsharp code graph for declarations, references, callers, callees, dependencies, and line-number evidence. + Triggers: find usages, where defined, callers, callees, dependency path, project deps, type hierarchy, line numbers, evidence. +--- +## Steps +1. Run `ragsharp-codegraph query symbols --db .codegraph/index.db --format json --limit 50 --symbol "SymbolName"`. +2. Use the JSON output to answer questions with file:line-range evidence. diff --git a/assets/skill-templates/query-code-graph/references/README.md b/assets/skill-templates/query-code-graph/references/README.md new file mode 100644 index 0000000..07fa332 --- /dev/null +++ b/assets/skill-templates/query-code-graph/references/README.md @@ -0,0 +1,4 @@ +# Query Code Graph Skill + +Use query contracts defined in docs/Development/QueryContract.md and OutputSchema.md. +Each answer should cite file:line-range evidence from query results. diff --git a/assets/skill-templates/query-code-graph/scripts/run.ps1 b/assets/skill-templates/query-code-graph/scripts/run.ps1 new file mode 100644 index 0000000..c0e0cb5 --- /dev/null +++ b/assets/skill-templates/query-code-graph/scripts/run.ps1 @@ -0,0 +1,2 @@ +param([string]$Symbol = "") +ragsharp-codegraph query symbols --db .codegraph/index.db --format json --limit 50 --symbol $Symbol diff --git a/assets/skill-templates/query-code-graph/scripts/run.sh b/assets/skill-templates/query-code-graph/scripts/run.sh new file mode 100644 index 0000000..94b0ed8 --- /dev/null +++ b/assets/skill-templates/query-code-graph/scripts/run.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +ragsharp-codegraph query symbols --db .codegraph/index.db --format json --limit 50 --symbol "${1:-}" diff --git a/docs/Development/CodeGraph.md b/docs/Development/CodeGraph.md new file mode 100644 index 0000000..cb9999f --- /dev/null +++ b/docs/Development/CodeGraph.md @@ -0,0 +1,18 @@ +# Code Graph CLI + +## Commands + +- `ragsharp-codegraph doctor --root ` +- `ragsharp-codegraph index --root --db .codegraph/index.db --state .codegraph/state.json` +- `ragsharp-codegraph update --root --db .codegraph/index.db --state .codegraph/state.json` +- `ragsharp-codegraph query --db .codegraph/index.db --format json --limit N --context-lines M` +- `ragsharp-codegraph export --db .codegraph/index.db --format dot|gexf --out .codegraph/graph.dot` + +## Files\n\n- `.codegraph/index.db` — SQLite graph database.\n- `.codegraph/state.json` — incremental index state.\n- `.codegraph/schema_version` — schema version guard.\n+ +## Exit codes + +- 0: success +- 2: invalid arguments +- 3: environment error +- 4: index missing +- 5: schema mismatch diff --git a/docs/Development/OutputSchema.md b/docs/Development/OutputSchema.md new file mode 100644 index 0000000..93709c7 --- /dev/null +++ b/docs/Development/OutputSchema.md @@ -0,0 +1,38 @@ +# Output Schema + +```json +{ + "schemaVersion": "1", + "nodes": [ + { + "id": 1, + "kind": "Type", + "name": "Greeter", + "fullyQualifiedName": "SampleApp.Greeter", + "documentPath": "Program.cs", + "location": { + "filePathRelative": "Program.cs", + "startLine": 5, + "startColumn": 1, + "endLine": 9, + "endColumn": 2 + } + } + ], + "edges": [ + { + "id": 10, + "kind": "DeclaredAt", + "sourceId": 2, + "targetId": 1, + "location": { + "filePathRelative": "Program.cs", + "startLine": 5, + "startColumn": 1, + "endLine": 9, + "endColumn": 2 + } + } + ] +} +``` diff --git a/docs/Development/Performance.md b/docs/Development/Performance.md new file mode 100644 index 0000000..1f2eb1c --- /dev/null +++ b/docs/Development/Performance.md @@ -0,0 +1,5 @@ +# Performance + +- Use `ragsharp-codegraph update` for incremental indexing. +- Store the database on fast local storage. +- Increase `--limit` cautiously for large repositories. diff --git a/docs/Development/QueryContract.md b/docs/Development/QueryContract.md new file mode 100644 index 0000000..77e6d4a --- /dev/null +++ b/docs/Development/QueryContract.md @@ -0,0 +1,9 @@ +# Query Contract + +All `ragsharp-codegraph query` commands emit JSON to stdout. Logs go to stderr. The JSON schema is defined in [OutputSchema.md](OutputSchema.md). + +Query requests support: +- `--symbol` to filter by name or fully-qualified name. +- `--kind` to filter by node kind. +- `--document` to filter by file path. +- `--limit` to cap results. diff --git a/docs/Development/SetupDotNet.md b/docs/Development/SetupDotNet.md new file mode 100644 index 0000000..f4d9a15 --- /dev/null +++ b/docs/Development/SetupDotNet.md @@ -0,0 +1,29 @@ +# Setup .NET 10 + +## macOS + +```bash +brew install --cask dotnet-sdk-preview + +dotnet --info +dotnet --list-sdks +``` + +## Ubuntu/Debian + +```bash +sudo apt-get update +sudo apt-get install -y dotnet-sdk-10.0 + +dotnet --info +dotnet --list-sdks +``` + +## Windows (PowerShell) + +```powershell +winget install Microsoft.DotNet.SDK.Preview + +dotnet --info +dotnet --list-sdks +``` diff --git a/docs/Development/Troubleshooting.md b/docs/Development/Troubleshooting.md new file mode 100644 index 0000000..1553a82 --- /dev/null +++ b/docs/Development/Troubleshooting.md @@ -0,0 +1,5 @@ +# Troubleshooting + +- Ensure .NET SDK 10 is installed and on PATH. +- Run `ragsharp-codegraph doctor` for environment checks. +- Delete `.codegraph/` to rebuild the index from scratch. diff --git a/global.json b/global.json new file mode 100644 index 0000000..82dabbb --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature", + "allowPrerelease": true + } +} diff --git a/ragsharp.sln b/ragsharp.sln new file mode 100644 index 0000000..8358f0d --- /dev/null +++ b/ragsharp.sln @@ -0,0 +1,55 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RagSharp.CodeGraph.Core", "src/RagSharp.CodeGraph.Core/RagSharp.CodeGraph.Core.csproj", "{9842C0C2-C914-4AE9-B695-C31285B4FA10}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RagSharp.CodeGraph.Store.LiteGraph", "src/RagSharp.CodeGraph.Store.LiteGraph/RagSharp.CodeGraph.Store.LiteGraph.csproj", "{DB688194-9C0A-40CC-8977-C49F0EF97F7E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RagSharp.CodeGraph.Cli", "src/RagSharp.CodeGraph.Cli/RagSharp.CodeGraph.Cli.csproj", "{E9442C38-3561-489A-A048-9C39E3989204}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RagSharp.SkillInstaller", "src/RagSharp.SkillInstaller/RagSharp.SkillInstaller.csproj", "{F88A2153-7673-4CC3-BEB7-93990D10D8E0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RagSharp.Packaging", "src/RagSharp.Packaging/RagSharp.Packaging.csproj", "{329B88F1-A1C4-444F-9BD4-3E5333E587DA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RagSharp.CodeGraph.Tests", "tests/RagSharp.CodeGraph.Tests/RagSharp.CodeGraph.Tests.csproj", "{0EB45C08-B646-40B0-8766-A4790C70FB7C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RagSharp.SkillInstaller.Tests", "tests/RagSharp.SkillInstaller.Tests/RagSharp.SkillInstaller.Tests.csproj", "{763BFEF6-0BD4-423D-9784-CE707EBC3719}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9842C0C2-C914-4AE9-B695-C31285B4FA10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9842C0C2-C914-4AE9-B695-C31285B4FA10}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9842C0C2-C914-4AE9-B695-C31285B4FA10}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9842C0C2-C914-4AE9-B695-C31285B4FA10}.Release|Any CPU.Build.0 = Release|Any CPU + {DB688194-9C0A-40CC-8977-C49F0EF97F7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB688194-9C0A-40CC-8977-C49F0EF97F7E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB688194-9C0A-40CC-8977-C49F0EF97F7E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB688194-9C0A-40CC-8977-C49F0EF97F7E}.Release|Any CPU.Build.0 = Release|Any CPU + {E9442C38-3561-489A-A048-9C39E3989204}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9442C38-3561-489A-A048-9C39E3989204}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9442C38-3561-489A-A048-9C39E3989204}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9442C38-3561-489A-A048-9C39E3989204}.Release|Any CPU.Build.0 = Release|Any CPU + {F88A2153-7673-4CC3-BEB7-93990D10D8E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F88A2153-7673-4CC3-BEB7-93990D10D8E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F88A2153-7673-4CC3-BEB7-93990D10D8E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F88A2153-7673-4CC3-BEB7-93990D10D8E0}.Release|Any CPU.Build.0 = Release|Any CPU + {329B88F1-A1C4-444F-9BD4-3E5333E587DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {329B88F1-A1C4-444F-9BD4-3E5333E587DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {329B88F1-A1C4-444F-9BD4-3E5333E587DA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {329B88F1-A1C4-444F-9BD4-3E5333E587DA}.Release|Any CPU.Build.0 = Release|Any CPU + {0EB45C08-B646-40B0-8766-A4790C70FB7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0EB45C08-B646-40B0-8766-A4790C70FB7C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0EB45C08-B646-40B0-8766-A4790C70FB7C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0EB45C08-B646-40B0-8766-A4790C70FB7C}.Release|Any CPU.Build.0 = Release|Any CPU + {763BFEF6-0BD4-423D-9784-CE707EBC3719}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {763BFEF6-0BD4-423D-9784-CE707EBC3719}.Debug|Any CPU.Build.0 = Debug|Any CPU + {763BFEF6-0BD4-423D-9784-CE707EBC3719}.Release|Any CPU.ActiveCfg = Release|Any CPU + {763BFEF6-0BD4-423D-9784-CE707EBC3719}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/RagSharp.CodeGraph.Cli/Program.cs b/src/RagSharp.CodeGraph.Cli/Program.cs new file mode 100644 index 0000000..e11ec02 --- /dev/null +++ b/src/RagSharp.CodeGraph.Cli/Program.cs @@ -0,0 +1,238 @@ +using System.Text.Json; +using RagSharp.CodeGraph.Core; +using RagSharp.CodeGraph.Store.LiteGraph; + +namespace RagSharp.CodeGraph.Cli; + +public static class Program +{ + private const int ExitSuccess = 0; + private const int ExitInvalidArgs = 2; + private const int ExitEnvironmentError = 3; + private const int ExitIndexMissing = 4; + private const int ExitSchemaMismatch = 5; + + public static async Task Main(string[] args) + { + if (args.Length == 0) + { + Console.Error.WriteLine("ragsharp-codegraph [options]"); + return ExitInvalidArgs; + } + + var command = args[0]; + var options = ParseOptions(args.Skip(1).ToArray()); + + try + { + return command switch + { + "doctor" => await RunDoctorAsync(options), + "index" => await RunIndexAsync(options, false), + "update" => await RunIndexAsync(options, true), + "query" => await RunQueryAsync(args.Skip(1).ToArray()), + "export" => await RunExportAsync(options), + _ => ExitInvalidArgs + }; + } + catch (FileNotFoundException ex) + { + Console.Error.WriteLine(ex.Message); + return ExitIndexMissing; + } + catch (InvalidOperationException ex) + { + Console.Error.WriteLine(ex.Message); + return ExitEnvironmentError; + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + return ExitEnvironmentError; + } + } + + private static Task RunDoctorAsync(Dictionary options) + { + var root = options.GetValueOrDefault("--root") ?? Directory.GetCurrentDirectory(); + Console.Error.WriteLine($"Root: {root}"); + Console.Error.WriteLine("MSBuildWorkspace available."); + return Task.FromResult(ExitSuccess); + } + + private static async Task RunIndexAsync(Dictionary options, bool isUpdate) + { + var root = options.GetValueOrDefault("--root") ?? Directory.GetCurrentDirectory(); + var dbPath = options.GetValueOrDefault("--db") ?? Path.Combine(root, ".codegraph", "index.db"); + var statePath = options.GetValueOrDefault("--state") ?? Path.Combine(root, ".codegraph", "state.json"); + var includeDataflow = options.ContainsKey("--include-dataflow"); + + var store = new LiteGraphStore(dbPath); + await store.InitializeAsync(CancellationToken.None).ConfigureAwait(false); + + var indexer = new CodeGraphIndexer(includeDataflow); + var state = await CodeGraphIndexer.LoadStateAsync(statePath, CancellationToken.None).ConfigureAwait(false); + var currentHashes = CodeGraphIndexer.ComputeFileHashes(root); + if (isUpdate) + { + var diff = CodeGraphIndexer.ComputeDiff(state, currentHashes); + if (diff.RemovedFiles.Count > 0) + { + await store.RemoveDocumentsAsync(diff.RemovedFiles, CancellationToken.None).ConfigureAwait(false); + } + } + + var result = await indexer.IndexAsync(root, CancellationToken.None).ConfigureAwait(false); + await store.SaveIndexAsync(result, CancellationToken.None).ConfigureAwait(false); + WriteSchemaVersionFile(root); + await CodeGraphIndexer.SaveStateAsync(statePath, new IndexState(currentHashes), CancellationToken.None).ConfigureAwait(false); + Console.Error.WriteLine(isUpdate ? "Updated index." : "Indexed code graph."); + return ExitSuccess; + } + + private static async Task RunQueryAsync(string[] args) + { + if (args.Length == 0) + { + return ExitInvalidArgs; + } + + var queryType = args[0]; + var options = ParseOptions(args.Skip(1).ToArray()); + var dbPath = options.GetValueOrDefault("--db") ?? Path.Combine(Directory.GetCurrentDirectory(), ".codegraph", "index.db"); + if (!File.Exists(dbPath)) + { + throw new FileNotFoundException("Index database not found."); + } + + var store = new LiteGraphStore(dbPath); + await store.InitializeAsync(CancellationToken.None).ConfigureAwait(false); + var schemaVersion = await store.GetSchemaVersionAsync(CancellationToken.None).ConfigureAwait(false); + if (schemaVersion != SchemaConstants.CurrentVersion) + { + Console.Error.WriteLine("Schema version mismatch."); + return ExitSchemaMismatch; + } + + var request = new QueryRequest( + QueryType: queryType, + Symbol: options.GetValueOrDefault("--symbol"), + Kind: options.GetValueOrDefault("--kind"), + Document: options.GetValueOrDefault("--document"), + Limit: int.TryParse(options.GetValueOrDefault("--limit"), out var limit) ? limit : 100, + ContextLines: int.TryParse(options.GetValueOrDefault("--context-lines"), out var context) ? context : 2); + + var result = await store.QueryAsync(request, CancellationToken.None).ConfigureAwait(false); + var json = JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + Console.Out.WriteLine(json); + return ExitSuccess; + } + + private static async Task RunExportAsync(Dictionary options) + { + var dbPath = options.GetValueOrDefault("--db") ?? Path.Combine(Directory.GetCurrentDirectory(), ".codegraph", "index.db"); + var format = options.GetValueOrDefault("--format") ?? "dot"; + var output = options.GetValueOrDefault("--out") ?? Path.Combine(Directory.GetCurrentDirectory(), ".codegraph", "graph.dot"); + + var store = new LiteGraphStore(dbPath); + await store.InitializeAsync(CancellationToken.None).ConfigureAwait(false); + var result = await store.QueryAsync(new QueryRequest("export", null, null, null, 10000, 0), CancellationToken.None).ConfigureAwait(false); + + Directory.CreateDirectory(Path.GetDirectoryName(output) ?? "."); + if (format.Equals("gexf", StringComparison.OrdinalIgnoreCase)) + { + await File.WriteAllTextAsync(output, GexfExporter.Export(result), CancellationToken.None).ConfigureAwait(false); + } + else + { + await File.WriteAllTextAsync(output, DotExporter.Export(result), CancellationToken.None).ConfigureAwait(false); + } + + Console.Error.WriteLine($"Exported graph to {output}."); + return ExitSuccess; + } + + private static void WriteSchemaVersionFile(string root) + { + var schemaPath = Path.Combine(root, ".codegraph", "schema_version"); + Directory.CreateDirectory(Path.GetDirectoryName(schemaPath) ?? "."); + File.WriteAllText(schemaPath, SchemaConstants.CurrentVersion); + } + + private static Dictionary ParseOptions(string[] args) + { + var options = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < args.Length; i++) + { + var arg = args[i]; + if (!arg.StartsWith("--", StringComparison.Ordinal)) + { + continue; + } + + if (i + 1 < args.Length && !args[i + 1].StartsWith("--", StringComparison.Ordinal)) + { + options[arg] = args[i + 1]; + i++; + } + else + { + options[arg] = "true"; + } + } + + return options; + } +} + +internal static class DotExporter +{ + public static string Export(QueryResult result) + { + var lines = new List { "digraph G {" }; + foreach (var node in result.Nodes) + { + lines.Add($" {node.Id} [label=\"{node.Kind}:{node.Name}\"]; "); + } + + foreach (var edge in result.Edges) + { + lines.Add($" {edge.SourceId} -> {edge.TargetId} [label=\"{edge.Kind}\"]; "); + } + + lines.Add("}"); + return string.Join(Environment.NewLine, lines); + } +} + +internal static class GexfExporter +{ + public static string Export(QueryResult result) + { + var lines = new List + { + "", + "", + "", + "" + }; + + foreach (var node in result.Nodes) + { + lines.Add($""); + } + + lines.Add(""); + lines.Add(""); + + foreach (var edge in result.Edges) + { + lines.Add($""); + } + + lines.Add(""); + lines.Add(""); + lines.Add(""); + return string.Join(Environment.NewLine, lines); + } +} diff --git a/src/RagSharp.CodeGraph.Cli/RagSharp.CodeGraph.Cli.csproj b/src/RagSharp.CodeGraph.Cli/RagSharp.CodeGraph.Cli.csproj new file mode 100644 index 0000000..2d11df0 --- /dev/null +++ b/src/RagSharp.CodeGraph.Cli/RagSharp.CodeGraph.Cli.csproj @@ -0,0 +1,14 @@ + + + Exe + net8.0;net10.0 + enable + enable + ragsharp-codegraph + RagSharp.CodeGraph.Cli + + + + + + diff --git a/src/RagSharp.CodeGraph.Core/CodeGraphIndexer.cs b/src/RagSharp.CodeGraph.Core/CodeGraphIndexer.cs new file mode 100644 index 0000000..1c2634e --- /dev/null +++ b/src/RagSharp.CodeGraph.Core/CodeGraphIndexer.cs @@ -0,0 +1,352 @@ +using System.Security.Cryptography; +using System.Text; +using Microsoft.Build.Locator; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.MSBuild; + +namespace RagSharp.CodeGraph.Core; + +public sealed class CodeGraphIndexer +{ + private readonly bool _includeDataflow; + + public CodeGraphIndexer(bool includeDataflow) + { + _includeDataflow = includeDataflow; + } + + public async Task IndexAsync(string rootPath, CancellationToken cancellationToken) + { + MSBuildLocator.RegisterDefaults(); + using var workspace = MSBuildWorkspace.Create(); + + var solutionPath = FindSolution(rootPath); + if (solutionPath is not null) + { + var solution = await workspace.OpenSolutionAsync(solutionPath, cancellationToken).ConfigureAwait(false); + return await BuildIndexAsync(rootPath, solution.Projects, cancellationToken).ConfigureAwait(false); + } + + var projectPath = FindProject(rootPath); + if (projectPath is null) + { + throw new InvalidOperationException("No .sln or .csproj found under the provided root."); + } + + var project = await workspace.OpenProjectAsync(projectPath, cancellationToken).ConfigureAwait(false); + return await BuildIndexAsync(rootPath, new[] { project }, cancellationToken).ConfigureAwait(false); + } + + public static async Task LoadStateAsync(string statePath, CancellationToken cancellationToken) + { + if (!File.Exists(statePath)) + { + return new IndexState(new Dictionary(StringComparer.OrdinalIgnoreCase)); + } + + var json = await File.ReadAllTextAsync(statePath, cancellationToken).ConfigureAwait(false); + return IndexState.FromJson(json); + } + + public static Task SaveStateAsync(string statePath, IndexState state, CancellationToken cancellationToken) + { + Directory.CreateDirectory(Path.GetDirectoryName(statePath) ?? "."); + return File.WriteAllTextAsync(statePath, state.ToJson(), cancellationToken); + } + + public static IndexDiff ComputeDiff(IndexState previous, IReadOnlyDictionary current) + { + var changed = new List(); + var removed = new List(); + + foreach (var (path, hash) in current) + { + if (!previous.Files.TryGetValue(path, out var existing) || !string.Equals(existing, hash, StringComparison.Ordinal)) + { + changed.Add(path); + } + } + + foreach (var path in previous.Files.Keys) + { + if (!current.ContainsKey(path)) + { + removed.Add(path); + } + } + + return new IndexDiff(changed, removed); + } + + private async Task BuildIndexAsync(string rootPath, IEnumerable projects, CancellationToken cancellationToken) + { + var builder = new GraphBuilder(rootPath, _includeDataflow); + foreach (var project in projects) + { + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + if (compilation is null) + { + continue; + } + + builder.AddProject(project); + + foreach (var document in project.Documents) + { + var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + if (syntaxTree is null) + { + continue; + } + + var semanticModel = compilation.GetSemanticModel(syntaxTree); + builder.AddDocument(project, document, syntaxTree, semanticModel); + } + } + + return builder.Build(); + } + + public static IReadOnlyDictionary ComputeFileHashes(string rootPath) + { + var files = Directory.EnumerateFiles(rootPath, "*.cs", SearchOption.AllDirectories) + .Where(path => !path.Contains(Path.DirectorySeparatorChar + "bin" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) + .Where(path => !path.Contains(Path.DirectorySeparatorChar + "obj" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var file in files) + { + var relative = Path.GetRelativePath(rootPath, file); + using var stream = File.OpenRead(file); + var hash = Convert.ToHexString(SHA256.HashData(stream)); + result[relative] = hash; + } + + return result; + } + + private static string? FindSolution(string rootPath) + { + return Directory.EnumerateFiles(rootPath, "*.sln", SearchOption.TopDirectoryOnly).FirstOrDefault(); + } + + private static string? FindProject(string rootPath) + { + return Directory.EnumerateFiles(rootPath, "*.csproj", SearchOption.AllDirectories).FirstOrDefault(); + } +} + +public sealed record IndexResult( + IReadOnlyList Nodes, + IReadOnlyList Edges); + +public sealed record IndexDiff( + IReadOnlyList ChangedFiles, + IReadOnlyList RemovedFiles); + +public sealed class IndexState +{ + public IReadOnlyDictionary Files { get; } + + public IndexState(IReadOnlyDictionary files) + { + Files = files; + } + + public string ToJson() + { + var payload = new IndexStatePayload { Files = new Dictionary(Files) }; + return System.Text.Json.JsonSerializer.Serialize(payload, new() { WriteIndented = true, PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase }); + } + + public static IndexState FromJson(string json) + { + var payload = System.Text.Json.JsonSerializer.Deserialize(json) ?? new IndexStatePayload(); + return new IndexState(payload.Files); + } + + private sealed class IndexStatePayload + { + public Dictionary Files { get; set; } = new(StringComparer.OrdinalIgnoreCase); + } +} + +internal sealed class GraphBuilder +{ + private readonly string _rootPath; + private readonly bool _includeDataflow; + private readonly List _nodes = new(); + private readonly List _edges = new(); + private long _nextNodeId = 1; + private long _nextEdgeId = 1; + private readonly Dictionary _documentNodes = new(StringComparer.OrdinalIgnoreCase); + private readonly long _solutionNodeId; + + public GraphBuilder(string rootPath, bool includeDataflow) + { + _rootPath = rootPath; + _includeDataflow = includeDataflow; + _solutionNodeId = AddNode(NodeKind.Solution, Path.GetFileName(rootPath.TrimEnd(Path.DirectorySeparatorChar)), rootPath, null); + } + + public void AddProject(Project project) + { + var nodeId = AddNode(NodeKind.Project, project.Name, project.FilePath, null); + AddEdge(EdgeKind.Contains, _solutionNodeId, nodeId, null); + if (project.FilePath is { } filePath) + { + var relative = Path.GetRelativePath(_rootPath, filePath); + _documentNodes[relative] = nodeId; + } + } + + public void AddDocument(Project project, Document document, SyntaxTree tree, SemanticModel semanticModel) + { + if (document.FilePath is null) + { + return; + } + + var relativePath = Path.GetRelativePath(_rootPath, document.FilePath); + var documentNodeId = AddNode(NodeKind.Document, document.Name, document.FilePath, null); + _documentNodes[relativePath] = documentNodeId; + AddEdge(EdgeKind.Contains, _solutionNodeId, documentNodeId, null); + + var root = tree.GetRoot(); + foreach (var usingDirective in root.DescendantNodes().OfType()) + { + var name = usingDirective.Name.ToString(); + var nodeId = AddNode(NodeKind.UsingDirective, name, document.FilePath, GetLocation(_rootPath, usingDirective)); + AddEdge(EdgeKind.UsingDirective, documentNodeId, nodeId, GetLocation(_rootPath, usingDirective)); + } + + foreach (var typeDecl in root.DescendantNodes().OfType()) + { + var symbol = semanticModel.GetDeclaredSymbol(typeDecl); + var name = symbol?.Name ?? typeDecl.Identifier.Text; + var fq = symbol?.ToDisplayString(); + var typeNodeId = AddNode(NodeKind.Type, name, document.FilePath, GetLocation(_rootPath, typeDecl), fq); + AddEdge(EdgeKind.DeclaredAt, documentNodeId, typeNodeId, GetLocation(_rootPath, typeDecl)); + + if (symbol is INamedTypeSymbol namedType) + { + if (namedType.BaseType is { } baseType && baseType.SpecialType != SpecialType.System_Object) + { + var baseNodeId = AddNode(NodeKind.Type, baseType.Name, document.FilePath, null, baseType.ToDisplayString()); + AddEdge(EdgeKind.Inherits, typeNodeId, baseNodeId, GetLocation(_rootPath, typeDecl)); + } + + foreach (var iface in namedType.Interfaces) + { + var ifaceNodeId = AddNode(NodeKind.Type, iface.Name, document.FilePath, null, iface.ToDisplayString()); + AddEdge(EdgeKind.Implements, typeNodeId, ifaceNodeId, GetLocation(_rootPath, typeDecl)); + } + } + } + + foreach (var memberDecl in root.DescendantNodes().OfType()) + { + if (memberDecl is BaseTypeDeclarationSyntax) + { + continue; + } + + var symbol = semanticModel.GetDeclaredSymbol(memberDecl); + if (symbol is null) + { + continue; + } + + var name = symbol.Name; + var fq = symbol.ToDisplayString(); + var memberNodeId = AddNode(NodeKind.Member, name, document.FilePath, GetLocation(_rootPath, memberDecl), fq); + AddEdge(EdgeKind.DeclaredAt, documentNodeId, memberNodeId, GetLocation(_rootPath, memberDecl)); + } + + foreach (var invocation in root.DescendantNodes().OfType()) + { + var symbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol; + if (symbol is null) + { + continue; + } + + var targetNodeId = AddNode(NodeKind.Member, symbol.Name, document.FilePath, null, symbol.ToDisplayString()); + AddEdge(EdgeKind.MethodInvocation, documentNodeId, targetNodeId, GetLocation(_rootPath, invocation)); + } + + foreach (var attribute in root.DescendantNodes().OfType()) + { + var symbol = semanticModel.GetSymbolInfo(attribute).Symbol?.ContainingType; + if (symbol is null) + { + continue; + } + + var attrNodeId = AddNode(NodeKind.Type, symbol.Name, document.FilePath, null, symbol.ToDisplayString()); + AddEdge(EdgeKind.AttributeUsage, documentNodeId, attrNodeId, GetLocation(_rootPath, attribute)); + } + + if (_includeDataflow) + { + foreach (var method in root.DescendantNodes().OfType()) + { + var bodyNode = (SyntaxNode?)method.Body ?? method.ExpressionBody?.Expression; + if (bodyNode is null) + { + continue; + } + + var dataFlow = semanticModel.AnalyzeDataFlow(bodyNode); + if (dataFlow is null) + { + continue; + } + + foreach (var symbol in dataFlow.VariablesDeclared) + { + var variableNodeId = AddNode(NodeKind.LocalVariable, symbol.Name, document.FilePath, null, symbol.ToDisplayString()); + AddEdge(EdgeKind.DefinedAt, documentNodeId, variableNodeId, GetLocation(_rootPath, method)); + } + + foreach (var symbol in dataFlow.ReadInside) + { + var variableNodeId = AddNode(NodeKind.LocalVariable, symbol.Name, document.FilePath, null, symbol.ToDisplayString()); + AddEdge(EdgeKind.UsedAt, documentNodeId, variableNodeId, GetLocation(_rootPath, method)); + } + } + } + } + + public IndexResult Build() => new(_nodes, _edges); + + private long AddNode(NodeKind kind, string name, string? documentPath, LocationSpan? location, string? fullyQualifiedName = null) + { + var node = new GraphNode(_nextNodeId++, kind, name, fullyQualifiedName, documentPath is null ? null : Path.GetRelativePath(_rootPath, documentPath), location); + _nodes.Add(node); + return node.Id; + } + + private void AddEdge(EdgeKind kind, long sourceId, long targetId, LocationSpan? location) + { + _edges.Add(new GraphEdge(_nextEdgeId++, kind, sourceId, targetId, location)); + } + + private static LocationSpan? GetLocation(string rootPath, SyntaxNode node) + { + var location = node.GetLocation(); + var span = location.GetLineSpan(); + var start = span.StartLinePosition; + var end = span.EndLinePosition; + var relativePath = string.IsNullOrEmpty(span.Path) ? span.Path : Path.GetRelativePath(rootPath, span.Path); + return new LocationSpan( + FilePathRelative: relativePath, + StartLine: start.Line + 1, + StartColumn: start.Character + 1, + EndLine: end.Line + 1, + EndColumn: end.Character + 1); + } +} diff --git a/src/RagSharp.CodeGraph.Core/GraphModels.cs b/src/RagSharp.CodeGraph.Core/GraphModels.cs new file mode 100644 index 0000000..09f8efb --- /dev/null +++ b/src/RagSharp.CodeGraph.Core/GraphModels.cs @@ -0,0 +1,75 @@ +using System.Text.Json.Serialization; + +namespace RagSharp.CodeGraph.Core; + +public enum NodeKind +{ + Solution, + Project, + Document, + Namespace, + Type, + Member, + UsingDirective, + LocalVariable +} + +public enum EdgeKind +{ + Contains, + ProjectReference, + UsingDirective, + AliasTarget, + GlobalUsingAppliesToProject, + Inherits, + Implements, + TypeReference, + MemberReference, + MethodInvocation, + AttributeUsage, + DeclaredAt, + ReferencedAt, + DefinedAt, + UsedAt, + UseBeforeAssign +} + +public sealed record LocationSpan( + string FilePathRelative, + int StartLine, + int StartColumn, + int EndLine, + int EndColumn); + +public sealed record GraphNode( + long Id, + NodeKind Kind, + string Name, + string? FullyQualifiedName, + string? DocumentPath, + LocationSpan? Location); + +public sealed record GraphEdge( + long Id, + EdgeKind Kind, + long SourceId, + long TargetId, + LocationSpan? Location); + +public sealed record QueryResult( + string SchemaVersion, + IReadOnlyList Nodes, + IReadOnlyList Edges); + +public sealed record QueryRequest( + string QueryType, + string? Symbol, + string? Kind, + string? Document, + int Limit, + int ContextLines); + +public static class SchemaConstants +{ + public const string CurrentVersion = "1"; +} diff --git a/src/RagSharp.CodeGraph.Core/IGraphStore.cs b/src/RagSharp.CodeGraph.Core/IGraphStore.cs new file mode 100644 index 0000000..fb82194 --- /dev/null +++ b/src/RagSharp.CodeGraph.Core/IGraphStore.cs @@ -0,0 +1,10 @@ +namespace RagSharp.CodeGraph.Core; + +public interface IGraphStore : IAsyncDisposable +{ + Task InitializeAsync(CancellationToken cancellationToken); + Task SaveIndexAsync(IndexResult result, CancellationToken cancellationToken); + Task RemoveDocumentsAsync(IEnumerable documentPaths, CancellationToken cancellationToken); + Task QueryAsync(QueryRequest request, CancellationToken cancellationToken); + Task GetSchemaVersionAsync(CancellationToken cancellationToken); +} diff --git a/src/RagSharp.CodeGraph.Core/RagSharp.CodeGraph.Core.csproj b/src/RagSharp.CodeGraph.Core/RagSharp.CodeGraph.Core.csproj new file mode 100644 index 0000000..da9448f --- /dev/null +++ b/src/RagSharp.CodeGraph.Core/RagSharp.CodeGraph.Core.csproj @@ -0,0 +1,12 @@ + + + net8.0;net10.0 + enable + enable + + + + + + + diff --git a/src/RagSharp.CodeGraph.Store.LiteGraph/LiteGraphStore.cs b/src/RagSharp.CodeGraph.Store.LiteGraph/LiteGraphStore.cs new file mode 100644 index 0000000..ff3c9dc --- /dev/null +++ b/src/RagSharp.CodeGraph.Store.LiteGraph/LiteGraphStore.cs @@ -0,0 +1,240 @@ +using Microsoft.Data.Sqlite; +using RagSharp.CodeGraph.Core; + +namespace RagSharp.CodeGraph.Store.LiteGraph; + +public sealed class LiteGraphStore : IGraphStore +{ + private readonly string _databasePath; + private SqliteConnection? _connection; + + public LiteGraphStore(string databasePath) + { + _databasePath = databasePath; + } + + public async Task InitializeAsync(CancellationToken cancellationToken) + { + Directory.CreateDirectory(Path.GetDirectoryName(_databasePath) ?? "."); + _connection = new SqliteConnection($"Data Source={_databasePath}"); + await _connection.OpenAsync(cancellationToken).ConfigureAwait(false); + + var command = _connection.CreateCommand(); + command.CommandText = @" +CREATE TABLE IF NOT EXISTS schema_version (version TEXT NOT NULL); +CREATE TABLE IF NOT EXISTS nodes ( + id INTEGER PRIMARY KEY, + kind TEXT NOT NULL, + name TEXT NOT NULL, + fullyQualifiedName TEXT, + documentPath TEXT, + filePathRelative TEXT, + startLine INTEGER, + startColumn INTEGER, + endLine INTEGER, + endColumn INTEGER +); +CREATE TABLE IF NOT EXISTS edges ( + id INTEGER PRIMARY KEY, + kind TEXT NOT NULL, + sourceId INTEGER NOT NULL, + targetId INTEGER NOT NULL, + filePathRelative TEXT, + startLine INTEGER, + startColumn INTEGER, + endLine INTEGER, + endColumn INTEGER +); +CREATE INDEX IF NOT EXISTS idx_nodes_fqn ON nodes(fullyQualifiedName); +CREATE INDEX IF NOT EXISTS idx_nodes_doc ON nodes(documentPath); +CREATE INDEX IF NOT EXISTS idx_edges_kind ON edges(kind); +"; + await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + + var version = await GetSchemaVersionAsync(cancellationToken).ConfigureAwait(false); + if (string.IsNullOrEmpty(version)) + { + var insert = _connection.CreateCommand(); + insert.CommandText = "INSERT INTO schema_version (version) VALUES ($version);"; + insert.Parameters.AddWithValue("$version", SchemaConstants.CurrentVersion); + await insert.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + } + } + + public async Task SaveIndexAsync(IndexResult result, CancellationToken cancellationToken) + { + if (_connection is null) + { + throw new InvalidOperationException("Store not initialized."); + } + + using var transaction = _connection.BeginTransaction(); + var clearNodes = _connection.CreateCommand(); + clearNodes.CommandText = "DELETE FROM nodes;"; + await clearNodes.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + var clearEdges = _connection.CreateCommand(); + clearEdges.CommandText = "DELETE FROM edges;"; + await clearEdges.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + + foreach (var node in result.Nodes) + { + var command = _connection.CreateCommand(); + command.CommandText = @" +INSERT INTO nodes (id, kind, name, fullyQualifiedName, documentPath, filePathRelative, startLine, startColumn, endLine, endColumn) +VALUES ($id, $kind, $name, $fqn, $doc, $file, $sl, $sc, $el, $ec);"; + command.Parameters.AddWithValue("$id", node.Id); + command.Parameters.AddWithValue("$kind", node.Kind.ToString()); + command.Parameters.AddWithValue("$name", node.Name); + command.Parameters.AddWithValue("$fqn", (object?)node.FullyQualifiedName ?? DBNull.Value); + command.Parameters.AddWithValue("$doc", (object?)node.DocumentPath ?? DBNull.Value); + command.Parameters.AddWithValue("$file", (object?)node.Location?.FilePathRelative ?? DBNull.Value); + command.Parameters.AddWithValue("$sl", (object?)node.Location?.StartLine ?? DBNull.Value); + command.Parameters.AddWithValue("$sc", (object?)node.Location?.StartColumn ?? DBNull.Value); + command.Parameters.AddWithValue("$el", (object?)node.Location?.EndLine ?? DBNull.Value); + command.Parameters.AddWithValue("$ec", (object?)node.Location?.EndColumn ?? DBNull.Value); + await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + } + + foreach (var edge in result.Edges) + { + var command = _connection.CreateCommand(); + command.CommandText = @" +INSERT INTO edges (id, kind, sourceId, targetId, filePathRelative, startLine, startColumn, endLine, endColumn) +VALUES ($id, $kind, $source, $target, $file, $sl, $sc, $el, $ec);"; + command.Parameters.AddWithValue("$id", edge.Id); + command.Parameters.AddWithValue("$kind", edge.Kind.ToString()); + command.Parameters.AddWithValue("$source", edge.SourceId); + command.Parameters.AddWithValue("$target", edge.TargetId); + command.Parameters.AddWithValue("$file", (object?)edge.Location?.FilePathRelative ?? DBNull.Value); + command.Parameters.AddWithValue("$sl", (object?)edge.Location?.StartLine ?? DBNull.Value); + command.Parameters.AddWithValue("$sc", (object?)edge.Location?.StartColumn ?? DBNull.Value); + command.Parameters.AddWithValue("$el", (object?)edge.Location?.EndLine ?? DBNull.Value); + command.Parameters.AddWithValue("$ec", (object?)edge.Location?.EndColumn ?? DBNull.Value); + await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + } + + transaction.Commit(); + } + + public async Task RemoveDocumentsAsync(IEnumerable documentPaths, CancellationToken cancellationToken) + { + if (_connection is null) + { + throw new InvalidOperationException("Store not initialized."); + } + + foreach (var doc in documentPaths) + { + var command = _connection.CreateCommand(); + command.CommandText = "DELETE FROM nodes WHERE documentPath = $doc;"; + command.Parameters.AddWithValue("$doc", doc); + await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + } + } + + public async Task QueryAsync(QueryRequest request, CancellationToken cancellationToken) + { + if (_connection is null) + { + throw new InvalidOperationException("Store not initialized."); + } + + var nodes = new List(); + var edges = new List(); + + var nodeCommand = _connection.CreateCommand(); + nodeCommand.CommandText = @" +SELECT id, kind, name, fullyQualifiedName, documentPath, filePathRelative, startLine, startColumn, endLine, endColumn +FROM nodes +WHERE ($kind IS NULL OR kind = $kind) + AND ($symbol IS NULL OR name LIKE $symbol OR fullyQualifiedName LIKE $symbol) +LIMIT $limit;"; + nodeCommand.Parameters.AddWithValue("$kind", (object?)request.Kind ?? DBNull.Value); + nodeCommand.Parameters.AddWithValue("$symbol", (object?)request.Symbol is null ? DBNull.Value : $"%{request.Symbol}%"); + nodeCommand.Parameters.AddWithValue("$limit", request.Limit); + + using (var reader = await nodeCommand.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false)) + { + while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + nodes.Add(ReadNode(reader)); + } + } + + var edgeCommand = _connection.CreateCommand(); + edgeCommand.CommandText = @" +SELECT id, kind, sourceId, targetId, filePathRelative, startLine, startColumn, endLine, endColumn +FROM edges +WHERE ($symbol IS NULL OR filePathRelative LIKE $symbol) +LIMIT $limit;"; + edgeCommand.Parameters.AddWithValue("$symbol", (object?)request.Document is null ? DBNull.Value : $"%{request.Document}%"); + edgeCommand.Parameters.AddWithValue("$limit", request.Limit); + + using (var reader = await edgeCommand.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false)) + { + while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + edges.Add(ReadEdge(reader)); + } + } + + return new QueryResult(SchemaConstants.CurrentVersion, nodes, edges); + } + + public async Task GetSchemaVersionAsync(CancellationToken cancellationToken) + { + if (_connection is null) + { + return string.Empty; + } + + var command = _connection.CreateCommand(); + command.CommandText = "SELECT version FROM schema_version LIMIT 1;"; + var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false); + return result?.ToString() ?? string.Empty; + } + + public ValueTask DisposeAsync() + { + _connection?.Dispose(); + return ValueTask.CompletedTask; + } + + private static GraphNode ReadNode(SqliteDataReader reader) + { + var location = ReadLocation(reader, 5); + return new GraphNode( + reader.GetInt64(0), + Enum.Parse(reader.GetString(1)), + reader.GetString(2), + reader.IsDBNull(3) ? null : reader.GetString(3), + reader.IsDBNull(4) ? null : reader.GetString(4), + location); + } + + private static GraphEdge ReadEdge(SqliteDataReader reader) + { + var location = ReadLocation(reader, 4); + return new GraphEdge( + reader.GetInt64(0), + Enum.Parse(reader.GetString(1)), + reader.GetInt64(2), + reader.GetInt64(3), + location); + } + + private static LocationSpan? ReadLocation(SqliteDataReader reader, int startIndex) + { + if (reader.IsDBNull(startIndex)) + { + return null; + } + + return new LocationSpan( + reader.GetString(startIndex), + reader.GetInt32(startIndex + 1), + reader.GetInt32(startIndex + 2), + reader.GetInt32(startIndex + 3), + reader.GetInt32(startIndex + 4)); + } +} diff --git a/src/RagSharp.CodeGraph.Store.LiteGraph/RagSharp.CodeGraph.Store.LiteGraph.csproj b/src/RagSharp.CodeGraph.Store.LiteGraph/RagSharp.CodeGraph.Store.LiteGraph.csproj new file mode 100644 index 0000000..4f66dcd --- /dev/null +++ b/src/RagSharp.CodeGraph.Store.LiteGraph/RagSharp.CodeGraph.Store.LiteGraph.csproj @@ -0,0 +1,11 @@ + + + net8.0;net10.0 + enable + enable + + + + + + diff --git a/src/RagSharp.Packaging/RagSharp.Packaging.csproj b/src/RagSharp.Packaging/RagSharp.Packaging.csproj new file mode 100644 index 0000000..dad984b --- /dev/null +++ b/src/RagSharp.Packaging/RagSharp.Packaging.csproj @@ -0,0 +1,24 @@ + + + net8.0 + enable + enable + true + RagSharp + 0.1.0 + RagSharp + RagSharp code graph skills installer and CLI tools. + MIT + https://example.invalid/ragsharp + false + false + + + + + + + + + + diff --git a/src/RagSharp.SkillInstaller/Installer.cs b/src/RagSharp.SkillInstaller/Installer.cs new file mode 100644 index 0000000..7577b4f --- /dev/null +++ b/src/RagSharp.SkillInstaller/Installer.cs @@ -0,0 +1,175 @@ +using System.Reflection; +using System.Text.Json; + +namespace RagSharp.SkillInstaller; + +public static class Installer +{ + private const string ManifestName = "ragsharp.manifest.json"; + + public static async Task InstallAsync(string root, string skillDir, bool force, bool verbose, CancellationToken cancellationToken) + { + var rootPath = ResolveRoot(root); + var targetDir = Path.Combine(rootPath, skillDir); + Directory.CreateDirectory(targetDir); + + var manifestPath = Path.Combine(targetDir, ManifestName); + if (File.Exists(manifestPath)) + { + if (!force) + { + throw new InvalidOperationException("Skills already installed. Use --force to reinstall."); + } + + await RemoveInstalledFilesAsync(manifestPath, targetDir, cancellationToken).ConfigureAwait(false); + } + + var tempDir = Path.Combine(targetDir, $".tmp-{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDir); + + var installedFiles = ExtractTemplates(tempDir, verbose); + foreach (var file in installedFiles) + { + var destination = Path.Combine(targetDir, file); + Directory.CreateDirectory(Path.GetDirectoryName(destination) ?? targetDir); + File.Copy(Path.Combine(tempDir, file), destination, true); + } + + var manifest = new InstallManifest + { + Version = GetVersion(), + InstalledAtUtc = DateTimeOffset.UtcNow, + Files = installedFiles + }; + + var manifestJson = JsonSerializer.Serialize(manifest, new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + await File.WriteAllTextAsync(manifestPath, manifestJson, cancellationToken).ConfigureAwait(false); + + Directory.Delete(tempDir, true); + Console.Error.WriteLine($"Installed ragsharp skills to {targetDir}."); + } + + public static async Task UninstallAsync(string root, string skillDir, CancellationToken cancellationToken) + { + var rootPath = ResolveRoot(root); + var targetDir = Path.Combine(rootPath, skillDir); + var manifestPath = Path.Combine(targetDir, ManifestName); + if (!File.Exists(manifestPath)) + { + Console.Error.WriteLine("No ragsharp manifest found."); + return; + } + + await RemoveInstalledFilesAsync(manifestPath, targetDir, cancellationToken).ConfigureAwait(false); + + Console.Error.WriteLine("Removed ragsharp skills."); + } + + public static async Task GetStatusAsync(string root, string skillDir, CancellationToken cancellationToken) + { + var rootPath = ResolveRoot(root); + var targetDir = Path.Combine(rootPath, skillDir); + var manifestPath = Path.Combine(targetDir, ManifestName); + if (!File.Exists(manifestPath)) + { + return new InstallStatus(false, null, null, targetDir, Array.Empty()); + } + + var manifest = JsonSerializer.Deserialize(await File.ReadAllTextAsync(manifestPath, cancellationToken).ConfigureAwait(false)); + return new InstallStatus(true, manifest?.Version, manifest?.InstalledAtUtc, targetDir, manifest?.Files ?? Array.Empty()); + } + + public static void RunDoctor(string root) + { + var rootPath = ResolveRoot(root); + Console.Error.WriteLine($"Root: {rootPath}"); + Console.Error.WriteLine("dotnet: ensure .NET SDK is installed and on PATH."); + Console.Error.WriteLine("MSBuildWorkspace: available with Microsoft.CodeAnalysis.Workspaces.MSBuild."); + } + + private static string ResolveRoot(string root) + { + var current = Path.GetFullPath(root); + var dir = new DirectoryInfo(current); + while (dir is not null) + { + if (Directory.Exists(Path.Combine(dir.FullName, ".git"))) + { + return dir.FullName; + } + dir = dir.Parent; + } + + return current; + } + + private static List ExtractTemplates(string outputDir, bool verbose) + { + var assembly = Assembly.GetExecutingAssembly(); + var resources = assembly.GetManifestResourceNames() + .Where(name => name.StartsWith("skill-templates/", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + var files = new List(); + foreach (var resource in resources) + { + var relative = resource.Replace("skill-templates/", string.Empty, StringComparison.OrdinalIgnoreCase); + var outputPath = Path.Combine(outputDir, relative); + Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? outputDir); + using var stream = assembly.GetManifestResourceStream(resource); + if (stream is null) + { + continue; + } + + using var fileStream = File.Create(outputPath); + stream.CopyTo(fileStream); + files.Add(relative); + if (verbose) + { + Console.Error.WriteLine($"Extracted {relative}"); + } + } + + return files; + } + + private static async Task RemoveInstalledFilesAsync(string manifestPath, string targetDir, CancellationToken cancellationToken) + { + var manifest = JsonSerializer.Deserialize(await File.ReadAllTextAsync(manifestPath, cancellationToken).ConfigureAwait(false)) + ?? new InstallManifest(); + + foreach (var file in manifest.Files) + { + var path = Path.Combine(targetDir, file); + if (File.Exists(path)) + { + File.Delete(path); + } + } + + if (File.Exists(manifestPath)) + { + File.Delete(manifestPath); + } + } + + private static string GetVersion() + { + return Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0"; + } +} + +public sealed class InstallManifest +{ + public string? Version { get; set; } + public DateTimeOffset InstalledAtUtc { get; set; } + public List Files { get; set; } = new(); +} + +public sealed record InstallStatus( + bool Installed, + string? Version, + DateTimeOffset? InstalledAtUtc, + string SkillDir, + IReadOnlyList Files); diff --git a/src/RagSharp.SkillInstaller/Program.cs b/src/RagSharp.SkillInstaller/Program.cs new file mode 100644 index 0000000..61966cd --- /dev/null +++ b/src/RagSharp.SkillInstaller/Program.cs @@ -0,0 +1,68 @@ +using System.CommandLine; +using System.Text.Json; + +namespace RagSharp.SkillInstaller; + +public static class Program +{ + public static async Task Main(string[] args) + { + var rootOption = new Option("--root", () => Directory.GetCurrentDirectory(), "Repository root"); + var skillDirOption = new Option("--skill-dir", () => ".codex/skills", "Skill directory"); + var forceOption = new Option("--force", "Force install"); + var verboseOption = new Option("--verbose", "Verbose output"); + + var installCommand = new Command("install", "Install ragsharp skills"); + installCommand.AddOption(rootOption); + installCommand.AddOption(skillDirOption); + installCommand.AddOption(forceOption); + installCommand.AddOption(verboseOption); + installCommand.SetHandler(async (root, skillDir, force, verbose) => + { + await Installer.InstallAsync(root, skillDir, force, verbose, CancellationToken.None).ConfigureAwait(false); + }, rootOption, skillDirOption, forceOption, verboseOption); + + var uninstallCommand = new Command("uninstall", "Uninstall ragsharp skills"); + uninstallCommand.AddOption(rootOption); + uninstallCommand.AddOption(skillDirOption); + uninstallCommand.SetHandler(async (root, skillDir) => + { + await Installer.UninstallAsync(root, skillDir, CancellationToken.None).ConfigureAwait(false); + }, rootOption, skillDirOption); + + var statusCommand = new Command("status", "Show install status"); + var formatOption = new Option("--format", () => "json", "Output format"); + statusCommand.AddOption(rootOption); + statusCommand.AddOption(skillDirOption); + statusCommand.AddOption(formatOption); + statusCommand.SetHandler(async (root, skillDir, format) => + { + var status = await Installer.GetStatusAsync(root, skillDir, CancellationToken.None).ConfigureAwait(false); + if (format.Equals("json", StringComparison.OrdinalIgnoreCase)) + { + Console.Out.WriteLine(JsonSerializer.Serialize(status, new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase })); + } + else + { + Console.Out.WriteLine(status.Installed ? "installed" : "not installed"); + } + }, rootOption, skillDirOption, formatOption); + + var doctorCommand = new Command("doctor", "Check environment"); + doctorCommand.AddOption(rootOption); + doctorCommand.SetHandler((root) => + { + Installer.RunDoctor(root); + }, rootOption); + + var rootCommand = new RootCommand("ragsharp installer") + { + installCommand, + uninstallCommand, + statusCommand, + doctorCommand + }; + + return await rootCommand.InvokeAsync(args).ConfigureAwait(false); + } +} diff --git a/src/RagSharp.SkillInstaller/RagSharp.SkillInstaller.csproj b/src/RagSharp.SkillInstaller/RagSharp.SkillInstaller.csproj new file mode 100644 index 0000000..851414a --- /dev/null +++ b/src/RagSharp.SkillInstaller/RagSharp.SkillInstaller.csproj @@ -0,0 +1,18 @@ + + + Exe + net8.0;net10.0 + enable + enable + ragsharp + RagSharp.SkillInstaller + + + + + + + skill-templates/%(RecursiveDir)%(Filename)%(Extension) + + + diff --git a/tests/RagSharp.CodeGraph.Tests/IndexTests.cs b/tests/RagSharp.CodeGraph.Tests/IndexTests.cs new file mode 100644 index 0000000..76040a7 --- /dev/null +++ b/tests/RagSharp.CodeGraph.Tests/IndexTests.cs @@ -0,0 +1,29 @@ +using RagSharp.CodeGraph.Core; +using RagSharp.CodeGraph.Store.LiteGraph; +using Xunit; + +namespace RagSharp.CodeGraph.Tests; + +public class IndexTests +{ + [Fact] + public async Task IndexesSampleSolution() + { + var root = Path.GetFullPath(Path.Combine("..", "..", "..", "samples", "SampleApp")); + var dbPath = Path.Combine(root, ".codegraph", "index.db"); + if (File.Exists(dbPath)) + { + File.Delete(dbPath); + } + + var indexer = new CodeGraphIndexer(includeDataflow: false); + var result = await indexer.IndexAsync(root, CancellationToken.None); + + var store = new LiteGraphStore(dbPath); + await store.InitializeAsync(CancellationToken.None); + await store.SaveIndexAsync(result, CancellationToken.None); + + var query = await store.QueryAsync(new QueryRequest("symbols", "Greeter", null, null, 10, 0), CancellationToken.None); + Assert.Contains(query.Nodes, node => node.Name == "Greeter"); + } +} diff --git a/tests/RagSharp.CodeGraph.Tests/RagSharp.CodeGraph.Tests.csproj b/tests/RagSharp.CodeGraph.Tests/RagSharp.CodeGraph.Tests.csproj new file mode 100644 index 0000000..8fb19b2 --- /dev/null +++ b/tests/RagSharp.CodeGraph.Tests/RagSharp.CodeGraph.Tests.csproj @@ -0,0 +1,17 @@ + + + net8.0 + false + enable + enable + + + + + + + + + + + diff --git a/tests/RagSharp.SkillInstaller.Tests/InstallerTests.cs b/tests/RagSharp.SkillInstaller.Tests/InstallerTests.cs new file mode 100644 index 0000000..0abc3a8 --- /dev/null +++ b/tests/RagSharp.SkillInstaller.Tests/InstallerTests.cs @@ -0,0 +1,24 @@ +using RagSharp.SkillInstaller; +using Xunit; + +namespace RagSharp.SkillInstaller.Tests; + +public class InstallerTests +{ + [Fact] + public async Task InstallsAndUninstallsSkills() + { + var tempRoot = Path.Combine(Path.GetTempPath(), $"ragsharp-test-{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempRoot); + Directory.CreateDirectory(Path.Combine(tempRoot, ".git")); + + await Installer.InstallAsync(tempRoot, ".codex/skills", force: true, verbose: false, CancellationToken.None); + var status = await Installer.GetStatusAsync(tempRoot, ".codex/skills", CancellationToken.None); + Assert.True(status.Installed); + Assert.NotEmpty(status.Files); + + await Installer.UninstallAsync(tempRoot, ".codex/skills", CancellationToken.None); + status = await Installer.GetStatusAsync(tempRoot, ".codex/skills", CancellationToken.None); + Assert.False(status.Installed); + } +} diff --git a/tests/RagSharp.SkillInstaller.Tests/RagSharp.SkillInstaller.Tests.csproj b/tests/RagSharp.SkillInstaller.Tests/RagSharp.SkillInstaller.Tests.csproj new file mode 100644 index 0000000..4929f76 --- /dev/null +++ b/tests/RagSharp.SkillInstaller.Tests/RagSharp.SkillInstaller.Tests.csproj @@ -0,0 +1,16 @@ + + + net8.0 + false + enable + enable + + + + + + + + + + diff --git a/tests/samples/SampleApp/Program.cs b/tests/samples/SampleApp/Program.cs new file mode 100644 index 0000000..a879c41 --- /dev/null +++ b/tests/samples/SampleApp/Program.cs @@ -0,0 +1,17 @@ +using System; + +namespace SampleApp; + +public class Greeter +{ + public string Hello(string name) => $"Hello, {name}"; +} + +public static class Program +{ + public static void Main() + { + var greeter = new Greeter(); + Console.WriteLine(greeter.Hello("world")); + } +} diff --git a/tests/samples/SampleApp/SampleApp.csproj b/tests/samples/SampleApp/SampleApp.csproj new file mode 100644 index 0000000..e8cd599 --- /dev/null +++ b/tests/samples/SampleApp/SampleApp.csproj @@ -0,0 +1,7 @@ + + + net8.0 + enable + enable + + diff --git a/tests/samples/SampleApp/SampleApp.sln b/tests/samples/SampleApp/SampleApp.sln new file mode 100644 index 0000000..a53c17d --- /dev/null +++ b/tests/samples/SampleApp/SampleApp.sln @@ -0,0 +1,19 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleApp", "SampleApp.csproj", "{E9F1CC19-2E65-4B82-8D18-3F6C85A1B7D7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E9F1CC19-2E65-4B82-8D18-3F6C85A1B7D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9F1CC19-2E65-4B82-8D18-3F6C85A1B7D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9F1CC19-2E65-4B82-8D18-3F6C85A1B7D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9F1CC19-2E65-4B82-8D18-3F6C85A1B7D7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal