From 7faeb807272686f84ea7f066e205af23590578a6 Mon Sep 17 00:00:00 2001 From: florianbgt Date: Sun, 12 Apr 2026 15:15:16 +0200 Subject: [PATCH] Update deno.json and README.md for improved dependency management and documentation - Added new dependencies to deno.json, including various Radix UI components and Tailwind CSS. - Updated README.md to enhance project description, features, installation instructions, and CLI commands. - Refactored the structure of the README for better clarity and added new sections for quick start and supported languages. - Adjusted the CLI commands to reflect the new structure and functionality. --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .github/workflows/lint_test_compile.yml | 4 +- README.md | 329 ++- deno.json | 42 +- examples/c/network/.napirc | 25 +- examples/csharp/EndpointExample/.napirc | 14 +- .../src/main/resources/static/echo.html | 6 +- .../manifests/1775598176791-d5c2217.json | 2128 +++++++++++++++++ examples/python/flask/.napirc | 14 +- src/apiService/index.ts | 39 - src/cli/handlers/extract/index.ts | 121 +- .../generate.ts => generate/index.ts} | 146 +- src/cli/handlers/init/index.ts | 450 +--- src/cli/handlers/login/index.ts | 141 -- src/cli/handlers/manifest/index.ts | 21 - src/cli/handlers/view/index.ts | 284 +++ src/cli/index.ts | 8 +- src/cli/middlewares/globalConfig.ts | 9 +- src/cli/middlewares/isAuthenticated.ts | 47 - src/cli/middlewares/napiConfig.ts | 21 +- src/helpers/treeSitter/parsers.ts | 6 +- src/languagePlugins/c/testFiles/index.ts | 1 - .../java/dependencyFormatting/index.ts | 2 +- .../java/dependencyFormatting/types.ts | 2 +- .../java/invocationResolver/queries.ts | 2 +- .../java/packageResolver/queries.ts | 2 +- .../python/symbolExtractor/index.test.ts | 1 - src/manifest/auditManifest/index.ts | 257 ++ src/manifest/auditManifest/types.ts | 65 + viewer/.gitignore | 24 + viewer/README.md | 73 + viewer/index.html | 12 + viewer/src/App.tsx | 20 + viewer/src/api.ts | 39 + .../DependencyVisualizer.tsx | 135 ++ .../components/BreadcrumbNav.tsx | 57 + .../components/DisplayNameWithTooltip.tsx | 31 + .../components/FileExplorerSidebar.tsx | 426 ++++ .../components/SymbolExtractionDialog.tsx | 104 + .../contextMenu/FileContextMenu.tsx | 84 + .../contextMenu/SymbolContextMenu.tsx | 88 + .../ControlExtensions/FiltersExtension.tsx | 95 + .../ControlExtensions/GraphDepthExtension.tsx | 76 + .../ControlExtensions/MetricsExtension.tsx | 79 + .../components/controls/Controls.tsx | 69 + .../components/detailsPanes/AlertBadge.tsx | 16 + .../detailsPanes/FileDetailsPane.tsx | 167 ++ .../components/detailsPanes/Metrics.tsx | 59 + .../detailsPanes/SymbolDetailsPane.tsx | 165 ++ .../visualizers/FileVisualizer.tsx | 212 ++ .../visualizers/ProjectVisualizer.tsx | 162 ++ .../visualizers/SymbolVisualizer.tsx | 200 ++ viewer/src/components/shadcn/Alert.tsx | 66 + viewer/src/components/shadcn/Breadcrumb.tsx | 109 + viewer/src/components/shadcn/Button.tsx | 61 + viewer/src/components/shadcn/Card.tsx | 92 + viewer/src/components/shadcn/Dialog.tsx | 141 ++ viewer/src/components/shadcn/Dropdownmenu.tsx | 255 ++ viewer/src/components/shadcn/Input.tsx | 21 + viewer/src/components/shadcn/Label.tsx | 22 + viewer/src/components/shadcn/Scrollarea.tsx | 56 + viewer/src/components/shadcn/Separator.tsx | 26 + viewer/src/components/shadcn/Sheet.tsx | 137 ++ viewer/src/components/shadcn/Sidebar.tsx | 721 ++++++ viewer/src/components/shadcn/Skeleton.tsx | 13 + viewer/src/components/shadcn/Slider.tsx | 64 + viewer/src/components/shadcn/Tooltip.tsx | 59 + .../components/shadcn/hooks/use-mobile.tsx | 23 + viewer/src/contexts/ThemeProvider.tsx | 64 + viewer/src/cytoscape/elements/file.ts | 293 +++ viewer/src/cytoscape/elements/project.ts | 95 + viewer/src/cytoscape/elements/symbol.ts | 338 +++ viewer/src/cytoscape/elements/types.ts | 37 + .../fileDependencyVisualizer/index.ts | 353 +++ viewer/src/cytoscape/label/index.ts | 165 ++ viewer/src/cytoscape/layout/index.ts | 22 + viewer/src/cytoscape/metrics/index.ts | 38 + .../projectDependencyVisualizer/index.ts | 269 +++ viewer/src/cytoscape/styles/index.ts | 257 ++ .../symbolDependencyVisualizer/index.ts | 300 +++ viewer/src/index.css | 147 ++ viewer/src/lib/utils.ts | 6 + viewer/src/main.tsx | 10 + viewer/src/pages/ManifestList.tsx | 94 + viewer/src/pages/ManifestView.tsx | 57 + viewer/src/types/auditManifest.ts | 23 + viewer/src/types/dependencyManifest.ts | 106 + viewer/vite.config.ts | 15 + 89 files changed, 10247 insertions(+), 892 deletions(-) create mode 100644 examples/python/flask/.napi/manifests/1775598176791-d5c2217.json delete mode 100644 src/apiService/index.ts rename src/cli/handlers/{manifest/generate.ts => generate/index.ts} (65%) delete mode 100644 src/cli/handlers/login/index.ts delete mode 100644 src/cli/handlers/manifest/index.ts create mode 100644 src/cli/handlers/view/index.ts delete mode 100644 src/cli/middlewares/isAuthenticated.ts create mode 100644 src/manifest/auditManifest/index.ts create mode 100644 src/manifest/auditManifest/types.ts create mode 100644 viewer/.gitignore create mode 100644 viewer/README.md create mode 100644 viewer/index.html create mode 100644 viewer/src/App.tsx create mode 100644 viewer/src/api.ts create mode 100644 viewer/src/components/DependencyVisualizer/DependencyVisualizer.tsx create mode 100644 viewer/src/components/DependencyVisualizer/components/BreadcrumbNav.tsx create mode 100644 viewer/src/components/DependencyVisualizer/components/DisplayNameWithTooltip.tsx create mode 100644 viewer/src/components/DependencyVisualizer/components/FileExplorerSidebar.tsx create mode 100644 viewer/src/components/DependencyVisualizer/components/SymbolExtractionDialog.tsx create mode 100644 viewer/src/components/DependencyVisualizer/components/contextMenu/FileContextMenu.tsx create mode 100644 viewer/src/components/DependencyVisualizer/components/contextMenu/SymbolContextMenu.tsx create mode 100644 viewer/src/components/DependencyVisualizer/components/controls/ControlExtensions/FiltersExtension.tsx create mode 100644 viewer/src/components/DependencyVisualizer/components/controls/ControlExtensions/GraphDepthExtension.tsx create mode 100644 viewer/src/components/DependencyVisualizer/components/controls/ControlExtensions/MetricsExtension.tsx create mode 100644 viewer/src/components/DependencyVisualizer/components/controls/Controls.tsx create mode 100644 viewer/src/components/DependencyVisualizer/components/detailsPanes/AlertBadge.tsx create mode 100644 viewer/src/components/DependencyVisualizer/components/detailsPanes/FileDetailsPane.tsx create mode 100644 viewer/src/components/DependencyVisualizer/components/detailsPanes/Metrics.tsx create mode 100644 viewer/src/components/DependencyVisualizer/components/detailsPanes/SymbolDetailsPane.tsx create mode 100644 viewer/src/components/DependencyVisualizer/visualizers/FileVisualizer.tsx create mode 100644 viewer/src/components/DependencyVisualizer/visualizers/ProjectVisualizer.tsx create mode 100644 viewer/src/components/DependencyVisualizer/visualizers/SymbolVisualizer.tsx create mode 100644 viewer/src/components/shadcn/Alert.tsx create mode 100644 viewer/src/components/shadcn/Breadcrumb.tsx create mode 100644 viewer/src/components/shadcn/Button.tsx create mode 100644 viewer/src/components/shadcn/Card.tsx create mode 100644 viewer/src/components/shadcn/Dialog.tsx create mode 100644 viewer/src/components/shadcn/Dropdownmenu.tsx create mode 100644 viewer/src/components/shadcn/Input.tsx create mode 100644 viewer/src/components/shadcn/Label.tsx create mode 100644 viewer/src/components/shadcn/Scrollarea.tsx create mode 100644 viewer/src/components/shadcn/Separator.tsx create mode 100644 viewer/src/components/shadcn/Sheet.tsx create mode 100644 viewer/src/components/shadcn/Sidebar.tsx create mode 100644 viewer/src/components/shadcn/Skeleton.tsx create mode 100644 viewer/src/components/shadcn/Slider.tsx create mode 100644 viewer/src/components/shadcn/Tooltip.tsx create mode 100644 viewer/src/components/shadcn/hooks/use-mobile.tsx create mode 100644 viewer/src/contexts/ThemeProvider.tsx create mode 100644 viewer/src/cytoscape/elements/file.ts create mode 100644 viewer/src/cytoscape/elements/project.ts create mode 100644 viewer/src/cytoscape/elements/symbol.ts create mode 100644 viewer/src/cytoscape/elements/types.ts create mode 100644 viewer/src/cytoscape/fileDependencyVisualizer/index.ts create mode 100644 viewer/src/cytoscape/label/index.ts create mode 100644 viewer/src/cytoscape/layout/index.ts create mode 100644 viewer/src/cytoscape/metrics/index.ts create mode 100644 viewer/src/cytoscape/projectDependencyVisualizer/index.ts create mode 100644 viewer/src/cytoscape/styles/index.ts create mode 100644 viewer/src/cytoscape/symbolDependencyVisualizer/index.ts create mode 100644 viewer/src/index.css create mode 100644 viewer/src/lib/utils.ts create mode 100644 viewer/src/main.tsx create mode 100644 viewer/src/pages/ManifestList.tsx create mode 100644 viewer/src/pages/ManifestView.tsx create mode 100644 viewer/src/types/auditManifest.ts create mode 100644 viewer/src/types/dependencyManifest.ts create mode 100644 viewer/vite.config.ts diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d345c8d1..adbae7d2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -3,7 +3,7 @@ name: Bug report about: Create a report for a bug, regression, or unexpected behavior title: "[BUG] " labels: bug -assignees: '' +assignees: "" --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index b66f0348..b1005a7d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -3,7 +3,7 @@ name: Feature request about: Suggest an idea for this project title: "[FEATURE] " labels: enhancement -assignees: '' +assignees: "" --- diff --git a/.github/workflows/lint_test_compile.yml b/.github/workflows/lint_test_compile.yml index 3db7e5e8..879214e0 100644 --- a/.github/workflows/lint_test_compile.yml +++ b/.github/workflows/lint_test_compile.yml @@ -32,7 +32,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Node.js + - name: Set up Deno uses: denoland/setup-deno@v2 with: deno-version: "2.4.0" @@ -50,7 +50,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Node.js + - name: Set up Deno uses: denoland/setup-deno@v2 with: deno-version: "2.4.0" diff --git a/README.md b/README.md index e7e647d4..a69f13c2 100644 --- a/README.md +++ b/README.md @@ -1,154 +1,281 @@ ![NanoAPI Banner](/media/github-banner.png) -- [Documentation](https://docs.nanoapi.io) - # napi - Better Software Architecture for the AI Age -`napi` is a versatile tool built by NanoAPI and designed to automatically -provide insights into the architectural complexity of your software, while -allowing for the novel extraction of functionality from codebases into smaller -units. With both a powerful CLI and an intuitive UI, `napi` is compatible with -all major CI/CD platforms, allowing seamless integration into your development -and deployment pipelines. - -Historically, tools like this have only been built by large consulting firms or -contractors and kept behind the paywalls of consulting fees. `napi` aims to make -these tools accessible to developers of all skill levels, without the cost. Our -vision is to help you gain deeper insights into system architecture-level -concerns before they become hundred-million-dollar problems. The added benefit? -No more black-box tools running on your code and the confidence of a 100% -determinstic tool. +`napi` is a fully offline CLI that analyzes your codebase's architecture -- +dependencies, complexity, and structure -- then lets you visualize and refactor +it, all without sending your code anywhere. + +It generates dependency manifests from your source code, stores them locally, +and serves an interactive graph visualizer directly from the CLI. ![NanoAPI UI Overview](/media/hero-app.png) ## Features -- **🚨 Audit**: Pinpoint areas of your code that need refactoring or cleanup. -- **📝 Refactor**: Extract functionality using the UI to improve architecture. -- **🏗️ Build**: Generate modular microservices ready for deployment. -- **⚙️ Integrate**: Use CLI commands compatible with all CI/CD workflows for - automation. -- **🔍 Architecture**: Get a live view of all your software and their - interactions; scoped to a specific moment in time. - - - -## Why `napi`? - -- **Application Library**: `napi` is not just a CLI tool; it is a comprehensive - application library of all projects and their interactions within your - organization. -- **Enables discovery into legacy systems**: indentify problematic code and - potential improvements early. -- **Modular Monoliths**: Simplifies the process of extracting functionality - using non-AI strangler refactoring. -- **Risk assessment**: Improve understanding, maintainability, and robustness at - both the architecture and code level. -- **Refactoring ROI**: Reduces dependency on outside sources for complex - refactoring tasks. -- **From black box to open-book**: Gain a deeper trust of what your system is - doing today - even in the face of AI-generated code. +- **🔍 Dependency Analysis**: Map every file, symbol, and dependency in your + codebase automatically. +- **🚨 Audit**: Detect files and symbols that exceed complexity, size, or + coupling thresholds. +- **📊 Interactive Visualizer**: Explore your architecture through Cytoscape.js + graphs served locally in your browser. +- **📝 Symbol Extraction**: Extract specific functions, classes, or symbols into + standalone files for refactoring. +- **🏷️ AI Labeling** (optional): Use OpenAI, Google, or Anthropic models to + auto-label dependencies. +- **⚙️ CI/CD Ready**: Integrates into any pipeline -- generate manifests on + every push and track architecture over time. +- **🔒 Fully Offline**: No accounts, no servers, no data leaves your machine. ## Supported Languages -`napi` aims to support all major programming languages. Here is the current -status: +| Language | Status | +| -------- | -------------- | +| Python | ✅ Supported | +| C# | ✅ Supported | +| C | ✅ Supported | +| Java | ✅ Supported | +| C++ | 🚧 In Progress | +| PHP | 🚧 In Progress | +| JS/TS | 🚧 In Progress | -| Language/Framework | Status | -| ------------------ | -------------- | -| Python | ✅ Supported | -| C# | ✅ Supported | -| C | ✅ Supported | -| Java | ✅ Supported | -| C++ | 🚧 In Progress | -| PHP | 🚧 In Progress | -| JavaScript | 🚧 In Progress | -| TypeScript | 🚧 In Progress | +## Installation -For the latest updates, visit our [project board](/projects). +### Unix (macOS, Linux) -## Installation +```bash +curl -fsSL https://raw.githubusercontent.com/nanoapi-io/napi/refs/heads/main/install_scripts/install.sh | bash +``` -`napi` works out of the box on both mac, linux, and windows systems. +Or download a binary directly from +[GitHub Releases](https://github.com/nanoapi-io/napi/releases/latest). -To install `napi`, you can use our installation script: +### Windows -### Unix Systems (MacOS, Linux) +Use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) to run napi. +Native Windows support is in progress. -Wyou to install napi using our convenience script: +## Quick Start ```bash -curl -fsSL https://raw.githubusercontent.com/nanoapi-io/napi/refs/heads/main/install_scripts/install.sh | bash +# 1. Initialize your project (creates .napirc) +napi init + +# 2. Generate a dependency manifest +napi generate + +# 3. Open the visualizer in your browser +napi view ``` -You can also download and install the latest release manually directly from our -GitHub repository: +That's it. Your manifest is saved locally in `.napi/manifests/` and the +visualizer opens at `http://localhost:3000`. -https://github.com/nanoapi-io/napi/releases/latest +## CLI Commands -### Windows +### `napi init` + +Interactive setup that creates a `.napirc` configuration file in your project +root. -You can run napi using Windows Subsystem for Linux (WSL) -https://learn.microsoft.com/en-us/windows/wsl/install +Prompts you for: -We are actively working on supporting windows natievely. +- **Language** -- Python, C#, C, or Java +- **Include/exclude patterns** -- which files to analyze +- **Output directory** -- where extracted symbols are written +- **AI labeling** (optional) -- provider and concurrency settings -### Troubleshooting +```bash +napi init +``` + +### `napi generate` + +Analyzes your codebase and generates a dependency manifest. The manifest +captures every file, symbol, dependency, and metric (lines, complexity, +coupling). + +Manifests are saved as JSON files in `.napi/manifests/` with the naming pattern +`{timestamp}-{commitSha}.json`. + +```bash +# Interactive (prompts for branch/commit if not in git) +napi generate -If you encounter any issues during installation, please refer to our -[Troubleshooting Guide](https://docs.nanoapi.io/default-guide/troubleshooting) +# Non-interactive (for CI) +napi generate --branch main --commit-sha abc1234 --commit-sha-date 2026-01-01T00:00:00Z +``` + +Options: -## CLI Usage +- `--branch` -- Git branch name (auto-detected if omitted) +- `--commit-sha` -- Git commit hash (auto-detected if omitted) +- `--commit-sha-date` -- Commit date in ISO 8601 format (auto-detected if + omitted) +- `--labelingApiKey` -- API key for AI labeling (overrides global config) -`napi` provides a streamlined Command-Line Interface (CLI) to interact with and -refactor your software projects quickly and efficiently. +### `napi view` -For a full list of commands, run: +Starts a local web server and opens an interactive dependency visualizer in your +browser. ```bash -napi --help +napi view +napi view --port 8080 ``` -## Overview of all commands +The viewer provides: -### `napi login` +- **Manifest list** -- browse all locally stored manifests by branch, commit, + and date +- **Project graph** -- file-level dependency map with Cytoscape.js +- **File graph** -- symbol-level view within a file (functions, classes, + variables) +- **Symbol graph** -- transitive dependency chain for a specific symbol +- **File explorer sidebar** -- navigate your codebase structure +- **Audit alerts** -- visual indicators for files/symbols exceeding thresholds -Authenticate with the NanoAPI service. This step is required to access the -NanoAPI UI and to use certain features of `napi`. +### `napi extract` -### `napi init` +Extracts specific symbols from your codebase into separate files using a local +manifest. -Initialize the project. This step is required before running any other command. +```bash +# Extract a function from a specific file +napi extract --symbol "src/auth/login.py|authenticate" -This will create a .napirc configuration file in the project root, storing paths -and settings necessary for further commands. +# Extract multiple symbols +napi extract --symbol "src/models.py|User" --symbol "src/models.py|Session" -### `napi manifest generate` +# Use a specific manifest (defaults to latest) +napi extract --symbol "src/main.py|run" --manifestId 1712500000000-a1b2c3d +``` -Generate a manifest of your codebase that captures its structure, dependencies, -and relationships and pushes it to your NanoAPI workspace in the app. +Output is written to `{outDir}/extracted-{timestamp}/`. -### `napi extract` +### `napi set apiKey` -Extract specific symbols (functions, classes, etc.) from your codebase into -separate files. Use the format `--symbol file|symbol` where file is the path -relative to your project root and symbol is the name to extract. The UI can -generate these commands for convenient copy-pasting when browsing your code. +Configure API keys for AI-powered dependency labeling. Keys are stored in the +global config (not in your project). -> **Important**: Run `napi manifest generate` whenever you make significant -> changes to your codebase to ensure your manifest stays up-to-date. The -> manifest data can be integrated into CI/CD workflows to track architectural -> changes over time. +```bash +napi set apiKey +``` + +Prompts for: + +- **Provider** -- Google, OpenAI, or Anthropic +- **API key** -- your provider API key + +## Local Manifest Storage + +All manifests are stored in `.napi/manifests/` relative to your project root. +Each manifest is a self-contained JSON file: + +```json +{ + "id": "1712500000000-a1b2c3d", + "branch": "main", + "commitSha": "a1b2c3d4e5f6...", + "commitShaDate": "2026-04-07T10:00:00Z", + "createdAt": "2026-04-07T10:01:00Z", + "manifest": {} +} +``` + +Add `.napi/` to your `.gitignore` or commit it to track architecture history in +version control -- your choice. ## CI/CD Integration -`napi` works seamlessly with CI/CD platforms like GitHub Actions, GitLab CI/CD, -and Jenkins. This allows us to build the code manifest needed for visualization -and refactoring in the background, without needing to wait for it to run locally -in the case of very large codebases (>1M lines of code). +Generate manifests automatically on every push: + +```yaml +# .github/workflows/napi.yml +name: Generate Manifest +on: [push] +jobs: + manifest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install napi + run: curl -fsSL https://raw.githubusercontent.com/nanoapi-io/napi/refs/heads/main/install_scripts/install.sh | bash + + - name: Generate manifest + run: napi generate --branch ${{ github.ref_name }} --commit-sha ${{ github.sha }} --commit-sha-date "$(git log -1 --format=%cI)" +``` + +## Configuration Reference + +### `.napirc` + +Project-level configuration created by `napi init`: + +```json +{ + "language": "python", + "python": { "version": "3.10" }, + "project": { + "include": ["src/**/*.py"], + "exclude": [".git/**", "**/__pycache__/**", "napi_out/**"] + }, + "outDir": "napi_out", + "labeling": { + "modelProvider": "openai", + "maxConcurrency": 5 + } +} +``` + +### Global Config + +Stored in your OS config directory (`~/.config/napi/config.json` on Linux, +`~/Library/Application Support/napi/config.json` on macOS). Managed via +`napi set apiKey`. + +```json +{ + "labeling": { + "apiKeys": { + "openai": "sk-...", + "google": "AIza...", + "anthropic": "sk-ant-..." + } + } +} +``` + +## Development + +Requires [Deno](https://deno.land/) v2.4+. + +```bash +# Install dependencies +deno install --allow-scripts -More information +# Run CLI in dev mode +deno task dev + +# Run viewer dev server (hot-reload) +deno task dev:viewer + +# Build viewer for production +deno task build:viewer + +# Compile binary (includes viewer) +deno task compile + +# Run tests +deno task test + +# Lint +deno lint + +# Format +deno fmt +``` ## Contributing diff --git a/deno.json b/deno.json index 674249e3..46c2a5c9 100644 --- a/deno.json +++ b/deno.json @@ -5,6 +5,7 @@ "nodeModulesDir": "auto", "lock": false, "imports": { + "@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.4", "@inquirer/prompts": "npm:@inquirer/prompts@^7.5.3", "@langchain/anthropic": "npm:@langchain/anthropic@^0.3.23", "@langchain/core": "npm:@langchain/core@^0.3.61", @@ -13,26 +14,59 @@ "@langchain/langgraph": "npm:@langchain/langgraph@^0.3.5", "@langchain/openai": "npm:@langchain/openai@^0.5.15", "@oak/oak": "jsr:@oak/oak@^17.1.4", + "@radix-ui/react-dialog": "npm:@radix-ui/react-dialog@^1.1.14", + "@radix-ui/react-dropdown-menu": "npm:@radix-ui/react-dropdown-menu@^2.1.15", + "@radix-ui/react-label": "npm:@radix-ui/react-label@^2.1.7", + "@radix-ui/react-scroll-area": "npm:@radix-ui/react-scroll-area@^1.2.9", + "@radix-ui/react-separator": "npm:@radix-ui/react-separator@^1.1.7", + "@radix-ui/react-slider": "npm:@radix-ui/react-slider@^1.3.5", + "@radix-ui/react-slot": "npm:@radix-ui/react-slot@^1.2.3", + "@radix-ui/react-tooltip": "npm:@radix-ui/react-tooltip@^1.2.7", "@std/expect": "jsr:@std/expect@^1.0.16", "@std/path": "jsr:@std/path@^1.0.9", "@std/testing": "jsr:@std/testing@^1.0.14", + "@tailwindcss/vite": "npm:@tailwindcss/vite@^4.1.8", + "@types/cytoscape-fcose": "npm:@types/cytoscape-fcose@^2.2.4", + "@types/react": "npm:@types/react@^19.1.6", + "@types/react-dom": "npm:@types/react-dom@^19.1.3", + "@vitejs/plugin-react": "npm:@vitejs/plugin-react@^4.4.1", + "class-variance-authority": "npm:class-variance-authority@^0.7.1", + "clsx": "npm:clsx@^2.1.1", + "cytoscape": "npm:cytoscape@^3.32.0", + "cytoscape-fcose": "npm:cytoscape-fcose@^2.2.0", "glob": "npm:glob@^11.0.2", + "lucide-react": "npm:lucide-react@^0.513.0", + "react": "npm:react@^19.1.0", + "react-dom": "npm:react-dom@^19.1.0", + "react-router-dom": "npm:react-router-dom@^7.5.3", + "tailwind-merge": "npm:tailwind-merge@^3.3.0", + "tailwindcss": "npm:tailwindcss@^4.1.8", "tree-sitter": "npm:tree-sitter@^0.22.4", "tree-sitter-c": "npm:tree-sitter-c@0.23.6", "tree-sitter-c-sharp": "npm:tree-sitter-c-sharp@^0.23.1", "tree-sitter-python": "npm:tree-sitter-python@^0.23.6", "tree-sitter-java": "npm:tree-sitter-java@^0.23.5", + "tw-animate-css": "npm:tw-animate-css@^1.3.4", + "vite": "npm:vite@^6.3.5", "yargs": "https://deno.land/x/yargs@v18.0.0-deno/deno.ts", "yargs-types": "https://deno.land/x/yargs@v18.0.0-deno/deno-types.ts", "zod": "npm:zod@^3.24.4" }, + "exclude": ["viewer/"], "tasks": { "dev": "deno run -A src/index.ts", - "compile": "deno compile -A --output=dist/napi src/index.ts", - "compile-linux": "deno compile -A --output=dist/napi.linux --target=x86_64-unknown-linux-gnu src/index.ts", - "compile-macos": "deno compile -A --output=dist/napi.macos --target=x86_64-apple-darwin src/index.ts", - "compile-windows": "deno compile -A --output=dist/napi.exe --target=x86_64-pc-windows-msvc src/index.ts", + "dev:viewer": "cd viewer && deno run -A npm:vite", + "build:viewer": "cd viewer && deno run -A npm:vite build", + "compile": "deno task build:viewer && deno compile -A --include=viewer/dist --output=dist/napi src/index.ts", + "compile-linux": "deno task build:viewer && deno compile -A --include=viewer/dist --output=dist/napi.linux --target=x86_64-unknown-linux-gnu src/index.ts", + "compile-macos": "deno task build:viewer && deno compile -A --include=viewer/dist --output=dist/napi.macos --target=x86_64-apple-darwin src/index.ts", + "compile-windows": "deno task build:viewer && deno compile -A --include=viewer/dist --output=dist/napi.exe --target=x86_64-pc-windows-msvc src/index.ts", "compile:all": "deno task 'compile-*'", "test": "deno test -A" + }, + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "jsxImportSourceTypes": "@types/react" } } diff --git a/examples/c/network/.napirc b/examples/c/network/.napirc index e213da94..a2cb8775 100644 --- a/examples/c/network/.napirc +++ b/examples/c/network/.napirc @@ -11,31 +11,12 @@ "**/build/**", "**/bin/**", "**/obj/**", - "**/packages/**", - "**/.vs/**", - "**/TestResults/**", - "**/*.user", - "**/*.suo", - "**/.nuget/**", - "**/artifacts/**", - "**/packages/**", - "**/util/**", - "**/test/**", - "**/perf/**", "**/napi_out/**" ] }, "outDir": "napi_out", - "metrics": { - "file": { - "maxChar": 100000, - "maxLine": 1000, - "maxDep": 10 - }, - "symbol": { - "maxChar": 50000, - "maxLine": 500, - "maxDep": 5 - } + "audit": { + "file": { "maxCodeChar": 5000, "maxCodeLine": 300, "maxDependency": 20, "maxCyclomaticComplexity": 50 }, + "symbol": { "maxCodeChar": 1000, "maxCodeLine": 80, "maxDependency": 10, "maxCyclomaticComplexity": 15 } } } diff --git a/examples/csharp/EndpointExample/.napirc b/examples/csharp/EndpointExample/.napirc index f34005b9..27d25646 100644 --- a/examples/csharp/EndpointExample/.napirc +++ b/examples/csharp/EndpointExample/.napirc @@ -5,16 +5,8 @@ "exclude": ["bin/**", "obj/**", "napi-output/**"] }, "outDir": "napi-output", - "metrics": { - "file": { - "maxChar": 100000, - "maxLine": 1000, - "maxDep": 10 - }, - "symbol": { - "maxChar": 50000, - "maxLine": 500, - "maxDep": 5 - } + "audit": { + "file": { "maxCodeChar": 5000, "maxCodeLine": 300, "maxDependency": 20, "maxCyclomaticComplexity": 50 }, + "symbol": { "maxCodeChar": 1000, "maxCodeLine": 80, "maxDependency": 10, "maxCyclomaticComplexity": 15 } } } diff --git a/examples/java/websocket/src/main/resources/static/echo.html b/examples/java/websocket/src/main/resources/static/echo.html index 3a37f451..b0a6406a 100644 --- a/examples/java/websocket/src/main/resources/static/echo.html +++ b/examples/java/websocket/src/main/resources/static/echo.html @@ -125,8 +125,8 @@ />
- - +
@@ -135,7 +135,7 @@ >Here is a message!
-
diff --git a/examples/python/flask/.napi/manifests/1775598176791-d5c2217.json b/examples/python/flask/.napi/manifests/1775598176791-d5c2217.json new file mode 100644 index 00000000..b52885ce --- /dev/null +++ b/examples/python/flask/.napi/manifests/1775598176791-d5c2217.json @@ -0,0 +1,2128 @@ +{ + "id": "1775598176791-d5c2217", + "branch": "main", + "commitSha": "d5c22175a36c5de08482bfa3e1ea59758e888405", + "commitShaDate": "2025-07-07T11:42:05+02:00", + "createdAt": "2026-04-07T21:42:56.791Z", + "manifest": { + "api/data/elves.py": { + "id": "api/data/elves.py", + "filePath": "api/data/elves.py", + "metrics": { + "linesCount": 11, + "codeLineCount": 11, + "characterCount": 297, + "codeCharacterCount": 239, + "dependencyCount": 0, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "language": "python", + "dependencies": {}, + "dependents": { + "api/services/elves.py": { + "id": "api/services/elves.py", + "symbols": { + "Elf": "Elf", + "elves": "elves", + "ElfService": "ElfService" + } + } + }, + "symbols": { + "Elf": { + "id": "Elf", + "type": "class", + "positions": [ + { + "start": { + "index": 0, + "row": 0, + "column": 0 + }, + "end": { + "index": 176, + "row": 6, + "column": 36 + } + } + ], + "description": "", + "metrics": { + "linesCount": 7, + "codeLineCount": 6, + "characterCount": 176, + "codeCharacterCount": 138, + "dependencyCount": 0, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": {}, + "dependents": { + "api/services/elves.py": { + "id": "api/services/elves.py", + "symbols": { + "ElfService": "ElfService" + } + } + } + }, + "elves": { + "id": "elves", + "type": "variable", + "positions": [ + { + "start": { + "index": 179, + "row": 9, + "column": 0 + }, + "end": { + "index": 296, + "row": 13, + "column": 1 + } + } + ], + "description": "", + "metrics": { + "linesCount": 5, + "codeLineCount": 5, + "characterCount": 117, + "codeCharacterCount": 101, + "dependencyCount": 0, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": {}, + "dependents": { + "api/services/elves.py": { + "id": "api/services/elves.py", + "symbols": { + "ElfService": "ElfService" + } + } + } + } + } + }, + "api/data/hobbits.py": { + "id": "api/data/hobbits.py", + "filePath": "api/data/hobbits.py", + "metrics": { + "linesCount": 11, + "codeLineCount": 11, + "characterCount": 323, + "codeCharacterCount": 265, + "dependencyCount": 0, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "language": "python", + "dependencies": {}, + "dependents": { + "api/services/hobbits.py": { + "id": "api/services/hobbits.py", + "symbols": { + "Hobbit": "Hobbit", + "hobbits": "hobbits", + "HobbitService": "HobbitService" + } + } + }, + "symbols": { + "Hobbit": { + "id": "Hobbit", + "type": "class", + "positions": [ + { + "start": { + "index": 0, + "row": 0, + "column": 0 + }, + "end": { + "index": 179, + "row": 6, + "column": 36 + } + } + ], + "description": "", + "metrics": { + "linesCount": 7, + "codeLineCount": 6, + "characterCount": 179, + "codeCharacterCount": 141, + "dependencyCount": 0, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": {}, + "dependents": { + "api/services/hobbits.py": { + "id": "api/services/hobbits.py", + "symbols": { + "HobbitService": "HobbitService" + } + } + } + }, + "hobbits": { + "id": "hobbits", + "type": "variable", + "positions": [ + { + "start": { + "index": 182, + "row": 9, + "column": 0 + }, + "end": { + "index": 322, + "row": 13, + "column": 1 + } + } + ], + "description": "", + "metrics": { + "linesCount": 5, + "codeLineCount": 5, + "characterCount": 140, + "codeCharacterCount": 124, + "dependencyCount": 0, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": {}, + "dependents": { + "api/services/hobbits.py": { + "id": "api/services/hobbits.py", + "symbols": { + "HobbitService": "HobbitService" + } + } + } + } + } + }, + "api/services/elves.py": { + "id": "api/services/elves.py", + "filePath": "api/services/elves.py", + "metrics": { + "linesCount": 21, + "codeLineCount": 21, + "characterCount": 602, + "codeCharacterCount": 419, + "dependencyCount": 1, + "dependentCount": 1, + "cyclomaticComplexity": 5 + }, + "language": "python", + "dependencies": { + "api/data/elves.py": { + "id": "api/data/elves.py", + "isExternal": false, + "symbols": { + "Elf": "Elf", + "elves": "elves" + } + } + }, + "dependents": { + "api/views/elves.py": { + "id": "api/views/elves.py", + "symbols": { + "ElfService": "ElfService", + "ElfListView": "ElfListView", + "ElfCreateView": "ElfCreateView", + "ElfDetailView": "ElfDetailView", + "ElfUpdateView": "ElfUpdateView", + "ElfDeleteView": "ElfDeleteView" + } + } + }, + "symbols": { + "ElfService": { + "id": "ElfService", + "type": "class", + "positions": [ + { + "start": { + "index": 40, + "row": 3, + "column": 0 + }, + "end": { + "index": 601, + "row": 26, + "column": 33 + } + } + ], + "description": "", + "metrics": { + "linesCount": 24, + "codeLineCount": 20, + "characterCount": 561, + "codeCharacterCount": 382, + "dependencyCount": 1, + "dependentCount": 1, + "cyclomaticComplexity": 5 + }, + "dependencies": { + "api/data/elves.py": { + "id": "api/data/elves.py", + "isExternal": false, + "symbols": { + "Elf": "Elf", + "elves": "elves" + } + } + }, + "dependents": { + "api/views/elves.py": { + "id": "api/views/elves.py", + "symbols": { + "ElfListView": "ElfListView", + "ElfCreateView": "ElfCreateView", + "ElfDetailView": "ElfDetailView", + "ElfUpdateView": "ElfUpdateView", + "ElfDeleteView": "ElfDeleteView" + } + } + } + } + } + }, + "api/services/hobbits.py": { + "id": "api/services/hobbits.py", + "filePath": "api/services/hobbits.py", + "metrics": { + "linesCount": 21, + "codeLineCount": 21, + "characterCount": 698, + "codeCharacterCount": 515, + "dependencyCount": 1, + "dependentCount": 1, + "cyclomaticComplexity": 5 + }, + "language": "python", + "dependencies": { + "api/data/hobbits.py": { + "id": "api/data/hobbits.py", + "isExternal": false, + "symbols": { + "Hobbit": "Hobbit", + "hobbits": "hobbits" + } + } + }, + "dependents": { + "api/views/hobbits.py": { + "id": "api/views/hobbits.py", + "symbols": { + "HobbitService": "HobbitService", + "HobbitListView": "HobbitListView", + "HobbitCreateView": "HobbitCreateView", + "HobbitDetailView": "HobbitDetailView", + "HobbitUpdateView": "HobbitUpdateView", + "HobbitDeleteView": "HobbitDeleteView" + } + } + }, + "symbols": { + "HobbitService": { + "id": "HobbitService", + "type": "class", + "positions": [ + { + "start": { + "index": 47, + "row": 3, + "column": 0 + }, + "end": { + "index": 697, + "row": 26, + "column": 38 + } + } + ], + "description": "", + "metrics": { + "linesCount": 24, + "codeLineCount": 20, + "characterCount": 650, + "codeCharacterCount": 471, + "dependencyCount": 1, + "dependentCount": 1, + "cyclomaticComplexity": 5 + }, + "dependencies": { + "api/data/hobbits.py": { + "id": "api/data/hobbits.py", + "isExternal": false, + "symbols": { + "Hobbit": "Hobbit", + "hobbits": "hobbits" + } + } + }, + "dependents": { + "api/views/hobbits.py": { + "id": "api/views/hobbits.py", + "symbols": { + "HobbitListView": "HobbitListView", + "HobbitCreateView": "HobbitCreateView", + "HobbitDetailView": "HobbitDetailView", + "HobbitUpdateView": "HobbitUpdateView", + "HobbitDeleteView": "HobbitDeleteView" + } + } + } + } + } + }, + "api/views/elves.py": { + "id": "api/views/elves.py", + "filePath": "api/views/elves.py", + "metrics": { + "linesCount": 39, + "codeLineCount": 39, + "characterCount": 1770, + "codeCharacterCount": 1279, + "dependencyCount": 3, + "dependentCount": 2, + "cyclomaticComplexity": 1 + }, + "language": "python", + "dependencies": { + "api/services/elves.py": { + "id": "api/services/elves.py", + "isExternal": false, + "symbols": { + "ElfService": "ElfService" + } + }, + "flask.views": { + "id": "flask.views", + "isExternal": true, + "symbols": { + "MethodView": "MethodView" + } + }, + "flask": { + "id": "flask", + "isExternal": true, + "symbols": { + "request": "request", + "Blueprint": "Blueprint" + } + } + }, + "dependents": { + "app.py": { + "id": "app.py", + "symbols": { + "elves_bp": "elves_bp", + "app": "app" + } + }, + "api/views/elves.py": { + "id": "api/views/elves.py", + "symbols": { + "elves_bp": "elves_bp" + } + } + }, + "symbols": { + "ElfCreateView": { + "id": "ElfCreateView", + "type": "class", + "positions": [ + { + "start": { + "index": 430, + "row": 17, + "column": 0 + }, + "end": { + "index": 604, + "row": 21, + "column": 22 + } + } + ], + "description": "", + "metrics": { + "linesCount": 5, + "codeLineCount": 5, + "characterCount": 174, + "codeCharacterCount": 142, + "dependencyCount": 3, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "api/services/elves.py": { + "id": "api/services/elves.py", + "isExternal": false, + "symbols": { + "ElfService": "ElfService" + } + }, + "flask.views": { + "id": "flask.views", + "isExternal": true, + "symbols": { + "MethodView": "MethodView" + } + }, + "flask": { + "id": "flask", + "isExternal": true, + "symbols": { + "request": "request" + } + } + }, + "dependents": { + "api/views/elves.py": { + "id": "api/views/elves.py", + "symbols": { + "elves_bp": "elves_bp" + } + } + } + }, + "ElfDeleteView": { + "id": "ElfDeleteView", + "type": "class", + "positions": [ + { + "start": { + "index": 1450, + "row": 55, + "column": 0 + }, + "end": { + "index": 1589, + "row": 58, + "column": 19 + } + } + ], + "description": "", + "metrics": { + "linesCount": 4, + "codeLineCount": 4, + "characterCount": 139, + "codeCharacterCount": 116, + "dependencyCount": 2, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "api/services/elves.py": { + "id": "api/services/elves.py", + "isExternal": false, + "symbols": { + "ElfService": "ElfService" + } + }, + "flask.views": { + "id": "flask.views", + "isExternal": true, + "symbols": { + "MethodView": "MethodView" + } + } + }, + "dependents": { + "api/views/elves.py": { + "id": "api/views/elves.py", + "symbols": { + "elves_bp": "elves_bp" + } + } + } + }, + "ElfDetailView": { + "id": "ElfDetailView", + "type": "class", + "positions": [ + { + "start": { + "index": 762, + "row": 30, + "column": 0 + }, + "end": { + "index": 900, + "row": 33, + "column": 18 + } + } + ], + "description": "", + "metrics": { + "linesCount": 4, + "codeLineCount": 4, + "characterCount": 138, + "codeCharacterCount": 115, + "dependencyCount": 2, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "api/services/elves.py": { + "id": "api/services/elves.py", + "isExternal": false, + "symbols": { + "ElfService": "ElfService" + } + }, + "flask.views": { + "id": "flask.views", + "isExternal": true, + "symbols": { + "MethodView": "MethodView" + } + } + }, + "dependents": { + "api/views/elves.py": { + "id": "api/views/elves.py", + "symbols": { + "elves_bp": "elves_bp" + } + } + } + }, + "ElfListView": { + "id": "ElfListView", + "type": "class", + "positions": [ + { + "start": { + "index": 157, + "row": 7, + "column": 0 + }, + "end": { + "index": 285, + "row": 10, + "column": 20 + } + } + ], + "description": "", + "metrics": { + "linesCount": 4, + "codeLineCount": 4, + "characterCount": 128, + "codeCharacterCount": 105, + "dependencyCount": 2, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "api/services/elves.py": { + "id": "api/services/elves.py", + "isExternal": false, + "symbols": { + "ElfService": "ElfService" + } + }, + "flask.views": { + "id": "flask.views", + "isExternal": true, + "symbols": { + "MethodView": "MethodView" + } + } + }, + "dependents": { + "api/views/elves.py": { + "id": "api/views/elves.py", + "symbols": { + "elves_bp": "elves_bp" + } + } + } + }, + "ElfUpdateView": { + "id": "ElfUpdateView", + "type": "class", + "positions": [ + { + "start": { + "index": 1076, + "row": 42, + "column": 0 + }, + "end": { + "index": 1273, + "row": 46, + "column": 26 + } + } + ], + "description": "", + "metrics": { + "linesCount": 5, + "codeLineCount": 5, + "characterCount": 197, + "codeCharacterCount": 165, + "dependencyCount": 3, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "api/services/elves.py": { + "id": "api/services/elves.py", + "isExternal": false, + "symbols": { + "ElfService": "ElfService" + } + }, + "flask.views": { + "id": "flask.views", + "isExternal": true, + "symbols": { + "MethodView": "MethodView" + } + }, + "flask": { + "id": "flask", + "isExternal": true, + "symbols": { + "request": "request" + } + } + }, + "dependents": { + "api/views/elves.py": { + "id": "api/views/elves.py", + "symbols": { + "elves_bp": "elves_bp" + } + } + } + }, + "elves_bp": { + "id": "elves_bp", + "type": "variable", + "positions": [ + { + "start": { + "index": 115, + "row": 4, + "column": 0 + }, + "end": { + "index": 154, + "row": 4, + "column": 39 + } + }, + { + "start": { + "index": 341, + "row": 14, + "column": 0 + }, + "end": { + "index": 427, + "row": 14, + "column": 86 + } + }, + { + "start": { + "index": 662, + "row": 25, + "column": 0 + }, + "end": { + "index": 759, + "row": 27, + "column": 1 + } + }, + { + "start": { + "index": 965, + "row": 37, + "column": 0 + }, + "end": { + "index": 1073, + "row": 39, + "column": 1 + } + }, + { + "start": { + "index": 1339, + "row": 50, + "column": 0 + }, + "end": { + "index": 1447, + "row": 52, + "column": 1 + } + }, + { + "start": { + "index": 1658, + "row": 62, + "column": 0 + }, + "end": { + "index": 1769, + "row": 64, + "column": 1 + } + } + ], + "description": "", + "metrics": { + "linesCount": 14, + "codeLineCount": 14, + "characterCount": 549, + "codeCharacterCount": 525, + "dependencyCount": 2, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "api/views/elves.py": { + "id": "api/views/elves.py", + "isExternal": false, + "symbols": { + "ElfListView": "ElfListView", + "ElfCreateView": "ElfCreateView", + "ElfDetailView": "ElfDetailView", + "ElfUpdateView": "ElfUpdateView", + "ElfDeleteView": "ElfDeleteView" + } + }, + "flask": { + "id": "flask", + "isExternal": true, + "symbols": { + "Blueprint": "Blueprint" + } + } + }, + "dependents": { + "app.py": { + "id": "app.py", + "symbols": { + "app": "app" + } + } + } + } + } + }, + "api/views/hobbits.py": { + "id": "api/views/hobbits.py", + "filePath": "api/views/hobbits.py", + "metrics": { + "linesCount": 47, + "codeLineCount": 47, + "characterCount": 1979, + "codeCharacterCount": 1418, + "dependencyCount": 3, + "dependentCount": 2, + "cyclomaticComplexity": 1 + }, + "language": "python", + "dependencies": { + "api/services/hobbits.py": { + "id": "api/services/hobbits.py", + "isExternal": false, + "symbols": { + "HobbitService": "HobbitService" + } + }, + "flask.views": { + "id": "flask.views", + "isExternal": true, + "symbols": { + "MethodView": "MethodView" + } + }, + "flask": { + "id": "flask", + "isExternal": true, + "symbols": { + "request": "request", + "Blueprint": "Blueprint" + } + } + }, + "dependents": { + "app.py": { + "id": "app.py", + "symbols": { + "hobbits_bp": "hobbits_bp", + "app": "app" + } + }, + "api/views/hobbits.py": { + "id": "api/views/hobbits.py", + "symbols": { + "hobbits_bp": "hobbits_bp" + } + } + }, + "symbols": { + "HobbitCreateView": { + "id": "HobbitCreateView", + "type": "class", + "positions": [ + { + "start": { + "index": 470, + "row": 19, + "column": 0 + }, + "end": { + "index": 659, + "row": 23, + "column": 25 + } + } + ], + "description": "", + "metrics": { + "linesCount": 5, + "codeLineCount": 5, + "characterCount": 189, + "codeCharacterCount": 157, + "dependencyCount": 3, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "api/services/hobbits.py": { + "id": "api/services/hobbits.py", + "isExternal": false, + "symbols": { + "HobbitService": "HobbitService" + } + }, + "flask.views": { + "id": "flask.views", + "isExternal": true, + "symbols": { + "MethodView": "MethodView" + } + }, + "flask": { + "id": "flask", + "isExternal": true, + "symbols": { + "request": "request" + } + } + }, + "dependents": { + "api/views/hobbits.py": { + "id": "api/views/hobbits.py", + "symbols": { + "hobbits_bp": "hobbits_bp" + } + } + } + }, + "HobbitDeleteView": { + "id": "HobbitDeleteView", + "type": "class", + "positions": [ + { + "start": { + "index": 1616, + "row": 61, + "column": 0 + }, + "end": { + "index": 1770, + "row": 64, + "column": 19 + } + } + ], + "description": "", + "metrics": { + "linesCount": 4, + "codeLineCount": 4, + "characterCount": 154, + "codeCharacterCount": 131, + "dependencyCount": 2, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "api/services/hobbits.py": { + "id": "api/services/hobbits.py", + "isExternal": false, + "symbols": { + "HobbitService": "HobbitService" + } + }, + "flask.views": { + "id": "flask.views", + "isExternal": true, + "symbols": { + "MethodView": "MethodView" + } + } + }, + "dependents": { + "api/views/hobbits.py": { + "id": "api/views/hobbits.py", + "symbols": { + "hobbits_bp": "hobbits_bp" + } + } + } + }, + "HobbitDetailView": { + "id": "HobbitDetailView", + "type": "class", + "positions": [ + { + "start": { + "index": 830, + "row": 32, + "column": 0 + }, + "end": { + "index": 989, + "row": 35, + "column": 21 + } + } + ], + "description": "", + "metrics": { + "linesCount": 4, + "codeLineCount": 4, + "characterCount": 159, + "codeCharacterCount": 136, + "dependencyCount": 2, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "api/services/hobbits.py": { + "id": "api/services/hobbits.py", + "isExternal": false, + "symbols": { + "HobbitService": "HobbitService" + } + }, + "flask.views": { + "id": "flask.views", + "isExternal": true, + "symbols": { + "MethodView": "MethodView" + } + } + }, + "dependents": { + "api/views/hobbits.py": { + "id": "api/views/hobbits.py", + "symbols": { + "hobbits_bp": "hobbits_bp" + } + } + } + }, + "HobbitListView": { + "id": "HobbitListView", + "type": "class", + "positions": [ + { + "start": { + "index": 166, + "row": 7, + "column": 0 + }, + "end": { + "index": 306, + "row": 10, + "column": 22 + } + } + ], + "description": "", + "metrics": { + "linesCount": 4, + "codeLineCount": 4, + "characterCount": 140, + "codeCharacterCount": 117, + "dependencyCount": 2, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "api/services/hobbits.py": { + "id": "api/services/hobbits.py", + "isExternal": false, + "symbols": { + "HobbitService": "HobbitService" + } + }, + "flask.views": { + "id": "flask.views", + "isExternal": true, + "symbols": { + "MethodView": "MethodView" + } + } + }, + "dependents": { + "api/views/hobbits.py": { + "id": "api/views/hobbits.py", + "symbols": { + "hobbits_bp": "hobbits_bp" + } + } + } + }, + "hobbits_bp": { + "id": "hobbits_bp", + "type": "variable", + "positions": [ + { + "start": { + "index": 120, + "row": 4, + "column": 0 + }, + "end": { + "index": 163, + "row": 4, + "column": 43 + } + }, + { + "start": { + "index": 367, + "row": 14, + "column": 0 + }, + "end": { + "index": 467, + "row": 16, + "column": 1 + } + }, + { + "start": { + "index": 722, + "row": 27, + "column": 0 + }, + "end": { + "index": 827, + "row": 29, + "column": 1 + } + }, + { + "start": { + "index": 1062, + "row": 39, + "column": 0 + }, + "end": { + "index": 1190, + "row": 43, + "column": 1 + } + }, + { + "start": { + "index": 1485, + "row": 54, + "column": 0 + }, + "end": { + "index": 1613, + "row": 58, + "column": 1 + } + }, + { + "start": { + "index": 1847, + "row": 68, + "column": 0 + }, + "end": { + "index": 1978, + "row": 72, + "column": 1 + } + } + ], + "description": "", + "metrics": { + "linesCount": 22, + "codeLineCount": 22, + "characterCount": 635, + "codeCharacterCount": 575, + "dependencyCount": 2, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "api/views/hobbits.py": { + "id": "api/views/hobbits.py", + "isExternal": false, + "symbols": { + "HobbitListView": "HobbitListView", + "HobbitCreateView": "HobbitCreateView", + "HobbitDetailView": "HobbitDetailView", + "HobbitUpdateView": "HobbitUpdateView", + "HobbitDeleteView": "HobbitDeleteView" + } + }, + "flask": { + "id": "flask", + "isExternal": true, + "symbols": { + "Blueprint": "Blueprint" + } + } + }, + "dependents": { + "app.py": { + "id": "app.py", + "symbols": { + "app": "app" + } + } + } + }, + "HobbitUpdateView": { + "id": "HobbitUpdateView", + "type": "class", + "positions": [ + { + "start": { + "index": 1193, + "row": 46, + "column": 0 + }, + "end": { + "index": 1411, + "row": 50, + "column": 29 + } + } + ], + "description": "", + "metrics": { + "linesCount": 5, + "codeLineCount": 5, + "characterCount": 218, + "codeCharacterCount": 186, + "dependencyCount": 3, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "api/services/hobbits.py": { + "id": "api/services/hobbits.py", + "isExternal": false, + "symbols": { + "HobbitService": "HobbitService" + } + }, + "flask.views": { + "id": "flask.views", + "isExternal": true, + "symbols": { + "MethodView": "MethodView" + } + }, + "flask": { + "id": "flask", + "isExternal": true, + "symbols": { + "request": "request" + } + } + }, + "dependents": { + "api/views/hobbits.py": { + "id": "api/views/hobbits.py", + "symbols": { + "hobbits_bp": "hobbits_bp" + } + } + } + } + } + }, + "api/wizards/data.py": { + "id": "api/wizards/data.py", + "filePath": "api/wizards/data.py", + "metrics": { + "linesCount": 11, + "codeLineCount": 11, + "characterCount": 316, + "codeCharacterCount": 258, + "dependencyCount": 0, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "language": "python", + "dependencies": {}, + "dependents": { + "api/wizards/services.py": { + "id": "api/wizards/services.py", + "symbols": { + "Wizard": "Wizard", + "wizards": "wizards", + "WizardService": "WizardService" + } + } + }, + "symbols": { + "Wizard": { + "id": "Wizard", + "type": "class", + "positions": [ + { + "start": { + "index": 0, + "row": 0, + "column": 0 + }, + "end": { + "index": 179, + "row": 6, + "column": 36 + } + } + ], + "description": "", + "metrics": { + "linesCount": 7, + "codeLineCount": 6, + "characterCount": 179, + "codeCharacterCount": 141, + "dependencyCount": 0, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": {}, + "dependents": { + "api/wizards/services.py": { + "id": "api/wizards/services.py", + "symbols": { + "WizardService": "WizardService" + } + } + } + }, + "wizards": { + "id": "wizards", + "type": "variable", + "positions": [ + { + "start": { + "index": 182, + "row": 9, + "column": 0 + }, + "end": { + "index": 315, + "row": 13, + "column": 1 + } + } + ], + "description": "", + "metrics": { + "linesCount": 5, + "codeLineCount": 5, + "characterCount": 133, + "codeCharacterCount": 117, + "dependencyCount": 0, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": {}, + "dependents": { + "api/wizards/services.py": { + "id": "api/wizards/services.py", + "symbols": { + "WizardService": "WizardService" + } + } + } + } + } + }, + "api/wizards/services.py": { + "id": "api/wizards/services.py", + "filePath": "api/wizards/services.py", + "metrics": { + "linesCount": 22, + "codeLineCount": 22, + "characterCount": 742, + "codeCharacterCount": 559, + "dependencyCount": 1, + "dependentCount": 1, + "cyclomaticComplexity": 5 + }, + "language": "python", + "dependencies": { + "api/wizards/data.py": { + "id": "api/wizards/data.py", + "isExternal": false, + "symbols": { + "Wizard": "Wizard", + "wizards": "wizards" + } + } + }, + "dependents": { + "api/wizards/views.py": { + "id": "api/wizards/views.py", + "symbols": { + "WizardService": "WizardService", + "WizardListView": "WizardListView", + "WizardCreateView": "WizardCreateView", + "WizardDetailView": "WizardDetailView", + "WizardUpdateView": "WizardUpdateView", + "WizardDeleteView": "WizardDeleteView" + } + } + }, + "symbols": { + "WizardService": { + "id": "WizardService", + "type": "class", + "positions": [ + { + "start": { + "index": 91, + "row": 3, + "column": 0 + }, + "end": { + "index": 741, + "row": 26, + "column": 38 + } + } + ], + "description": "", + "metrics": { + "linesCount": 24, + "codeLineCount": 20, + "characterCount": 650, + "codeCharacterCount": 471, + "dependencyCount": 1, + "dependentCount": 1, + "cyclomaticComplexity": 5 + }, + "dependencies": { + "api/wizards/data.py": { + "id": "api/wizards/data.py", + "isExternal": false, + "symbols": { + "Wizard": "Wizard", + "wizards": "wizards" + } + } + }, + "dependents": { + "api/wizards/views.py": { + "id": "api/wizards/views.py", + "symbols": { + "WizardListView": "WizardListView", + "WizardCreateView": "WizardCreateView", + "WizardDetailView": "WizardDetailView", + "WizardUpdateView": "WizardUpdateView", + "WizardDeleteView": "WizardDeleteView" + } + } + } + } + } + }, + "api/wizards/views.py": { + "id": "api/wizards/views.py", + "filePath": "api/wizards/views.py", + "metrics": { + "linesCount": 49, + "codeLineCount": 49, + "characterCount": 2120, + "codeCharacterCount": 1446, + "dependencyCount": 3, + "dependentCount": 2, + "cyclomaticComplexity": 1 + }, + "language": "python", + "dependencies": { + "api/wizards/services.py": { + "id": "api/wizards/services.py", + "isExternal": false, + "symbols": { + "WizardService": "WizardService" + } + }, + "flask.views": { + "id": "flask.views", + "isExternal": true, + "symbols": { + "MethodView": "MethodView" + } + }, + "flask": { + "id": "flask", + "isExternal": true, + "symbols": { + "request": "request", + "Blueprint": "Blueprint" + } + } + }, + "dependents": { + "app.py": { + "id": "app.py", + "symbols": { + "get_wizzards_bp": "get_wizzards_bp", + "app": "app" + } + }, + "api/wizards/views.py": { + "id": "api/wizards/views.py", + "symbols": { + "get_wizzards_bp": "get_wizzards_bp" + } + } + }, + "symbols": { + "get_wizzards_bp": { + "id": "get_wizzards_bp", + "type": "function", + "positions": [ + { + "start": { + "index": 984, + "row": 36, + "column": 0 + }, + "end": { + "index": 2120, + "row": 70, + "column": 21 + } + } + ], + "description": "", + "metrics": { + "linesCount": 35, + "codeLineCount": 24, + "characterCount": 1136, + "codeCharacterCount": 614, + "dependencyCount": 2, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "api/wizards/views.py": { + "id": "api/wizards/views.py", + "isExternal": false, + "symbols": { + "WizardListView": "WizardListView", + "WizardCreateView": "WizardCreateView", + "WizardDetailView": "WizardDetailView", + "WizardUpdateView": "WizardUpdateView", + "WizardDeleteView": "WizardDeleteView" + } + }, + "flask": { + "id": "flask", + "isExternal": true, + "symbols": { + "Blueprint": "Blueprint" + } + } + }, + "dependents": { + "app.py": { + "id": "app.py", + "symbols": { + "app": "app" + } + } + } + }, + "WizardCreateView": { + "id": "WizardCreateView", + "type": "class", + "positions": [ + { + "start": { + "index": 252, + "row": 10, + "column": 0 + }, + "end": { + "index": 441, + "row": 14, + "column": 25 + } + } + ], + "description": "", + "metrics": { + "linesCount": 5, + "codeLineCount": 5, + "characterCount": 189, + "codeCharacterCount": 157, + "dependencyCount": 3, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "api/wizards/services.py": { + "id": "api/wizards/services.py", + "isExternal": false, + "symbols": { + "WizardService": "WizardService" + } + }, + "flask.views": { + "id": "flask.views", + "isExternal": true, + "symbols": { + "MethodView": "MethodView" + } + }, + "flask": { + "id": "flask", + "isExternal": true, + "symbols": { + "request": "request" + } + } + }, + "dependents": { + "api/wizards/views.py": { + "id": "api/wizards/views.py", + "symbols": { + "get_wizzards_bp": "get_wizzards_bp" + } + } + } + }, + "WizardDeleteView": { + "id": "WizardDeleteView", + "type": "class", + "positions": [ + { + "start": { + "index": 827, + "row": 30, + "column": 0 + }, + "end": { + "index": 981, + "row": 33, + "column": 19 + } + } + ], + "description": "", + "metrics": { + "linesCount": 4, + "codeLineCount": 4, + "characterCount": 154, + "codeCharacterCount": 131, + "dependencyCount": 2, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "api/wizards/services.py": { + "id": "api/wizards/services.py", + "isExternal": false, + "symbols": { + "WizardService": "WizardService" + } + }, + "flask.views": { + "id": "flask.views", + "isExternal": true, + "symbols": { + "MethodView": "MethodView" + } + } + }, + "dependents": { + "api/wizards/views.py": { + "id": "api/wizards/views.py", + "symbols": { + "get_wizzards_bp": "get_wizzards_bp" + } + } + } + }, + "WizardDetailView": { + "id": "WizardDetailView", + "type": "class", + "positions": [ + { + "start": { + "index": 444, + "row": 17, + "column": 0 + }, + "end": { + "index": 603, + "row": 20, + "column": 21 + } + } + ], + "description": "", + "metrics": { + "linesCount": 4, + "codeLineCount": 4, + "characterCount": 159, + "codeCharacterCount": 136, + "dependencyCount": 2, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "api/wizards/services.py": { + "id": "api/wizards/services.py", + "isExternal": false, + "symbols": { + "WizardService": "WizardService" + } + }, + "flask.views": { + "id": "flask.views", + "isExternal": true, + "symbols": { + "MethodView": "MethodView" + } + } + }, + "dependents": { + "api/wizards/views.py": { + "id": "api/wizards/views.py", + "symbols": { + "get_wizzards_bp": "get_wizzards_bp" + } + } + } + }, + "WizardListView": { + "id": "WizardListView", + "type": "class", + "positions": [ + { + "start": { + "index": 109, + "row": 4, + "column": 0 + }, + "end": { + "index": 249, + "row": 7, + "column": 22 + } + } + ], + "description": "", + "metrics": { + "linesCount": 4, + "codeLineCount": 4, + "characterCount": 140, + "codeCharacterCount": 117, + "dependencyCount": 2, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "api/wizards/services.py": { + "id": "api/wizards/services.py", + "isExternal": false, + "symbols": { + "WizardService": "WizardService" + } + }, + "flask.views": { + "id": "flask.views", + "isExternal": true, + "symbols": { + "MethodView": "MethodView" + } + } + }, + "dependents": { + "api/wizards/views.py": { + "id": "api/wizards/views.py", + "symbols": { + "get_wizzards_bp": "get_wizzards_bp" + } + } + } + }, + "WizardUpdateView": { + "id": "WizardUpdateView", + "type": "class", + "positions": [ + { + "start": { + "index": 606, + "row": 23, + "column": 0 + }, + "end": { + "index": 824, + "row": 27, + "column": 29 + } + } + ], + "description": "", + "metrics": { + "linesCount": 5, + "codeLineCount": 5, + "characterCount": 218, + "codeCharacterCount": 186, + "dependencyCount": 3, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "api/wizards/services.py": { + "id": "api/wizards/services.py", + "isExternal": false, + "symbols": { + "WizardService": "WizardService" + } + }, + "flask.views": { + "id": "flask.views", + "isExternal": true, + "symbols": { + "MethodView": "MethodView" + } + }, + "flask": { + "id": "flask", + "isExternal": true, + "symbols": { + "request": "request" + } + } + }, + "dependents": { + "api/wizards/views.py": { + "id": "api/wizards/views.py", + "symbols": { + "get_wizzards_bp": "get_wizzards_bp" + } + } + } + } + } + }, + "app.py": { + "id": "app.py", + "filePath": "app.py", + "metrics": { + "linesCount": 17, + "codeLineCount": 17, + "characterCount": 698, + "codeCharacterCount": 538, + "dependencyCount": 4, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "language": "python", + "dependencies": { + "api/wizards/views.py": { + "id": "api/wizards/views.py", + "isExternal": false, + "symbols": { + "get_wizzards_bp": "get_wizzards_bp" + } + }, + "api/views/elves.py": { + "id": "api/views/elves.py", + "isExternal": false, + "symbols": { + "elves_bp": "elves_bp" + } + }, + "api/views/hobbits.py": { + "id": "api/views/hobbits.py", + "isExternal": false, + "symbols": { + "hobbits_bp": "hobbits_bp" + } + }, + "flask": { + "id": "flask", + "isExternal": true, + "symbols": { + "Flask": "Flask" + } + } + }, + "dependents": { + "app.py": { + "id": "app.py", + "symbols": { + "hello_world": "hello_world", + "liveness": "liveness", + "readiness": "readiness" + } + } + }, + "symbols": { + "app": { + "id": "app", + "type": "variable", + "positions": [ + { + "start": { + "index": 150, + "row": 5, + "column": 0 + }, + "end": { + "index": 171, + "row": 5, + "column": 21 + } + }, + { + "start": { + "index": 451, + "row": 23, + "column": 0 + }, + "end": { + "index": 519, + "row": 23, + "column": 68 + } + }, + { + "start": { + "index": 548, + "row": 26, + "column": 0 + }, + "end": { + "index": 605, + "row": 26, + "column": 57 + } + }, + { + "start": { + "index": 636, + "row": 29, + "column": 0 + }, + "end": { + "index": 697, + "row": 29, + "column": 61 + } + } + ], + "description": "", + "metrics": { + "linesCount": 4, + "codeLineCount": 4, + "characterCount": 207, + "codeCharacterCount": 207, + "dependencyCount": 4, + "dependentCount": 1, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "api/wizards/views.py": { + "id": "api/wizards/views.py", + "isExternal": false, + "symbols": { + "get_wizzards_bp": "get_wizzards_bp" + } + }, + "api/views/elves.py": { + "id": "api/views/elves.py", + "isExternal": false, + "symbols": { + "elves_bp": "elves_bp" + } + }, + "api/views/hobbits.py": { + "id": "api/views/hobbits.py", + "isExternal": false, + "symbols": { + "hobbits_bp": "hobbits_bp" + } + }, + "flask": { + "id": "flask", + "isExternal": true, + "symbols": { + "Flask": "Flask" + } + } + }, + "dependents": { + "app.py": { + "id": "app.py", + "symbols": { + "hello_world": "hello_world", + "liveness": "liveness", + "readiness": "readiness" + } + } + } + }, + "hello_world": { + "id": "hello_world", + "type": "function", + "positions": [ + { + "start": { + "index": 174, + "row": 8, + "column": 0 + }, + "end": { + "index": 246, + "row": 10, + "column": 39 + } + } + ], + "description": "", + "metrics": { + "linesCount": 3, + "codeLineCount": 3, + "characterCount": 72, + "codeCharacterCount": 66, + "dependencyCount": 1, + "dependentCount": 0, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "app.py": { + "id": "app.py", + "isExternal": false, + "symbols": { + "app": "app" + } + } + }, + "dependents": {} + }, + "liveness": { + "id": "liveness", + "type": "function", + "positions": [ + { + "start": { + "index": 249, + "row": 13, + "column": 0 + }, + "end": { + "index": 314, + "row": 15, + "column": 27 + } + } + ], + "description": "", + "metrics": { + "linesCount": 3, + "codeLineCount": 3, + "characterCount": 65, + "codeCharacterCount": 59, + "dependencyCount": 1, + "dependentCount": 0, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "app.py": { + "id": "app.py", + "isExternal": false, + "symbols": { + "app": "app" + } + } + }, + "dependents": {} + }, + "readiness": { + "id": "readiness", + "type": "function", + "positions": [ + { + "start": { + "index": 353, + "row": 18, + "column": 0 + }, + "end": { + "index": 420, + "row": 20, + "column": 27 + } + } + ], + "description": "", + "metrics": { + "linesCount": 3, + "codeLineCount": 3, + "characterCount": 67, + "codeCharacterCount": 61, + "dependencyCount": 1, + "dependentCount": 0, + "cyclomaticComplexity": 1 + }, + "dependencies": { + "app.py": { + "id": "app.py", + "isExternal": false, + "symbols": { + "app": "app" + } + } + }, + "dependents": {} + } + } + } + } +} diff --git a/examples/python/flask/.napirc b/examples/python/flask/.napirc index 1f2c6bd5..326fd1d8 100644 --- a/examples/python/flask/.napirc +++ b/examples/python/flask/.napirc @@ -8,16 +8,8 @@ "exclude": [] }, "outDir": "napi-output", - "metrics": { - "file": { - "maxChar": 100000, - "maxLine": 1000, - "maxDep": 10 - }, - "symbol": { - "maxChar": 50000, - "maxLine": 500, - "maxDep": 5 - } + "audit": { + "file": { "maxCodeChar": 5000, "maxCodeLine": 300, "maxDependency": 20, "maxCyclomaticComplexity": 50 }, + "symbol": { "maxCodeChar": 1000, "maxCodeLine": 80, "maxDependency": 10, "maxCyclomaticComplexity": 15 } } } diff --git a/src/apiService/index.ts b/src/apiService/index.ts deleted file mode 100644 index 5caa9e37..00000000 --- a/src/apiService/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { z } from "zod"; -import type { globalConfigSchema } from "../cli/middlewares/globalConfig.ts"; - -export class ApiService { - private readonly apiHost: string; - private readonly jwt: string | undefined; - private readonly token: string | undefined; - - constructor( - globalConfig: z.infer, - ) { - this.apiHost = globalConfig.apiHost; - this.token = globalConfig.token; - this.jwt = globalConfig.jwt; - } - - private getHeaders() { - const headers = new Headers(); - if (this.token) { - headers.set("x-api-token", this.token); - } else if (this.jwt) { - headers.set("Authorization", `Bearer ${this.jwt}`); - } - return headers; - } - - public async performRequest( - method: "GET" | "POST" | "PUT" | "DELETE", - path: string, - body?: object, - ) { - const response = await fetch(`${this.apiHost}${path}`, { - method, - headers: this.getHeaders(), - body: body ? JSON.stringify(body) : undefined, - }); - return response; - } -} diff --git a/src/cli/handlers/extract/index.ts b/src/cli/handlers/extract/index.ts index c1813d99..1bc979c1 100644 --- a/src/cli/handlers/extract/index.ts +++ b/src/cli/handlers/extract/index.ts @@ -10,12 +10,48 @@ import { } from "../../../helpers/fileSystem/index.ts"; import { join } from "@std/path"; import { napiConfigMiddleware } from "../../middlewares/napiConfig.ts"; -import { ApiService } from "../../../apiService/index.ts"; -import type { globalConfigSchema } from "../../middlewares/globalConfig.ts"; import type { DependencyManifest } from "../../../manifest/dependencyManifest/types.ts"; -import { isAuthenticatedMiddleware } from "../../middlewares/isAuthenticated.ts"; +import type { globalConfigSchema } from "../../middlewares/globalConfig.ts"; + +const NAPI_DIR = ".napi"; +const MANIFESTS_DIR = "manifests"; + +interface ManifestEnvelope { + id: string; + branch: string; + commitSha: string; + commitShaDate: string; + createdAt: string; + manifest: DependencyManifest; +} + +function getLatestManifestId(workdir: string): string | null { + const manifestsDir = join(workdir, NAPI_DIR, MANIFESTS_DIR); + try { + const entries: { name: string }[] = []; + for (const entry of Deno.readDirSync(manifestsDir)) { + if (entry.isFile && entry.name.endsWith(".json")) { + entries.push(entry); + } + } + if (entries.length === 0) return null; + entries.sort((a, b) => b.name.localeCompare(a.name)); + return entries[0].name.replace(".json", ""); + } catch { + return null; + } +} -// Type for the symbol option +function loadManifest(workdir: string, manifestId: string): ManifestEnvelope { + const manifestPath = join( + workdir, + NAPI_DIR, + MANIFESTS_DIR, + `${manifestId}.json`, + ); + const content = Deno.readTextFileSync(manifestPath); + return JSON.parse(content) as ManifestEnvelope; +} function builderFunction( yargs: Arguments & { @@ -24,7 +60,6 @@ function builderFunction( ) { return yargs .middleware(napiConfigMiddleware) - .middleware(isAuthenticatedMiddleware) .option("symbol", { type: "array" as const, description: @@ -35,9 +70,9 @@ function builderFunction( }) .option("manifestId", { type: "string", - description: "The manifest ID to use for the extraction", + description: + "The manifest ID to use for the extraction (defaults to latest)", requiresArg: true, - demandOption: true, }) .check( ( @@ -53,7 +88,6 @@ function builderFunction( throw new Error("At least one symbol must be specified"); } - // Validate each symbol format for (const symbolSpec of symbols) { const splitSymbol = symbolSpec.split("|"); if (splitSymbol.length !== 2) { @@ -68,56 +102,52 @@ function builderFunction( ); } -async function handler( +function handler( argv: Arguments & { globalConfig: z.infer; } & { symbol: string[]; - manifestId: string; + manifestId?: string; }, ) { const napiConfig = argv.napiConfig as z.infer; - const globalConfig = argv.globalConfig as z.infer; const start = Date.now(); console.info("🎯 Starting symbol extraction..."); try { - console.info(`📄 Fetching manifest from API (ID: ${argv.manifestId})...`); - - // Create API service instance - const apiService = new ApiService( - globalConfig, - ); - - // Fetch manifest from API - const response = await apiService.performRequest( - "GET", - `/manifests/${argv.manifestId}`, - ); + let manifestId = argv.manifestId; + + if (!manifestId) { + manifestId = getLatestManifestId(argv.workdir); + if (!manifestId) { + console.error("❌ No manifests found in .napi/manifests/"); + console.error(" Run 'napi generate' first to create a manifest."); + Deno.exit(1); + } + console.info(`📄 Using latest manifest: ${manifestId}`); + } else { + console.info(`📄 Using manifest: ${manifestId}`); + } - if (response.status !== 200) { - console.error("❌ Failed to fetch manifest from API"); - console.error(` Status: ${response.status}`); - try { - const errorBody = await response.json(); - if (errorBody.error) { - console.error(` Error: ${errorBody.error}`); - } - } catch { - // Ignore JSON parsing errors + let envelope: ManifestEnvelope; + try { + envelope = loadManifest(argv.workdir, manifestId); + } catch (error) { + console.error(`❌ Failed to load manifest: ${manifestId}`); + if (error instanceof Deno.errors.NotFound) { + console.error( + ` File not found: .napi/manifests/${manifestId}.json`, + ); + } else { + console.error( + ` Error: ${error instanceof Error ? error.message : String(error)}`, + ); } - console.error(""); - console.error("💡 Common solutions:"); - console.error(" • Check that the manifest ID is correct"); - console.error(" • Verify the project exists and you have access"); Deno.exit(1); } - const responseData = await response.json() as { - manifest: DependencyManifest; - }; - const dependencyManifest = responseData.manifest; + const dependencyManifest = envelope.manifest; console.info("🔍 Validating symbol specifications..."); const symbolsToExtract = new Map< @@ -128,7 +158,6 @@ async function handler( for (const symbolSpec of argv.symbol) { const [filePath, symbolName] = symbolSpec.split("|", 2); - // Check if the file exists in the manifest if (!dependencyManifest[filePath]) { console.warn(`⚠️ File not found in manifest: ${filePath}`); console.warn( @@ -136,16 +165,12 @@ async function handler( ); } - // Get existing entry or create new one const existingEntry = symbolsToExtract.get(filePath) || { filePath, symbols: new Set(), }; - // Add the symbol to the set existingEntry.symbols.add(symbolName); - - // Set/update the entry symbolsToExtract.set(filePath, existingEntry); } @@ -163,7 +188,7 @@ async function handler( includes: napiConfig.project.include, excludes: napiConfig.project.exclude, extensions: fileExtensions, - logMessages: false, // We'll handle our own logging + logMessages: false, }); console.info(`📁 Found ${files.size} files to process`); @@ -204,7 +229,7 @@ async function handler( " • Check your symbol specifications (format: file|symbol)", ); console.error( - " • Ensure the manifest is up to date: napi manifest generate", + " • Ensure the manifest is up to date: napi generate", ); console.error( " • Verify the specified files contain the requested symbols", diff --git a/src/cli/handlers/manifest/generate.ts b/src/cli/handlers/generate/index.ts similarity index 65% rename from src/cli/handlers/manifest/generate.ts rename to src/cli/handlers/generate/index.ts index a754e6b7..2a4ac9a2 100644 --- a/src/cli/handlers/manifest/generate.ts +++ b/src/cli/handlers/generate/index.ts @@ -11,12 +11,11 @@ import { generateDependencyManifest, } from "../../../manifest/dependencyManifest/index.ts"; import type { z } from "zod"; -import { ApiService } from "../../../apiService/index.ts"; -import { - defaultApiHost, - type globalConfigSchema, -} from "../../middlewares/globalConfig.ts"; -import { isAuthenticatedMiddleware } from "../../middlewares/isAuthenticated.ts"; +import type { globalConfigSchema } from "../../middlewares/globalConfig.ts"; +import { join } from "@std/path"; + +const NAPI_DIR = ".napi"; +const MANIFESTS_DIR = "manifests"; function builder( yargs: Arguments & { @@ -25,7 +24,6 @@ function builder( ) { return yargs .middleware(napiConfigMiddleware) - .middleware(isAuthenticatedMiddleware) .option("branch", { type: "string", description: "The branch to use for the manifest", @@ -36,7 +34,6 @@ function builder( type: "string", description: "The commit SHA date to use for the manifest", coerce: (value: string) => { - // Validate date format const date = new Date(value); if (isNaN(date.getTime())) { throw new Error( @@ -51,9 +48,6 @@ function builder( }); } -/** - * Get the current Git branch name - */ async function getGitBranch(workDir: string): Promise { try { const command = new Deno.Command("git", { @@ -67,18 +61,15 @@ async function getGitBranch(workDir: string): Promise { if (code === 0) { const branch = new TextDecoder().decode(stdout).trim(); - return branch || "main"; // fallback to 'main' if branch name is empty + return branch || "main"; } - return "main"; // fallback branch name + return "main"; } catch { - return "main"; // fallback if git command fails + return "main"; } } -/** - * Get the current Git commit hash - */ async function getGitCommitSha(workDir: string): Promise { try { const command = new Deno.Command("git", { @@ -94,15 +85,12 @@ async function getGitCommitSha(workDir: string): Promise { return new TextDecoder().decode(stdout).trim(); } - return ""; // return empty string if no commit found + return ""; } catch { - return ""; // return empty string if git command fails + return ""; } } -/** - * Get the current Git commit date in ISO format - */ async function getGitCommitDate(workDir: string): Promise { try { const command = new Deno.Command("git", { @@ -118,10 +106,22 @@ async function getGitCommitDate(workDir: string): Promise { return new TextDecoder().decode(stdout).trim(); } - return new Date().toISOString(); // fallback to current date + return new Date().toISOString(); } catch { - return new Date().toISOString(); // fallback to current date if git command fails + return new Date().toISOString(); + } +} + +function ensureManifestsDir(workdir: string): string { + const manifestsDir = join(workdir, NAPI_DIR, MANIFESTS_DIR); + try { + Deno.mkdirSync(manifestsDir, { recursive: true }); + } catch (error) { + if (!(error instanceof Deno.errors.AlreadyExists)) { + throw error; + } } + return manifestsDir; } async function handler( @@ -141,7 +141,6 @@ async function handler( let commitSha: string; let commitShaDate: string; - // Handle branch if (argv.branch) { branch = argv.branch; console.info(`🌿 Using provided branch: ${branch}`); @@ -156,7 +155,6 @@ async function handler( console.info(`🌿 Branch: ${branch}`); } - // Handle commit SHA if (argv.commitSha) { commitSha = argv.commitSha; console.info(`📝 Using provided commit: ${commitSha.substring(0, 8)}...`); @@ -173,7 +171,6 @@ async function handler( } } - // Handle commit SHA date if (argv.commitShaDate) { commitShaDate = argv.commitShaDate; console.info(`📅 Using provided commit date: ${commitShaDate}`); @@ -228,71 +225,37 @@ async function handler( argv.labelingApiKey, ); - // Upload manifest to API instead of writing to disk - const apiService = new ApiService( - globalConfig, + const manifestsDir = ensureManifestsDir(argv.workdir); + + const timestamp = Date.now(); + const shortSha = commitSha ? commitSha.substring(0, 7) : "unknown"; + const manifestId = `${timestamp}-${shortSha}`; + const manifestFileName = `${manifestId}.json`; + + const manifestEnvelope = { + id: manifestId, + branch, + commitSha, + commitShaDate, + createdAt: new Date().toISOString(), + manifest: dependencyManifest, + }; + + const manifestPath = join(manifestsDir, manifestFileName); + Deno.writeTextFileSync( + manifestPath, + JSON.stringify(manifestEnvelope, null, 2), ); - for (const projectId of napiConfig.projectIds) { - try { - const response = await apiService.performRequest( - "POST", - "/manifests", - { - projectId, - branch, - commitSha, - commitShaDate, - manifest: dependencyManifest, - }, - ); - if (response.status !== 201) { - console.error( - `❌ Failed to upload manifest to API for project id: ${projectId}`, - ); - const responseBody = await response.json(); - if (responseBody.error) { - if (responseBody.error.includes("access_disabled")) { - console.error( - "\n💳 Your workspace has been disabled you need to add or change your payment method to continue uploading manifests", - ); - console.error( - "Go to your workspace settings and add or update a payment method: https://app.nanoapi.io", - ); - } else { - console.error(` Error: ${responseBody.error}`); - } - } else { - console.error(` Status: ${response.status}`); - } - Deno.exit(1); - } - - const responseBody = await response.json() as { id: number }; - - const duration = Date.now() - start; - console.info( - `✅ Manifest uploaded successfully for project id: ${projectId} in ${duration}ms`, - ); - console.info(`📄 Generated manifest contains:`); - console.info(` • ${Object.keys(dependencyManifest).length} files`); - console.info(` • Dependencies and relationships mapped`); - - if (globalConfig.apiHost === defaultApiHost) { - console.info( - `\nView it here: https://app.nanoapi.io/projects/${projectId}/manifests/${responseBody.id}`, - ); - } - } catch (error) { - console.error( - `❌ Failed to upload manifest to API for project id: ${projectId}`, - ); - console.error( - ` Error: ${error instanceof Error ? error.message : String(error)}`, - ); - Deno.exit(1); - } - } + const duration = Date.now() - start; + console.info( + `✅ Manifest saved successfully in ${duration}ms`, + ); + console.info(`📄 Generated manifest contains:`); + console.info(` • ${Object.keys(dependencyManifest).length} files`); + console.info(` • Dependencies and relationships mapped`); + console.info(`\n💾 Saved to: ${manifestPath}`); + console.info(`🔍 View it with: napi view`); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -302,7 +265,6 @@ async function handler( console.error("💡 Common solutions:"); console.error(" • Check that your project files are accessible"); console.error(" • Verify your .napirc configuration"); - console.error(" • Ensure you're logged in (run 'napi login')"); Deno.exit(1); } @@ -310,7 +272,7 @@ async function handler( export default { command: "generate", - describe: "generate a manifest for your program", + describe: "generate a dependency manifest for your program", builder, handler, }; diff --git a/src/cli/handlers/init/index.ts b/src/cli/handlers/init/index.ts index 5844e1ef..f70073df 100644 --- a/src/cli/handlers/init/index.ts +++ b/src/cli/handlers/init/index.ts @@ -4,7 +4,7 @@ import { getConfigFromWorkDir, } from "../../middlewares/napiConfig.ts"; import { join, normalize, relative, SEPARATOR } from "@std/path"; -import z from "zod"; +import type z from "zod"; import type { localConfigSchema } from "../../middlewares/napiConfig.ts"; import pythonStdlibList from "../../../scripts/generate_python_stdlib_list/output.json" with { type: "json", @@ -17,22 +17,20 @@ import { javaLanguage, pythonLanguage, } from "../../../helpers/treeSitter/parsers.ts"; -import { ApiService } from "../../../apiService/index.ts"; import type { globalConfigSchema } from "../../middlewares/globalConfig.ts"; -import { isAuthenticatedMiddleware } from "../../middlewares/isAuthenticated.ts"; import { ANTHROPIC_PROVIDER, GOOGLE_PROVIDER, OPENAI_PROVIDER, } from "../../../manifest/dependencyManifest/labeling/model.ts"; +import { defaultAuditConfig } from "../../../manifest/auditManifest/types.ts"; function builder( yargs: Arguments & { globalConfig: z.infer; }, ) { - return yargs - .middleware(isAuthenticatedMiddleware); + return yargs; } async function handler( @@ -40,10 +38,7 @@ async function handler( globalConfig: z.infer; }, ) { - const globalConfig = argv.globalConfig as z.infer; - try { - // Check if config already exists try { if (getConfigFromWorkDir(argv.workdir)) { const confirmOverwrite = await confirm({ @@ -58,7 +53,6 @@ async function handler( console.info("🔄 Proceeding with configuration overwrite"); } } catch { - // Config doesn't exist, continue with initialization console.info( "📝 No existing valid configuration found, creating new one", ); @@ -66,13 +60,8 @@ async function handler( console.info("\n🔧 Starting interactive configuration..."); - // Generate the config using the interactive prompts - const napiConfig = await generateConfig( - argv.workdir, - globalConfig, - ); + const napiConfig = await generateConfig(argv.workdir); - // Confirm and show the config console.info("\n📋 Generated configuration:"); console.info("─".repeat(50)); console.info(JSON.stringify(napiConfig, null, 2)); @@ -88,6 +77,9 @@ async function handler( console.info("\n✅ Configuration saved successfully!"); console.info(`📄 Created: ${argv.workdir}${SEPARATOR}.napirc`); console.info("🎉 Your NanoAPI project is ready!"); + console.info("\n💡 Next steps:"); + console.info(" 1. Run: napi generate"); + console.info(" 2. Run: napi view"); } else { console.info("❌ Configuration not saved"); console.info(" Run 'napi init' again when you're ready to configure"); @@ -114,16 +106,12 @@ export default { handler, }; -/** - * Shows files that match a given glob pattern - */ function showMatchingFiles( workDir: string, pattern: string, maxFilesToShow = 10, ) { try { - // Find matching files using glob const files = globSync(pattern, { cwd: workDir, nodir: true, @@ -148,9 +136,6 @@ function showMatchingFiles( } } -/** - * Shows the final set of files that will be included after applying include and exclude patterns - */ function showFinalFileSelection( workDir: string, includePatterns: string[], @@ -158,14 +143,12 @@ function showFinalFileSelection( maxFilesToShow = 20, ) { try { - // Get all included files const files = globSync(includePatterns, { cwd: workDir, nodir: true, - ignore: excludePatterns, // Default ignores + ignore: excludePatterns, }); - // Display results console.info("\n🔍 FINAL FILE SELECTION"); console.info( `After applying all patterns, ${files.length} files will be processed:`, @@ -188,9 +171,6 @@ function showFinalFileSelection( } } -/** - * Get an overview of the project structure - */ function getProjectStructureOverview(workDir: string): string[] { const overview: string[] = []; @@ -215,7 +195,6 @@ function getProjectStructureOverview(workDir: string): string[] { traverseDirectory(fullPath, depth + 1); } } catch (error) { - // Skip entries we can't access console.info(`Could not access ${fullPath}: ${error}`); } } @@ -229,14 +208,10 @@ function getProjectStructureOverview(workDir: string): string[] { return overview; } -/** - * Utility function to collect glob patterns for include patterns - */ async function collectIncludePatterns( workDir: string, language: string, ): Promise { - // Show project structure overview const projectStructure = getProjectStructureOverview(workDir); console.info( @@ -250,18 +225,15 @@ Examples: `, ); - // Suggest intelligent defaults based on project structure const suggestedIncludes = suggestIncludePatterns(projectStructure, language); console.info("\nSuggested include patterns (based on project structure):"); suggestedIncludes.forEach((pattern) => console.info(`- ${pattern}`)); - // Show preview of files that would be included with suggested patterns console.info("\nPreview of files that would be included:"); for (const pattern of suggestedIncludes) { showMatchingFiles(workDir, pattern); } - // Prompt user to use suggested includes or customize const useSuggested = await confirm({ message: "Do you want to use the suggested include patterns?", default: true, @@ -276,7 +248,6 @@ Examples: "Please enter the glob patterns for files to include (one per line):", ); - // Start with empty pattern list let includePatterns: string[] = []; let continueAdding = true; let validSelection = false; @@ -292,10 +263,8 @@ Examples: validate: (value) => { if (!value.trim()) return "Pattern cannot be empty"; try { - // Basic validation - a more thorough validation could use a glob validation library new RegExp(value.replace(/\*\*/g, "*").replace(/\*/g, ".*")); - // Show preview of files that match this pattern console.info(`\nPreviewing files matching '${value}':`); const files = globSync(value, { cwd: workDir, @@ -338,10 +307,8 @@ Examples: console.info("\nSelected include patterns:"); includePatterns.forEach((pattern) => console.info(`- ${pattern}`)); - // Show a preview of all files that match the patterns collectively showFinalFileSelection(workDir, includePatterns, []); - // Ask user to validate the selection validSelection = await confirm({ message: "Are you satisfied with this file selection?", default: true, @@ -355,9 +322,6 @@ Examples: return includePatterns; } -/** - * Utility function to collect glob patterns for exclude patterns - */ async function collectExcludePatterns( workDir: string, includePatterns: string[], @@ -376,7 +340,6 @@ Examples: `, ); - // Suggest intelligent defaults based on include patterns const suggestedExcludes = suggestExcludePatterns( includePatterns, language, @@ -385,13 +348,11 @@ Examples: console.info("\nSuggested exclude patterns (based on included files):"); suggestedExcludes.forEach((pattern) => console.info(`- ${pattern}`)); - // Show preview of files that would be excluded with suggested patterns console.info("\nPreview of files that would be excluded:"); for (const pattern of suggestedExcludes) { showMatchingFiles(workDir, pattern); } - // Prompt user to use suggested excludes or customize const useSuggested = await confirm({ message: "Do you want to use the suggested exclude patterns?", default: true, @@ -406,7 +367,6 @@ Examples: "Please enter the glob patterns for files to exclude (one per line):", ); - // Start with empty pattern list let excludePatterns: string[] = []; let continueAdding = true; let validSelection = false; @@ -422,10 +382,8 @@ Examples: validate: (value) => { if (!value.trim()) return "Pattern cannot be empty"; try { - // Basic validation - a more thorough validation could use a glob validation library new RegExp(value.replace(/\*\*/g, "*").replace(/\*/g, ".*")); - // Show preview of files that match this pattern console.info(`\nPreviewing files matching '${value}':`); const files = globSync(value, { cwd: workDir, @@ -436,7 +394,6 @@ Examples: console.info( `Note: No files currently match the pattern '${value}'`, ); - // Allow empty matches for exclude patterns as they may be preventative return true; } @@ -467,10 +424,8 @@ Examples: console.info("\nSelected exclude patterns:"); excludePatterns.forEach((pattern) => console.info(`- ${pattern}`)); - // Show final file selection after applying include and exclude patterns showFinalFileSelection(workDir, includePatterns, excludePatterns); - // Ask user to validate the selection validSelection = await confirm({ message: "Are you satisfied with this file selection?", default: true, @@ -484,18 +439,13 @@ Examples: return excludePatterns; } -/** - * Suggest include patterns based on project structure and language - */ function suggestIncludePatterns( projectStructure: string[], language: string, ): string[] { const suggestions: string[] = []; - // Language-specific suggestions if (language === pythonLanguage) { - // Check for common Python project structures if ( projectStructure.some((entry) => entry.includes(`📂 src${SEPARATOR}`)) ) { @@ -513,13 +463,10 @@ function suggestIncludePatterns( suggestions.push(`app${SEPARATOR}**${SEPARATOR}*.py`); } } - - // If no specific directories found, suggest all Python files if (suggestions.length === 0) { suggestions.push(`**${SEPARATOR}*.py`); } } else if (language === csharpLanguage) { - // Check for common C# project structures if ( projectStructure.some((entry) => entry.includes(`📂 src${SEPARATOR}`)) ) { @@ -553,13 +500,10 @@ function suggestIncludePatterns( suggestions.push(`Services${SEPARATOR}**${SEPARATOR}*.cs`); } } - - // If no specific directories found, suggest all C# files if (suggestions.length === 0) { suggestions.push(`**${SEPARATOR}*.cs`); } } else if (language === cLanguage) { - // Check for common C project structures if ( projectStructure.some((entry) => entry.includes(`📂 src${SEPARATOR}`)) ) { @@ -587,7 +531,6 @@ function suggestIncludePatterns( suggestions.push(`**${SEPARATOR}*.h`); } } else if (language === javaLanguage) { - // Check for common Java project structures if ( projectStructure.some((entry) => entry.includes(`📂 src${SEPARATOR}`)) ) { @@ -635,9 +578,6 @@ function suggestIncludePatterns( return suggestions; } -/** - * Suggest exclude patterns based on include patterns and language - */ function suggestExcludePatterns( _includePatterns: string[], language: string, @@ -645,15 +585,12 @@ function suggestExcludePatterns( ): string[] { const suggestions: string[] = []; - // add outDir to the suggestions suggestions.push(`${outDir}${SEPARATOR}**`); - - // Common exclusions for all languages + suggestions.push(`.napi${SEPARATOR}**`); suggestions.push(`.git${SEPARATOR}**`); suggestions.push(`**${SEPARATOR}dist${SEPARATOR}**`); suggestions.push(`**${SEPARATOR}build${SEPARATOR}**`); - // Language-specific suggestions if (language === pythonLanguage) { suggestions.push(`**${SEPARATOR}__pycache__${SEPARATOR}**`); suggestions.push(`**${SEPARATOR}*.pyc`); @@ -675,7 +612,6 @@ function suggestExcludePatterns( suggestions.push(`**${SEPARATOR}*.suo`); suggestions.push(`**${SEPARATOR}.nuget${SEPARATOR}**`); suggestions.push(`**${SEPARATOR}artifacts${SEPARATOR}**`); - suggestions.push(`**${SEPARATOR}packages${SEPARATOR}**`); } else if (language === javaLanguage) { suggestions.push(`**${SEPARATOR}bin${SEPARATOR}**`); suggestions.push(`**${SEPARATOR}obj${SEPARATOR}**`); @@ -689,246 +625,9 @@ function suggestExcludePatterns( return suggestions; } -/** - * Create a new project via the API - */ -async function createNewProject(apiService: ApiService): Promise { - // Workspace selection with dynamic search - console.info("\n🏢 WORKSPACE SELECTION"); - const selectedWorkspaceId = await search({ - message: "Search for the workspace to create your project in:", - source: async (term) => { - try { - const workspacesResponse = await apiService.performRequest( - "GET", - `/workspaces?search=${ - encodeURIComponent(term || "") - }&page=1&limit=10`, - ); - - if (workspacesResponse.status !== 200) { - return []; - } - - const response = await workspacesResponse.json() as { - results: Array<{ id: number; name: string }>; - total: number; - }; - - if (response.results.length === 0) { - return [ - { - name: `No workspaces found matching "${term}"`, - value: -1, - disabled: true, - }, - ]; - } - - return response.results.map((w) => ({ - name: w.name, - value: w.id, - })); - } catch { - return [ - { - name: "Error fetching workspaces", - value: -1, - disabled: true, - }, - ]; - } - }, - }); - - console.info(`✅ Selected workspace ID: ${selectedWorkspaceId}`); - - const projectName = await input({ - message: "Enter a name for your new project:", - validate: (value) => { - if (!value.trim()) return "Project name cannot be empty"; - if (value.length > 100) { - return "Project name must be less than 100 characters"; - } - return true; - }, - }); - - const projectRepoUrl = await input({ - message: "Enter the URL of your project repository:", - validate: (value) => { - if (!value.trim()) return "Project repository URL cannot be empty"; - const result = z.string().url().safeParse(value); - if (!result.success) { - return result.error.message; - } - return true; - }, - }); - - try { - const createProjectResponse = await apiService.performRequest( - "POST", - "/projects", - { - name: projectName, - repoUrl: projectRepoUrl, - workspaceId: selectedWorkspaceId, - maxCodeCharPerSymbol: 100, - maxCodeCharPerFile: 1000, - maxCharPerSymbol: 100, - maxCharPerFile: 1000, - maxCodeLinePerSymbol: 10, - maxCodeLinePerFile: 100, - maxLinePerSymbol: 10, - maxLinePerFile: 100, - maxDependencyPerSymbol: 10, - maxDependencyPerFile: 100, - maxDependentPerSymbol: 10, - maxDependentPerFile: 100, - maxCyclomaticComplexityPerSymbol: 10, - maxCyclomaticComplexityPerFile: 100, - }, - ); - - if (createProjectResponse.status !== 201) { - let errorMessage = "Unknown error"; - try { - const responseBody = await createProjectResponse.json(); - if (responseBody.error) { - errorMessage = responseBody.error; - } - } catch { - errorMessage = `HTTP ${createProjectResponse.status}`; - } - console.error(`❌ Failed to create project: ${errorMessage}`); - Deno.exit(1); - } - - const newProject = await createProjectResponse.json() as { - id: number; - }; - console.info(`✅ Created new project: ${projectName}`); - return newProject.id; - } catch (error) { - console.error("❌ Failed to create project"); - console.error( - ` Error: ${error instanceof Error ? error.message : String(error)}`, - ); - Deno.exit(1); - } -} - -/** - * Generate a configuration object based on user input - */ export async function generateConfig( workDir: string, - globalConfig: z.infer, ): Promise> { - const apiService = new ApiService(globalConfig); - - // Project selection/creation - console.info("\n🏗️ PROJECT SETUP"); - const projectChoice = await select({ - message: - "Would you like to connect to an existing project or create a new one?", - choices: [ - { name: "Create a new project", value: "create" }, - { name: "Chose to an existing project", value: "existing" }, - ], - }); - - let selectedProjectId: number; - - if (projectChoice === "existing") { - // Fetch and select existing project with dynamic search - try { - const selectedProject = await search({ - message: "Search for your project:", - source: async (term) => { - if (!term || term.length < 1) { - // Show recent/popular projects when no search term - try { - const projectsResponse = await apiService.performRequest( - "GET", - `/projects?page=1&limit=10`, - ); - - if (projectsResponse.status !== 200) { - return []; - } - - const response = await projectsResponse.json() as { - results: Array<{ id: number; name: string }>; - total: number; - }; - - return response.results.map((p) => ({ - name: p.name, - value: p.id, - })); - } catch { - return []; - } - } - - try { - const projectsResponse = await apiService.performRequest( - "GET", - `/projects?search=${encodeURIComponent(term)}&page=1&limit=10`, - ); - - if (projectsResponse.status !== 200) { - return []; - } - - const response = await projectsResponse.json() as { - results: Array<{ id: number; name: string }>; - total: number; - }; - - if (response.results.length === 0) { - return [ - { - name: `No projects found matching "${term}"`, - value: -1, - disabled: true, - }, - ]; - } - - return response.results.map((p) => ({ - name: p.name, - value: p.id, - })); - } catch { - return [ - { - name: "Error fetching projects", - value: -1, - disabled: true, - }, - ]; - } - }, - }); - - selectedProjectId = selectedProject; - console.info(`✅ Selected project ID: ${selectedProject}`); - } catch (error) { - console.error("❌ Failed to search projects"); - console.error( - ` Error: ${error instanceof Error ? error.message : String(error)}`, - ); - Deno.exit(1); - } - } else { - // Create new project - selectedProjectId = await createNewProject(apiService); - } - - // Language selection const language = await select({ message: "Select the language of your project", choices: [ @@ -939,7 +638,6 @@ export async function generateConfig( ], }); - // Python-specific config (if Python is selected) let pythonConfig: z.infer["python"] | undefined = undefined; if (language === pythonLanguage) { @@ -959,7 +657,6 @@ export async function generateConfig( } } - // C-specific config let cConfig: z.infer["c"] | undefined = undefined; if (language === cLanguage) { const hasIncludeDirs = await confirm({ @@ -984,7 +681,6 @@ export async function generateConfig( } } - // Output directory - must be a valid directory name within the project const outDir = await input({ message: "Enter the output directory for NanoAPI artifacts", default: "napi_out", @@ -992,15 +688,12 @@ export async function generateConfig( if (!value.trim()) return "Output directory cannot be empty"; try { - // Check if the path is valid by attempting to normalize it const normalizedPath = normalize(join(workDir, value)); - // Ensure the path is within the project directory (prevent directory traversal) if (!normalizedPath.startsWith(normalize(workDir))) { return "Output directory must be within the project directory"; } - // Check if the directory exists but is a file try { const stat = Deno.statSync(normalizedPath); if (stat && !stat.isDirectory) { @@ -1022,10 +715,8 @@ export async function generateConfig( console.info("\n🔍 ANALYZING PROJECT STRUCTURE..."); - // Collect include patterns const includePatterns = await collectIncludePatterns(workDir, language); - // Collect exclude patterns const excludePatterns = await collectExcludePatterns( workDir, includePatterns, @@ -1033,10 +724,8 @@ export async function generateConfig( outDir, ); - // Show final file selection to the user showFinalFileSelection(workDir, includePatterns, excludePatterns); - // Labeling configuration console.info("\n🏷️ LABELING CONFIGURATION"); console.info( "Labeling helps categorize and organize your code dependencies using AI models.", @@ -1072,7 +761,7 @@ export async function generateConfig( const maxConcurrency = await input({ message: "Enter maximum concurrent requests (leave empty for unlimited):", validate: (value) => { - if (!value.trim()) return true; // Allow empty for unlimited + if (!value.trim()) return true; const num = parseInt(value); if (isNaN(num) || num <= 0) { return "Please enter a positive number or leave empty for unlimited"; @@ -1091,7 +780,116 @@ export async function generateConfig( console.info("✅ Labeling configuration added"); } - // Build the config object + console.info("\n📊 AUDIT THRESHOLDS"); + console.info( + "Audit thresholds flag files and symbols that exceed size or complexity limits.", + ); + + const enableAudit = await confirm({ + message: "Would you like to configure custom audit thresholds?", + default: false, + }); + + let auditConfig: + | z.infer["audit"] + | undefined = undefined; + + if (enableAudit) { + console.info( + "\n📄 File-level thresholds (defaults shown, press Enter to keep):", + ); + + const fileMaxCodeLine = await input({ + message: + `Max code lines per file [${defaultAuditConfig.file.maxCodeLine}]:`, + }); + const fileMaxCodeChar = await input({ + message: + `Max code characters per file [${defaultAuditConfig.file.maxCodeChar}]:`, + }); + const fileMaxDependency = await input({ + message: + `Max dependencies per file [${defaultAuditConfig.file.maxDependency}]:`, + }); + const fileMaxDependent = await input({ + message: + `Max dependents per file [${defaultAuditConfig.file.maxDependent}]:`, + }); + const fileMaxCyclomaticComplexity = await input({ + message: + `Max cyclomatic complexity per file [${defaultAuditConfig.file.maxCyclomaticComplexity}]:`, + }); + + console.info( + "\n🔤 Symbol-level thresholds (defaults shown, press Enter to keep):", + ); + + const symbolMaxCodeLine = await input({ + message: + `Max code lines per symbol [${defaultAuditConfig.symbol.maxCodeLine}]:`, + }); + const symbolMaxCodeChar = await input({ + message: + `Max code characters per symbol [${defaultAuditConfig.symbol.maxCodeChar}]:`, + }); + const symbolMaxDependency = await input({ + message: + `Max dependencies per symbol [${defaultAuditConfig.symbol.maxDependency}]:`, + }); + const symbolMaxDependent = await input({ + message: + `Max dependents per symbol [${defaultAuditConfig.symbol.maxDependent}]:`, + }); + const symbolMaxCyclomaticComplexity = await input({ + message: + `Max cyclomatic complexity per symbol [${defaultAuditConfig.symbol.maxCyclomaticComplexity}]:`, + }); + + const parseOptionalInt = (val: string): number | undefined => { + const trimmed = val.trim(); + if (!trimmed) return undefined; + const num = parseInt(trimmed, 10); + return isNaN(num) ? undefined : num; + }; + + const fileOverrides = { + maxCodeLine: parseOptionalInt(fileMaxCodeLine), + maxCodeChar: parseOptionalInt(fileMaxCodeChar), + maxDependency: parseOptionalInt(fileMaxDependency), + maxDependent: parseOptionalInt(fileMaxDependent), + maxCyclomaticComplexity: parseOptionalInt(fileMaxCyclomaticComplexity), + }; + + const symbolOverrides = { + maxCodeLine: parseOptionalInt(symbolMaxCodeLine), + maxCodeChar: parseOptionalInt(symbolMaxCodeChar), + maxDependency: parseOptionalInt(symbolMaxDependency), + maxDependent: parseOptionalInt(symbolMaxDependent), + maxCyclomaticComplexity: parseOptionalInt(symbolMaxCyclomaticComplexity), + }; + + const cleanObj = (obj: Record) => { + const result: Record = {}; + for (const [k, v] of Object.entries(obj)) { + if (v !== undefined) result[k] = v; + } + return Object.keys(result).length > 0 ? result : undefined; + }; + + const fileClean = cleanObj(fileOverrides); + const symbolClean = cleanObj(symbolOverrides); + + if (fileClean || symbolClean) { + auditConfig = {}; + if (fileClean) auditConfig.file = fileClean as typeof auditConfig.file; + if (symbolClean) { + auditConfig.symbol = symbolClean as typeof auditConfig.symbol; + } + } + + console.info("✅ Audit threshold configuration added"); + } + const config: z.infer = { language: language, project: { @@ -1099,23 +897,23 @@ export async function generateConfig( exclude: excludePatterns.length > 0 ? excludePatterns : undefined, }, outDir: outDir ? outDir : "napi_out", - projectIds: [selectedProjectId], }; - // Add python config if it exists if (pythonConfig) { config.python = pythonConfig; } - // Add C config if it exists if (cConfig) { config.c = cConfig; } - // Add labeling config if it exists if (labelingConfig) { config.labeling = labelingConfig; } + if (auditConfig) { + config.audit = auditConfig; + } + return config; } diff --git a/src/cli/handlers/login/index.ts b/src/cli/handlers/login/index.ts deleted file mode 100644 index 49c791fd..00000000 --- a/src/cli/handlers/login/index.ts +++ /dev/null @@ -1,141 +0,0 @@ -import type { Arguments } from "yargs-types"; -import { - defaultApiHost, - type globalConfigSchema, - setConfig, -} from "../../middlewares/globalConfig.ts"; -import z from "zod"; -import { input } from "@inquirer/prompts"; -import { ApiService } from "../../../apiService/index.ts"; - -const builder = { - "api-host": { - type: "string" as const, - describe: "API server URL to save to config", - alias: "H", - default: defaultApiHost, - }, - "api-token": { - type: "string" as const, - describe: - "API token to use for authentication. Can be generated here: https://app.nanoapi.io/profile", - alias: "T", - }, -}; - -async function handler( - argv: Arguments & { - globalConfig: z.infer; - } & { - apiHost: string; - apiToken: string | undefined; - }, -) { - const globalConfig = argv.globalConfig as z.infer; - globalConfig.apiHost = argv.apiHost; - globalConfig.token = argv.apiToken; - - if (globalConfig.token) { - const apiService = new ApiService( - globalConfig, - ); - const response = await apiService.performRequest("GET", "/auth/me"); - if (response.status !== 200) { - console.error("❌ Authentication failed"); - console.error(` Status: ${response.status}`); - console.error(" Make sure the API token is valid"); - Deno.exit(1); - } - - setConfig(globalConfig); - console.info("🔑 You are already logged in using an API token"); - Deno.exit(0); - } - - console.info( - `🎫 You are about to login to NanoAPI (${globalConfig.apiHost})`, - ); - - const email = await input({ - message: "Enter your email:", - validate: (value) => { - const result = z.string().email().safeParse(value); - if (!result.success) { - return "Please enter a valid email address"; - } - return true; - }, - }); - - const apiService = new ApiService( - globalConfig, - ); - - const requestOtpResponse = await apiService.performRequest( - "POST", - "/auth/requestOtp", - { - email, - }, - ); - - if (requestOtpResponse.status !== 200) { - let errorMessage = "Unknown error"; - const responseBody = await requestOtpResponse.json(); - if (responseBody.error) { - errorMessage = responseBody.error; - } - console.error(`Failed to request OTP: ${errorMessage}`); - Deno.exit(1); - } - - console.info(`OTP sent to ${email}`); - - const otp = await input({ - message: "Enter the OTP you received:", - validate: (value) => { - const result = z.string().min(6).max(6).refine( - (val) => !isNaN(Number(val)), - ).safeParse(value); - if (!result.success) { - return "OTP must be exactly 6 digits"; - } - return true; - }, - }); - - const verifyOtpResponse = await apiService.performRequest( - "POST", - "/auth/verifyOtp", - { - email, - otp, - }, - ); - - if (verifyOtpResponse.status !== 200) { - let errorMessage = "Unknown error"; - const responseBody = await verifyOtpResponse.json(); - if (responseBody.error) { - errorMessage = responseBody.error; - } - console.error(`Failed to verify OTP: ${errorMessage}`); - Deno.exit(1); - } - - const responseBody = await verifyOtpResponse.json() as { - token: string; - }; - - globalConfig.jwt = responseBody.token; - setConfig(globalConfig); - - console.info(`🚀 You are now logged in as ${email}!`); -} - -export default { - command: "login", - describe: "Login to NanoAPI", - builder, - handler, -}; diff --git a/src/cli/handlers/manifest/index.ts b/src/cli/handlers/manifest/index.ts deleted file mode 100644 index 994b259d..00000000 --- a/src/cli/handlers/manifest/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import generateHandler from "./generate.ts"; -import type { Arguments } from "yargs-types"; -import type { globalConfigSchema } from "../../middlewares/globalConfig.ts"; -import type { z } from "zod"; - -function builder( - yargs: Arguments & { - globalConfig: z.infer; - }, -) { - return yargs - .command(generateHandler) - .demandCommand(1, "You need to specify a valid command"); -} - -export default { - command: "manifest", - describe: "generate a manifest for your program", - builder, - handler: () => {}, -}; diff --git a/src/cli/handlers/view/index.ts b/src/cli/handlers/view/index.ts new file mode 100644 index 00000000..a9e8697f --- /dev/null +++ b/src/cli/handlers/view/index.ts @@ -0,0 +1,284 @@ +import type { Arguments } from "yargs-types"; +import type { z } from "zod"; +import type { globalConfigSchema } from "../../middlewares/globalConfig.ts"; +import { + type localConfigSchema, + napiConfigMiddleware, +} from "../../middlewares/napiConfig.ts"; +import { dirname, fromFileUrl, join } from "@std/path"; +import { generateAuditManifest } from "../../../manifest/auditManifest/index.ts"; +import { + type AuditConfig, + defaultAuditConfig, +} from "../../../manifest/auditManifest/types.ts"; +import type { DependencyManifest } from "../../../manifest/dependencyManifest/types.ts"; + +const NAPI_DIR = ".napi"; +const MANIFESTS_DIR = "manifests"; + +interface ManifestEnvelope { + id: string; + branch: string; + commitSha: string; + commitShaDate: string; + createdAt: string; + manifest: DependencyManifest; +} + +interface ManifestListItem { + id: string; + branch: string; + commitSha: string; + commitShaDate: string; + createdAt: string; + fileCount: number; +} + +function getManifestsDir(workdir: string): string { + return join(workdir, NAPI_DIR, MANIFESTS_DIR); +} + +function listManifests(workdir: string): ManifestListItem[] { + const manifestsDir = getManifestsDir(workdir); + const items: ManifestListItem[] = []; + + try { + for (const entry of Deno.readDirSync(manifestsDir)) { + if (!entry.isFile || !entry.name.endsWith(".json")) continue; + + try { + const content = Deno.readTextFileSync(join(manifestsDir, entry.name)); + const envelope = JSON.parse(content) as ManifestEnvelope; + items.push({ + id: envelope.id, + branch: envelope.branch, + commitSha: envelope.commitSha, + commitShaDate: envelope.commitShaDate, + createdAt: envelope.createdAt, + fileCount: Object.keys(envelope.manifest).length, + }); + } catch { + // Skip malformed manifest files + } + } + } catch { + // Directory doesn't exist yet + } + + items.sort((a, b) => b.createdAt.localeCompare(a.createdAt)); + return items; +} + +function loadManifest( + workdir: string, + manifestId: string, +): ManifestEnvelope | null { + const manifestPath = join( + getManifestsDir(workdir), + `${manifestId}.json`, + ); + try { + const content = Deno.readTextFileSync(manifestPath); + return JSON.parse(content) as ManifestEnvelope; + } catch { + return null; + } +} + +function getViewerDistDir(): string { + const thisDir = dirname(fromFileUrl(import.meta.url)); + return join(thisDir, "..", "..", "..", "..", "viewer", "dist"); +} + +function getContentType(path: string): string { + if (path.endsWith(".html")) return "text/html; charset=utf-8"; + if (path.endsWith(".js")) return "application/javascript; charset=utf-8"; + if (path.endsWith(".css")) return "text/css; charset=utf-8"; + if (path.endsWith(".json")) return "application/json; charset=utf-8"; + if (path.endsWith(".svg")) return "image/svg+xml"; + if (path.endsWith(".png")) return "image/png"; + if (path.endsWith(".ico")) return "image/x-icon"; + if (path.endsWith(".woff2")) return "font/woff2"; + if (path.endsWith(".woff")) return "font/woff"; + return "application/octet-stream"; +} + +async function tryServeStatic( + viewerDir: string, + pathname: string, +): Promise { + let filePath = join(viewerDir, pathname); + + try { + const stat = Deno.statSync(filePath); + if (stat.isDirectory) { + filePath = join(filePath, "index.html"); + } + } catch { + // File doesn't exist + } + + try { + const content = await Deno.readFile(filePath); + return new Response(content, { + headers: { "content-type": getContentType(filePath) }, + }); + } catch { + return null; + } +} + +function jsonResponse(data: unknown, status = 200): Response { + return new Response(JSON.stringify(data), { + status, + headers: { + "content-type": "application/json; charset=utf-8", + "access-control-allow-origin": "*", + }, + }); +} + +async function openBrowser(url: string) { + try { + let cmd: string[]; + if (Deno.build.os === "darwin") { + cmd = ["open", url]; + } else if (Deno.build.os === "windows") { + cmd = ["cmd", "/c", "start", url]; + } else { + cmd = ["xdg-open", url]; + } + const command = new Deno.Command(cmd[0], { + args: cmd.slice(1), + stdout: "null", + stderr: "null", + }); + const child = command.spawn(); + await child.status; + } catch { + // Silently fail if browser can't be opened + } +} + +function findAvailablePort(startPort: number): number { + for (let port = startPort; port < startPort + 100; port++) { + try { + const listener = Deno.listen({ port }); + listener.close(); + return port; + } catch { + continue; + } + } + throw new Error("No available port found"); +} + +function mergeAuditConfig( + userAudit?: z.infer["audit"], +): AuditConfig { + return { + file: { + ...defaultAuditConfig.file, + ...userAudit?.file, + }, + symbol: { + ...defaultAuditConfig.symbol, + ...userAudit?.symbol, + }, + }; +} + +function builder( + yargs: Arguments & { + globalConfig: z.infer; + }, +) { + return yargs + .middleware(napiConfigMiddleware) + .option("port", { + type: "number", + description: "Port to serve the viewer on", + default: 3000, + }); +} + +async function handler( + argv: Arguments & { + globalConfig: z.infer; + napiConfig: z.infer; + } & { + port: number; + }, +) { + const workdir = argv.workdir as string; + const viewerDir = getViewerDistDir(); + const auditConfig = mergeAuditConfig(argv.napiConfig?.audit); + + const port = findAvailablePort(argv.port); + + const handler = async (request: Request): Promise => { + const url = new URL(request.url); + const pathname = url.pathname; + + if (pathname === "/api/manifests" && request.method === "GET") { + const manifests = listManifests(workdir); + return jsonResponse(manifests); + } + + const manifestDetailMatch = pathname.match( + /^\/api\/manifests\/([^/]+)$/, + ); + if (manifestDetailMatch && request.method === "GET") { + const manifestId = manifestDetailMatch[1]; + const envelope = loadManifest(workdir, manifestId); + if (!envelope) { + return jsonResponse({ error: "Manifest not found" }, 404); + } + return jsonResponse(envelope); + } + + const auditMatch = pathname.match( + /^\/api\/manifests\/([^/]+)\/audit$/, + ); + if (auditMatch && request.method === "GET") { + const manifestId = auditMatch[1]; + const envelope = loadManifest(workdir, manifestId); + if (!envelope) { + return jsonResponse({ error: "Manifest not found" }, 404); + } + const auditManifest = generateAuditManifest( + envelope.manifest, + auditConfig, + ); + return jsonResponse(auditManifest); + } + + // Serve static files from viewer dist + const staticResponse = await tryServeStatic(viewerDir, pathname); + if (staticResponse) return staticResponse; + + // SPA fallback: serve index.html for any unmatched route + const indexResponse = await tryServeStatic(viewerDir, "/index.html"); + if (indexResponse) return indexResponse; + + return new Response("Not Found", { status: 404 }); + }; + + console.info(`🚀 Starting napi viewer...`); + console.info( + `📂 Serving manifests from: ${join(workdir, NAPI_DIR, MANIFESTS_DIR)}`, + ); + console.info(`🌐 Open: http://localhost:${port}`); + console.info(`\nPress Ctrl+C to stop.\n`); + + await openBrowser(`http://localhost:${port}`); + + Deno.serve({ port }, handler); +} + +export default { + command: "view", + describe: "open the dependency visualizer in your browser", + builder, + handler, +}; diff --git a/src/cli/index.ts b/src/cli/index.ts index d8dfbfae..5c5a1269 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -3,10 +3,10 @@ import { checkVersionMiddleware, getCurrentVersion, } from "./middlewares/checkVersion.ts"; -import loginCommand from "./handlers/login/index.ts"; import initCommand from "./handlers/init/index.ts"; import setCommand from "./handlers/set/index.ts"; -import manifestCommand from "./handlers/manifest/index.ts"; +import generateCommand from "./handlers/generate/index.ts"; +import viewCommand from "./handlers/view/index.ts"; import extractCommand from "./handlers/extract/index.ts"; import { globalConfigMiddleware } from "./middlewares/globalConfig.ts"; @@ -26,10 +26,10 @@ export function initCli() { .options(globalOptions) .middleware(checkVersionMiddleware) .middleware(globalConfigMiddleware) - .command(loginCommand) .command(initCommand) .command(setCommand) - .command(manifestCommand) + .command(generateCommand) + .command(viewCommand) .command(extractCommand) .demandCommand(1, "You need to specify a command") .strict() diff --git a/src/cli/middlewares/globalConfig.ts b/src/cli/middlewares/globalConfig.ts index f8a64c9d..d6e980b1 100644 --- a/src/cli/middlewares/globalConfig.ts +++ b/src/cli/middlewares/globalConfig.ts @@ -3,9 +3,6 @@ import { dirname, join } from "@std/path"; import z from "zod"; export const globalConfigSchema = z.object({ - jwt: z.string().optional(), - token: z.string().optional(), - apiHost: z.string(), labeling: z.object({ apiKeys: z.object({ google: z.string().optional(), @@ -15,11 +12,7 @@ export const globalConfigSchema = z.object({ }).optional(), }); -export const defaultApiHost = "https://api.nanoapi.io"; - -const defaultConfig: z.infer = { - apiHost: defaultApiHost, -}; +const defaultConfig: z.infer = {}; function getConfigPath() { const appName = "napi"; diff --git a/src/cli/middlewares/isAuthenticated.ts b/src/cli/middlewares/isAuthenticated.ts deleted file mode 100644 index 3cd0c63d..00000000 --- a/src/cli/middlewares/isAuthenticated.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { Arguments } from "yargs-types"; -import type { z } from "zod"; -import type { globalConfigSchema } from "./globalConfig.ts"; -import { ApiService } from "../../apiService/index.ts"; - -export async function isAuthenticatedMiddleware( - args: Arguments & { - globalConfig: z.infer; - }, -) { - const globalConfig = args.globalConfig as z.infer; - - if (!globalConfig.jwt) { - console.error("❌ Not logged in"); - console.error(" Please login first using: napi login"); - Deno.exit(1); - } - - try { - const apiService = new ApiService( - globalConfig, - ); - const response = await apiService.performRequest("GET", "/auth/me"); - - if (response.status === 401) { - console.error("❌ Authentication expired"); - console.error(" Please login again using: napi login"); - Deno.exit(1); - } - - if (response.status !== 200) { - console.error("❌ Authentication check failed"); - console.error(` Status: ${response.status}`); - console.error(" Please try logging in again using: napi login"); - Deno.exit(1); - } - - console.info("✅ Authentication verified"); - } catch (error) { - console.error("❌ Failed to verify authentication"); - console.error( - ` Error: ${error instanceof Error ? error.message : String(error)}`, - ); - console.error(" Please check your connection and try logging in again"); - Deno.exit(1); - } -} diff --git a/src/cli/middlewares/napiConfig.ts b/src/cli/middlewares/napiConfig.ts index 7192139c..e6abf0f6 100644 --- a/src/cli/middlewares/napiConfig.ts +++ b/src/cli/middlewares/napiConfig.ts @@ -19,7 +19,6 @@ import { const pythonVersions = Object.keys(pythonStdlibList); export const localConfigSchema = z.object({ - projectIds: z.array(z.number().int()), language: z.enum([pythonLanguage, csharpLanguage, cLanguage, javaLanguage]), [pythonLanguage]: z .object({ @@ -51,6 +50,26 @@ export const localConfigSchema = z.object({ ]), maxConcurrency: z.number().optional(), }).optional(), + audit: z.object({ + file: z.object({ + maxCodeChar: z.number().optional(), + maxChar: z.number().optional(), + maxCodeLine: z.number().optional(), + maxLine: z.number().optional(), + maxDependency: z.number().optional(), + maxDependent: z.number().optional(), + maxCyclomaticComplexity: z.number().optional(), + }).optional(), + symbol: z.object({ + maxCodeChar: z.number().optional(), + maxChar: z.number().optional(), + maxCodeLine: z.number().optional(), + maxLine: z.number().optional(), + maxDependency: z.number().optional(), + maxDependent: z.number().optional(), + maxCyclomaticComplexity: z.number().optional(), + }).optional(), + }).optional(), }); const napiConfigFileName = ".napirc"; diff --git a/src/helpers/treeSitter/parsers.ts b/src/helpers/treeSitter/parsers.ts index bc04dfb3..5d267a36 100644 --- a/src/helpers/treeSitter/parsers.ts +++ b/src/helpers/treeSitter/parsers.ts @@ -1,8 +1,8 @@ import Parser, { type Language } from "tree-sitter"; -import Python from "npm:tree-sitter-python"; -import CSharp from "npm:tree-sitter-c-sharp"; +import Python from "tree-sitter-python"; +import CSharp from "tree-sitter-c-sharp"; import C from "tree-sitter-c"; -import Java from "npm:tree-sitter-java"; +import Java from "tree-sitter-java"; const pythonParser = new Parser(); pythonParser.setLanguage(Python as Language); diff --git a/src/languagePlugins/c/testFiles/index.ts b/src/languagePlugins/c/testFiles/index.ts index 89254685..5aa4a0cf 100644 --- a/src/languagePlugins/c/testFiles/index.ts +++ b/src/languagePlugins/c/testFiles/index.ts @@ -66,7 +66,6 @@ export function getCFilesContentMap(): Map< } export const dummyLocalConfig = { - projectIds: [1], language: cLanguage, project: { include: [], diff --git a/src/languagePlugins/java/dependencyFormatting/index.ts b/src/languagePlugins/java/dependencyFormatting/index.ts index 5c4645de..3d601a78 100644 --- a/src/languagePlugins/java/dependencyFormatting/index.ts +++ b/src/languagePlugins/java/dependencyFormatting/index.ts @@ -1,4 +1,4 @@ -import type Parser from "npm:tree-sitter"; +import type Parser from "tree-sitter"; import { javaParser } from "../../../helpers/treeSitter/parsers.ts"; import { JavaImportResolver } from "../importResolver/index.ts"; import { JavaInvocationResolver } from "../invocationResolver/index.ts"; diff --git a/src/languagePlugins/java/dependencyFormatting/types.ts b/src/languagePlugins/java/dependencyFormatting/types.ts index 990c3406..086bb1f7 100644 --- a/src/languagePlugins/java/dependencyFormatting/types.ts +++ b/src/languagePlugins/java/dependencyFormatting/types.ts @@ -1,5 +1,5 @@ import type { SymbolType } from "../packageResolver/types.ts"; -import type Parser from "npm:tree-sitter"; +import type Parser from "tree-sitter"; /** * Represents a dependency in a Java file diff --git a/src/languagePlugins/java/invocationResolver/queries.ts b/src/languagePlugins/java/invocationResolver/queries.ts index 8d0e0be5..cdffd271 100644 --- a/src/languagePlugins/java/invocationResolver/queries.ts +++ b/src/languagePlugins/java/invocationResolver/queries.ts @@ -1,4 +1,4 @@ -import Parser from "npm:tree-sitter"; +import Parser from "tree-sitter"; import { javaParser } from "../../../helpers/treeSitter/parsers.ts"; /** diff --git a/src/languagePlugins/java/packageResolver/queries.ts b/src/languagePlugins/java/packageResolver/queries.ts index 0fa00252..f6808744 100644 --- a/src/languagePlugins/java/packageResolver/queries.ts +++ b/src/languagePlugins/java/packageResolver/queries.ts @@ -1,4 +1,4 @@ -import Parser from "npm:tree-sitter"; +import Parser from "tree-sitter"; import { javaParser } from "../../../helpers/treeSitter/parsers.ts"; /** diff --git a/src/languagePlugins/python/symbolExtractor/index.test.ts b/src/languagePlugins/python/symbolExtractor/index.test.ts index 25ac3276..1b3cc04c 100644 --- a/src/languagePlugins/python/symbolExtractor/index.test.ts +++ b/src/languagePlugins/python/symbolExtractor/index.test.ts @@ -41,7 +41,6 @@ describe("PythonSymbolExtractor", () => { files: Map, ): DependencyManifest { const dependencyManifest = generatePythonDependencyManifest(files, { - projectIds: [1], language: pythonLanguage, python: { version: "3.10", diff --git a/src/manifest/auditManifest/index.ts b/src/manifest/auditManifest/index.ts new file mode 100644 index 00000000..893baa68 --- /dev/null +++ b/src/manifest/auditManifest/index.ts @@ -0,0 +1,257 @@ +import type { DependencyManifest } from "../dependencyManifest/types.ts"; +import { + metricCharacterCount, + metricCodeCharacterCount, + metricCodeLineCount, + metricCyclomaticComplexity, + metricDependencyCount, + metricDependentCount, + metricLinesCount, +} from "../dependencyManifest/types.ts"; +import type { + AuditConfig, + AuditManifest, + FileAuditManifest, + SymbolAuditManifest, +} from "./types.ts"; + +function getSeverityLevel( + value: number, + targetValue = 0, +): 1 | 2 | 3 | 4 | 5 { + if (value > targetValue * 10) { + return 5; + } else if (value > targetValue * 5) { + return 4; + } else if (value > targetValue * 2) { + return 3; + } else if (value > targetValue * 1.5) { + return 2; + } else { + return 1; + } +} + +export function generateAuditManifest( + dependencyManifest: DependencyManifest, + config: AuditConfig, +): AuditManifest { + const auditManifest: AuditManifest = {}; + + for (const fileDependencyManifest of Object.values(dependencyManifest)) { + const fileAudit: FileAuditManifest = { + id: fileDependencyManifest.id, + alerts: {}, + symbols: {}, + }; + + const fm = fileDependencyManifest.metrics; + + if (fm.codeCharacterCount > config.file.maxCodeChar) { + fileAudit.alerts[metricCodeCharacterCount] = { + metric: metricCodeCharacterCount, + severity: getSeverityLevel( + fm.codeCharacterCount, + config.file.maxCodeChar, + ), + message: { + short: "File too large", + long: + `File exceeds maximum character limit (${fm.codeCharacterCount}/${config.file.maxCodeChar})`, + }, + }; + } + + if (fm.characterCount > config.file.maxChar) { + fileAudit.alerts[metricCharacterCount] = { + metric: metricCharacterCount, + severity: getSeverityLevel(fm.characterCount, config.file.maxChar), + message: { + short: "File too large", + long: + `File exceeds maximum character limit (${fm.characterCount}/${config.file.maxChar})`, + }, + }; + } + + if (fm.codeLineCount > config.file.maxCodeLine) { + fileAudit.alerts[metricCodeLineCount] = { + metric: metricCodeLineCount, + severity: getSeverityLevel(fm.codeLineCount, config.file.maxCodeLine), + message: { + short: "Too many lines", + long: + `File exceeds maximum line count (${fm.codeLineCount}/${config.file.maxCodeLine})`, + }, + }; + } + + if (fm.linesCount > config.file.maxLine) { + fileAudit.alerts[metricLinesCount] = { + metric: metricLinesCount, + severity: getSeverityLevel(fm.linesCount, config.file.maxLine), + message: { + short: "Too many lines", + long: + `File exceeds maximum line count (${fm.linesCount}/${config.file.maxLine})`, + }, + }; + } + + if (fm.dependencyCount > config.file.maxDependency) { + fileAudit.alerts[metricDependencyCount] = { + metric: metricDependencyCount, + severity: getSeverityLevel( + fm.dependencyCount, + config.file.maxDependency, + ), + message: { + short: "Too many dependencies", + long: + `File exceeds maximum dependency count (${fm.dependencyCount}/${config.file.maxDependency})`, + }, + }; + } + + if (fm.dependentCount > config.file.maxDependent) { + fileAudit.alerts[metricDependentCount] = { + metric: metricDependentCount, + severity: getSeverityLevel(fm.dependentCount, config.file.maxDependent), + message: { + short: "Too many dependents", + long: + `File exceeds maximum dependent count (${fm.dependentCount}/${config.file.maxDependent})`, + }, + }; + } + + if (fm.cyclomaticComplexity > config.file.maxCyclomaticComplexity) { + fileAudit.alerts[metricCyclomaticComplexity] = { + metric: metricCyclomaticComplexity, + severity: getSeverityLevel( + fm.cyclomaticComplexity, + config.file.maxCyclomaticComplexity, + ), + message: { + short: "Too complex", + long: + `File exceeds maximum cyclomatic complexity (${fm.cyclomaticComplexity}/${config.file.maxCyclomaticComplexity})`, + }, + }; + } + + for (const symbol of Object.values(fileDependencyManifest.symbols)) { + const symbolAudit: SymbolAuditManifest = { + id: symbol.id, + alerts: {}, + }; + + const sm = symbol.metrics; + + if (sm.codeCharacterCount > config.symbol.maxCodeChar) { + symbolAudit.alerts[metricCodeCharacterCount] = { + metric: metricCodeCharacterCount, + severity: getSeverityLevel( + sm.codeCharacterCount, + config.symbol.maxCodeChar, + ), + message: { + short: "Symbol too large", + long: + `Symbol exceeds maximum character limit (${sm.codeCharacterCount}/${config.symbol.maxCodeChar})`, + }, + }; + } + + if (sm.characterCount > config.symbol.maxChar) { + symbolAudit.alerts[metricCharacterCount] = { + metric: metricCharacterCount, + severity: getSeverityLevel(sm.characterCount, config.symbol.maxChar), + message: { + short: "Symbol too large", + long: + `Symbol exceeds maximum character limit (${sm.characterCount}/${config.symbol.maxChar})`, + }, + }; + } + + if (sm.codeLineCount > config.symbol.maxCodeLine) { + symbolAudit.alerts[metricCodeLineCount] = { + metric: metricCodeLineCount, + severity: getSeverityLevel( + sm.codeLineCount, + config.symbol.maxCodeLine, + ), + message: { + short: "Symbol too long", + long: + `Symbol exceeds maximum line count (${sm.codeLineCount}/${config.symbol.maxCodeLine})`, + }, + }; + } + + if (sm.linesCount > config.symbol.maxLine) { + symbolAudit.alerts[metricLinesCount] = { + metric: metricLinesCount, + severity: getSeverityLevel(sm.linesCount, config.symbol.maxLine), + message: { + short: "Symbol too long", + long: + `Symbol exceeds maximum line count (${sm.linesCount}/${config.symbol.maxLine})`, + }, + }; + } + + if (sm.dependencyCount > config.symbol.maxDependency) { + symbolAudit.alerts[metricDependencyCount] = { + metric: metricDependencyCount, + severity: getSeverityLevel( + sm.dependencyCount, + config.symbol.maxDependency, + ), + message: { + short: "Too many dependencies", + long: + `Symbol exceeds maximum dependency count (${sm.dependencyCount}/${config.symbol.maxDependency})`, + }, + }; + } + + if (sm.dependentCount > config.symbol.maxDependent) { + symbolAudit.alerts[metricDependentCount] = { + metric: metricDependentCount, + severity: getSeverityLevel( + sm.dependentCount, + config.symbol.maxDependent, + ), + message: { + short: "Too many dependents", + long: + `Symbol exceeds maximum dependent count (${sm.dependentCount}/${config.symbol.maxDependent})`, + }, + }; + } + + if (sm.cyclomaticComplexity > config.symbol.maxCyclomaticComplexity) { + symbolAudit.alerts[metricCyclomaticComplexity] = { + metric: metricCyclomaticComplexity, + severity: getSeverityLevel( + sm.cyclomaticComplexity, + config.symbol.maxCyclomaticComplexity, + ), + message: { + short: "Symbol too complex", + long: + `Symbol exceeds maximum cyclomatic complexity (${sm.cyclomaticComplexity}/${config.symbol.maxCyclomaticComplexity})`, + }, + }; + } + + fileAudit.symbols[symbol.id] = symbolAudit; + } + + auditManifest[fileDependencyManifest.id] = fileAudit; + } + + return auditManifest; +} diff --git a/src/manifest/auditManifest/types.ts b/src/manifest/auditManifest/types.ts new file mode 100644 index 00000000..b41fc5bd --- /dev/null +++ b/src/manifest/auditManifest/types.ts @@ -0,0 +1,65 @@ +import type { Metric } from "../dependencyManifest/types.ts"; + +export type AuditAlert = { + metric: Metric; + severity: number; + message: { + short: string; + long: string; + }; +}; + +export type SymbolAuditManifest = { + id: string; + alerts: Record; +}; + +export type FileAuditManifest = { + id: string; + alerts: Record; + symbols: Record; +}; + +export type AuditManifest = Record; + +export interface AuditConfig { + file: { + maxCodeChar: number; + maxChar: number; + maxCodeLine: number; + maxLine: number; + maxDependency: number; + maxDependent: number; + maxCyclomaticComplexity: number; + }; + symbol: { + maxCodeChar: number; + maxChar: number; + maxCodeLine: number; + maxLine: number; + maxDependency: number; + maxDependent: number; + maxCyclomaticComplexity: number; + }; +} + +export const defaultAuditConfig: AuditConfig = { + file: { + maxCodeChar: 1000, + maxChar: 1000, + maxCodeLine: 100, + maxLine: 100, + maxDependency: 100, + maxDependent: 100, + maxCyclomaticComplexity: 100, + }, + symbol: { + maxCodeChar: 100, + maxChar: 100, + maxCodeLine: 10, + maxLine: 10, + maxDependency: 10, + maxDependent: 10, + maxCyclomaticComplexity: 10, + }, +}; diff --git a/viewer/.gitignore b/viewer/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/viewer/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/viewer/README.md b/viewer/README.md new file mode 100644 index 00000000..7dbf7ebf --- /dev/null +++ b/viewer/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/viewer/index.html b/viewer/index.html new file mode 100644 index 00000000..f8f67ac1 --- /dev/null +++ b/viewer/index.html @@ -0,0 +1,12 @@ + + + + + + NanoAPI Viewer + + +
+ + + diff --git a/viewer/src/App.tsx b/viewer/src/App.tsx new file mode 100644 index 00000000..ca5b2db3 --- /dev/null +++ b/viewer/src/App.tsx @@ -0,0 +1,20 @@ +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { ThemeProvider } from "./contexts/ThemeProvider"; +import { TooltipProvider } from "./components/shadcn/Tooltip"; +import ManifestList from "./pages/ManifestList"; +import ManifestView from "./pages/ManifestView"; + +export default function App() { + return ( + + + + + } /> + } /> + + + + + ); +} diff --git a/viewer/src/api.ts b/viewer/src/api.ts new file mode 100644 index 00000000..54f4f095 --- /dev/null +++ b/viewer/src/api.ts @@ -0,0 +1,39 @@ +import type { DependencyManifest } from "./types/dependencyManifest"; +import type { AuditManifest } from "./types/auditManifest"; + +export interface ManifestListItem { + id: string; + branch: string; + commitSha: string; + commitShaDate: string; + createdAt: string; + fileCount: number; +} + +export interface ManifestEnvelope { + id: string; + branch: string; + commitSha: string; + commitShaDate: string; + createdAt: string; + manifest: DependencyManifest; +} + +const BASE = "/api"; + +export async function fetchManifests(): Promise { + const res = await fetch(`${BASE}/manifests`); + return res.json(); +} + +export async function fetchManifest(id: string): Promise { + const res = await fetch(`${BASE}/manifests/${id}`); + if (!res.ok) throw new Error("Manifest not found"); + return res.json(); +} + +export async function fetchAudit(id: string): Promise { + const res = await fetch(`${BASE}/manifests/${id}/audit`); + if (!res.ok) throw new Error("Audit not found"); + return res.json(); +} diff --git a/viewer/src/components/DependencyVisualizer/DependencyVisualizer.tsx b/viewer/src/components/DependencyVisualizer/DependencyVisualizer.tsx new file mode 100644 index 00000000..99ff1a31 --- /dev/null +++ b/viewer/src/components/DependencyVisualizer/DependencyVisualizer.tsx @@ -0,0 +1,135 @@ +import { useState } from "react"; +import { useSearchParams } from "react-router-dom"; +import type { DependencyManifest } from "../../types/dependencyManifest.ts"; +import type { AuditManifest } from "../../types/auditManifest.ts"; +import { SidebarProvider, SidebarTrigger } from "../shadcn/Sidebar.tsx"; +import { + FileExplorerSidebar, + type ExplorerNodeData, +} from "./components/FileExplorerSidebar.tsx"; +import BreadcrumbNav from "./components/BreadcrumbNav.tsx"; +import ProjectVisualizer from "./visualizers/ProjectVisualizer.tsx"; +import FileVisualizer from "./visualizers/FileVisualizer.tsx"; +import SymbolVisualizer from "./visualizers/SymbolVisualizer.tsx"; + +export default function DependencyVisualizer(props: { + manifestId: string; + dependencyManifest: DependencyManifest; + auditManifest: AuditManifest; +}) { + const [searchParams] = useSearchParams(); + const [highlightedCytoscapeRef, setHighlightedCytoscapeRef] = useState< + { filePath: string; symbolId: string | undefined } | undefined + >(undefined); + + function handleHighlight(node: ExplorerNodeData) { + if (!node.fileId) return; + const newRef = { + filePath: node.fileId, + symbolId: node.symbolId, + }; + if ( + highlightedCytoscapeRef?.filePath === newRef.filePath && + highlightedCytoscapeRef?.symbolId === newRef.symbolId + ) { + setHighlightedCytoscapeRef(undefined); + } else { + setHighlightedCytoscapeRef(newRef); + } + } + + function toDetails(node: ExplorerNodeData) { + if (node.symbolId && node.fileId) { + const p = new URLSearchParams(searchParams); + p.set("fileId", node.fileId); + p.set("instanceId", node.symbolId); + return `?${p.toString()}`; + } else if (node.fileId) { + const p = new URLSearchParams(searchParams); + p.set("fileId", node.fileId); + p.delete("instanceId"); + return `?${p.toString()}`; + } else { + const p = new URLSearchParams(searchParams); + p.delete("fileId"); + p.delete("instanceId"); + return `?${p.toString()}`; + } + } + + const fileId = searchParams.get("fileId"); + const instanceId = searchParams.get("instanceId"); + + return ( + + +
+
+
+ + { + const p = new URLSearchParams(searchParams); + p.delete("fileId"); + p.delete("instanceId"); + return `?${p.toString()}`; + }} + fileId={fileId} + toFileIdLink={(fId) => { + const p = new URLSearchParams(searchParams); + p.set("fileId", fId); + p.delete("instanceId"); + return `?${p.toString()}`; + }} + instanceId={instanceId} + toInstanceIdLink={(fId, iId) => { + const p = new URLSearchParams(searchParams); + p.set("fileId", fId); + p.set("instanceId", iId); + return `?${p.toString()}`; + }} + /> +
+
+
+ {fileId && instanceId ? ( + + ) : fileId ? ( + + ) : ( + + )} +
+
+
+ ); +} diff --git a/viewer/src/components/DependencyVisualizer/components/BreadcrumbNav.tsx b/viewer/src/components/DependencyVisualizer/components/BreadcrumbNav.tsx new file mode 100644 index 00000000..0f7eca27 --- /dev/null +++ b/viewer/src/components/DependencyVisualizer/components/BreadcrumbNav.tsx @@ -0,0 +1,57 @@ +import { Link } from "react-router-dom"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbSeparator, +} from "../../shadcn/Breadcrumb.tsx"; + +export default function BreadcrumbNav(props: { + toProjectLink: () => string; + fileId: string | null; + toFileIdLink: (fileId: string) => string; + instanceId: string | null; + toInstanceIdLink: (fileId: string, instanceId: string) => string; +}) { + return ( + + + + + Project + + + {props.fileId && ( + <> + + + + + {props.fileId} + + + + {props.instanceId && ( + <> + + + + + {props.instanceId} + + + + + )} + + )} + + + ); +} diff --git a/viewer/src/components/DependencyVisualizer/components/DisplayNameWithTooltip.tsx b/viewer/src/components/DependencyVisualizer/components/DisplayNameWithTooltip.tsx new file mode 100644 index 00000000..c043780a --- /dev/null +++ b/viewer/src/components/DependencyVisualizer/components/DisplayNameWithTooltip.tsx @@ -0,0 +1,31 @@ +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "../../shadcn/Tooltip.tsx"; + +export default function DisplayNameWithTooltip(props: { + name: string; + maxChar?: number; + truncateBefore?: boolean; +}) { + const maxChar = props.maxChar || 30; + + if (props.name.length > maxChar) { + const displayedName = props.truncateBefore + ? "..." + props.name.slice(0, maxChar) + : props.name.slice(0, maxChar) + "..."; + + return ( + + + {displayedName} + + +
{props.name}
+
+
+ ); + } + return {props.name}; +} diff --git a/viewer/src/components/DependencyVisualizer/components/FileExplorerSidebar.tsx b/viewer/src/components/DependencyVisualizer/components/FileExplorerSidebar.tsx new file mode 100644 index 00000000..0323f689 --- /dev/null +++ b/viewer/src/components/DependencyVisualizer/components/FileExplorerSidebar.tsx @@ -0,0 +1,426 @@ +import { useEffect, useMemo, useState } from "react"; +import { Link } from "react-router-dom"; +import type { DependencyManifest } from "../../../types/dependencyManifest.ts"; +import type { AuditManifest } from "../../../types/auditManifest.ts"; +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarHeader, + SidebarRail, +} from "../../shadcn/Sidebar.tsx"; +import { Button } from "../../shadcn/Button.tsx"; +import { Input } from "../../shadcn/Input.tsx"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "../../shadcn/Tooltip.tsx"; +import { + ChevronDown, + ChevronRight, + Code, + File, + FolderClosed, + FolderOpen, + ScanEye, + Search, + SearchCode, + X, +} from "lucide-react"; +import { ScrollArea, ScrollBar } from "../../shadcn/Scrollarea.tsx"; +import DisplayNameWithTooltip from "./DisplayNameWithTooltip.tsx"; + +export interface ExplorerNodeData { + id: string; + displayName: string; + fileId?: string; + symbolId?: string; + children: Map; +} + +function buildExplorerTree( + dependencyManifest: DependencyManifest, + filteredSymbols: { fileId: string; symbolId: string }[], +): ExplorerNodeData | undefined { + const getExplorerNodeId = (filePath: string, instanceId?: string) => { + if (instanceId) { + return `${filePath}#${instanceId}`; + } + return filePath; + }; + + const root: ExplorerNodeData = { + id: "root", + displayName: "Project", + children: new Map(), + }; + + const filteredSymbolsSet = new Set( + filteredSymbols.map((s) => `${s.fileId}#${s.symbolId}`), + ); + + const filesWithFilteredSymbols = new Set( + filteredSymbols.map((s) => s.fileId), + ); + + const shouldShowAll = filteredSymbols.length === 0; + + let hasMatchingNodes = false; + + for (const fileDependencyManifest of Object.values(dependencyManifest)) { + const filePath = fileDependencyManifest.filePath; + + const fileShouldBeIncluded = shouldShowAll || + filesWithFilteredSymbols.has(filePath); + + if (!fileShouldBeIncluded) { + continue; + } + + hasMatchingNodes = true; + + const parts = filePath.split("/"); + let currentNode: ExplorerNodeData = root; + for (const part of parts) { + const id = getExplorerNodeId(part); + if (!currentNode.children.has(id)) { + currentNode.children.set(id, { + id: id, + displayName: part, + children: new Map(), + }); + } + currentNode = currentNode.children.get(id)!; + } + currentNode.fileId = getExplorerNodeId(filePath); + + for (const instanceId of Object.keys(fileDependencyManifest.symbols)) { + const symbolKey = `${filePath}#${instanceId}`; + if (shouldShowAll || filteredSymbolsSet.has(symbolKey)) { + const id = getExplorerNodeId(filePath, instanceId); + currentNode.children.set(id, { + id: id, + displayName: instanceId, + fileId: filePath, + symbolId: instanceId, + children: new Map(), + }); + } + } + } + + if (!shouldShowAll && !hasMatchingNodes) { + return undefined; + } + + const flattenTree = (node: ExplorerNodeData): ExplorerNodeData => { + if (node.children.size > 0) { + const flattenedChildren = new Map(); + + const folders: Array<[string, ExplorerNodeData]> = []; + const files: Array<[string, ExplorerNodeData]> = []; + + for (const [id, child] of node.children) { + const flattenedChild = flattenTree(child); + if (flattenedChild.fileId) { + files.push([id, flattenedChild]); + } else { + folders.push([id, flattenedChild]); + } + } + + for (const [id, folder] of folders) { + flattenedChildren.set(id, folder); + } + for (const [id, file] of files) { + flattenedChildren.set(id, file); + } + + node.children = flattenedChildren; + } + + while (node.children.size === 1) { + const childEntry = Array.from(node.children.entries())[0]; + const child = childEntry[1]; + + if (child.fileId) { + break; + } + + node.displayName = `${node.displayName}/${child.displayName}`; + node.id = child.id; + node.children = child.children; + } + + return node; + }; + + return flattenTree(root); +} + +function computeFilteredSymbols( + dependencyManifest: DependencyManifest, + searchTerm: string, +): { fileId: string; symbolId: string }[] { + const term = searchTerm.toLowerCase(); + const results: { fileId: string; symbolId: string }[] = []; + + for (const file of Object.values(dependencyManifest)) { + const filePathMatch = file.filePath.toLowerCase().includes(term); + + for (const symbolId of Object.keys(file.symbols)) { + if (filePathMatch || symbolId.toLowerCase().includes(term)) { + results.push({ fileId: file.filePath, symbolId }); + } + } + } + + return results; +} + +export function FileExplorerSidebar(props: { + dependencyManifest: DependencyManifest; + auditManifest: AuditManifest; + onHighlightInCytoscape: (node: ExplorerNodeData) => void; + toDetails: (node: ExplorerNodeData) => string; +}) { + const [searchTerm, setSearchTerm] = useState(""); + const [activeSearch, setActiveSearch] = useState(""); + + const filteredSymbols = useMemo(() => { + if (!activeSearch) return []; + return computeFilteredSymbols(props.dependencyManifest, activeSearch); + }, [props.dependencyManifest, activeSearch]); + + const [explorerTree, setExplorerTree] = useState(); + + useEffect(() => { + const tree = buildExplorerTree(props.dependencyManifest, filteredSymbols); + setExplorerTree(tree); + }, [props.dependencyManifest, filteredSymbols]); + + function onSubmitSearch(e: React.FormEvent) { + e.preventDefault(); + if (searchTerm.trim()) { + setActiveSearch(searchTerm.trim()); + } + } + + function onClearSearch() { + setSearchTerm(""); + setActiveSearch(""); + } + + return ( + + + + logo +
NanoAPI
+
+
+ + + + +
+ setSearchTerm(e.target.value)} + placeholder="Search files & symbols" + /> + + {activeSearch && ( + + )} +
+ +
+ {!explorerTree + ? ( +
+ No matching files found +
+ ) + : ( + + )} +
+ +
+
+
+ +
+ ); +} + +function ExplorerNode(props: { + node: ExplorerNodeData; + level: number; + onHighlightInCytoscape: (node: ExplorerNodeData) => void; + toDetails: (node: ExplorerNodeData) => string; +}) { + const [showChildren, setShowChildren] = useState(false); + + const type: "folder" | "file" | "symbol" = props.node.symbolId + ? "symbol" + : props.node.fileId + ? "file" + : "folder"; + + return ( +
+ {(() => { + switch (type) { + case "folder": + return ( + + ); + case "file": + return ( +
+
+ +
+
+ + + + + + Highlight in graph + + + + + + + + View graph for this file + + +
+
+ ); + case "symbol": + return ( +
+
+ + +
+
+ + + + + + Highlight in graph + + + + + + + + View graph for this symbol + + +
+
+ ); + } + })()} + {showChildren && + Array.from(props.node.children.values()).map((child) => ( + + ))} +
+ ); +} diff --git a/viewer/src/components/DependencyVisualizer/components/SymbolExtractionDialog.tsx b/viewer/src/components/DependencyVisualizer/components/SymbolExtractionDialog.tsx new file mode 100644 index 00000000..d1cf3dcf --- /dev/null +++ b/viewer/src/components/DependencyVisualizer/components/SymbolExtractionDialog.tsx @@ -0,0 +1,104 @@ +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "../../shadcn/Dialog.tsx"; +import { Button } from "../../shadcn/Button.tsx"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "../../shadcn/Card.tsx"; +import { Alert, AlertDescription, AlertTitle } from "../../shadcn/Alert.tsx"; +import { Separator } from "../../shadcn/Separator.tsx"; +import { Code, Copy, Info, Terminal } from "lucide-react"; +import { ScrollArea } from "../../shadcn/Scrollarea.tsx"; + +export default function SymbolExtractionDialog(props: { + manifestId: string; + children: React.ReactNode; + filePath: string; + symbolIds: string[]; +}) { + const [open, setOpen] = useState(false); + const [copied, setCopied] = useState(false); + + const generateCommand = () => { + const symbolOptions = props.symbolIds.map((symbolId) => { + return `--symbol="${props.filePath}|${symbolId}"`; + }); + return `napi extract --manifestId=${props.manifestId} ${symbolOptions.join(" ")}`; + }; + + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(generateCommand()); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error("Failed to copy to clipboard:", err); + } + }; + + return ( + + + {props.children} + + + + + + + Extract Symbol(s) via CLI + + + Use the napi CLI to extract symbols from your program. + + +
+ + + + Prerequisites + + +
+ Ensure you have a napi manifest generated locally: +
+ + napi generate + +
+
+ + + + +
+ + Command +
+ +
+
+ + + {generateCommand()} + + +
+
+
+
+
+ ); +} diff --git a/viewer/src/components/DependencyVisualizer/components/contextMenu/FileContextMenu.tsx b/viewer/src/components/DependencyVisualizer/components/contextMenu/FileContextMenu.tsx new file mode 100644 index 00000000..ee58290f --- /dev/null +++ b/viewer/src/components/DependencyVisualizer/components/contextMenu/FileContextMenu.tsx @@ -0,0 +1,84 @@ +import { Link, useSearchParams } from "react-router-dom"; +import type { DependencyManifest } from "../../../../types/dependencyManifest.ts"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "../../../shadcn/Dropdownmenu.tsx"; +import { PanelRight, SearchCode } from "lucide-react"; +import DisplayNameWithTooltip from "../DisplayNameWithTooltip.tsx"; + +export default function FileContextMenu(props: { + context: + | { + position: { x: number; y: number }; + fileDependencyManifest: DependencyManifest[string]; + } + | undefined; + onClose: () => void; + onOpenDetails: (filePath: string) => void; +}) { + const [searchParams] = useSearchParams(); + + function getToFileLink(filePath: string) { + const newSearchParams = new URLSearchParams(searchParams); + newSearchParams.set("fileId", filePath); + newSearchParams.delete("instanceId"); + return `?${newSearchParams.toString()}`; + } + + return ( +
+ props.onClose()} + > + + + + + + + { + props.context?.fileDependencyManifest && + props.onOpenDetails( + props.context.fileDependencyManifest.filePath, + ); + props.onClose(); + }} + > +
+ +
Show details
+
+
+ + +
+ +
Inspect symbols
+
+ +
+
+
+
+ ); +} diff --git a/viewer/src/components/DependencyVisualizer/components/contextMenu/SymbolContextMenu.tsx b/viewer/src/components/DependencyVisualizer/components/contextMenu/SymbolContextMenu.tsx new file mode 100644 index 00000000..29a925a4 --- /dev/null +++ b/viewer/src/components/DependencyVisualizer/components/contextMenu/SymbolContextMenu.tsx @@ -0,0 +1,88 @@ +import { Link, useSearchParams } from "react-router-dom"; +import type { DependencyManifest } from "../../../../types/dependencyManifest.ts"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "../../../shadcn/Dropdownmenu.tsx"; +import { PanelRight, SearchCode } from "lucide-react"; +import DisplayNameWithTooltip from "../DisplayNameWithTooltip.tsx"; + +export default function SymbolContextMenu(props: { + context: + | { + position: { x: number; y: number }; + fileDependencyManifest: DependencyManifest[string]; + symbolDependencyManifest: DependencyManifest[string]["symbols"][string]; + } + | undefined; + onClose: () => void; + onOpenDetails: (filePath: string, symbolId: string) => void; +}) { + const [searchParams] = useSearchParams(); + + function getToSymbolLink(filePath: string, symbolId: string) { + const newSearchParams = new URLSearchParams(searchParams); + newSearchParams.set("fileId", filePath); + newSearchParams.set("instanceId", symbolId); + return `?${newSearchParams.toString()}`; + } + + return ( +
+ props.onClose()} + > + + + + + + + { + props.context?.fileDependencyManifest && + props.onOpenDetails( + props.context.fileDependencyManifest.filePath, + props.context.symbolDependencyManifest.id, + ); + props.onClose(); + }} + > +
+ +
Show details
+
+
+ + +
+ +
Inspect symbol
+
+ +
+
+
+
+ ); +} diff --git a/viewer/src/components/DependencyVisualizer/components/controls/ControlExtensions/FiltersExtension.tsx b/viewer/src/components/DependencyVisualizer/components/controls/ControlExtensions/FiltersExtension.tsx new file mode 100644 index 00000000..c26f1099 --- /dev/null +++ b/viewer/src/components/DependencyVisualizer/components/controls/ControlExtensions/FiltersExtension.tsx @@ -0,0 +1,95 @@ +import { type MouseEvent, useEffect, useState } from "react"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "../../../../shadcn/Tooltip.tsx"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "../../../../shadcn/Dropdownmenu.tsx"; +import { Button } from "../../../../shadcn/Button.tsx"; +import { Funnel } from "lucide-react"; + +export default function FiltersExtension(props: { + busy: boolean; + currentFileName?: string; + onFilterChange: ( + showExternal: boolean, + showVariables: boolean, + showFunctions: boolean, + showClasses: boolean, + showStructs: boolean, + showEnums: boolean, + showInterfaces: boolean, + showRecords: boolean, + showDelegates: boolean, + ) => void; +}) { + const [showExternal, setShowExternal] = useState(true); + const [showVariables, setShowVariables] = useState(true); + const [showFunctions, setShowFunctions] = useState(true); + const [showClasses, setShowClasses] = useState(true); + const [showStructs, setShowStructs] = useState(true); + const [showEnums, setShowEnums] = useState(true); + const [showInterfaces, setShowInterfaces] = useState(true); + const [showRecords, setShowRecords] = useState(true); + const [showDelegates, setShowDelegates] = useState(true); + + useEffect(() => { + props.onFilterChange( + showExternal, showVariables, showFunctions, showClasses, + showStructs, showEnums, showInterfaces, showRecords, showDelegates, + ); + }, [showExternal, showVariables, showFunctions, showClasses, showStructs, showEnums, showInterfaces, showRecords, showDelegates]); + + function handleFilterClick( + e: MouseEvent, + set: React.Dispatch>, + ) { + e.preventDefault(); + set((prev) => !prev); + } + + return ( + + + + + + + + + Filters + + {[ + { label: "Show external", checked: showExternal, set: setShowExternal }, + { label: "Show variables", checked: showVariables, set: setShowVariables }, + { label: "Show functions", checked: showFunctions, set: setShowFunctions }, + { label: "Show classes", checked: showClasses, set: setShowClasses }, + { label: "Show structs", checked: showStructs, set: setShowStructs }, + { label: "Show enums", checked: showEnums, set: setShowEnums }, + { label: "Show interfaces", checked: showInterfaces, set: setShowInterfaces }, + { label: "Show records", checked: showRecords, set: setShowRecords }, + { label: "Show delegates", checked: showDelegates, set: setShowDelegates }, + ].map(({ label, checked, set }) => ( + handleFilterClick(e, set)} + > + {label} + + ))} + + + Hide or show specific elements in the graph. + + ); +} diff --git a/viewer/src/components/DependencyVisualizer/components/controls/ControlExtensions/GraphDepthExtension.tsx b/viewer/src/components/DependencyVisualizer/components/controls/ControlExtensions/GraphDepthExtension.tsx new file mode 100644 index 00000000..3a5e52d8 --- /dev/null +++ b/viewer/src/components/DependencyVisualizer/components/controls/ControlExtensions/GraphDepthExtension.tsx @@ -0,0 +1,76 @@ +import { useState } from "react"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "../../../../shadcn/Tooltip.tsx"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, +} from "../../../../shadcn/Dropdownmenu.tsx"; +import { Button } from "../../../../shadcn/Button.tsx"; +import { Settings2 } from "lucide-react"; +import { Slider } from "../../../../shadcn/Slider.tsx"; +import { Input } from "../../../../shadcn/Input.tsx"; +import { Label } from "../../../../shadcn/Label.tsx"; + +export default function GraphDepthExtension(props: { + busy: boolean; + dependencyState: { + depth: number; + setDepth: (depth: number) => void; + }; + dependentState: { + depth: number; + setDepth: (depth: number) => void; + }; +}) { + const { dependencyState, dependentState } = props; + const [tempDependencyDepth, setTempDependencyDepth] = useState(dependencyState.depth); + const [tempDependentDepth, setTempDependentDepth] = useState(dependentState.depth); + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (tempDependencyDepth !== dependencyState.depth) { + dependencyState.setDepth(tempDependencyDepth); + } + if (tempDependentDepth !== dependentState.depth) { + dependentState.setDepth(tempDependentDepth); + } + } + + return ( + + + + + + + + +
+ +
+ setTempDependencyDepth(value[0])} /> + setTempDependencyDepth(parseInt(e.target.value, 10))} className="w-20" /> +
+ +
+ setTempDependentDepth(value[0])} /> + setTempDependentDepth(parseInt(e.target.value, 10))} disabled={props.busy} className="w-20" /> +
+ +
+
+
+ Set the depth of the dependencies shown on the graph. +
+ ); +} diff --git a/viewer/src/components/DependencyVisualizer/components/controls/ControlExtensions/MetricsExtension.tsx b/viewer/src/components/DependencyVisualizer/components/controls/ControlExtensions/MetricsExtension.tsx new file mode 100644 index 00000000..45613c1d --- /dev/null +++ b/viewer/src/components/DependencyVisualizer/components/controls/ControlExtensions/MetricsExtension.tsx @@ -0,0 +1,79 @@ +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "../../../../shadcn/Tooltip.tsx"; +import { Button } from "../../../../shadcn/Button.tsx"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "../../../../shadcn/Dropdownmenu.tsx"; +import type { Metric } from "../../../../../types/dependencyManifest.ts"; +import { + metricLinesCount, + metricCodeLineCount, + metricCharacterCount, + metricCodeCharacterCount, + metricDependencyCount, + metricDependentCount, + metricCyclomaticComplexity, +} from "../../../../../types/dependencyManifest.ts"; + +export default function MetricsExtension(props: { + busy: boolean; + metricState: { + metric: Metric | undefined; + setMetric: (metric: Metric | undefined) => void; + }; +}) { + const metric = props.metricState.metric; + + function getMetricLabel(metric: Metric | undefined) { + switch (metric) { + case metricLinesCount: return "Lines"; + case metricCodeLineCount: return "Code Lines"; + case metricCharacterCount: return "Chars"; + case metricCodeCharacterCount: return "Code Chars"; + case metricDependencyCount: return "Dependencies"; + case metricDependentCount: return "Dependents"; + case metricCyclomaticComplexity: return "Complexity"; + default: return "None"; + } + } + + return ( + + + + + + + + + {([ + { metric: undefined, label: "No Metric" }, + { metric: metricLinesCount, label: "Lines" }, + { metric: metricCodeLineCount, label: "Code Lines" }, + { metric: metricCharacterCount, label: "Total Characters" }, + { metric: metricCodeCharacterCount, label: "Code Characters" }, + { metric: metricDependencyCount, label: "Dependencies" }, + { metric: metricDependentCount, label: "Dependents" }, + { metric: metricCyclomaticComplexity, label: "Complexity" }, + ] as { metric: Metric | undefined; label: string }[]).map((val) => ( + props.metricState?.setMetric?.(val.metric)} + > + {val.label} + + ))} + + + Select a metric to display on the graph + + ); +} diff --git a/viewer/src/components/DependencyVisualizer/components/controls/Controls.tsx b/viewer/src/components/DependencyVisualizer/components/controls/Controls.tsx new file mode 100644 index 00000000..6f5df472 --- /dev/null +++ b/viewer/src/components/DependencyVisualizer/components/controls/Controls.tsx @@ -0,0 +1,69 @@ +import type { ReactNode } from "react"; +import { Button } from "../../../shadcn/Button.tsx"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "../../../shadcn/Tooltip.tsx"; +import type { Core } from "cytoscape"; +import { Focus, Network, ZoomIn, ZoomOut } from "lucide-react"; + +export default function Controls(props: { + busy: boolean; + cy: Core | undefined; + onLayout: () => void; + children?: ReactNode; +}) { + function handleFit() { + if (!props.cy) return; + const elements = props.cy.elements(); + const padding = 10; + props.cy.center(elements).fit(elements, padding); + } + + function handleZoom(zoom: number) { + if (!props.cy) return; + const level = props.cy.zoom() * zoom; + const x = props.cy.width() / 2; + const y = props.cy.height() / 2; + props.cy.zoom({ level, renderedPosition: { x, y } }); + } + + return ( +
+ + + + + Fit to screen + + + + + + Reset layout + + + + + + Zoom out + + + + + + Zoom in + + {props.children} +
+ ); +} diff --git a/viewer/src/components/DependencyVisualizer/components/detailsPanes/AlertBadge.tsx b/viewer/src/components/DependencyVisualizer/components/detailsPanes/AlertBadge.tsx new file mode 100644 index 00000000..fcab2042 --- /dev/null +++ b/viewer/src/components/DependencyVisualizer/components/detailsPanes/AlertBadge.tsx @@ -0,0 +1,16 @@ +import { Check, TriangleAlert } from "lucide-react"; + +export default function AlertBadge(props: { count: number }) { + return ( +
+ {props.count > 0 + ? ( + <> + + {props.count} + + ) + : } +
+ ); +} diff --git a/viewer/src/components/DependencyVisualizer/components/detailsPanes/FileDetailsPane.tsx b/viewer/src/components/DependencyVisualizer/components/detailsPanes/FileDetailsPane.tsx new file mode 100644 index 00000000..d4517051 --- /dev/null +++ b/viewer/src/components/DependencyVisualizer/components/detailsPanes/FileDetailsPane.tsx @@ -0,0 +1,167 @@ +import type { DependencyManifest } from "../../../../types/dependencyManifest.ts"; +import type { AuditManifest } from "../../../../types/auditManifest.ts"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from "../../../shadcn/Sheet.tsx"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "../../../shadcn/Card.tsx"; +import { Code, File, SearchCode } from "lucide-react"; +import { ScrollArea } from "../../../shadcn/Scrollarea.tsx"; +import { Button } from "../../../shadcn/Button.tsx"; +import { Link, useSearchParams } from "react-router-dom"; +import DisplayNameWithTooltip from "../DisplayNameWithTooltip.tsx"; +import Metrics from "./Metrics.tsx"; +import AlertBadge from "./AlertBadge.tsx"; + +export default function FileDetailsPane(props: { + context: + | { + manifestId: string; + fileDependencyManifest: DependencyManifest[string]; + fileAuditManifest: AuditManifest[string]; + } + | undefined; + onClose: () => void; +}) { + const [searchParams] = useSearchParams(); + + function getToFileLink(filePath: string) { + const newSearchParams = new URLSearchParams(searchParams); + newSearchParams.set("fileId", filePath); + newSearchParams.delete("instanceId"); + return `?${newSearchParams.toString()}`; + } + + return ( +
+ props.onClose()} + > + + + +
+ + +
+
+ +
+ +
+ + + +
+ +
File Metrics
+
+ +
+
+ + + + + +
+ +
+ Symbols ( + {Object.keys( + props.context?.fileDependencyManifest?.symbols || {}, + ).length || 0} + ) +
+
+
+
+ +
+ {Object.entries( + Object.values( + props.context?.fileDependencyManifest?.symbols || {}, + ).reduce( + (acc, symbol) => { + acc[symbol.type] = (acc[symbol.type] || 0) + 1; + return acc; + }, + {} as Record, + ), + ).map(([type, count]) => ( +
+
{type}
+
{count}
+
+ ))} +
+
+
+ {Object.values( + props.context?.fileDependencyManifest?.symbols || {}, + ).map((symbol) => ( + + + +
+ + +
+
+
+ + + +
+ ))} +
+
+
+
+
+ ); +} diff --git a/viewer/src/components/DependencyVisualizer/components/detailsPanes/Metrics.tsx b/viewer/src/components/DependencyVisualizer/components/detailsPanes/Metrics.tsx new file mode 100644 index 00000000..cab84ca7 --- /dev/null +++ b/viewer/src/components/DependencyVisualizer/components/detailsPanes/Metrics.tsx @@ -0,0 +1,59 @@ +import type { DependencyManifest } from "../../../../types/dependencyManifest.ts"; +import type { AuditManifest } from "../../../../types/auditManifest.ts"; +import { Alert, AlertDescription } from "../../../shadcn/Alert.tsx"; + +export default function Metrics(props: { + dependencyManifest: + | DependencyManifest[string] + | DependencyManifest[string]["symbols"][string] + | undefined; + auditManifest: + | AuditManifest[string] + | AuditManifest[string]["symbols"][string] + | undefined; +}) { + function metricToHumanString(metric: string) { + switch (metric) { + case "linesCount": + return "Lines"; + case "codeLineCount": + return "Code Lines"; + case "characterCount": + return "Characters"; + case "codeCharacterCount": + return "Code Characters"; + case "dependencyCount": + return "Dependencies"; + case "dependentCount": + return "Dependents"; + case "cyclomaticComplexity": + return "Cyclomatic Complexity"; + default: + return metric; + } + } + + return ( +
+ {Object.entries(props.dependencyManifest?.metrics || {}).map(( + [key, value], + ) => ( +
+
+
+ {metricToHumanString(key)} +
+
{value}
+
+ {(props.auditManifest?.alerts || {})?.[key] && ( + + + {props.auditManifest?.alerts?.[key]?.message?.long} + + + )} +
+ ))} +
+ ); +} diff --git a/viewer/src/components/DependencyVisualizer/components/detailsPanes/SymbolDetailsPane.tsx b/viewer/src/components/DependencyVisualizer/components/detailsPanes/SymbolDetailsPane.tsx new file mode 100644 index 00000000..c3685407 --- /dev/null +++ b/viewer/src/components/DependencyVisualizer/components/detailsPanes/SymbolDetailsPane.tsx @@ -0,0 +1,165 @@ +import type { DependencyManifest } from "../../../../types/dependencyManifest.ts"; +import type { AuditManifest } from "../../../../types/auditManifest.ts"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "../../../shadcn/Sheet.tsx"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "../../../shadcn/Card.tsx"; +import { Code, File, SearchCode } from "lucide-react"; +import { ScrollArea } from "../../../shadcn/Scrollarea.tsx"; +import { Button } from "../../../shadcn/Button.tsx"; +import { Link, useSearchParams } from "react-router-dom"; +import DisplayNameWithTooltip from "../DisplayNameWithTooltip.tsx"; +import Metrics from "./Metrics.tsx"; +import AlertBadge from "./AlertBadge.tsx"; + +export default function SymbolDetailsPane(props: { + context: + | { + manifestId: string; + fileDependencyManifest: DependencyManifest[string]; + symbolDependencyManifest: DependencyManifest[string]["symbols"][string]; + fileAuditManifest: AuditManifest[string]; + symbolAuditManifest: AuditManifest[string]["symbols"][string]; + } + | undefined; + onClose: () => void; +}) { + const [searchParams] = useSearchParams(); + + function getToSymbolLink(filePath: string, symbolId: string) { + const newSearchParams = new URLSearchParams(searchParams); + newSearchParams.set("fileId", filePath); + newSearchParams.set("instanceId", symbolId); + return `?${newSearchParams.toString()}`; + } + + function getToFileLink(filePath: string) { + const newSearchParams = new URLSearchParams(searchParams); + newSearchParams.set("fileId", filePath); + newSearchParams.delete("instanceId"); + return `?${newSearchParams.toString()}`; + } + + return ( +
+ props.onClose()} + > + + + + +
+ + +
+
+ + +
+
+ + +
+ +
+ + + +
+ +
Symbol Metrics
+
+ +
+
+ + + +
+ + + +
+ +
File Metrics
+
+ +
+
+ + + +
+
+
+
+
+
+ ); +} diff --git a/viewer/src/components/DependencyVisualizer/visualizers/FileVisualizer.tsx b/viewer/src/components/DependencyVisualizer/visualizers/FileVisualizer.tsx new file mode 100644 index 00000000..dfa2bc43 --- /dev/null +++ b/viewer/src/components/DependencyVisualizer/visualizers/FileVisualizer.tsx @@ -0,0 +1,212 @@ +import { useEffect, useRef, useState } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import Controls from "../components/controls/Controls.tsx"; +import MetricsExtension from "../components/controls/ControlExtensions/MetricsExtension.tsx"; +import FiltersExtension from "../components/controls/ControlExtensions/FiltersExtension.tsx"; +import SymbolContextMenu from "../components/contextMenu/SymbolContextMenu.tsx"; +import SymbolDetailsPane from "../components/detailsPanes/SymbolDetailsPane.tsx"; +import { FileDependencyVisualizer } from "../../../cytoscape/fileDependencyVisualizer/index.ts"; +import type { DependencyManifest, Metric } from "../../../types/dependencyManifest.ts"; +import type { AuditManifest } from "../../../types/auditManifest.ts"; +import { useTheme } from "../../../contexts/ThemeProvider.tsx"; + +interface FileVisualizerProps { + manifestId: string; + dependencyManifest: DependencyManifest; + auditManifest: AuditManifest; + highlightedCytoscapeRef: + | { filePath: string; symbolId: string | undefined } + | undefined; + fileId: string; +} + +export default function FileVisualizer(props: FileVisualizerProps) { + const navigate = useNavigate(); + const { theme } = useTheme(); + const [searchParams, setSearchParams] = useSearchParams(); + + const containerRef = useRef(null); + const [busy, setBusy] = useState(true); + const [fileVisualizer, setFileVisualizer] = useState< + FileDependencyVisualizer | undefined + >(undefined); + + const metricFromUrl = (searchParams.get("metric") || undefined) as + | Metric + | undefined; + + const [metric, setMetric] = useState(metricFromUrl); + + function handleMetricChange(newMetric: Metric | undefined) { + if (newMetric) { + searchParams.set("metric", newMetric); + setSearchParams(searchParams); + } else { + searchParams.delete("metric"); + setSearchParams(searchParams); + } + setMetric(newMetric); + } + + const [contextMenu, setContextMenu] = useState< + | { + position: { x: number; y: number }; + fileDependencyManifest: DependencyManifest[string]; + symbolDependencyManifest: DependencyManifest[string]["symbols"][string]; + } + | undefined + >(undefined); + + const [detailsPane, setDetailsPane] = useState< + | { + manifestId: string; + fileDependencyManifest: DependencyManifest[string]; + symbolDependencyManifest: DependencyManifest[string]["symbols"][string]; + fileAuditManifest: AuditManifest[string]; + symbolAuditManifest: AuditManifest[string]["symbols"][string]; + } + | undefined + >(undefined); + + useEffect(() => { + setBusy(true); + const visualizer = new FileDependencyVisualizer( + containerRef.current as HTMLElement, + props.fileId, + props.dependencyManifest, + props.auditManifest, + { + theme, + defaultMetric: metric, + onAfterNodeRightClick: (value: { + position: { x: number; y: number }; + filePath: string; + symbolId: string; + }) => { + const fileDependencyManifest = + props.dependencyManifest[value.filePath]; + const symbolDependencyManifest = + fileDependencyManifest.symbols[value.symbolId]; + setContextMenu({ + position: value.position, + fileDependencyManifest, + symbolDependencyManifest, + }); + }, + onAfterNodeDblClick: (filePath: string, symbolId: string) => { + const newSearchParams = new URLSearchParams(searchParams); + newSearchParams.set("fileId", filePath); + newSearchParams.set("instanceId", symbolId); + navigate(`?${newSearchParams.toString()}`); + }, + }, + ); + + setFileVisualizer(visualizer); + setBusy(false); + + return () => { + visualizer?.cy.destroy(); + setFileVisualizer(undefined); + }; + }, [props.dependencyManifest, props.auditManifest, props.fileId]); + + useEffect(() => { + if (fileVisualizer) { + fileVisualizer.setTargetMetric(metric); + } + }, [metric]); + + useEffect(() => { + if (fileVisualizer) { + if (props.highlightedCytoscapeRef) { + fileVisualizer.highlightNode(props.highlightedCytoscapeRef); + } else { + fileVisualizer.unhighlightNodes(); + } + } + }, [props.highlightedCytoscapeRef]); + + useEffect(() => { + if (fileVisualizer) { + fileVisualizer.updateTheme(theme); + } + }, [theme]); + + function handleFilterChange( + showExternal: boolean, + showVariables: boolean, + showFunctions: boolean, + showClasses: boolean, + showStructs: boolean, + showEnums: boolean, + showInterfaces: boolean, + showRecords: boolean, + showDelegates: boolean, + ) { + if (fileVisualizer) { + fileVisualizer.filterNodes( + showExternal, + showVariables, + showFunctions, + showClasses, + showStructs, + showEnums, + showInterfaces, + showRecords, + showDelegates, + ); + } + } + + return ( +
+
+ +
+ fileVisualizer?.layoutGraph(fileVisualizer.cy)} + > + + + +
+ + setContextMenu(undefined)} + onOpenDetails={(filePath, symbolId) => { + const fileDependencyManifest = props.dependencyManifest[filePath]; + const symbolDependencyManifest = + fileDependencyManifest.symbols[symbolId]; + const fileAuditManifest = props.auditManifest[filePath]; + const symbolAuditManifest = fileAuditManifest.symbols[symbolId]; + setDetailsPane({ + manifestId: props.manifestId, + fileDependencyManifest, + symbolDependencyManifest, + fileAuditManifest, + symbolAuditManifest, + }); + }} + /> + + setDetailsPane(undefined)} + /> +
+ ); +} diff --git a/viewer/src/components/DependencyVisualizer/visualizers/ProjectVisualizer.tsx b/viewer/src/components/DependencyVisualizer/visualizers/ProjectVisualizer.tsx new file mode 100644 index 00000000..eb4b160c --- /dev/null +++ b/viewer/src/components/DependencyVisualizer/visualizers/ProjectVisualizer.tsx @@ -0,0 +1,162 @@ +import { useEffect, useRef, useState } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import Controls from "../components/controls/Controls.tsx"; +import MetricsExtension from "../components/controls/ControlExtensions/MetricsExtension.tsx"; +import FileContextMenu from "../components/contextMenu/FileContextMenu.tsx"; +import FileDetailsPane from "../components/detailsPanes/FileDetailsPane.tsx"; +import { ProjectDependencyVisualizer } from "../../../cytoscape/projectDependencyVisualizer/index.ts"; +import type { DependencyManifest, Metric } from "../../../types/dependencyManifest.ts"; +import type { AuditManifest } from "../../../types/auditManifest.ts"; +import { useTheme } from "../../../contexts/ThemeProvider.tsx"; + +interface ProjectVisualizerProps { + manifestId: string; + dependencyManifest: DependencyManifest; + auditManifest: AuditManifest; + highlightedCytoscapeRef: + | { filePath: string; symbolId: string | undefined } + | undefined; +} + +export default function ProjectVisualizer(props: ProjectVisualizerProps) { + const navigate = useNavigate(); + const { theme } = useTheme(); + const [searchParams, setSearchParams] = useSearchParams(); + + const containerRef = useRef(null); + const [busy, setBusy] = useState(true); + const [projectVisualizer, setProjectVisualizer] = useState< + ProjectDependencyVisualizer | undefined + >(undefined); + + const metricFromUrl = (searchParams.get("metric") || undefined) as + | Metric + | undefined; + + const [metric, setMetric] = useState(metricFromUrl); + + function handleMetricChange(newMetric: Metric | undefined) { + if (newMetric) { + searchParams.set("metric", newMetric); + setSearchParams(searchParams); + } else { + searchParams.delete("metric"); + setSearchParams(searchParams); + } + setMetric(newMetric); + } + + const [contextMenu, setContextMenu] = useState< + | { + position: { x: number; y: number }; + fileDependencyManifest: DependencyManifest[string]; + } + | undefined + >(undefined); + + const [detailsPane, setDetailsPane] = useState< + | { + manifestId: string; + fileDependencyManifest: DependencyManifest[string]; + fileAuditManifest: AuditManifest[string]; + } + | undefined + >(undefined); + + useEffect(() => { + setBusy(true); + const visualizer = new ProjectDependencyVisualizer( + containerRef.current as HTMLElement, + props.dependencyManifest, + props.auditManifest, + { + theme, + defaultMetric: metric, + onAfterNodeRightClick: (value: { + position: { x: number; y: number }; + filePath: string; + }) => { + setContextMenu({ + position: value.position, + fileDependencyManifest: props.dependencyManifest[value.filePath], + }); + }, + onAfterNodeDblClick: (filePath: string) => { + const newSearchParams = new URLSearchParams(searchParams); + newSearchParams.set("fileId", filePath); + newSearchParams.delete("instanceId"); + navigate(`?${newSearchParams.toString()}`); + }, + }, + ); + + setProjectVisualizer(visualizer); + setBusy(false); + + return () => { + visualizer?.cy.destroy(); + setProjectVisualizer(undefined); + }; + }, [props.dependencyManifest, props.auditManifest]); + + useEffect(() => { + if (projectVisualizer) { + projectVisualizer.setTargetMetric(metric); + } + }, [metric]); + + useEffect(() => { + if (projectVisualizer) { + if (props.highlightedCytoscapeRef) { + projectVisualizer.highlightNode(props.highlightedCytoscapeRef); + } else { + projectVisualizer.unhighlightNodes(); + } + } + }, [props.highlightedCytoscapeRef]); + + useEffect(() => { + if (projectVisualizer) { + projectVisualizer.updateTheme(theme); + } + }, [theme]); + + return ( +
+
+ +
+ projectVisualizer?.layoutGraph(projectVisualizer.cy)} + > + + +
+ + setDetailsPane(undefined)} + /> + + setContextMenu(undefined)} + onOpenDetails={(filePath) => { + setDetailsPane({ + manifestId: props.manifestId, + fileDependencyManifest: props.dependencyManifest[filePath], + fileAuditManifest: props.auditManifest[filePath], + }); + }} + /> +
+ ); +} diff --git a/viewer/src/components/DependencyVisualizer/visualizers/SymbolVisualizer.tsx b/viewer/src/components/DependencyVisualizer/visualizers/SymbolVisualizer.tsx new file mode 100644 index 00000000..8533572e --- /dev/null +++ b/viewer/src/components/DependencyVisualizer/visualizers/SymbolVisualizer.tsx @@ -0,0 +1,200 @@ +import { useEffect, useRef, useState } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import Controls from "../components/controls/Controls.tsx"; +import GraphDepthExtension from "../components/controls/ControlExtensions/GraphDepthExtension.tsx"; +import SymbolContextMenu from "../components/contextMenu/SymbolContextMenu.tsx"; +import SymbolDetailsPane from "../components/detailsPanes/SymbolDetailsPane.tsx"; +import { SymbolDependencyVisualizer } from "../../../cytoscape/symbolDependencyVisualizer/index.ts"; +import type { DependencyManifest } from "../../../types/dependencyManifest.ts"; +import type { AuditManifest } from "../../../types/auditManifest.ts"; +import { useTheme } from "../../../contexts/ThemeProvider.tsx"; + +interface SymbolVisualizerProps { + manifestId: string; + dependencyManifest: DependencyManifest; + auditManifest: AuditManifest; + highlightedCytoscapeRef: + | { filePath: string; symbolId: string | undefined } + | undefined; + fileId: string; + instanceId: string; +} + +export default function SymbolVisualizer(props: SymbolVisualizerProps) { + const navigate = useNavigate(); + const { theme } = useTheme(); + const [searchParams, setSearchParams] = useSearchParams(); + + const containerRef = useRef(null); + const [busy, setBusy] = useState(true); + const [symbolVisualizer, setSymbolVisualizer] = useState< + SymbolDependencyVisualizer | undefined + >(undefined); + + const dependencyDepthFromUrl = + (searchParams.get("dependencyDepth") || undefined) as number | undefined; + const dependentDepthFromUrl = + (searchParams.get("dependentDepth") || undefined) as number | undefined; + + const [dependencyDepth, setDependencyDepth] = useState( + dependencyDepthFromUrl || 3, + ); + const [dependentDepth, setDependentDepth] = useState( + dependentDepthFromUrl || 0, + ); + + function handleDependencyDepthChange(depth: number) { + searchParams.set("dependencyDepth", depth.toString()); + setSearchParams(searchParams); + setDependencyDepth(depth); + } + + function handleDependentDepthChange(depth: number) { + searchParams.set("dependentDepth", depth.toString()); + setSearchParams(searchParams); + setDependentDepth(depth); + } + + const [contextMenu, setContextMenu] = useState< + | { + position: { x: number; y: number }; + fileDependencyManifest: DependencyManifest[string]; + symbolDependencyManifest: DependencyManifest[string]["symbols"][string]; + } + | undefined + >(undefined); + + const [detailsPane, setDetailsPane] = useState< + | { + manifestId: string; + fileDependencyManifest: DependencyManifest[string]; + symbolDependencyManifest: DependencyManifest[string]["symbols"][string]; + fileAuditManifest: AuditManifest[string]; + symbolAuditManifest: AuditManifest[string]["symbols"][string]; + } + | undefined + >(undefined); + + useEffect(() => { + setBusy(true); + + if (!props.fileId || !props.instanceId) { + return; + } + + const visualizer = new SymbolDependencyVisualizer( + containerRef.current as HTMLElement, + props.fileId, + props.instanceId, + dependencyDepth, + dependentDepth, + props.dependencyManifest, + props.auditManifest, + { + theme, + onAfterNodeRightClick: (value: { + position: { x: number; y: number }; + filePath: string; + symbolId: string; + }) => { + const fileDependencyManifest = + props.dependencyManifest[value.filePath]; + const symbolDependencyManifest = + fileDependencyManifest.symbols[value.symbolId]; + setContextMenu({ + position: value.position, + fileDependencyManifest, + symbolDependencyManifest, + }); + }, + onAfterNodeDblClick: (filePath: string, symbolId: string) => { + const newSearchParams = new URLSearchParams(searchParams); + newSearchParams.set("fileId", filePath); + newSearchParams.set("instanceId", symbolId); + navigate(`?${newSearchParams.toString()}`); + }, + }, + ); + + setSymbolVisualizer(visualizer); + setBusy(false); + + return () => { + visualizer?.cy.destroy(); + setSymbolVisualizer(undefined); + }; + }, [ + props.dependencyManifest, + props.auditManifest, + props.fileId, + props.instanceId, + dependencyDepth, + dependentDepth, + ]); + + useEffect(() => { + if (symbolVisualizer) { + if (props.highlightedCytoscapeRef) { + symbolVisualizer.highlightNode(props.highlightedCytoscapeRef); + } else { + symbolVisualizer.unhighlightNodes(); + } + } + }, [props.highlightedCytoscapeRef]); + + useEffect(() => { + if (symbolVisualizer) { + symbolVisualizer.updateTheme(theme); + } + }, [theme]); + + return ( +
+
+ +
+ symbolVisualizer?.layoutGraph(symbolVisualizer.cy)} + > + + +
+ + setContextMenu(undefined)} + onOpenDetails={(filePath, symbolId) => { + const fileDependencyManifest = props.dependencyManifest[filePath]; + const symbolDependencyManifest = + fileDependencyManifest.symbols[symbolId]; + const fileAuditManifest = props.auditManifest[filePath]; + const symbolAuditManifest = fileAuditManifest.symbols[symbolId]; + setDetailsPane({ + manifestId: props.manifestId, + fileDependencyManifest, + symbolDependencyManifest, + fileAuditManifest, + symbolAuditManifest, + }); + }} + /> + + setDetailsPane(undefined)} + /> +
+ ); +} diff --git a/viewer/src/components/shadcn/Alert.tsx b/viewer/src/components/shadcn/Alert.tsx new file mode 100644 index 00000000..9ef9269b --- /dev/null +++ b/viewer/src/components/shadcn/Alert.tsx @@ -0,0 +1,66 @@ +import type * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "../../lib/utils.ts"; + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ); +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { Alert, AlertDescription, AlertTitle }; diff --git a/viewer/src/components/shadcn/Breadcrumb.tsx b/viewer/src/components/shadcn/Breadcrumb.tsx new file mode 100644 index 00000000..3de86be9 --- /dev/null +++ b/viewer/src/components/shadcn/Breadcrumb.tsx @@ -0,0 +1,109 @@ +import type * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; + +import { cn } from "../../lib/utils.ts"; + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return