legacy-squad 1.0.0-beta.1
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/LICENSE +21 -0
- package/README.md +401 -0
- package/dist/cli.mjs +1232 -0
- package/dist/templates/claude-commands/architecture.md +42 -0
- package/dist/templates/claude-commands/business-rules.md +35 -0
- package/dist/templates/claude-commands/generate-prs.md +33 -0
- package/dist/templates/claude-commands/legacy-code.md +34 -0
- package/dist/templates/claude-commands/modernization.md +35 -0
- package/dist/templates/claude-commands/scan.md +11 -0
- package/dist/templates/claude-commands/security.md +69 -0
- package/package.json +59 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,1232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// apps/cli/src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import path8 from "node:path";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
// packages/agents/src/agent-definitions.ts
|
|
10
|
+
var SECURITY_AGENT = {
|
|
11
|
+
id: "security-agent",
|
|
12
|
+
name: "Security Agent",
|
|
13
|
+
pillar: "security",
|
|
14
|
+
role: "Application security specialist for legacy mobile and web systems.",
|
|
15
|
+
expertise: [
|
|
16
|
+
"Authentication and session management",
|
|
17
|
+
"Secrets and credential handling",
|
|
18
|
+
"Data protection (PII, LGPD)",
|
|
19
|
+
"Insecure storage patterns",
|
|
20
|
+
"API security",
|
|
21
|
+
"Mobile-specific vulnerabilities"
|
|
22
|
+
],
|
|
23
|
+
frameworks: ["OWASP MASVS V2", "OWASP ASVS", "CWE Top 25", "LGPD", "NIST SSDF"],
|
|
24
|
+
outputSections: [
|
|
25
|
+
"Authentication & Session Analysis",
|
|
26
|
+
"Secrets & Credential Management",
|
|
27
|
+
"Data Protection & Privacy",
|
|
28
|
+
"API Security Posture",
|
|
29
|
+
"Security Recommendations"
|
|
30
|
+
],
|
|
31
|
+
instructions: `You are a Security Agent for the Legacy Squad Framework.
|
|
32
|
+
|
|
33
|
+
## Your Role
|
|
34
|
+
Analyze the provided codebase context and compliance findings to produce a deep security assessment. Go beyond pattern matching \u2014 understand authentication flows, data handling patterns, and architectural security decisions.
|
|
35
|
+
|
|
36
|
+
## What You Receive
|
|
37
|
+
1. **Repo Index** \u2014 project structure, stack, dependencies, integrations
|
|
38
|
+
2. **Context Packs** \u2014 summarized source code from key modules
|
|
39
|
+
3. **Compliance Findings** \u2014 deterministic findings already detected by the engine
|
|
40
|
+
|
|
41
|
+
## What You Must Produce
|
|
42
|
+
A structured security assessment with:
|
|
43
|
+
- Confirmation or refinement of existing findings (add context the engine missed)
|
|
44
|
+
- NEW findings that regex cannot detect (logic flaws, missing authorization, insecure flows)
|
|
45
|
+
- Risk prioritization considering the production context
|
|
46
|
+
- Specific, actionable remediation steps
|
|
47
|
+
|
|
48
|
+
## Rules
|
|
49
|
+
- Every claim must reference a specific file or pattern from the context
|
|
50
|
+
- Never invent findings without evidence in the provided context
|
|
51
|
+
- Severity must follow: critical > high > medium > low > info
|
|
52
|
+
- Recommendations must be incremental (no "rewrite everything")
|
|
53
|
+
- Consider LGPD compliance for any PII handling
|
|
54
|
+
`
|
|
55
|
+
};
|
|
56
|
+
var ARCHITECTURE_AGENT = {
|
|
57
|
+
id: "architecture-agent",
|
|
58
|
+
name: "Architecture Agent",
|
|
59
|
+
pillar: "architecture",
|
|
60
|
+
role: "Software architecture specialist for legacy system evaluation.",
|
|
61
|
+
expertise: [
|
|
62
|
+
"Component and layer separation",
|
|
63
|
+
"Coupling and cohesion analysis",
|
|
64
|
+
"Integration patterns",
|
|
65
|
+
"State management architecture",
|
|
66
|
+
"Navigation and routing design",
|
|
67
|
+
"Dependency structure"
|
|
68
|
+
],
|
|
69
|
+
frameworks: ["C4 Model", "Clean Architecture", "arc42", "ADR"],
|
|
70
|
+
outputSections: [
|
|
71
|
+
"Current Architecture Overview",
|
|
72
|
+
"Layer Separation Analysis",
|
|
73
|
+
"Coupling & Cohesion Assessment",
|
|
74
|
+
"Integration Points",
|
|
75
|
+
"Architecture Risks",
|
|
76
|
+
"Target Architecture Recommendations"
|
|
77
|
+
],
|
|
78
|
+
instructions: `You are an Architecture Agent for the Legacy Squad Framework.
|
|
79
|
+
|
|
80
|
+
## Your Role
|
|
81
|
+
Analyze the codebase structure, dependencies, and integration points to map the current architecture and identify structural risks. Propose an incremental target architecture.
|
|
82
|
+
|
|
83
|
+
## What You Must Produce
|
|
84
|
+
- Current architecture description (layers, components, data flow)
|
|
85
|
+
- Coupling analysis (which modules are tightly coupled and why)
|
|
86
|
+
- Integration map (external services, APIs, databases)
|
|
87
|
+
- Architecture risks (single points of failure, circular dependencies)
|
|
88
|
+
- Target architecture proposal (incremental, not a rewrite)
|
|
89
|
+
|
|
90
|
+
## Rules
|
|
91
|
+
- Base all analysis on evidence from the Repo Index and Context Packs
|
|
92
|
+
- Architecture proposals must be incremental \u2014 Production First principle
|
|
93
|
+
- Identify the minimum viable decoupling steps
|
|
94
|
+
- Use C4 terminology (Context, Container, Component) where applicable
|
|
95
|
+
`
|
|
96
|
+
};
|
|
97
|
+
var LEGACY_CODE_AGENT = {
|
|
98
|
+
id: "legacy-code-agent",
|
|
99
|
+
name: "Legacy Code Agent",
|
|
100
|
+
pillar: "legacy_code",
|
|
101
|
+
role: "Code quality specialist for legacy codebase assessment.",
|
|
102
|
+
expertise: [
|
|
103
|
+
"Code complexity and cognitive load",
|
|
104
|
+
"Dead code and unused dependencies",
|
|
105
|
+
"Duplication patterns",
|
|
106
|
+
"Migration status (JS to TS)",
|
|
107
|
+
"Test coverage gaps",
|
|
108
|
+
"Error handling patterns"
|
|
109
|
+
],
|
|
110
|
+
frameworks: ["Clean Code", "Sonar Rules", "Cognitive Complexity"],
|
|
111
|
+
outputSections: [
|
|
112
|
+
"Code Quality Overview",
|
|
113
|
+
"Complexity Hotspots",
|
|
114
|
+
"Migration Status",
|
|
115
|
+
"Duplication Analysis",
|
|
116
|
+
"Test Coverage Assessment",
|
|
117
|
+
"Refactoring Priorities"
|
|
118
|
+
],
|
|
119
|
+
instructions: `You are a Legacy Code Agent for the Legacy Squad Framework.
|
|
120
|
+
|
|
121
|
+
## Your Role
|
|
122
|
+
Evaluate code quality, identify hotspots, assess migration status, and propose refactoring priorities that reduce risk incrementally.
|
|
123
|
+
|
|
124
|
+
## What You Must Produce
|
|
125
|
+
- Hotspot analysis (files with highest complexity/size/coupling)
|
|
126
|
+
- Migration status (JS\u2192TS, old patterns\u2192new patterns)
|
|
127
|
+
- Duplication patterns worth extracting
|
|
128
|
+
- Test coverage assessment and priority areas
|
|
129
|
+
- Ranked refactoring backlog with effort estimates
|
|
130
|
+
|
|
131
|
+
## Rules
|
|
132
|
+
- Before proposing refactoring, understand what the code does
|
|
133
|
+
- Prioritize refactoring that reduces risk, not just improves aesthetics
|
|
134
|
+
- Consider test coverage before recommending changes
|
|
135
|
+
- Estimates should be relative (S/M/L), not absolute hours
|
|
136
|
+
`
|
|
137
|
+
};
|
|
138
|
+
var BUSINESS_RULES_AGENT = {
|
|
139
|
+
id: "business-rules-agent",
|
|
140
|
+
name: "Business Rules Agent",
|
|
141
|
+
pillar: "business_rules",
|
|
142
|
+
role: "Domain logic specialist for extracting implicit business rules from legacy code.",
|
|
143
|
+
expertise: [
|
|
144
|
+
"Domain logic extraction",
|
|
145
|
+
"Validation rules",
|
|
146
|
+
"Permission and access patterns",
|
|
147
|
+
"Workflow and state machines",
|
|
148
|
+
"Exception handling as business logic",
|
|
149
|
+
"Configuration-driven behavior"
|
|
150
|
+
],
|
|
151
|
+
frameworks: ["DDD", "Event Storming", "User Story Mapping"],
|
|
152
|
+
outputSections: [
|
|
153
|
+
"Business Domain Overview",
|
|
154
|
+
"Extracted Business Rules",
|
|
155
|
+
"Validation Rules Catalog",
|
|
156
|
+
"Permission Model",
|
|
157
|
+
"Implicit Rules (hidden in code)",
|
|
158
|
+
"Rules Documentation Recommendations"
|
|
159
|
+
],
|
|
160
|
+
instructions: `You are a Business Rules Agent for the Legacy Squad Framework.
|
|
161
|
+
|
|
162
|
+
## Your Role
|
|
163
|
+
Extract business rules hidden in the legacy code. Legacy systems often encode critical business logic in conditionals, validations, and error handling that is never documented.
|
|
164
|
+
|
|
165
|
+
## What You Must Produce
|
|
166
|
+
- Catalog of explicit business rules (validations, permissions, flows)
|
|
167
|
+
- Catalog of IMPLICIT rules (hidden in conditionals, catch blocks, API responses)
|
|
168
|
+
- Domain model overview (key entities and relationships)
|
|
169
|
+
- Rules that must be preserved during modernization
|
|
170
|
+
|
|
171
|
+
## Rules
|
|
172
|
+
- Every extracted rule must cite the source file and line
|
|
173
|
+
- Distinguish between business rules and technical implementation details
|
|
174
|
+
- Flag rules that seem accidental vs intentional
|
|
175
|
+
- Use domain language, not technical jargon
|
|
176
|
+
`
|
|
177
|
+
};
|
|
178
|
+
var MODERNIZATION_AGENT = {
|
|
179
|
+
id: "modernization-agent",
|
|
180
|
+
name: "Modernization Agent",
|
|
181
|
+
pillar: "modernization",
|
|
182
|
+
role: "Modernization strategy specialist for incremental legacy evolution.",
|
|
183
|
+
expertise: [
|
|
184
|
+
"Strangler Fig pattern",
|
|
185
|
+
"Branch by Abstraction",
|
|
186
|
+
"Progressive Delivery",
|
|
187
|
+
"Stack upgrade planning",
|
|
188
|
+
"Risk-based prioritization",
|
|
189
|
+
"Deployability assessment"
|
|
190
|
+
],
|
|
191
|
+
frameworks: ["Strangler Fig", "Branch by Abstraction", "Progressive Delivery", "Feature Flags"],
|
|
192
|
+
outputSections: [
|
|
193
|
+
"Modernization Strategy",
|
|
194
|
+
"Phase Roadmap",
|
|
195
|
+
"Stack Upgrade Plan",
|
|
196
|
+
"Risk Matrix",
|
|
197
|
+
"Rollback Strategy",
|
|
198
|
+
"Execution Readiness Score"
|
|
199
|
+
],
|
|
200
|
+
instructions: `You are a Modernization Agent for the Legacy Squad Framework.
|
|
201
|
+
|
|
202
|
+
## Your Role
|
|
203
|
+
Synthesize findings from all other agents into a concrete, phased modernization plan. Every recommendation must be incremental, reversible, and deployable.
|
|
204
|
+
|
|
205
|
+
## What You Must Produce
|
|
206
|
+
- Recommended modernization strategy (Strangler Fig, Branch by Abstraction, etc.)
|
|
207
|
+
- Phased roadmap (Foundation \u2192 Core \u2192 Evolution)
|
|
208
|
+
- Stack upgrade plan with risk assessment
|
|
209
|
+
- Deployability Score per phase (1-10)
|
|
210
|
+
- Execution Readiness Score (0-100)
|
|
211
|
+
|
|
212
|
+
## Rules
|
|
213
|
+
- No big-bang rewrites \u2014 every phase must be deployable independently
|
|
214
|
+
- Consider production risk in every recommendation
|
|
215
|
+
- Rollback strategy is mandatory for every phase
|
|
216
|
+
- Human approval required for high-risk changes
|
|
217
|
+
`
|
|
218
|
+
};
|
|
219
|
+
var ALL_AGENTS = [
|
|
220
|
+
SECURITY_AGENT,
|
|
221
|
+
ARCHITECTURE_AGENT,
|
|
222
|
+
LEGACY_CODE_AGENT,
|
|
223
|
+
BUSINESS_RULES_AGENT,
|
|
224
|
+
MODERNIZATION_AGENT
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
// packages/agents/src/installer.ts
|
|
228
|
+
import { writeFile, mkdir, readFile as readFile2 } from "node:fs/promises";
|
|
229
|
+
import path6 from "node:path";
|
|
230
|
+
|
|
231
|
+
// packages/scanner/src/node-filesystem.ts
|
|
232
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
233
|
+
import path from "node:path";
|
|
234
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
235
|
+
"node_modules",
|
|
236
|
+
".git",
|
|
237
|
+
"dist",
|
|
238
|
+
"build",
|
|
239
|
+
".next",
|
|
240
|
+
"vendor",
|
|
241
|
+
"__pycache__",
|
|
242
|
+
".gradle",
|
|
243
|
+
"bin",
|
|
244
|
+
"obj",
|
|
245
|
+
"coverage",
|
|
246
|
+
".legacy-squad"
|
|
247
|
+
]);
|
|
248
|
+
var NodeFileSystem = class {
|
|
249
|
+
async readDir(dirPath) {
|
|
250
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
251
|
+
return entries.filter((e) => !IGNORED_DIRS.has(e.name)).map((e) => path.join(dirPath, e.name));
|
|
252
|
+
}
|
|
253
|
+
async readFile(filePath) {
|
|
254
|
+
return readFile(filePath, "utf-8");
|
|
255
|
+
}
|
|
256
|
+
async stat(filePath) {
|
|
257
|
+
const s = await stat(filePath);
|
|
258
|
+
return { size: s.size, isDirectory: s.isDirectory() };
|
|
259
|
+
}
|
|
260
|
+
async exists(filePath) {
|
|
261
|
+
try {
|
|
262
|
+
await stat(filePath);
|
|
263
|
+
return true;
|
|
264
|
+
} catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async glob(rootPath, pattern) {
|
|
269
|
+
const results = [];
|
|
270
|
+
await this.walkForGlob(rootPath, pattern, results);
|
|
271
|
+
return results;
|
|
272
|
+
}
|
|
273
|
+
async walkForGlob(dir, pattern, results) {
|
|
274
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
277
|
+
const fullPath = path.join(dir, entry.name);
|
|
278
|
+
if (entry.isDirectory()) {
|
|
279
|
+
await this.walkForGlob(fullPath, pattern, results);
|
|
280
|
+
} else if (entry.name.match(pattern)) {
|
|
281
|
+
results.push(fullPath);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// packages/scanner/src/repo-scanner.ts
|
|
288
|
+
import path3 from "node:path";
|
|
289
|
+
|
|
290
|
+
// packages/scanner/src/stack-detector.ts
|
|
291
|
+
import path2 from "node:path";
|
|
292
|
+
var packageJsonDetector = {
|
|
293
|
+
filename: "package.json",
|
|
294
|
+
detect(content, filePath) {
|
|
295
|
+
const pkg = JSON.parse(content);
|
|
296
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
297
|
+
const stack = [];
|
|
298
|
+
const dependencies = [];
|
|
299
|
+
let projectType = "backend";
|
|
300
|
+
for (const [name, version] of Object.entries(pkg.dependencies ?? {})) {
|
|
301
|
+
dependencies.push({
|
|
302
|
+
name,
|
|
303
|
+
version: String(version),
|
|
304
|
+
manager: "npm",
|
|
305
|
+
scope: "runtime"
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
for (const [name, version] of Object.entries(pkg.devDependencies ?? {})) {
|
|
309
|
+
dependencies.push({
|
|
310
|
+
name,
|
|
311
|
+
version: String(version),
|
|
312
|
+
manager: "npm",
|
|
313
|
+
scope: "dev"
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
if (allDeps["react-native"]) {
|
|
317
|
+
stack.push({ name: "react-native", type: "framework", version: String(allDeps["react-native"]), source: filePath });
|
|
318
|
+
projectType = "mobile";
|
|
319
|
+
}
|
|
320
|
+
if (allDeps["expo"]) {
|
|
321
|
+
stack.push({ name: "expo", type: "framework", version: String(allDeps["expo"]), source: filePath });
|
|
322
|
+
projectType = "mobile";
|
|
323
|
+
}
|
|
324
|
+
if (allDeps["react"] && !allDeps["react-native"]) {
|
|
325
|
+
stack.push({ name: "react", type: "framework", version: String(allDeps["react"]), source: filePath });
|
|
326
|
+
projectType = "frontend";
|
|
327
|
+
}
|
|
328
|
+
if (allDeps["next"]) {
|
|
329
|
+
stack.push({ name: "next", type: "framework", version: String(allDeps["next"]), source: filePath });
|
|
330
|
+
projectType = "fullstack";
|
|
331
|
+
}
|
|
332
|
+
if (allDeps["express"]) {
|
|
333
|
+
stack.push({ name: "express", type: "framework", version: String(allDeps["express"]), source: filePath });
|
|
334
|
+
projectType = "backend";
|
|
335
|
+
}
|
|
336
|
+
if (allDeps["typescript"]) {
|
|
337
|
+
stack.push({ name: "typescript", type: "language", version: String(allDeps["typescript"]), source: filePath });
|
|
338
|
+
}
|
|
339
|
+
if (allDeps["mobx"]) {
|
|
340
|
+
stack.push({ name: "mobx", type: "library", version: String(allDeps["mobx"]), source: filePath });
|
|
341
|
+
}
|
|
342
|
+
if (allDeps["axios"]) {
|
|
343
|
+
stack.push({ name: "axios", type: "library", version: String(allDeps["axios"]), source: filePath });
|
|
344
|
+
}
|
|
345
|
+
if (allDeps["styled-components"]) {
|
|
346
|
+
stack.push({ name: "styled-components", type: "library", version: String(allDeps["styled-components"]), source: filePath });
|
|
347
|
+
}
|
|
348
|
+
const nodeVersion = pkg.engines?.node ?? "unknown";
|
|
349
|
+
stack.push({ name: "node", type: "runtime", version: nodeVersion, source: filePath });
|
|
350
|
+
return {
|
|
351
|
+
stack,
|
|
352
|
+
dependencies,
|
|
353
|
+
projectType,
|
|
354
|
+
projectName: pkg.name ?? path2.basename(path2.dirname(filePath))
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
var composerJsonDetector = {
|
|
359
|
+
filename: "composer.json",
|
|
360
|
+
detect(content, filePath) {
|
|
361
|
+
const composer = JSON.parse(content);
|
|
362
|
+
const stack = [
|
|
363
|
+
{ name: "php", type: "language", version: composer.require?.php ?? "unknown", source: filePath }
|
|
364
|
+
];
|
|
365
|
+
const dependencies = [];
|
|
366
|
+
for (const [name, version] of Object.entries(composer.require ?? {})) {
|
|
367
|
+
if (name === "php") continue;
|
|
368
|
+
dependencies.push({ name, version: String(version), manager: "composer", scope: "runtime" });
|
|
369
|
+
if (name === "laravel/framework") {
|
|
370
|
+
stack.push({ name: "laravel", type: "framework", version: String(version), source: filePath });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return { stack, dependencies, projectType: "backend", projectName: composer.name ?? "php-project" };
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
var csprojDetector = {
|
|
377
|
+
filename: ".csproj",
|
|
378
|
+
detect(content, filePath) {
|
|
379
|
+
const stack = [];
|
|
380
|
+
const dependencies = [];
|
|
381
|
+
const tfmMatch = content.match(/<TargetFramework>(.*?)<\/TargetFramework>/);
|
|
382
|
+
if (tfmMatch) {
|
|
383
|
+
stack.push({ name: "dotnet", type: "runtime", version: tfmMatch[1], source: filePath });
|
|
384
|
+
}
|
|
385
|
+
const pkgRefRegex = /<PackageReference\s+Include="([^"]+)"\s+Version="([^"]+)"/g;
|
|
386
|
+
let match;
|
|
387
|
+
while ((match = pkgRefRegex.exec(content)) !== null) {
|
|
388
|
+
dependencies.push({ name: match[1], version: match[2], manager: "nuget", scope: "runtime" });
|
|
389
|
+
}
|
|
390
|
+
return { stack, dependencies, projectType: "backend", projectName: path2.basename(filePath, ".csproj") };
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
var pomXmlDetector = {
|
|
394
|
+
filename: "pom.xml",
|
|
395
|
+
detect(content, filePath) {
|
|
396
|
+
const stack = [
|
|
397
|
+
{ name: "java", type: "language", version: "unknown", source: filePath }
|
|
398
|
+
];
|
|
399
|
+
const dependencies = [];
|
|
400
|
+
if (content.includes("spring-boot")) {
|
|
401
|
+
stack.push({ name: "spring-boot", type: "framework", version: "detected", source: filePath });
|
|
402
|
+
}
|
|
403
|
+
return { stack, dependencies, projectType: "backend", projectName: "java-project" };
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
var ALL_DETECTORS = [
|
|
407
|
+
packageJsonDetector,
|
|
408
|
+
composerJsonDetector,
|
|
409
|
+
csprojDetector,
|
|
410
|
+
pomXmlDetector
|
|
411
|
+
];
|
|
412
|
+
var MANIFEST_FILES = ALL_DETECTORS.map((d) => d.filename);
|
|
413
|
+
async function detectFromManifests(rootPath, fs) {
|
|
414
|
+
for (const detector of ALL_DETECTORS) {
|
|
415
|
+
if (detector.filename === ".csproj") {
|
|
416
|
+
const files = await fs.glob(rootPath, /\.csproj$/.source);
|
|
417
|
+
if (files.length > 0) {
|
|
418
|
+
const content = await fs.readFile(files[0]);
|
|
419
|
+
return detector.detect(content, files[0]);
|
|
420
|
+
}
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
const manifestPath = path2.join(rootPath, detector.filename);
|
|
424
|
+
if (await fs.exists(manifestPath)) {
|
|
425
|
+
const content = await fs.readFile(manifestPath);
|
|
426
|
+
return detector.detect(content, manifestPath);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
async function detectFromExtensions(rootPath, fs) {
|
|
432
|
+
const extensionMap = {
|
|
433
|
+
"\\.tsx?$": { name: "typescript", type: "language", version: "detected", source: "file-extensions" },
|
|
434
|
+
"\\.jsx?$": { name: "javascript", type: "language", version: "detected", source: "file-extensions" },
|
|
435
|
+
"\\.php$": { name: "php", type: "language", version: "detected", source: "file-extensions" },
|
|
436
|
+
"\\.cs$": { name: "csharp", type: "language", version: "detected", source: "file-extensions" },
|
|
437
|
+
"\\.java$": { name: "java", type: "language", version: "detected", source: "file-extensions" },
|
|
438
|
+
"\\.py$": { name: "python", type: "language", version: "detected", source: "file-extensions" },
|
|
439
|
+
"\\.dart$": { name: "dart", type: "language", version: "detected", source: "file-extensions" }
|
|
440
|
+
};
|
|
441
|
+
const detected = [];
|
|
442
|
+
for (const [pattern, item] of Object.entries(extensionMap)) {
|
|
443
|
+
const files = await fs.glob(rootPath, pattern);
|
|
444
|
+
if (files.length > 0) {
|
|
445
|
+
detected.push(item);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return detected;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// packages/scanner/src/repo-scanner.ts
|
|
452
|
+
function toPosix(p) {
|
|
453
|
+
return p.replace(/\\/g, "/");
|
|
454
|
+
}
|
|
455
|
+
var SOURCE_EXTENSIONS = /\.(tsx?|jsx?|php|cs|java|py|dart|vue|svelte)$/;
|
|
456
|
+
var LARGE_FILE_THRESHOLD = 1e4;
|
|
457
|
+
var RepoScanner = class {
|
|
458
|
+
constructor(fs) {
|
|
459
|
+
this.fs = fs;
|
|
460
|
+
}
|
|
461
|
+
fs;
|
|
462
|
+
async scan(rootPath) {
|
|
463
|
+
const manifestResult = await detectFromManifests(rootPath, this.fs);
|
|
464
|
+
let stack = manifestResult?.stack ?? [];
|
|
465
|
+
if (stack.length === 0) {
|
|
466
|
+
stack = await detectFromExtensions(rootPath, this.fs);
|
|
467
|
+
}
|
|
468
|
+
const projectName = manifestResult?.projectName ?? path3.basename(rootPath);
|
|
469
|
+
const projectType = manifestResult?.projectType ?? "backend";
|
|
470
|
+
const dependencies = manifestResult?.dependencies ?? [];
|
|
471
|
+
const sourceFiles = await this.collectSourceFiles(rootPath);
|
|
472
|
+
const modules = this.detectModules(rootPath, sourceFiles);
|
|
473
|
+
const entrypoints = this.detectEntrypoints(rootPath, sourceFiles, projectType);
|
|
474
|
+
const integrations = await this.detectIntegrations(rootPath, sourceFiles);
|
|
475
|
+
const hotspots = await this.detectHotspots(rootPath, sourceFiles);
|
|
476
|
+
return {
|
|
477
|
+
project: {
|
|
478
|
+
name: projectName,
|
|
479
|
+
type: projectType,
|
|
480
|
+
rootPath,
|
|
481
|
+
detectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
482
|
+
},
|
|
483
|
+
stack,
|
|
484
|
+
modules,
|
|
485
|
+
entrypoints,
|
|
486
|
+
dependencies,
|
|
487
|
+
integrations,
|
|
488
|
+
hotspots
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
async collectSourceFiles(rootPath) {
|
|
492
|
+
return this.fs.glob(rootPath, SOURCE_EXTENSIONS.source);
|
|
493
|
+
}
|
|
494
|
+
detectModules(rootPath, files) {
|
|
495
|
+
const moduleMap = /* @__PURE__ */ new Map();
|
|
496
|
+
for (const file of files) {
|
|
497
|
+
const relative = toPosix(path3.relative(rootPath, file));
|
|
498
|
+
const parts = relative.split("/");
|
|
499
|
+
if (parts.length >= 2) {
|
|
500
|
+
const moduleKey = parts.slice(0, 2).join("/");
|
|
501
|
+
const existing = moduleMap.get(moduleKey) ?? [];
|
|
502
|
+
existing.push(file);
|
|
503
|
+
moduleMap.set(moduleKey, existing);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return Array.from(moduleMap.entries()).map(([modulePath, moduleFiles]) => ({
|
|
507
|
+
name: path3.basename(modulePath),
|
|
508
|
+
path: modulePath,
|
|
509
|
+
type: "module",
|
|
510
|
+
filesCount: moduleFiles.length,
|
|
511
|
+
summary: `Module with ${moduleFiles.length} source files`
|
|
512
|
+
}));
|
|
513
|
+
}
|
|
514
|
+
detectEntrypoints(rootPath, files, projectType) {
|
|
515
|
+
const entrypoints = [];
|
|
516
|
+
if (projectType === "mobile") {
|
|
517
|
+
for (const file of files) {
|
|
518
|
+
const relative = toPosix(path3.relative(rootPath, file));
|
|
519
|
+
if (relative.match(/src\/screens\/[^/]+\/index\.(tsx?|jsx?)$/)) {
|
|
520
|
+
const screenName = relative.split("/").slice(-2, -1)[0];
|
|
521
|
+
entrypoints.push({
|
|
522
|
+
type: "screen",
|
|
523
|
+
name: screenName,
|
|
524
|
+
path: relative,
|
|
525
|
+
method: "render"
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
for (const file of files) {
|
|
531
|
+
const relative = toPosix(path3.relative(rootPath, file));
|
|
532
|
+
if (relative.match(/^(index|main|App)\.(tsx?|jsx?|js)$/)) {
|
|
533
|
+
entrypoints.push({
|
|
534
|
+
type: "component",
|
|
535
|
+
name: path3.basename(relative),
|
|
536
|
+
path: relative,
|
|
537
|
+
method: "main"
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return entrypoints;
|
|
542
|
+
}
|
|
543
|
+
async detectIntegrations(rootPath, files) {
|
|
544
|
+
const integrations = [];
|
|
545
|
+
const seen = /* @__PURE__ */ new Set();
|
|
546
|
+
for (const file of files) {
|
|
547
|
+
try {
|
|
548
|
+
const content = await this.fs.readFile(file);
|
|
549
|
+
const relative = toPosix(path3.relative(rootPath, file));
|
|
550
|
+
const urlMatches = content.matchAll(/https?:\/\/[^\s'"`,)]+/g);
|
|
551
|
+
for (const match of urlMatches) {
|
|
552
|
+
const url = match[0];
|
|
553
|
+
try {
|
|
554
|
+
const hostname = new URL(url).hostname;
|
|
555
|
+
if (!seen.has(hostname) && !hostname.includes("google.com/viewer")) {
|
|
556
|
+
seen.add(hostname);
|
|
557
|
+
integrations.push({
|
|
558
|
+
type: "api",
|
|
559
|
+
name: hostname,
|
|
560
|
+
evidence: url.substring(0, 100),
|
|
561
|
+
path: relative
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
} catch {
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (content.includes("firebase") || content.includes("Firebase")) {
|
|
568
|
+
if (!seen.has("firebase")) {
|
|
569
|
+
seen.add("firebase");
|
|
570
|
+
integrations.push({
|
|
571
|
+
type: "external_service",
|
|
572
|
+
name: "Firebase",
|
|
573
|
+
evidence: "Firebase SDK usage detected",
|
|
574
|
+
path: relative
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
} catch {
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return integrations;
|
|
582
|
+
}
|
|
583
|
+
async detectHotspots(rootPath, files) {
|
|
584
|
+
const hotspots = [];
|
|
585
|
+
for (const file of files) {
|
|
586
|
+
try {
|
|
587
|
+
const s = await this.fs.stat(file);
|
|
588
|
+
const relative = toPosix(path3.relative(rootPath, file));
|
|
589
|
+
if (s.size > LARGE_FILE_THRESHOLD) {
|
|
590
|
+
hotspots.push({
|
|
591
|
+
path: relative,
|
|
592
|
+
reason: "large_file",
|
|
593
|
+
score: Math.min(Math.round(s.size / 1e3), 100)
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
} catch {
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return hotspots.sort((a, b) => b.score - a.score).slice(0, 20);
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
// packages/context/src/context-builder.ts
|
|
604
|
+
import path4 from "node:path";
|
|
605
|
+
function toPosix2(p) {
|
|
606
|
+
return p.replace(/\\/g, "/");
|
|
607
|
+
}
|
|
608
|
+
var TOKENS_PER_CHAR = 0.25;
|
|
609
|
+
var ContextBuilder = class {
|
|
610
|
+
constructor(fs) {
|
|
611
|
+
this.fs = fs;
|
|
612
|
+
}
|
|
613
|
+
fs;
|
|
614
|
+
async buildPacks(rootPath, repoIndex) {
|
|
615
|
+
const packs = [];
|
|
616
|
+
for (const mod of repoIndex.modules) {
|
|
617
|
+
const pack = await this.buildModulePack(rootPath, mod.name, mod.path, repoIndex);
|
|
618
|
+
packs.push(pack);
|
|
619
|
+
}
|
|
620
|
+
return packs;
|
|
621
|
+
}
|
|
622
|
+
async buildModulePack(rootPath, moduleName, modulePath, repoIndex) {
|
|
623
|
+
const fullModulePath = path4.join(rootPath, modulePath);
|
|
624
|
+
let keyFiles = [];
|
|
625
|
+
let totalSize = 0;
|
|
626
|
+
try {
|
|
627
|
+
const files = await this.fs.glob(fullModulePath, "\\.(tsx?|jsx?|php|cs|java)$");
|
|
628
|
+
keyFiles = files.map((f) => toPosix2(path4.relative(rootPath, f))).slice(0, 10);
|
|
629
|
+
for (const file of files.slice(0, 10)) {
|
|
630
|
+
try {
|
|
631
|
+
const stat3 = await this.fs.stat(file);
|
|
632
|
+
totalSize += stat3.size;
|
|
633
|
+
} catch {
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
} catch {
|
|
637
|
+
}
|
|
638
|
+
const moduleEntrypoints = repoIndex.entrypoints.filter((e) => e.path.startsWith(modulePath)).map((e) => `${e.type}: ${e.name}`);
|
|
639
|
+
const moduleDeps = repoIndex.dependencies.slice(0, 5).map((d) => d.name);
|
|
640
|
+
return {
|
|
641
|
+
id: `${moduleName}.context`,
|
|
642
|
+
module: moduleName,
|
|
643
|
+
summary: `Module ${moduleName} with ${keyFiles.length} key files.`,
|
|
644
|
+
keyFiles,
|
|
645
|
+
entrypoints: moduleEntrypoints,
|
|
646
|
+
dependencies: moduleDeps,
|
|
647
|
+
risks: [],
|
|
648
|
+
tokenEstimate: Math.round(totalSize * TOKENS_PER_CHAR)
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
// packages/rules/src/compliance-engine.ts
|
|
654
|
+
import path5 from "node:path";
|
|
655
|
+
|
|
656
|
+
// packages/rules/src/rule-catalog.ts
|
|
657
|
+
var SECURITY_RULES = [
|
|
658
|
+
{
|
|
659
|
+
id: "SEC-CRED-001",
|
|
660
|
+
title: "Hardcoded credentials in source code",
|
|
661
|
+
category: "security",
|
|
662
|
+
severity: "critical",
|
|
663
|
+
appliesTo: ["react-native", "node", "mobile", "backend", "frontend"],
|
|
664
|
+
frameworks: ["OWASP MASVS V2", "CWE-798"],
|
|
665
|
+
detection: {
|
|
666
|
+
type: "pattern",
|
|
667
|
+
patterns: [
|
|
668
|
+
`password\\s*[:=]\\s*['"][^\\s'"]{4,}['"]`,
|
|
669
|
+
`senha\\s*[:=]\\s*['"][^\\s'"]{4,}['"]`,
|
|
670
|
+
`secret\\s*[:=]\\s*['"][^\\s'"]{4,}['"]`,
|
|
671
|
+
`api_?key\\s*[:=]\\s*['"][^\\s'"]{4,}['"]`
|
|
672
|
+
]
|
|
673
|
+
},
|
|
674
|
+
impact: "Credentials can be extracted from app binary or source repository, enabling unauthorized access.",
|
|
675
|
+
recommendation: "Move all credentials to environment variables, secure vault (Azure Key Vault, AWS Secrets Manager), or expo-secure-store for mobile."
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
id: "SEC-CRED-002",
|
|
679
|
+
title: "Keystore or signing certificate committed to repository",
|
|
680
|
+
category: "security",
|
|
681
|
+
severity: "high",
|
|
682
|
+
appliesTo: ["react-native", "mobile", "android"],
|
|
683
|
+
frameworks: ["OWASP MASVS V2", "CWE-312"],
|
|
684
|
+
detection: {
|
|
685
|
+
type: "filename",
|
|
686
|
+
patterns: [
|
|
687
|
+
"\\.keystore$",
|
|
688
|
+
"\\.jks$",
|
|
689
|
+
"google-services\\.json$",
|
|
690
|
+
"GoogleService-Info\\.plist$"
|
|
691
|
+
]
|
|
692
|
+
},
|
|
693
|
+
impact: "Signing keys or service credentials exposed in version control may allow app impersonation or unauthorized API access.",
|
|
694
|
+
recommendation: "Remove from repository, add to .gitignore, and distribute via secure CI/CD secrets."
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
id: "SEC-LOG-001",
|
|
698
|
+
title: "Console.log active in production code",
|
|
699
|
+
category: "security",
|
|
700
|
+
severity: "medium",
|
|
701
|
+
appliesTo: ["react-native", "node", "mobile", "frontend"],
|
|
702
|
+
frameworks: ["OWASP MASVS V2", "CWE-532"],
|
|
703
|
+
detection: {
|
|
704
|
+
type: "pattern",
|
|
705
|
+
patterns: ["console\\.log\\("]
|
|
706
|
+
},
|
|
707
|
+
impact: "Sensitive data (tokens, user info, API responses) may leak to device logs accessible by other apps or debugging tools.",
|
|
708
|
+
recommendation: "Replace console.log with a logger that strips output in production builds, or use crashlytics().recordError() for error tracking."
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
id: "SEC-LOG-002",
|
|
712
|
+
title: "Sensitive data (CPF/PII) logged or sent to external service",
|
|
713
|
+
category: "security",
|
|
714
|
+
severity: "high",
|
|
715
|
+
appliesTo: ["react-native", "node", "mobile", "backend"],
|
|
716
|
+
frameworks: ["OWASP MASVS V2", "CWE-532", "LGPD"],
|
|
717
|
+
detection: {
|
|
718
|
+
type: "pattern",
|
|
719
|
+
patterns: [
|
|
720
|
+
"cpf[^a-zA-Z].*(?:log|set|ref|database|send)",
|
|
721
|
+
"_CPF.*(?:log|set|ref|database|send)",
|
|
722
|
+
"generateRawCPF"
|
|
723
|
+
]
|
|
724
|
+
},
|
|
725
|
+
impact: "PII (CPF) transmitted or logged without masking violates data protection regulations (LGPD) and may expose user identity.",
|
|
726
|
+
recommendation: "Mask PII before logging. Never send raw CPF to external services. Use anonymized identifiers for analytics."
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
id: "SEC-ERR-001",
|
|
730
|
+
title: "Empty catch block swallowing errors silently",
|
|
731
|
+
category: "security",
|
|
732
|
+
severity: "medium",
|
|
733
|
+
appliesTo: ["react-native", "node", "mobile", "frontend", "backend"],
|
|
734
|
+
frameworks: ["CWE-390", "Clean Code"],
|
|
735
|
+
detection: {
|
|
736
|
+
type: "pattern",
|
|
737
|
+
patterns: [
|
|
738
|
+
"catch\\s*\\([^)]*\\)\\s*\\{\\s*\\}",
|
|
739
|
+
"catch\\s*\\{\\s*\\}"
|
|
740
|
+
]
|
|
741
|
+
},
|
|
742
|
+
impact: "Errors are silently discarded, masking bugs and security issues in production.",
|
|
743
|
+
recommendation: "Log errors to a monitoring service (Sentry, Crashlytics). Never leave catch blocks empty."
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
id: "SEC-STORE-001",
|
|
747
|
+
title: "Token stored in insecure storage (AsyncStorage)",
|
|
748
|
+
category: "security",
|
|
749
|
+
severity: "high",
|
|
750
|
+
appliesTo: ["react-native", "mobile"],
|
|
751
|
+
frameworks: ["OWASP MASVS V2"],
|
|
752
|
+
detection: {
|
|
753
|
+
type: "pattern",
|
|
754
|
+
patterns: [
|
|
755
|
+
`AsyncStorage\\.setItem\\s*\\(\\s*['"](?:token|access_token|refresh_token|api_key)`
|
|
756
|
+
]
|
|
757
|
+
},
|
|
758
|
+
impact: "Authentication tokens in AsyncStorage are accessible to other apps on rooted devices.",
|
|
759
|
+
recommendation: "Use expo-secure-store, react-native-keychain, or native Keychain/Keystore APIs."
|
|
760
|
+
}
|
|
761
|
+
];
|
|
762
|
+
var CODE_QUALITY_RULES = [
|
|
763
|
+
{
|
|
764
|
+
id: "CQ-MIX-001",
|
|
765
|
+
title: "Mixed JavaScript and TypeScript files in same module",
|
|
766
|
+
category: "legacy_code",
|
|
767
|
+
severity: "low",
|
|
768
|
+
appliesTo: ["react-native", "node", "frontend"],
|
|
769
|
+
frameworks: ["Clean Code"],
|
|
770
|
+
detection: {
|
|
771
|
+
type: "structure",
|
|
772
|
+
patterns: ["mixed-js-ts"]
|
|
773
|
+
},
|
|
774
|
+
impact: "Inconsistent typing reduces IDE support and increases risk of runtime errors.",
|
|
775
|
+
recommendation: "Complete TypeScript migration module by module. Prioritize modules with business logic."
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
id: "CQ-DEP-001",
|
|
779
|
+
title: "Transitive dependency used without explicit declaration",
|
|
780
|
+
category: "legacy_code",
|
|
781
|
+
severity: "medium",
|
|
782
|
+
appliesTo: ["react-native", "node", "frontend"],
|
|
783
|
+
frameworks: ["Clean Code"],
|
|
784
|
+
detection: {
|
|
785
|
+
type: "pattern",
|
|
786
|
+
patterns: [
|
|
787
|
+
`require\\(['"](?:prop-types|lodash|sprintf-js|payment|react-native-flip-card|react-native-animatable)['"]\\)`,
|
|
788
|
+
`from\\s+['"](?:prop-types|lodash|sprintf-js|payment|react-native-flip-card|react-native-animatable)['"]`
|
|
789
|
+
]
|
|
790
|
+
},
|
|
791
|
+
impact: "Transitive dependencies may be removed in future updates, causing silent breakage.",
|
|
792
|
+
recommendation: "Declare all used packages explicitly in package.json or replace with native alternatives."
|
|
793
|
+
}
|
|
794
|
+
];
|
|
795
|
+
var ALL_RULES = [...SECURITY_RULES, ...CODE_QUALITY_RULES];
|
|
796
|
+
|
|
797
|
+
// packages/rules/src/compliance-engine.ts
|
|
798
|
+
function toPosix3(p) {
|
|
799
|
+
return p.replace(/\\/g, "/");
|
|
800
|
+
}
|
|
801
|
+
var SEVERITY_TO_PRIORITY = {
|
|
802
|
+
critical: "P0",
|
|
803
|
+
high: "P1",
|
|
804
|
+
medium: "P2",
|
|
805
|
+
low: "P3",
|
|
806
|
+
info: "P4"
|
|
807
|
+
};
|
|
808
|
+
var SOURCE_EXTENSIONS2 = /\.(tsx?|jsx?|php|cs|java|py|dart)$/;
|
|
809
|
+
var ComplianceEngine = class {
|
|
810
|
+
constructor(fs) {
|
|
811
|
+
this.fs = fs;
|
|
812
|
+
}
|
|
813
|
+
fs;
|
|
814
|
+
loadRules() {
|
|
815
|
+
return ALL_RULES;
|
|
816
|
+
}
|
|
817
|
+
async evaluate(rootPath, repoIndex) {
|
|
818
|
+
const applicableRules = this.filterApplicableRules(repoIndex);
|
|
819
|
+
const allFiles = await this.collectAllFiles(rootPath);
|
|
820
|
+
const findings = [];
|
|
821
|
+
for (const rule of applicableRules) {
|
|
822
|
+
const ruleFindings = await this.evaluateRule(rule, rootPath, allFiles);
|
|
823
|
+
findings.push(...ruleFindings);
|
|
824
|
+
}
|
|
825
|
+
return this.deduplicateFindings(findings);
|
|
826
|
+
}
|
|
827
|
+
filterApplicableRules(repoIndex) {
|
|
828
|
+
const projectTags = /* @__PURE__ */ new Set();
|
|
829
|
+
projectTags.add(repoIndex.project.type);
|
|
830
|
+
for (const item of repoIndex.stack) {
|
|
831
|
+
projectTags.add(item.name);
|
|
832
|
+
}
|
|
833
|
+
return ALL_RULES.filter(
|
|
834
|
+
(rule) => rule.appliesTo.some((tag) => projectTags.has(tag))
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
async collectAllFiles(rootPath) {
|
|
838
|
+
return this.fs.glob(rootPath, SOURCE_EXTENSIONS2.source);
|
|
839
|
+
}
|
|
840
|
+
async evaluateRule(rule, rootPath, files) {
|
|
841
|
+
switch (rule.detection.type) {
|
|
842
|
+
case "pattern":
|
|
843
|
+
return this.evaluatePatternRule(rule, rootPath, files);
|
|
844
|
+
case "filename":
|
|
845
|
+
return this.evaluateFilenameRule(rule, rootPath, files);
|
|
846
|
+
case "structure":
|
|
847
|
+
return this.evaluateStructureRule(rule, rootPath, files);
|
|
848
|
+
default:
|
|
849
|
+
return [];
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
async evaluatePatternRule(rule, rootPath, files) {
|
|
853
|
+
const allEvidence = [];
|
|
854
|
+
for (const file of files) {
|
|
855
|
+
try {
|
|
856
|
+
const content = await this.fs.readFile(file);
|
|
857
|
+
const lines = content.split("\n");
|
|
858
|
+
const relative = toPosix3(path5.relative(rootPath, file));
|
|
859
|
+
for (const pattern of rule.detection.patterns) {
|
|
860
|
+
const regex = new RegExp(pattern, "gi");
|
|
861
|
+
for (let i = 0; i < lines.length; i++) {
|
|
862
|
+
if (regex.test(lines[i])) {
|
|
863
|
+
allEvidence.push({
|
|
864
|
+
file: relative,
|
|
865
|
+
line: i + 1,
|
|
866
|
+
snippet: lines[i].trim().substring(0, 120)
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
regex.lastIndex = 0;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
} catch {
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (allEvidence.length === 0) return [];
|
|
876
|
+
return [{
|
|
877
|
+
id: rule.id,
|
|
878
|
+
title: rule.title,
|
|
879
|
+
pillar: rule.category,
|
|
880
|
+
severity: rule.severity,
|
|
881
|
+
evidence: allEvidence.slice(0, 10),
|
|
882
|
+
frameworks: rule.frameworks,
|
|
883
|
+
impact: rule.impact,
|
|
884
|
+
recommendation: rule.recommendation,
|
|
885
|
+
priority: SEVERITY_TO_PRIORITY[rule.severity]
|
|
886
|
+
}];
|
|
887
|
+
}
|
|
888
|
+
async evaluateFilenameRule(rule, rootPath, _files) {
|
|
889
|
+
const allEvidence = [];
|
|
890
|
+
for (const pattern of rule.detection.patterns) {
|
|
891
|
+
const matchingFiles = await this.fs.glob(rootPath, pattern);
|
|
892
|
+
for (const file of matchingFiles) {
|
|
893
|
+
const relative = toPosix3(path5.relative(rootPath, file));
|
|
894
|
+
allEvidence.push({
|
|
895
|
+
file: relative,
|
|
896
|
+
line: 0,
|
|
897
|
+
snippet: `File matches sensitive pattern: ${pattern}`
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (allEvidence.length === 0) return [];
|
|
902
|
+
return [{
|
|
903
|
+
id: rule.id,
|
|
904
|
+
title: rule.title,
|
|
905
|
+
pillar: rule.category,
|
|
906
|
+
severity: rule.severity,
|
|
907
|
+
evidence: allEvidence,
|
|
908
|
+
frameworks: rule.frameworks,
|
|
909
|
+
impact: rule.impact,
|
|
910
|
+
recommendation: rule.recommendation,
|
|
911
|
+
priority: SEVERITY_TO_PRIORITY[rule.severity]
|
|
912
|
+
}];
|
|
913
|
+
}
|
|
914
|
+
async evaluateStructureRule(rule, rootPath, files) {
|
|
915
|
+
if (rule.detection.patterns.includes("mixed-js-ts")) {
|
|
916
|
+
return this.checkMixedJsTs(rule, rootPath, files);
|
|
917
|
+
}
|
|
918
|
+
return [];
|
|
919
|
+
}
|
|
920
|
+
async checkMixedJsTs(rule, rootPath, files) {
|
|
921
|
+
const hasJs = files.some((f) => f.endsWith(".js") || f.endsWith(".jsx"));
|
|
922
|
+
const hasTs = files.some((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
|
|
923
|
+
if (!hasJs || !hasTs) return [];
|
|
924
|
+
const jsFiles = files.filter((f) => f.endsWith(".js") || f.endsWith(".jsx"));
|
|
925
|
+
return [{
|
|
926
|
+
id: rule.id,
|
|
927
|
+
title: rule.title,
|
|
928
|
+
pillar: rule.category,
|
|
929
|
+
severity: rule.severity,
|
|
930
|
+
evidence: jsFiles.slice(0, 5).map((f) => ({
|
|
931
|
+
file: toPosix3(path5.relative(rootPath, f)),
|
|
932
|
+
line: 0,
|
|
933
|
+
snippet: "JavaScript file in a TypeScript project"
|
|
934
|
+
})),
|
|
935
|
+
frameworks: rule.frameworks,
|
|
936
|
+
impact: rule.impact,
|
|
937
|
+
recommendation: rule.recommendation,
|
|
938
|
+
priority: SEVERITY_TO_PRIORITY[rule.severity]
|
|
939
|
+
}];
|
|
940
|
+
}
|
|
941
|
+
deduplicateFindings(findings) {
|
|
942
|
+
const seen = /* @__PURE__ */ new Set();
|
|
943
|
+
return findings.filter((f) => {
|
|
944
|
+
if (seen.has(f.id)) return false;
|
|
945
|
+
seen.add(f.id);
|
|
946
|
+
return true;
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
// packages/agents/src/installer.ts
|
|
952
|
+
var Installer = class {
|
|
953
|
+
/**
|
|
954
|
+
* Instala o Legacy Squad Framework dentro do projeto alvo.
|
|
955
|
+
* Escaneia o repo, gera dados, instala agentes como slash commands.
|
|
956
|
+
*/
|
|
957
|
+
async install(projectRoot, templateDir) {
|
|
958
|
+
const fs = new NodeFileSystem();
|
|
959
|
+
const scanner = new RepoScanner(fs);
|
|
960
|
+
const repoIndex = await scanner.scan(projectRoot);
|
|
961
|
+
const compliance = new ComplianceEngine(fs);
|
|
962
|
+
const findings = await compliance.evaluate(projectRoot, repoIndex);
|
|
963
|
+
const contextBuilder = new ContextBuilder(fs);
|
|
964
|
+
const contextPacks = await contextBuilder.buildPacks(projectRoot, repoIndex);
|
|
965
|
+
const memoryDir = path6.join(projectRoot, ".legacy-squad", "memory");
|
|
966
|
+
await mkdir(memoryDir, { recursive: true });
|
|
967
|
+
const repoIndexPath = path6.join(memoryDir, "repo-index.json");
|
|
968
|
+
const findingsPath = path6.join(memoryDir, "findings.json");
|
|
969
|
+
const contextPacksPath = path6.join(memoryDir, "context-packs.json");
|
|
970
|
+
await writeFile(repoIndexPath, JSON.stringify(repoIndex, null, 2), "utf-8");
|
|
971
|
+
await writeFile(findingsPath, JSON.stringify(findings, null, 2), "utf-8");
|
|
972
|
+
await writeFile(contextPacksPath, JSON.stringify(contextPacks, null, 2), "utf-8");
|
|
973
|
+
const configDir = path6.join(projectRoot, ".legacy-squad", "config");
|
|
974
|
+
await mkdir(configDir, { recursive: true });
|
|
975
|
+
const projectConfig = {
|
|
976
|
+
project: {
|
|
977
|
+
name: repoIndex.project.name,
|
|
978
|
+
type: repoIndex.project.type,
|
|
979
|
+
stack: repoIndex.stack.map((s) => s.name)
|
|
980
|
+
},
|
|
981
|
+
scope: {
|
|
982
|
+
mode: "full-project",
|
|
983
|
+
exclude: ["node_modules", "build", "dist", ".git", "vendor"]
|
|
984
|
+
},
|
|
985
|
+
pillars: {
|
|
986
|
+
security: true,
|
|
987
|
+
architecture: true,
|
|
988
|
+
legacy_code: true,
|
|
989
|
+
business_rules: true,
|
|
990
|
+
modernization: true
|
|
991
|
+
},
|
|
992
|
+
mode: { execution: "read_only" },
|
|
993
|
+
ide: { primary: "claude-code" },
|
|
994
|
+
installed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
995
|
+
framework_version: "1.0.0"
|
|
996
|
+
};
|
|
997
|
+
await writeFile(
|
|
998
|
+
path6.join(configDir, "project.yaml"),
|
|
999
|
+
this.toYaml(projectConfig),
|
|
1000
|
+
"utf-8"
|
|
1001
|
+
);
|
|
1002
|
+
await mkdir(path6.join(projectRoot, ".legacy-squad", "outputs", "reports"), { recursive: true });
|
|
1003
|
+
await mkdir(path6.join(projectRoot, ".legacy-squad", "outputs", "assessments"), { recursive: true });
|
|
1004
|
+
await mkdir(path6.join(projectRoot, ".legacy-squad", "logs"), { recursive: true });
|
|
1005
|
+
const claudeCommandsDir = path6.join(projectRoot, ".claude", "commands", "legacy-squad");
|
|
1006
|
+
await mkdir(claudeCommandsDir, { recursive: true });
|
|
1007
|
+
await this.copySlashCommands(templateDir, claudeCommandsDir);
|
|
1008
|
+
await this.generateAgentsMd(projectRoot, repoIndex.project.name);
|
|
1009
|
+
const logContent = [
|
|
1010
|
+
`Legacy Squad Framework \u2014 Install Log`,
|
|
1011
|
+
`Date: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1012
|
+
`Project: ${repoIndex.project.name}`,
|
|
1013
|
+
`Type: ${repoIndex.project.type}`,
|
|
1014
|
+
`Stack: ${repoIndex.stack.map((s) => s.name).join(", ")}`,
|
|
1015
|
+
`Modules: ${repoIndex.modules.length}`,
|
|
1016
|
+
`Dependencies: ${repoIndex.dependencies.length}`,
|
|
1017
|
+
`Findings: ${findings.length}`,
|
|
1018
|
+
`Context Packs: ${contextPacks.length}`,
|
|
1019
|
+
`Agents installed: ${ALL_AGENTS.length}`
|
|
1020
|
+
].join("\n");
|
|
1021
|
+
await writeFile(
|
|
1022
|
+
path6.join(projectRoot, ".legacy-squad", "logs", "install.log"),
|
|
1023
|
+
logContent,
|
|
1024
|
+
"utf-8"
|
|
1025
|
+
);
|
|
1026
|
+
return {
|
|
1027
|
+
repoIndexPath,
|
|
1028
|
+
findingsPath,
|
|
1029
|
+
contextPacksPath,
|
|
1030
|
+
agentCount: ALL_AGENTS.length,
|
|
1031
|
+
findingCount: findings.length,
|
|
1032
|
+
stackNames: repoIndex.stack.map((s) => s.name),
|
|
1033
|
+
moduleCount: repoIndex.modules.length,
|
|
1034
|
+
dependencyCount: repoIndex.dependencies.length
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
async copySlashCommands(templateDir, targetDir) {
|
|
1038
|
+
const commandFiles = [
|
|
1039
|
+
"security.md",
|
|
1040
|
+
"architecture.md",
|
|
1041
|
+
"legacy-code.md",
|
|
1042
|
+
"business-rules.md",
|
|
1043
|
+
"modernization.md",
|
|
1044
|
+
"generate-prs.md",
|
|
1045
|
+
"scan.md"
|
|
1046
|
+
];
|
|
1047
|
+
for (const file of commandFiles) {
|
|
1048
|
+
const sourcePath = path6.join(templateDir, file);
|
|
1049
|
+
const targetPath = path6.join(targetDir, file);
|
|
1050
|
+
try {
|
|
1051
|
+
const content = await readFile2(sourcePath, "utf-8");
|
|
1052
|
+
await writeFile(targetPath, content, "utf-8");
|
|
1053
|
+
} catch {
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
async generateAgentsMd(projectRoot, projectName) {
|
|
1058
|
+
const lines = [
|
|
1059
|
+
`# Legacy Squad Agents \u2014 ${projectName}`,
|
|
1060
|
+
"",
|
|
1061
|
+
"Este arquivo define os agentes do Legacy Squad Framework para Codex CLI.",
|
|
1062
|
+
"Ative um agente com: `@legacy-squad-{nome}`",
|
|
1063
|
+
""
|
|
1064
|
+
];
|
|
1065
|
+
for (const agent of ALL_AGENTS) {
|
|
1066
|
+
lines.push(`## ${agent.name} (@legacy-squad-${agent.id.replace("-agent", "")})`);
|
|
1067
|
+
lines.push("");
|
|
1068
|
+
lines.push(`**Role:** ${agent.role}`);
|
|
1069
|
+
lines.push(`**Pillar:** ${agent.pillar}`);
|
|
1070
|
+
lines.push(`**Frameworks:** ${agent.frameworks.join(", ")}`);
|
|
1071
|
+
lines.push("");
|
|
1072
|
+
lines.push("Leia `.legacy-squad/memory/` para contexto e produza o assessment em `.legacy-squad/outputs/assessments/`.");
|
|
1073
|
+
lines.push("");
|
|
1074
|
+
}
|
|
1075
|
+
await writeFile(path6.join(projectRoot, "AGENTS.md"), lines.join("\n"), "utf-8");
|
|
1076
|
+
}
|
|
1077
|
+
toYaml(obj, indent = 0) {
|
|
1078
|
+
const lines = [];
|
|
1079
|
+
const prefix = " ".repeat(indent);
|
|
1080
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1081
|
+
if (value === null || value === void 0) continue;
|
|
1082
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
1083
|
+
lines.push(`${prefix}${key}:`);
|
|
1084
|
+
lines.push(this.toYaml(value, indent + 1));
|
|
1085
|
+
} else if (Array.isArray(value)) {
|
|
1086
|
+
lines.push(`${prefix}${key}:`);
|
|
1087
|
+
for (const item of value) {
|
|
1088
|
+
lines.push(`${prefix} - ${item}`);
|
|
1089
|
+
}
|
|
1090
|
+
} else {
|
|
1091
|
+
lines.push(`${prefix}${key}: ${value}`);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return lines.join("\n");
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
// packages/agents/src/doctor.ts
|
|
1099
|
+
import { stat as stat2 } from "node:fs/promises";
|
|
1100
|
+
import path7 from "node:path";
|
|
1101
|
+
var Doctor = class {
|
|
1102
|
+
async check(projectRoot) {
|
|
1103
|
+
const checks = [];
|
|
1104
|
+
checks.push(await this.checkFile(projectRoot, ".legacy-squad/memory/repo-index.json", "Repo Index"));
|
|
1105
|
+
checks.push(await this.checkFile(projectRoot, ".legacy-squad/memory/findings.json", "Findings"));
|
|
1106
|
+
checks.push(await this.checkFile(projectRoot, ".legacy-squad/memory/context-packs.json", "Context Packs"));
|
|
1107
|
+
checks.push(await this.checkFile(projectRoot, ".legacy-squad/config/project.yaml", "Project Config"));
|
|
1108
|
+
checks.push(await this.checkDir(projectRoot, ".claude/commands/legacy-squad", "Claude Code Agents"));
|
|
1109
|
+
checks.push(await this.checkFile(projectRoot, "AGENTS.md", "Codex AGENTS.md"));
|
|
1110
|
+
checks.push(await this.checkDir(projectRoot, ".legacy-squad/outputs/assessments", "Assessments Dir"));
|
|
1111
|
+
checks.push(await this.checkDir(projectRoot, ".legacy-squad/outputs/reports", "Reports Dir"));
|
|
1112
|
+
return checks;
|
|
1113
|
+
}
|
|
1114
|
+
async checkFile(root, relativePath, label) {
|
|
1115
|
+
try {
|
|
1116
|
+
const fullPath = path7.join(root, relativePath);
|
|
1117
|
+
const s = await stat2(fullPath);
|
|
1118
|
+
if (s.size === 0) {
|
|
1119
|
+
return { name: label, status: "warning", message: `${relativePath} exists but is empty` };
|
|
1120
|
+
}
|
|
1121
|
+
return { name: label, status: "ok", message: `${relativePath} \u2713` };
|
|
1122
|
+
} catch {
|
|
1123
|
+
return { name: label, status: "error", message: `${relativePath} not found` };
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
async checkDir(root, relativePath, label) {
|
|
1127
|
+
try {
|
|
1128
|
+
const fullPath = path7.join(root, relativePath);
|
|
1129
|
+
const s = await stat2(fullPath);
|
|
1130
|
+
if (!s.isDirectory()) {
|
|
1131
|
+
return { name: label, status: "error", message: `${relativePath} is not a directory` };
|
|
1132
|
+
}
|
|
1133
|
+
return { name: label, status: "ok", message: `${relativePath} \u2713` };
|
|
1134
|
+
} catch {
|
|
1135
|
+
return { name: label, status: "error", message: `${relativePath} not found` };
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
// apps/cli/src/index.ts
|
|
1141
|
+
var program = new Command();
|
|
1142
|
+
function getTemplateDir() {
|
|
1143
|
+
const cliDir = path8.dirname(fileURLToPath(import.meta.url));
|
|
1144
|
+
const bundledPath = path8.resolve(cliDir, "templates", "claude-commands");
|
|
1145
|
+
if (existsSync(bundledPath)) return bundledPath;
|
|
1146
|
+
const devPath = path8.resolve(cliDir, "..", "..", "..", "templates", "claude-commands");
|
|
1147
|
+
if (existsSync(devPath)) return devPath;
|
|
1148
|
+
throw new Error("Templates not found. Run from the framework root or use the published package.");
|
|
1149
|
+
}
|
|
1150
|
+
program.name("legacy-squad").description("AI-Powered Legacy Modernization Platform \u2014 Understand. Plan. Modernize.").version("1.0.0");
|
|
1151
|
+
program.command("install").description("Install Legacy Squad Framework inside the current project").option("-p, --path <dir>", "Project root directory", ".").action(async (opts) => {
|
|
1152
|
+
const projectRoot = path8.resolve(opts.path);
|
|
1153
|
+
const templateDir = getTemplateDir();
|
|
1154
|
+
console.log("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
1155
|
+
console.log(" Legacy Squad Framework V1 \u2014 Install");
|
|
1156
|
+
console.log(" Understand. Plan. Modernize.");
|
|
1157
|
+
console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
|
|
1158
|
+
console.log(`\u{1F4C2} Project: ${projectRoot}
|
|
1159
|
+
`);
|
|
1160
|
+
console.log("\u{1F50D} Step 1/3 \u2014 Scanning & analyzing...");
|
|
1161
|
+
const installer = new Installer();
|
|
1162
|
+
const result = await installer.install(projectRoot, templateDir);
|
|
1163
|
+
console.log(` Stack: ${result.stackNames.join(", ")}`);
|
|
1164
|
+
console.log(` Modules: ${result.moduleCount} | Dependencies: ${result.dependencyCount}`);
|
|
1165
|
+
console.log(` Findings: ${result.findingCount}`);
|
|
1166
|
+
console.log("\n\u{1F916} Step 2/3 \u2014 Installing agents...");
|
|
1167
|
+
console.log(` Claude Code: .claude/commands/legacy-squad/ (${result.agentCount} agents)`);
|
|
1168
|
+
console.log(" Codex CLI: AGENTS.md");
|
|
1169
|
+
console.log("\n\u2705 Step 3/3 \u2014 Verifying installation...");
|
|
1170
|
+
const doctor = new Doctor();
|
|
1171
|
+
const checks = await doctor.check(projectRoot);
|
|
1172
|
+
const errors = checks.filter((c) => c.status === "error");
|
|
1173
|
+
const ok = checks.filter((c) => c.status === "ok");
|
|
1174
|
+
console.log(` ${ok.length}/${checks.length} checks passed`);
|
|
1175
|
+
if (errors.length > 0) {
|
|
1176
|
+
for (const e of errors) {
|
|
1177
|
+
console.log(` \u274C ${e.name}: ${e.message}`);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
console.log("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
|
|
1181
|
+
console.log(" \u2705 Installation complete!");
|
|
1182
|
+
console.log("");
|
|
1183
|
+
console.log(" \u{1F4C2} Data: .legacy-squad/memory/");
|
|
1184
|
+
console.log(" \u{1F916} Agents: .claude/commands/legacy-squad/");
|
|
1185
|
+
console.log("");
|
|
1186
|
+
console.log(" Next steps:");
|
|
1187
|
+
console.log(" 1. Open Claude Code: claude");
|
|
1188
|
+
console.log(" 2. Run: /legacy-squad-security");
|
|
1189
|
+
console.log(" 3. Run: /legacy-squad-architecture");
|
|
1190
|
+
console.log(" 4. Run: /legacy-squad-generate-prs");
|
|
1191
|
+
console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
|
|
1192
|
+
});
|
|
1193
|
+
program.command("scan").description("Re-scan the project and update .legacy-squad/memory/").option("-p, --path <dir>", "Project root directory", ".").action(async (opts) => {
|
|
1194
|
+
const projectRoot = path8.resolve(opts.path);
|
|
1195
|
+
console.log(`
|
|
1196
|
+
\u{1F50D} Re-scanning: ${projectRoot}
|
|
1197
|
+
`);
|
|
1198
|
+
const fs = new NodeFileSystem();
|
|
1199
|
+
const scanner = new RepoScanner(fs);
|
|
1200
|
+
const repoIndex = await scanner.scan(projectRoot);
|
|
1201
|
+
const compliance = new ComplianceEngine(fs);
|
|
1202
|
+
const findings = await compliance.evaluate(projectRoot, repoIndex);
|
|
1203
|
+
const { mkdir: mkdir2, writeFile: writeFile2 } = await import("node:fs/promises");
|
|
1204
|
+
const memoryDir = path8.join(projectRoot, ".legacy-squad", "memory");
|
|
1205
|
+
await mkdir2(memoryDir, { recursive: true });
|
|
1206
|
+
await writeFile2(path8.join(memoryDir, "repo-index.json"), JSON.stringify(repoIndex, null, 2), "utf-8");
|
|
1207
|
+
await writeFile2(path8.join(memoryDir, "findings.json"), JSON.stringify(findings, null, 2), "utf-8");
|
|
1208
|
+
console.log(`\u2705 Stack: ${repoIndex.stack.map((s) => s.name).join(", ")}`);
|
|
1209
|
+
console.log(`\u{1F4E6} Modules: ${repoIndex.modules.length}`);
|
|
1210
|
+
console.log(`\u{1F512} Findings: ${findings.length}`);
|
|
1211
|
+
console.log(`\u{1F4C4} Updated: .legacy-squad/memory/
|
|
1212
|
+
`);
|
|
1213
|
+
});
|
|
1214
|
+
program.command("doctor").description("Verify Legacy Squad installation health").option("-p, --path <dir>", "Project root directory", ".").action(async (opts) => {
|
|
1215
|
+
const projectRoot = path8.resolve(opts.path);
|
|
1216
|
+
console.log("\n\u{1FA7A} Legacy Squad Doctor\n");
|
|
1217
|
+
const doctor = new Doctor();
|
|
1218
|
+
const checks = await doctor.check(projectRoot);
|
|
1219
|
+
for (const check of checks) {
|
|
1220
|
+
const icon = check.status === "ok" ? "\u2705" : check.status === "warning" ? "\u26A0\uFE0F" : "\u274C";
|
|
1221
|
+
console.log(` ${icon} ${check.name}: ${check.message}`);
|
|
1222
|
+
}
|
|
1223
|
+
const errors = checks.filter((c) => c.status === "error");
|
|
1224
|
+
if (errors.length > 0) {
|
|
1225
|
+
console.log(`
|
|
1226
|
+
\u274C ${errors.length} issues found. Run 'npx legacy-squad install' to fix.
|
|
1227
|
+
`);
|
|
1228
|
+
} else {
|
|
1229
|
+
console.log("\n\u2705 All checks passed.\n");
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
program.parse();
|