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.
Files changed (111) hide show
  1. package/README.md +104 -3
  2. package/package.json +1 -1
  3. package/src/agents.js +8 -4
  4. package/src/analyzer/dotnetScanner.js +111 -0
  5. package/src/analyzer/engine.js +290 -0
  6. package/src/analyzer/vueScanner.js +200 -0
  7. package/src/cli.js +165 -5
  8. package/src/commandBundles.js +105 -0
  9. package/src/config.js +20 -2
  10. package/src/dashboard/dashboard.js +272 -0
  11. package/templates/PLANKIT.md +7 -0
  12. package/templates/dotnet-commands/codex/dotnet-architecture-audit.md +25 -0
  13. package/templates/dotnet-commands/codex/dotnet-clean-guardrails.md +24 -0
  14. package/templates/dotnet-commands/codex/dotnet-ef-migration.md +25 -0
  15. package/templates/dotnet-commands/codex/dotnet-endpoint-from-model.md +26 -0
  16. package/templates/dotnet-commands/codex/dotnet-extract-service.md +25 -0
  17. package/templates/dotnet-commands/codex/dotnet-feature.md +26 -0
  18. package/templates/dotnet-commands/codex/dotnet-generate-tests.md +25 -0
  19. package/templates/dotnet-commands/codex/dotnet-health.md +24 -0
  20. package/templates/dotnet-commands/codex/dotnet-migrate-minimal-api.md +25 -0
  21. package/templates/dotnet-commands/codex/dotnet-modernize.md +25 -0
  22. package/templates/dotnet-commands/cursor/dotnet-architecture-audit.md +26 -0
  23. package/templates/dotnet-commands/cursor/dotnet-clean-guardrails.md +25 -0
  24. package/templates/dotnet-commands/cursor/dotnet-ef-migration.md +26 -0
  25. package/templates/dotnet-commands/cursor/dotnet-endpoint-from-model.md +27 -0
  26. package/templates/dotnet-commands/cursor/dotnet-extract-service.md +26 -0
  27. package/templates/dotnet-commands/cursor/dotnet-feature.md +27 -0
  28. package/templates/dotnet-commands/cursor/dotnet-generate-tests.md +26 -0
  29. package/templates/dotnet-commands/cursor/dotnet-health.md +25 -0
  30. package/templates/dotnet-commands/cursor/dotnet-migrate-minimal-api.md +26 -0
  31. package/templates/dotnet-commands/cursor/dotnet-modernize.md +26 -0
  32. package/templates/dotnet-commands/opencode/dotnet-architecture-audit.md +23 -0
  33. package/templates/dotnet-commands/opencode/dotnet-clean-guardrails.md +22 -0
  34. package/templates/dotnet-commands/opencode/dotnet-ef-migration.md +23 -0
  35. package/templates/dotnet-commands/opencode/dotnet-endpoint-from-model.md +24 -0
  36. package/templates/dotnet-commands/opencode/dotnet-extract-service.md +23 -0
  37. package/templates/dotnet-commands/opencode/dotnet-feature.md +24 -0
  38. package/templates/dotnet-commands/opencode/dotnet-generate-tests.md +23 -0
  39. package/templates/dotnet-commands/opencode/dotnet-health.md +22 -0
  40. package/templates/dotnet-commands/opencode/dotnet-migrate-minimal-api.md +23 -0
  41. package/templates/dotnet-commands/opencode/dotnet-modernize.md +23 -0
  42. package/templates/dotnet-skills/gemini/dotnet-architecture-audit/SKILL.md +26 -0
  43. package/templates/dotnet-skills/gemini/dotnet-clean-guardrails/SKILL.md +25 -0
  44. package/templates/dotnet-skills/gemini/dotnet-ef-migration/SKILL.md +26 -0
  45. package/templates/dotnet-skills/gemini/dotnet-endpoint-from-model/SKILL.md +27 -0
  46. package/templates/dotnet-skills/gemini/dotnet-extract-service/SKILL.md +26 -0
  47. package/templates/dotnet-skills/gemini/dotnet-feature/SKILL.md +27 -0
  48. package/templates/dotnet-skills/gemini/dotnet-generate-tests/SKILL.md +26 -0
  49. package/templates/dotnet-skills/gemini/dotnet-health/SKILL.md +25 -0
  50. package/templates/dotnet-skills/gemini/dotnet-migrate-minimal-api/SKILL.md +26 -0
  51. package/templates/dotnet-skills/gemini/dotnet-modernize/SKILL.md +26 -0
  52. package/templates/vue-commands/codex/component-pattern-audit.md +24 -0
  53. package/templates/vue-commands/codex/design-token-discover.md +24 -0
  54. package/templates/vue-commands/codex/design-token-migrate.md +24 -0
  55. package/templates/vue-commands/codex/js-to-ts.md +25 -0
  56. package/templates/vue-commands/codex/reactivity-audit.md +25 -0
  57. package/templates/vue-commands/codex/state-strategy-audit.md +23 -0
  58. package/templates/vue-commands/codex/vue-guardrails.md +24 -0
  59. package/templates/vue-commands/codex/vue-health.md +28 -0
  60. package/templates/vue-commands/codex/vue-legacy-map.md +25 -0
  61. package/templates/vue-commands/codex/vue-migration-plan.md +24 -0
  62. package/templates/vue-commands/codex/vue-modernize-slice.md +26 -0
  63. package/templates/vue-commands/codex/vue-pattern-map.md +24 -0
  64. package/templates/vue-commands/codex/vue-standardize.md +26 -0
  65. package/templates/vue-commands/codex/vue-style-audit.md +24 -0
  66. package/templates/vue-commands/codex/vue-target-architecture.md +24 -0
  67. package/templates/vue-commands/cursor/component-pattern-audit.md +25 -0
  68. package/templates/vue-commands/cursor/design-token-discover.md +25 -0
  69. package/templates/vue-commands/cursor/design-token-migrate.md +25 -0
  70. package/templates/vue-commands/cursor/js-to-ts.md +26 -0
  71. package/templates/vue-commands/cursor/reactivity-audit.md +26 -0
  72. package/templates/vue-commands/cursor/state-strategy-audit.md +24 -0
  73. package/templates/vue-commands/cursor/vue-guardrails.md +25 -0
  74. package/templates/vue-commands/cursor/vue-health.md +29 -0
  75. package/templates/vue-commands/cursor/vue-legacy-map.md +26 -0
  76. package/templates/vue-commands/cursor/vue-migration-plan.md +25 -0
  77. package/templates/vue-commands/cursor/vue-modernize-slice.md +27 -0
  78. package/templates/vue-commands/cursor/vue-pattern-map.md +25 -0
  79. package/templates/vue-commands/cursor/vue-standardize.md +27 -0
  80. package/templates/vue-commands/cursor/vue-style-audit.md +25 -0
  81. package/templates/vue-commands/cursor/vue-target-architecture.md +25 -0
  82. package/templates/vue-commands/opencode/component-pattern-audit.md +22 -0
  83. package/templates/vue-commands/opencode/design-token-discover.md +22 -0
  84. package/templates/vue-commands/opencode/design-token-migrate.md +22 -0
  85. package/templates/vue-commands/opencode/js-to-ts.md +23 -0
  86. package/templates/vue-commands/opencode/reactivity-audit.md +23 -0
  87. package/templates/vue-commands/opencode/state-strategy-audit.md +21 -0
  88. package/templates/vue-commands/opencode/vue-guardrails.md +22 -0
  89. package/templates/vue-commands/opencode/vue-health.md +26 -0
  90. package/templates/vue-commands/opencode/vue-legacy-map.md +23 -0
  91. package/templates/vue-commands/opencode/vue-migration-plan.md +22 -0
  92. package/templates/vue-commands/opencode/vue-modernize-slice.md +24 -0
  93. package/templates/vue-commands/opencode/vue-pattern-map.md +22 -0
  94. package/templates/vue-commands/opencode/vue-standardize.md +24 -0
  95. package/templates/vue-commands/opencode/vue-style-audit.md +22 -0
  96. package/templates/vue-commands/opencode/vue-target-architecture.md +22 -0
  97. package/templates/vue-skills/gemini/component-pattern-audit/SKILL.md +25 -0
  98. package/templates/vue-skills/gemini/design-token-discover/SKILL.md +25 -0
  99. package/templates/vue-skills/gemini/design-token-migrate/SKILL.md +25 -0
  100. package/templates/vue-skills/gemini/js-to-ts/SKILL.md +26 -0
  101. package/templates/vue-skills/gemini/reactivity-audit/SKILL.md +26 -0
  102. package/templates/vue-skills/gemini/state-strategy-audit/SKILL.md +24 -0
  103. package/templates/vue-skills/gemini/vue-guardrails/SKILL.md +25 -0
  104. package/templates/vue-skills/gemini/vue-health/SKILL.md +29 -0
  105. package/templates/vue-skills/gemini/vue-legacy-map/SKILL.md +26 -0
  106. package/templates/vue-skills/gemini/vue-migration-plan/SKILL.md +25 -0
  107. package/templates/vue-skills/gemini/vue-modernize-slice/SKILL.md +27 -0
  108. package/templates/vue-skills/gemini/vue-pattern-map/SKILL.md +25 -0
  109. package/templates/vue-skills/gemini/vue-standardize/SKILL.md +27 -0
  110. package/templates/vue-skills/gemini/vue-style-audit/SKILL.md +25 -0
  111. package/templates/vue-skills/gemini/vue-target-architecture/SKILL.md +25 -0
@@ -0,0 +1,200 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /**
5
+ * Fast static analyzer for Vue SFC (.vue) and JS/TS files.
6
+ * Uses lightweight block extraction and heuristic parsing without external dependencies.
7
+ */
8
+
9
+ export function analyzeVueFile(filePath, content = null) {
10
+ const code = content !== null ? content : fs.readFileSync(filePath, 'utf8');
11
+ const lines = code.split(/\r?\n/);
12
+ const lineCount = lines.length;
13
+
14
+ const templateBlock = extractBlock(code, 'template');
15
+ const scriptBlock = extractBlock(code, 'script');
16
+ const styleBlocks = extractAllBlocks(code, 'style');
17
+
18
+ const isScriptSetup = /<script\s+[^>]*setup/i.test(code);
19
+ const hasScript = scriptBlock !== null || isScriptSetup;
20
+ const scriptContent = scriptBlock ? scriptBlock.content : '';
21
+ const scriptAttributes = scriptBlock ? scriptBlock.attributes : '';
22
+
23
+ // Language detection
24
+ const isTs = /lang=["'](?:ts|typescript)["']/i.test(code) ||
25
+ filePath.endsWith('.ts') ||
26
+ filePath.endsWith('.tsx');
27
+ const language = isTs ? 'ts' : 'js';
28
+
29
+ // Vue style detection (Options API, Composition API, or <script setup>)
30
+ let vueStyle = 'unknown';
31
+ if (isScriptSetup) {
32
+ vueStyle = 'script_setup';
33
+ } else if (hasScript) {
34
+ if (/\bsetup\s*\([^)]*\)\s*\{/i.test(scriptContent) || /\bdefineComponent\s*\(/i.test(scriptContent)) {
35
+ vueStyle = 'composition';
36
+ } else if (/\b(?:data\s*\(\)|methods\s*:|computed\s*:|watch\s*:)/i.test(scriptContent)) {
37
+ vueStyle = 'options';
38
+ } else {
39
+ vueStyle = 'transitional';
40
+ }
41
+ }
42
+
43
+ // State detection
44
+ const hasPinia = /\b(?:use\w+Store|defineStore)\b/i.test(code);
45
+ const hasVuex = /\b(?:this\.\$store|mapState|mapGetters|mapActions|mapMutations|createNamespacedHelpers)\b/i.test(code);
46
+ const hasComposable = /\buse(?!Store\b)\w+\s*\(/i.test(code);
47
+ const hasLocal = /\b(?:ref|reactive|shallowRef)\s*\(/i.test(code);
48
+
49
+ let state = 'local';
50
+ if (hasPinia && hasVuex) {
51
+ state = 'mixed';
52
+ } else if (hasPinia) {
53
+ state = 'pinia';
54
+ } else if (hasVuex) {
55
+ state = 'vuex';
56
+ } else if (hasComposable) {
57
+ state = 'composable';
58
+ } else if (hasLocal) {
59
+ state = 'local';
60
+ }
61
+
62
+ // Data access detection
63
+ let dataAccess = 'unknown';
64
+ if (/\baxios\.(?:get|post|put|delete|patch)\b/i.test(code)) {
65
+ dataAccess = 'axios_component';
66
+ } else if (/\buseQuery\b|\buseMutation\b/i.test(code)) {
67
+ dataAccess = 'query_library';
68
+ } else if (/\b(?:useFetch|\$fetch)\b/i.test(code)) {
69
+ dataAccess = 'fetch_composable';
70
+ } else if (/\b(?:api|service|client)\.\w+\b/i.test(code)) {
71
+ dataAccess = 'api_service';
72
+ }
73
+
74
+ // Styling analysis
75
+ const stylingTypes = new Set();
76
+ let hasScoped = false;
77
+ let hasGlobal = false;
78
+ for (const style of styleBlocks) {
79
+ const isScoped = /\bscoped\b/i.test(style.attributes);
80
+ const isModule = /\bmodule\b/i.test(style.attributes);
81
+ const isScss = /lang=["'](?:scss|sass)["']/i.test(style.attributes);
82
+
83
+ if (isModule) stylingTypes.add('modules');
84
+ if (isScoped) {
85
+ hasScoped = true;
86
+ stylingTypes.add(isScss ? 'scoped_scss' : 'scoped_css');
87
+ } else {
88
+ hasGlobal = true;
89
+ stylingTypes.add(isScss ? 'global_scss' : 'global_css');
90
+ }
91
+ }
92
+ if (templateBlock && /\bclass=["'][^"']*\b(?:flex|grid|p-|m-|text-|bg-|w-|h-)\b/i.test(templateBlock.content)) {
93
+ stylingTypes.add('utility');
94
+ }
95
+ const styling = stylingTypes.size > 0 ? Array.from(stylingTypes) : ['scoped_css'];
96
+
97
+ // Design token discovery (hex colors, font sizes, margins, padding)
98
+ const tokens = extractTokens(code);
99
+
100
+ // Anti-patterns and hotspots
101
+ const hotspots = [];
102
+ if (lineCount > 400) {
103
+ hotspots.push({ type: 'god_component', message: `Large SFC (${lineCount} lines)` });
104
+ }
105
+ if (/\b(?:this\.\$)/.test(code)) {
106
+ hotspots.push({ type: 'deprecated_global_property', message: 'Uses legacy this.$ properties' });
107
+ }
108
+ if (/::v-deep|>>>|\/deep\//.test(code)) {
109
+ hotspots.push({ type: 'deprecated_deep_selector', message: 'Uses deprecated deep CSS selector (::v-deep or /deep/)' });
110
+ }
111
+ if (/\bmixins\s*:\s*\[/i.test(code)) {
112
+ hotspots.push({ type: 'vue_mixins', message: 'Uses Vue mixins (replace with composables)' });
113
+ }
114
+ if (/\bfilters\s*:\s*\{/i.test(code)) {
115
+ hotspots.push({ type: 'vue_filters', message: 'Uses Vue 2 filters (not supported in Vue 3)' });
116
+ }
117
+ if (/\b\$on\b|\b\$emit\b.*eventBus|new\s+Vue\(\)/i.test(code)) {
118
+ hotspots.push({ type: 'event_bus', message: 'Uses global event bus' });
119
+ }
120
+
121
+ // Maturity classification
122
+ let maturity = 'preferred';
123
+ if (vueStyle === 'options' || hotspots.some((h) => h.type === 'vue_mixins' || h.type === 'vue_filters')) {
124
+ maturity = 'legacy';
125
+ } else if (vueStyle === 'composition' || language === 'js' || state === 'vuex' || hasGlobal) {
126
+ maturity = 'transitional';
127
+ } else if (vueStyle === 'script_setup' && language === 'ts') {
128
+ maturity = 'preferred';
129
+ }
130
+
131
+ return {
132
+ filePath,
133
+ fileName: path.basename(filePath),
134
+ lineCount,
135
+ language,
136
+ vueStyle,
137
+ state,
138
+ dataAccess,
139
+ styling,
140
+ hasScoped,
141
+ hasGlobal,
142
+ tokens,
143
+ hotspots,
144
+ maturity
145
+ };
146
+ }
147
+
148
+ function extractBlock(code, tag) {
149
+ const openRegex = new RegExp(`<${tag}(\\s+[^>]*)?>`, 'i');
150
+ const match = openRegex.exec(code);
151
+ if (!match) return null;
152
+
153
+ const startIndex = match.index + match[0].length;
154
+ const closeTag = `</${tag}>`;
155
+ const endIndex = code.indexOf(closeTag, startIndex);
156
+ if (endIndex === -1) {
157
+ return {
158
+ attributes: match[1] || '',
159
+ content: code.slice(startIndex)
160
+ };
161
+ }
162
+
163
+ return {
164
+ attributes: match[1] || '',
165
+ content: code.slice(startIndex, endIndex)
166
+ };
167
+ }
168
+
169
+ function extractAllBlocks(code, tag) {
170
+ const blocks = [];
171
+ const regex = new RegExp(`<${tag}(\\s+[^>]*)?>([\\s\\S]*?)<\\/${tag}>`, 'gi');
172
+ let match;
173
+ while ((match = regex.exec(code)) !== null) {
174
+ blocks.push({
175
+ attributes: match[1] || '',
176
+ content: match[2]
177
+ });
178
+ }
179
+ return blocks;
180
+ }
181
+
182
+ function extractTokens(code) {
183
+ const hexColors = new Set();
184
+ const hexRegex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g;
185
+ let match;
186
+ while ((match = hexRegex.exec(code)) !== null) {
187
+ hexColors.add(match[0].toLowerCase());
188
+ }
189
+
190
+ const spacingValues = new Set();
191
+ const spacingRegex = /(?:margin|padding|gap|top|bottom|left|right):\s*([0-9]+(?:px|rem|em))/gi;
192
+ while ((match = spacingRegex.exec(code)) !== null) {
193
+ spacingValues.add(match[1].toLowerCase());
194
+ }
195
+
196
+ return {
197
+ hexColors: Array.from(hexColors),
198
+ spacing: Array.from(spacingValues)
199
+ };
200
+ }
package/src/cli.js CHANGED
@@ -15,8 +15,16 @@ import {
15
15
  writeManagedFile
16
16
  } from './config.js';
17
17
  import { renderList, renderTemplate, packageRoot } from './templates.js';
18
- import { DEFAULT_AGENT, GEMINI_SKILL_ENTRIES, getAgent, listAgents, validAgentNames } from './agents.js';
19
- import { bundleEntry, bundleSourceExists } from './commandBundles.js';
18
+ import { DEFAULT_AGENT, GEMINI_SKILL_ENTRIES, getAgent, listAgents, resolveAgentBundles, validAgentNames } from './agents.js';
19
+ import {
20
+ SUPPORTED_FRAMEWORKS,
21
+ bundleEntry,
22
+ bundleSourceExists,
23
+ normalizeFramework,
24
+ parseFrameworksInput
25
+ } from './commandBundles.js';
26
+ import { runAnalysis, formatReport } from './analyzer/engine.js';
27
+ import { loadDashboardData, renderStaticDashboard, runInteractiveDashboard } from './dashboard/dashboard.js';
20
28
 
21
29
  export async function runCli(args, context = {}) {
22
30
  const io = createIo(context);
@@ -44,6 +52,15 @@ export async function runCli(args, context = {}) {
44
52
  case 'status':
45
53
  showStatus(parsed, io);
46
54
  break;
55
+ case 'scan':
56
+ case 'health':
57
+ case 'audit':
58
+ await scanRepository(parsed, io);
59
+ break;
60
+ case 'ui':
61
+ case 'dashboard':
62
+ await showDashboard(parsed, io);
63
+ break;
47
64
  case 'archive':
48
65
  archiveFeature(parsed, io);
49
66
  break;
@@ -71,6 +88,7 @@ async function initProject(parsed, io) {
71
88
  const { config, path: configPath } = loadConfig(io.cwd);
72
89
  const options = writeOptions(parsed, config);
73
90
  const agent = await resolveAgent(parsed, config, io, configPath);
91
+ const frameworks = await resolveFrameworks(parsed, config, io, configPath);
74
92
  const results = [];
75
93
  const artifactsDir = safeJoin(io.cwd, config.artifactsDir);
76
94
 
@@ -84,7 +102,7 @@ async function initProject(parsed, io) {
84
102
  ));
85
103
  results.push(writeManagedFile(path.join(artifactsDir, 'current', '.gitkeep'), '', options));
86
104
  results.push(writeManagedFile(path.join(artifactsDir, 'archived', '.gitkeep'), '', options));
87
- results.push(createDefaultConfigFile(io.cwd, options));
105
+ results.push(createDefaultConfigFile(io.cwd, options, frameworks));
88
106
 
89
107
  for (const binding of agent.bindings) {
90
108
  results.push(writeManagedFile(
@@ -94,7 +112,8 @@ async function initProject(parsed, io) {
94
112
  ));
95
113
  }
96
114
 
97
- for (const bundleName of agent.commandBundles) {
115
+ const bundlesToInstall = resolveAgentBundles(agent.id, frameworks);
116
+ for (const bundleName of bundlesToInstall) {
98
117
  results.push(installCommandBundle(agent, bundleName, config, options, io));
99
118
  }
100
119
 
@@ -102,7 +121,10 @@ async function initProject(parsed, io) {
102
121
  results.push(...installGlobalGeminiSkills(config, options));
103
122
  }
104
123
 
105
- io.stdout(`PlanKit initialized for agent "${agent.id}".`);
124
+ const fwMsg = frameworks.length > 0
125
+ ? ` with framework suite(s): ${frameworks.join(', ')}`
126
+ : ' (base planning commands only)';
127
+ io.stdout(`PlanKit initialized for agent "${agent.id}"${fwMsg}.`);
106
128
  printResults(results, io);
107
129
  }
108
130
 
@@ -354,6 +376,49 @@ function archiveFeature(parsed, io) {
354
376
  io.stdout(`Feature "${featureName}" archived.`);
355
377
  }
356
378
 
379
+ async function scanRepository(parsed, io) {
380
+ const { config } = loadConfig(io.cwd);
381
+ const targetDir = parsed.positionals[0] ? path.resolve(io.cwd, parsed.positionals[0]) : io.cwd;
382
+ const report = await runAnalysis(targetDir, {
383
+ artifactsDir: config.artifactsDir
384
+ });
385
+
386
+ if (parsed.flags.json) {
387
+ io.stdout(JSON.stringify(report, null, 2));
388
+ } else {
389
+ io.stdout(formatReport(report, io.isTTY));
390
+ }
391
+
392
+ if (parsed.flags.threshold) {
393
+ const threshold = Number.parseInt(parsed.flags.threshold, 10);
394
+ if (Number.isInteger(threshold) && report.score.total < threshold) {
395
+ throw new Error(`Health score ${report.score.total} is below required threshold ${threshold}`);
396
+ }
397
+ }
398
+ }
399
+
400
+ async function showDashboard(parsed, io) {
401
+ const { config } = loadConfig(io.cwd);
402
+ const data = loadDashboardData(io.cwd, config);
403
+
404
+ if (!io.isTTY || parsed.flags['no-tui']) {
405
+ renderStaticDashboard(data, io);
406
+ return;
407
+ }
408
+
409
+ await runInteractiveDashboard(io.cwd, io, config, {
410
+ implement: async (featureName, phaseNum) => {
411
+ prepareImplementation({ positionals: [featureName, String(phaseNum)], flags: {} }, io);
412
+ },
413
+ clarify: async (featureName) => {
414
+ clarifyFeature({ positionals: [featureName], flags: {} }, io);
415
+ },
416
+ review: async (featureName) => {
417
+ reviewFeature({ positionals: [featureName], flags: { 'skip-test': true, 'skip-build': true } }, io);
418
+ }
419
+ });
420
+ }
421
+
357
422
  function moveFeatureToArchive(cwd, config, featureName, dryRun = false) {
358
423
  const sourceDir = requireFeatureDir(cwd, config, featureName);
359
424
  const targetDir = safeJoin(cwd, config.artifactsDir, 'archived', featureName);
@@ -522,6 +587,93 @@ async function promptAgent(io) {
522
587
  return getAgent(trimmed) || null;
523
588
  }
524
589
 
590
+ async function resolveFrameworks(parsed, config, io, configPath = null) {
591
+ const fromFlag = parsed.flags.framework || parsed.flags.frameworks;
592
+ if (fromFlag !== undefined) {
593
+ const frameworks = parseFrameworksInput(fromFlag);
594
+ if (frameworks.length > 0) {
595
+ io.stdout(`Using framework(s) from --framework flag: "${frameworks.join(', ')}".`);
596
+ } else {
597
+ io.stdout('No framework suites selected via --framework flag.');
598
+ }
599
+ return frameworks;
600
+ }
601
+
602
+ if (configPath && Array.isArray(config.frameworks) && config.frameworks.length > 0) {
603
+ io.stdout(`Using configured framework(s) "${config.frameworks.join(', ')}" from plan config.`);
604
+ return config.frameworks;
605
+ }
606
+
607
+ if (io.isTTY && io.promptChoice) {
608
+ const selected = await promptFrameworks(io);
609
+ if (selected !== null) {
610
+ if (selected.length > 0) {
611
+ io.stdout(`Selected framework(s): "${selected.join(', ')}".`);
612
+ } else {
613
+ io.stdout('Selected base planning commands only.');
614
+ }
615
+ return selected;
616
+ }
617
+ }
618
+
619
+ io.stdout('No framework suite selected — installing base planning commands only. Pass --framework <angular|vue|dotnet|none> to add framework suites.');
620
+ return [];
621
+ }
622
+
623
+ async function promptFrameworks(io) {
624
+ const promptText = `Select framework command suites to install (in addition to base planning commands):
625
+ 1. None (Base planning commands only)
626
+ 2. Angular command suite
627
+ 3. Vue modernization & architecture suite
628
+ 4. .NET clean architecture & API suite
629
+ 5. All framework suites
630
+
631
+ Enter a number (1-5), comma-separated numbers, or framework names (default: 1)`;
632
+
633
+ const validChoices = ['1', '2', '3', '4', '5', 'none', 'angular', 'vue', 'dotnet', 'all'];
634
+ const answer = await io.promptChoice(promptText, validChoices);
635
+ if (!answer || !answer.trim()) {
636
+ return [];
637
+ }
638
+
639
+ const raw = answer.trim().toLowerCase();
640
+ const tokens = raw.split(',').map((t) => t.trim()).filter(Boolean);
641
+ const result = new Set();
642
+
643
+ for (const token of tokens) {
644
+ if (token === '1' || token === 'none') {
645
+ continue;
646
+ }
647
+ if (token === '2' || token === 'angular' || token === 'ng') {
648
+ result.add('angular');
649
+ continue;
650
+ }
651
+ if (token === '3' || token === 'vue' || token === 'vuejs') {
652
+ result.add('vue');
653
+ continue;
654
+ }
655
+ if (token === '4' || token === 'dotnet' || token === '.net' || token === 'net') {
656
+ result.add('dotnet');
657
+ continue;
658
+ }
659
+ if (token === '5' || token === 'all') {
660
+ SUPPORTED_FRAMEWORKS.forEach((fw) => result.add(fw));
661
+ continue;
662
+ }
663
+
664
+ const normalized = normalizeFramework(token);
665
+ if (Array.isArray(normalized)) {
666
+ normalized.forEach((fw) => result.add(fw));
667
+ } else if (normalized) {
668
+ result.add(normalized);
669
+ } else {
670
+ throw new Error(`Unknown framework selection "${token}". Valid options: 1-5, ${SUPPORTED_FRAMEWORKS.join(', ')}, none, all.`);
671
+ }
672
+ }
673
+
674
+ return Array.from(result);
675
+ }
676
+
525
677
  function writeOptions(parsed, config) {
526
678
  return {
527
679
  force: Boolean(parsed.flags.force) || config.overwrite === 'force',
@@ -636,6 +788,8 @@ Commands:
636
788
  implement <feature> <phase> Validate phase context and prepare phase output artifact
637
789
  review <feature> Run verification and archive a completed feature
638
790
  status [--json] Show active and archived features
791
+ scan [dir] Run static analysis and generate artifacts/.plankit-index.json
792
+ ui Launch interactive feature progress dashboard
639
793
  archive <feature> Move a feature to archived without running verification
640
794
  version Show the installed PlankKit CLI version
641
795
  help Show this help message
@@ -645,6 +799,12 @@ Options:
645
799
  --dry-run Show intended actions without writing
646
800
  --agent <name> Select your coding agent during init
647
801
  (opencode, gemini, codex, cursor; default: opencode)
802
+ --framework <name> Select framework command suite during init
803
+ (angular, vue, dotnet, none, all; default: none)
804
+ --frameworks <list> Comma-separated list of framework suites during init
805
+ --threshold <score> Fail scan if health score is below threshold
806
+ --json Output scan report or status as JSON
807
+ --no-tui Render static summary table instead of interactive dashboard
648
808
  --global-gemini-skills Also install Gemini skills under the user profile
649
809
  --phases 3 Generate N default phases during plan
650
810
  --phases "Design,Build,Test" Generate named phases during plan
@@ -4,6 +4,24 @@ import fs from 'node:fs';
4
4
 
5
5
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
6
6
 
7
+ export const SUPPORTED_FRAMEWORKS = ['angular', 'vue', 'dotnet'];
8
+
9
+ export const FRAMEWORK_ALIASES = {
10
+ ng: 'angular',
11
+ angular: 'angular',
12
+ vue: 'vue',
13
+ vuejs: 'vue',
14
+ dotnet: 'dotnet',
15
+ net: 'dotnet',
16
+ '.net': 'dotnet'
17
+ };
18
+
19
+ export const FRAMEWORK_DESCRIPTIONS = {
20
+ angular: 'Angular command suite',
21
+ vue: 'Vue modernization & architecture suite',
22
+ dotnet: '.NET clean architecture & API suite'
23
+ };
24
+
7
25
  export const COMMAND_BUNDLES = {
8
26
  'opencode-angular': {
9
27
  label: 'Angular command suite',
@@ -28,13 +46,100 @@ export const COMMAND_BUNDLES = {
28
46
  src: path.join('angular-commands', 'cursor'),
29
47
  dest: ['.cursor', 'rules'],
30
48
  kind: 'folder'
49
+ },
50
+ 'opencode-vue': {
51
+ label: 'Vue command suite',
52
+ src: path.join('vue-commands', 'opencode'),
53
+ dest: ['.opencode', 'commands'],
54
+ kind: 'folder'
55
+ },
56
+ 'gemini-vue': {
57
+ label: 'Vue skill suite',
58
+ src: path.join('vue-skills', 'gemini'),
59
+ dest: ['.gemini', 'skills'],
60
+ kind: 'folder'
61
+ },
62
+ 'codex-vue': {
63
+ label: 'Vue command suite',
64
+ src: path.join('vue-commands', 'codex'),
65
+ dest: ['.codex'],
66
+ kind: 'folder'
67
+ },
68
+ 'cursor-vue': {
69
+ label: 'Vue command suite',
70
+ src: path.join('vue-commands', 'cursor'),
71
+ dest: ['.cursor', 'rules'],
72
+ kind: 'folder'
73
+ },
74
+ 'opencode-dotnet': {
75
+ label: '.NET command suite',
76
+ src: path.join('dotnet-commands', 'opencode'),
77
+ dest: ['.opencode', 'commands'],
78
+ kind: 'folder'
79
+ },
80
+ 'gemini-dotnet': {
81
+ label: '.NET skill suite',
82
+ src: path.join('dotnet-skills', 'gemini'),
83
+ dest: ['.gemini', 'skills'],
84
+ kind: 'folder'
85
+ },
86
+ 'codex-dotnet': {
87
+ label: '.NET command suite',
88
+ src: path.join('dotnet-commands', 'codex'),
89
+ dest: ['.codex'],
90
+ kind: 'folder'
91
+ },
92
+ 'cursor-dotnet': {
93
+ label: '.NET command suite',
94
+ src: path.join('dotnet-commands', 'cursor'),
95
+ dest: ['.cursor', 'rules'],
96
+ kind: 'folder'
31
97
  }
32
98
  };
33
99
 
100
+ export function normalizeFramework(input) {
101
+ if (!input) return null;
102
+ const cleaned = String(input).trim().toLowerCase();
103
+ if (cleaned === 'none') return null;
104
+ if (cleaned === 'all') return [...SUPPORTED_FRAMEWORKS];
105
+ return FRAMEWORK_ALIASES[cleaned] || null;
106
+ }
107
+
108
+ export function parseFrameworksInput(input) {
109
+ if (!input) return [];
110
+ if (Array.isArray(input)) {
111
+ return input.flatMap((item) => parseFrameworksInput(item));
112
+ }
113
+ const parts = String(input)
114
+ .split(',')
115
+ .map((item) => item.trim())
116
+ .filter(Boolean);
117
+
118
+ const frameworks = new Set();
119
+ for (const part of parts) {
120
+ const normalized = normalizeFramework(part);
121
+ if (Array.isArray(normalized)) {
122
+ normalized.forEach((fw) => frameworks.add(fw));
123
+ } else if (normalized) {
124
+ frameworks.add(normalized);
125
+ } else if (part.toLowerCase() === 'none') {
126
+ // explicit none
127
+ continue;
128
+ } else {
129
+ throw new Error(`Unknown framework "${part}". Valid frameworks: ${SUPPORTED_FRAMEWORKS.join(', ')}, none, all.`);
130
+ }
131
+ }
132
+ return Array.from(frameworks);
133
+ }
134
+
34
135
  export function bundleEntry(bundleName) {
35
136
  return COMMAND_BUNDLES[bundleName] || null;
36
137
  }
37
138
 
139
+ export function bundleNameFor(agentId, framework) {
140
+ return `${agentId}-${framework}`;
141
+ }
142
+
38
143
  export function bundleSourceExists(bundleName, cwd, config) {
39
144
  const entry = COMMAND_BUNDLES[bundleName];
40
145
  if (!entry) {
package/src/config.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { AGENT_ORDER, DEFAULT_AGENT, getAgent } from './agents.js';
4
+ import { parseFrameworksInput } from './commandBundles.js';
4
5
 
5
6
  export const DEFAULT_CONFIG = {
6
7
  artifactsDir: 'artifacts',
@@ -8,6 +9,7 @@ export const DEFAULT_CONFIG = {
8
9
  agents: {
9
10
  default: DEFAULT_AGENT
10
11
  },
12
+ frameworks: [],
11
13
  defaultPhases: [
12
14
  {
13
15
  title: 'Foundation & Setup',
@@ -57,9 +59,13 @@ export function loadConfig(cwd = process.cwd()) {
57
59
  };
58
60
  }
59
61
 
60
- export function createDefaultConfigFile(cwd, options = {}) {
62
+ export function createDefaultConfigFile(cwd, options = {}, frameworks = []) {
61
63
  const filePath = path.join(cwd, 'plankit.config.json');
62
- const content = `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`;
64
+ const baseConfig = structuredClone(DEFAULT_CONFIG);
65
+ if (Array.isArray(frameworks) && frameworks.length > 0) {
66
+ baseConfig.frameworks = frameworks;
67
+ }
68
+ const content = `${JSON.stringify(baseConfig, null, 2)}\n`;
63
69
  return writeManagedFile(filePath, content, options);
64
70
  }
65
71
 
@@ -69,10 +75,22 @@ export function normalizeConfig(config) {
69
75
  normalized.artifactsDir = normalized.artifactsDir || DEFAULT_CONFIG.artifactsDir;
70
76
  normalized.overwrite = normalized.overwrite || DEFAULT_CONFIG.overwrite;
71
77
  normalized.agents = normalizeAgents(normalized.agents);
78
+ normalized.frameworks = normalizeFrameworksConfig(normalized.frameworks);
72
79
  normalized.verification = { ...DEFAULT_CONFIG.verification, ...(normalized.verification || {}) };
73
80
  return normalized;
74
81
  }
75
82
 
83
+ export function normalizeFrameworksConfig(frameworks) {
84
+ if (!frameworks) return [];
85
+ if (Array.isArray(frameworks)) {
86
+ return parseFrameworksInput(frameworks);
87
+ }
88
+ if (typeof frameworks === 'string') {
89
+ return parseFrameworksInput(frameworks);
90
+ }
91
+ return [];
92
+ }
93
+
76
94
  export function normalizeAgents(agents) {
77
95
  if (!agents || typeof agents !== 'object') {
78
96
  return { ...DEFAULT_CONFIG.agents };