@kody-ade/kody-engine-lite 0.1.101 → 0.1.102
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bin/cli.js +196 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -154,7 +154,7 @@ kody-engine-lite rerun --issue-number 42 --from verify
|
|
|
154
154
|
|
|
155
155
|
## Documentation
|
|
156
156
|
|
|
157
|
-
**Understand Kody:** [About](docs/ABOUT.md) · [Features](docs/FEATURES.md) · [Pipeline](docs/PIPELINE.md) · [Comparison](docs/COMPARISON.md)
|
|
157
|
+
**Understand Kody:** [About](docs/ABOUT.md) · [Tech Stack](docs/TECH-STACK.md) · [Features](docs/FEATURES.md) · [Pipeline](docs/PIPELINE.md) · [Comparison](docs/COMPARISON.md)
|
|
158
158
|
|
|
159
159
|
**Set up & use:** [CLI](docs/CLI.md) · [Configuration](docs/CONFIGURATION.md) · [Bootstrap](docs/BOOTSTRAP.md) · [LiteLLM](docs/LITELLM.md)
|
|
160
160
|
|
package/dist/bin/cli.js
CHANGED
|
@@ -1546,6 +1546,11 @@ ${prompt}` : prompt;
|
|
|
1546
1546
|
}
|
|
1547
1547
|
if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
|
|
1548
1548
|
assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
|
|
1549
|
+
const qaGuidePath = path5.join(projectDir, ".kody", "qa-guide.md");
|
|
1550
|
+
if (fs5.existsSync(qaGuidePath)) {
|
|
1551
|
+
const qaGuide = fs5.readFileSync(qaGuidePath, "utf-8").trim();
|
|
1552
|
+
assembled = assembled + "\n\n" + qaGuide;
|
|
1553
|
+
}
|
|
1549
1554
|
}
|
|
1550
1555
|
return assembled;
|
|
1551
1556
|
}
|
|
@@ -4532,12 +4537,7 @@ function detectMcpConfig(cwd, pm, pkg) {
|
|
|
4532
4537
|
const defaultPort = isNext ? 3e3 : isVite ? 5173 : 3e3;
|
|
4533
4538
|
const mcp = {
|
|
4534
4539
|
enabled: true,
|
|
4535
|
-
servers: {
|
|
4536
|
-
playwright: {
|
|
4537
|
-
command: "npx",
|
|
4538
|
-
args: ["@playwright/mcp@latest"]
|
|
4539
|
-
}
|
|
4540
|
-
},
|
|
4540
|
+
servers: {},
|
|
4541
4541
|
stages: ["build", "review"]
|
|
4542
4542
|
};
|
|
4543
4543
|
if (hasDevScript) {
|
|
@@ -5034,6 +5034,23 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5034
5034
|
}
|
|
5035
5035
|
}
|
|
5036
5036
|
console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
|
|
5037
|
+
console.log("\n\u2500\u2500 QA Guide \u2500\u2500");
|
|
5038
|
+
const qaGuidePath = path21.join(cwd, ".kody", "qa-guide.md");
|
|
5039
|
+
if (!fs22.existsSync(qaGuidePath) || opts.force) {
|
|
5040
|
+
const discovery = discoverQaContext(cwd);
|
|
5041
|
+
if (discovery.routes.length > 0) {
|
|
5042
|
+
const qaGuide = generateQaGuide(discovery);
|
|
5043
|
+
fs22.writeFileSync(qaGuidePath, qaGuide);
|
|
5044
|
+
console.log(` \u2713 .kody/qa-guide.md (${discovery.routes.length} routes, ${discovery.roles.length} roles)`);
|
|
5045
|
+
if (discovery.loginPage) console.log(` \u2713 Login page detected: ${discovery.loginPage}`);
|
|
5046
|
+
if (discovery.adminPath) console.log(` \u2713 Admin panel detected: ${discovery.adminPath}`);
|
|
5047
|
+
console.log(" \u2139 Add QA_ADMIN_EMAIL, QA_ADMIN_PASSWORD, QA_USER_EMAIL, QA_USER_PASSWORD as GitHub secrets");
|
|
5048
|
+
} else {
|
|
5049
|
+
console.log(" \u25CB No routes detected \u2014 skipping QA guide");
|
|
5050
|
+
}
|
|
5051
|
+
} else {
|
|
5052
|
+
console.log(" \u25CB .kody/qa-guide.md already exists (use --force to regenerate)");
|
|
5053
|
+
}
|
|
5037
5054
|
console.log("\n\u2500\u2500 Labels \u2500\u2500");
|
|
5038
5055
|
try {
|
|
5039
5056
|
let repoSlug = "";
|
|
@@ -5111,6 +5128,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5111
5128
|
const filesToCommit = [
|
|
5112
5129
|
".kody/memory/architecture.md",
|
|
5113
5130
|
".kody/memory/conventions.md",
|
|
5131
|
+
".kody/qa-guide.md",
|
|
5114
5132
|
...installedSkillPaths
|
|
5115
5133
|
].filter((f) => fs22.existsSync(path21.join(cwd, f)));
|
|
5116
5134
|
if (fs22.existsSync(path21.join(cwd, "skills-lock.json"))) {
|
|
@@ -5224,6 +5242,178 @@ Create it manually.`, cwd);
|
|
|
5224
5242
|
console.log(" \u2713 Project bootstrap complete!");
|
|
5225
5243
|
console.log(" Kody now has project-specific memory and customized step files.\n");
|
|
5226
5244
|
}
|
|
5245
|
+
function discoverQaContext(cwd) {
|
|
5246
|
+
const result = {
|
|
5247
|
+
routes: [],
|
|
5248
|
+
authFiles: [],
|
|
5249
|
+
loginPage: null,
|
|
5250
|
+
adminPath: null,
|
|
5251
|
+
roles: [],
|
|
5252
|
+
devCommand: "",
|
|
5253
|
+
devPort: 3e3
|
|
5254
|
+
};
|
|
5255
|
+
try {
|
|
5256
|
+
const pkg = JSON.parse(fs22.readFileSync(path21.join(cwd, "package.json"), "utf-8"));
|
|
5257
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
5258
|
+
const pm = fs22.existsSync(path21.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs22.existsSync(path21.join(cwd, "yarn.lock")) ? "yarn" : "npm";
|
|
5259
|
+
if (pkg.scripts?.dev) result.devCommand = `${pm} dev`;
|
|
5260
|
+
if (allDeps.next || allDeps.nuxt) result.devPort = 3e3;
|
|
5261
|
+
else if (allDeps.vite) result.devPort = 5173;
|
|
5262
|
+
} catch {
|
|
5263
|
+
}
|
|
5264
|
+
const appDirs = ["src/app", "app"];
|
|
5265
|
+
for (const appDir of appDirs) {
|
|
5266
|
+
const fullAppDir = path21.join(cwd, appDir);
|
|
5267
|
+
if (!fs22.existsSync(fullAppDir)) continue;
|
|
5268
|
+
scanRoutes(fullAppDir, appDir, "", result);
|
|
5269
|
+
break;
|
|
5270
|
+
}
|
|
5271
|
+
const authPatterns = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
|
|
5272
|
+
for (const p of authPatterns) {
|
|
5273
|
+
if (fs22.existsSync(path21.join(cwd, p))) result.authFiles.push(p);
|
|
5274
|
+
}
|
|
5275
|
+
const authConfigGlobs = [
|
|
5276
|
+
"src/app/api/auth",
|
|
5277
|
+
"src/auth",
|
|
5278
|
+
"src/lib/auth",
|
|
5279
|
+
"auth.config.ts",
|
|
5280
|
+
"auth.ts",
|
|
5281
|
+
"src/app/api/oauth"
|
|
5282
|
+
];
|
|
5283
|
+
for (const g of authConfigGlobs) {
|
|
5284
|
+
if (fs22.existsSync(path21.join(cwd, g))) result.authFiles.push(g);
|
|
5285
|
+
}
|
|
5286
|
+
try {
|
|
5287
|
+
const rolePaths = [
|
|
5288
|
+
"src/types",
|
|
5289
|
+
"src/lib",
|
|
5290
|
+
"src/utils",
|
|
5291
|
+
"src/constants",
|
|
5292
|
+
"src/access",
|
|
5293
|
+
"src/collections"
|
|
5294
|
+
];
|
|
5295
|
+
for (const rp of rolePaths) {
|
|
5296
|
+
const dir = path21.join(cwd, rp);
|
|
5297
|
+
if (!fs22.existsSync(dir)) continue;
|
|
5298
|
+
const files = fs22.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
|
|
5299
|
+
for (const f of files) {
|
|
5300
|
+
try {
|
|
5301
|
+
const content = fs22.readFileSync(path21.join(dir, f), "utf-8").slice(0, 5e3);
|
|
5302
|
+
const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
|
|
5303
|
+
if (roleMatches) {
|
|
5304
|
+
for (const m of roleMatches) {
|
|
5305
|
+
const val = m.match(/['"](\w+)['"]/);
|
|
5306
|
+
if (val && !result.roles.includes(val[1])) result.roles.push(val[1]);
|
|
5307
|
+
}
|
|
5308
|
+
}
|
|
5309
|
+
const enumMatch = content.match(/(?:enum|type)\s+\w*[Rr]ole\w*\s*[={]([^}]+)/s);
|
|
5310
|
+
if (enumMatch) {
|
|
5311
|
+
const vals = enumMatch[1].match(/['"](\w+)['"]/g);
|
|
5312
|
+
if (vals) {
|
|
5313
|
+
for (const v of vals) {
|
|
5314
|
+
const clean = v.replace(/['"]/g, "");
|
|
5315
|
+
if (!result.roles.includes(clean)) result.roles.push(clean);
|
|
5316
|
+
}
|
|
5317
|
+
}
|
|
5318
|
+
}
|
|
5319
|
+
} catch {
|
|
5320
|
+
}
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
} catch {
|
|
5324
|
+
}
|
|
5325
|
+
return result;
|
|
5326
|
+
}
|
|
5327
|
+
function scanRoutes(dir, baseDir, prefix, result) {
|
|
5328
|
+
let entries;
|
|
5329
|
+
try {
|
|
5330
|
+
entries = fs22.readdirSync(dir, { withFileTypes: true });
|
|
5331
|
+
} catch {
|
|
5332
|
+
return;
|
|
5333
|
+
}
|
|
5334
|
+
const hasPage = entries.some((e) => e.isFile() && /^page\.(tsx?|jsx?)$/.test(e.name));
|
|
5335
|
+
if (hasPage) {
|
|
5336
|
+
const routePath = prefix || "/";
|
|
5337
|
+
const group = prefix.startsWith("/admin") ? "admin" : prefix.includes("/login") ? "auth" : prefix.includes("/signup") ? "auth" : prefix.includes("/api") ? "api" : "frontend";
|
|
5338
|
+
result.routes.push({ path: routePath, group });
|
|
5339
|
+
if (prefix.includes("/login")) result.loginPage = routePath;
|
|
5340
|
+
if (prefix.startsWith("/admin") && !result.adminPath) result.adminPath = prefix;
|
|
5341
|
+
}
|
|
5342
|
+
for (const entry of entries) {
|
|
5343
|
+
if (!entry.isDirectory()) continue;
|
|
5344
|
+
if (entry.name === "node_modules" || entry.name === ".next") continue;
|
|
5345
|
+
let segment = entry.name;
|
|
5346
|
+
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
5347
|
+
scanRoutes(path21.join(dir, entry.name), baseDir, prefix, result);
|
|
5348
|
+
continue;
|
|
5349
|
+
}
|
|
5350
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
5351
|
+
segment = `:${segment.slice(1, -1)}`;
|
|
5352
|
+
}
|
|
5353
|
+
if (segment.startsWith("[[") && segment.endsWith("]]")) {
|
|
5354
|
+
segment = `:${segment.slice(2, -2)}?`;
|
|
5355
|
+
}
|
|
5356
|
+
scanRoutes(path21.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result);
|
|
5357
|
+
}
|
|
5358
|
+
}
|
|
5359
|
+
function generateQaGuide(discovery) {
|
|
5360
|
+
const lines = ["# QA Guide", "", "## Authentication", ""];
|
|
5361
|
+
if (discovery.loginPage) {
|
|
5362
|
+
lines.push(`- Login page: \`${discovery.loginPage}\``);
|
|
5363
|
+
}
|
|
5364
|
+
lines.push(
|
|
5365
|
+
"",
|
|
5366
|
+
"### Test Accounts",
|
|
5367
|
+
"<!-- Fill in your test/preview environment credentials below -->",
|
|
5368
|
+
"| Role | Email | Password |",
|
|
5369
|
+
"|------|-------|----------|",
|
|
5370
|
+
"| Admin | admin@example.com | CHANGE_ME |",
|
|
5371
|
+
"| User | user@example.com | CHANGE_ME |",
|
|
5372
|
+
"",
|
|
5373
|
+
"### Login Steps",
|
|
5374
|
+
`1. Navigate to \`${discovery.loginPage ?? "/login"}\``,
|
|
5375
|
+
"2. Enter credentials from the test accounts table above",
|
|
5376
|
+
"3. Submit the login form",
|
|
5377
|
+
"4. Verify redirect to dashboard or home page"
|
|
5378
|
+
);
|
|
5379
|
+
if (discovery.authFiles.length > 0) {
|
|
5380
|
+
lines.push("", "### Auth Files");
|
|
5381
|
+
for (const f of discovery.authFiles) {
|
|
5382
|
+
lines.push(`- \`${f}\``);
|
|
5383
|
+
}
|
|
5384
|
+
}
|
|
5385
|
+
if (discovery.roles.length > 0) {
|
|
5386
|
+
lines.push("", "## Roles", "");
|
|
5387
|
+
for (const role of discovery.roles) {
|
|
5388
|
+
lines.push(`- \`${role}\``);
|
|
5389
|
+
}
|
|
5390
|
+
}
|
|
5391
|
+
lines.push("", "## Key Pages", "");
|
|
5392
|
+
const groups = {};
|
|
5393
|
+
for (const route of discovery.routes) {
|
|
5394
|
+
if (!groups[route.group]) groups[route.group] = [];
|
|
5395
|
+
groups[route.group].push(route.path);
|
|
5396
|
+
}
|
|
5397
|
+
for (const [group, routes] of Object.entries(groups)) {
|
|
5398
|
+
lines.push(`### ${group.charAt(0).toUpperCase() + group.slice(1)}`);
|
|
5399
|
+
const sorted = routes.sort();
|
|
5400
|
+
for (const r of sorted.slice(0, 20)) {
|
|
5401
|
+
lines.push(`- \`${r}\``);
|
|
5402
|
+
}
|
|
5403
|
+
if (sorted.length > 20) {
|
|
5404
|
+
lines.push(`- ... and ${sorted.length - 20} more`);
|
|
5405
|
+
}
|
|
5406
|
+
lines.push("");
|
|
5407
|
+
}
|
|
5408
|
+
lines.push(
|
|
5409
|
+
"## Dev Server",
|
|
5410
|
+
"",
|
|
5411
|
+
`- Command: \`${discovery.devCommand || "pnpm dev"}\``,
|
|
5412
|
+
`- URL: \`http://localhost:${discovery.devPort}\``,
|
|
5413
|
+
""
|
|
5414
|
+
);
|
|
5415
|
+
return lines.join("\n");
|
|
5416
|
+
}
|
|
5227
5417
|
function detectArchitectureBasic(cwd) {
|
|
5228
5418
|
const detected = [];
|
|
5229
5419
|
const pkgPath = path21.join(cwd, "package.json");
|