@kody-ade/kody-engine-lite 0.1.43 → 0.1.45
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/dist/bin/cli.js +409 -138
- package/package.json +1 -1
package/dist/bin/cli.js
CHANGED
|
@@ -515,6 +515,32 @@ function postComment(issueNumber, body) {
|
|
|
515
515
|
logger.warn(` Failed to post comment: ${err}`);
|
|
516
516
|
}
|
|
517
517
|
}
|
|
518
|
+
function getPRForBranch(branch) {
|
|
519
|
+
try {
|
|
520
|
+
const output = gh([
|
|
521
|
+
"pr",
|
|
522
|
+
"view",
|
|
523
|
+
branch,
|
|
524
|
+
"--json",
|
|
525
|
+
"number,url"
|
|
526
|
+
]);
|
|
527
|
+
const data = JSON.parse(output);
|
|
528
|
+
return { number: data.number, url: data.url };
|
|
529
|
+
} catch {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function updatePR(prNumber, body) {
|
|
534
|
+
try {
|
|
535
|
+
gh(
|
|
536
|
+
["pr", "edit", String(prNumber), "--body-file", "-"],
|
|
537
|
+
{ input: body }
|
|
538
|
+
);
|
|
539
|
+
logger.info(` PR #${prNumber} body updated`);
|
|
540
|
+
} catch (err) {
|
|
541
|
+
logger.warn(` Failed to update PR #${prNumber}: ${err}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
518
544
|
function createPR(head, base, title, body) {
|
|
519
545
|
try {
|
|
520
546
|
const output = gh(
|
|
@@ -556,6 +582,75 @@ function setLifecycleLabel(issueNumber, phase) {
|
|
|
556
582
|
}
|
|
557
583
|
setLabel(issueNumber, `kody:${phase}`);
|
|
558
584
|
}
|
|
585
|
+
function getPRsForIssue(issueNumber) {
|
|
586
|
+
try {
|
|
587
|
+
const output = gh([
|
|
588
|
+
"pr",
|
|
589
|
+
"list",
|
|
590
|
+
"--search",
|
|
591
|
+
`${issueNumber} in:body`,
|
|
592
|
+
"--json",
|
|
593
|
+
"number,title,url,headRefName",
|
|
594
|
+
"--state",
|
|
595
|
+
"open"
|
|
596
|
+
]);
|
|
597
|
+
const prs = JSON.parse(output);
|
|
598
|
+
const branchPrs = (() => {
|
|
599
|
+
try {
|
|
600
|
+
const branchOutput = gh([
|
|
601
|
+
"pr",
|
|
602
|
+
"list",
|
|
603
|
+
"--json",
|
|
604
|
+
"number,title,url,headRefName",
|
|
605
|
+
"--state",
|
|
606
|
+
"open"
|
|
607
|
+
]);
|
|
608
|
+
return JSON.parse(branchOutput).filter((pr) => pr.headRefName.startsWith(`${issueNumber}-`));
|
|
609
|
+
} catch {
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
})();
|
|
613
|
+
const seen = /* @__PURE__ */ new Set();
|
|
614
|
+
const merged = [];
|
|
615
|
+
for (const pr of [...prs, ...branchPrs]) {
|
|
616
|
+
if (!seen.has(pr.number)) {
|
|
617
|
+
seen.add(pr.number);
|
|
618
|
+
merged.push({ number: pr.number, title: pr.title, url: pr.url, headBranch: pr.headRefName });
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return merged;
|
|
622
|
+
} catch (err) {
|
|
623
|
+
logger.error(` Failed to get PRs for issue #${issueNumber}: ${err}`);
|
|
624
|
+
return [];
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function getPRDetails(prNumber) {
|
|
628
|
+
try {
|
|
629
|
+
const output = gh([
|
|
630
|
+
"pr",
|
|
631
|
+
"view",
|
|
632
|
+
String(prNumber),
|
|
633
|
+
"--json",
|
|
634
|
+
"title,body,headRefName,baseRefName"
|
|
635
|
+
]);
|
|
636
|
+
const data = JSON.parse(output);
|
|
637
|
+
return { title: data.title, body: data.body, headBranch: data.headRefName, baseBranch: data.baseRefName };
|
|
638
|
+
} catch (err) {
|
|
639
|
+
logger.error(` Failed to get PR #${prNumber}: ${err}`);
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function postPRComment(prNumber, body) {
|
|
644
|
+
try {
|
|
645
|
+
gh(
|
|
646
|
+
["pr", "comment", String(prNumber), "--body-file", "-"],
|
|
647
|
+
{ input: body }
|
|
648
|
+
);
|
|
649
|
+
logger.info(` Comment posted on PR #${prNumber}`);
|
|
650
|
+
} catch (err) {
|
|
651
|
+
logger.warn(` Failed to post PR comment: ${err}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
559
654
|
var API_TIMEOUT_MS, LIFECYCLE_LABELS, _ghCwd;
|
|
560
655
|
var init_github_api = __esm({
|
|
561
656
|
"src/github-api.ts"() {
|
|
@@ -1520,21 +1615,37 @@ function executeShipStage(ctx, _def) {
|
|
|
1520
1615
|
}
|
|
1521
1616
|
}
|
|
1522
1617
|
const body = buildPrBody(ctx);
|
|
1523
|
-
const
|
|
1524
|
-
if (
|
|
1618
|
+
const existingPr = getPRForBranch(head);
|
|
1619
|
+
if (existingPr) {
|
|
1620
|
+
updatePR(existingPr.number, body);
|
|
1525
1621
|
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1526
1622
|
try {
|
|
1527
|
-
postComment(ctx.input.issueNumber, `\
|
|
1623
|
+
postComment(ctx.input.issueNumber, `\u2705 Fix pushed to existing PR: ${existingPr.url}`);
|
|
1528
1624
|
} catch {
|
|
1529
1625
|
}
|
|
1530
1626
|
}
|
|
1531
1627
|
fs9.writeFileSync(shipPath, `# Ship
|
|
1532
1628
|
|
|
1629
|
+
Updated existing PR: ${existingPr.url}
|
|
1630
|
+
PR #${existingPr.number}
|
|
1631
|
+
`);
|
|
1632
|
+
} else {
|
|
1633
|
+
const pr = createPR(head, base, title, body);
|
|
1634
|
+
if (pr) {
|
|
1635
|
+
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
1636
|
+
try {
|
|
1637
|
+
postComment(ctx.input.issueNumber, `\u{1F389} PR created: ${pr.url}`);
|
|
1638
|
+
} catch {
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
fs9.writeFileSync(shipPath, `# Ship
|
|
1642
|
+
|
|
1533
1643
|
PR created: ${pr.url}
|
|
1534
1644
|
PR #${pr.number}
|
|
1535
1645
|
`);
|
|
1536
|
-
|
|
1537
|
-
|
|
1646
|
+
} else {
|
|
1647
|
+
fs9.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
1648
|
+
}
|
|
1538
1649
|
}
|
|
1539
1650
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
1540
1651
|
} catch (err) {
|
|
@@ -2371,6 +2482,128 @@ var init_preflight = __esm({
|
|
|
2371
2482
|
}
|
|
2372
2483
|
});
|
|
2373
2484
|
|
|
2485
|
+
// src/cli/task-resolution.ts
|
|
2486
|
+
import * as fs16 from "fs";
|
|
2487
|
+
import * as path15 from "path";
|
|
2488
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
2489
|
+
function findLatestTaskForIssue(issueNumber, projectDir) {
|
|
2490
|
+
const tasksDir = path15.join(projectDir, ".tasks");
|
|
2491
|
+
if (!fs16.existsSync(tasksDir)) return null;
|
|
2492
|
+
const allDirs = fs16.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
|
|
2493
|
+
const prefix = `${issueNumber}-`;
|
|
2494
|
+
const direct = allDirs.find((d) => d.startsWith(prefix));
|
|
2495
|
+
if (direct) return direct;
|
|
2496
|
+
try {
|
|
2497
|
+
const branch = execFileSync9("git", ["branch", "--show-current"], {
|
|
2498
|
+
encoding: "utf-8",
|
|
2499
|
+
cwd: projectDir,
|
|
2500
|
+
timeout: 5e3,
|
|
2501
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2502
|
+
}).trim();
|
|
2503
|
+
const branchIssueMatch = branch.match(/^(\d+)-/);
|
|
2504
|
+
if (branchIssueMatch) {
|
|
2505
|
+
const branchIssueNum = branchIssueMatch[1];
|
|
2506
|
+
const branchPrefix = `${branchIssueNum}-`;
|
|
2507
|
+
const fromBranch = allDirs.find((d) => d.startsWith(branchPrefix));
|
|
2508
|
+
if (fromBranch) return fromBranch;
|
|
2509
|
+
}
|
|
2510
|
+
} catch {
|
|
2511
|
+
}
|
|
2512
|
+
return null;
|
|
2513
|
+
}
|
|
2514
|
+
function generateTaskId() {
|
|
2515
|
+
const now = /* @__PURE__ */ new Date();
|
|
2516
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
2517
|
+
return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
2518
|
+
}
|
|
2519
|
+
var init_task_resolution = __esm({
|
|
2520
|
+
"src/cli/task-resolution.ts"() {
|
|
2521
|
+
"use strict";
|
|
2522
|
+
}
|
|
2523
|
+
});
|
|
2524
|
+
|
|
2525
|
+
// src/review-standalone.ts
|
|
2526
|
+
import * as fs17 from "fs";
|
|
2527
|
+
import * as path16 from "path";
|
|
2528
|
+
function resolveReviewTarget(input) {
|
|
2529
|
+
if (input.prs.length === 0) {
|
|
2530
|
+
return {
|
|
2531
|
+
action: "none",
|
|
2532
|
+
message: `Issue #${input.issueNumber} has no open PRs. Nothing to review.`
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
if (input.prs.length === 1) {
|
|
2536
|
+
return { action: "review", prNumber: input.prs[0].number };
|
|
2537
|
+
}
|
|
2538
|
+
const prList = input.prs.map((pr) => ` - #${pr.number}: ${pr.title}`).join("\n");
|
|
2539
|
+
return {
|
|
2540
|
+
action: "pick",
|
|
2541
|
+
prs: input.prs,
|
|
2542
|
+
message: `\u26A0\uFE0F Issue #${input.issueNumber} has ${input.prs.length} open PRs:
|
|
2543
|
+
${prList}
|
|
2544
|
+
|
|
2545
|
+
Run: \`pnpm kody review --pr-number <n>\`
|
|
2546
|
+
Or comment on the specific PR: \`@kody review\``
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
async function runStandaloneReview(input) {
|
|
2550
|
+
const taskId = input.taskId ?? `review-${generateTaskId()}`;
|
|
2551
|
+
const taskDir = path16.join(input.projectDir, ".tasks", taskId);
|
|
2552
|
+
fs17.mkdirSync(taskDir, { recursive: true });
|
|
2553
|
+
const taskContent = `# ${input.prTitle}
|
|
2554
|
+
|
|
2555
|
+
${input.prBody ?? ""}`;
|
|
2556
|
+
fs17.writeFileSync(path16.join(taskDir, "task.md"), taskContent);
|
|
2557
|
+
const reviewDef = STAGES.find((s) => s.name === "review");
|
|
2558
|
+
const ctx = {
|
|
2559
|
+
taskId,
|
|
2560
|
+
taskDir,
|
|
2561
|
+
projectDir: input.projectDir,
|
|
2562
|
+
runners: input.runners,
|
|
2563
|
+
sessions: {},
|
|
2564
|
+
input: {
|
|
2565
|
+
mode: "full",
|
|
2566
|
+
local: input.local
|
|
2567
|
+
}
|
|
2568
|
+
};
|
|
2569
|
+
logger.info(`[review] standalone review for: ${input.prTitle}`);
|
|
2570
|
+
const result = await executeAgentStage(ctx, reviewDef);
|
|
2571
|
+
if (result.outcome !== "completed") {
|
|
2572
|
+
return {
|
|
2573
|
+
outcome: "failed",
|
|
2574
|
+
taskDir,
|
|
2575
|
+
error: result.error ?? "Review stage failed"
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
const reviewPath = path16.join(taskDir, "review.md");
|
|
2579
|
+
let reviewContent;
|
|
2580
|
+
if (fs17.existsSync(reviewPath)) {
|
|
2581
|
+
reviewContent = fs17.readFileSync(reviewPath, "utf-8");
|
|
2582
|
+
}
|
|
2583
|
+
return {
|
|
2584
|
+
outcome: "completed",
|
|
2585
|
+
reviewContent,
|
|
2586
|
+
taskDir
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
function formatReviewComment(reviewContent, taskId) {
|
|
2590
|
+
return `## \u{1F50D} Kody Review (\`${taskId}\`)
|
|
2591
|
+
|
|
2592
|
+
${reviewContent}
|
|
2593
|
+
|
|
2594
|
+
---
|
|
2595
|
+
\u{1F916} Generated by Kody`;
|
|
2596
|
+
}
|
|
2597
|
+
var init_review_standalone = __esm({
|
|
2598
|
+
"src/review-standalone.ts"() {
|
|
2599
|
+
"use strict";
|
|
2600
|
+
init_definitions();
|
|
2601
|
+
init_agent();
|
|
2602
|
+
init_task_resolution();
|
|
2603
|
+
init_logger();
|
|
2604
|
+
}
|
|
2605
|
+
});
|
|
2606
|
+
|
|
2374
2607
|
// src/cli/args.ts
|
|
2375
2608
|
function getArg(args2, flag) {
|
|
2376
2609
|
const idx = args2.indexOf(flag);
|
|
@@ -2389,16 +2622,18 @@ function parseArgs() {
|
|
|
2389
2622
|
kody run --task-id <id> [--task "<desc>"] [--cwd <path>] [--issue-number <n>] [--complexity low|medium|high] [--feedback "<text>"] [--local] [--dry-run]
|
|
2390
2623
|
kody rerun --task-id <id> --from <stage> [--cwd <path>] [--issue-number <n>]
|
|
2391
2624
|
kody fix --task-id <id> [--cwd <path>] [--issue-number <n>] [--feedback "<text>"]
|
|
2625
|
+
kody review [--pr-number <n>] [--issue-number <n>] [--cwd <path>] [--local]
|
|
2392
2626
|
kody status --task-id <id> [--cwd <path>]
|
|
2393
2627
|
kody --help`);
|
|
2394
2628
|
process.exit(0);
|
|
2395
2629
|
}
|
|
2396
2630
|
const command2 = args2[0];
|
|
2397
|
-
if (!["run", "rerun", "fix", "status"].includes(command2)) {
|
|
2631
|
+
if (!["run", "rerun", "fix", "status", "review"].includes(command2)) {
|
|
2398
2632
|
console.error(`Unknown command: ${command2}`);
|
|
2399
2633
|
process.exit(1);
|
|
2400
2634
|
}
|
|
2401
2635
|
const issueStr = getArg(args2, "--issue-number") ?? process.env.ISSUE_NUMBER;
|
|
2636
|
+
const prStr = getArg(args2, "--pr-number") ?? process.env.PR_NUMBER;
|
|
2402
2637
|
const localFlag = hasFlag(args2, "--local");
|
|
2403
2638
|
return {
|
|
2404
2639
|
command: command2,
|
|
@@ -2408,6 +2643,7 @@ function parseArgs() {
|
|
|
2408
2643
|
dryRun: hasFlag(args2, "--dry-run") || process.env.DRY_RUN === "true",
|
|
2409
2644
|
cwd: getArg(args2, "--cwd"),
|
|
2410
2645
|
issueNumber: issueStr ? parseInt(issueStr, 10) : void 0,
|
|
2646
|
+
prNumber: prStr ? parseInt(prStr, 10) : void 0,
|
|
2411
2647
|
feedback: getArg(args2, "--feedback") ?? process.env.FEEDBACK,
|
|
2412
2648
|
local: localFlag || !isCI2 && !hasFlag(args2, "--no-local"),
|
|
2413
2649
|
complexity: getArg(args2, "--complexity") ?? process.env.COMPLEXITY
|
|
@@ -2422,9 +2658,9 @@ var init_args = __esm({
|
|
|
2422
2658
|
});
|
|
2423
2659
|
|
|
2424
2660
|
// src/cli/litellm.ts
|
|
2425
|
-
import * as
|
|
2426
|
-
import * as
|
|
2427
|
-
import { execFileSync as
|
|
2661
|
+
import * as fs18 from "fs";
|
|
2662
|
+
import * as path17 from "path";
|
|
2663
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
2428
2664
|
async function checkLitellmHealth(url) {
|
|
2429
2665
|
try {
|
|
2430
2666
|
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
@@ -2434,8 +2670,8 @@ async function checkLitellmHealth(url) {
|
|
|
2434
2670
|
}
|
|
2435
2671
|
}
|
|
2436
2672
|
async function tryStartLitellm(url, projectDir) {
|
|
2437
|
-
const configPath =
|
|
2438
|
-
if (!
|
|
2673
|
+
const configPath = path17.join(projectDir, "litellm-config.yaml");
|
|
2674
|
+
if (!fs18.existsSync(configPath)) {
|
|
2439
2675
|
logger.warn("litellm-config.yaml not found \u2014 cannot start proxy");
|
|
2440
2676
|
return null;
|
|
2441
2677
|
}
|
|
@@ -2443,11 +2679,11 @@ async function tryStartLitellm(url, projectDir) {
|
|
|
2443
2679
|
const port = portMatch ? portMatch[1] : "4000";
|
|
2444
2680
|
let litellmFound = false;
|
|
2445
2681
|
try {
|
|
2446
|
-
|
|
2682
|
+
execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
2447
2683
|
litellmFound = true;
|
|
2448
2684
|
} catch {
|
|
2449
2685
|
try {
|
|
2450
|
-
|
|
2686
|
+
execFileSync10("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
|
|
2451
2687
|
litellmFound = true;
|
|
2452
2688
|
} catch {
|
|
2453
2689
|
}
|
|
@@ -2460,17 +2696,17 @@ async function tryStartLitellm(url, projectDir) {
|
|
|
2460
2696
|
let cmd;
|
|
2461
2697
|
let args2;
|
|
2462
2698
|
try {
|
|
2463
|
-
|
|
2699
|
+
execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
2464
2700
|
cmd = "litellm";
|
|
2465
2701
|
args2 = ["--config", configPath, "--port", port];
|
|
2466
2702
|
} catch {
|
|
2467
2703
|
cmd = "python3";
|
|
2468
2704
|
args2 = ["-m", "litellm", "--config", configPath, "--port", port];
|
|
2469
2705
|
}
|
|
2470
|
-
const dotenvPath =
|
|
2706
|
+
const dotenvPath = path17.join(projectDir, ".env");
|
|
2471
2707
|
const dotenvVars = {};
|
|
2472
|
-
if (
|
|
2473
|
-
for (const line of
|
|
2708
|
+
if (fs18.existsSync(dotenvPath)) {
|
|
2709
|
+
for (const line of fs18.readFileSync(dotenvPath, "utf-8").split("\n")) {
|
|
2474
2710
|
const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
|
|
2475
2711
|
if (match) dotenvVars[match[1]] = match[2];
|
|
2476
2712
|
}
|
|
@@ -2509,49 +2745,9 @@ var init_litellm = __esm({
|
|
|
2509
2745
|
}
|
|
2510
2746
|
});
|
|
2511
2747
|
|
|
2512
|
-
// src/cli/task-resolution.ts
|
|
2513
|
-
import * as fs17 from "fs";
|
|
2514
|
-
import * as path16 from "path";
|
|
2515
|
-
import { execFileSync as execFileSync10 } from "child_process";
|
|
2516
|
-
function findLatestTaskForIssue(issueNumber, projectDir) {
|
|
2517
|
-
const tasksDir = path16.join(projectDir, ".tasks");
|
|
2518
|
-
if (!fs17.existsSync(tasksDir)) return null;
|
|
2519
|
-
const allDirs = fs17.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
|
|
2520
|
-
const prefix = `${issueNumber}-`;
|
|
2521
|
-
const direct = allDirs.find((d) => d.startsWith(prefix));
|
|
2522
|
-
if (direct) return direct;
|
|
2523
|
-
try {
|
|
2524
|
-
const branch = execFileSync10("git", ["branch", "--show-current"], {
|
|
2525
|
-
encoding: "utf-8",
|
|
2526
|
-
cwd: projectDir,
|
|
2527
|
-
timeout: 5e3,
|
|
2528
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
2529
|
-
}).trim();
|
|
2530
|
-
const branchIssueMatch = branch.match(/^(\d+)-/);
|
|
2531
|
-
if (branchIssueMatch) {
|
|
2532
|
-
const branchIssueNum = branchIssueMatch[1];
|
|
2533
|
-
const branchPrefix = `${branchIssueNum}-`;
|
|
2534
|
-
const fromBranch = allDirs.find((d) => d.startsWith(branchPrefix));
|
|
2535
|
-
if (fromBranch) return fromBranch;
|
|
2536
|
-
}
|
|
2537
|
-
} catch {
|
|
2538
|
-
}
|
|
2539
|
-
return null;
|
|
2540
|
-
}
|
|
2541
|
-
function generateTaskId() {
|
|
2542
|
-
const now = /* @__PURE__ */ new Date();
|
|
2543
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
2544
|
-
return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
2545
|
-
}
|
|
2546
|
-
var init_task_resolution = __esm({
|
|
2547
|
-
"src/cli/task-resolution.ts"() {
|
|
2548
|
-
"use strict";
|
|
2549
|
-
}
|
|
2550
|
-
});
|
|
2551
|
-
|
|
2552
2748
|
// src/cli/task-state.ts
|
|
2553
|
-
import * as
|
|
2554
|
-
import * as
|
|
2749
|
+
import * as fs19 from "fs";
|
|
2750
|
+
import * as path18 from "path";
|
|
2555
2751
|
function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
2556
2752
|
if (!existingTaskId || !existingState) {
|
|
2557
2753
|
return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
|
|
@@ -2583,11 +2779,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
|
2583
2779
|
function resolveForIssue(issueNumber, projectDir) {
|
|
2584
2780
|
const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
|
|
2585
2781
|
if (existingTaskId) {
|
|
2586
|
-
const statusPath =
|
|
2782
|
+
const statusPath = path18.join(projectDir, ".tasks", existingTaskId, "status.json");
|
|
2587
2783
|
let existingState = null;
|
|
2588
|
-
if (
|
|
2784
|
+
if (fs19.existsSync(statusPath)) {
|
|
2589
2785
|
try {
|
|
2590
|
-
existingState = JSON.parse(
|
|
2786
|
+
existingState = JSON.parse(fs19.readFileSync(statusPath, "utf-8"));
|
|
2591
2787
|
} catch {
|
|
2592
2788
|
}
|
|
2593
2789
|
}
|
|
@@ -2617,13 +2813,13 @@ var init_task_state = __esm({
|
|
|
2617
2813
|
|
|
2618
2814
|
// src/entry.ts
|
|
2619
2815
|
var entry_exports = {};
|
|
2620
|
-
import * as
|
|
2621
|
-
import * as
|
|
2816
|
+
import * as fs20 from "fs";
|
|
2817
|
+
import * as path19 from "path";
|
|
2622
2818
|
async function main() {
|
|
2623
2819
|
const input = parseArgs();
|
|
2624
|
-
const projectDir = input.cwd ?
|
|
2820
|
+
const projectDir = input.cwd ? path19.resolve(input.cwd) : process.cwd();
|
|
2625
2821
|
if (input.cwd) {
|
|
2626
|
-
if (!
|
|
2822
|
+
if (!fs20.existsSync(projectDir)) {
|
|
2627
2823
|
console.error(`--cwd path does not exist: ${projectDir}`);
|
|
2628
2824
|
process.exit(1);
|
|
2629
2825
|
}
|
|
@@ -2631,7 +2827,7 @@ async function main() {
|
|
|
2631
2827
|
setGhCwd(projectDir);
|
|
2632
2828
|
logger.info(`Working directory: ${projectDir}`);
|
|
2633
2829
|
}
|
|
2634
|
-
if (input.issueNumber) {
|
|
2830
|
+
if (input.issueNumber && input.command !== "review") {
|
|
2635
2831
|
const taskAction = resolveForIssue(input.issueNumber, projectDir);
|
|
2636
2832
|
logger.info(`Task action: ${taskAction.action}`);
|
|
2637
2833
|
if (taskAction.action === "already-completed") {
|
|
@@ -2667,35 +2863,109 @@ async function main() {
|
|
|
2667
2863
|
taskId = `${input.issueNumber}-${generateTaskId()}`;
|
|
2668
2864
|
} else if (input.command === "run" && input.task) {
|
|
2669
2865
|
taskId = generateTaskId();
|
|
2866
|
+
} else if (input.command === "review") {
|
|
2867
|
+
taskId = input.prNumber ? `review-pr-${input.prNumber}-${generateTaskId()}` : `review-${generateTaskId()}`;
|
|
2670
2868
|
} else {
|
|
2671
2869
|
console.error("--task-id is required (or provide --issue-number to auto-generate)");
|
|
2672
2870
|
process.exit(1);
|
|
2673
2871
|
}
|
|
2674
2872
|
}
|
|
2675
|
-
const taskDir =
|
|
2676
|
-
|
|
2873
|
+
const taskDir = path19.join(projectDir, ".tasks", taskId);
|
|
2874
|
+
fs20.mkdirSync(taskDir, { recursive: true });
|
|
2677
2875
|
if (input.command === "status") {
|
|
2678
2876
|
printStatus(taskId, taskDir);
|
|
2679
2877
|
return;
|
|
2680
2878
|
}
|
|
2879
|
+
if (input.command === "review") {
|
|
2880
|
+
runPreflight();
|
|
2881
|
+
let prTitle = "Code review";
|
|
2882
|
+
let prBody = "";
|
|
2883
|
+
let prNumber = input.prNumber;
|
|
2884
|
+
if (!prNumber && input.issueNumber) {
|
|
2885
|
+
const prs = getPRsForIssue(input.issueNumber);
|
|
2886
|
+
const target = resolveReviewTarget({ issueNumber: input.issueNumber, prs });
|
|
2887
|
+
if (target.action === "none" || target.action === "pick") {
|
|
2888
|
+
console.log(target.message);
|
|
2889
|
+
if (!input.local && input.issueNumber) {
|
|
2890
|
+
try {
|
|
2891
|
+
postComment(input.issueNumber, target.message);
|
|
2892
|
+
} catch {
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
process.exit(target.action === "none" ? 1 : 0);
|
|
2896
|
+
}
|
|
2897
|
+
prNumber = target.prNumber;
|
|
2898
|
+
}
|
|
2899
|
+
if (prNumber) {
|
|
2900
|
+
const details = getPRDetails(prNumber);
|
|
2901
|
+
if (details) {
|
|
2902
|
+
prTitle = details.title;
|
|
2903
|
+
prBody = details.body ?? "";
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
const config2 = getProjectConfig();
|
|
2907
|
+
let litellmProcess2 = null;
|
|
2908
|
+
if (config2.agent.litellmUrl) {
|
|
2909
|
+
const proxyRunning = await checkLitellmHealth(config2.agent.litellmUrl);
|
|
2910
|
+
if (!proxyRunning) {
|
|
2911
|
+
litellmProcess2 = await tryStartLitellm(config2.agent.litellmUrl, projectDir);
|
|
2912
|
+
}
|
|
2913
|
+
if (config2.agent.litellmUrl) {
|
|
2914
|
+
process.env.ANTHROPIC_BASE_URL = config2.agent.litellmUrl;
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
const runners2 = createRunners(config2);
|
|
2918
|
+
const defaultRunnerName2 = config2.agent.defaultRunner ?? Object.keys(runners2)[0] ?? "claude";
|
|
2919
|
+
const defaultRunner2 = runners2[defaultRunnerName2];
|
|
2920
|
+
if (!defaultRunner2) {
|
|
2921
|
+
console.error(`Default runner "${defaultRunnerName2}" not configured`);
|
|
2922
|
+
process.exit(1);
|
|
2923
|
+
}
|
|
2924
|
+
const healthy2 = await defaultRunner2.healthCheck();
|
|
2925
|
+
if (!healthy2) {
|
|
2926
|
+
console.error(`Runner "${defaultRunnerName2}" health check failed`);
|
|
2927
|
+
process.exit(1);
|
|
2928
|
+
}
|
|
2929
|
+
const result = await runStandaloneReview({
|
|
2930
|
+
projectDir,
|
|
2931
|
+
runners: runners2,
|
|
2932
|
+
prTitle,
|
|
2933
|
+
prBody,
|
|
2934
|
+
local: input.local ?? true,
|
|
2935
|
+
taskId
|
|
2936
|
+
});
|
|
2937
|
+
if (litellmProcess2) litellmProcess2.kill();
|
|
2938
|
+
if (result.outcome === "failed") {
|
|
2939
|
+
console.error(`Review failed: ${result.error}`);
|
|
2940
|
+
process.exit(1);
|
|
2941
|
+
}
|
|
2942
|
+
if (result.reviewContent) {
|
|
2943
|
+
console.log(result.reviewContent);
|
|
2944
|
+
if (!input.local && prNumber) {
|
|
2945
|
+
const comment = formatReviewComment(result.reviewContent, taskId);
|
|
2946
|
+
postPRComment(prNumber, comment);
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
process.exit(0);
|
|
2950
|
+
}
|
|
2681
2951
|
logger.info("Preflight checks:");
|
|
2682
2952
|
runPreflight();
|
|
2683
2953
|
if (input.task) {
|
|
2684
|
-
|
|
2954
|
+
fs20.writeFileSync(path19.join(taskDir, "task.md"), input.task);
|
|
2685
2955
|
}
|
|
2686
|
-
const taskMdPath =
|
|
2687
|
-
if (!
|
|
2956
|
+
const taskMdPath = path19.join(taskDir, "task.md");
|
|
2957
|
+
if (!fs20.existsSync(taskMdPath) && input.issueNumber) {
|
|
2688
2958
|
logger.info(`Fetching issue #${input.issueNumber} body as task...`);
|
|
2689
2959
|
const issue = getIssue(input.issueNumber);
|
|
2690
2960
|
if (issue) {
|
|
2691
2961
|
const taskContent = `# ${issue.title}
|
|
2692
2962
|
|
|
2693
2963
|
${issue.body ?? ""}`;
|
|
2694
|
-
|
|
2964
|
+
fs20.writeFileSync(taskMdPath, taskContent);
|
|
2695
2965
|
logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
|
|
2696
2966
|
}
|
|
2697
2967
|
}
|
|
2698
|
-
if (!
|
|
2968
|
+
if (!fs20.existsSync(taskMdPath)) {
|
|
2699
2969
|
console.error("No task.md found. Provide --task, --issue-number, or ensure .tasks/<id>/task.md exists.");
|
|
2700
2970
|
process.exit(1);
|
|
2701
2971
|
}
|
|
@@ -2779,7 +3049,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
|
|
|
2779
3049
|
}
|
|
2780
3050
|
}
|
|
2781
3051
|
const state = await runPipeline(ctx);
|
|
2782
|
-
const files =
|
|
3052
|
+
const files = fs20.readdirSync(taskDir);
|
|
2783
3053
|
console.log(`
|
|
2784
3054
|
Artifacts in ${taskDir}:`);
|
|
2785
3055
|
for (const f of files) {
|
|
@@ -2820,6 +3090,7 @@ var init_entry = __esm({
|
|
|
2820
3090
|
init_config();
|
|
2821
3091
|
init_github_api();
|
|
2822
3092
|
init_logger();
|
|
3093
|
+
init_review_standalone();
|
|
2823
3094
|
init_args();
|
|
2824
3095
|
init_litellm();
|
|
2825
3096
|
init_task_resolution();
|
|
@@ -2841,15 +3112,15 @@ var init_entry = __esm({
|
|
|
2841
3112
|
});
|
|
2842
3113
|
|
|
2843
3114
|
// src/bin/cli.ts
|
|
2844
|
-
import * as
|
|
2845
|
-
import * as
|
|
3115
|
+
import * as fs21 from "fs";
|
|
3116
|
+
import * as path20 from "path";
|
|
2846
3117
|
import { execFileSync as execFileSync11 } from "child_process";
|
|
2847
3118
|
import { fileURLToPath } from "url";
|
|
2848
|
-
var __dirname =
|
|
2849
|
-
var PKG_ROOT =
|
|
3119
|
+
var __dirname = path20.dirname(fileURLToPath(import.meta.url));
|
|
3120
|
+
var PKG_ROOT = path20.resolve(__dirname, "..", "..");
|
|
2850
3121
|
function getVersion() {
|
|
2851
|
-
const pkgPath =
|
|
2852
|
-
const pkg = JSON.parse(
|
|
3122
|
+
const pkgPath = path20.join(PKG_ROOT, "package.json");
|
|
3123
|
+
const pkg = JSON.parse(fs21.readFileSync(pkgPath, "utf-8"));
|
|
2853
3124
|
return pkg.version;
|
|
2854
3125
|
}
|
|
2855
3126
|
function checkCommand2(name, args2, fix) {
|
|
@@ -2865,7 +3136,7 @@ function checkCommand2(name, args2, fix) {
|
|
|
2865
3136
|
}
|
|
2866
3137
|
}
|
|
2867
3138
|
function checkFile(filePath, description, fix) {
|
|
2868
|
-
if (
|
|
3139
|
+
if (fs21.existsSync(filePath)) {
|
|
2869
3140
|
return { name: description, ok: true, detail: filePath };
|
|
2870
3141
|
}
|
|
2871
3142
|
return { name: description, ok: false, fix };
|
|
@@ -2937,10 +3208,10 @@ function checkGhSecret(repoSlug, secretName) {
|
|
|
2937
3208
|
}
|
|
2938
3209
|
function detectArchitecture(cwd) {
|
|
2939
3210
|
const detected = [];
|
|
2940
|
-
const pkgPath =
|
|
2941
|
-
if (
|
|
3211
|
+
const pkgPath = path20.join(cwd, "package.json");
|
|
3212
|
+
if (fs21.existsSync(pkgPath)) {
|
|
2942
3213
|
try {
|
|
2943
|
-
const pkg = JSON.parse(
|
|
3214
|
+
const pkg = JSON.parse(fs21.readFileSync(pkgPath, "utf-8"));
|
|
2944
3215
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2945
3216
|
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
2946
3217
|
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
@@ -2963,41 +3234,41 @@ function detectArchitecture(cwd) {
|
|
|
2963
3234
|
if (allDeps.tailwindcss) detected.push(`- CSS: Tailwind CSS ${allDeps.tailwindcss}`);
|
|
2964
3235
|
if (pkg.type === "module") detected.push("- Module system: ESM");
|
|
2965
3236
|
else detected.push("- Module system: CommonJS");
|
|
2966
|
-
if (
|
|
2967
|
-
else if (
|
|
2968
|
-
else if (
|
|
2969
|
-
else if (
|
|
3237
|
+
if (fs21.existsSync(path20.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
3238
|
+
else if (fs21.existsSync(path20.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
3239
|
+
else if (fs21.existsSync(path20.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
|
|
3240
|
+
else if (fs21.existsSync(path20.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
2970
3241
|
} catch {
|
|
2971
3242
|
}
|
|
2972
3243
|
}
|
|
2973
3244
|
try {
|
|
2974
|
-
const entries =
|
|
3245
|
+
const entries = fs21.readdirSync(cwd, { withFileTypes: true });
|
|
2975
3246
|
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
2976
3247
|
if (dirs.length > 0) detected.push(`- Top-level directories: ${dirs.join(", ")}`);
|
|
2977
3248
|
} catch {
|
|
2978
3249
|
}
|
|
2979
|
-
const srcDir =
|
|
2980
|
-
if (
|
|
3250
|
+
const srcDir = path20.join(cwd, "src");
|
|
3251
|
+
if (fs21.existsSync(srcDir)) {
|
|
2981
3252
|
try {
|
|
2982
|
-
const srcEntries =
|
|
3253
|
+
const srcEntries = fs21.readdirSync(srcDir, { withFileTypes: true });
|
|
2983
3254
|
const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
2984
3255
|
if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
|
|
2985
3256
|
} catch {
|
|
2986
3257
|
}
|
|
2987
3258
|
}
|
|
2988
3259
|
const configs = [];
|
|
2989
|
-
if (
|
|
2990
|
-
if (
|
|
2991
|
-
if (
|
|
2992
|
-
if (
|
|
3260
|
+
if (fs21.existsSync(path20.join(cwd, "tsconfig.json"))) configs.push("tsconfig.json");
|
|
3261
|
+
if (fs21.existsSync(path20.join(cwd, "docker-compose.yml")) || fs21.existsSync(path20.join(cwd, "docker-compose.yaml"))) configs.push("docker-compose");
|
|
3262
|
+
if (fs21.existsSync(path20.join(cwd, "Dockerfile"))) configs.push("Dockerfile");
|
|
3263
|
+
if (fs21.existsSync(path20.join(cwd, ".env")) || fs21.existsSync(path20.join(cwd, ".env.local"))) configs.push(".env");
|
|
2993
3264
|
if (configs.length > 0) detected.push(`- Config files: ${configs.join(", ")}`);
|
|
2994
3265
|
return detected;
|
|
2995
3266
|
}
|
|
2996
3267
|
function detectBasicConfig(cwd) {
|
|
2997
3268
|
let pm = "pnpm";
|
|
2998
|
-
if (
|
|
2999
|
-
else if (
|
|
3000
|
-
else if (!
|
|
3269
|
+
if (fs21.existsSync(path20.join(cwd, "yarn.lock"))) pm = "yarn";
|
|
3270
|
+
else if (fs21.existsSync(path20.join(cwd, "bun.lockb"))) pm = "bun";
|
|
3271
|
+
else if (!fs21.existsSync(path20.join(cwd, "pnpm-lock.yaml")) && fs21.existsSync(path20.join(cwd, "package-lock.json"))) pm = "npm";
|
|
3001
3272
|
let defaultBranch = "main";
|
|
3002
3273
|
try {
|
|
3003
3274
|
const ref = execFileSync11("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
@@ -3041,9 +3312,9 @@ function smartInit(cwd) {
|
|
|
3041
3312
|
const basic = detectBasicConfig(cwd);
|
|
3042
3313
|
let context = "";
|
|
3043
3314
|
const readIfExists = (rel, maxChars = 3e3) => {
|
|
3044
|
-
const p =
|
|
3045
|
-
if (
|
|
3046
|
-
const content =
|
|
3315
|
+
const p = path20.join(cwd, rel);
|
|
3316
|
+
if (fs21.existsSync(p)) {
|
|
3317
|
+
const content = fs21.readFileSync(p, "utf-8");
|
|
3047
3318
|
return content.slice(0, maxChars);
|
|
3048
3319
|
}
|
|
3049
3320
|
return null;
|
|
@@ -3069,14 +3340,14 @@ ${claudeMd}
|
|
|
3069
3340
|
|
|
3070
3341
|
`;
|
|
3071
3342
|
try {
|
|
3072
|
-
const topDirs =
|
|
3343
|
+
const topDirs = fs21.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
3073
3344
|
context += `## Top-level directories
|
|
3074
3345
|
${topDirs.join(", ")}
|
|
3075
3346
|
|
|
3076
3347
|
`;
|
|
3077
|
-
const srcDir =
|
|
3078
|
-
if (
|
|
3079
|
-
const srcDirs =
|
|
3348
|
+
const srcDir = path20.join(cwd, "src");
|
|
3349
|
+
if (fs21.existsSync(srcDir)) {
|
|
3350
|
+
const srcDirs = fs21.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3080
3351
|
context += `## src/ subdirectories
|
|
3081
3352
|
${srcDirs.join(", ")}
|
|
3082
3353
|
|
|
@@ -3086,7 +3357,7 @@ ${srcDirs.join(", ")}
|
|
|
3086
3357
|
}
|
|
3087
3358
|
const existingFiles = [];
|
|
3088
3359
|
for (const f of [".env.example", "CLAUDE.md", ".ai-docs", "vitest.config.ts", "vitest.config.mts", "jest.config.ts", "playwright.config.ts", ".eslintrc.js", "eslint.config.mjs", ".prettierrc"]) {
|
|
3089
|
-
if (
|
|
3360
|
+
if (fs21.existsSync(path20.join(cwd, f))) existingFiles.push(f);
|
|
3090
3361
|
}
|
|
3091
3362
|
if (existingFiles.length) context += `## Config files present
|
|
3092
3363
|
${existingFiles.join(", ")}
|
|
@@ -3192,7 +3463,7 @@ ${context}`;
|
|
|
3192
3463
|
function validateQualityCommands(cwd, config, pm) {
|
|
3193
3464
|
let scripts = {};
|
|
3194
3465
|
try {
|
|
3195
|
-
const pkg = JSON.parse(
|
|
3466
|
+
const pkg = JSON.parse(fs21.readFileSync(path20.join(cwd, "package.json"), "utf-8"));
|
|
3196
3467
|
scripts = pkg.scripts ?? {};
|
|
3197
3468
|
} catch {
|
|
3198
3469
|
return;
|
|
@@ -3226,7 +3497,7 @@ function validateQualityCommands(cwd, config, pm) {
|
|
|
3226
3497
|
function buildFallbackConfig(cwd, basic) {
|
|
3227
3498
|
const pkg = (() => {
|
|
3228
3499
|
try {
|
|
3229
|
-
return JSON.parse(
|
|
3500
|
+
return JSON.parse(fs21.readFileSync(path20.join(cwd, "package.json"), "utf-8"));
|
|
3230
3501
|
} catch {
|
|
3231
3502
|
return {};
|
|
3232
3503
|
}
|
|
@@ -3266,34 +3537,34 @@ function initCommand(opts) {
|
|
|
3266
3537
|
console.log(`Project: ${cwd}
|
|
3267
3538
|
`);
|
|
3268
3539
|
console.log("\u2500\u2500 Files \u2500\u2500");
|
|
3269
|
-
const templatesDir =
|
|
3270
|
-
const workflowSrc =
|
|
3271
|
-
const workflowDest =
|
|
3272
|
-
if (!
|
|
3540
|
+
const templatesDir = path20.join(PKG_ROOT, "templates");
|
|
3541
|
+
const workflowSrc = path20.join(templatesDir, "kody.yml");
|
|
3542
|
+
const workflowDest = path20.join(cwd, ".github", "workflows", "kody.yml");
|
|
3543
|
+
if (!fs21.existsSync(workflowSrc)) {
|
|
3273
3544
|
console.error(" \u2717 Template kody.yml not found in package");
|
|
3274
3545
|
process.exit(1);
|
|
3275
3546
|
}
|
|
3276
|
-
if (
|
|
3547
|
+
if (fs21.existsSync(workflowDest) && !opts.force) {
|
|
3277
3548
|
console.log(" \u25CB .github/workflows/kody.yml (exists, use --force to overwrite)");
|
|
3278
3549
|
} else {
|
|
3279
|
-
|
|
3280
|
-
|
|
3550
|
+
fs21.mkdirSync(path20.dirname(workflowDest), { recursive: true });
|
|
3551
|
+
fs21.copyFileSync(workflowSrc, workflowDest);
|
|
3281
3552
|
console.log(" \u2713 .github/workflows/kody.yml");
|
|
3282
3553
|
}
|
|
3283
|
-
const configDest =
|
|
3554
|
+
const configDest = path20.join(cwd, "kody.config.json");
|
|
3284
3555
|
let smartResult = null;
|
|
3285
|
-
if (!
|
|
3556
|
+
if (!fs21.existsSync(configDest) || opts.force) {
|
|
3286
3557
|
smartResult = smartInit(cwd);
|
|
3287
|
-
|
|
3558
|
+
fs21.writeFileSync(configDest, JSON.stringify(smartResult.config, null, 2) + "\n");
|
|
3288
3559
|
console.log(" \u2713 kody.config.json (auto-configured)");
|
|
3289
3560
|
} else {
|
|
3290
3561
|
console.log(" \u25CB kody.config.json (exists)");
|
|
3291
3562
|
}
|
|
3292
|
-
const gitignorePath =
|
|
3293
|
-
if (
|
|
3294
|
-
const content =
|
|
3563
|
+
const gitignorePath = path20.join(cwd, ".gitignore");
|
|
3564
|
+
if (fs21.existsSync(gitignorePath)) {
|
|
3565
|
+
const content = fs21.readFileSync(gitignorePath, "utf-8");
|
|
3295
3566
|
if (!content.includes(".tasks/")) {
|
|
3296
|
-
|
|
3567
|
+
fs21.appendFileSync(gitignorePath, "\n.tasks/\n");
|
|
3297
3568
|
console.log(" \u2713 .gitignore (added .tasks/)");
|
|
3298
3569
|
} else {
|
|
3299
3570
|
console.log(" \u25CB .gitignore (.tasks/ already present)");
|
|
@@ -3306,7 +3577,7 @@ function initCommand(opts) {
|
|
|
3306
3577
|
checkCommand2("git", ["--version"], "Install git"),
|
|
3307
3578
|
checkCommand2("node", ["--version"], "Install Node.js >= 22"),
|
|
3308
3579
|
checkCommand2("pnpm", ["--version"], "Install: npm i -g pnpm"),
|
|
3309
|
-
checkFile(
|
|
3580
|
+
checkFile(path20.join(cwd, "package.json"), "package.json", "Run: pnpm init")
|
|
3310
3581
|
];
|
|
3311
3582
|
for (const c of checks) {
|
|
3312
3583
|
if (c.ok) {
|
|
@@ -3383,9 +3654,9 @@ function initCommand(opts) {
|
|
|
3383
3654
|
}
|
|
3384
3655
|
}
|
|
3385
3656
|
console.log("\n\u2500\u2500 Config \u2500\u2500");
|
|
3386
|
-
if (
|
|
3657
|
+
if (fs21.existsSync(configDest)) {
|
|
3387
3658
|
try {
|
|
3388
|
-
const config = JSON.parse(
|
|
3659
|
+
const config = JSON.parse(fs21.readFileSync(configDest, "utf-8"));
|
|
3389
3660
|
const configChecks = [];
|
|
3390
3661
|
if (config.github?.owner && config.github?.repo) {
|
|
3391
3662
|
configChecks.push({ name: "github.owner/repo", ok: true, detail: `${config.github.owner}/${config.github.repo}` });
|
|
@@ -3412,21 +3683,21 @@ function initCommand(opts) {
|
|
|
3412
3683
|
}
|
|
3413
3684
|
}
|
|
3414
3685
|
console.log("\n\u2500\u2500 Project Memory \u2500\u2500");
|
|
3415
|
-
const memoryDir =
|
|
3416
|
-
|
|
3417
|
-
const archPath =
|
|
3418
|
-
const conventionsPath =
|
|
3419
|
-
if (
|
|
3686
|
+
const memoryDir = path20.join(cwd, ".kody", "memory");
|
|
3687
|
+
fs21.mkdirSync(memoryDir, { recursive: true });
|
|
3688
|
+
const archPath = path20.join(memoryDir, "architecture.md");
|
|
3689
|
+
const conventionsPath = path20.join(memoryDir, "conventions.md");
|
|
3690
|
+
if (fs21.existsSync(archPath) && !opts.force) {
|
|
3420
3691
|
console.log(" \u25CB .kody/memory/architecture.md (exists, use --force to regenerate)");
|
|
3421
3692
|
} else if (smartResult?.architecture) {
|
|
3422
|
-
|
|
3693
|
+
fs21.writeFileSync(archPath, smartResult.architecture);
|
|
3423
3694
|
const lineCount = smartResult.architecture.split("\n").length;
|
|
3424
3695
|
console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines, LLM-generated)`);
|
|
3425
3696
|
} else {
|
|
3426
3697
|
const archItems = detectArchitecture(cwd);
|
|
3427
3698
|
if (archItems.length > 0) {
|
|
3428
3699
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3429
|
-
|
|
3700
|
+
fs21.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
|
|
3430
3701
|
|
|
3431
3702
|
## Overview
|
|
3432
3703
|
${archItems.join("\n")}
|
|
@@ -3436,14 +3707,14 @@ ${archItems.join("\n")}
|
|
|
3436
3707
|
console.log(" \u25CB No architecture detected");
|
|
3437
3708
|
}
|
|
3438
3709
|
}
|
|
3439
|
-
if (
|
|
3710
|
+
if (fs21.existsSync(conventionsPath) && !opts.force) {
|
|
3440
3711
|
console.log(" \u25CB .kody/memory/conventions.md (exists, use --force to regenerate)");
|
|
3441
3712
|
} else if (smartResult?.conventions) {
|
|
3442
|
-
|
|
3713
|
+
fs21.writeFileSync(conventionsPath, smartResult.conventions);
|
|
3443
3714
|
const lineCount = smartResult.conventions.split("\n").length;
|
|
3444
3715
|
console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines, LLM-generated)`);
|
|
3445
3716
|
} else {
|
|
3446
|
-
|
|
3717
|
+
fs21.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
|
|
3447
3718
|
console.log(" \u2713 .kody/memory/conventions.md (seed)");
|
|
3448
3719
|
}
|
|
3449
3720
|
console.log("\n\u2500\u2500 Git \u2500\u2500");
|
|
@@ -3452,7 +3723,7 @@ ${archItems.join("\n")}
|
|
|
3452
3723
|
"kody.config.json",
|
|
3453
3724
|
".kody/memory/architecture.md",
|
|
3454
3725
|
".kody/memory/conventions.md"
|
|
3455
|
-
].filter((f) =>
|
|
3726
|
+
].filter((f) => fs21.existsSync(path20.join(cwd, f)));
|
|
3456
3727
|
if (filesToCommit.length > 0) {
|
|
3457
3728
|
try {
|
|
3458
3729
|
execFileSync11("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
|