@kody-ade/kody-engine 0.2.5 → 0.2.7
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/kody2.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "@kody-ade/kody-engine",
|
|
6
|
-
version: "0.2.
|
|
6
|
+
version: "0.2.7",
|
|
7
7
|
description: "kody2 \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
8
8
|
license: "MIT",
|
|
9
9
|
type: "module",
|
|
@@ -81,7 +81,7 @@ function loadConfig(projectDir = process.cwd()) {
|
|
|
81
81
|
throw new Error(`kody.config.json is invalid JSON: ${msg}`);
|
|
82
82
|
}
|
|
83
83
|
const quality = raw.quality ?? {};
|
|
84
|
-
const
|
|
84
|
+
const git4 = raw.git ?? {};
|
|
85
85
|
const github = raw.github ?? {};
|
|
86
86
|
const agent = raw.agent ?? {};
|
|
87
87
|
if (!agent.model || typeof agent.model !== "string") {
|
|
@@ -97,7 +97,7 @@ function loadConfig(projectDir = process.cwd()) {
|
|
|
97
97
|
testUnit: typeof quality.testUnit === "string" ? quality.testUnit : ""
|
|
98
98
|
},
|
|
99
99
|
git: {
|
|
100
|
-
defaultBranch: typeof
|
|
100
|
+
defaultBranch: typeof git4.defaultBranch === "string" ? git4.defaultBranch : "main"
|
|
101
101
|
},
|
|
102
102
|
github: {
|
|
103
103
|
owner: String(github.owner),
|
|
@@ -107,9 +107,23 @@ function loadConfig(projectDir = process.cwd()) {
|
|
|
107
107
|
model: String(agent.model)
|
|
108
108
|
},
|
|
109
109
|
issueContext: parseIssueContext(raw.issueContext),
|
|
110
|
-
testRequirements: parseTestRequirements(raw.testRequirements)
|
|
110
|
+
testRequirements: parseTestRequirements(raw.testRequirements),
|
|
111
|
+
release: parseReleaseConfig(raw.release)
|
|
111
112
|
};
|
|
112
113
|
}
|
|
114
|
+
function parseReleaseConfig(raw) {
|
|
115
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
116
|
+
const r = raw;
|
|
117
|
+
const out = {};
|
|
118
|
+
if (Array.isArray(r.versionFiles)) out.versionFiles = r.versionFiles.filter((f) => typeof f === "string");
|
|
119
|
+
if (typeof r.publishCommand === "string") out.publishCommand = r.publishCommand;
|
|
120
|
+
if (typeof r.notifyCommand === "string") out.notifyCommand = r.notifyCommand;
|
|
121
|
+
if (typeof r.e2eCommand === "string") out.e2eCommand = r.e2eCommand;
|
|
122
|
+
if (typeof r.draftRelease === "boolean") out.draftRelease = r.draftRelease;
|
|
123
|
+
if (typeof r.releaseBranch === "string") out.releaseBranch = r.releaseBranch;
|
|
124
|
+
if (typeof r.timeoutMs === "number" && r.timeoutMs > 0) out.timeoutMs = Math.floor(r.timeoutMs);
|
|
125
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
126
|
+
}
|
|
113
127
|
function parseIssueContext(raw) {
|
|
114
128
|
if (!raw || typeof raw !== "object") return void 0;
|
|
115
129
|
const r = raw;
|
|
@@ -137,8 +151,8 @@ function getAnthropicApiKeyOrDummy() {
|
|
|
137
151
|
}
|
|
138
152
|
|
|
139
153
|
// src/executor.ts
|
|
140
|
-
import * as
|
|
141
|
-
import * as
|
|
154
|
+
import * as fs13 from "fs";
|
|
155
|
+
import * as path11 from "path";
|
|
142
156
|
|
|
143
157
|
// src/agent.ts
|
|
144
158
|
import * as fs2 from "fs";
|
|
@@ -449,9 +463,15 @@ function loadProfile(profilePath) {
|
|
|
449
463
|
throw new ProfileError(profilePath, "profile must be a JSON object");
|
|
450
464
|
}
|
|
451
465
|
const r = raw;
|
|
466
|
+
const kind = r.kind === "scheduled" ? "scheduled" : "oneshot";
|
|
467
|
+
if (kind === "scheduled" && typeof r.schedule !== "string") {
|
|
468
|
+
throw new ProfileError(profilePath, `kind: "scheduled" requires a "schedule" cron string`);
|
|
469
|
+
}
|
|
452
470
|
const profile = {
|
|
453
471
|
name: requireString(profilePath, r, "name"),
|
|
454
472
|
describe: typeof r.describe === "string" ? r.describe : "",
|
|
473
|
+
kind,
|
|
474
|
+
schedule: typeof r.schedule === "string" ? r.schedule : void 0,
|
|
455
475
|
inputs: parseInputs(profilePath, r.inputs),
|
|
456
476
|
claudeCode: parseClaudeCode(profilePath, r.claudeCode),
|
|
457
477
|
cliTools: parseCliTools(profilePath, r.cliTools),
|
|
@@ -1650,12 +1670,75 @@ function tryPostPr2(prNumber, body, cwd) {
|
|
|
1650
1670
|
|
|
1651
1671
|
// src/scripts/initFlow.ts
|
|
1652
1672
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
1673
|
+
import * as fs10 from "fs";
|
|
1674
|
+
import * as path9 from "path";
|
|
1675
|
+
|
|
1676
|
+
// src/registry.ts
|
|
1653
1677
|
import * as fs9 from "fs";
|
|
1654
1678
|
import * as path8 from "path";
|
|
1679
|
+
function getExecutablesRoot() {
|
|
1680
|
+
const here = path8.dirname(new URL(import.meta.url).pathname);
|
|
1681
|
+
const candidates = [
|
|
1682
|
+
path8.join(here, "executables"),
|
|
1683
|
+
// dev: src/
|
|
1684
|
+
path8.join(here, "..", "executables"),
|
|
1685
|
+
// built: dist/bin → dist/executables
|
|
1686
|
+
path8.join(here, "..", "src", "executables")
|
|
1687
|
+
// fallback
|
|
1688
|
+
];
|
|
1689
|
+
for (const c of candidates) {
|
|
1690
|
+
if (fs9.existsSync(c) && fs9.statSync(c).isDirectory()) return c;
|
|
1691
|
+
}
|
|
1692
|
+
return candidates[0];
|
|
1693
|
+
}
|
|
1694
|
+
function listExecutables(root = getExecutablesRoot()) {
|
|
1695
|
+
if (!fs9.existsSync(root)) return [];
|
|
1696
|
+
const entries = fs9.readdirSync(root, { withFileTypes: true });
|
|
1697
|
+
const out = [];
|
|
1698
|
+
for (const ent of entries) {
|
|
1699
|
+
if (!ent.isDirectory()) continue;
|
|
1700
|
+
const profilePath = path8.join(root, ent.name, "profile.json");
|
|
1701
|
+
if (fs9.existsSync(profilePath) && fs9.statSync(profilePath).isFile()) {
|
|
1702
|
+
out.push({ name: ent.name, profilePath });
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
1706
|
+
}
|
|
1707
|
+
function hasExecutable(name, root = getExecutablesRoot()) {
|
|
1708
|
+
if (!isSafeName(name)) return false;
|
|
1709
|
+
const profilePath = path8.join(root, name, "profile.json");
|
|
1710
|
+
return fs9.existsSync(profilePath) && fs9.statSync(profilePath).isFile();
|
|
1711
|
+
}
|
|
1712
|
+
function isSafeName(name) {
|
|
1713
|
+
return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
|
|
1714
|
+
}
|
|
1715
|
+
function parseGenericFlags(argv) {
|
|
1716
|
+
const args = {};
|
|
1717
|
+
const positional = [];
|
|
1718
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1719
|
+
const arg = argv[i];
|
|
1720
|
+
if (!arg.startsWith("--")) {
|
|
1721
|
+
positional.push(arg);
|
|
1722
|
+
continue;
|
|
1723
|
+
}
|
|
1724
|
+
const key = arg.slice(2);
|
|
1725
|
+
const next = argv[i + 1];
|
|
1726
|
+
if (next !== void 0 && !next.startsWith("--")) {
|
|
1727
|
+
args[key] = next;
|
|
1728
|
+
i++;
|
|
1729
|
+
} else {
|
|
1730
|
+
args[key] = true;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
if (positional.length > 0) args._ = positional;
|
|
1734
|
+
return args;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// src/scripts/initFlow.ts
|
|
1655
1738
|
function detectPackageManager(cwd) {
|
|
1656
|
-
if (
|
|
1657
|
-
if (
|
|
1658
|
-
if (
|
|
1739
|
+
if (fs10.existsSync(path9.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
1740
|
+
if (fs10.existsSync(path9.join(cwd, "yarn.lock"))) return "yarn";
|
|
1741
|
+
if (fs10.existsSync(path9.join(cwd, "bun.lockb"))) return "bun";
|
|
1659
1742
|
return "npm";
|
|
1660
1743
|
}
|
|
1661
1744
|
function qualityCommandsFor(pm) {
|
|
@@ -1776,26 +1859,74 @@ function performInit(cwd, force) {
|
|
|
1776
1859
|
const pm = detectPackageManager(cwd);
|
|
1777
1860
|
const ownerRepo = detectOwnerRepo(cwd);
|
|
1778
1861
|
const defaultBranch = defaultBranchFromGit(cwd);
|
|
1779
|
-
const configPath =
|
|
1780
|
-
if (
|
|
1862
|
+
const configPath = path9.join(cwd, "kody.config.json");
|
|
1863
|
+
if (fs10.existsSync(configPath) && !force) {
|
|
1781
1864
|
skipped.push("kody.config.json");
|
|
1782
1865
|
} else {
|
|
1783
1866
|
const cfg = makeConfig(pm, ownerRepo, defaultBranch);
|
|
1784
|
-
|
|
1867
|
+
fs10.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
|
|
1785
1868
|
`);
|
|
1786
1869
|
wrote.push("kody.config.json");
|
|
1787
1870
|
}
|
|
1788
|
-
const workflowDir =
|
|
1789
|
-
const workflowPath =
|
|
1790
|
-
if (
|
|
1871
|
+
const workflowDir = path9.join(cwd, ".github", "workflows");
|
|
1872
|
+
const workflowPath = path9.join(workflowDir, "kody2.yml");
|
|
1873
|
+
if (fs10.existsSync(workflowPath) && !force) {
|
|
1791
1874
|
skipped.push(".github/workflows/kody2.yml");
|
|
1792
1875
|
} else {
|
|
1793
|
-
|
|
1794
|
-
|
|
1876
|
+
fs10.mkdirSync(workflowDir, { recursive: true });
|
|
1877
|
+
fs10.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
|
|
1795
1878
|
wrote.push(".github/workflows/kody2.yml");
|
|
1796
1879
|
}
|
|
1880
|
+
for (const exe of listExecutables()) {
|
|
1881
|
+
let profile;
|
|
1882
|
+
try {
|
|
1883
|
+
profile = loadProfile(exe.profilePath);
|
|
1884
|
+
} catch {
|
|
1885
|
+
continue;
|
|
1886
|
+
}
|
|
1887
|
+
if (profile.kind !== "scheduled" || !profile.schedule) continue;
|
|
1888
|
+
const target = path9.join(workflowDir, `kody2-${exe.name}.yml`);
|
|
1889
|
+
if (fs10.existsSync(target) && !force) {
|
|
1890
|
+
skipped.push(`.github/workflows/kody2-${exe.name}.yml`);
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
fs10.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
|
|
1894
|
+
wrote.push(`.github/workflows/kody2-${exe.name}.yml`);
|
|
1895
|
+
}
|
|
1797
1896
|
return { wrote, skipped };
|
|
1798
1897
|
}
|
|
1898
|
+
function renderScheduledWorkflow(name, cron) {
|
|
1899
|
+
return `# Scheduled kody2 executable: ${name}
|
|
1900
|
+
# Generated by \`kody2 init\`. Regenerate with \`kody2 init --force\`.
|
|
1901
|
+
# Edit the cron below or the executable's profile.json#schedule.
|
|
1902
|
+
|
|
1903
|
+
name: kody2 ${name}
|
|
1904
|
+
|
|
1905
|
+
on:
|
|
1906
|
+
schedule:
|
|
1907
|
+
- cron: "${cron}"
|
|
1908
|
+
workflow_dispatch:
|
|
1909
|
+
|
|
1910
|
+
jobs:
|
|
1911
|
+
run:
|
|
1912
|
+
runs-on: ubuntu-latest
|
|
1913
|
+
timeout-minutes: 30
|
|
1914
|
+
permissions:
|
|
1915
|
+
issues: write
|
|
1916
|
+
pull-requests: read
|
|
1917
|
+
contents: read
|
|
1918
|
+
steps:
|
|
1919
|
+
- uses: actions/checkout@v4
|
|
1920
|
+
with:
|
|
1921
|
+
token: \${{ secrets.KODY_TOKEN || github.token }}
|
|
1922
|
+
- uses: actions/setup-node@v4
|
|
1923
|
+
with:
|
|
1924
|
+
node-version: 22
|
|
1925
|
+
- env:
|
|
1926
|
+
GH_TOKEN: \${{ secrets.KODY_TOKEN || github.token }}
|
|
1927
|
+
run: npx -y -p @kody-ade/kody-engine@latest kody2 ${name}
|
|
1928
|
+
`;
|
|
1929
|
+
}
|
|
1799
1930
|
var initFlow = async (ctx) => {
|
|
1800
1931
|
const force = ctx.args.force === true;
|
|
1801
1932
|
const cwd = ctx.cwd;
|
|
@@ -1947,8 +2078,310 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
|
|
|
1947
2078
|
`);
|
|
1948
2079
|
};
|
|
1949
2080
|
|
|
2081
|
+
// src/scripts/releaseFlow.ts
|
|
2082
|
+
import { execFileSync as execFileSync10, spawnSync } from "child_process";
|
|
2083
|
+
import * as fs11 from "fs";
|
|
2084
|
+
import * as path10 from "path";
|
|
2085
|
+
function bumpVersion(current, bump) {
|
|
2086
|
+
const m = current.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
|
|
2087
|
+
if (!m) throw new Error(`cannot parse version '${current}' (expected x.y.z[-suffix])`);
|
|
2088
|
+
let [major, minor, patch] = [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)];
|
|
2089
|
+
if (bump === "major") {
|
|
2090
|
+
major++;
|
|
2091
|
+
minor = 0;
|
|
2092
|
+
patch = 0;
|
|
2093
|
+
} else if (bump === "minor") {
|
|
2094
|
+
minor++;
|
|
2095
|
+
patch = 0;
|
|
2096
|
+
} else patch++;
|
|
2097
|
+
return `${major}.${minor}.${patch}`;
|
|
2098
|
+
}
|
|
2099
|
+
function updateVersionInFile(file, newVersion, cwd) {
|
|
2100
|
+
const abs = path10.join(cwd, file);
|
|
2101
|
+
if (!fs11.existsSync(abs)) return false;
|
|
2102
|
+
const content = fs11.readFileSync(abs, "utf-8");
|
|
2103
|
+
const updated = content.replace(/"version"\s*:\s*"[^"]+"/, `"version": "${newVersion}"`);
|
|
2104
|
+
if (updated === content) return false;
|
|
2105
|
+
fs11.writeFileSync(abs, updated);
|
|
2106
|
+
return true;
|
|
2107
|
+
}
|
|
2108
|
+
function generateChangelog(cwd, newVersion, lastTag) {
|
|
2109
|
+
const range = lastTag ? `${lastTag}..HEAD` : "HEAD";
|
|
2110
|
+
let log = "";
|
|
2111
|
+
try {
|
|
2112
|
+
log = execFileSync10("git", ["log", range, "--pretty=format:%s||%h", "--no-merges"], {
|
|
2113
|
+
cwd,
|
|
2114
|
+
encoding: "utf-8",
|
|
2115
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2116
|
+
}).trim();
|
|
2117
|
+
} catch {
|
|
2118
|
+
}
|
|
2119
|
+
const commits = log.split("\n").filter((l) => l.length > 0).map((line) => {
|
|
2120
|
+
const [subject, sha] = line.split("||");
|
|
2121
|
+
return { subject: subject ?? "", sha: sha ?? "" };
|
|
2122
|
+
}).filter((c) => !/^chore:\s*release\s+v\d/i.test(c.subject));
|
|
2123
|
+
const groups = { feat: [], fix: [], perf: [], refactor: [], docs: [], chore: [], other: [] };
|
|
2124
|
+
for (const c of commits) {
|
|
2125
|
+
const m = c.subject.match(/^(\w+)(?:\(.*?\))?\s*:\s*(.+)$/);
|
|
2126
|
+
const type = m?.[1]?.toLowerCase() ?? "other";
|
|
2127
|
+
const msg = m?.[2] ?? c.subject;
|
|
2128
|
+
const bucket = groups[type] ?? groups.other;
|
|
2129
|
+
bucket.push(`- ${msg} (${c.sha})`);
|
|
2130
|
+
}
|
|
2131
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2132
|
+
const parts = [`## v${newVersion} \u2014 ${date}`, ""];
|
|
2133
|
+
const labels = [
|
|
2134
|
+
["feat", "Features"],
|
|
2135
|
+
["fix", "Fixes"],
|
|
2136
|
+
["perf", "Performance"],
|
|
2137
|
+
["refactor", "Refactoring"],
|
|
2138
|
+
["docs", "Docs"],
|
|
2139
|
+
["chore", "Chores"],
|
|
2140
|
+
["other", "Other"]
|
|
2141
|
+
];
|
|
2142
|
+
for (const [key, label] of labels) {
|
|
2143
|
+
const items = groups[key];
|
|
2144
|
+
if (!items || items.length === 0) continue;
|
|
2145
|
+
parts.push(`### ${label}`);
|
|
2146
|
+
parts.push(...items);
|
|
2147
|
+
parts.push("");
|
|
2148
|
+
}
|
|
2149
|
+
if (parts.length === 2) parts.push("_No notable commits since the last release._", "");
|
|
2150
|
+
return parts.join("\n");
|
|
2151
|
+
}
|
|
2152
|
+
function prependChangelog(cwd, entry) {
|
|
2153
|
+
const p = path10.join(cwd, "CHANGELOG.md");
|
|
2154
|
+
const header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n";
|
|
2155
|
+
if (fs11.existsSync(p)) {
|
|
2156
|
+
const prior = fs11.readFileSync(p, "utf-8");
|
|
2157
|
+
if (/^#\s*Changelog\b/m.test(prior)) {
|
|
2158
|
+
const idx = prior.indexOf("\n", prior.indexOf("# Changelog"));
|
|
2159
|
+
fs11.writeFileSync(p, `${prior.slice(0, idx + 1)}
|
|
2160
|
+
${entry}${prior.slice(idx + 1)}`);
|
|
2161
|
+
} else {
|
|
2162
|
+
fs11.writeFileSync(p, `${header}${entry}${prior}`);
|
|
2163
|
+
}
|
|
2164
|
+
} else {
|
|
2165
|
+
fs11.writeFileSync(p, `${header}${entry}`);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
function git3(args, cwd, timeout = 6e4) {
|
|
2169
|
+
return execFileSync10("git", args, {
|
|
2170
|
+
encoding: "utf-8",
|
|
2171
|
+
timeout,
|
|
2172
|
+
cwd,
|
|
2173
|
+
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
2174
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2175
|
+
}).trim();
|
|
2176
|
+
}
|
|
2177
|
+
function lastReleaseTag(cwd) {
|
|
2178
|
+
try {
|
|
2179
|
+
return git3(["describe", "--tags", "--abbrev=0", "--match", "v*"], cwd);
|
|
2180
|
+
} catch {
|
|
2181
|
+
return null;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
function runShell(cmd, cwd, timeoutMs) {
|
|
2185
|
+
const r = spawnSync(cmd, {
|
|
2186
|
+
cwd,
|
|
2187
|
+
shell: true,
|
|
2188
|
+
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" },
|
|
2189
|
+
encoding: "utf-8",
|
|
2190
|
+
timeout: timeoutMs
|
|
2191
|
+
});
|
|
2192
|
+
return { exitCode: r.status ?? -1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
2193
|
+
}
|
|
2194
|
+
var releaseFlow = async (ctx) => {
|
|
2195
|
+
const mode = ctx.args.mode ?? "prepare";
|
|
2196
|
+
const bump = ctx.args.bump ?? "patch";
|
|
2197
|
+
const dryRun = ctx.args["dry-run"] === true || ctx.args.dryRun === true;
|
|
2198
|
+
const cwd = ctx.cwd;
|
|
2199
|
+
const releaseCfg = ctx.config.release ?? {};
|
|
2200
|
+
const versionFiles = releaseCfg.versionFiles && releaseCfg.versionFiles.length > 0 ? releaseCfg.versionFiles : ["package.json"];
|
|
2201
|
+
const timeoutMs = releaseCfg.timeoutMs ?? 6e5;
|
|
2202
|
+
ctx.skipAgent = true;
|
|
2203
|
+
if (mode === "prepare") {
|
|
2204
|
+
await runPrepare({ cwd, bump, dryRun, versionFiles, ctx });
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
if (mode === "finalize") {
|
|
2208
|
+
await runFinalize({ cwd, dryRun, timeoutMs, releaseCfg, ctx });
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
ctx.output.exitCode = 64;
|
|
2212
|
+
ctx.output.reason = `release: unknown mode '${mode}'`;
|
|
2213
|
+
};
|
|
2214
|
+
async function runPrepare(args) {
|
|
2215
|
+
const { cwd, bump, dryRun, versionFiles, ctx } = args;
|
|
2216
|
+
const pkgPath = path10.join(cwd, "package.json");
|
|
2217
|
+
if (!fs11.existsSync(pkgPath)) {
|
|
2218
|
+
ctx.output.exitCode = 99;
|
|
2219
|
+
ctx.output.reason = "release prepare: package.json not found";
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
|
|
2223
|
+
if (typeof pkg.version !== "string") {
|
|
2224
|
+
ctx.output.exitCode = 99;
|
|
2225
|
+
ctx.output.reason = "release prepare: package.json has no version";
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
2228
|
+
const oldVersion = pkg.version;
|
|
2229
|
+
const newVersion = bumpVersion(oldVersion, bump);
|
|
2230
|
+
const tag = `v${newVersion}`;
|
|
2231
|
+
process.stdout.write(`\u2192 release prepare: ${oldVersion} \u2192 ${newVersion} (${bump})
|
|
2232
|
+
`);
|
|
2233
|
+
if (dryRun) {
|
|
2234
|
+
ctx.output.exitCode = 0;
|
|
2235
|
+
ctx.output.reason = `dry-run \u2014 would bump to ${newVersion}`;
|
|
2236
|
+
process.stdout.write(`RELEASE_PLAN=bump=${newVersion} tag=${tag}
|
|
2237
|
+
`);
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
const touched = [];
|
|
2241
|
+
for (const f of versionFiles) {
|
|
2242
|
+
if (updateVersionInFile(f, newVersion, cwd)) touched.push(f);
|
|
2243
|
+
}
|
|
2244
|
+
if (touched.length === 0) {
|
|
2245
|
+
ctx.output.exitCode = 1;
|
|
2246
|
+
ctx.output.reason = `release prepare: no version strings updated (files: ${versionFiles.join(", ")})`;
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
process.stdout.write(` wrote ${touched.join(", ")}
|
|
2250
|
+
`);
|
|
2251
|
+
const entry = generateChangelog(cwd, newVersion, lastReleaseTag(cwd));
|
|
2252
|
+
prependChangelog(cwd, entry);
|
|
2253
|
+
process.stdout.write(` wrote CHANGELOG.md
|
|
2254
|
+
`);
|
|
2255
|
+
const releaseBranch = `release/${tag}`;
|
|
2256
|
+
try {
|
|
2257
|
+
git3(["checkout", "-b", releaseBranch], cwd);
|
|
2258
|
+
for (const f of [...touched, "CHANGELOG.md"]) git3(["add", "--", f], cwd);
|
|
2259
|
+
git3(["commit", "--no-gpg-sign", "-m", `chore: release ${tag}`], cwd);
|
|
2260
|
+
git3(["push", "-u", "origin", releaseBranch], cwd, 12e4);
|
|
2261
|
+
} catch (err) {
|
|
2262
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2263
|
+
ctx.output.exitCode = 4;
|
|
2264
|
+
ctx.output.reason = `release prepare: git commit/push failed: ${msg}`;
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
const base = ctx.config.git.defaultBranch;
|
|
2268
|
+
const title = `chore: release ${tag}`;
|
|
2269
|
+
const body = `Automated release PR opened by kody2.
|
|
2270
|
+
|
|
2271
|
+
${entry}
|
|
2272
|
+
|
|
2273
|
+
Merge this and then run \`kody2 release --mode finalize\`.`;
|
|
2274
|
+
let prUrl = "";
|
|
2275
|
+
try {
|
|
2276
|
+
prUrl = gh(
|
|
2277
|
+
["pr", "create", "--head", releaseBranch, "--base", base, "--title", title, "--body-file", "-"],
|
|
2278
|
+
{ input: body, cwd }
|
|
2279
|
+
).trim();
|
|
2280
|
+
} catch (err) {
|
|
2281
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2282
|
+
ctx.output.exitCode = 4;
|
|
2283
|
+
ctx.output.reason = `release prepare: gh pr create failed: ${msg}`;
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
ctx.output.prUrl = prUrl;
|
|
2287
|
+
ctx.output.exitCode = 0;
|
|
2288
|
+
process.stdout.write(`RELEASE_PR=${prUrl}
|
|
2289
|
+
`);
|
|
2290
|
+
}
|
|
2291
|
+
async function runFinalize(args) {
|
|
2292
|
+
const { cwd, dryRun, timeoutMs, releaseCfg, ctx } = args;
|
|
2293
|
+
const pkgPath = path10.join(cwd, "package.json");
|
|
2294
|
+
const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
|
|
2295
|
+
if (typeof pkg.version !== "string") {
|
|
2296
|
+
ctx.output.exitCode = 99;
|
|
2297
|
+
ctx.output.reason = "release finalize: package.json has no version";
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
const version = pkg.version;
|
|
2301
|
+
const tag = `v${version}`;
|
|
2302
|
+
process.stdout.write(`\u2192 release finalize: ${tag}
|
|
2303
|
+
`);
|
|
2304
|
+
try {
|
|
2305
|
+
git3(["rev-parse", "--verify", tag], cwd);
|
|
2306
|
+
ctx.output.exitCode = 1;
|
|
2307
|
+
ctx.output.reason = `release finalize: tag ${tag} already exists`;
|
|
2308
|
+
return;
|
|
2309
|
+
} catch {
|
|
2310
|
+
}
|
|
2311
|
+
if (dryRun) {
|
|
2312
|
+
ctx.output.exitCode = 0;
|
|
2313
|
+
ctx.output.reason = `dry-run \u2014 would tag + publish ${tag}`;
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
if (releaseCfg.e2eCommand && releaseCfg.e2eCommand.trim().length > 0) {
|
|
2317
|
+
const cmd = releaseCfg.e2eCommand.replace(/\$VERSION/g, version);
|
|
2318
|
+
process.stdout.write(` E2E gate: ${cmd}
|
|
2319
|
+
`);
|
|
2320
|
+
const r = runShell(cmd, cwd, timeoutMs);
|
|
2321
|
+
if (r.exitCode !== 0) {
|
|
2322
|
+
ctx.output.exitCode = 2;
|
|
2323
|
+
ctx.output.reason = `release finalize: E2E gate failed (exit ${r.exitCode}): ${truncate2(r.stderr, 600)}`;
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
try {
|
|
2328
|
+
git3(["tag", "-a", tag, "-m", `Release ${tag}`], cwd);
|
|
2329
|
+
git3(["push", "origin", tag], cwd, 12e4);
|
|
2330
|
+
} catch (err) {
|
|
2331
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2332
|
+
ctx.output.exitCode = 4;
|
|
2333
|
+
ctx.output.reason = `release finalize: tag/push failed: ${msg}`;
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
let publishStatus = "skipped";
|
|
2337
|
+
if (releaseCfg.publishCommand && releaseCfg.publishCommand.trim().length > 0) {
|
|
2338
|
+
const cmd = releaseCfg.publishCommand.replace(/\$VERSION/g, version);
|
|
2339
|
+
process.stdout.write(` publish: ${cmd}
|
|
2340
|
+
`);
|
|
2341
|
+
const r = runShell(cmd, cwd, timeoutMs);
|
|
2342
|
+
publishStatus = r.exitCode === 0 ? "ok" : "failed";
|
|
2343
|
+
if (r.exitCode !== 0) {
|
|
2344
|
+
process.stderr.write(`[kody2 release] publishCommand exit ${r.exitCode}
|
|
2345
|
+
${truncate2(r.stderr, 2e3)}
|
|
2346
|
+
`);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
let releaseUrl = "";
|
|
2350
|
+
try {
|
|
2351
|
+
const releaseArgs = [
|
|
2352
|
+
"release",
|
|
2353
|
+
"create",
|
|
2354
|
+
tag,
|
|
2355
|
+
"--title",
|
|
2356
|
+
tag,
|
|
2357
|
+
"--notes",
|
|
2358
|
+
`Release ${tag} \u2014 automated by kody2.`
|
|
2359
|
+
];
|
|
2360
|
+
if (releaseCfg.draftRelease) releaseArgs.push("--draft");
|
|
2361
|
+
releaseUrl = gh(releaseArgs, { cwd }).trim();
|
|
2362
|
+
} catch (err) {
|
|
2363
|
+
process.stderr.write(`[kody2 release] gh release create failed: ${err instanceof Error ? err.message : String(err)}
|
|
2364
|
+
`);
|
|
2365
|
+
}
|
|
2366
|
+
if (releaseCfg.notifyCommand && releaseCfg.notifyCommand.trim().length > 0) {
|
|
2367
|
+
const cmd = releaseCfg.notifyCommand.replace(/\$VERSION/g, version);
|
|
2368
|
+
runShell(cmd, cwd, timeoutMs);
|
|
2369
|
+
}
|
|
2370
|
+
if (releaseUrl) ctx.output.prUrl = releaseUrl;
|
|
2371
|
+
if (publishStatus === "failed") {
|
|
2372
|
+
ctx.output.exitCode = 1;
|
|
2373
|
+
ctx.output.reason = `release finalize: tag + gh release created, but publishCommand failed`;
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
ctx.output.exitCode = 0;
|
|
2377
|
+
process.stdout.write(`RELEASE_TAG=${tag}
|
|
2378
|
+
`);
|
|
2379
|
+
if (releaseUrl) process.stdout.write(`RELEASE_URL=${releaseUrl}
|
|
2380
|
+
`);
|
|
2381
|
+
}
|
|
2382
|
+
|
|
1950
2383
|
// src/scripts/resolveFlow.ts
|
|
1951
|
-
import { execFileSync as
|
|
2384
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
1952
2385
|
var CONFLICT_DIFF_MAX_BYTES = 4e4;
|
|
1953
2386
|
var resolveFlow = async (ctx) => {
|
|
1954
2387
|
const prNumber = ctx.args.pr;
|
|
@@ -2000,7 +2433,7 @@ var resolveFlow = async (ctx) => {
|
|
|
2000
2433
|
};
|
|
2001
2434
|
function getConflictedFiles(cwd) {
|
|
2002
2435
|
try {
|
|
2003
|
-
const out =
|
|
2436
|
+
const out = execFileSync11("git", ["diff", "--name-only", "--diff-filter=U"], {
|
|
2004
2437
|
encoding: "utf-8",
|
|
2005
2438
|
cwd,
|
|
2006
2439
|
env: { ...process.env, HUSKY: "0" }
|
|
@@ -2015,7 +2448,7 @@ function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTE
|
|
|
2015
2448
|
let total = 0;
|
|
2016
2449
|
for (const f of files) {
|
|
2017
2450
|
try {
|
|
2018
|
-
const content =
|
|
2451
|
+
const content = execFileSync11("cat", [f], { encoding: "utf-8", cwd }).toString();
|
|
2019
2452
|
const snippet = `### ${f}
|
|
2020
2453
|
|
|
2021
2454
|
\`\`\`
|
|
@@ -2179,8 +2612,87 @@ var verify = async (ctx) => {
|
|
|
2179
2612
|
}
|
|
2180
2613
|
};
|
|
2181
2614
|
|
|
2615
|
+
// src/scripts/watchStalePrsFlow.ts
|
|
2616
|
+
function readWatchConfig(ctx) {
|
|
2617
|
+
const cfg = ctx.config.watch;
|
|
2618
|
+
if (!cfg || typeof cfg !== "object") return {};
|
|
2619
|
+
const r = cfg;
|
|
2620
|
+
return {
|
|
2621
|
+
staleDays: typeof r.staleDays === "number" && r.staleDays > 0 ? Math.floor(r.staleDays) : void 0,
|
|
2622
|
+
reportIssueNumber: typeof r.reportIssueNumber === "number" && r.reportIssueNumber > 0 ? Math.floor(r.reportIssueNumber) : void 0
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
function findStalePrs(cwd, staleDays, now = /* @__PURE__ */ new Date()) {
|
|
2626
|
+
let raw = "";
|
|
2627
|
+
try {
|
|
2628
|
+
raw = gh(
|
|
2629
|
+
[
|
|
2630
|
+
"pr",
|
|
2631
|
+
"list",
|
|
2632
|
+
"--state",
|
|
2633
|
+
"open",
|
|
2634
|
+
"--limit",
|
|
2635
|
+
"100",
|
|
2636
|
+
"--json",
|
|
2637
|
+
"number,title,url,updatedAt"
|
|
2638
|
+
],
|
|
2639
|
+
{ cwd }
|
|
2640
|
+
);
|
|
2641
|
+
} catch {
|
|
2642
|
+
return [];
|
|
2643
|
+
}
|
|
2644
|
+
let list;
|
|
2645
|
+
try {
|
|
2646
|
+
list = JSON.parse(raw);
|
|
2647
|
+
} catch {
|
|
2648
|
+
return [];
|
|
2649
|
+
}
|
|
2650
|
+
if (!Array.isArray(list)) return [];
|
|
2651
|
+
const cutoffMs = now.getTime() - staleDays * 24 * 60 * 60 * 1e3;
|
|
2652
|
+
const stale = [];
|
|
2653
|
+
for (const pr of list) {
|
|
2654
|
+
const ts = Date.parse(pr.updatedAt);
|
|
2655
|
+
if (!Number.isFinite(ts) || ts > cutoffMs) continue;
|
|
2656
|
+
const daysStale = Math.floor((now.getTime() - ts) / (24 * 60 * 60 * 1e3));
|
|
2657
|
+
stale.push({ number: pr.number, title: pr.title, url: pr.url, updatedAt: pr.updatedAt, daysStale });
|
|
2658
|
+
}
|
|
2659
|
+
return stale.sort((a, b) => b.daysStale - a.daysStale);
|
|
2660
|
+
}
|
|
2661
|
+
function formatStaleReport(stale, staleDays) {
|
|
2662
|
+
if (stale.length === 0) {
|
|
2663
|
+
return `\u{1F7E2} **kody2 watch-stale-prs** \u2014 no open PRs untouched for more than ${staleDays} days. \u2728`;
|
|
2664
|
+
}
|
|
2665
|
+
const lines = [
|
|
2666
|
+
`\u{1F7E1} **kody2 watch-stale-prs** \u2014 ${stale.length} PR(s) untouched for > ${staleDays} days:`,
|
|
2667
|
+
""
|
|
2668
|
+
];
|
|
2669
|
+
for (const pr of stale.slice(0, 50)) {
|
|
2670
|
+
lines.push(`- [#${pr.number}](${pr.url}) \u2014 *${truncate2(pr.title, 80)}* (${pr.daysStale} days stale)`);
|
|
2671
|
+
}
|
|
2672
|
+
if (stale.length > 50) lines.push(`- \u2026 and ${stale.length - 50} more`);
|
|
2673
|
+
return lines.join("\n");
|
|
2674
|
+
}
|
|
2675
|
+
var watchStalePrsFlow = async (ctx) => {
|
|
2676
|
+
ctx.skipAgent = true;
|
|
2677
|
+
const { staleDays = 7, reportIssueNumber } = readWatchConfig(ctx);
|
|
2678
|
+
const stale = findStalePrs(ctx.cwd, staleDays);
|
|
2679
|
+
const report = formatStaleReport(stale, staleDays);
|
|
2680
|
+
process.stdout.write(`${report}
|
|
2681
|
+
`);
|
|
2682
|
+
if (reportIssueNumber) {
|
|
2683
|
+
try {
|
|
2684
|
+
postIssueComment(reportIssueNumber, report, ctx.cwd);
|
|
2685
|
+
} catch (err) {
|
|
2686
|
+
process.stderr.write(`[kody2 watch] failed to post to issue #${reportIssueNumber}: ${err instanceof Error ? err.message : String(err)}
|
|
2687
|
+
`);
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
ctx.output.exitCode = 0;
|
|
2691
|
+
ctx.data.staleCount = stale.length;
|
|
2692
|
+
};
|
|
2693
|
+
|
|
2182
2694
|
// src/scripts/writeRunSummary.ts
|
|
2183
|
-
import * as
|
|
2695
|
+
import * as fs12 from "fs";
|
|
2184
2696
|
var writeRunSummary = async (ctx) => {
|
|
2185
2697
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
2186
2698
|
if (!summaryPath) return;
|
|
@@ -2202,7 +2714,7 @@ var writeRunSummary = async (ctx) => {
|
|
|
2202
2714
|
if (reason) lines.push(`- **Reason:** ${reason}`);
|
|
2203
2715
|
lines.push("");
|
|
2204
2716
|
try {
|
|
2205
|
-
|
|
2717
|
+
fs12.appendFileSync(summaryPath, `${lines.join("\n")}
|
|
2206
2718
|
`);
|
|
2207
2719
|
} catch {
|
|
2208
2720
|
}
|
|
@@ -2216,6 +2728,8 @@ var preflightScripts = {
|
|
|
2216
2728
|
resolveFlow,
|
|
2217
2729
|
reviewFlow,
|
|
2218
2730
|
initFlow,
|
|
2731
|
+
releaseFlow,
|
|
2732
|
+
watchStalePrsFlow,
|
|
2219
2733
|
loadConventions,
|
|
2220
2734
|
loadCoverageRules,
|
|
2221
2735
|
composePrompt
|
|
@@ -2236,7 +2750,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
|
|
|
2236
2750
|
]);
|
|
2237
2751
|
|
|
2238
2752
|
// src/tools.ts
|
|
2239
|
-
import { execFileSync as
|
|
2753
|
+
import { execFileSync as execFileSync12 } from "child_process";
|
|
2240
2754
|
function verifyCliTools(tools, cwd) {
|
|
2241
2755
|
const out = [];
|
|
2242
2756
|
for (const t of tools) out.push(verifyOne(t, cwd));
|
|
@@ -2252,24 +2766,24 @@ function firstRequiredFailure(results, tools) {
|
|
|
2252
2766
|
}
|
|
2253
2767
|
function verifyOne(tool, cwd) {
|
|
2254
2768
|
const result = { name: tool.name, present: false, verified: false };
|
|
2255
|
-
let present =
|
|
2769
|
+
let present = runShell2(tool.install.checkCommand, cwd);
|
|
2256
2770
|
if (!present && tool.install.installCommand) {
|
|
2257
|
-
|
|
2258
|
-
present =
|
|
2771
|
+
runShell2(tool.install.installCommand, cwd, 12e4);
|
|
2772
|
+
present = runShell2(tool.install.checkCommand, cwd);
|
|
2259
2773
|
}
|
|
2260
2774
|
result.present = present;
|
|
2261
2775
|
if (!present) {
|
|
2262
2776
|
result.error = `tool "${tool.name}" not on PATH (check: ${tool.install.checkCommand})`;
|
|
2263
2777
|
return result;
|
|
2264
2778
|
}
|
|
2265
|
-
const verified =
|
|
2779
|
+
const verified = runShell2(tool.verify, cwd);
|
|
2266
2780
|
result.verified = verified;
|
|
2267
2781
|
if (!verified) result.error = `tool "${tool.name}" failed verify: ${tool.verify}`;
|
|
2268
2782
|
return result;
|
|
2269
2783
|
}
|
|
2270
|
-
function
|
|
2784
|
+
function runShell2(cmd, cwd, timeoutMs = 3e4) {
|
|
2271
2785
|
try {
|
|
2272
|
-
|
|
2786
|
+
execFileSync12("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
|
|
2273
2787
|
return true;
|
|
2274
2788
|
} catch {
|
|
2275
2789
|
return false;
|
|
@@ -2320,7 +2834,7 @@ async function runExecutable(profileName, input) {
|
|
|
2320
2834
|
data: {},
|
|
2321
2835
|
output: { exitCode: 0 }
|
|
2322
2836
|
};
|
|
2323
|
-
const ndjsonDir =
|
|
2837
|
+
const ndjsonDir = path11.join(input.cwd, ".kody2");
|
|
2324
2838
|
const invokeAgent = async (prompt) => runAgent({
|
|
2325
2839
|
prompt,
|
|
2326
2840
|
model,
|
|
@@ -2379,17 +2893,17 @@ async function runExecutable(profileName, input) {
|
|
|
2379
2893
|
}
|
|
2380
2894
|
}
|
|
2381
2895
|
function resolveProfilePath(profileName) {
|
|
2382
|
-
const here =
|
|
2896
|
+
const here = path11.dirname(new URL(import.meta.url).pathname);
|
|
2383
2897
|
const candidates = [
|
|
2384
|
-
|
|
2898
|
+
path11.join(here, "executables", profileName, "profile.json"),
|
|
2385
2899
|
// same-dir sibling (dev)
|
|
2386
|
-
|
|
2900
|
+
path11.join(here, "..", "executables", profileName, "profile.json"),
|
|
2387
2901
|
// up one (prod: dist/bin → dist/executables)
|
|
2388
|
-
|
|
2902
|
+
path11.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
2389
2903
|
// fallback
|
|
2390
2904
|
];
|
|
2391
2905
|
for (const c of candidates) {
|
|
2392
|
-
if (
|
|
2906
|
+
if (fs13.existsSync(c)) return c;
|
|
2393
2907
|
}
|
|
2394
2908
|
return candidates[0];
|
|
2395
2909
|
}
|
|
@@ -2467,12 +2981,12 @@ function finish(out) {
|
|
|
2467
2981
|
}
|
|
2468
2982
|
|
|
2469
2983
|
// src/kody2-cli.ts
|
|
2470
|
-
import { execFileSync as
|
|
2471
|
-
import * as
|
|
2472
|
-
import * as
|
|
2984
|
+
import { execFileSync as execFileSync13 } from "child_process";
|
|
2985
|
+
import * as fs15 from "fs";
|
|
2986
|
+
import * as path12 from "path";
|
|
2473
2987
|
|
|
2474
2988
|
// src/dispatch.ts
|
|
2475
|
-
import * as
|
|
2989
|
+
import * as fs14 from "fs";
|
|
2476
2990
|
function autoDispatch(explicit) {
|
|
2477
2991
|
if (explicit?.mode && explicit.target) {
|
|
2478
2992
|
return {
|
|
@@ -2482,10 +2996,10 @@ function autoDispatch(explicit) {
|
|
|
2482
2996
|
}
|
|
2483
2997
|
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
2484
2998
|
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
2485
|
-
if (!eventName || !eventPath || !
|
|
2999
|
+
if (!eventName || !eventPath || !fs14.existsSync(eventPath)) return null;
|
|
2486
3000
|
let event = {};
|
|
2487
3001
|
try {
|
|
2488
|
-
event = JSON.parse(
|
|
3002
|
+
event = JSON.parse(fs14.readFileSync(eventPath, "utf-8"));
|
|
2489
3003
|
} catch {
|
|
2490
3004
|
return null;
|
|
2491
3005
|
}
|
|
@@ -2605,14 +3119,14 @@ function resolveAuthToken(env = process.env) {
|
|
|
2605
3119
|
return token;
|
|
2606
3120
|
}
|
|
2607
3121
|
function detectPackageManager2(cwd) {
|
|
2608
|
-
if (
|
|
2609
|
-
if (
|
|
2610
|
-
if (
|
|
3122
|
+
if (fs15.existsSync(path12.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
3123
|
+
if (fs15.existsSync(path12.join(cwd, "yarn.lock"))) return "yarn";
|
|
3124
|
+
if (fs15.existsSync(path12.join(cwd, "bun.lockb"))) return "bun";
|
|
2611
3125
|
return "npm";
|
|
2612
3126
|
}
|
|
2613
3127
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
2614
3128
|
try {
|
|
2615
|
-
|
|
3129
|
+
execFileSync13(cmd, args, {
|
|
2616
3130
|
cwd,
|
|
2617
3131
|
stdio: stream ? "inherit" : "pipe",
|
|
2618
3132
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
|
|
@@ -2625,7 +3139,7 @@ function shellOut(cmd, args, cwd, stream = true) {
|
|
|
2625
3139
|
}
|
|
2626
3140
|
function isOnPath(bin) {
|
|
2627
3141
|
try {
|
|
2628
|
-
|
|
3142
|
+
execFileSync13("which", [bin], { stdio: "pipe" });
|
|
2629
3143
|
return true;
|
|
2630
3144
|
} catch {
|
|
2631
3145
|
return false;
|
|
@@ -2659,7 +3173,7 @@ function installLitellmIfNeeded(cwd) {
|
|
|
2659
3173
|
} catch {
|
|
2660
3174
|
}
|
|
2661
3175
|
try {
|
|
2662
|
-
|
|
3176
|
+
execFileSync13("python3", ["-c", "import litellm"], { stdio: "pipe" });
|
|
2663
3177
|
process.stdout.write("\u2192 kody2: litellm already installed\n");
|
|
2664
3178
|
return 0;
|
|
2665
3179
|
} catch {
|
|
@@ -2669,26 +3183,26 @@ function installLitellmIfNeeded(cwd) {
|
|
|
2669
3183
|
}
|
|
2670
3184
|
function configureGitIdentity(cwd) {
|
|
2671
3185
|
try {
|
|
2672
|
-
const name =
|
|
3186
|
+
const name = execFileSync13("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
|
|
2673
3187
|
if (name) return;
|
|
2674
3188
|
} catch {
|
|
2675
3189
|
}
|
|
2676
3190
|
try {
|
|
2677
|
-
|
|
3191
|
+
execFileSync13("git", ["config", "user.name", "kody2-bot"], { cwd, stdio: "pipe" });
|
|
2678
3192
|
} catch {
|
|
2679
3193
|
}
|
|
2680
3194
|
try {
|
|
2681
|
-
|
|
3195
|
+
execFileSync13("git", ["config", "user.email", "kody2-bot@users.noreply.github.com"], { cwd, stdio: "pipe" });
|
|
2682
3196
|
} catch {
|
|
2683
3197
|
}
|
|
2684
3198
|
}
|
|
2685
3199
|
function postFailureTail(issueNumber, cwd, reason) {
|
|
2686
3200
|
if (!issueNumber) return;
|
|
2687
|
-
const logPath =
|
|
3201
|
+
const logPath = path12.join(cwd, ".kody2", "last-run.jsonl");
|
|
2688
3202
|
let tail = "";
|
|
2689
3203
|
try {
|
|
2690
|
-
if (
|
|
2691
|
-
const content =
|
|
3204
|
+
if (fs15.existsSync(logPath)) {
|
|
3205
|
+
const content = fs15.readFileSync(logPath, "utf-8");
|
|
2692
3206
|
tail = content.slice(-3e3);
|
|
2693
3207
|
}
|
|
2694
3208
|
} catch {
|
|
@@ -2725,7 +3239,7 @@ async function runCi(argv) {
|
|
|
2725
3239
|
${CI_HELP}`);
|
|
2726
3240
|
return 64;
|
|
2727
3241
|
}
|
|
2728
|
-
const cwd = args.cwd ?
|
|
3242
|
+
const cwd = args.cwd ? path12.resolve(args.cwd) : process.cwd();
|
|
2729
3243
|
const dispatch = autoFallback ?? {
|
|
2730
3244
|
mode: "run",
|
|
2731
3245
|
target: args.issueNumber,
|
|
@@ -2800,67 +3314,6 @@ ${CI_HELP}`);
|
|
|
2800
3314
|
}
|
|
2801
3315
|
}
|
|
2802
3316
|
|
|
2803
|
-
// src/registry.ts
|
|
2804
|
-
import * as fs14 from "fs";
|
|
2805
|
-
import * as path11 from "path";
|
|
2806
|
-
function getExecutablesRoot() {
|
|
2807
|
-
const here = path11.dirname(new URL(import.meta.url).pathname);
|
|
2808
|
-
const candidates = [
|
|
2809
|
-
path11.join(here, "executables"),
|
|
2810
|
-
// dev: src/
|
|
2811
|
-
path11.join(here, "..", "executables"),
|
|
2812
|
-
// built: dist/bin → dist/executables
|
|
2813
|
-
path11.join(here, "..", "src", "executables")
|
|
2814
|
-
// fallback
|
|
2815
|
-
];
|
|
2816
|
-
for (const c of candidates) {
|
|
2817
|
-
if (fs14.existsSync(c) && fs14.statSync(c).isDirectory()) return c;
|
|
2818
|
-
}
|
|
2819
|
-
return candidates[0];
|
|
2820
|
-
}
|
|
2821
|
-
function listExecutables(root = getExecutablesRoot()) {
|
|
2822
|
-
if (!fs14.existsSync(root)) return [];
|
|
2823
|
-
const entries = fs14.readdirSync(root, { withFileTypes: true });
|
|
2824
|
-
const out = [];
|
|
2825
|
-
for (const ent of entries) {
|
|
2826
|
-
if (!ent.isDirectory()) continue;
|
|
2827
|
-
const profilePath = path11.join(root, ent.name, "profile.json");
|
|
2828
|
-
if (fs14.existsSync(profilePath) && fs14.statSync(profilePath).isFile()) {
|
|
2829
|
-
out.push({ name: ent.name, profilePath });
|
|
2830
|
-
}
|
|
2831
|
-
}
|
|
2832
|
-
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
2833
|
-
}
|
|
2834
|
-
function hasExecutable(name, root = getExecutablesRoot()) {
|
|
2835
|
-
if (!isSafeName(name)) return false;
|
|
2836
|
-
const profilePath = path11.join(root, name, "profile.json");
|
|
2837
|
-
return fs14.existsSync(profilePath) && fs14.statSync(profilePath).isFile();
|
|
2838
|
-
}
|
|
2839
|
-
function isSafeName(name) {
|
|
2840
|
-
return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
|
|
2841
|
-
}
|
|
2842
|
-
function parseGenericFlags(argv) {
|
|
2843
|
-
const args = {};
|
|
2844
|
-
const positional = [];
|
|
2845
|
-
for (let i = 0; i < argv.length; i++) {
|
|
2846
|
-
const arg = argv[i];
|
|
2847
|
-
if (!arg.startsWith("--")) {
|
|
2848
|
-
positional.push(arg);
|
|
2849
|
-
continue;
|
|
2850
|
-
}
|
|
2851
|
-
const key = arg.slice(2);
|
|
2852
|
-
const next = argv[i + 1];
|
|
2853
|
-
if (next !== void 0 && !next.startsWith("--")) {
|
|
2854
|
-
args[key] = next;
|
|
2855
|
-
i++;
|
|
2856
|
-
} else {
|
|
2857
|
-
args[key] = true;
|
|
2858
|
-
}
|
|
2859
|
-
}
|
|
2860
|
-
if (positional.length > 0) args._ = positional;
|
|
2861
|
-
return args;
|
|
2862
|
-
}
|
|
2863
|
-
|
|
2864
3317
|
// src/entry.ts
|
|
2865
3318
|
var HELP_TEXT = `kody2 \u2014 single-session autonomous engineer
|
|
2866
3319
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "release",
|
|
3
|
+
"describe": "Version bump + changelog + release PR (prepare), or tag + publish + GH release (finalize). No agent.",
|
|
4
|
+
|
|
5
|
+
"inputs": [
|
|
6
|
+
{
|
|
7
|
+
"name": "mode",
|
|
8
|
+
"flag": "--mode",
|
|
9
|
+
"type": "enum",
|
|
10
|
+
"values": ["prepare", "finalize"],
|
|
11
|
+
"required": false,
|
|
12
|
+
"describe": "`prepare` (default): bump + changelog + release PR. `finalize`: E2E gate + tag + publish + GH release."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "bump",
|
|
16
|
+
"flag": "--bump",
|
|
17
|
+
"type": "enum",
|
|
18
|
+
"values": ["patch", "minor", "major"],
|
|
19
|
+
"required": false,
|
|
20
|
+
"describe": "Version bump when mode=prepare (ignored in finalize). Default patch."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "dry-run",
|
|
24
|
+
"flag": "--dry-run",
|
|
25
|
+
"type": "bool",
|
|
26
|
+
"required": false,
|
|
27
|
+
"describe": "Print plan without writing files, creating PRs, tagging, or publishing."
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
|
|
31
|
+
"claudeCode": {
|
|
32
|
+
"model": "inherit",
|
|
33
|
+
"permissionMode": "acceptEdits",
|
|
34
|
+
"maxTurns": null,
|
|
35
|
+
"systemPromptAppend": null,
|
|
36
|
+
"tools": [],
|
|
37
|
+
"hooks": { "PreToolUse": [], "PostToolUse": [], "Stop": [] },
|
|
38
|
+
"skills": [],
|
|
39
|
+
"commands": [],
|
|
40
|
+
"subagents": [],
|
|
41
|
+
"plugins": [],
|
|
42
|
+
"mcpServers": []
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
"cliTools": [],
|
|
46
|
+
|
|
47
|
+
"scripts": {
|
|
48
|
+
"preflight": [
|
|
49
|
+
{ "script": "releaseFlow" }
|
|
50
|
+
],
|
|
51
|
+
"postflight": []
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -17,6 +17,14 @@ import type { Kody2Config } from "../config.js"
|
|
|
17
17
|
export interface Profile {
|
|
18
18
|
name: string
|
|
19
19
|
describe: string
|
|
20
|
+
/**
|
|
21
|
+
* Execution model. `oneshot` (default): single invocation on demand.
|
|
22
|
+
* `scheduled`: fires periodically via an external cron (typically GHA
|
|
23
|
+
* `schedule:`). Scheduled profiles must declare a `schedule` cron string.
|
|
24
|
+
*/
|
|
25
|
+
kind: "oneshot" | "scheduled"
|
|
26
|
+
/** Cron expression for scheduled profiles (e.g. "0 8 * * MON"). */
|
|
27
|
+
schedule?: string
|
|
20
28
|
inputs: InputSpec[]
|
|
21
29
|
claudeCode: ClaudeCodeSpec
|
|
22
30
|
cliTools: CliToolSpec[]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "watch-stale-prs",
|
|
3
|
+
"describe": "Scheduled: list open PRs untouched for N days and report. No agent invocation.",
|
|
4
|
+
|
|
5
|
+
"kind": "scheduled",
|
|
6
|
+
"schedule": "0 8 * * MON",
|
|
7
|
+
|
|
8
|
+
"inputs": [],
|
|
9
|
+
|
|
10
|
+
"claudeCode": {
|
|
11
|
+
"model": "inherit",
|
|
12
|
+
"permissionMode": "default",
|
|
13
|
+
"maxTurns": null,
|
|
14
|
+
"systemPromptAppend": null,
|
|
15
|
+
"tools": [],
|
|
16
|
+
"hooks": { "PreToolUse": [], "PostToolUse": [], "Stop": [] },
|
|
17
|
+
"skills": [],
|
|
18
|
+
"commands": [],
|
|
19
|
+
"subagents": [],
|
|
20
|
+
"plugins": [],
|
|
21
|
+
"mcpServers": []
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
"cliTools": [],
|
|
25
|
+
|
|
26
|
+
"scripts": {
|
|
27
|
+
"preflight": [
|
|
28
|
+
{ "script": "watchStalePrsFlow" }
|
|
29
|
+
],
|
|
30
|
+
"postflight": []
|
|
31
|
+
}
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kody-ade/kody-engine",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "kody2 — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|