agent-kitchen 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,252 +0,0 @@
1
- /**
2
- * @file What `apply --commit` does with git: before any step, it checks that the repo has no
3
- * changes (the repo's worktrees under it do not count) and is on its base branch; after the
4
- * steps, it commits what changed where the person chose. A new branch is made by passing
5
- * through it for the length of the command: `git switch -c`, the commit there (so a hook that
6
- * forbids commits on the base does not stop it), the push and the PR if asked, and back to
7
- * the base, which never moves. git runs its hooks; nothing here skips them.
8
- */
9
- import { spawnSync } from "node:child_process";
10
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
11
- import { tmpdir } from "node:os";
12
- import { join, relative } from "node:path";
13
- import { COMMIT_TITLE as TITLE, prBody } from "./commit-texts.js";
14
- import { KitError } from "./output.js";
15
- import { FLOW_CONFIG } from "./piece.js";
16
- import { dependencyVersion, readJson } from "./repo.js";
17
- import { captureNpxWithout, isRelease, npmLatest } from "./tools.js";
18
- /** github-rest on npm, which opens the PR (REST, never gh's GraphQL). */
19
- const GITHUB_REST = "@ramon-sg/github-rest";
20
- /**
21
- * Runs git in the repo.
22
- * @param root - The repo.
23
- * @param args - The arguments.
24
- * @param inherit - True to show its output on the terminal, for a push that may ask for credentials.
25
- * @returns How it went.
26
- */
27
- function git(root, args, inherit = false) {
28
- const result = spawnSync("git", args, { cwd: root, encoding: "utf8", stdio: inherit ? "inherit" : "pipe" });
29
- return { ok: result.status === 0, out: (result.stdout ?? "").trimEnd(), err: (result.stderr ?? "").trim() };
30
- }
31
- /**
32
- * The base branch, as issue-flow takes it: `baseBranch` of `.agents/flow.json`, or origin's
33
- * default as git knows it, or `main`.
34
- * @param root - The repo.
35
- * @returns The branch.
36
- * @example baseBranch(root) // "main"
37
- */
38
- export function baseBranch(root) {
39
- const flow = readJson(root, FLOW_CONFIG);
40
- if (typeof flow?.baseBranch === "string" && flow.baseBranch !== "")
41
- return flow.baseBranch;
42
- const head = git(root, ["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"]);
43
- return head.ok && head.out.startsWith("origin/") ? head.out.slice("origin/".length) : "main";
44
- }
45
- /**
46
- * The repo's other worktrees that live inside it, relative to it.
47
- * @param root - The repo.
48
- * @returns Their folders, like `.claude/worktrees/feat+1-x`.
49
- */
50
- function innerWorktrees(root) {
51
- const list = git(root, ["worktree", "list", "--porcelain"]);
52
- if (!list.ok)
53
- return [];
54
- return list.out
55
- .split("\n")
56
- .filter((line) => line.startsWith("worktree "))
57
- .map((line) => relative(root, line.slice("worktree ".length)))
58
- .filter((path) => path !== "" && !path.startsWith(".."));
59
- }
60
- /**
61
- * The files that differ from HEAD, tracked or not, one by one, without the repo's worktrees.
62
- * @param root - The repo.
63
- * @returns Their paths.
64
- * @example changedPaths(root) // ["package.json", "package-lock.json"]
65
- */
66
- export function changedPaths(root) {
67
- const status = git(root, ["status", "--porcelain", "-uall", "-z"]);
68
- if (!status.ok)
69
- throw new KitError(`no pude leer git status: ${status.err}`);
70
- const worktrees = innerWorktrees(root);
71
- const entries = status.out.split("\0").filter((entry) => entry !== "");
72
- const paths = [];
73
- for (let index = 0; index < entries.length; index += 1) {
74
- const entry = entries[index] ?? "";
75
- if (/[RC]/.test(entry.slice(0, 2)))
76
- index += 1;
77
- const path = entry.slice(3).replace(/\/$/, "");
78
- if (!worktrees.some((tree) => path === tree || path.startsWith(`${tree}/`)))
79
- paths.push(path);
80
- }
81
- return paths;
82
- }
83
- /**
84
- * Checks, before any step, that the repo has no changes and is on its base branch.
85
- * @param root - The repo.
86
- * @param base - The base branch.
87
- * @throws KitError saying what is wrong and what to do.
88
- */
89
- export function checkReady(root, base) {
90
- const branch = git(root, ["branch", "--show-current"]).out;
91
- if (branch === "")
92
- throw new KitError(`--commit necesita estar en ${base}, y HEAD no está en ninguna rama.`);
93
- if (branch !== base)
94
- throw new KitError(`--commit necesita estar en ${base}, la rama base, y estás en ${branch}.`);
95
- const paths = changedPaths(root);
96
- if (paths.length > 0)
97
- throw new KitError(`--commit necesita el repo sin cambios, y hay cambios en: ${paths.join(", ")}. Commitéalos o guárdalos aparte y corre otra vez.`);
98
- }
99
- /**
100
- * The branch for today's changes: a new name, also against origin's branches git knows of.
101
- * @param root - The repo.
102
- * @param date - Today.
103
- * @returns `chore/agent-kitchen-apply-AAAA-MM-DD`, or with `-2`, `-3`… if taken.
104
- * @example branchName(root, new Date()) // "chore/agent-kitchen-apply-2026-11-01"
105
- */
106
- export function branchName(root, date) {
107
- const pad = (n) => String(n).padStart(2, "0");
108
- const name = `chore/agent-kitchen-apply-${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
109
- const taken = (candidate) => git(root, ["rev-parse", "-q", "--verify", `refs/heads/${candidate}`]).ok || git(root, ["rev-parse", "-q", "--verify", `refs/remotes/origin/${candidate}`]).ok;
110
- if (!taken(name))
111
- return name;
112
- let n = 2;
113
- while (taken(`${name}-${n}`))
114
- n += 1;
115
- return `${name}-${n}`;
116
- }
117
- /**
118
- * Stages the files shown to the person, taken literally, and commits them, with the hooks.
119
- * @param root - The repo.
120
- * @param paths - The files.
121
- * @param steps - The steps that ran, for the body.
122
- * @returns How git went.
123
- */
124
- function commit(root, paths, steps) {
125
- const add = git(root, ["--literal-pathspecs", "add", "-A", "--", ...paths]);
126
- if (!add.ok)
127
- return add;
128
- return git(root, ["commit", "-q", "-m", TITLE, "-m", steps.map((step) => `- ${step}`).join("\n")]);
129
- }
130
- /**
131
- * The version of github-rest to open the PR with: the repo's exact one, or npm's latest.
132
- * @param root - The repo.
133
- * @returns The version, or null if neither is known.
134
- */
135
- async function githubRestVersion(root) {
136
- const own = dependencyVersion(readJson(root, "package.json"), GITHUB_REST);
137
- if (own !== null && isRelease(own))
138
- return own;
139
- return npmLatest(GITHUB_REST);
140
- }
141
- /**
142
- * Opens the PR with github-rest, without GH_REPO, so the repo is origin's.
143
- * @param root - The repo, on the branch.
144
- * @param branch - The branch.
145
- * @param base - The base.
146
- * @param steps - The steps, for the body.
147
- * @returns The PR's URL, or why it failed.
148
- */
149
- async function openPr(root, branch, base, steps) {
150
- const version = await githubRestVersion(root);
151
- if (version === null)
152
- return { error: `no sé qué versión de ${GITHUB_REST} usar.` };
153
- const dir = mkdtempSync(join(tmpdir(), "agent-kitchen-pr-"));
154
- const bodyFile = join(dir, "body.md");
155
- writeFileSync(bodyFile, prBody(steps));
156
- try {
157
- const result = await captureNpxWithout(root, [`${GITHUB_REST}@${version}`, "pr", "create", "--head", branch, "--base", base, "--title", TITLE, "--body-file", bodyFile], ["GH_REPO"]);
158
- const url = result.out.trim().split("\n").at(-1) ?? "";
159
- return result.status === 0 && url !== "" ? { url } : { error: `${result.err}${result.out}`.trim() || "github-rest falló sin decir por qué." };
160
- }
161
- finally {
162
- rmSync(dir, { recursive: true, force: true });
163
- }
164
- }
165
- /**
166
- * What the person can run to open the PR by hand.
167
- * @param branch - The branch.
168
- * @param base - The base.
169
- * @returns The command.
170
- */
171
- function prCommand(branch, base) {
172
- return `npx @ramon-sg/github-rest pr create --head ${branch} --base ${base} --title "${TITLE}" --body "..."`;
173
- }
174
- /**
175
- * Pushes the branch and opens its PR, saying what to run if either fails.
176
- * @param root - The repo, on the branch.
177
- * @param branch - The branch.
178
- * @param base - The base.
179
- * @param steps - The steps, for the body.
180
- * @param lines - Where to add what happened.
181
- * @returns Whether both worked.
182
- */
183
- async function pushAndOpen(root, branch, base, steps, lines) {
184
- const pushed = git(root, ["push", "-u", "origin", branch], true);
185
- if (!pushed.ok) {
186
- lines.push(`No pude subir la rama. Falta: git push -u origin ${branch}, y luego ${prCommand(branch, base)}`);
187
- return false;
188
- }
189
- const pr = await openPr(root, branch, base, steps);
190
- if ("url" in pr) {
191
- lines.push(`PR: ${pr.url}`);
192
- return true;
193
- }
194
- lines.push(`La rama está en origin, pero no pude abrir el PR: ${pr.error}`, `Falta: ${prCommand(branch, base)}`);
195
- return false;
196
- }
197
- /**
198
- * Does what the person chose with the changes.
199
- * @param root - The repo, on the base, with the changes of the run.
200
- * @param choice - What to do.
201
- * @param options - The base, the new branch's name, the steps that ran, the files to commit (the ones shown), the package manager and whether package.json changed.
202
- * @returns The lines to show and whether it all worked.
203
- * @example await finish(root, "pr", { base: "main", branch: "chore/agent-kitchen-apply-2026-11-01", steps, manager: "npm" })
204
- */
205
- export async function finish(root, choice, options) {
206
- const { base, branch, steps, paths } = options;
207
- if (choice === "leave")
208
- return { ok: true, lines: ["Falta: revisa el diff y haz commit."] };
209
- if (choice === "base") {
210
- const done = commit(root, paths, steps);
211
- if (!done.ok)
212
- return { ok: false, lines: [`No pude commitear en ${base}; los cambios quedan sin commit.`, done.err || done.out] };
213
- return { ok: true, lines: [`Commit en ${base}. Falta: git push cuando quieras.`] };
214
- }
215
- const moved = git(root, ["switch", "-q", "-c", branch]);
216
- if (!moved.ok)
217
- return { ok: false, lines: [`No pude crear la rama ${branch}; los cambios quedan sin commit.`, moved.err] };
218
- const lines = [];
219
- let ok = true;
220
- let committed = false;
221
- try {
222
- const done = commit(root, paths, steps);
223
- if (!done.ok) {
224
- ok = false;
225
- lines.push("No pude commitear.", done.err || done.out);
226
- }
227
- else {
228
- committed = true;
229
- lines.push(`Commit en la rama ${branch}.`);
230
- if (choice === "pr")
231
- ok = await pushAndOpen(root, branch, base, steps, lines);
232
- }
233
- }
234
- finally {
235
- const back = git(root, ["switch", "-q", base]);
236
- if (!back.ok) {
237
- ok = false;
238
- lines.push(`No pude volver a ${base}: ${back.err}. Sigues en ${branch}.`);
239
- }
240
- else if (!committed) {
241
- git(root, ["branch", "-q", "-d", branch]);
242
- lines.push(`Vuelves a ${base} con los cambios sin commit.`);
243
- }
244
- else
245
- lines.push(`Vuelves a ${base}, sin los cambios: están en ${branch}.`);
246
- }
247
- if (!committed)
248
- return { ok: false, lines };
249
- if (options.packageChanged === true)
250
- lines.push(`node_modules quedó con lo de la rama; si sigues en ${base}, corre '${options.manager} install'.`);
251
- return { ok, lines };
252
- }