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
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/src/commandBundles.js
CHANGED
|
@@ -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
|
|
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 };
|