agent-kitchen 0.8.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.
- package/README.md +10 -7
- package/dist/cli.js +13 -9
- package/dist/commands/sync.d.ts +6 -3
- package/dist/commands/sync.js +116 -49
- package/dist/commit-texts.js +1 -1
- package/dist/menu.d.ts +6 -8
- package/dist/menu.js +13 -18
- package/dist/pr-clone.d.ts +68 -0
- package/dist/pr-clone.js +254 -0
- package/dist/progress.d.ts +4 -5
- package/dist/progress.js +5 -6
- package/dist/texts/catalog.d.ts +9 -1
- package/dist/texts/es.js +7 -0
- package/package.json +1 -1
- package/dist/git-finish.d.ts +0 -53
- package/dist/git-finish.js +0 -252
package/README.md
CHANGED
|
@@ -48,7 +48,8 @@ Si npm o GitHub no responden, la pieza se muestra pero no se ofrece actualizarla
|
|
|
48
48
|
npx agent-kitchen apply # pregunta, en una terminal (Enter: todo lo sugerido)
|
|
49
49
|
npx agent-kitchen apply --dry-run # solo muestra qué haría
|
|
50
50
|
npx agent-kitchen apply --yes # hace lo sugerido sin preguntar
|
|
51
|
-
npx agent-kitchen apply --
|
|
51
|
+
npx agent-kitchen apply --pr # en una copia de origin, y abre un PR en draft
|
|
52
|
+
npx agent-kitchen apply --here # en tu copia, sin commit
|
|
52
53
|
npx agent-kitchen apply --verbose # muestra la salida de cada herramienta
|
|
53
54
|
npx agent-kitchen apply --skip claude-cloud # deja fuera una pieza (o varias, con comas)
|
|
54
55
|
npx agent-kitchen apply --pieces # cambia qué piezas usa el repo (solo en una terminal)
|
|
@@ -59,14 +60,16 @@ npx agent-kitchen doctor # revisa cada pieza con su propio
|
|
|
59
60
|
|
|
60
61
|
- **Sin terminal y sin `--yes`** (por ejemplo, desde un agente), solo muestra lo que ofrecería.
|
|
61
62
|
- **Corre en el checkout principal,** no en un worktree, y desde tu terminal. Dentro de una sesión de Claude, el sandbox de `claude-repo-guard` no deja escribir `.claude/settings.json`.
|
|
62
|
-
|
|
63
|
+
**Dónde trabaja.** En una terminal, sin `--pr` ni `--here`, lo primero que pregunta es dónde aplicar los cambios. Con `--yes`, sin terminal o sin remoto `origin`, trabaja en tu copia.
|
|
63
64
|
|
|
64
|
-
**`--
|
|
65
|
+
**`--pr`** deja los cambios en un PR sin tocar tu copia:
|
|
65
66
|
|
|
66
|
-
- **
|
|
67
|
-
- **Al terminar,**
|
|
68
|
-
- **
|
|
69
|
-
- **
|
|
67
|
+
- **Parte de origin, no de tu copia.** Hace `git fetch` y clona el repo en una carpeta temporal en `origin/<base>`: `baseBranch` de `.agents/flow.json`, si no la rama por defecto de `origin`, si no `main`. Da igual si tu copia está atrás, en otra rama o con cambios: ni sus archivos ni su `node_modules` cambian. Es un clon y no un worktree porque `issue-flow apply` no corre en un worktree; el clon comparte los objetos del repo, así que no copia el historial.
|
|
68
|
+
- **Al terminar,** instala las dependencias en el clon (los hooks del repo pueden necesitarlas) y hace el commit con los hooks, el nombre y la firma de tu repo. El lockfile entra solo si cambió `package.json`.
|
|
69
|
+
- **Sube la rama directo a origin.** Tu repo empuja el commit a `chore/agent-kitchen-apply-<fecha>` sin crear una rama local, y abre el PR en draft con `@ramon-sg/github-rest`. Tras el merge, `git fetch --prune` limpia la referencia `origin/chore/…`.
|
|
70
|
+
- **Si algo falla antes del push** (un paso, el install o el commit), no sube nada y deja el clon para revisarlo; la salida dice dónde está. Si el push funciona y el PR falla, la salida dice qué correr.
|
|
71
|
+
|
|
72
|
+
**`--here`** trabaja en tu copia, como `ng update` o `copier update`. Avisa si tu copia tiene cambios sin commit o si tu rama está atrás de origin según el último fetch. Si los pasos cambiaron `package.json`, corre el `install` de tu gestor de paquetes al final. Te queda revisar el diff y hacer commit.
|
|
70
73
|
|
|
71
74
|
**`doctor`** corre el doctor de cada herramienta en la versión del repo: `issue-flow doctor --json`, `skills-manager check` y `claude-repo-guard doctor --json`. Además revisa que `skills-manager` esté al menos en 1.2.0, que las skills del flujo estén en la tag de su CLI, qué labels del flujo le faltan a GitHub (con `·`), que `.claude/cloud.json` fije una versión exacta y, en macOS, `weakerNetworkIsolation`. En un repo con `package.json`, también la línea de `AGENTS.md` y, si hay `CLAUDE.md`, su `@AGENTS.md`. Lo que no está instalado o tiene una versión nueva solo se anota con `·`. Sale con 1 si algo falla (`✗`).
|
|
72
75
|
|
package/dist/cli.js
CHANGED
|
@@ -21,18 +21,20 @@ const PACKAGE = "agent-kitchen";
|
|
|
21
21
|
const HANDED_OVER = "AGENT_KITCHEN_HANDED_OVER";
|
|
22
22
|
/** Usage text. */
|
|
23
23
|
const USAGE = `Uso:
|
|
24
|
-
agent-kitchen apply [--yes | --dry-run] [--
|
|
25
|
-
agent-kitchen apply --pieces [--
|
|
24
|
+
agent-kitchen apply [--yes | --dry-run] [--pr | --here] [--verbose] [--skip <pieza>,...]
|
|
25
|
+
agent-kitchen apply --pieces [--pr | --here]
|
|
26
26
|
agent-kitchen doctor
|
|
27
27
|
|
|
28
28
|
agent-kitchen revisa cada pieza de las herramientas de agentes del repo (${pieceNames().join(", ")}),
|
|
29
29
|
muestra qué tiene el repo y la última versión de cada una, y apply ofrece instalar lo que falta
|
|
30
30
|
y actualizar lo que está atrasado. En una terminal pregunta una vez: hacer todo lo sugerido,
|
|
31
31
|
o elegir los pasos y las skills. --yes hace lo sugerido sin preguntar; sin terminal solo lo
|
|
32
|
-
muestra. --verbose muestra la salida de cada herramienta.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
muestra. --verbose muestra la salida de cada herramienta.
|
|
33
|
+
--pr trabaja en una copia temporal de origin/<base> recién bajada, sin tocar tu copia: instala
|
|
34
|
+
las dependencias, hace el commit, sube una rama nueva a origin y abre un PR en draft. --here
|
|
35
|
+
trabaja en tu copia y deja los cambios sin commit; si los pasos cambiaron package.json, corre
|
|
36
|
+
el install. En una terminal, sin ninguna de las dos, pregunta; con --yes o sin terminal, usa
|
|
37
|
+
--here. apply --pieces elige qué piezas usa el repo y lo guarda en .agents/kitchen.json
|
|
36
38
|
(en una terminal, sin --yes ni --dry-run). Córrelo en la copia principal del
|
|
37
39
|
repo, desde tu terminal. Sin subcomando hace lo mismo que apply, por ahora.
|
|
38
40
|
|
|
@@ -82,7 +84,7 @@ async function main(argv) {
|
|
|
82
84
|
const { values, positionals } = parseArgs({
|
|
83
85
|
args: argv,
|
|
84
86
|
allowPositionals: true,
|
|
85
|
-
options: { yes: { type: "boolean" }, "dry-run": { type: "boolean" }, commit: { type: "boolean" }, verbose: { type: "boolean", short: "v" }, skip: { type: "string" }, pieces: { type: "boolean" }, help: { type: "boolean", short: "h" }, version: { type: "boolean" } },
|
|
87
|
+
options: { yes: { type: "boolean" }, "dry-run": { type: "boolean" }, commit: { type: "boolean" }, pr: { type: "boolean" }, here: { type: "boolean" }, verbose: { type: "boolean", short: "v" }, skip: { type: "string" }, pieces: { type: "boolean" }, help: { type: "boolean", short: "h" }, version: { type: "boolean" } },
|
|
86
88
|
});
|
|
87
89
|
if (values.help === true) {
|
|
88
90
|
out.stdout(`${USAGE}\n`);
|
|
@@ -100,14 +102,16 @@ async function main(argv) {
|
|
|
100
102
|
if (rest.length > 0 || (command !== undefined && command !== "doctor" && command !== "apply"))
|
|
101
103
|
throw new UsageError(`no existe el comando ${positionals.join(" ")}.`);
|
|
102
104
|
if (command === "doctor") {
|
|
103
|
-
if (values.yes !== undefined || values["dry-run"] !== undefined || values.skip !== undefined || values.verbose !== undefined || values.commit !== undefined || values.pieces !== undefined)
|
|
105
|
+
if (values.yes !== undefined || values["dry-run"] !== undefined || values.skip !== undefined || values.verbose !== undefined || values.commit !== undefined || values.pr !== undefined || values.here !== undefined || values.pieces !== undefined)
|
|
104
106
|
throw new UsageError("doctor no lleva opciones.");
|
|
105
107
|
return doctor(process.cwd(), out);
|
|
106
108
|
}
|
|
109
|
+
if (values.commit !== undefined)
|
|
110
|
+
throw new UsageError("--commit ya no existe: usa --pr para abrir un PR desde una copia de origin, o --here para dejar los cambios sin commit en tu copia.");
|
|
107
111
|
if (command === undefined)
|
|
108
112
|
out.stderr("agent-kitchen: usa 'agent-kitchen apply'; sin subcomando dejará de instalar en una versión mayor.\n");
|
|
109
113
|
const skip = (values.skip ?? "").split(",").map((name) => name.trim()).filter((name) => name !== "");
|
|
110
|
-
return sync({ cwd: process.cwd(), yes: values.yes ?? false, dryRun: values["dry-run"] ?? false, verbose: values.verbose ?? false, skip,
|
|
114
|
+
return sync({ cwd: process.cwd(), yes: values.yes ?? false, dryRun: values["dry-run"] ?? false, verbose: values.verbose ?? false, skip, pr: values.pr ?? false, here: values.here ?? false, pieces: values.pieces ?? false }, out);
|
|
111
115
|
}
|
|
112
116
|
try {
|
|
113
117
|
process.exitCode = await main(process.argv.slice(2));
|
package/dist/commands/sync.d.ts
CHANGED
|
@@ -7,7 +7,10 @@ export interface SyncOptions {
|
|
|
7
7
|
dryRun: boolean;
|
|
8
8
|
verbose: boolean;
|
|
9
9
|
skip: string[];
|
|
10
|
-
|
|
10
|
+
/** Work in a clone of origin and open a PR. */
|
|
11
|
+
pr: boolean;
|
|
12
|
+
/** Work in the checkout, leaving the changes uncommitted. */
|
|
13
|
+
here: boolean;
|
|
11
14
|
/** Ask which pieces the repo uses, save the answer, then go on as usual. */
|
|
12
15
|
pieces: boolean;
|
|
13
16
|
}
|
|
@@ -26,7 +29,7 @@ export declare function pieceNames(): string[];
|
|
|
26
29
|
*/
|
|
27
30
|
export declare function noStepsMessage(why: "leave" | "none", wrote: boolean): string;
|
|
28
31
|
/**
|
|
29
|
-
* What the commit of `--
|
|
32
|
+
* What the commit of `--pr` lists: the steps that worked, and the decisions file if this
|
|
30
33
|
* run wrote it, in the commit's texts (not the interface's).
|
|
31
34
|
* @param done - The steps that worked.
|
|
32
35
|
* @param wrote - Whether the decisions file was written in this run.
|
|
@@ -40,6 +43,6 @@ export declare function commitLabels(done: Pick<Step, "text">[], wrote: boolean)
|
|
|
40
43
|
* @param out - Where to write and ask.
|
|
41
44
|
* @returns 0, or 1 if a step failed or the person cancelled.
|
|
42
45
|
* @throws KitError outside the main checkout or with a broken file; UsageError for an unknown piece.
|
|
43
|
-
* @example await sync({ cwd, yes: false, dryRun: true, verbose: false, skip: [],
|
|
46
|
+
* @example await sync({ cwd, yes: false, dryRun: true, verbose: false, skip: [], pr: false, here: false, pieces: false }, out)
|
|
44
47
|
*/
|
|
45
48
|
export declare function sync(options: SyncOptions, out: Output): Promise<number>;
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file `agent-kitchen apply [--yes] [--dry-run] [--
|
|
3
|
-
* that changes the repo (and, for now, also the bare `agent-kitchen`).
|
|
4
|
-
*
|
|
2
|
+
* @file `agent-kitchen apply [--yes] [--dry-run] [--pr | --here] [--verbose] [--skip <piece>,...]`:
|
|
3
|
+
* the one command that changes the repo (and, for now, also the bare `agent-kitchen`).
|
|
4
|
+
* First it settles where to work: `--pr` in a clone of `origin/<base>` that ends in a PR in
|
|
5
|
+
* draft (see pr-clone.ts), `--here` in the person's checkout, leaving the changes uncommitted.
|
|
6
|
+
* In a terminal, without either, it asks; with `--yes` or without a terminal, or without an
|
|
7
|
+
* origin, it works here. Here it warns if the checkout has changes or is behind its upstream.
|
|
8
|
+
* Then it looks at each piece (the version of skills-manager, the issue flow, claude-cloud,
|
|
5
9
|
* claude-repo-guard, the skills, the rule in AGENTS.md) with
|
|
6
10
|
* every lookup at once, shows what the repo has, and offers the steps that install what is
|
|
7
11
|
* missing or update what is behind. In a terminal it asks first, all in one go: everything
|
|
@@ -11,11 +15,10 @@
|
|
|
11
15
|
* flow's version), one line each, with the
|
|
12
16
|
* tools' output only when one fails; one that fails does not stop the rest. With `--yes`
|
|
13
17
|
* it does what is suggested without asking (and keeps the repo's own skills); without a
|
|
14
|
-
* terminal it only shows the steps.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* the steps then show as plain lines, since a spinner before a question leaves it deaf.
|
|
18
|
+
* terminal it only shows the steps. Here, it installs at the end if the steps changed
|
|
19
|
+
* package.json. In the clone it always installs (the repo's hooks may need node_modules),
|
|
20
|
+
* commits, pushes and opens the PR, also when the only change is the decisions file below;
|
|
21
|
+
* if a step, the install or the commit fails, nothing is pushed and the clone is kept.
|
|
19
22
|
* What the repo decided of each piece, in `.agents/kitchen.json`, is respected: a piece set to
|
|
20
23
|
* false is left out as with `--skip`, and one with no decision is marked as new. Without
|
|
21
24
|
* asking (`--yes` or no terminal), a new piece that is not installed is left out for the run.
|
|
@@ -25,8 +28,8 @@
|
|
|
25
28
|
* uses, writes all of them to the file, and then goes on as usual.
|
|
26
29
|
*/
|
|
27
30
|
import { DECIDABLE, decide, DECISIONS, off, readDecisions, undecided, writeDecisions } from "../decisions.js";
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
31
|
+
import { behindUpstream, baseBranch, changedPaths, cloneFromOrigin, originUrl, publish, removeClone } from "../pr-clone.js";
|
|
32
|
+
import { askPath, askPieces, askPlace, askReplace, askSkills, askSteps } from "../menu.js";
|
|
30
33
|
import { KitError, UsageError, warn } from "../output.js";
|
|
31
34
|
import { cloudStatus, NAME as CLOUD } from "../pieces/cloud.js";
|
|
32
35
|
import { guardStatus, NAME as GUARD } from "../pieces/guard.js";
|
|
@@ -96,7 +99,7 @@ function printStatus(statuses, decidedOff, out) {
|
|
|
96
99
|
* @param saved - What was read, or null if there is no file.
|
|
97
100
|
* @param statuses - What each piece found.
|
|
98
101
|
* @param chosen - The steps the person picked.
|
|
99
|
-
* @param commits - Whether
|
|
102
|
+
* @param commits - Whether the run ends in a PR, so the file need not be committed by hand.
|
|
100
103
|
* @param out - Where to write.
|
|
101
104
|
* @returns True if the file was written.
|
|
102
105
|
*/
|
|
@@ -122,14 +125,14 @@ export function noStepsMessage(why, wrote) {
|
|
|
122
125
|
}
|
|
123
126
|
/**
|
|
124
127
|
* The line that says the decisions were saved.
|
|
125
|
-
* @param commits - Whether
|
|
128
|
+
* @param commits - Whether the run ends in a PR, so the file need not be committed by hand.
|
|
126
129
|
* @returns The line.
|
|
127
130
|
*/
|
|
128
131
|
function savedMessage(commits) {
|
|
129
132
|
return `${ui.saved(DECISIONS, commits)}\n`;
|
|
130
133
|
}
|
|
131
134
|
/**
|
|
132
|
-
* What the commit of `--
|
|
135
|
+
* What the commit of `--pr` lists: the steps that worked, and the decisions file if this
|
|
133
136
|
* run wrote it, in the commit's texts (not the interface's).
|
|
134
137
|
* @param done - The steps that worked.
|
|
135
138
|
* @param wrote - Whether the decisions file was written in this run.
|
|
@@ -144,7 +147,7 @@ export function commitLabels(done, wrote) {
|
|
|
144
147
|
* Asks which pieces the repo uses and writes the answer for every decidable piece.
|
|
145
148
|
* @param root - The repo.
|
|
146
149
|
* @param saved - What was read, or null if there is no file.
|
|
147
|
-
* @param commits - Whether
|
|
150
|
+
* @param commits - Whether the run ends in a PR.
|
|
148
151
|
* @param out - Where to write.
|
|
149
152
|
* @returns False if the person cancelled.
|
|
150
153
|
*/
|
|
@@ -279,13 +282,45 @@ async function runStep(root, step, progress, out) {
|
|
|
279
282
|
out.stderr(failure);
|
|
280
283
|
return ok;
|
|
281
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Settles where to work: the flag, or the person's answer in a terminal, or the checkout.
|
|
287
|
+
* @param root - The repo.
|
|
288
|
+
* @param options - The options.
|
|
289
|
+
* @param out - Where to write and ask.
|
|
290
|
+
* @returns The place, or null if the person cancelled.
|
|
291
|
+
* @throws KitError with `--pr` and no origin.
|
|
292
|
+
*/
|
|
293
|
+
async function choosePlace(root, options, out) {
|
|
294
|
+
if (options.pr) {
|
|
295
|
+
if (originUrl(root) === null)
|
|
296
|
+
throw new KitError("--pr necesita un remoto origin, y este repo no tiene. Usa --here.");
|
|
297
|
+
return "pr";
|
|
298
|
+
}
|
|
299
|
+
if (options.here || options.yes || options.dryRun || !out.interactive || originUrl(root) === null)
|
|
300
|
+
return "here";
|
|
301
|
+
return askPlace(baseBranch(root));
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Warns about what the checkout already has before working on it: changes that will mix with
|
|
305
|
+
* the run's, or a branch behind its upstream.
|
|
306
|
+
* @param root - The repo.
|
|
307
|
+
* @param out - Where to write.
|
|
308
|
+
*/
|
|
309
|
+
function warnHere(root, out) {
|
|
310
|
+
const changed = changedPaths(root);
|
|
311
|
+
if (changed.length > 0)
|
|
312
|
+
warn(out, `tu copia tiene cambios sin commit (${changed.length === 1 ? changed[0] : `${changed.length} archivos`}); los de apply se mezclarán con ellos.`);
|
|
313
|
+
const behind = behindUpstream(root);
|
|
314
|
+
if (behind > 0)
|
|
315
|
+
warn(out, `tu rama está ${behind} ${behind === 1 ? "commit" : "commits"} atrás de origin, según el último fetch; corre git pull antes, o usa --pr.`);
|
|
316
|
+
}
|
|
282
317
|
/**
|
|
283
318
|
* Runs the command; see the file's header.
|
|
284
319
|
* @param options - The options.
|
|
285
320
|
* @param out - Where to write and ask.
|
|
286
321
|
* @returns 0, or 1 if a step failed or the person cancelled.
|
|
287
322
|
* @throws KitError outside the main checkout or with a broken file; UsageError for an unknown piece.
|
|
288
|
-
* @example await sync({ cwd, yes: false, dryRun: true, verbose: false, skip: [],
|
|
323
|
+
* @example await sync({ cwd, yes: false, dryRun: true, verbose: false, skip: [], pr: false, here: false, pieces: false }, out)
|
|
289
324
|
*/
|
|
290
325
|
export async function sync(options, out) {
|
|
291
326
|
const unknown = options.skip.find((name) => !pieceNames().includes(name));
|
|
@@ -293,21 +328,51 @@ export async function sync(options, out) {
|
|
|
293
328
|
throw new UsageError(`no existe la pieza ${unknown}; son ${pieceNames().join(", ")}.`);
|
|
294
329
|
if (options.yes && options.dryRun)
|
|
295
330
|
throw new UsageError("usa --yes o --dry-run, no los dos.");
|
|
331
|
+
if (options.pr && options.here)
|
|
332
|
+
throw new UsageError("usa --pr o --here, no los dos.");
|
|
296
333
|
if (options.pieces && (options.yes || options.dryRun))
|
|
297
334
|
throw new UsageError("usa --pieces solo, sin --yes ni --dry-run.");
|
|
298
335
|
if (options.pieces && !out.interactive)
|
|
299
336
|
throw new UsageError("--pieces pregunta qué piezas usa el repo; córrelo en una terminal.");
|
|
300
337
|
const root = mainCheckout(options.cwd, "agent-kitchen apply");
|
|
301
|
-
const base = options.commit ? baseBranch(root) : null;
|
|
302
|
-
if (base !== null)
|
|
303
|
-
checkReady(root, base);
|
|
304
338
|
setVerbose(options.verbose);
|
|
339
|
+
const place = await choosePlace(root, options, out);
|
|
340
|
+
if (place === null) {
|
|
341
|
+
out.stdout("No se cambió nada.\n");
|
|
342
|
+
return 1;
|
|
343
|
+
}
|
|
344
|
+
if (place === "here") {
|
|
345
|
+
if (!options.dryRun)
|
|
346
|
+
warnHere(root, out);
|
|
347
|
+
return applyIn({ kind: "here", root }, options, out);
|
|
348
|
+
}
|
|
349
|
+
const base = baseBranch(root);
|
|
350
|
+
out.stdout(`${ui.cloning(base)}\n`);
|
|
351
|
+
const work = { kind: "pr", root: cloneFromOrigin(root, base), repo: root, base, keep: false };
|
|
352
|
+
try {
|
|
353
|
+
return await applyIn(work, options, out);
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
if (!work.keep)
|
|
357
|
+
removeClone(work.root);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Looks at the pieces, asks, runs the steps and ends as the place says.
|
|
362
|
+
* @param work - Where to work.
|
|
363
|
+
* @param options - The options.
|
|
364
|
+
* @param out - Where to write and ask.
|
|
365
|
+
* @returns 0, or 1 if a step failed or the person cancelled.
|
|
366
|
+
*/
|
|
367
|
+
async function applyIn(work, options, out) {
|
|
368
|
+
const { root } = work;
|
|
369
|
+
const pr = work.kind === "pr";
|
|
305
370
|
let saved = readDecisions(root);
|
|
306
371
|
let wrote = false;
|
|
307
372
|
for (const warning of saved?.warnings ?? [])
|
|
308
373
|
warn(out, warning);
|
|
309
374
|
if (options.pieces) {
|
|
310
|
-
if (!(await choosePieces(root, saved,
|
|
375
|
+
if (!(await choosePieces(root, saved, pr, out))) {
|
|
311
376
|
out.stdout("No se cambió nada.\n");
|
|
312
377
|
return 0;
|
|
313
378
|
}
|
|
@@ -338,18 +403,18 @@ export async function sync(options, out) {
|
|
|
338
403
|
}
|
|
339
404
|
printStatus(statuses, off(saved).filter((name) => !options.skip.includes(name)), out);
|
|
340
405
|
const steps = statuses.flatMap((status) => status.steps);
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const endWithoutSteps = async () => (
|
|
406
|
+
const progress = progressFor(out, options.verbose);
|
|
407
|
+
// In a clone, a run that only wrote the decisions file still ends in a PR.
|
|
408
|
+
const endWithoutSteps = async () => (work.kind === "pr" && wrote ? openPr(work, commitLabels([], true), [], progress, out) : 0);
|
|
344
409
|
if (steps.length === 0) {
|
|
345
410
|
if (saves)
|
|
346
|
-
wrote = saveDecisions(root, saved, statuses, [],
|
|
411
|
+
wrote = saveDecisions(root, saved, statuses, [], pr, out) || wrote;
|
|
347
412
|
out.stdout(`${ui.upToDate}\n`);
|
|
348
413
|
return endWithoutSteps();
|
|
349
414
|
}
|
|
350
415
|
const chosen = await chooseSteps(root, steps, options, out);
|
|
351
416
|
if (saves && Array.isArray(chosen))
|
|
352
|
-
wrote = saveDecisions(root, saved, statuses, chosen,
|
|
417
|
+
wrote = saveDecisions(root, saved, statuses, chosen, pr, out) || wrote;
|
|
353
418
|
if (chosen === "leave") {
|
|
354
419
|
out.stdout(noStepsMessage("leave", wrote));
|
|
355
420
|
return endWithoutSteps();
|
|
@@ -375,8 +440,8 @@ export async function sync(options, out) {
|
|
|
375
440
|
failedSteps.push(step);
|
|
376
441
|
}
|
|
377
442
|
const failed = failedSteps.map(stepLabel);
|
|
378
|
-
if (
|
|
379
|
-
return
|
|
443
|
+
if (work.kind === "pr")
|
|
444
|
+
return openPr(work, commitLabels(chosen.filter((step) => !failedSteps.includes(step)), wrote), failed, progress, out);
|
|
380
445
|
const installFailed = packageText(root) !== pkgBefore ? !(await install(root, progress, out)) : false;
|
|
381
446
|
if (installFailed)
|
|
382
447
|
failed.push(phrases({ kind: "install-deps", manager: packageManager(root) }).menu);
|
|
@@ -400,40 +465,42 @@ async function install(root, progress, out) {
|
|
|
400
465
|
return installed.ok;
|
|
401
466
|
}
|
|
402
467
|
/**
|
|
403
|
-
* The end of
|
|
404
|
-
*
|
|
405
|
-
* @param
|
|
406
|
-
* @param base - The base branch.
|
|
468
|
+
* The end of a run in a clone: the install, the changes, the commit, the push and the PR.
|
|
469
|
+
* Nothing is pushed if a step, the install or the commit failed; the clone is kept then.
|
|
470
|
+
* @param work - The clone.
|
|
407
471
|
* @param done - What the commit lists: the steps that worked, and the decisions file if written.
|
|
408
472
|
* @param failed - The steps that failed, as the interface names them.
|
|
409
473
|
* @param progress - How progress is shown, for the install.
|
|
410
|
-
* @param
|
|
411
|
-
* @
|
|
412
|
-
* @returns 0, or 1 if a step, the install or git failed.
|
|
474
|
+
* @param out - Where to write.
|
|
475
|
+
* @returns 0, or 1 if a step, the install, git or the PR failed.
|
|
413
476
|
*/
|
|
414
|
-
async function
|
|
477
|
+
async function openPr(work, done, failed, progress, out) {
|
|
415
478
|
const summary = ui.summary(failed);
|
|
416
|
-
const
|
|
417
|
-
if (
|
|
418
|
-
out.stdout(`\n${summary}
|
|
479
|
+
const before = changedPaths(work.root);
|
|
480
|
+
if (before.length === 0) {
|
|
481
|
+
out.stdout(`\n${summary} ${ui.noChanges}\n`);
|
|
419
482
|
return failed.length === 0 ? 0 : 1;
|
|
420
483
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
out.stdout(`\n${summary} No commiteé: el install falló y los cambios quedan sin commit.\n`);
|
|
484
|
+
if (failed.length > 0) {
|
|
485
|
+
work.keep = true;
|
|
486
|
+
out.stdout(`\n${summary} ${ui.prNotOpened("step", work.root)}\n`);
|
|
425
487
|
return 1;
|
|
426
488
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
out.stdout("
|
|
489
|
+
// A clone starts without node_modules, and the repo's hooks may need them.
|
|
490
|
+
if (packageText(work.root) !== null && !(await install(work.root, progress, out))) {
|
|
491
|
+
work.keep = true;
|
|
492
|
+
out.stdout(`\n${summary} ${ui.prNotOpened("install", work.root)}\n`);
|
|
431
493
|
return 1;
|
|
432
494
|
}
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
|
|
495
|
+
// Only a changed package.json brings the lockfile along; an install that only rewrites it stays out.
|
|
496
|
+
const paths = before.includes("package.json") ? changedPaths(work.root) : before;
|
|
497
|
+
out.stdout(`\n${summary} ${ui.changes}\n${paths.map((path) => ` ${path}`).join("\n")}\n`);
|
|
498
|
+
const result = await publish(work.repo, work.root, { base: work.base, steps: done, paths });
|
|
436
499
|
for (const line of result.lines)
|
|
437
500
|
out.stdout(`${line}\n`);
|
|
438
|
-
|
|
501
|
+
if (!result.pushed) {
|
|
502
|
+
work.keep = true;
|
|
503
|
+
out.stdout(`${ui.prNotOpened("git", work.root)}\n`);
|
|
504
|
+
}
|
|
505
|
+
return result.ok ? 0 : 1;
|
|
439
506
|
}
|
package/dist/commit-texts.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file The texts of the commit and the PR of `apply --
|
|
2
|
+
* @file The texts of the commit and the PR of `apply --pr`. They are always in Spanish,
|
|
3
3
|
* whatever language the interface shows: a repo's history should not change language with the
|
|
4
4
|
* terminal of whoever ran the command. A step reads as its Spanish menu order.
|
|
5
5
|
*/
|
package/dist/menu.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { FinishChoice } from "./git-finish.js";
|
|
2
1
|
import type { Candidate, Step } from "./piece.js";
|
|
3
2
|
/** What the person wants after seeing the table. */
|
|
4
3
|
export type Path = "all" | "pick" | "leave";
|
|
@@ -77,13 +76,12 @@ export declare function askSkills(candidates: Candidate[], ticked: string[]): Pr
|
|
|
77
76
|
* @example await askReplace(["planning"])
|
|
78
77
|
*/
|
|
79
78
|
export declare function askReplace(own: string[]): Promise<boolean | null>;
|
|
79
|
+
/** Where apply works: a PR from a clone of origin, or the person's checkout. */
|
|
80
|
+
export type Place = "pr" | "here";
|
|
80
81
|
/**
|
|
81
|
-
* Asks,
|
|
82
|
-
* cancelling leaves them too.
|
|
82
|
+
* Asks, before anything else, where to apply the changes.
|
|
83
83
|
* @param base - The base branch.
|
|
84
|
-
* @
|
|
85
|
-
* @
|
|
86
|
-
* @returns The answer.
|
|
87
|
-
* @example await askFinish("main", "chore/agent-kitchen-apply-2026-11-01", false) // "pr"
|
|
84
|
+
* @returns The answer, or null if the person cancelled.
|
|
85
|
+
* @example await askPlace("main") // "pr"
|
|
88
86
|
*/
|
|
89
|
-
export declare function
|
|
87
|
+
export declare function askPlace(base: string): Promise<Place | null>;
|
package/dist/menu.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file The questions, in Spanish (`@clack/prompts`), all asked before any step runs:
|
|
3
|
-
* quick path (do everything suggested, pick, or leave), the
|
|
4
|
-
* skills of a step grouped by catalog with their description,
|
|
5
|
-
* repo's own skills. They only run with a terminal.
|
|
2
|
+
* @file The questions, in Spanish (`@clack/prompts`), all asked before any step runs: where to
|
|
3
|
+
* apply (a PR or the checkout), the quick path (do everything suggested, pick, or leave), the
|
|
4
|
+
* checkbox menu of the steps, the skills of a step grouped by catalog with their description,
|
|
5
|
+
* and whether to replace the repo's own skills. They only run with a terminal.
|
|
6
6
|
*/
|
|
7
7
|
import { styleText } from "node:util";
|
|
8
8
|
import { confirm, groupMultiselect, isCancel, MULTISELECT_INSTRUCTIONS, multiselect, select, SELECT_INSTRUCTIONS } from "@clack/prompts";
|
|
@@ -123,25 +123,20 @@ export async function askReplace(own) {
|
|
|
123
123
|
return isCancel(answer) ? null : answer;
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
|
-
* Asks,
|
|
127
|
-
* cancelling leaves them too.
|
|
126
|
+
* Asks, before anything else, where to apply the changes.
|
|
128
127
|
* @param base - The base branch.
|
|
129
|
-
* @
|
|
130
|
-
* @
|
|
131
|
-
* @returns The answer.
|
|
132
|
-
* @example await askFinish("main", "chore/agent-kitchen-apply-2026-11-01", false) // "pr"
|
|
128
|
+
* @returns The answer, or null if the person cancelled.
|
|
129
|
+
* @example await askPlace("main") // "pr"
|
|
133
130
|
*/
|
|
134
|
-
export async function
|
|
131
|
+
export async function askPlace(base) {
|
|
135
132
|
spanishKeys();
|
|
136
133
|
const answer = await select({
|
|
137
|
-
message:
|
|
138
|
-
initialValue: "
|
|
134
|
+
message: "¿Dónde aplico los cambios?",
|
|
135
|
+
initialValue: "pr",
|
|
139
136
|
options: [
|
|
140
|
-
{ value: "
|
|
141
|
-
{ value: "
|
|
142
|
-
{ value: "branch", label: `Commit en una rama nueva, ${branch}`, hint: `${base} queda como está` },
|
|
143
|
-
{ value: "pr", label: "Commit en una rama nueva, push y PR", hint: `contra ${base}` },
|
|
137
|
+
{ value: "pr", label: "En un PR nuevo", hint: `recomendado · parte de origin/${base}, no toca tu copia` },
|
|
138
|
+
{ value: "here", label: "En tu copia, sin commit", hint: "los revisas con git diff y haces commit tú" },
|
|
144
139
|
],
|
|
145
140
|
});
|
|
146
|
-
return isCancel(answer) ?
|
|
141
|
+
return isCancel(answer) ? null : answer;
|
|
147
142
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The base branch, as issue-flow takes it: `baseBranch` of `.agents/flow.json`, or origin's
|
|
3
|
+
* default as git knows it, or `main`.
|
|
4
|
+
* @param root - The repo.
|
|
5
|
+
* @returns The branch.
|
|
6
|
+
* @example baseBranch(root) // "main"
|
|
7
|
+
*/
|
|
8
|
+
export declare function baseBranch(root: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Origin's URL.
|
|
11
|
+
* @param root - The repo.
|
|
12
|
+
* @returns The URL, or null if the repo has no origin.
|
|
13
|
+
*/
|
|
14
|
+
export declare function originUrl(root: string): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* The files that differ from HEAD, tracked or not, one by one, without the repo's worktrees.
|
|
17
|
+
* @param root - The repo.
|
|
18
|
+
* @returns Their paths.
|
|
19
|
+
* @example changedPaths(root) // ["package.json", "package-lock.json"]
|
|
20
|
+
*/
|
|
21
|
+
export declare function changedPaths(root: string): string[];
|
|
22
|
+
/**
|
|
23
|
+
* How many commits the current branch is behind its upstream, as the last fetch saw it.
|
|
24
|
+
* @param root - The repo.
|
|
25
|
+
* @returns The count, or 0 without an upstream.
|
|
26
|
+
*/
|
|
27
|
+
export declare function behindUpstream(root: string): number;
|
|
28
|
+
/**
|
|
29
|
+
* The branch for today's changes: a new name, also against origin's branches.
|
|
30
|
+
* @param root - The repo, just fetched.
|
|
31
|
+
* @param date - Today.
|
|
32
|
+
* @returns `chore/agent-kitchen-apply-AAAA-MM-DD`, or with `-2`, `-3`… if taken.
|
|
33
|
+
* @example branchName(root, new Date()) // "chore/agent-kitchen-apply-2026-11-01"
|
|
34
|
+
*/
|
|
35
|
+
export declare function branchName(root: string, date: Date): string;
|
|
36
|
+
/**
|
|
37
|
+
* Fetches origin and clones the repo at `origin/<base>` into a temporary folder.
|
|
38
|
+
* @param root - The repo.
|
|
39
|
+
* @param base - The base branch.
|
|
40
|
+
* @returns The clone's folder.
|
|
41
|
+
* @throws KitError if the repo has no origin, the fetch fails or origin has no such branch.
|
|
42
|
+
*/
|
|
43
|
+
export declare function cloneFromOrigin(root: string, base: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* Deletes the clone.
|
|
46
|
+
* @param clone - Its folder.
|
|
47
|
+
*/
|
|
48
|
+
export declare function removeClone(clone: string): void;
|
|
49
|
+
/** What `publish` needs to know. */
|
|
50
|
+
export interface PublishOptions {
|
|
51
|
+
base: string;
|
|
52
|
+
steps: string[];
|
|
53
|
+
paths: string[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Commits the clone's changes, pushes them from the repo to a new branch of origin and opens
|
|
57
|
+
* the PR. Until the push works, the commit lives only in the clone, which the caller keeps.
|
|
58
|
+
* @param root - The repo.
|
|
59
|
+
* @param clone - The clone, with the changes.
|
|
60
|
+
* @param options - The base, the steps that ran (for the commit and the PR) and the files to commit (the ones shown).
|
|
61
|
+
* @returns The lines to show, whether it all worked and whether the changes reached origin.
|
|
62
|
+
* @example await publish(root, clone, { base: "main", steps, paths })
|
|
63
|
+
*/
|
|
64
|
+
export declare function publish(root: string, clone: string, options: PublishOptions): Promise<{
|
|
65
|
+
ok: boolean;
|
|
66
|
+
pushed: boolean;
|
|
67
|
+
lines: string[];
|
|
68
|
+
}>;
|
package/dist/pr-clone.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file What `apply --pr` does with git. It fetches origin and clones the repo into a temporary
|
|
3
|
+
* folder at `origin/<base>`, so the steps see what the base has today and the person's checkout
|
|
4
|
+
* is never touched: not its branch, its files, nor its node_modules. A clone and not a worktree,
|
|
5
|
+
* because `issue-flow apply` refuses to run in a worktree. The clone shares the repo's objects
|
|
6
|
+
* (`--shared`), points its origin where the repo's does, and commits with the repo's hooks,
|
|
7
|
+
* identity and signing. The commit goes from the clone into the repo's objects and the repo
|
|
8
|
+
* pushes it straight to a new branch of origin, with its own credentials, so no local branch
|
|
9
|
+
* is made. Then github-rest opens the PR in draft. git runs its hooks; nothing here skips them.
|
|
10
|
+
*/
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
15
|
+
import { COMMIT_TITLE as TITLE, prBody } from "./commit-texts.js";
|
|
16
|
+
import { KitError } from "./output.js";
|
|
17
|
+
import { FLOW_CONFIG } from "./piece.js";
|
|
18
|
+
import { dependencyVersion, readJson } from "./repo.js";
|
|
19
|
+
import { captureNpxWithout, isRelease, npmLatest } from "./tools.js";
|
|
20
|
+
/** github-rest on npm, which opens the PR (REST, never gh's GraphQL). */
|
|
21
|
+
const GITHUB_REST = "@ramon-sg/github-rest";
|
|
22
|
+
/** The settings of the repo a commit made in the clone needs: who signs it and how. */
|
|
23
|
+
const IDENTITY = ["user.name", "user.email", "user.signingkey", "commit.gpgsign", "gpg.format", "gpg.program", "gpg.openpgp.program", "gpg.x509.program", "gpg.ssh.program", "gpg.ssh.allowedsignersfile"];
|
|
24
|
+
/**
|
|
25
|
+
* Runs git in a folder.
|
|
26
|
+
* @param cwd - The folder.
|
|
27
|
+
* @param args - The arguments.
|
|
28
|
+
* @returns How it went.
|
|
29
|
+
*/
|
|
30
|
+
function git(cwd, args) {
|
|
31
|
+
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
32
|
+
return { ok: result.status === 0, out: (result.stdout ?? "").trimEnd(), err: (result.stderr ?? "").trim() };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The base branch, as issue-flow takes it: `baseBranch` of `.agents/flow.json`, or origin's
|
|
36
|
+
* default as git knows it, or `main`.
|
|
37
|
+
* @param root - The repo.
|
|
38
|
+
* @returns The branch.
|
|
39
|
+
* @example baseBranch(root) // "main"
|
|
40
|
+
*/
|
|
41
|
+
export function baseBranch(root) {
|
|
42
|
+
const flow = readJson(root, FLOW_CONFIG);
|
|
43
|
+
if (typeof flow?.baseBranch === "string" && flow.baseBranch !== "")
|
|
44
|
+
return flow.baseBranch;
|
|
45
|
+
const head = git(root, ["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"]);
|
|
46
|
+
return head.ok && head.out.startsWith("origin/") ? head.out.slice("origin/".length) : "main";
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Origin's URL.
|
|
50
|
+
* @param root - The repo.
|
|
51
|
+
* @returns The URL, or null if the repo has no origin.
|
|
52
|
+
*/
|
|
53
|
+
export function originUrl(root) {
|
|
54
|
+
const url = git(root, ["remote", "get-url", "origin"]);
|
|
55
|
+
return url.ok && url.out !== "" ? url.out : null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The repo's other worktrees that live inside it, relative to it.
|
|
59
|
+
* @param root - The repo.
|
|
60
|
+
* @returns Their folders, like `.claude/worktrees/feat+1-x`.
|
|
61
|
+
*/
|
|
62
|
+
function innerWorktrees(root) {
|
|
63
|
+
const list = git(root, ["worktree", "list", "--porcelain"]);
|
|
64
|
+
if (!list.ok)
|
|
65
|
+
return [];
|
|
66
|
+
return list.out
|
|
67
|
+
.split("\n")
|
|
68
|
+
.filter((line) => line.startsWith("worktree "))
|
|
69
|
+
.map((line) => relative(root, line.slice("worktree ".length)))
|
|
70
|
+
.filter((path) => path !== "" && !path.startsWith(".."));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The files that differ from HEAD, tracked or not, one by one, without the repo's worktrees.
|
|
74
|
+
* @param root - The repo.
|
|
75
|
+
* @returns Their paths.
|
|
76
|
+
* @example changedPaths(root) // ["package.json", "package-lock.json"]
|
|
77
|
+
*/
|
|
78
|
+
export function changedPaths(root) {
|
|
79
|
+
const status = git(root, ["status", "--porcelain", "-uall", "-z"]);
|
|
80
|
+
if (!status.ok)
|
|
81
|
+
throw new KitError(`no pude leer git status: ${status.err}`);
|
|
82
|
+
const worktrees = innerWorktrees(root);
|
|
83
|
+
const entries = status.out.split("\0").filter((entry) => entry !== "");
|
|
84
|
+
const paths = [];
|
|
85
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
86
|
+
const entry = entries[index] ?? "";
|
|
87
|
+
if (/[RC]/.test(entry.slice(0, 2)))
|
|
88
|
+
index += 1;
|
|
89
|
+
const path = entry.slice(3).replace(/\/$/, "");
|
|
90
|
+
if (!worktrees.some((tree) => path === tree || path.startsWith(`${tree}/`)))
|
|
91
|
+
paths.push(path);
|
|
92
|
+
}
|
|
93
|
+
return paths;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* How many commits the current branch is behind its upstream, as the last fetch saw it.
|
|
97
|
+
* @param root - The repo.
|
|
98
|
+
* @returns The count, or 0 without an upstream.
|
|
99
|
+
*/
|
|
100
|
+
export function behindUpstream(root) {
|
|
101
|
+
const count = git(root, ["rev-list", "--count", "HEAD..@{upstream}"]);
|
|
102
|
+
return count.ok ? Number(count.out) || 0 : 0;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The branch for today's changes: a new name, also against origin's branches.
|
|
106
|
+
* @param root - The repo, just fetched.
|
|
107
|
+
* @param date - Today.
|
|
108
|
+
* @returns `chore/agent-kitchen-apply-AAAA-MM-DD`, or with `-2`, `-3`… if taken.
|
|
109
|
+
* @example branchName(root, new Date()) // "chore/agent-kitchen-apply-2026-11-01"
|
|
110
|
+
*/
|
|
111
|
+
export function branchName(root, date) {
|
|
112
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
113
|
+
const name = `chore/agent-kitchen-apply-${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
114
|
+
const taken = (candidate) => git(root, ["rev-parse", "-q", "--verify", `refs/heads/${candidate}`]).ok || git(root, ["rev-parse", "-q", "--verify", `refs/remotes/origin/${candidate}`]).ok;
|
|
115
|
+
if (!taken(name))
|
|
116
|
+
return name;
|
|
117
|
+
let n = 2;
|
|
118
|
+
while (taken(`${name}-${n}`))
|
|
119
|
+
n += 1;
|
|
120
|
+
return `${name}-${n}`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The hooks the clone runs: the repo's `core.hooksPath` as is when it is relative and the
|
|
124
|
+
* folder is in the clone (committed, like `.githooks`), or else the folder the repo uses,
|
|
125
|
+
* absolute (`.git/hooks`, or a generated one like husky's).
|
|
126
|
+
* @param root - The repo.
|
|
127
|
+
* @param clone - The clone.
|
|
128
|
+
* @returns The folder.
|
|
129
|
+
*/
|
|
130
|
+
function hooksPath(root, clone) {
|
|
131
|
+
const own = git(root, ["config", "--get", "core.hooksPath"]);
|
|
132
|
+
if (own.ok && own.out !== "" && !isAbsolute(own.out) && existsSync(join(clone, own.out)))
|
|
133
|
+
return own.out;
|
|
134
|
+
return git(root, ["rev-parse", "--path-format=absolute", "--git-path", "hooks"]).out;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Fetches origin and clones the repo at `origin/<base>` into a temporary folder.
|
|
138
|
+
* @param root - The repo.
|
|
139
|
+
* @param base - The base branch.
|
|
140
|
+
* @returns The clone's folder.
|
|
141
|
+
* @throws KitError if the repo has no origin, the fetch fails or origin has no such branch.
|
|
142
|
+
*/
|
|
143
|
+
export function cloneFromOrigin(root, base) {
|
|
144
|
+
const url = originUrl(root);
|
|
145
|
+
if (url === null)
|
|
146
|
+
throw new KitError("--pr necesita un remoto origin, y este repo no tiene.");
|
|
147
|
+
const fetched = git(root, ["fetch", "-q", "--prune", "origin"]);
|
|
148
|
+
if (!fetched.ok)
|
|
149
|
+
throw new KitError(`no pude bajar origin con git fetch; revisa la conexión o las credenciales y corre otra vez.${fetched.err === "" ? "" : `\n${fetched.err}`}`);
|
|
150
|
+
const head = git(root, ["rev-parse", "-q", "--verify", `refs/remotes/origin/${base}^{commit}`]);
|
|
151
|
+
if (!head.ok)
|
|
152
|
+
throw new KitError(`origin no tiene la rama ${base}.`);
|
|
153
|
+
const clone = realpathSync(mkdtempSync(join(tmpdir(), "agent-kitchen-pr-")));
|
|
154
|
+
const steps = [
|
|
155
|
+
["clone", "-q", "--shared", "--no-checkout", root, clone],
|
|
156
|
+
["-C", clone, "remote", "set-url", "origin", url],
|
|
157
|
+
["-C", clone, "update-ref", `refs/remotes/origin/${base}`, head.out],
|
|
158
|
+
["-C", clone, "symbolic-ref", "refs/remotes/origin/HEAD", `refs/remotes/origin/${base}`],
|
|
159
|
+
["-C", clone, "checkout", "-q", "--detach", head.out],
|
|
160
|
+
];
|
|
161
|
+
for (const args of steps) {
|
|
162
|
+
const done = git(root, args);
|
|
163
|
+
if (!done.ok) {
|
|
164
|
+
rmSync(clone, { recursive: true, force: true });
|
|
165
|
+
throw new KitError(`no pude preparar la copia temporal: ${done.err}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const key of IDENTITY) {
|
|
169
|
+
const value = git(root, ["config", "--get", key]);
|
|
170
|
+
if (value.ok && value.out !== "")
|
|
171
|
+
git(clone, ["config", key, value.out]);
|
|
172
|
+
}
|
|
173
|
+
git(clone, ["config", "core.hooksPath", hooksPath(root, clone)]);
|
|
174
|
+
return clone;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Deletes the clone.
|
|
178
|
+
* @param clone - Its folder.
|
|
179
|
+
*/
|
|
180
|
+
export function removeClone(clone) {
|
|
181
|
+
rmSync(clone, { recursive: true, force: true });
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The version of github-rest to open the PR with: the repo's exact one, or npm's latest.
|
|
185
|
+
* @param root - The checkout to read package.json from.
|
|
186
|
+
* @returns The version, or null if neither is known.
|
|
187
|
+
*/
|
|
188
|
+
async function githubRestVersion(root) {
|
|
189
|
+
const own = dependencyVersion(readJson(root, "package.json"), GITHUB_REST);
|
|
190
|
+
if (own !== null && isRelease(own))
|
|
191
|
+
return own;
|
|
192
|
+
return npmLatest(GITHUB_REST);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Opens the PR in draft with github-rest, without GH_REPO, so the repo is origin's.
|
|
196
|
+
* @param root - The repo.
|
|
197
|
+
* @param clone - The clone, whose package.json says which github-rest the base uses.
|
|
198
|
+
* @param branch - The branch.
|
|
199
|
+
* @param base - The base.
|
|
200
|
+
* @param steps - The steps, for the body.
|
|
201
|
+
* @returns The PR's URL, or why it failed.
|
|
202
|
+
*/
|
|
203
|
+
async function openPr(root, clone, branch, base, steps) {
|
|
204
|
+
const version = await githubRestVersion(clone);
|
|
205
|
+
if (version === null)
|
|
206
|
+
return { error: `no sé qué versión de ${GITHUB_REST} usar.` };
|
|
207
|
+
const dir = mkdtempSync(join(tmpdir(), "agent-kitchen-body-"));
|
|
208
|
+
const bodyFile = join(dir, "body.md");
|
|
209
|
+
writeFileSync(bodyFile, prBody(steps));
|
|
210
|
+
try {
|
|
211
|
+
const result = await captureNpxWithout(root, [`${GITHUB_REST}@${version}`, "pr", "create", "--head", branch, "--base", base, "--title", TITLE, "--body-file", bodyFile, "--draft"], ["GH_REPO"]);
|
|
212
|
+
const url = result.out.trim().split("\n").at(-1) ?? "";
|
|
213
|
+
return result.status === 0 && url !== "" ? { url } : { error: `${result.err}${result.out}`.trim() || "github-rest falló sin decir por qué." };
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
rmSync(dir, { recursive: true, force: true });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* What the person can run to open the PR by hand.
|
|
221
|
+
* @param branch - The branch.
|
|
222
|
+
* @param base - The base.
|
|
223
|
+
* @returns The command.
|
|
224
|
+
*/
|
|
225
|
+
function prCommand(branch, base) {
|
|
226
|
+
return `npx @ramon-sg/github-rest pr create --head ${branch} --base ${base} --title "${TITLE}" --body "..." --draft`;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Commits the clone's changes, pushes them from the repo to a new branch of origin and opens
|
|
230
|
+
* the PR. Until the push works, the commit lives only in the clone, which the caller keeps.
|
|
231
|
+
* @param root - The repo.
|
|
232
|
+
* @param clone - The clone, with the changes.
|
|
233
|
+
* @param options - The base, the steps that ran (for the commit and the PR) and the files to commit (the ones shown).
|
|
234
|
+
* @returns The lines to show, whether it all worked and whether the changes reached origin.
|
|
235
|
+
* @example await publish(root, clone, { base: "main", steps, paths })
|
|
236
|
+
*/
|
|
237
|
+
export async function publish(root, clone, options) {
|
|
238
|
+
const { base, steps, paths } = options;
|
|
239
|
+
const add = git(clone, ["--literal-pathspecs", "add", "-A", "--", ...paths]);
|
|
240
|
+
const done = add.ok ? git(clone, ["commit", "-q", "-m", TITLE, "-m", steps.map((step) => `- ${step}`).join("\n")]) : add;
|
|
241
|
+
if (!done.ok)
|
|
242
|
+
return { ok: false, pushed: false, lines: ["No pude hacer el commit.", done.err || done.out].filter((line) => line !== "") };
|
|
243
|
+
const sha = git(clone, ["rev-parse", "HEAD"]).out;
|
|
244
|
+
const branch = branchName(root, new Date());
|
|
245
|
+
// git asks for credentials on the terminal itself, so its output can stay captured.
|
|
246
|
+
const fetched = git(root, ["fetch", "-q", clone, "HEAD"]);
|
|
247
|
+
const pushed = fetched.ok ? git(root, ["push", "-q", "origin", `${sha}:refs/heads/${branch}`]) : fetched;
|
|
248
|
+
if (!pushed.ok)
|
|
249
|
+
return { ok: false, pushed: false, lines: [`No pude subir la rama ${branch} a origin.`, pushed.err].filter((line) => line !== "") };
|
|
250
|
+
const pr = await openPr(root, clone, branch, base, steps);
|
|
251
|
+
if ("url" in pr)
|
|
252
|
+
return { ok: true, pushed: true, lines: [`PR en draft: ${pr.url}`] };
|
|
253
|
+
return { ok: false, pushed: true, lines: [`La rama ${branch} está en origin, pero no pude abrir el PR: ${pr.error}`, `Falta: ${prCommand(branch, base)}`] };
|
|
254
|
+
}
|
package/dist/progress.d.ts
CHANGED
|
@@ -19,12 +19,11 @@ export declare function plainProgress(out: Output, options: {
|
|
|
19
19
|
now?: () => number;
|
|
20
20
|
}): Progress;
|
|
21
21
|
/**
|
|
22
|
-
* How progress is shown: a spinner in a terminal, plain lines otherwise
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* How progress is shown: a spinner in a terminal, plain lines otherwise. Only for the steps,
|
|
23
|
+
* after every question: a spinner shown before a question leaves the second question deaf to
|
|
24
|
+
* the keyboard (@clack/prompts 1.8.1).
|
|
25
25
|
* @param out - Where to write.
|
|
26
26
|
* @param verbose - Whether the tools print on the terminal.
|
|
27
|
-
* @param plain - Whether a question may come after the steps (`--commit`).
|
|
28
27
|
* @returns The progress.
|
|
29
28
|
*/
|
|
30
|
-
export declare function progressFor(out: Output, verbose: boolean
|
|
29
|
+
export declare function progressFor(out: Output, verbose: boolean): Progress;
|
package/dist/progress.js
CHANGED
|
@@ -46,16 +46,15 @@ export function plainProgress(out, options) {
|
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
49
|
-
* How progress is shown: a spinner in a terminal, plain lines otherwise
|
|
50
|
-
*
|
|
51
|
-
*
|
|
49
|
+
* How progress is shown: a spinner in a terminal, plain lines otherwise. Only for the steps,
|
|
50
|
+
* after every question: a spinner shown before a question leaves the second question deaf to
|
|
51
|
+
* the keyboard (@clack/prompts 1.8.1).
|
|
52
52
|
* @param out - Where to write.
|
|
53
53
|
* @param verbose - Whether the tools print on the terminal.
|
|
54
|
-
* @param plain - Whether a question may come after the steps (`--commit`).
|
|
55
54
|
* @returns The progress.
|
|
56
55
|
*/
|
|
57
|
-
export function progressFor(out, verbose
|
|
58
|
-
if (out.interactive && !verbose
|
|
56
|
+
export function progressFor(out, verbose) {
|
|
57
|
+
if (out.interactive && !verbose) {
|
|
59
58
|
const spin = spinner();
|
|
60
59
|
let started = 0;
|
|
61
60
|
return {
|
package/dist/texts/catalog.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export interface Catalog {
|
|
|
37
37
|
suggested: (dryRun: boolean) => string;
|
|
38
38
|
/** The last line when every step worked or some failed. */
|
|
39
39
|
summary: (failed: string[]) => string;
|
|
40
|
-
/** What is left to do by hand after the steps,
|
|
40
|
+
/** What is left to do by hand after the steps, in the checkout; with the manager if its install failed. */
|
|
41
41
|
next: (failedInstall: string | null) => string;
|
|
42
42
|
/** A new piece left out because no one was asked about it. */
|
|
43
43
|
undecided: (names: string[]) => string;
|
|
@@ -47,4 +47,12 @@ export interface Catalog {
|
|
|
47
47
|
saved: (file: string, commits: boolean) => string;
|
|
48
48
|
/** The closing line when no step ran: the person left, or picked none. */
|
|
49
49
|
noSteps: (why: "leave" | "none", wrote: boolean) => string;
|
|
50
|
+
/** With --pr, before the clone: from which branch of origin. */
|
|
51
|
+
cloning: (base: string) => string;
|
|
52
|
+
/** With --pr, the heading of the files that go in the PR. */
|
|
53
|
+
changes: string;
|
|
54
|
+
/** With --pr, nothing changed. */
|
|
55
|
+
noChanges: string;
|
|
56
|
+
/** With --pr, why no PR was opened and where the changes stay. */
|
|
57
|
+
prNotOpened: (why: "step" | "install" | "git", clone: string) => string;
|
|
50
58
|
}
|
package/dist/texts/es.js
CHANGED
|
@@ -136,4 +136,11 @@ export const es = {
|
|
|
136
136
|
return wrote ? "No corrí ningún paso; solo guardé qué piezas usa el repo." : "No se cambió nada.";
|
|
137
137
|
return wrote ? "No elegiste pasos; solo guardé qué piezas usa el repo." : "No elegiste nada; no se cambió nada.";
|
|
138
138
|
},
|
|
139
|
+
cloning: (base) => `Trabajo en una copia temporal de origin/${base}; tu copia no se toca.`,
|
|
140
|
+
changes: "Cambios del PR:",
|
|
141
|
+
noChanges: "No cambió ningún archivo: no hay PR que abrir.",
|
|
142
|
+
prNotOpened: (why, clone) => {
|
|
143
|
+
const reason = { step: "No abrí el PR porque algún paso falló.", install: "No abrí el PR porque falló el install.", git: "" }[why];
|
|
144
|
+
return `${reason === "" ? "" : `${reason} `}Los cambios quedan en la copia temporal ${clone}; bórrala con rm -rf cuando termines.`;
|
|
145
|
+
},
|
|
139
146
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-kitchen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "One command to install, update and check a repo's agent tooling: the issue flow and its skills, the general skills, Claude cloud sessions, claude-repo-guard and the installed-versions rule in AGENTS.md.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/dist/git-finish.d.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/** What to do with the changes: leave them, commit on the base, on a branch, or a branch with a PR. */
|
|
2
|
-
export type FinishChoice = "leave" | "base" | "branch" | "pr";
|
|
3
|
-
/**
|
|
4
|
-
* The base branch, as issue-flow takes it: `baseBranch` of `.agents/flow.json`, or origin's
|
|
5
|
-
* default as git knows it, or `main`.
|
|
6
|
-
* @param root - The repo.
|
|
7
|
-
* @returns The branch.
|
|
8
|
-
* @example baseBranch(root) // "main"
|
|
9
|
-
*/
|
|
10
|
-
export declare function baseBranch(root: string): string;
|
|
11
|
-
/**
|
|
12
|
-
* The files that differ from HEAD, tracked or not, one by one, without the repo's worktrees.
|
|
13
|
-
* @param root - The repo.
|
|
14
|
-
* @returns Their paths.
|
|
15
|
-
* @example changedPaths(root) // ["package.json", "package-lock.json"]
|
|
16
|
-
*/
|
|
17
|
-
export declare function changedPaths(root: string): string[];
|
|
18
|
-
/**
|
|
19
|
-
* Checks, before any step, that the repo has no changes and is on its base branch.
|
|
20
|
-
* @param root - The repo.
|
|
21
|
-
* @param base - The base branch.
|
|
22
|
-
* @throws KitError saying what is wrong and what to do.
|
|
23
|
-
*/
|
|
24
|
-
export declare function checkReady(root: string, base: string): void;
|
|
25
|
-
/**
|
|
26
|
-
* The branch for today's changes: a new name, also against origin's branches git knows of.
|
|
27
|
-
* @param root - The repo.
|
|
28
|
-
* @param date - Today.
|
|
29
|
-
* @returns `chore/agent-kitchen-apply-AAAA-MM-DD`, or with `-2`, `-3`… if taken.
|
|
30
|
-
* @example branchName(root, new Date()) // "chore/agent-kitchen-apply-2026-11-01"
|
|
31
|
-
*/
|
|
32
|
-
export declare function branchName(root: string, date: Date): string;
|
|
33
|
-
/** What `finish` needs to know. */
|
|
34
|
-
export interface FinishOptions {
|
|
35
|
-
base: string;
|
|
36
|
-
branch: string;
|
|
37
|
-
steps: string[];
|
|
38
|
-
paths: string[];
|
|
39
|
-
manager: string;
|
|
40
|
-
packageChanged?: boolean;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Does what the person chose with the changes.
|
|
44
|
-
* @param root - The repo, on the base, with the changes of the run.
|
|
45
|
-
* @param choice - What to do.
|
|
46
|
-
* @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.
|
|
47
|
-
* @returns The lines to show and whether it all worked.
|
|
48
|
-
* @example await finish(root, "pr", { base: "main", branch: "chore/agent-kitchen-apply-2026-11-01", steps, manager: "npm" })
|
|
49
|
-
*/
|
|
50
|
-
export declare function finish(root: string, choice: FinishChoice, options: FinishOptions): Promise<{
|
|
51
|
-
ok: boolean;
|
|
52
|
-
lines: string[];
|
|
53
|
-
}>;
|
package/dist/git-finish.js
DELETED
|
@@ -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
|
-
}
|