speccrew 0.1.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/.speccrew/agents/speccrew-feature-designer.md +142 -0
- package/.speccrew/agents/speccrew-product-manager.md +61 -0
- package/.speccrew/agents/speccrew-system-designer.md +200 -0
- package/.speccrew/agents/speccrew-system-developer.md +238 -0
- package/.speccrew/agents/speccrew-task-worker.md +80 -0
- package/.speccrew/agents/speccrew-team-leader.md +92 -0
- package/.speccrew/agents/speccrew-test-manager.md +313 -0
- package/.speccrew/skills/speccrew-create-agents/SKILL.md +98 -0
- package/.speccrew/skills/speccrew-create-agents/templates/agents/designer-agent.md +54 -0
- package/.speccrew/skills/speccrew-create-agents/templates/agents/dev-agent.md +79 -0
- package/.speccrew/skills/speccrew-create-agents/templates/agents/test-agent.md +80 -0
- package/.speccrew/skills/speccrew-dev-backend/SKILL.md +205 -0
- package/.speccrew/skills/speccrew-dev-backend/templates/TASK-RECORD-TEMPLATE.md +118 -0
- package/.speccrew/skills/speccrew-dev-desktop/SKILL.md +258 -0
- package/.speccrew/skills/speccrew-dev-desktop/templates/TASK-RECORD-TEMPLATE.md +161 -0
- package/.speccrew/skills/speccrew-dev-frontend/SKILL.md +202 -0
- package/.speccrew/skills/speccrew-dev-frontend/templates/TASK-RECORD-TEMPLATE.md +115 -0
- package/.speccrew/skills/speccrew-dev-mobile/SKILL.md +200 -0
- package/.speccrew/skills/speccrew-dev-mobile/templates/TASK-RECORD-TEMPLATE.md +125 -0
- package/.speccrew/skills/speccrew-fd-api-contract/SKILL.md +73 -0
- package/.speccrew/skills/speccrew-fd-api-contract/templates/API-CONTRACT-TEMPLATE.md +96 -0
- package/.speccrew/skills/speccrew-fd-feature-design/SKILL.md +395 -0
- package/.speccrew/skills/speccrew-fd-feature-design/templates/FEATURE-SPEC-TEMPLATE.md +387 -0
- package/.speccrew/skills/speccrew-get-timestamp/SKILL.md +80 -0
- package/.speccrew/skills/speccrew-get-timestamp/scripts/get-timestamp.js +35 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/SKILL.md +1116 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/templates/FEATURE-DETAIL-TEMPLATE-FASTAPI.md +462 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/templates/FEATURE-DETAIL-TEMPLATE-JAVA.md +480 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/templates/FEATURE-DETAIL-TEMPLATE-NET.md +464 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/templates/FEATURE-DETAIL-TEMPLATE.md +480 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/templates/MODULE-OVERVIEW-TEMPLATE.md +367 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/SKILL.md +667 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/STATUS-FORMATS.md +74 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/batch-orchestrator.js +176 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/get-next-batch.js +150 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/get-pending-features.js +106 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/mark-stale.js +249 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/process-batch-results.js +848 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/update-feature-status.js +226 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/SKILL.md +264 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/examples/features.json +34 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/scripts/generate-inventory.js +867 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/scripts/test-inventory.js +26 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/SKILL.md +165 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/scripts/apply-module-mapping.js +208 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/scripts/extract-module-summary.js +180 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/scripts/reindex-modules.js +358 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/SKILL.md +1055 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/templates/FEATURE-DETAIL-TEMPLATE-UI-DESKTOP.md +303 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/templates/FEATURE-DETAIL-TEMPLATE-UI-ELECTRON.md +327 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/templates/FEATURE-DETAIL-TEMPLATE-UI-MINIAPP.md +292 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/templates/FEATURE-DETAIL-TEMPLATE-UI-MOBILE.md +281 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/templates/FEATURE-DETAIL-TEMPLATE-UI.md +324 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-style-extract/SKILL.md +270 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-style-extract/templates/COMPONENT-PATTERN-TEMPLATE.md +33 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-style-extract/templates/LAYOUT-PATTERN-TEMPLATE.md +33 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-style-extract/templates/PAGE-TYPE-TEMPLATE.md +33 -0
- package/.speccrew/skills/speccrew-knowledge-graph-query/SKILL.md +229 -0
- package/.speccrew/skills/speccrew-knowledge-graph-query/scripts/graph-query.js +549 -0
- package/.speccrew/skills/speccrew-knowledge-graph-write/SKILL.md +181 -0
- package/.speccrew/skills/speccrew-knowledge-graph-write/scripts/graph-write.js +651 -0
- package/.speccrew/skills/speccrew-knowledge-module-summarize/SKILL.md +305 -0
- package/.speccrew/skills/speccrew-knowledge-module-summarize/templates/MODULE-OVERVIEW-TEMPLATE.md +400 -0
- package/.speccrew/skills/speccrew-knowledge-system-summarize/SKILL.md +351 -0
- package/.speccrew/skills/speccrew-knowledge-system-summarize/templates/SYSTEM-OVERVIEW-TEMPLATE.md +294 -0
- package/.speccrew/skills/speccrew-knowledge-techs-dispatch/SKILL.md +683 -0
- package/.speccrew/skills/speccrew-knowledge-techs-dispatch/STATUS-FORMATS.md +550 -0
- package/.speccrew/skills/speccrew-knowledge-techs-dispatch/templates/techs-manifest-EXAMPLE.json +35 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/SKILL.md +1087 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/ARCHITECTURE-TEMPLATE.md +240 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/COLOR-SYSTEM-TEMPLATE.md +68 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/COMPONENT-LIBRARY-TEMPLATE.md +86 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/CONVENTIONS-BUILD-TEMPLATE.md +466 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/CONVENTIONS-DATA-TEMPLATE.md +432 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/CONVENTIONS-DESIGN-TEMPLATE.md +1209 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/CONVENTIONS-DEV-TEMPLATE.md +1433 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/CONVENTIONS-SYSTEM-TEST-TEMPLATE.md +1052 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/CONVENTIONS-UNIT-TEST-TEMPLATE.md +946 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/INDEX-TEMPLATE.md +29 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/PAGE-LAYOUTS-TEMPLATE.md +69 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/PAGE-TYPE-SUMMARY-TEMPLATE.md +74 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate/templates/TECH-STACK-TEMPLATE.md +232 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate-conventions/SKILL.md +628 -0
- package/.speccrew/skills/speccrew-knowledge-techs-generate-ui-style/SKILL.md +392 -0
- package/.speccrew/skills/speccrew-knowledge-techs-index/SKILL.md +489 -0
- package/.speccrew/skills/speccrew-knowledge-techs-index/templates/INDEX-TEMPLATE.md +243 -0
- package/.speccrew/skills/speccrew-knowledge-techs-init/SKILL.md +269 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/SKILL.md +562 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/BUSINESS-COMPONENTS-TEMPLATE.md +171 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/COMMON-COMPONENTS-TEMPLATE.md +177 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/COMPONENT-INDIVIDUAL-TEMPLATE.md +80 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/COMPONENT-LIBRARY-TEMPLATE.md +118 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/LAYOUT-INDIVIDUAL-TEMPLATE.md +97 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/LAYOUT-PATTERNS-TEMPLATE.md +208 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/NAVIGATION-PATTERNS-TEMPLATE.md +157 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/PAGE-TYPE-INDIVIDUAL-TEMPLATE.md +123 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/PAGE-TYPE-SUMMARY-TEMPLATE.md +58 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/SPACING-TEMPLATE.md +119 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/STYLE-SYSTEM-TEMPLATE.md +117 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/TYPOGRAPHY-TEMPLATE.md +107 -0
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/templates/UI-STYLE-GUIDE-TEMPLATE.md +171 -0
- package/.speccrew/skills/speccrew-pm-requirement-analysis/SKILL.md +434 -0
- package/.speccrew/skills/speccrew-pm-requirement-analysis/templates/BIZS-MODELING-TEMPLATE.md +332 -0
- package/.speccrew/skills/speccrew-pm-requirement-analysis/templates/PRD-TEMPLATE.md +200 -0
- package/.speccrew/skills/speccrew-pm-requirement-assess/SKILL.md +195 -0
- package/.speccrew/skills/speccrew-project-diagnosis/SKILL.md +208 -0
- package/.speccrew/skills/speccrew-project-diagnosis/templates/DIAGNOSIS-REPORT-TEMPLATE.md +202 -0
- package/.speccrew/skills/speccrew-sd-backend/SKILL.md +188 -0
- package/.speccrew/skills/speccrew-sd-backend/templates/INDEX-TEMPLATE.md +85 -0
- package/.speccrew/skills/speccrew-sd-backend/templates/SD-BACKEND-TEMPLATE.md +269 -0
- package/.speccrew/skills/speccrew-sd-desktop/SKILL.md +192 -0
- package/.speccrew/skills/speccrew-sd-desktop/templates/INDEX-TEMPLATE.md +271 -0
- package/.speccrew/skills/speccrew-sd-desktop/templates/SD-DESKTOP-TEMPLATE.md +673 -0
- package/.speccrew/skills/speccrew-sd-frontend/SKILL.md +176 -0
- package/.speccrew/skills/speccrew-sd-frontend/templates/INDEX-TEMPLATE.md +184 -0
- package/.speccrew/skills/speccrew-sd-frontend/templates/SD-FRONTEND-TEMPLATE.md +382 -0
- package/.speccrew/skills/speccrew-sd-mobile/SKILL.md +189 -0
- package/.speccrew/skills/speccrew-sd-mobile/templates/INDEX-TEMPLATE.md +219 -0
- package/.speccrew/skills/speccrew-sd-mobile/templates/SD-MOBILE-TEMPLATE.md +534 -0
- package/.speccrew/skills/speccrew-test-case-design/SKILL.md +284 -0
- package/.speccrew/skills/speccrew-test-case-design/templates/TEST-CASE-DESIGN-TEMPLATE.md +263 -0
- package/.speccrew/skills/speccrew-test-code-gen/SKILL.md +313 -0
- package/.speccrew/skills/speccrew-test-code-gen/templates/TEST-CODE-PLAN-TEMPLATE.md +180 -0
- package/.speccrew/skills/speccrew-test-execute/SKILL.md +283 -0
- package/.speccrew/skills/speccrew-test-execute/templates/BUG-REPORT-TEMPLATE.md +50 -0
- package/.speccrew/skills/speccrew-test-execute/templates/TEST-REPORT-TEMPLATE.md +57 -0
- package/.speccrew/skills/speccrew-workflow-diagnose/SKILL.md +155 -0
- package/LICENSE +21 -0
- package/README.ar.md +318 -0
- package/README.en.md +318 -0
- package/README.es.md +318 -0
- package/README.md +340 -0
- package/bin/cli.js +62 -0
- package/lib/commands/doctor.js +138 -0
- package/lib/commands/init.js +231 -0
- package/lib/commands/list.js +114 -0
- package/lib/commands/uninstall.js +117 -0
- package/lib/commands/update.js +351 -0
- package/lib/ide-adapters.js +73 -0
- package/lib/utils.js +104 -0
- package/package.json +28 -0
- package/workspace-template/docs/configs/document-templates.json +667 -0
- package/workspace-template/docs/configs/platform-mapping.json +194 -0
- package/workspace-template/docs/configs/tech-stack-mappings.json +313 -0
- package/workspace-template/docs/configs/validation-rules.json +87 -0
- package/workspace-template/docs/rules/mermaid-rule.md +114 -0
- package/workspace-template/docs/solutions/Agent/346/212/200/350/203/275/345/256/232/344/271/211+/351/234/200/346/261/202/346/226/207/346/241/243+UML/344/275/277/347/224/250/346/250/241/346/235/277/357/274/210ISA-95/345/205/255/346/256/265/345/274/217/350/236/215/345/220/210/347/211/210/357/274/211.md +586 -0
- package/workspace-template/docs/solutions/agent-knowledge-map.md +238 -0
- package/workspace-template/docs/solutions/bizs-knowledge-pipeline.md +678 -0
- package/workspace-template/docs/solutions/harness.md +410 -0
- package/workspace-template/docs/solutions/knowledge-incremental-sync-spec.md +943 -0
- package/workspace-template/docs/solutions/techs-knowledge-pipeline.md +803 -0
- package/workspace-template/docs/solutions/workspace-structure.md +318 -0
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate features.json for UI feature analysis
|
|
4
|
+
*
|
|
5
|
+
* Scans source directory for page files and generates a flat feature list with analysis status tracking.
|
|
6
|
+
* All configuration is passed via parameters - the script does not infer anything.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node generate-inventory.js --sourcePath <path> --outputFileName <name> --platformName <name> --platformType <type> --techStack <json> --fileExtensions <json> [--platformSubtype <subtype>] [--techIdentifier <identifier>] [--analysisMethod <method>] [--excludeDirs <json>] [--includeDataObjects <true|false>]
|
|
9
|
+
* Array parameters (--techStack, --fileExtensions, --excludeDirs) accept both JSON format and comma-separated format.
|
|
10
|
+
*
|
|
11
|
+
* Whitelist Mode (using --entryDirsFile):
|
|
12
|
+
* When --entryDirsFile is provided, the script operates in whitelist mode:
|
|
13
|
+
* - Reads entry-dirs JSON file with platformId, sourcePath, and modules array
|
|
14
|
+
* - Loads platform config from platform-mapping.json
|
|
15
|
+
* - Scans only the specified entryDirs for each module
|
|
16
|
+
* - Generates features-{platformId}.json
|
|
17
|
+
*
|
|
18
|
+
* Data Object Exclusion (backend only):
|
|
19
|
+
* By default, files ending with configured suffixes (e.g., VO/DTO/DO/Entity/Convert for Spring) are excluded for backend platforms.
|
|
20
|
+
* The suffixes are read from tech-stack-mappings.json (exclude_file_suffixes field).
|
|
21
|
+
* Use --includeDataObjects true to include them.
|
|
22
|
+
*
|
|
23
|
+
* Example (full scan mode):
|
|
24
|
+
* node generate-inventory.js \
|
|
25
|
+
* --sourcePath "src/views" \
|
|
26
|
+
* --outputFileName "features-web.json" \
|
|
27
|
+
* --platformName "Web Frontend" \
|
|
28
|
+
* --platformType "web" \
|
|
29
|
+
* --platformSubtype "vue" \
|
|
30
|
+
* --techIdentifier "vue" \
|
|
31
|
+
* --techStack "vue,typescript" \
|
|
32
|
+
* --fileExtensions ".vue,.ts" \
|
|
33
|
+
* --analysisMethod "ui-based" \
|
|
34
|
+
* --excludeDirs "components,composables,hooks,utils"
|
|
35
|
+
*
|
|
36
|
+
* Example (whitelist mode):
|
|
37
|
+
* node generate-inventory.js \
|
|
38
|
+
* --entryDirsFile "entry-dirs.json"
|
|
39
|
+
*
|
|
40
|
+
* entry-dirs.json format:
|
|
41
|
+
* {
|
|
42
|
+
* "platformId": "backend-ai",
|
|
43
|
+
* "platformName": "AI Module Backend",
|
|
44
|
+
* "platformType": "backend",
|
|
45
|
+
* "platformSubtype": "ai",
|
|
46
|
+
* "sourcePath": "yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai",
|
|
47
|
+
* "techStack": ["spring-boot", "mybatis-plus"],
|
|
48
|
+
* "modules": [
|
|
49
|
+
* { "name": "chat", "entryDirs": ["controller/admin/chat"] },
|
|
50
|
+
* { "name": "image", "entryDirs": ["controller/admin/image"] }
|
|
51
|
+
* ]
|
|
52
|
+
* }
|
|
53
|
+
*
|
|
54
|
+
* Optional fields (auto-inferred if missing):
|
|
55
|
+
* - platformName: defaults to "{platformType}-{platformSubtype}"
|
|
56
|
+
* - platformType: inferred from platformId (e.g., "backend-ai" → "backend")
|
|
57
|
+
* - platformSubtype: inferred from platformId (e.g., "backend-ai" → "ai")
|
|
58
|
+
* - techStack: defaults based on platformType (backend→["spring-boot"], web→["vue"], mobile→["uniapp"])
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
const fs = require('fs');
|
|
62
|
+
const path = require('path');
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse array parameter that supports both JSON format and comma-separated format.
|
|
66
|
+
* JSON format: '["vue","typescript"]'
|
|
67
|
+
* Comma-separated format: "vue,typescript" (recommended for PowerShell compatibility)
|
|
68
|
+
*/
|
|
69
|
+
function parseArrayParam(value) {
|
|
70
|
+
// Handle boolean true (from flag-only args like --excludeDirs without value)
|
|
71
|
+
if (value === true) return [];
|
|
72
|
+
if (!value) return [];
|
|
73
|
+
const trimmed = value.trim();
|
|
74
|
+
if (trimmed.startsWith('[')) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(trimmed);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
// PowerShell may strip quotes: [vue,typescript] → parse as comma-separated
|
|
79
|
+
return trimmed.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return trimmed.split(',').map(s => s.trim()).filter(Boolean);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Parse command line arguments
|
|
86
|
+
function parseArgs() {
|
|
87
|
+
const args = process.argv.slice(2);
|
|
88
|
+
const params = {};
|
|
89
|
+
|
|
90
|
+
for (let i = 0; i < args.length; i++) {
|
|
91
|
+
const arg = args[i];
|
|
92
|
+
if (arg.startsWith('--')) {
|
|
93
|
+
const key = arg.slice(2);
|
|
94
|
+
const value = args[i + 1];
|
|
95
|
+
// Accept empty string "" as valid value, only skip if undefined or next arg is a flag
|
|
96
|
+
if (value !== undefined && !value.startsWith('--')) {
|
|
97
|
+
params[key] = value;
|
|
98
|
+
i++;
|
|
99
|
+
} else {
|
|
100
|
+
params[key] = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return params;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Normalize path separators to forward slashes
|
|
109
|
+
function normalizePath(filePath) {
|
|
110
|
+
if (!filePath) return '';
|
|
111
|
+
return filePath.replace(/\\/g, '/');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Find project root by searching upward for speccrew-workspace directory
|
|
115
|
+
function findProjectRoot(startPath) {
|
|
116
|
+
let currentDir = path.resolve(startPath);
|
|
117
|
+
const root = path.parse(currentDir).root;
|
|
118
|
+
|
|
119
|
+
while (currentDir !== root) {
|
|
120
|
+
const workspaceDir = path.join(currentDir, 'speccrew-workspace');
|
|
121
|
+
if (fs.existsSync(workspaceDir) && fs.statSync(workspaceDir).isDirectory()) {
|
|
122
|
+
return currentDir;
|
|
123
|
+
}
|
|
124
|
+
currentDir = path.dirname(currentDir);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Fallback: return source path's drive root
|
|
128
|
+
return root;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check if path contains any excluded directory
|
|
132
|
+
function isExcludedPath(filePath, excludeDirs) {
|
|
133
|
+
const parts = normalizePath(filePath).split('/').filter(p => p);
|
|
134
|
+
for (const part of parts) {
|
|
135
|
+
if (excludeDirs.includes(part)) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check if file is a data object class (VO/DTO/DO/Entity/Convert) - should be excluded for backend
|
|
143
|
+
function isDataObjectFile(fileName, extension, excludeSuffixes) {
|
|
144
|
+
// If no suffixes configured, don't exclude any files
|
|
145
|
+
if (!excludeSuffixes || excludeSuffixes.length === 0) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
// Check if fileName ends with any configured suffix
|
|
149
|
+
for (const suffix of excludeSuffixes) {
|
|
150
|
+
if (fileName.endsWith(suffix)) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Get module name (first non-excluded directory level)
|
|
158
|
+
function getModuleName(dirPath, excludeDirs, fallbackModuleName) {
|
|
159
|
+
const parts = normalizePath(dirPath).split('/').filter(p => p && p !== '.');
|
|
160
|
+
for (const part of parts) {
|
|
161
|
+
if (!excludeDirs.includes(part)) {
|
|
162
|
+
return part;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// All parts were excluded (e.g., "src/App.vue" with src excluded)
|
|
166
|
+
// Return "_root" to indicate framework/root-level files
|
|
167
|
+
return '_root';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Load platform configuration from platform-mapping.json
|
|
172
|
+
* @param {string} platformId - Platform ID like "backend-ai", "web-vue"
|
|
173
|
+
* @param {string} projectRoot - Project root directory
|
|
174
|
+
* @returns {object|null} Platform config with platformName, platformType, techStack, extensions, or null if not found
|
|
175
|
+
*/
|
|
176
|
+
function loadPlatformConfig(platformId, projectRoot) {
|
|
177
|
+
const configPath = path.join(projectRoot, 'speccrew-workspace', 'docs', 'configs', 'platform-mapping.json');
|
|
178
|
+
if (!fs.existsSync(configPath)) {
|
|
179
|
+
console.error(`Error: platform-mapping.json not found at ${configPath}`);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
184
|
+
const config = JSON.parse(configContent);
|
|
185
|
+
|
|
186
|
+
// Find platform mapping by platform_id
|
|
187
|
+
const mapping = config.mappings.find(m => m.platform_id === platformId);
|
|
188
|
+
if (!mapping) {
|
|
189
|
+
console.error(`Error: Platform ID "${platformId}" not found in platform-mapping.json`);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Build platform config from mapping
|
|
194
|
+
const platformConfig = {
|
|
195
|
+
platformId: mapping.platform_id,
|
|
196
|
+
platformType: mapping.platform_type,
|
|
197
|
+
platformSubtype: mapping.platform_subtype || mapping.framework,
|
|
198
|
+
framework: mapping.framework,
|
|
199
|
+
platformName: `${mapping.platform_type}-${mapping.framework}` // Default name, can be customized
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
return platformConfig;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Load tech stack configuration from tech-stack-mappings.json
|
|
207
|
+
* @param {string} platformType - Platform type like "backend", "web"
|
|
208
|
+
* @param {string} framework - Framework like "spring", "vue"
|
|
209
|
+
* @param {string} projectRoot - Project root directory
|
|
210
|
+
* @returns {object} Tech config with extensions and exclude_file_suffixes
|
|
211
|
+
*/
|
|
212
|
+
function loadTechStackConfig(platformType, framework, projectRoot) {
|
|
213
|
+
const configPath = path.join(projectRoot, 'speccrew-workspace', 'docs', 'configs', 'tech-stack-mappings.json');
|
|
214
|
+
if (!fs.existsSync(configPath)) {
|
|
215
|
+
console.error(`Warning: tech-stack-mappings.json not found at ${configPath}`);
|
|
216
|
+
return { extensions: [], exclude_file_suffixes: [] };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
220
|
+
const config = JSON.parse(configContent);
|
|
221
|
+
|
|
222
|
+
if (config.tech_stacks &&
|
|
223
|
+
config.tech_stacks[platformType] &&
|
|
224
|
+
config.tech_stacks[platformType][framework]) {
|
|
225
|
+
const techConfig = config.tech_stacks[platformType][framework];
|
|
226
|
+
return {
|
|
227
|
+
extensions: techConfig.extensions || [],
|
|
228
|
+
exclude_file_suffixes: techConfig.exclude_file_suffixes || []
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { extensions: [], exclude_file_suffixes: [] };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Infer platform info from platformId
|
|
237
|
+
* @param {string} platformId - Platform ID like "backend-ai", "web-vue", "mobile-uniapp"
|
|
238
|
+
* @returns {object} Inferred platform info { platformType, platformSubtype }
|
|
239
|
+
*/
|
|
240
|
+
function inferPlatformInfo(platformId) {
|
|
241
|
+
// Parse platformId: "{type}-{subtype}" format
|
|
242
|
+
const parts = platformId.split('-');
|
|
243
|
+
if (parts.length >= 2) {
|
|
244
|
+
return {
|
|
245
|
+
platformType: parts[0],
|
|
246
|
+
platformSubtype: parts.slice(1).join('-')
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
// Fallback: treat entire platformId as type
|
|
250
|
+
return {
|
|
251
|
+
platformType: platformId,
|
|
252
|
+
platformSubtype: ''
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Infer framework from techStack array
|
|
258
|
+
* @param {string[]} techStack - Array of tech stack names
|
|
259
|
+
* @returns {string} Framework identifier like "spring", "vue", "uniapp"
|
|
260
|
+
*/
|
|
261
|
+
function inferFrameworkFromTechStack(techStack) {
|
|
262
|
+
if (!techStack || techStack.length === 0) {
|
|
263
|
+
return '';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Mapping: tech stack name → framework identifier
|
|
267
|
+
const techToFramework = {
|
|
268
|
+
// Backend
|
|
269
|
+
'spring-boot': 'spring',
|
|
270
|
+
'spring': 'spring',
|
|
271
|
+
'springboot': 'spring',
|
|
272
|
+
'mybatis-plus': 'spring',
|
|
273
|
+
'mybatis': 'spring',
|
|
274
|
+
'jpa': 'spring',
|
|
275
|
+
// Frontend
|
|
276
|
+
'vue': 'vue',
|
|
277
|
+
'vue3': 'vue',
|
|
278
|
+
'vue2': 'vue',
|
|
279
|
+
'react': 'react',
|
|
280
|
+
'reactjs': 'react',
|
|
281
|
+
'nextjs': 'next',
|
|
282
|
+
'next.js': 'next',
|
|
283
|
+
'angular': 'angular',
|
|
284
|
+
// Mobile
|
|
285
|
+
'uniapp': 'uniapp',
|
|
286
|
+
'uni-app': 'uniapp',
|
|
287
|
+
'flutter': 'flutter',
|
|
288
|
+
'react-native': 'react-native',
|
|
289
|
+
'reactnative': 'react-native'
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// Platform-specific frameworks have higher priority than generic ones
|
|
293
|
+
const platformSpecific = new Set(['uniapp', 'flutter', 'react-native', 'next']);
|
|
294
|
+
|
|
295
|
+
// First pass: look for platform-specific framework match
|
|
296
|
+
for (const tech of techStack) {
|
|
297
|
+
const normalizedTech = tech.toLowerCase();
|
|
298
|
+
const framework = techToFramework[normalizedTech];
|
|
299
|
+
if (framework && platformSpecific.has(framework)) {
|
|
300
|
+
return framework;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Second pass: fallback to first matching generic framework
|
|
305
|
+
for (const tech of techStack) {
|
|
306
|
+
const normalizedTech = tech.toLowerCase();
|
|
307
|
+
if (techToFramework[normalizedTech]) {
|
|
308
|
+
return techToFramework[normalizedTech];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Fallback: use first tech stack as framework
|
|
313
|
+
return techStack[0].toLowerCase();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get default techStack for platformType
|
|
318
|
+
* @param {string} platformType - Platform type like "backend", "web", "mobile"
|
|
319
|
+
* @returns {string[]} Default tech stack array
|
|
320
|
+
*/
|
|
321
|
+
function getDefaultTechStack(platformType) {
|
|
322
|
+
const defaults = {
|
|
323
|
+
backend: ['spring-boot'],
|
|
324
|
+
web: ['vue'],
|
|
325
|
+
mobile: ['uniapp'],
|
|
326
|
+
desktop: ['electron']
|
|
327
|
+
};
|
|
328
|
+
return defaults[platformType] || [];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Find files in a specific entry directory (non-recursive, just the directory itself)
|
|
333
|
+
* @param {string} dir - Directory to scan
|
|
334
|
+
* @param {string[]} extensions - File extensions to match
|
|
335
|
+
* @param {string} baseDir - Base directory for relative paths
|
|
336
|
+
* @returns {object[]} Array of file objects
|
|
337
|
+
*/
|
|
338
|
+
function findFilesInDir(dir, extensions, baseDir) {
|
|
339
|
+
const files = [];
|
|
340
|
+
|
|
341
|
+
if (!fs.existsSync(dir)) {
|
|
342
|
+
return files;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const items = fs.readdirSync(dir);
|
|
346
|
+
|
|
347
|
+
for (const item of items) {
|
|
348
|
+
const fullPath = path.join(dir, item);
|
|
349
|
+
const stat = fs.statSync(fullPath);
|
|
350
|
+
|
|
351
|
+
if (stat.isFile()) {
|
|
352
|
+
const ext = path.extname(item);
|
|
353
|
+
if (extensions.includes(ext)) {
|
|
354
|
+
const relativePath = normalizePath(path.relative(baseDir, fullPath));
|
|
355
|
+
files.push({
|
|
356
|
+
fullPath,
|
|
357
|
+
relativePath,
|
|
358
|
+
fileName: path.basename(item, ext),
|
|
359
|
+
extension: ext,
|
|
360
|
+
directory: path.dirname(relativePath)
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return files;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Generate features.json from entry-dirs whitelist mode
|
|
371
|
+
* @param {object} entryDirsData - Entry directories data from JSON file
|
|
372
|
+
* @param {object} platformConfig - Platform configuration
|
|
373
|
+
* @param {string} projectRoot - Project root directory
|
|
374
|
+
* @param {string} outputDir - Output directory for features JSON
|
|
375
|
+
* @returns {boolean} Success status
|
|
376
|
+
*/
|
|
377
|
+
function generateFromEntryDirs(entryDirsData, platformConfig, projectRoot, outputDir) {
|
|
378
|
+
const { platformId, sourcePath, modules } = entryDirsData;
|
|
379
|
+
const { platformType, platformSubtype, framework } = platformConfig;
|
|
380
|
+
|
|
381
|
+
// Load tech stack config for extensions and exclude_file_suffixes
|
|
382
|
+
const techConfig = loadTechStackConfig(platformType, framework, projectRoot);
|
|
383
|
+
const { extensions, exclude_file_suffixes } = techConfig;
|
|
384
|
+
|
|
385
|
+
if (extensions.length === 0) {
|
|
386
|
+
console.error(`Error: No extensions found for ${platformType}/${framework} in tech-stack-mappings.json`);
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
console.log(`Whitelist mode: Platform ${platformId}`);
|
|
391
|
+
console.log(`Source path: ${sourcePath}`);
|
|
392
|
+
console.log(`Extensions: ${extensions.join(', ')}`);
|
|
393
|
+
if (exclude_file_suffixes.length > 0) {
|
|
394
|
+
console.log(`Exclude suffixes: ${exclude_file_suffixes.join(', ')}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Resolve absolute source path
|
|
398
|
+
const absoluteSourcePath = path.resolve(projectRoot, sourcePath);
|
|
399
|
+
if (!fs.existsSync(absoluteSourcePath)) {
|
|
400
|
+
console.error(`Error: Source path does not exist: ${absoluteSourcePath}`);
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Collect all features
|
|
405
|
+
const features = [];
|
|
406
|
+
const moduleNames = [];
|
|
407
|
+
|
|
408
|
+
for (const module of modules) {
|
|
409
|
+
const { name: moduleName, entryDirs } = module;
|
|
410
|
+
moduleNames.push(moduleName);
|
|
411
|
+
|
|
412
|
+
for (const entryDir of entryDirs) {
|
|
413
|
+
// entryDir is relative to sourcePath
|
|
414
|
+
const entryFullPath = path.join(absoluteSourcePath, entryDir);
|
|
415
|
+
|
|
416
|
+
if (!fs.existsSync(entryFullPath)) {
|
|
417
|
+
console.log(` Skipping non-existent entry: ${entryDir}`);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Scan files in the entry directory (recursive for web/mobile platforms with nested dirs)
|
|
422
|
+
const excludeDirs = techConfig.exclude_dirs || [];
|
|
423
|
+
const files = findFiles(entryFullPath, extensions, excludeDirs, absoluteSourcePath);
|
|
424
|
+
|
|
425
|
+
for (const file of files) {
|
|
426
|
+
// Apply exclude_file_suffixes filter
|
|
427
|
+
if (isDataObjectFile(file.fileName, file.extension, exclude_file_suffixes)) {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Build feature ID: moduleName-entryDirSegs-fileName
|
|
432
|
+
// entryDir like "controller/admin/chat" → "controller-admin-chat"
|
|
433
|
+
const entryDirNormalized = normalizePath(entryDir).replace(/[\/\\]/g, '-');
|
|
434
|
+
const featureId = `${moduleName}-${entryDirNormalized}-${file.fileName}`;
|
|
435
|
+
|
|
436
|
+
// Build relative file path from sourcePath
|
|
437
|
+
const relativeFilePath = normalizePath(path.relative(projectRoot, file.fullPath));
|
|
438
|
+
|
|
439
|
+
// Build document path
|
|
440
|
+
const docPath = `speccrew-workspace/knowledges/bizs/${platformType}-${platformSubtype}/${moduleName}/${file.fileName}.md`;
|
|
441
|
+
|
|
442
|
+
const feature = {
|
|
443
|
+
id: featureId,
|
|
444
|
+
fileName: file.fileName,
|
|
445
|
+
sourcePath: relativeFilePath,
|
|
446
|
+
documentPath: docPath,
|
|
447
|
+
module: moduleName,
|
|
448
|
+
analyzed: false,
|
|
449
|
+
startedAt: null,
|
|
450
|
+
completedAt: null,
|
|
451
|
+
analysisNotes: null
|
|
452
|
+
};
|
|
453
|
+
features.push(feature);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
console.log(`Found ${features.length} features across ${moduleNames.length} modules`);
|
|
459
|
+
|
|
460
|
+
// Build inventory object
|
|
461
|
+
const inventory = {
|
|
462
|
+
platformId: platformId,
|
|
463
|
+
platformName: platformConfig.platformName,
|
|
464
|
+
platformType: platformType,
|
|
465
|
+
sourcePath: sourcePath,
|
|
466
|
+
techStack: [framework],
|
|
467
|
+
modules: [...new Set(moduleNames)].sort(),
|
|
468
|
+
totalFiles: features.length,
|
|
469
|
+
analyzedCount: 0,
|
|
470
|
+
pendingCount: features.length,
|
|
471
|
+
generatedAt: new Date().toISOString().replace(/[-:]/g, '').slice(0, 15).replace('T', '-'),
|
|
472
|
+
analysisMethod: 'api-based',
|
|
473
|
+
features: features
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// Add platformSubtype if present
|
|
477
|
+
if (platformSubtype) {
|
|
478
|
+
inventory.platformSubtype = platformSubtype;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Add techIdentifier
|
|
482
|
+
inventory.techIdentifier = framework;
|
|
483
|
+
|
|
484
|
+
// Write output file
|
|
485
|
+
const outputFileName = `features-${platformId}.json`;
|
|
486
|
+
const outputPath = path.join(outputDir, outputFileName);
|
|
487
|
+
|
|
488
|
+
// Ensure output directory exists
|
|
489
|
+
if (!fs.existsSync(outputDir)) {
|
|
490
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
fs.writeFileSync(outputPath, JSON.stringify(inventory, null, 2), 'utf8');
|
|
494
|
+
|
|
495
|
+
console.log(`Generated ${outputFileName} with ${features.length} features`);
|
|
496
|
+
console.log(`Output: ${outputPath}`);
|
|
497
|
+
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Recursively find all files matching extensions
|
|
502
|
+
function findFiles(dir, extensions, excludeDirs, baseDir) {
|
|
503
|
+
const files = [];
|
|
504
|
+
const items = fs.readdirSync(dir);
|
|
505
|
+
|
|
506
|
+
for (const item of items) {
|
|
507
|
+
const fullPath = path.join(dir, item);
|
|
508
|
+
const relativePath = normalizePath(path.relative(baseDir, fullPath));
|
|
509
|
+
const stat = fs.statSync(fullPath);
|
|
510
|
+
|
|
511
|
+
if (stat.isDirectory()) {
|
|
512
|
+
// Skip excluded directories
|
|
513
|
+
if (excludeDirs.includes(item)) {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
files.push(...findFiles(fullPath, extensions, excludeDirs, baseDir));
|
|
517
|
+
} else if (stat.isFile()) {
|
|
518
|
+
const ext = path.extname(item);
|
|
519
|
+
if (extensions.includes(ext)) {
|
|
520
|
+
files.push({
|
|
521
|
+
fullPath,
|
|
522
|
+
relativePath,
|
|
523
|
+
fileName: path.basename(item, ext),
|
|
524
|
+
extension: ext,
|
|
525
|
+
directory: path.dirname(relativePath)
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return files;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Main function
|
|
535
|
+
function main() {
|
|
536
|
+
const params = parseArgs();
|
|
537
|
+
|
|
538
|
+
// Check for whitelist mode (--entryDirsFile provided)
|
|
539
|
+
if (params.entryDirsFile) {
|
|
540
|
+
// Whitelist mode: scan only specified entry directories
|
|
541
|
+
const entryDirsFilePath = path.resolve(params.entryDirsFile);
|
|
542
|
+
|
|
543
|
+
if (!fs.existsSync(entryDirsFilePath)) {
|
|
544
|
+
console.error(`Error: entryDirsFile does not exist: ${params.entryDirsFile}`);
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Read entry-dirs JSON file
|
|
549
|
+
let entryDirsData;
|
|
550
|
+
try {
|
|
551
|
+
const content = fs.readFileSync(entryDirsFilePath, 'utf8');
|
|
552
|
+
entryDirsData = JSON.parse(content);
|
|
553
|
+
} catch (e) {
|
|
554
|
+
console.error(`Error: Failed to parse entryDirsFile: ${e.message}`);
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Validate entryDirsData structure
|
|
559
|
+
if (!entryDirsData.platformId) {
|
|
560
|
+
console.error('Error: entryDirsFile missing required field "platformId"');
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
563
|
+
if (!entryDirsData.sourcePath) {
|
|
564
|
+
console.error('Error: entryDirsFile missing required field "sourcePath"');
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
if (!entryDirsData.modules || !Array.isArray(entryDirsData.modules)) {
|
|
568
|
+
console.error('Error: entryDirsFile missing required field "modules" array');
|
|
569
|
+
process.exit(1);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Find project root (use current directory or entryDirsFile directory)
|
|
573
|
+
const projectRoot = findProjectRoot(path.dirname(entryDirsFilePath));
|
|
574
|
+
|
|
575
|
+
// Build platform config from entryDirsData (no longer requires platform-mapping.json)
|
|
576
|
+
// Step 1: Get platformType and platformSubtype (from entryDirsData or infer from platformId)
|
|
577
|
+
let platformType = entryDirsData.platformType;
|
|
578
|
+
let platformSubtype = entryDirsData.platformSubtype;
|
|
579
|
+
|
|
580
|
+
if (!platformType || !platformSubtype) {
|
|
581
|
+
const inferred = inferPlatformInfo(entryDirsData.platformId);
|
|
582
|
+
if (!platformType) {
|
|
583
|
+
platformType = inferred.platformType;
|
|
584
|
+
console.log(`Inferred platformType from platformId: ${platformType}`);
|
|
585
|
+
}
|
|
586
|
+
if (!platformSubtype) {
|
|
587
|
+
platformSubtype = inferred.platformSubtype;
|
|
588
|
+
console.log(`Inferred platformSubtype from platformId: ${platformSubtype}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Step 2: Get techStack (from entryDirsData or default based on platformType)
|
|
593
|
+
let techStack = entryDirsData.techStack;
|
|
594
|
+
if (!techStack || techStack.length === 0) {
|
|
595
|
+
techStack = getDefaultTechStack(platformType);
|
|
596
|
+
console.log(`Using default techStack for ${platformType}: [${techStack.join(', ')}]`);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Step 3: Infer framework from techStack
|
|
600
|
+
const framework = inferFrameworkFromTechStack(techStack);
|
|
601
|
+
|
|
602
|
+
// Step 4: Get platformName (from entryDirsData or build from type/subtype)
|
|
603
|
+
const platformName = entryDirsData.platformName || `${platformType}-${platformSubtype}`;
|
|
604
|
+
|
|
605
|
+
// Build final platformConfig object
|
|
606
|
+
const platformConfig = {
|
|
607
|
+
platformId: entryDirsData.platformId,
|
|
608
|
+
platformType: platformType,
|
|
609
|
+
platformSubtype: platformSubtype,
|
|
610
|
+
framework: framework,
|
|
611
|
+
platformName: platformName
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
console.log(`Platform config: type=${platformType}, subtype=${platformSubtype}, framework=${framework}`);
|
|
615
|
+
|
|
616
|
+
// Set output directory
|
|
617
|
+
const outputDir = path.join(projectRoot, 'speccrew-workspace', 'knowledges', 'base', 'sync-state', 'knowledge-bizs');
|
|
618
|
+
|
|
619
|
+
// Generate features from entry dirs
|
|
620
|
+
const success = generateFromEntryDirs(entryDirsData, platformConfig, projectRoot, outputDir);
|
|
621
|
+
process.exit(success ? 0 : 1);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Required parameters (full scan mode)
|
|
625
|
+
const sourcePath = params.sourcePath;
|
|
626
|
+
const outputFileName = params.outputFileName;
|
|
627
|
+
const platformName = params.platformName;
|
|
628
|
+
const platformType = params.platformType;
|
|
629
|
+
const techStackStr = params.techStack;
|
|
630
|
+
const fileExtensionsStr = params.fileExtensions;
|
|
631
|
+
|
|
632
|
+
// Optional parameters
|
|
633
|
+
const platformSubtype = params.platformSubtype || '';
|
|
634
|
+
const techIdentifier = params.techIdentifier || platformSubtype;
|
|
635
|
+
const analysisMethod = params.analysisMethod || 'ui-based';
|
|
636
|
+
let excludeDirsStr = params.excludeDirs;
|
|
637
|
+
// --includeDataObjects: set to "true" to include VO/DTO/DO/Entity/Convert files (default: false)
|
|
638
|
+
const includeDataObjects = params.includeDataObjects === 'true';
|
|
639
|
+
|
|
640
|
+
// Resolve source path first to find project root
|
|
641
|
+
const resolvedSourcePath = path.resolve(sourcePath);
|
|
642
|
+
if (!fs.existsSync(resolvedSourcePath)) {
|
|
643
|
+
console.error(`Error: Source path does not exist: ${sourcePath}`);
|
|
644
|
+
process.exit(1);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Find project root for config file lookup
|
|
648
|
+
const projectRoot = findProjectRoot(resolvedSourcePath);
|
|
649
|
+
|
|
650
|
+
// If excludeDirs not provided or empty, try to read from tech-stack-mappings.json
|
|
651
|
+
let excludeFileSuffixes = [];
|
|
652
|
+
if (!excludeDirsStr || excludeDirsStr === '[]') {
|
|
653
|
+
try {
|
|
654
|
+
const configPath = path.join(projectRoot, 'speccrew-workspace', 'docs', 'configs', 'tech-stack-mappings.json');
|
|
655
|
+
if (fs.existsSync(configPath)) {
|
|
656
|
+
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
657
|
+
const config = JSON.parse(configContent);
|
|
658
|
+
|
|
659
|
+
// Load tech-stack-specific exclude_dirs
|
|
660
|
+
let techExcludeDirs = [];
|
|
661
|
+
if (config.tech_stacks &&
|
|
662
|
+
config.tech_stacks[platformType] &&
|
|
663
|
+
config.tech_stacks[platformType][techIdentifier] &&
|
|
664
|
+
config.tech_stacks[platformType][techIdentifier].exclude_dirs) {
|
|
665
|
+
techExcludeDirs = config.tech_stacks[platformType][techIdentifier].exclude_dirs;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Load global exclude_dirs (applies to all platforms)
|
|
669
|
+
const globalExcludeDirs = config.global_exclude_dirs || [];
|
|
670
|
+
|
|
671
|
+
// Merge: global + tech-specific
|
|
672
|
+
const mergedDirs = [...new Set([...globalExcludeDirs, ...techExcludeDirs])];
|
|
673
|
+
excludeDirsStr = JSON.stringify(mergedDirs);
|
|
674
|
+
console.log(`Loaded exclude_dirs from tech-stack-mappings.json (${globalExcludeDirs.length} global + ${techExcludeDirs.length} tech-specific = ${mergedDirs.length} total)`);
|
|
675
|
+
|
|
676
|
+
// Load tech-stack-specific exclude_file_suffixes
|
|
677
|
+
if (config.tech_stacks &&
|
|
678
|
+
config.tech_stacks[platformType] &&
|
|
679
|
+
config.tech_stacks[platformType][techIdentifier] &&
|
|
680
|
+
config.tech_stacks[platformType][techIdentifier].exclude_file_suffixes) {
|
|
681
|
+
excludeFileSuffixes = config.tech_stacks[platformType][techIdentifier].exclude_file_suffixes;
|
|
682
|
+
if (excludeFileSuffixes.length > 0) {
|
|
683
|
+
console.log(`Loaded exclude_file_suffixes from tech-stack-mappings.json: ${excludeFileSuffixes.join(', ')}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
} catch (e) {
|
|
688
|
+
// Silent fallback - continue with default or empty
|
|
689
|
+
console.log(`Could not load exclude_dirs from config: ${e.message}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Default fallback if still not set
|
|
694
|
+
if (!excludeDirsStr) {
|
|
695
|
+
excludeDirsStr = '["components","composables","hooks","utils"]';
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Validate required parameters
|
|
699
|
+
if (!sourcePath || !outputFileName || !platformName || !platformType || !techStackStr || !fileExtensionsStr) {
|
|
700
|
+
console.error('Usage: node generate-inventory.js --sourcePath <path> --outputFileName <name> --platformName <name> --platformType <type> --techStack <json> --fileExtensions <json> [--platformSubtype <subtype>] [--techIdentifier <identifier>] [--analysisMethod <method>] [--excludeDirs <json>] [--includeDataObjects <true|false>]');
|
|
701
|
+
console.error('Example: node generate-inventory.js --sourcePath "src/views" --outputFileName "features-web.json" --platformName "Web Frontend" --platformType "web" --platformSubtype "vue" --techStack "vue,typescript" --fileExtensions ".vue,.ts" --analysisMethod "ui-based" --excludeDirs "components,composables,hooks,utils"');
|
|
702
|
+
process.exit(1);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Find sync-state directory
|
|
706
|
+
const syncStateDir = path.join(projectRoot, 'speccrew-workspace', 'knowledges', 'base', 'sync-state', 'knowledge-bizs');
|
|
707
|
+
const outputPath = path.join(syncStateDir, outputFileName);
|
|
708
|
+
|
|
709
|
+
// Calculate relative source path from project root
|
|
710
|
+
let relativeSourcePath = normalizePath(sourcePath);
|
|
711
|
+
if (/^[a-zA-Z]:/.test(sourcePath) || path.isAbsolute(sourcePath)) {
|
|
712
|
+
// Absolute path - make it relative to project root
|
|
713
|
+
relativeSourcePath = normalizePath(path.relative(projectRoot, resolvedSourcePath));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Handle special case: if source path is current directory (.), use empty string for proper replacement
|
|
717
|
+
if (relativeSourcePath === '.') {
|
|
718
|
+
relativeSourcePath = '';
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Calculate fallback module name from source path (last directory name)
|
|
722
|
+
const fallbackModuleName = path.basename(resolvedSourcePath);
|
|
723
|
+
|
|
724
|
+
console.log(`Scanning: ${sourcePath}`);
|
|
725
|
+
console.log(`Output: ${outputPath}`);
|
|
726
|
+
console.log(`Platform: ${platformName} (${platformType})`);
|
|
727
|
+
console.log(`TechStack: ${techStackStr}`);
|
|
728
|
+
console.log(`Extensions: ${fileExtensionsStr}`);
|
|
729
|
+
|
|
730
|
+
// Parse array parameters (supports both JSON and comma-separated formats)
|
|
731
|
+
const techStackArray = parseArrayParam(techStackStr);
|
|
732
|
+
const extensionsArray = parseArrayParam(fileExtensionsStr);
|
|
733
|
+
const excludeDirsArray = parseArrayParam(excludeDirsStr);
|
|
734
|
+
|
|
735
|
+
console.log(`Scanning for files: ${extensionsArray.map(ext => `*${ext}`).join(', ')}`);
|
|
736
|
+
|
|
737
|
+
// Find all files recursively matching the extensions
|
|
738
|
+
const allFiles = findFiles(resolvedSourcePath, extensionsArray, excludeDirsArray, resolvedSourcePath);
|
|
739
|
+
|
|
740
|
+
// Filter out files in excluded directories
|
|
741
|
+
let files = allFiles.filter(file => !isExcludedPath(file.relativePath, excludeDirsArray));
|
|
742
|
+
|
|
743
|
+
// For backend platforms, filter out data object files (VO/DTO/DO/Entity/Convert) unless includeDataObjects is set
|
|
744
|
+
let excludedDataObjectsCount = 0;
|
|
745
|
+
const isBackend = platformType === 'backend';
|
|
746
|
+
if (isBackend && !includeDataObjects && excludeFileSuffixes.length > 0) {
|
|
747
|
+
const filesBeforeFilter = files.length;
|
|
748
|
+
files = files.filter(file => !isDataObjectFile(file.fileName, file.extension, excludeFileSuffixes));
|
|
749
|
+
excludedDataObjectsCount = filesBeforeFilter - files.length;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
console.log(`Found ${allFiles.length} total files, ${files.length} after excluding components directories`);
|
|
753
|
+
if (excludedDataObjectsCount > 0) {
|
|
754
|
+
console.log(`Excluded: ${excludedDataObjectsCount} data objects (VO/DTO/DO/Entity/Convert)`);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Build flat feature list - each file is a feature
|
|
758
|
+
const features = [];
|
|
759
|
+
|
|
760
|
+
for (const file of files) {
|
|
761
|
+
// Calculate relative file path from project root
|
|
762
|
+
const relativeFilePath = normalizePath(path.relative(projectRoot, file.fullPath));
|
|
763
|
+
|
|
764
|
+
// Calculate document path: replace source path prefix with knowledge base path and change extension to .md
|
|
765
|
+
let docPath;
|
|
766
|
+
if (!relativeSourcePath) {
|
|
767
|
+
// Source is project root, just prepend knowledge base path
|
|
768
|
+
docPath = `speccrew-workspace/knowledges/bizs/${platformType}-${platformSubtype}/${relativeFilePath}`;
|
|
769
|
+
} else {
|
|
770
|
+
// Source is a subdirectory, replace the prefix
|
|
771
|
+
const regex = new RegExp(`^${relativeSourcePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
|
|
772
|
+
docPath = relativeFilePath.replace(regex, `speccrew-workspace/knowledges/bizs/${platformType}-${platformSubtype}`);
|
|
773
|
+
}
|
|
774
|
+
docPath = docPath.replace(/\.[^.]+$/, '.md');
|
|
775
|
+
|
|
776
|
+
// Extract module name from relative directory, with fallback to source path's last directory
|
|
777
|
+
const moduleName = getModuleName(file.directory, excludeDirsArray, fallbackModuleName);
|
|
778
|
+
|
|
779
|
+
// Ensure docPath contains module directory when file is directly under the platform root
|
|
780
|
+
const platformPrefix = `speccrew-workspace/knowledges/bizs/${platformType}-${platformSubtype}/`;
|
|
781
|
+
if (docPath.startsWith(platformPrefix)) {
|
|
782
|
+
const docRelative = docPath.slice(platformPrefix.length);
|
|
783
|
+
if (!docRelative.includes('/')) {
|
|
784
|
+
// File directly at root level, insert module directory
|
|
785
|
+
docPath = `${platformPrefix}${moduleName}/${docRelative}`;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// 使用目录路径构建唯一 ID,避免同名文件碰撞
|
|
790
|
+
// 例: mail/account/index.vue → mail-account-index
|
|
791
|
+
// 例: mail/template/index.vue → mail-template-index
|
|
792
|
+
// 例: dict/index.vue → dict-index (无嵌套时保持兼容)
|
|
793
|
+
let dirSegments = file.directory
|
|
794
|
+
? file.directory.replace(/[\/\\]/g, '-').replace(/^-+|-+$/g, '').replace(/-+/g, '-')
|
|
795
|
+
: '';
|
|
796
|
+
|
|
797
|
+
// 顶层文件(directory = ".")不应引入 "."
|
|
798
|
+
if (dirSegments === '.' || dirSegments === './') {
|
|
799
|
+
dirSegments = '';
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// 如果 dirSegments 已包含 moduleName 前缀,去除避免重复
|
|
803
|
+
// 例: directory='mail/account', moduleName='mail' → dirSegments='account'
|
|
804
|
+
if (moduleName && dirSegments.startsWith(moduleName + '-')) {
|
|
805
|
+
dirSegments = dirSegments.slice(moduleName.length + 1);
|
|
806
|
+
} else if (moduleName && dirSegments === moduleName) {
|
|
807
|
+
dirSegments = '';
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const featureId = dirSegments
|
|
811
|
+
? `${moduleName}-${dirSegments}-${file.fileName}`
|
|
812
|
+
: `${moduleName}-${file.fileName}`;
|
|
813
|
+
const feature = {
|
|
814
|
+
id: featureId,
|
|
815
|
+
fileName: file.fileName,
|
|
816
|
+
sourcePath: relativeFilePath,
|
|
817
|
+
documentPath: docPath,
|
|
818
|
+
module: moduleName,
|
|
819
|
+
analyzed: false,
|
|
820
|
+
startedAt: null,
|
|
821
|
+
completedAt: null,
|
|
822
|
+
analysisNotes: null
|
|
823
|
+
};
|
|
824
|
+
features.push(feature);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Collect unique module names
|
|
828
|
+
const moduleList = [...new Set(features.map(f => f.module))].sort();
|
|
829
|
+
|
|
830
|
+
// Build inventory object
|
|
831
|
+
const inventory = {
|
|
832
|
+
platformName: platformName,
|
|
833
|
+
platformType: platformType,
|
|
834
|
+
sourcePath: relativeSourcePath,
|
|
835
|
+
techStack: techStackArray,
|
|
836
|
+
modules: moduleList,
|
|
837
|
+
totalFiles: files.length,
|
|
838
|
+
analyzedCount: 0,
|
|
839
|
+
pendingCount: files.length,
|
|
840
|
+
generatedAt: new Date().toISOString().replace(/[-:]/g, '').slice(0, 15).replace('T', '-'),
|
|
841
|
+
analysisMethod: analysisMethod,
|
|
842
|
+
features: features
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
// Add platformSubtype if provided
|
|
846
|
+
if (platformSubtype) {
|
|
847
|
+
inventory.platformSubtype = platformSubtype;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Add techIdentifier if provided (used by reindex-modules.js to lookup exclude_dirs)
|
|
851
|
+
if (techIdentifier) {
|
|
852
|
+
inventory.techIdentifier = techIdentifier;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Ensure sync-state directory exists
|
|
856
|
+
if (!fs.existsSync(syncStateDir)) {
|
|
857
|
+
fs.mkdirSync(syncStateDir, { recursive: true });
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Write JSON output
|
|
861
|
+
fs.writeFileSync(outputPath, JSON.stringify(inventory, null, 2), 'utf8');
|
|
862
|
+
|
|
863
|
+
console.log(`Generated features.json with ${files.length} features`);
|
|
864
|
+
console.log(`Ready for analysis: ${outputPath}`);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
main();
|