project-compass 4.3.3 → 4.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1121 -22
- package/ARCHITECTURE.md +826 -10
- package/CONTRIBUTING.md +974 -10
- package/PROJECT_CONTEXT.md +404 -0
- package/README.md +594 -67
- package/commands.md +841 -104
- package/package.json +5 -4
- package/src/cli.js +310 -169
- package/src/components/AIHorizon.js +138 -255
- package/src/components/Footer.js +8 -64
- package/src/components/Header.js +8 -47
- package/src/components/Navigator.js +16 -70
- package/src/components/PackageRegistry.js +4 -3
- package/src/components/ProjectArchitect.js +6 -1
- package/src/components/TaskManager.js +12 -70
- package/src/detectors/dotnet.js +3 -2
- package/src/detectors/frameworks.js +28 -28
- package/src/detectors/go.js +6 -6
- package/src/detectors/java.js +2 -1
- package/src/detectors/node.js +3 -2
- package/src/detectors/php.js +1 -1
- package/src/detectors/python.js +33 -12
- package/src/detectors/ruby.js +1 -1
- package/src/detectors/rust.js +2 -2
- package/src/detectors/utils.js +6 -7
- package/src/projectDetection.js +41 -5
- package/src/store/useProjectStore.js +0 -32
package/ARCHITECTURE.md
CHANGED
|
@@ -1,21 +1,837 @@
|
|
|
1
1
|
# Project Compass Architecture
|
|
2
2
|
|
|
3
|
-
This document describes the high-level architecture of Project Compass.
|
|
3
|
+
This document describes the complete high-level architecture of Project Compass (v4.3.6).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Overview](#overview)
|
|
10
|
+
2. [Data Flow](#data-flow)
|
|
11
|
+
3. [Project Structure](#project-structure)
|
|
12
|
+
4. [Core Components](#core-components)
|
|
13
|
+
5. [Detection System](#detection-system)
|
|
14
|
+
6. [Framework Plugin System](#framework-plugin-system)
|
|
15
|
+
7. [State Management](#state-management)
|
|
16
|
+
8. [UI Rendering](#ui-rendering)
|
|
17
|
+
9. [Command Execution](#command-execution)
|
|
18
|
+
10. [Configuration System](#configuration-system)
|
|
19
|
+
11. [Recent Architecture Changes](#recent-architecture-changes)
|
|
20
|
+
12. [Design Patterns](#design-patterns)
|
|
21
|
+
13. [Security](#security)
|
|
22
|
+
14. [Performance](#performance)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Overview
|
|
27
|
+
|
|
28
|
+
Project Compass is a terminal-based project navigator and runner built with:
|
|
29
|
+
- **Runtime**: Node.js (ESM modules)
|
|
30
|
+
- **UI Framework**: Ink (React for CLI)
|
|
31
|
+
- **Styling**: kleur (terminal colors)
|
|
32
|
+
- **Execution**: execa (robust subprocess management)
|
|
33
|
+
- **File Search**: fast-glob (fast file globbing)
|
|
34
|
+
- **Intelligence**: Native fetch API (AI provider integration)
|
|
35
|
+
|
|
36
|
+
---
|
|
4
37
|
|
|
5
38
|
## Data Flow
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
39
|
+
|
|
40
|
+
### Complete Flow Diagram
|
|
41
|
+
```
|
|
42
|
+
User Command/Input
|
|
43
|
+
↓
|
|
44
|
+
[CLI Argument Parsing] (src/cli.js:766-781)
|
|
45
|
+
↓
|
|
46
|
+
[Mode Detection]
|
|
47
|
+
├─ --help │ --version → Show info & exit
|
|
48
|
+
├─ --mode test │ --list-projects → Headless detection
|
|
49
|
+
├─ --run "cmd" → Execute command & exit
|
|
50
|
+
├─ --studio-check → Runtime check & exit
|
|
51
|
+
├─ --scaffold → Create project & exit
|
|
52
|
+
├─ --add-pkg │ --remove-pkg → Package mgmt & exit
|
|
53
|
+
└─ (default) → Launch TUI
|
|
54
|
+
↓
|
|
55
|
+
[TUI Mode] → React/Ink Render Loop (src/cli.js:163-763)
|
|
56
|
+
↓
|
|
57
|
+
[Project Detection] (src/projectDetection.js:146-180)
|
|
58
|
+
↓
|
|
59
|
+
[Detector Orchestration] → Run detectors in priority order
|
|
60
|
+
↓
|
|
61
|
+
[fast-glob Scanning] → Find manifest files (package.json, Cargo.toml, etc.)
|
|
62
|
+
↓
|
|
63
|
+
[Per-Detector Build] (e.g., src/detectors/node.js:58-139)
|
|
64
|
+
↓
|
|
65
|
+
[Framework Plugin Application] (src/projectDetection.js:114-144)
|
|
66
|
+
↓
|
|
67
|
+
[compass-config.js Loading] (src/detectors/compass-config.js)
|
|
68
|
+
↓
|
|
69
|
+
[Project Object Creation] → {id, path, name, type, commands, frameworks, metadata}
|
|
70
|
+
↓
|
|
71
|
+
[State Update] → React state in Compass component
|
|
72
|
+
↓
|
|
73
|
+
[UI Render] → Ink components (Navigator, TaskManager, etc.)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Detailed Steps
|
|
77
|
+
|
|
78
|
+
1. **Initialization**: `cli.js` resolves the working directory (defaults to current folder) and parses CLI arguments.
|
|
79
|
+
|
|
80
|
+
2. **Discovery**: `projectDetection.js` orchestrates modular detectors in `src/detectors/` to perform a high-speed glob search for common manifest files:
|
|
81
|
+
- `package.json` (Node.js)
|
|
82
|
+
- `pyproject.toml`, `requirements.txt`, `setup.py`, `Pipfile`, `manage.py` (Python)
|
|
83
|
+
- `Cargo.toml` (Rust)
|
|
84
|
+
- `go.mod` (Go)
|
|
85
|
+
- `pom.xml`, `build.gradle`, `build.gradle.kts` (Java)
|
|
86
|
+
- `composer.json` (PHP)
|
|
87
|
+
- `Gemfile` (Ruby)
|
|
88
|
+
- `*.csproj`, `*.fsproj` (.NET)
|
|
89
|
+
|
|
90
|
+
3. **Config Loading**: `compass-config.js` is loaded from each project directory (if exists) and merged into project data.
|
|
91
|
+
|
|
92
|
+
4. **Framework Detection**: `frameworks.js` applies built-in (40+) and user plugins to detect frameworks based on actual dependencies (not file existence).
|
|
93
|
+
|
|
94
|
+
5. **State Management**: The discovered projects and their metadata (frameworks, scripts, dependencies) are passed into an Ink React tree.
|
|
95
|
+
|
|
96
|
+
6. **Rendering**: Components in `src/components` handle specific views:
|
|
97
|
+
- Navigator (main project list)
|
|
98
|
+
- TaskManager (background processes)
|
|
99
|
+
- PackageRegistry (dependency management)
|
|
100
|
+
- ProjectArchitect (scaffolding)
|
|
101
|
+
- AIHorizon (AI analysis)
|
|
102
|
+
- Studio (environment health)
|
|
103
|
+
- Header (top bar)
|
|
104
|
+
- Footer (bottom bar with stdin)
|
|
105
|
+
|
|
106
|
+
7. **Execution**: User-triggered scripts (like running `npm test`) are managed by `TaskManager.js` using `execa` with streaming logs.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Project Structure
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
project-compass/
|
|
114
|
+
├── package.json # NPM package config (v4.3.6)
|
|
115
|
+
├── README.md # Main documentation (COMPREHENSIVE)
|
|
116
|
+
├── ARCHITECTURE.md # This file (full architecture)
|
|
117
|
+
├── commands.md # All commands & shortcuts
|
|
118
|
+
├── CONTRIBUTING.md # Contribution guidelines
|
|
119
|
+
├── AGENTS.md # AI agent context
|
|
120
|
+
├── PROJECT_CONTEXT.md # Technical context for agents
|
|
121
|
+
├── LICENSE # MIT License
|
|
122
|
+
├── eslint.config.cjs # ESLint configuration
|
|
123
|
+
├── src/
|
|
124
|
+
│ ├── cli.js # Entry point (840+ lines)
|
|
125
|
+
│ │ # - Argument parsing (parseArgs)
|
|
126
|
+
│ │ # - Main React component (Compass)
|
|
127
|
+
│ │ # - Global input handling (useInput)
|
|
128
|
+
│ │ # - Project scanning (useScanner)
|
|
129
|
+
│ │ # - Command execution (runProjectCommand)
|
|
130
|
+
│ │ # - Task management (addLogToTask, killAllTasks)
|
|
131
|
+
│ ├── projectDetection.js # Orchestrator (189 lines)
|
|
132
|
+
│ │ # - discoverProjects(root)
|
|
133
|
+
│ │ # - applyFrameworkPlugins(project)
|
|
134
|
+
│ │ # - matchesPlugin(project, plugin)
|
|
135
|
+
│ │ # - getFrameworkPlugins()
|
|
136
|
+
│ ├── configPaths.js # Config directory paths
|
|
137
|
+
│ │ # - CONFIG_DIR: ~/.project-compass/
|
|
138
|
+
│ │ # - CONFIG_PATH: ~/.project-compass/config.json
|
|
139
|
+
│ │ # - PLUGIN_FILE: ~/.project-compass/plugins.json
|
|
140
|
+
│ │ # - ensureConfigDir()
|
|
141
|
+
│ ├── detectors/
|
|
142
|
+
│ │ ├── utils.js # Shared utilities (148 lines)
|
|
143
|
+
│ │ │ # - checkBinary(name)
|
|
144
|
+
│ │ │ # - hasProjectFile(projectPath, file)
|
|
145
|
+
│ │ │ # - getPackageManager(projectPath, language)
|
|
146
|
+
│ │ │ # - dependencyMatches(project, needle)
|
|
147
|
+
│ │ │ # - parseCommandTokens(value)
|
|
148
|
+
│ │ │ # - getLockfileInfo(projectPath)
|
|
149
|
+
│ │ ├── node.js # Node.js detection (140 lines)
|
|
150
|
+
│ │ │ # Priority: 100
|
|
151
|
+
│ │ │ # Files: package.json
|
|
152
|
+
│ │ │ # Binaries: node, npm
|
|
153
|
+
│ │ ├── python.js # Python detection (208 lines)
|
|
154
|
+
│ │ │ # Priority: 95
|
|
155
|
+
│ │ │ # Files: pyproject.toml, requirements.txt, setup.py, Pipfile, manage.py
|
|
156
|
+
│ │ │ # Binaries: python3, python, uv
|
|
157
|
+
│ │ ├── rust.js # Rust detection (136 lines)
|
|
158
|
+
│ │ │ # Priority: 90
|
|
159
|
+
│ │ │ # Files: Cargo.toml
|
|
160
|
+
│ │ │ # Binaries: cargo, rustc
|
|
161
|
+
│ │ ├── go.js # Go detection
|
|
162
|
+
│ │ │ # Priority: 85
|
|
163
|
+
│ │ │ # Files: go.mod
|
|
164
|
+
│ │ │ # Binaries: go
|
|
165
|
+
│ │ ├── java.js # Java detection
|
|
166
|
+
│ │ │ # Priority: 80
|
|
167
|
+
│ │ │ # Files: pom.xml, build.gradle, build.gradle.kts
|
|
168
|
+
│ │ │ # Binaries: java, mvn, gradle
|
|
169
|
+
│ │ ├── php.js # PHP detection
|
|
170
|
+
│ │ │ # Priority: 75
|
|
171
|
+
│ │ │ # Files: composer.json
|
|
172
|
+
│ │ │ # Binaries: php, composer
|
|
173
|
+
│ │ ├── ruby.js # Ruby detection
|
|
174
|
+
│ │ │ # Priority: 70
|
|
175
|
+
│ │ │ # Files: Gemfile
|
|
176
|
+
│ │ │ # Binaries: ruby, bundle
|
|
177
|
+
│ │ ├── dotnet.js # .NET detection
|
|
178
|
+
│ │ │ # Priority: 65
|
|
179
|
+
│ │ │ # Files: *.csproj, *.fsproj
|
|
180
|
+
│ │ │ # Binaries: dotnet
|
|
181
|
+
│ │ ├── generic.js # Generic fallback detector
|
|
182
|
+
│ │ │ # Priority: 10
|
|
183
|
+
│ │ │ # Files: Makefile, build.sh
|
|
184
|
+
│ │ ├── compass-config.js # Project-specific config loader (39 lines)
|
|
185
|
+
│ │ │ # - loadProjectConfig(projectPath)
|
|
186
|
+
│ │ │ # - saveProjectConfig(projectPath, config)
|
|
187
|
+
│ │ └── frameworks.js # 40+ built-in framework plugins (877 lines)
|
|
188
|
+
│ │ # Node.js: Next.js, React, Vue, NestJS, Express, etc.
|
|
189
|
+
│ │ # Python: FastAPI, Flask, Django, etc.
|
|
190
|
+
│ │ # Rust: Actix, Rocket, Axum, etc.
|
|
191
|
+
│ │ # Go: Gin, Echo, Fiber, etc.
|
|
192
|
+
│ │ # Java: Spring Boot, Quarkus, etc.
|
|
193
|
+
│ │ # PHP: Laravel, Symfony, etc.
|
|
194
|
+
│ │ # Ruby: Rails, Sinatra, etc.
|
|
195
|
+
│ │ # .NET: ASP.NET Core, Blazor, etc.
|
|
196
|
+
│ │ # ML/Data: Pandas, PyTorch, TensorFlow
|
|
197
|
+
├── components/
|
|
198
|
+
│ ├── Navigator.js # Paginated project list (110 lines)
|
|
199
|
+
│ ├── Header.js # Top bar with logo, status, time (60 lines)
|
|
200
|
+
│ ├── Footer.js # Bottom bar with stdin input (81 lines)
|
|
201
|
+
│ ├── TaskManager.js # Orbit Task Manager (82 lines)
|
|
202
|
+
│ ├── PackageRegistry.js # Dependency management (156 lines)
|
|
203
|
+
│ ├── ProjectArchitect.js # Scaffolding templates (113 lines)
|
|
204
|
+
│ ├── AIHorizon.js # AI-powered analysis (426 lines)
|
|
205
|
+
│ └── Studio.js # Environment health check (64 lines)
|
|
206
|
+
├── assets/ # Screenshots and branding
|
|
207
|
+
└── node_modules/ # Dependencies
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Core Components
|
|
213
|
+
|
|
214
|
+
### Entry Point: `src/cli.js`
|
|
215
|
+
|
|
216
|
+
**Lines**: 840+
|
|
217
|
+
**Purpose**: Main entry point, argument parsing, global state management, input handling
|
|
218
|
+
|
|
219
|
+
#### Key Functions:
|
|
220
|
+
- `parseArgs()` (lines 766-781): Parse CLI arguments
|
|
221
|
+
- `main()` (lines 783-840): Main async entry
|
|
222
|
+
- `saveConfig(config)`: Persist config to disk
|
|
223
|
+
- `loadConfig()`: Load config from `~/.project-compass/config.json`
|
|
224
|
+
|
|
225
|
+
#### Key React Component: `Compass` (lines 163-763)
|
|
226
|
+
- **State Variables**:
|
|
227
|
+
- `projects` - Detected projects array
|
|
228
|
+
- `selectedIndex` - Currently selected project index
|
|
229
|
+
- `viewMode` - 'list' or 'detail'
|
|
230
|
+
- `mainView` - 'navigator', 'tasks', 'registry', 'architect', 'ai', 'studio'
|
|
231
|
+
- `tasks` - Array of running/completed tasks
|
|
232
|
+
- `activeTaskId` - Currently selected task
|
|
233
|
+
- `config` - Loaded from `~/.project-compass/config.json`
|
|
234
|
+
|
|
235
|
+
- **Ref Objects**:
|
|
236
|
+
- `runningProcessMap` - Map of task IDs to child processes
|
|
237
|
+
- `lastCommandRef` - Last executed command for replay (Shift+L)
|
|
238
|
+
|
|
239
|
+
#### Hooks Used:
|
|
240
|
+
- `useScanner(rootPath)` - Async project detection
|
|
241
|
+
- `useInput()` - Global keyboard input handling
|
|
242
|
+
- `useState()` - Multiple state variables
|
|
243
|
+
- `useMemo()` - Memoized computations
|
|
244
|
+
- `useCallback()` - Memoized callbacks
|
|
245
|
+
- `useEffect()` - Side effects (timers, scanning)
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Detection System
|
|
250
|
+
|
|
251
|
+
### Detector Interface
|
|
252
|
+
|
|
253
|
+
Each detector in `src/detectors/` exports an object with:
|
|
254
|
+
|
|
255
|
+
```javascript
|
|
256
|
+
export default {
|
|
257
|
+
type: 'python', // Language identifier
|
|
258
|
+
label: 'Python', // Display name
|
|
259
|
+
icon: '🐍', // Emoji icon
|
|
260
|
+
priority: 95, // Numeric priority (higher = preferred)
|
|
261
|
+
files: ['pyproject.toml', 'requirements.txt', ...], // Manifest files to match
|
|
262
|
+
binaries: ['python3', 'python', 'uv'], // Required binaries to check
|
|
263
|
+
async build(projectPath, manifest) {
|
|
264
|
+
// Return project object or null
|
|
265
|
+
return {
|
|
266
|
+
id: `${projectPath}::python`,
|
|
267
|
+
path: projectPath,
|
|
268
|
+
name: path.basename(projectPath),
|
|
269
|
+
type: 'Python',
|
|
270
|
+
icon: '🐍',
|
|
271
|
+
priority: this.priority,
|
|
272
|
+
commands: { ... },
|
|
273
|
+
metadata: { ... },
|
|
274
|
+
manifest: path.basename(manifest),
|
|
275
|
+
description: '...',
|
|
276
|
+
missingBinaries: [...],
|
|
277
|
+
frameworks: [...],
|
|
278
|
+
extra: { ... }
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Detection Priority Order
|
|
285
|
+
|
|
286
|
+
Detectors are run in this order (highest priority wins):
|
|
287
|
+
|
|
288
|
+
1. **Node.js** (100) - `package.json`
|
|
289
|
+
2. **Python** (95) - `pyproject.toml`, `requirements.txt`, `setup.py`, `Pipfile`, `manage.py`
|
|
290
|
+
3. **Rust** (90) - `Cargo.toml`
|
|
291
|
+
4. **Go** (85) - `go.mod`
|
|
292
|
+
5. **Java** (80) - `pom.xml`, `build.gradle`
|
|
293
|
+
6. **PHP** (75) - `composer.json`
|
|
294
|
+
7. **Ruby** (70) - `Gemfile`
|
|
295
|
+
8. **.NET** (65) - `*.csproj`, `*.fsproj`
|
|
296
|
+
9. **Generic** (10) - `Makefile`, `build.sh` (fallback)
|
|
297
|
+
|
|
298
|
+
### Framework Plugin System
|
|
299
|
+
|
|
300
|
+
#### Built-in Frameworks (`src/detectors/frameworks.js`)
|
|
301
|
+
|
|
302
|
+
**40+ frameworks** with the following structure:
|
|
303
|
+
|
|
304
|
+
```javascript
|
|
305
|
+
{
|
|
306
|
+
id: 'fastapi',
|
|
307
|
+
name: 'FastAPI',
|
|
308
|
+
icon: '⚡',
|
|
309
|
+
description: 'Modern fast web framework for Python',
|
|
310
|
+
languages: ['Python'], // Which languages this applies to
|
|
311
|
+
priority: 112, // Plugin priority (boosts project priority)
|
|
312
|
+
match(project) {
|
|
313
|
+
// Return true if this framework is detected
|
|
314
|
+
return dependencyMatches(project, 'fastapi');
|
|
315
|
+
},
|
|
316
|
+
commands(project) {
|
|
317
|
+
// Return commands specific to this framework
|
|
318
|
+
return {
|
|
319
|
+
install: { label: 'FastAPI deps', command: ['pip', 'install', '-r', 'requirements.txt'], source: 'framework' },
|
|
320
|
+
run: { label: 'FastAPI dev', command: ['uvicorn', 'main:app', '--reload'], source: 'framework' },
|
|
321
|
+
test: { label: 'FastAPI test', command: ['pytest'], source: 'framework' }
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
#### Custom Plugins (`~/.project-compass/plugins.json`)
|
|
328
|
+
|
|
329
|
+
Users can add custom framework plugins:
|
|
330
|
+
|
|
331
|
+
```json
|
|
332
|
+
{
|
|
333
|
+
"plugins": [
|
|
334
|
+
{
|
|
335
|
+
"name": "My Framework",
|
|
336
|
+
"icon": "🚀",
|
|
337
|
+
"languages": ["Node.js"],
|
|
338
|
+
"files": ["my-framework.config.js"],
|
|
339
|
+
"dependencies": ["my-framework"],
|
|
340
|
+
"priority": 100,
|
|
341
|
+
"commands": {
|
|
342
|
+
"dev": { "label": "Dev", "command": ["my-cli", "dev"] }
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
]
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## State Management
|
|
352
|
+
|
|
353
|
+
### Current Implementation (in `Compass` component)
|
|
354
|
+
|
|
355
|
+
All state is managed directly in the `Compass` component:
|
|
356
|
+
|
|
357
|
+
```javascript
|
|
358
|
+
const [projects, setProjects] = useState([]);
|
|
359
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
360
|
+
const [viewMode, setViewMode] = useState('list');
|
|
361
|
+
const [mainView, setMainView] = useState(initialView);
|
|
362
|
+
const [tasks, setTasks] = useState([]);
|
|
363
|
+
const [activeTaskId, setActiveTaskId] = useState(null);
|
|
364
|
+
const [logOffset, setLogOffset] = useState(0);
|
|
365
|
+
const [customMode, setCustomMode] = useState(false);
|
|
366
|
+
const [portConfigMode, setPortConfigMode] = useState(false);
|
|
367
|
+
// ... more state variables
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## UI Rendering
|
|
373
|
+
|
|
374
|
+
### Component Tree
|
|
375
|
+
|
|
376
|
+
```
|
|
377
|
+
<Compass>
|
|
378
|
+
├─ useState: projects, selectedIndex, mainView, viewMode, tasks, etc.
|
|
379
|
+
├─ useScanner(rootPath) → projects
|
|
380
|
+
├─ useEffect: startup animation
|
|
381
|
+
├─ useInput: global keyboard handler
|
|
382
|
+
└─ renderView()
|
|
383
|
+
├─ Startup Screen (if startup)
|
|
384
|
+
└─ Main Views:
|
|
385
|
+
├─ Navigator View (mainView === 'navigator')
|
|
386
|
+
│ ├─ <Header> (projects count, time, status)
|
|
387
|
+
│ ├─ Quick Actions Bar (B/T/R/I/0)
|
|
388
|
+
│ ├─ Art Board (if showArtBoard)
|
|
389
|
+
│ ├─ Projects Row:
|
|
390
|
+
│ │ ├─ <Navigator> (project list with pagination)
|
|
391
|
+
│ │ └─ Details Panel:
|
|
392
|
+
│ │ ├─ Project name, type, path
|
|
393
|
+
│ │ ├─ Frameworks
|
|
394
|
+
│ │ ├─ Commands (builtin + custom)
|
|
395
|
+
│ │ └─ Missing binaries warning
|
|
396
|
+
│ ├─ Output Panel: <OutputPanel>
|
|
397
|
+
│ ├─ <Footer> (stdin input, toggle hints)
|
|
398
|
+
│ ├─ Help Cards (if showHelpCards)
|
|
399
|
+
│ ├─ Structure Guide (if showStructureGuide)
|
|
400
|
+
│ └─ Help Overlay (if showHelp)
|
|
401
|
+
│
|
|
402
|
+
├─ Tasks View (mainView === 'tasks')
|
|
403
|
+
│ └─ <TaskManager>
|
|
404
|
+
│
|
|
405
|
+
├─ Registry View (mainView === 'registry')
|
|
406
|
+
│ └─ <PackageRegistry>
|
|
407
|
+
│
|
|
408
|
+
├─ Architect View (mainView === 'architect')
|
|
409
|
+
│ └─ <ProjectArchitect>
|
|
410
|
+
│
|
|
411
|
+
├─ AI View (mainView === 'ai')
|
|
412
|
+
│ └─ <AIHorizon>
|
|
413
|
+
│
|
|
414
|
+
└─ Studio View (mainView === 'studio')
|
|
415
|
+
└─ <Studio>
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Component Details
|
|
419
|
+
|
|
420
|
+
#### `<Navigator>` (src/components/Navigator.js)
|
|
421
|
+
- **Props**: `projects`, `selectedIndex`, `rootPath`, `loading`, `error`, `maxVisibleProjects`
|
|
422
|
+
- **Features**:
|
|
423
|
+
- Paginated project list (page size = `maxVisibleProjects`, default 3)
|
|
424
|
+
- Loading spinner animation
|
|
425
|
+
- Error display
|
|
426
|
+
- Empty state message
|
|
427
|
+
- Framework badges display
|
|
428
|
+
- Missing runtime warnings
|
|
429
|
+
|
|
430
|
+
#### `<TaskManager>` (src/components/TaskManager.js)
|
|
431
|
+
- **Props**: `tasks`, `activeTaskId`, `renameMode`, `renameInput`, `renameCursor`, `CursorText`
|
|
432
|
+
- **Features**:
|
|
433
|
+
- Task list with status colors
|
|
434
|
+
- Active task highlighting
|
|
435
|
+
- Mini log preview (last 5 lines)
|
|
436
|
+
- Task renaming
|
|
437
|
+
- Keyboard shortcuts display
|
|
438
|
+
|
|
439
|
+
#### `<AIHorizon>` (src/components/AIHorizon.js)
|
|
440
|
+
- **Props**: `selectedProject`, `CursorText`, `config`, `setConfig`, `saveConfig`
|
|
441
|
+
- **Features**:
|
|
442
|
+
- Multi-step flow: provider → model → token → analyze → review
|
|
443
|
+
- AI provider selection (OpenRouter, Gemini, Claude, Ollama)
|
|
444
|
+
- Project context building (README, main file, config)
|
|
445
|
+
- Raw AI response display
|
|
446
|
+
- Editable suggestions
|
|
447
|
+
- Config persistence
|
|
448
|
+
|
|
449
|
+
#### `<PackageRegistry>` (src/components/PackageRegistry.js)
|
|
450
|
+
- **Props**: `selectedProject`, `projects`, `onRunCommand`, `CursorText`, `onSelectProject`
|
|
451
|
+
- **Features**:
|
|
452
|
+
- Project selection sub-view
|
|
453
|
+
- Package listing
|
|
454
|
+
- Add/remove packages
|
|
455
|
+
- Python venv creation
|
|
456
|
+
- Native package manager detection
|
|
457
|
+
|
|
458
|
+
#### `<ProjectArchitect>` (src/components/ProjectArchitect.js)
|
|
459
|
+
- **Props**: `rootPath`, `onRunCommand`, `CursorText`, `onReturn`
|
|
460
|
+
- **Features**:
|
|
461
|
+
- 7+ templates (Next.js, React, Vue, Rust, Django, Python, Go)
|
|
462
|
+
- Multi-step: framework → path → name
|
|
463
|
+
- Command execution via Orbit
|
|
464
|
+
|
|
465
|
+
#### `<Studio>` (src/components/Studio.js)
|
|
466
|
+
- **Props**: None (checks binaries)
|
|
467
|
+
- **Features**:
|
|
468
|
+
- Runtime version checking
|
|
469
|
+
- 9 languages checked (Node, npm, Python, Rust, Go, Java, PHP, Ruby, .NET)
|
|
470
|
+
- Status display (✓/✗)
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## Command Execution
|
|
475
|
+
|
|
476
|
+
### Execution Flow
|
|
477
|
+
|
|
478
|
+
```
|
|
479
|
+
User presses key (B/T/R/I or Enter on detail view)
|
|
480
|
+
↓
|
|
481
|
+
useInput handler in Compass component (src/cli.js:324-592)
|
|
482
|
+
↓
|
|
483
|
+
runProjectCommand(commandMeta, project)
|
|
484
|
+
↓
|
|
485
|
+
execa(commandMeta.command[0], commandMeta.command.slice(1), {
|
|
486
|
+
cwd: project.path,
|
|
487
|
+
env: process.env,
|
|
488
|
+
stdin: 'pipe', // For interactive input
|
|
489
|
+
detached: process.platform !== 'win32' // For proper cleanup
|
|
490
|
+
})
|
|
491
|
+
↓
|
|
492
|
+
subprocess.stdout?.on('data', ...) // Stream stdout
|
|
493
|
+
subprocess.stderr?.on('data', ...) // Stream stderr
|
|
494
|
+
↓
|
|
495
|
+
addLogToTask(taskId, line) // Append to task logs
|
|
496
|
+
↓
|
|
497
|
+
Task status updates: 'running' → 'finished' / 'failed' / 'killed'
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Task Object Structure
|
|
501
|
+
|
|
502
|
+
```javascript
|
|
503
|
+
{
|
|
504
|
+
id: 'task-' + Date.now(), // Unique task ID
|
|
505
|
+
name: `${project.name} · ${commandLabel}`, // Display name
|
|
506
|
+
status: 'running' | 'finished' | 'failed' | 'killed',
|
|
507
|
+
logs: ['line1', 'line2', ...], // Log lines (capped at 500)
|
|
508
|
+
project: 'Project Name' // Source project name
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Process Management
|
|
513
|
+
|
|
514
|
+
- **Process Map**: `runningProcessMap` (useRef Map)
|
|
515
|
+
- Stores references to child processes
|
|
516
|
+
- Key: taskId
|
|
517
|
+
- Value: execa subprocess object
|
|
518
|
+
|
|
519
|
+
- **Kill Process** (`handleKillTask`):
|
|
520
|
+
- Windows: `taskkill /pid <pid> /f /t`
|
|
521
|
+
- Unix: `process.kill(-pid, 'SIGKILL')` (process group)
|
|
522
|
+
- Fallback: `proc.kill('SIGKILL')`
|
|
523
|
+
|
|
524
|
+
- **Kill All** (`killAllTasks`):
|
|
525
|
+
- Iterates `runningProcessMap`
|
|
526
|
+
- Kills each process
|
|
527
|
+
- Clears the map
|
|
528
|
+
|
|
529
|
+
### Stdin Input
|
|
530
|
+
|
|
531
|
+
When a process is running and `activeTaskId` is set:
|
|
532
|
+
- User typing is captured by `useInput`
|
|
533
|
+
- Displayed in Footer's input area (with cursor)
|
|
534
|
+
- `Enter` sends `stdinBuffer + '\n'` to `proc.stdin`
|
|
535
|
+
- `Backspace/Delete` removes characters
|
|
536
|
+
- `Left/Right Arrow` moves cursor
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
## Configuration System
|
|
541
|
+
|
|
542
|
+
### Config File: `~/.project-compass/config.json`
|
|
543
|
+
|
|
544
|
+
```json
|
|
545
|
+
{
|
|
546
|
+
"customCommands": {
|
|
547
|
+
"/path/to/project": [
|
|
548
|
+
{ "label": "My Command", "command": ["echo", "hello"], "source": "custom" }
|
|
549
|
+
]
|
|
550
|
+
},
|
|
551
|
+
"showArtBoard": true,
|
|
552
|
+
"showHelpCards": false,
|
|
553
|
+
"showStructureGuide": false,
|
|
554
|
+
"maxVisibleProjects": 3,
|
|
555
|
+
"aiProvider": "openrouter",
|
|
556
|
+
"aiModel": "deepseek/deepseek-r1",
|
|
557
|
+
"aiToken": "your-api-token-here",
|
|
558
|
+
"projectMeta": {
|
|
559
|
+
"/path/to/project": { "port": "3000" }
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### Config Options
|
|
565
|
+
|
|
566
|
+
| Option | Type | Default | Description |
|
|
567
|
+
|--------|------|---------|-------------|
|
|
568
|
+
| `customCommands` | Object | `{}` | Per-project custom commands |
|
|
569
|
+
| `showArtBoard` | Boolean | `true` | Show/hide the art board |
|
|
570
|
+
| `showHelpCards` | Boolean | `false` | Show/hide help cards |
|
|
571
|
+
| `showStructureGuide` | Boolean | `false` | Show/hide structure guide |
|
|
572
|
+
| `maxVisibleProjects` | Number | `3` | Projects per page in navigator |
|
|
573
|
+
| `aiProvider` | String | `"openrouter"` | AI provider ID (openrouter, gemini, claude, ollama) |
|
|
574
|
+
| `aiModel` | String | `"deepseek/deepseek-r1"` | AI model to use |
|
|
575
|
+
| `aiToken` | String | `""` | API token for AI provider |
|
|
576
|
+
| `projectMeta` | Object | `{}` | Per-project metadata (ports, etc.) |
|
|
577
|
+
|
|
578
|
+
### Loading & Saving
|
|
579
|
+
|
|
580
|
+
```javascript
|
|
581
|
+
function loadConfig() {
|
|
582
|
+
try {
|
|
583
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
584
|
+
const payload = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
|
585
|
+
const parsed = JSON.parse(payload || '{}');
|
|
586
|
+
return {
|
|
587
|
+
customCommands: {},
|
|
588
|
+
showArtBoard: true,
|
|
589
|
+
showHelpCards: false,
|
|
590
|
+
showStructureGuide: false,
|
|
591
|
+
maxVisibleProjects: 3,
|
|
592
|
+
...parsed,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
} catch (error) {
|
|
596
|
+
console.error(`Ignoring corrupt config: ${error.message}`);
|
|
597
|
+
}
|
|
598
|
+
return { customCommands: {}, showArtBoard: true, showHelpCards: false, showStructureGuide: false, maxVisibleProjects: 3 };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function saveConfig(config) {
|
|
602
|
+
try {
|
|
603
|
+
ensureConfigDir();
|
|
604
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
605
|
+
} catch (error) {
|
|
606
|
+
console.error(`Unable to persist config: ${error.message}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### Project-Specific Config: `compass.config.js`
|
|
612
|
+
|
|
613
|
+
Create in your project root:
|
|
614
|
+
|
|
615
|
+
```javascript
|
|
616
|
+
export default {
|
|
617
|
+
commands: {
|
|
618
|
+
custom: {
|
|
619
|
+
label: 'My Command',
|
|
620
|
+
command: ['echo', 'hello'],
|
|
621
|
+
source: 'config'
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
frameworks: [
|
|
625
|
+
{ name: 'MyFramework', icon: '🚀' }
|
|
626
|
+
]
|
|
627
|
+
};
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
This file is:
|
|
631
|
+
1. Checked during `discoverProjects()`
|
|
632
|
+
2. Loaded via dynamic `import()` (ESM)
|
|
633
|
+
3. Merged into project data (commands + frameworks)
|
|
634
|
+
4. Applied BEFORE framework plugin detection
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
## Recent Architecture Changes (v4.3.6)
|
|
639
|
+
|
|
640
|
+
### 1. Framework Hallucination Bug FIXED
|
|
641
|
+
|
|
642
|
+
**Problem**: Projects without frameworks were showing random frameworks (e.g., simple Python project with `main.py` was detected as FastAPI).
|
|
643
|
+
|
|
644
|
+
**Root Cause**: Framework matchers in `frameworks.js` used file existence (`hasProjectFile`) instead of dependency matching.
|
|
645
|
+
|
|
646
|
+
**Fix Applied** (src/detectors/frameworks.js):
|
|
647
|
+
- `fastapi` matcher: Removed `|| hasProjectFile(project.path, 'main.py')` (line 328)
|
|
648
|
+
- `django` matcher: Removed `|| hasProjectFile(project.path, 'manage.py')` (line 370)
|
|
649
|
+
- `spring-boot` matcher: Removed `|| hasProjectFile(project.path, 'pom.xml') || hasProjectFile(project.path, 'build.gradle')`
|
|
650
|
+
- `quarkus` matcher: Removed `|| hasProjectFile(project.path, 'pom.xml')`
|
|
651
|
+
- `micronaut` matcher: Removed `|| hasProjectFile(project.path, 'pom.xml')`
|
|
652
|
+
- `rails` matcher: Changed to use `dependencyMatches(project, 'rails')` instead of file checks
|
|
653
|
+
- `sinatra` matcher: Removed `|| hasProjectFile(project.path, 'config.ru')`
|
|
654
|
+
- `.NET` matchers: Now use dependency checks instead of `*.csproj` file existence.
|
|
655
|
+
|
|
656
|
+
**Result**: Projects without explicit framework dependencies now correctly show `Frameworks: none`.
|
|
657
|
+
|
|
658
|
+
### 2. compass-config.js Integration
|
|
659
|
+
|
|
660
|
+
**Problem**: `compass-config.js` existed but was never integrated into project detection.
|
|
661
|
+
|
|
662
|
+
**Fix Applied** (src/projectDetection.js):
|
|
663
|
+
- Added import of `loadProjectConfig` in `projectDetection.js`
|
|
664
|
+
- Integrated into `discoverProjects()` function to load `compass.config.js` from project directories
|
|
665
|
+
- Project-specific commands and frameworks from `compass.config.js` are now merged into project data
|
|
666
|
+
|
|
667
|
+
### 3. AI Horizon Improvements
|
|
668
|
+
|
|
669
|
+
**Problem**: AI Horizon didn't properly show raw AI output and had poor JSON parsing.
|
|
670
|
+
|
|
671
|
+
**Fix Applied** (src/components/AIHorizon.js):
|
|
672
|
+
- Added `rawAIResponse` state to store raw AI output
|
|
673
|
+
- Improved JSON parsing to handle markdown code blocks (```json ... ```)
|
|
674
|
+
- Raw AI response is now displayed in the UI during review step
|
|
675
|
+
- Better error messages showing partial AI response if JSON parsing fails
|
|
676
|
+
|
|
677
|
+
### 4. Node.js Detector Fixed
|
|
678
|
+
|
|
679
|
+
**Problem**: `node.js` detector was adding "Node.js" as a framework.
|
|
680
|
+
|
|
681
|
+
**Fix Applied** (src/detectors/node.js):
|
|
682
|
+
- Detector now only adds framework if it's not the generic "Node.js" type
|
|
683
|
+
- Projects using plain Node.js without frameworks now show `Frameworks: none`
|
|
684
|
+
|
|
685
|
+
### 5. Framework Deduplication
|
|
686
|
+
|
|
687
|
+
**Problem**: `applyFrameworkPlugins()` could add duplicate frameworks.
|
|
688
|
+
|
|
689
|
+
**Fix Applied** (src/projectDetection.js):
|
|
690
|
+
- Added check to avoid adding duplicate frameworks
|
|
691
|
+
- Now preserves detector-detected frameworks and merges with plugin-detected ones
|
|
692
|
+
|
|
693
|
+
---
|
|
11
694
|
|
|
12
695
|
## Design Patterns
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
696
|
+
|
|
697
|
+
### 1. React-for-CLI
|
|
698
|
+
|
|
699
|
+
Leveraging React's lifecycle and state management for a terminal environment.
|
|
700
|
+
|
|
701
|
+
- **Components**: Use `React.createElement` (aliased as `create`), NOT JSX
|
|
702
|
+
- **Rendering**: Ink handles terminal rendering (not DOM)
|
|
703
|
+
- **Hooks**: Full usage of useState, useEffect, useMemo, useCallback, useRef
|
|
704
|
+
- **Events**: `useInput` from Ink for keyboard handling
|
|
705
|
+
|
|
706
|
+
### 2. Component-Driven
|
|
707
|
+
|
|
708
|
+
Each view is an isolated component in `src/components`:
|
|
709
|
+
|
|
710
|
+
- **Modular**: Each component has a single responsibility
|
|
711
|
+
- **Props**: Data flows down via props
|
|
712
|
+
- **Callbacks**: Actions flow up via callback props
|
|
713
|
+
- **Memoization**: Use `React.memo()` for performance
|
|
714
|
+
|
|
715
|
+
### 3. Async Execution
|
|
716
|
+
|
|
717
|
+
Heavy lifting (globbing, command execution) is offloaded from the main render loop to prevent UI lag:
|
|
718
|
+
|
|
719
|
+
- **Project Scanning**: `useScanner()` uses `useEffect` + async/await
|
|
720
|
+
- **Command Execution**: `execa` handles subprocesses with streaming
|
|
721
|
+
- **AI Calls**: `fetch` API with async/await
|
|
722
|
+
|
|
723
|
+
### 4. ESM Modules
|
|
724
|
+
|
|
725
|
+
All code uses ECMAScript modules (`import/export`):
|
|
726
|
+
|
|
727
|
+
```javascript
|
|
728
|
+
// Import
|
|
729
|
+
import { discoverProjects } from './projectDetection.js';
|
|
730
|
+
import { checkBinary } from './projectDetection.js';
|
|
731
|
+
|
|
732
|
+
// Export
|
|
733
|
+
export default {
|
|
734
|
+
type: 'python',
|
|
735
|
+
// ...
|
|
736
|
+
};
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
### 5. Framework Plugin System
|
|
740
|
+
|
|
741
|
+
Extensible framework detection via `~/.project-compass/plugins.json`:
|
|
742
|
+
|
|
743
|
+
- **Built-in**: 40+ frameworks in `frameworks.js`
|
|
744
|
+
- **Custom**: User-defined in `plugins.json`
|
|
745
|
+
- **Detection**: Based on `dependencyMatches()` (not file existence)
|
|
746
|
+
- **Priority**: Plugins can boost project priority
|
|
747
|
+
|
|
748
|
+
---
|
|
16
749
|
|
|
17
750
|
## Security
|
|
751
|
+
|
|
752
|
+
### 1. No Arbitrary Execution
|
|
753
|
+
|
|
18
754
|
Project Compass respects workspace boundaries and does not execute arbitrary code unless explicitly requested by the user.
|
|
19
755
|
|
|
756
|
+
- **Explicit Actions**: Commands only run when user presses B/T/R/I or Enter
|
|
757
|
+
- **Config Validation**: `compass.config.js` is loaded via ESM import (sandboxed)
|
|
758
|
+
- **No Auto-Run**: Detection does not execute any project scripts
|
|
759
|
+
|
|
760
|
+
### 2. Local Storage
|
|
761
|
+
|
|
762
|
+
API tokens are stored locally in `~/.project-compass/config.json`:
|
|
763
|
+
|
|
764
|
+
- **File Permissions**: Standard filesystem permissions apply
|
|
765
|
+
- **No Cloud**: Tokens never leave your machine except to AI provider
|
|
766
|
+
- **User Responsibility**: Users should protect their config file
|
|
767
|
+
|
|
768
|
+
### 3. Process Isolation
|
|
769
|
+
|
|
770
|
+
Background tasks are managed via `execa` with proper cleanup:
|
|
771
|
+
|
|
772
|
+
- **Detached Mode**: Unix uses detached processes for proper process group management
|
|
773
|
+
- **Kill Handling**: `SIGKILL` for forceful termination
|
|
774
|
+
- **Map Tracking**: `runningProcessMap` tracks all child processes
|
|
775
|
+
|
|
776
|
+
### 4. Input Sanitization
|
|
777
|
+
|
|
778
|
+
- **Command Tokens**: User input is split via `split(/\s+/)` and filtered
|
|
779
|
+
- **Path Resolution**: `path.resolve()` for safe path handling
|
|
780
|
+
- **No Injection**: Commands are executed as arrays (not shell strings)
|
|
781
|
+
|
|
20
782
|
---
|
|
21
|
-
|
|
783
|
+
|
|
784
|
+
## Performance
|
|
785
|
+
|
|
786
|
+
### 1. Fast Scanning
|
|
787
|
+
|
|
788
|
+
Uses `fast-glob` for high-speed project discovery:
|
|
789
|
+
|
|
790
|
+
- **Deep Scan**: Default depth of 5 directories
|
|
791
|
+
- **Ignore Patterns**: `node_modules`, `.git`, `dist`, `build`, `target`
|
|
792
|
+
- **Priority Order**: Higher priority detectors run first (fail-fast)
|
|
793
|
+
|
|
794
|
+
### 2. Non-Blocking
|
|
795
|
+
|
|
796
|
+
Heavy operations (globbing, command execution) are offloaded from the main render loop:
|
|
797
|
+
|
|
798
|
+
- **Async/Await**: All I/O operations use async/await
|
|
799
|
+
- **Streaming**: Log output streams in real-time
|
|
800
|
+
- **Timers**: Startup animation uses `setInterval` (cleaned up)
|
|
801
|
+
|
|
802
|
+
### 3. Smart Caching
|
|
803
|
+
|
|
804
|
+
Framework plugins are cached after first load:
|
|
805
|
+
|
|
806
|
+
```javascript
|
|
807
|
+
let cachedFrameworkPlugins = null;
|
|
808
|
+
|
|
809
|
+
function getFrameworkPlugins() {
|
|
810
|
+
if (cachedFrameworkPlugins) {
|
|
811
|
+
return cachedFrameworkPlugins; // Return cache
|
|
812
|
+
}
|
|
813
|
+
cachedFrameworkPlugins = [...builtInFrameworks, ...loadUserFrameworks()];
|
|
814
|
+
return cachedFrameworkPlugins;
|
|
815
|
+
}
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
### 4. Memory Efficient
|
|
819
|
+
|
|
820
|
+
Log buffers capped at 500 lines per task:
|
|
821
|
+
|
|
822
|
+
```javascript
|
|
823
|
+
const nextLogs = [...t.logs, ...newLines];
|
|
824
|
+
const updatedTask = { ...t, logs: nextLogs.length > 500 ? nextLogs.slice(-500) : nextLogs };
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
### 5. Pagination
|
|
828
|
+
|
|
829
|
+
Navigator uses pagination to handle large workspaces:
|
|
830
|
+
|
|
831
|
+
- **Configurable**: `maxVisibleProjects` (default: 3)
|
|
832
|
+
- **Page Navigation**: `PgUp/PgDn` for full page jumps
|
|
833
|
+
- **Boundary Guards**: Prevents out-of-bounds selection
|
|
834
|
+
|
|
835
|
+
---
|
|
836
|
+
|
|
837
|
+
*Built for scale and precision.*
|