plankit-cli 1.3.1 → 1.5.0
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/README.md +104 -3
- package/package.json +1 -1
- package/src/agents.js +8 -4
- package/src/analyzer/dotnetScanner.js +111 -0
- package/src/analyzer/engine.js +290 -0
- package/src/analyzer/vueScanner.js +200 -0
- package/src/cli.js +165 -5
- package/src/commandBundles.js +105 -0
- package/src/config.js +20 -2
- package/src/dashboard/dashboard.js +272 -0
- package/templates/PLANKIT.md +7 -0
- package/templates/dotnet-commands/codex/dotnet-architecture-audit.md +25 -0
- package/templates/dotnet-commands/codex/dotnet-clean-guardrails.md +24 -0
- package/templates/dotnet-commands/codex/dotnet-ef-migration.md +25 -0
- package/templates/dotnet-commands/codex/dotnet-endpoint-from-model.md +26 -0
- package/templates/dotnet-commands/codex/dotnet-extract-service.md +25 -0
- package/templates/dotnet-commands/codex/dotnet-feature.md +26 -0
- package/templates/dotnet-commands/codex/dotnet-generate-tests.md +25 -0
- package/templates/dotnet-commands/codex/dotnet-health.md +24 -0
- package/templates/dotnet-commands/codex/dotnet-migrate-minimal-api.md +25 -0
- package/templates/dotnet-commands/codex/dotnet-modernize.md +25 -0
- package/templates/dotnet-commands/cursor/dotnet-architecture-audit.md +26 -0
- package/templates/dotnet-commands/cursor/dotnet-clean-guardrails.md +25 -0
- package/templates/dotnet-commands/cursor/dotnet-ef-migration.md +26 -0
- package/templates/dotnet-commands/cursor/dotnet-endpoint-from-model.md +27 -0
- package/templates/dotnet-commands/cursor/dotnet-extract-service.md +26 -0
- package/templates/dotnet-commands/cursor/dotnet-feature.md +27 -0
- package/templates/dotnet-commands/cursor/dotnet-generate-tests.md +26 -0
- package/templates/dotnet-commands/cursor/dotnet-health.md +25 -0
- package/templates/dotnet-commands/cursor/dotnet-migrate-minimal-api.md +26 -0
- package/templates/dotnet-commands/cursor/dotnet-modernize.md +26 -0
- package/templates/dotnet-commands/opencode/dotnet-architecture-audit.md +23 -0
- package/templates/dotnet-commands/opencode/dotnet-clean-guardrails.md +22 -0
- package/templates/dotnet-commands/opencode/dotnet-ef-migration.md +23 -0
- package/templates/dotnet-commands/opencode/dotnet-endpoint-from-model.md +24 -0
- package/templates/dotnet-commands/opencode/dotnet-extract-service.md +23 -0
- package/templates/dotnet-commands/opencode/dotnet-feature.md +24 -0
- package/templates/dotnet-commands/opencode/dotnet-generate-tests.md +23 -0
- package/templates/dotnet-commands/opencode/dotnet-health.md +22 -0
- package/templates/dotnet-commands/opencode/dotnet-migrate-minimal-api.md +23 -0
- package/templates/dotnet-commands/opencode/dotnet-modernize.md +23 -0
- package/templates/dotnet-skills/gemini/dotnet-architecture-audit/SKILL.md +26 -0
- package/templates/dotnet-skills/gemini/dotnet-clean-guardrails/SKILL.md +25 -0
- package/templates/dotnet-skills/gemini/dotnet-ef-migration/SKILL.md +26 -0
- package/templates/dotnet-skills/gemini/dotnet-endpoint-from-model/SKILL.md +27 -0
- package/templates/dotnet-skills/gemini/dotnet-extract-service/SKILL.md +26 -0
- package/templates/dotnet-skills/gemini/dotnet-feature/SKILL.md +27 -0
- package/templates/dotnet-skills/gemini/dotnet-generate-tests/SKILL.md +26 -0
- package/templates/dotnet-skills/gemini/dotnet-health/SKILL.md +25 -0
- package/templates/dotnet-skills/gemini/dotnet-migrate-minimal-api/SKILL.md +26 -0
- package/templates/dotnet-skills/gemini/dotnet-modernize/SKILL.md +26 -0
- package/templates/vue-commands/codex/component-pattern-audit.md +24 -0
- package/templates/vue-commands/codex/design-token-discover.md +24 -0
- package/templates/vue-commands/codex/design-token-migrate.md +24 -0
- package/templates/vue-commands/codex/js-to-ts.md +25 -0
- package/templates/vue-commands/codex/reactivity-audit.md +25 -0
- package/templates/vue-commands/codex/state-strategy-audit.md +23 -0
- package/templates/vue-commands/codex/vue-guardrails.md +24 -0
- package/templates/vue-commands/codex/vue-health.md +28 -0
- package/templates/vue-commands/codex/vue-legacy-map.md +25 -0
- package/templates/vue-commands/codex/vue-migration-plan.md +24 -0
- package/templates/vue-commands/codex/vue-modernize-slice.md +26 -0
- package/templates/vue-commands/codex/vue-pattern-map.md +24 -0
- package/templates/vue-commands/codex/vue-standardize.md +26 -0
- package/templates/vue-commands/codex/vue-style-audit.md +24 -0
- package/templates/vue-commands/codex/vue-target-architecture.md +24 -0
- package/templates/vue-commands/cursor/component-pattern-audit.md +25 -0
- package/templates/vue-commands/cursor/design-token-discover.md +25 -0
- package/templates/vue-commands/cursor/design-token-migrate.md +25 -0
- package/templates/vue-commands/cursor/js-to-ts.md +26 -0
- package/templates/vue-commands/cursor/reactivity-audit.md +26 -0
- package/templates/vue-commands/cursor/state-strategy-audit.md +24 -0
- package/templates/vue-commands/cursor/vue-guardrails.md +25 -0
- package/templates/vue-commands/cursor/vue-health.md +29 -0
- package/templates/vue-commands/cursor/vue-legacy-map.md +26 -0
- package/templates/vue-commands/cursor/vue-migration-plan.md +25 -0
- package/templates/vue-commands/cursor/vue-modernize-slice.md +27 -0
- package/templates/vue-commands/cursor/vue-pattern-map.md +25 -0
- package/templates/vue-commands/cursor/vue-standardize.md +27 -0
- package/templates/vue-commands/cursor/vue-style-audit.md +25 -0
- package/templates/vue-commands/cursor/vue-target-architecture.md +25 -0
- package/templates/vue-commands/opencode/component-pattern-audit.md +22 -0
- package/templates/vue-commands/opencode/design-token-discover.md +22 -0
- package/templates/vue-commands/opencode/design-token-migrate.md +22 -0
- package/templates/vue-commands/opencode/js-to-ts.md +23 -0
- package/templates/vue-commands/opencode/reactivity-audit.md +23 -0
- package/templates/vue-commands/opencode/state-strategy-audit.md +21 -0
- package/templates/vue-commands/opencode/vue-guardrails.md +22 -0
- package/templates/vue-commands/opencode/vue-health.md +26 -0
- package/templates/vue-commands/opencode/vue-legacy-map.md +23 -0
- package/templates/vue-commands/opencode/vue-migration-plan.md +22 -0
- package/templates/vue-commands/opencode/vue-modernize-slice.md +24 -0
- package/templates/vue-commands/opencode/vue-pattern-map.md +22 -0
- package/templates/vue-commands/opencode/vue-standardize.md +24 -0
- package/templates/vue-commands/opencode/vue-style-audit.md +22 -0
- package/templates/vue-commands/opencode/vue-target-architecture.md +22 -0
- package/templates/vue-skills/gemini/component-pattern-audit/SKILL.md +25 -0
- package/templates/vue-skills/gemini/design-token-discover/SKILL.md +25 -0
- package/templates/vue-skills/gemini/design-token-migrate/SKILL.md +25 -0
- package/templates/vue-skills/gemini/js-to-ts/SKILL.md +26 -0
- package/templates/vue-skills/gemini/reactivity-audit/SKILL.md +26 -0
- package/templates/vue-skills/gemini/state-strategy-audit/SKILL.md +24 -0
- package/templates/vue-skills/gemini/vue-guardrails/SKILL.md +25 -0
- package/templates/vue-skills/gemini/vue-health/SKILL.md +29 -0
- package/templates/vue-skills/gemini/vue-legacy-map/SKILL.md +26 -0
- package/templates/vue-skills/gemini/vue-migration-plan/SKILL.md +25 -0
- package/templates/vue-skills/gemini/vue-modernize-slice/SKILL.md +27 -0
- package/templates/vue-skills/gemini/vue-pattern-map/SKILL.md +25 -0
- package/templates/vue-skills/gemini/vue-standardize/SKILL.md +27 -0
- package/templates/vue-skills/gemini/vue-style-audit/SKILL.md +25 -0
- package/templates/vue-skills/gemini/vue-target-architecture/SKILL.md +25 -0
package/README.md
CHANGED
|
@@ -58,12 +58,14 @@ plankit review my-feature
|
|
|
58
58
|
|
|
59
59
|
| Command | Action |
|
|
60
60
|
|---|---|
|
|
61
|
-
| `plankit init [--agent <name>]` | Create PlanKit config, artifact folders, spec, and the **selected agent's** folders/commands. Prompts for the agent when
|
|
61
|
+
| `plankit init [--agent <name>] [--framework <name>]` | Create PlanKit config, artifact folders, spec, and the **selected agent's** folders/commands. Prompts for the agent and framework suite(s) when run interactively. |
|
|
62
62
|
| `plankit plan <feature> [requirements]` | Create `artifacts/current/<feature>/` with README, phase specs, outputs folder, and state metadata. |
|
|
63
63
|
| `plankit clarify <feature> [phase]` | Create a clarification artifact for requirements and design decisions. |
|
|
64
64
|
| `plankit implement <feature> <phase>` | Validate phase context (spec + prior outputs) and prepare `outputs/phase-N-output.md`. |
|
|
65
65
|
| `plankit review <feature>` | Run verification commands, mark README archived, and move the feature to `artifacts/archived/`. |
|
|
66
66
|
| `plankit status [--json]` | List active and archived features. |
|
|
67
|
+
| `plankit scan [dir] [--json] [--threshold <n>]` | Run zero-dependency static analysis, audit health (0-100), extract design tokens, and generate `artifacts/.plankit-index.json`. |
|
|
68
|
+
| `plankit ui [--no-tui]` | Launch interactive terminal feature progress matrix & action dashboard. |
|
|
67
69
|
| `plankit archive <feature>` | Move a feature to archived without running verification. |
|
|
68
70
|
| `plankit version` / `--version` / `-v` | Print the installed version (`plankit-cli vX.Y.Z`). |
|
|
69
71
|
| `plankit help` / `--help` / `-h` | Show usage. |
|
|
@@ -77,6 +79,11 @@ plankit review my-feature
|
|
|
77
79
|
| `--force` | Overwrite generated files where safe. |
|
|
78
80
|
| `--dry-run` | Show intended actions without writing anything. |
|
|
79
81
|
| `--agent <name>` | Select your coding agent during `init` (`opencode`, `gemini`, `codex`, `cursor`; default `opencode`). |
|
|
82
|
+
| `--framework <name>` | Select framework command suite during `init` (`angular`, `vue`, `dotnet`, `none`, `all`; default `none`). |
|
|
83
|
+
| `--frameworks <list>` | Comma-separated list of framework command suites (e.g. `vue,dotnet`). |
|
|
84
|
+
| `--threshold <score>` | Fail `scan` command with exit code 1 if health score is below threshold (CI quality gate). |
|
|
85
|
+
| `--json` | Output `scan` or `status` as machine-readable JSON. |
|
|
86
|
+
| `--no-tui` | Render static ASCII table instead of interactive dashboard for `ui` command. |
|
|
80
87
|
|
|
81
88
|
### Plan options
|
|
82
89
|
|
|
@@ -162,7 +169,101 @@ plankit init --agent cursor
|
|
|
162
169
|
|
|
163
170
|
---
|
|
164
171
|
|
|
165
|
-
##
|
|
172
|
+
## Static analysis engine (`plankit scan`)
|
|
173
|
+
|
|
174
|
+
PlanKit ships with an ultra-fast, **zero-dependency static analyzer** that audits codebase modernization health without burning LLM tokens:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# Run audit and output terminal report + save artifacts/.plankit-index.json
|
|
178
|
+
plankit scan
|
|
179
|
+
|
|
180
|
+
# Output machine-readable JSON metrics
|
|
181
|
+
plankit scan --json
|
|
182
|
+
|
|
183
|
+
# CI Quality Gate: fail if health score drops below threshold
|
|
184
|
+
plankit scan --threshold 80
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### What it checks:
|
|
188
|
+
- **Vue SFC Architecture**: Options API vs `<script setup>` ratio, TypeScript vs JavaScript, Pinia vs Vuex vs composables.
|
|
189
|
+
- **Design Token Mining**: Automatically detects repeated hex colors, font sizes, margins, and paddings across templates and styles.
|
|
190
|
+
- **.NET Architecture**: Target frameworks, layer dependency boundaries (Clean Architecture / Vertical Slices), and Minimal APIs vs Controllers.
|
|
191
|
+
- **Modernization Hotspots**: Flags god components (> 400 lines), deprecated deep selectors (`::v-deep`, `/deep/`), mixins, and layer boundary violations.
|
|
192
|
+
- **Index Generation**: Saves `artifacts/.plankit-index.json` so AI agents can query repository intelligence immediately.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Interactive feature dashboard (`plankit ui`)
|
|
197
|
+
|
|
198
|
+
Launch a terminal UI to visualize active features, phase completion status, and trigger development workflows with single keystrokes:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# Launch interactive TUI
|
|
202
|
+
plankit ui
|
|
203
|
+
|
|
204
|
+
# Non-interactive / CI ASCII table output
|
|
205
|
+
plankit ui --no-tui
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Interactive Actions:
|
|
209
|
+
- **`[↑/↓]` or `[j/k]`**: Navigate between active features.
|
|
210
|
+
- **`[i]`**: Prepare and validate implementation context for the next pending phase.
|
|
211
|
+
- **`[c]`**: Generate a design alignment and clarification artifact.
|
|
212
|
+
- **`[r]`**: Review, verify DoD, and archive completed feature.
|
|
213
|
+
- **`[s]`**: Run the repository static scan and view live health scorecard.
|
|
214
|
+
- **`[q]`**: Exit dashboard.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Framework command suites
|
|
219
|
+
|
|
220
|
+
During `plankit init`, PlanKit prompts you to choose optional framework-specific command suites (or pass `--framework <name>` / `--frameworks <names>`):
|
|
221
|
+
- **None**: Installs only the core PlanKit feature planning workflow.
|
|
222
|
+
- **Angular**: Installs 17 Angular engineering workflows.
|
|
223
|
+
- **Vue**: Installs 15 Vue modernization and architectural standardization workflows.
|
|
224
|
+
- **.NET**: Installs 10 .NET clean architecture and API engineering workflows.
|
|
225
|
+
- **All**: Installs all supported framework suites.
|
|
226
|
+
|
|
227
|
+
### Vue modernization suite (`--framework vue`)
|
|
228
|
+
|
|
229
|
+
Designed for legacy and mixed Vue codebases (JavaScript/TypeScript, Options API, Composition API, `<script setup>`, Vuex/Pinia) following the PlanKit Vue Modernization Specification:
|
|
230
|
+
|
|
231
|
+
| Command | Purpose |
|
|
232
|
+
|---|---|
|
|
233
|
+
| `vue-health` | Audit Vue codebase health, tech debt score, Options vs Composition ratio, and modernization risks. |
|
|
234
|
+
| `vue-pattern-map` | Map architectural patterns across state, data fetching, styling, and components. |
|
|
235
|
+
| `vue-target-architecture` | Define or inspect target architectural conventions (Vue 3, `<script setup>`, TS, Pinia). |
|
|
236
|
+
| `vue-legacy-map` | Catalog legacy Vue files, mixins, filters, Options API components, and hotspots. |
|
|
237
|
+
| `vue-standardize` | Refactor a component or composable to conform to the established target architecture. |
|
|
238
|
+
| `vue-migration-plan` | Generate an incremental, phased migration plan for a legacy feature slice. |
|
|
239
|
+
| `js-to-ts` | Convert JavaScript Vue SFCs and utility modules to TypeScript with typed props/emits. |
|
|
240
|
+
| `vue-style-audit` | Audit styling patterns, global vs scoped CSS/SCSS, deep selectors, and hardcoded values. |
|
|
241
|
+
| `design-token-discover` | Discover repeated color literals, typography, spacing, and shadows into token candidates. |
|
|
242
|
+
| `design-token-migrate` | Migrate hardcoded styles in Vue components to standardized design tokens. |
|
|
243
|
+
| `state-strategy-audit` | Audit Vuex vs Pinia vs composables; detect state duplication and reactivity leaks. |
|
|
244
|
+
| `reactivity-audit` | Audit reactivity anti-patterns (destructuring props without `toRefs`, watcher leaks). |
|
|
245
|
+
| `component-pattern-audit` | Audit SFC component design, god components, prop drilling, and emits declarations. |
|
|
246
|
+
| `vue-modernize-slice` | Orchestrate an end-to-end modernization of a Vue feature slice with validation. |
|
|
247
|
+
| `vue-guardrails` | Configure ESLint rules and TypeScript strict settings to prevent legacy regressions. |
|
|
248
|
+
|
|
249
|
+
### .NET clean architecture suite (`--framework dotnet`)
|
|
250
|
+
|
|
251
|
+
Designed for ASP.NET Core and .NET solutions following Clean Architecture and Vertical Slice patterns:
|
|
252
|
+
|
|
253
|
+
| Command | Purpose |
|
|
254
|
+
|---|---|
|
|
255
|
+
| `dotnet-health` | Audit solution health: SDK versions, NuGet packages, vulnerabilities, and compiler warnings. |
|
|
256
|
+
| `dotnet-architecture-audit` | Audit layer dependencies (Domain, Application, Infrastructure, API) and circular references. |
|
|
257
|
+
| `dotnet-feature` | Build an end-to-end vertical slice feature (Command/Query, MediatR, FluentValidation, Endpoint). |
|
|
258
|
+
| `dotnet-endpoint-from-model` | Generate ASP.NET Core Minimal API or Controller endpoints with typed results and error handling. |
|
|
259
|
+
| `dotnet-migrate-minimal-api` | Migrate traditional controllers to modern Minimal API route groups. |
|
|
260
|
+
| `dotnet-ef-migration` | Safe Entity Framework Core migration lifecycle: entity check, migration, SQL preview, rollback. |
|
|
261
|
+
| `dotnet-extract-service` | Refactor fat services into isolated CQRS handlers or domain services. |
|
|
262
|
+
| `dotnet-generate-tests` | Generate xUnit/NUnit tests with FluentAssertions and WebApplicationFactory integration tests. |
|
|
263
|
+
| `dotnet-clean-guardrails` | Generate NetArchTest architectural unit tests and EditorConfig rules to guard layer boundaries. |
|
|
264
|
+
| `dotnet-modernize` | Upgrade legacy C# syntax to modern C# 12+ / .NET 8+ patterns (primary constructors, collection expressions). |
|
|
265
|
+
|
|
266
|
+
### Angular command suite (`--framework angular`)
|
|
166
267
|
|
|
167
268
|
Alongside the phase workflow, `init` installs a per-agent Angular command library that gives your AI assistant **16 deterministic Angular engineering workflows**, driven by repository discovery rather than generic prompts:
|
|
168
269
|
|
|
@@ -187,7 +288,7 @@ Alongside the phase workflow, `init` installs a per-agent Angular command librar
|
|
|
187
288
|
|
|
188
289
|
Plus a shared `angular-project-discovery` reference that every command leans on (Angular version, standalone vs NgModule, Signals/RxJS, state management, forms strategy, UI library, styling, testing, conventions, architecture).
|
|
189
290
|
|
|
190
|
-
Only your **selected agent's** folder/commands are copied. To customize the
|
|
291
|
+
Only your **selected agent's** folder/commands are copied. To customize the command templates in your own `templatesDir`, mirror the `templates/<framework>-commands/` and `templates/<framework>-skills/` folder layout from the package.
|
|
191
292
|
|
|
192
293
|
---
|
|
193
294
|
|
package/package.json
CHANGED
package/src/agents.js
CHANGED
|
@@ -33,7 +33,7 @@ export const AGENTS = {
|
|
|
33
33
|
{ template: 'opencode/plankit-implement.md', dest: ['.opencode', 'commands', 'plankit-implement.md'] },
|
|
34
34
|
{ template: 'opencode/plankit-review.md', dest: ['.opencode', 'commands', 'plankit-review.md'] }
|
|
35
35
|
],
|
|
36
|
-
commandBundles: [
|
|
36
|
+
commandBundles: [],
|
|
37
37
|
globalSkillEntries: null
|
|
38
38
|
},
|
|
39
39
|
gemini: {
|
|
@@ -44,7 +44,7 @@ export const AGENTS = {
|
|
|
44
44
|
bindings: [
|
|
45
45
|
{ template: 'gemini-instructions.md', dest: ['.gemini', 'instructions.md'] }
|
|
46
46
|
],
|
|
47
|
-
commandBundles: [
|
|
47
|
+
commandBundles: [],
|
|
48
48
|
globalSkillEntries: GEMINI_SKILL_ENTRIES
|
|
49
49
|
},
|
|
50
50
|
codex: {
|
|
@@ -55,7 +55,7 @@ export const AGENTS = {
|
|
|
55
55
|
bindings: [
|
|
56
56
|
{ template: 'AGENTS.md', dest: ['AGENTS.md'] }
|
|
57
57
|
],
|
|
58
|
-
commandBundles: [
|
|
58
|
+
commandBundles: [],
|
|
59
59
|
globalSkillEntries: null
|
|
60
60
|
},
|
|
61
61
|
cursor: {
|
|
@@ -66,11 +66,15 @@ export const AGENTS = {
|
|
|
66
66
|
bindings: [
|
|
67
67
|
{ template: 'cursorrules', dest: ['.cursorrules'] }
|
|
68
68
|
],
|
|
69
|
-
commandBundles: [
|
|
69
|
+
commandBundles: [],
|
|
70
70
|
globalSkillEntries: null
|
|
71
71
|
}
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
+
export function resolveAgentBundles(agentId, frameworks = []) {
|
|
75
|
+
return (frameworks || []).map((fw) => `${agentId}-${fw}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
74
78
|
export function listAgents() {
|
|
75
79
|
return AGENT_ORDER.map((id) => AGENTS[id]);
|
|
76
80
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fast static analyzer for .NET projects and solutions (.sln, .csproj, .cs).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export function analyzeCsprojFile(filePath, content = null) {
|
|
9
|
+
const code = content !== null ? content : fs.readFileSync(filePath, 'utf8');
|
|
10
|
+
const fileName = path.basename(filePath);
|
|
11
|
+
const projectName = fileName.replace(/\.csproj$/i, '');
|
|
12
|
+
|
|
13
|
+
// Target Framework
|
|
14
|
+
const tfMatch = /<TargetFramework(?:s)?>([^<]+)<\/TargetFramework(?:s)?>/i.exec(code);
|
|
15
|
+
const targetFramework = tfMatch ? tfMatch[1].trim() : 'unknown';
|
|
16
|
+
|
|
17
|
+
// Nullable
|
|
18
|
+
const nullableMatch = /<Nullable>([^<]+)<\/Nullable>/i.exec(code);
|
|
19
|
+
const nullable = nullableMatch ? nullableMatch[1].trim().toLowerCase() === 'enable' : false;
|
|
20
|
+
|
|
21
|
+
// Project References
|
|
22
|
+
const projectReferences = [];
|
|
23
|
+
const refRegex = /<ProjectReference\s+Include=["']([^"']+)["']/gi;
|
|
24
|
+
let match;
|
|
25
|
+
while ((match = refRegex.exec(code)) !== null) {
|
|
26
|
+
const refPath = match[1].replace(/\\/g, '/');
|
|
27
|
+
const refProject = path.basename(refPath).replace(/\.csproj$/i, '');
|
|
28
|
+
projectReferences.push(refProject);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Package References
|
|
32
|
+
const packageReferences = [];
|
|
33
|
+
const pkgRegex = /<PackageReference\s+Include=["']([^"']+)["'](?:\s+Version=["']([^"']+)["'])?/gi;
|
|
34
|
+
while ((match = pkgRegex.exec(code)) !== null) {
|
|
35
|
+
packageReferences.push({
|
|
36
|
+
name: match[1],
|
|
37
|
+
version: match[2] || 'unknown'
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Determine Layer (Clean Architecture heuristics)
|
|
42
|
+
let layer = 'other';
|
|
43
|
+
const lowerName = projectName.toLowerCase();
|
|
44
|
+
if (lowerName.includes('domain') || lowerName.includes('core')) {
|
|
45
|
+
layer = 'domain';
|
|
46
|
+
} else if (lowerName.includes('application') || lowerName.includes('usecase')) {
|
|
47
|
+
layer = 'application';
|
|
48
|
+
} else if (lowerName.includes('infrastructure') || lowerName.includes('data') || lowerName.includes('persistence')) {
|
|
49
|
+
layer = 'infrastructure';
|
|
50
|
+
} else if (lowerName.includes('api') || lowerName.includes('web') || lowerName.includes('server')) {
|
|
51
|
+
layer = 'api';
|
|
52
|
+
} else if (lowerName.includes('test')) {
|
|
53
|
+
layer = 'tests';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Layer Boundary Violations
|
|
57
|
+
const violations = [];
|
|
58
|
+
if (layer === 'domain') {
|
|
59
|
+
for (const ref of projectReferences) {
|
|
60
|
+
const lowerRef = ref.toLowerCase();
|
|
61
|
+
if (lowerRef.includes('infra') || lowerRef.includes('api') || lowerRef.includes('web') || lowerRef.includes('app')) {
|
|
62
|
+
violations.push({
|
|
63
|
+
type: 'domain_layer_violation',
|
|
64
|
+
message: `Domain project "${projectName}" illegally references outer layer "${ref}"`
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} else if (layer === 'application') {
|
|
69
|
+
for (const ref of projectReferences) {
|
|
70
|
+
const lowerRef = ref.toLowerCase();
|
|
71
|
+
if (lowerRef.includes('infra') || lowerRef.includes('api') || lowerRef.includes('web')) {
|
|
72
|
+
violations.push({
|
|
73
|
+
type: 'application_layer_violation',
|
|
74
|
+
message: `Application project "${projectName}" illegally references outer layer "${ref}"`
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
filePath,
|
|
82
|
+
projectName,
|
|
83
|
+
targetFramework,
|
|
84
|
+
nullable,
|
|
85
|
+
layer,
|
|
86
|
+
projectReferences,
|
|
87
|
+
packageReferences,
|
|
88
|
+
violations
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function analyzeCSharpFile(filePath, content = null) {
|
|
93
|
+
const code = content !== null ? content : fs.readFileSync(filePath, 'utf8');
|
|
94
|
+
const lines = code.split(/\r?\n/).length;
|
|
95
|
+
|
|
96
|
+
const isMinimalApi = /\bapp\.Map(?:Get|Post|Put|Delete|Patch|Group)\b/.test(code) ||
|
|
97
|
+
/\bgroup\.Map(?:Get|Post|Put|Delete|Patch)\b/.test(code);
|
|
98
|
+
const isController = /:\s*(?:ControllerBase|Controller)\b/.test(code) || /\[ApiController\]/.test(code);
|
|
99
|
+
const hasFileScopedNamespace = /^namespace\s+[\w.]+\s*;/m.test(code);
|
|
100
|
+
const hasPrimaryConstructor = /(?:public|internal|private)\s+(?:class|record|struct)\s+\w+\s*\([^)]*\)\s*(?::|\{)/.test(code);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
filePath,
|
|
104
|
+
fileName: path.basename(filePath),
|
|
105
|
+
lines,
|
|
106
|
+
isMinimalApi,
|
|
107
|
+
isController,
|
|
108
|
+
hasFileScopedNamespace,
|
|
109
|
+
hasPrimaryConstructor
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { analyzeVueFile } from './vueScanner.js';
|
|
4
|
+
import { analyzeCSharpFile, analyzeCsprojFile } from './dotnetScanner.js';
|
|
5
|
+
|
|
6
|
+
const IGNORED_DIRS = new Set([
|
|
7
|
+
'node_modules',
|
|
8
|
+
'.git',
|
|
9
|
+
'dist',
|
|
10
|
+
'build',
|
|
11
|
+
'bin',
|
|
12
|
+
'obj',
|
|
13
|
+
'coverage',
|
|
14
|
+
'.gemini',
|
|
15
|
+
'.opencode',
|
|
16
|
+
'.codex',
|
|
17
|
+
'artifacts'
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export async function runAnalysis(targetDir, options = {}) {
|
|
21
|
+
const rootDir = path.resolve(targetDir);
|
|
22
|
+
const files = collectScannableFiles(rootDir);
|
|
23
|
+
|
|
24
|
+
const vueResults = [];
|
|
25
|
+
const csprojResults = [];
|
|
26
|
+
const csharpResults = [];
|
|
27
|
+
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
const ext = path.extname(file).toLowerCase();
|
|
30
|
+
if (ext === '.vue') {
|
|
31
|
+
vueResults.push(analyzeVueFile(file));
|
|
32
|
+
} else if (ext === '.csproj') {
|
|
33
|
+
csprojResults.push(analyzeCsprojFile(file));
|
|
34
|
+
} else if (ext === '.cs') {
|
|
35
|
+
csharpResults.push(analyzeCSharpFile(file));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const score = calculateHealthScore(vueResults, csprojResults, csharpResults);
|
|
40
|
+
const tokenCandidates = aggregateTokens(vueResults);
|
|
41
|
+
const hotspots = aggregateHotspots(vueResults, csprojResults);
|
|
42
|
+
|
|
43
|
+
const report = {
|
|
44
|
+
timestamp: new Date().toISOString(),
|
|
45
|
+
rootDir,
|
|
46
|
+
totalFilesScanned: files.length,
|
|
47
|
+
score,
|
|
48
|
+
vue: summarizeVueMetrics(vueResults),
|
|
49
|
+
dotnet: summarizeDotnetMetrics(csprojResults, csharpResults),
|
|
50
|
+
designTokens: tokenCandidates,
|
|
51
|
+
hotspots
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (options.saveIndex !== false) {
|
|
55
|
+
saveIndexFile(rootDir, report, options.artifactsDir || 'artifacts');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return report;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function formatReport(report, useColors = true) {
|
|
62
|
+
const c = useColors
|
|
63
|
+
? {
|
|
64
|
+
reset: '\x1b[0m',
|
|
65
|
+
bold: '\x1b[1m',
|
|
66
|
+
green: '\x1b[32m',
|
|
67
|
+
yellow: '\x1b[33m',
|
|
68
|
+
red: '\x1b[31m',
|
|
69
|
+
cyan: '\x1b[36m',
|
|
70
|
+
dim: '\x1b[2m'
|
|
71
|
+
}
|
|
72
|
+
: {
|
|
73
|
+
reset: '',
|
|
74
|
+
bold: '',
|
|
75
|
+
green: '',
|
|
76
|
+
yellow: '',
|
|
77
|
+
red: '',
|
|
78
|
+
cyan: '',
|
|
79
|
+
dim: ''
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const scoreColor = report.score.total >= 80 ? c.green : (report.score.total >= 50 ? c.yellow : c.red);
|
|
83
|
+
const out = [];
|
|
84
|
+
|
|
85
|
+
out.push(`${c.bold}======================================================${c.reset}`);
|
|
86
|
+
out.push(`${c.bold} PlanKit Repository Health Audit ${c.reset}`);
|
|
87
|
+
out.push(`${c.bold}======================================================${c.reset}`);
|
|
88
|
+
out.push(`Root Directory: ${c.cyan}${report.rootDir}${c.reset}`);
|
|
89
|
+
out.push(`Files Scanned: ${report.totalFilesScanned}`);
|
|
90
|
+
out.push(`Health Score: ${scoreColor}${c.bold}${report.score.total} / 100${c.reset} (${report.score.grade})`);
|
|
91
|
+
out.push('');
|
|
92
|
+
|
|
93
|
+
// Vue summary if Vue files detected
|
|
94
|
+
if (report.vue && report.vue.totalSfc > 0) {
|
|
95
|
+
out.push(`${c.bold}Vue Architecture & Modernization Metrics:${c.reset}`);
|
|
96
|
+
out.push(` SFC Count: ${report.vue.totalSfc}`);
|
|
97
|
+
out.push(` <script setup> Ratio: ${c.green}${report.vue.scriptSetupPercent}%${c.reset} (${report.vue.scriptSetupCount}/${report.vue.totalSfc})`);
|
|
98
|
+
out.push(` TypeScript Ratio: ${c.green}${report.vue.tsPercent}%${c.reset} (${report.vue.tsCount}/${report.vue.totalSfc})`);
|
|
99
|
+
out.push(` Pinia / Modern State: ${report.vue.piniaCount} files (Vuex legacy: ${report.vue.vuexCount})`);
|
|
100
|
+
out.push(` Maturity Distribution: ${c.green}Preferred: ${report.vue.maturity.preferred}${c.reset} | ${c.yellow}Transitional: ${report.vue.maturity.transitional}${c.reset} | ${c.red}Legacy: ${report.vue.maturity.legacy}${c.reset}`);
|
|
101
|
+
out.push('');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// .NET summary if .NET files detected
|
|
105
|
+
if (report.dotnet && (report.dotnet.projectsCount > 0 || report.dotnet.csharpFilesCount > 0)) {
|
|
106
|
+
out.push(`${c.bold}.NET Solution Architecture Metrics:${c.reset}`);
|
|
107
|
+
out.push(` Projects: ${report.dotnet.projectsCount}`);
|
|
108
|
+
out.push(` C# Source Files: ${report.dotnet.csharpFilesCount}`);
|
|
109
|
+
out.push(` Minimal APIs: ${report.dotnet.minimalApiCount} | Controllers: ${report.dotnet.controllerCount}`);
|
|
110
|
+
if (report.dotnet.layerViolations.length > 0) {
|
|
111
|
+
out.push(` ${c.red}Layer Boundary Violations:${c.reset} ${report.dotnet.layerViolations.length}`);
|
|
112
|
+
}
|
|
113
|
+
out.push('');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Token candidates
|
|
117
|
+
if (report.designTokens && report.designTokens.frequentColors.length > 0) {
|
|
118
|
+
out.push(`${c.bold}Top Discovered Design Token Candidates (Colors):${c.reset}`);
|
|
119
|
+
for (const item of report.designTokens.frequentColors.slice(0, 5)) {
|
|
120
|
+
out.push(` ${c.cyan}${item.color}${c.reset} (used in ${item.count} component/style rules)`);
|
|
121
|
+
}
|
|
122
|
+
out.push('');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Hotspots
|
|
126
|
+
if (report.hotspots.length > 0) {
|
|
127
|
+
out.push(`${c.bold}Modernization Hotspots & Anti-Patterns (${report.hotspots.length}):${c.reset}`);
|
|
128
|
+
for (const h of report.hotspots.slice(0, 8)) {
|
|
129
|
+
out.push(` - ${c.yellow}${h.file}${c.reset}: ${h.message}`);
|
|
130
|
+
}
|
|
131
|
+
if (report.hotspots.length > 8) {
|
|
132
|
+
out.push(` ${c.dim}... and ${report.hotspots.length - 8} more in artifacts/.plankit-index.json${c.reset}`);
|
|
133
|
+
}
|
|
134
|
+
out.push('');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
out.push(`${c.bold}======================================================${c.reset}`);
|
|
138
|
+
out.push(`Index saved to ${c.cyan}artifacts/.plankit-index.json${c.reset}`);
|
|
139
|
+
return out.join('\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function collectScannableFiles(dir, files = []) {
|
|
143
|
+
if (!fs.existsSync(dir)) return files;
|
|
144
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
145
|
+
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
if (IGNORED_DIRS.has(entry.name) || entry.name.startsWith('.')) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const fullPath = path.join(dir, entry.name);
|
|
151
|
+
if (entry.isDirectory()) {
|
|
152
|
+
collectScannableFiles(fullPath, files);
|
|
153
|
+
} else if (entry.isFile()) {
|
|
154
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
155
|
+
if (['.vue', '.csproj', '.cs', '.ts', '.js'].includes(ext)) {
|
|
156
|
+
files.push(fullPath);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return files;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function calculateHealthScore(vueResults, csprojResults, csharpResults) {
|
|
165
|
+
let score = 100;
|
|
166
|
+
|
|
167
|
+
if (vueResults.length > 0) {
|
|
168
|
+
const total = vueResults.length;
|
|
169
|
+
const scriptSetup = vueResults.filter((r) => r.vueStyle === 'script_setup').length;
|
|
170
|
+
const ts = vueResults.filter((r) => r.language === 'ts').length;
|
|
171
|
+
const legacy = vueResults.filter((r) => r.maturity === 'legacy').length;
|
|
172
|
+
const vuex = vueResults.filter((r) => r.state === 'vuex').length;
|
|
173
|
+
|
|
174
|
+
// Deduct for non-script setup ratio
|
|
175
|
+
score -= Math.round(((total - scriptSetup) / total) * 35);
|
|
176
|
+
// Deduct for JS ratio (lack of TS)
|
|
177
|
+
score -= Math.round(((total - ts) / total) * 20);
|
|
178
|
+
// Deduct for legacy Vuex
|
|
179
|
+
if (vuex > 0) {
|
|
180
|
+
score -= Math.min(15, vuex * 3);
|
|
181
|
+
}
|
|
182
|
+
// Deduct for legacy anti-patterns (mixins, filters, etc.)
|
|
183
|
+
score -= Math.min(20, legacy * 4);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (csprojResults.length > 0) {
|
|
187
|
+
const violations = csprojResults.flatMap((r) => r.violations).length;
|
|
188
|
+
score -= Math.min(25, violations * 10);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const finalScore = Math.max(0, Math.min(100, score));
|
|
192
|
+
let grade = 'A';
|
|
193
|
+
if (finalScore < 60) grade = 'F';
|
|
194
|
+
else if (finalScore < 70) grade = 'D';
|
|
195
|
+
else if (finalScore < 80) grade = 'C';
|
|
196
|
+
else if (finalScore < 90) grade = 'B';
|
|
197
|
+
|
|
198
|
+
return { total: finalScore, grade };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function summarizeVueMetrics(vueResults) {
|
|
202
|
+
const totalSfc = vueResults.length;
|
|
203
|
+
if (totalSfc === 0) return null;
|
|
204
|
+
|
|
205
|
+
const scriptSetupCount = vueResults.filter((r) => r.vueStyle === 'script_setup').length;
|
|
206
|
+
const tsCount = vueResults.filter((r) => r.language === 'ts').length;
|
|
207
|
+
const piniaCount = vueResults.filter((r) => r.state === 'pinia').length;
|
|
208
|
+
const vuexCount = vueResults.filter((r) => r.state === 'vuex').length;
|
|
209
|
+
|
|
210
|
+
const maturity = {
|
|
211
|
+
preferred: vueResults.filter((r) => r.maturity === 'preferred').length,
|
|
212
|
+
transitional: vueResults.filter((r) => r.maturity === 'transitional').length,
|
|
213
|
+
legacy: vueResults.filter((r) => r.maturity === 'legacy').length
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
totalSfc,
|
|
218
|
+
scriptSetupCount,
|
|
219
|
+
scriptSetupPercent: Math.round((scriptSetupCount / totalSfc) * 100),
|
|
220
|
+
tsCount,
|
|
221
|
+
tsPercent: Math.round((tsCount / totalSfc) * 100),
|
|
222
|
+
piniaCount,
|
|
223
|
+
vuexCount,
|
|
224
|
+
maturity
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function summarizeDotnetMetrics(csprojResults, csharpResults) {
|
|
229
|
+
if (csprojResults.length === 0 && csharpResults.length === 0) return null;
|
|
230
|
+
|
|
231
|
+
const layerViolations = csprojResults.flatMap((r) => r.violations);
|
|
232
|
+
const minimalApiCount = csharpResults.filter((r) => r.isMinimalApi).length;
|
|
233
|
+
const controllerCount = csharpResults.filter((r) => r.isController).length;
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
projectsCount: csprojResults.length,
|
|
237
|
+
csharpFilesCount: csharpResults.length,
|
|
238
|
+
minimalApiCount,
|
|
239
|
+
controllerCount,
|
|
240
|
+
layerViolations
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function aggregateTokens(vueResults) {
|
|
245
|
+
const colorCounts = new Map();
|
|
246
|
+
const spacingCounts = new Map();
|
|
247
|
+
|
|
248
|
+
for (const r of vueResults) {
|
|
249
|
+
for (const color of r.tokens.hexColors) {
|
|
250
|
+
colorCounts.set(color, (colorCounts.get(color) || 0) + 1);
|
|
251
|
+
}
|
|
252
|
+
for (const spacing of r.tokens.spacing) {
|
|
253
|
+
spacingCounts.set(spacing, (spacingCounts.get(spacing) || 0) + 1);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const frequentColors = Array.from(colorCounts.entries())
|
|
258
|
+
.map(([color, count]) => ({ color, count }))
|
|
259
|
+
.sort((a, b) => b.count - a.count);
|
|
260
|
+
|
|
261
|
+
const frequentSpacing = Array.from(spacingCounts.entries())
|
|
262
|
+
.map(([spacing, count]) => ({ spacing, count }))
|
|
263
|
+
.sort((a, b) => b.count - a.count);
|
|
264
|
+
|
|
265
|
+
return { frequentColors, frequentSpacing };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function aggregateHotspots(vueResults, csprojResults) {
|
|
269
|
+
const hotspots = [];
|
|
270
|
+
for (const r of vueResults) {
|
|
271
|
+
for (const h of r.hotspots) {
|
|
272
|
+
hotspots.push({ file: path.basename(r.filePath), relativePath: r.filePath, ...h });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (const p of csprojResults) {
|
|
276
|
+
for (const v of p.violations) {
|
|
277
|
+
hotspots.push({ file: path.basename(p.filePath), relativePath: p.filePath, ...v });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return hotspots;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function saveIndexFile(rootDir, report, artifactsDirName = 'artifacts') {
|
|
284
|
+
const targetDir = path.join(rootDir, artifactsDirName);
|
|
285
|
+
if (!fs.existsSync(targetDir)) {
|
|
286
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
287
|
+
}
|
|
288
|
+
const indexPath = path.join(targetDir, '.plankit-index.json');
|
|
289
|
+
fs.writeFileSync(indexPath, JSON.stringify(report, null, 2), 'utf8');
|
|
290
|
+
}
|