agent-kitchen 0.5.0 → 0.6.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 CHANGED
@@ -17,15 +17,15 @@ Al arrancar revisa si npm tiene una versión más nueva de sí mismo. Si la hay,
17
17
  - Pregunta una vez: **todo lo sugerido** (Enter), **elegir** los pasos y las skills, o **salir**. Todas las preguntas van antes de instalar; después corre solo.
18
18
  - Cada paso es una línea con ✓ o ✗. La salida de cada herramienta solo aparece si falla, o con `--verbose`.
19
19
  - Es rápido: las versiones se consultan todas a la vez y las herramientas se descargan mientras respondes.
20
- - Cada pieza la instala su propia herramienta. `agent-kitchen` solo sabe cuáles hay, en qué versión están y cómo se conectan entre sí. Las excepciones son una línea de `AGENTS.md` y la versión de `skills-manager` en `package.json`, que escribe él mismo.
20
+ - Cada pieza la instala su propia herramienta. `agent-kitchen` solo sabe cuáles hay, en qué versión están y cómo se conectan entre sí. Las excepciones son una línea de `AGENTS.md` y, en `package.json`, la versión de `skills-manager` y la de `github-rest` cuando queda bajo el mínimo de issue-flow, que escribe él mismo.
21
21
  - Correrlo de nuevo no rompe nada: si todo está al día, lo dice y termina.
22
22
 
23
23
  ## Las piezas
24
24
 
25
25
  | Pieza | Qué es | Versión del repo | Última | Instalar | Actualizar |
26
26
  |---|---|---|---|---|---|
27
- | `skills-manager` | La versión de [`@ramon-sg/skills-manager`](https://github.com/ramon-sg/skills-manager) que corre `agent-kitchen`: como mínimo 1.2.0. Si falta la agrega y si es menor la sube; nunca la baja. Un rango o `workspace:` no se toca, y avisa si la versión que corre queda bajo el mínimo. Solo en repos con `package.json` que usan skills | `package.json` | — | La agrega `agent-kitchen` | La sube `agent-kitchen` |
28
- | `issue-flow` | Flujo de issues ([`@ramon-sg/issue-flow`](https://github.com/ramon-sg/issue-flow)): rama, worktree y PR en draft por issue, y sus 5 skills | `package.json` | npm | `issue-flow init` | `issue-flow upgrade` en la versión nueva, y sus skills a la misma tag. Desde 0.2.6, si a GitHub le faltan labels del flujo (`spike`, `P1`–`P4`…), `issue-flow labels sync` los crea |
27
+ | `skills-manager` | La versión de [`@ramon-sg/skills-manager`](https://github.com/ramon-sg/skills-manager) que corre `agent-kitchen`: como mínimo 1.2.0. Si falta la agrega y si es menor la sube; nunca la baja. Un rango o `workspace:` no se toca, y avisa si la versión que corre queda bajo el mínimo. Solo en repos que usan skills y tienen `package.json`, o donde issue-flow se instala en la misma corrida: entonces crea el `package.json` | `package.json` | — | La agrega `agent-kitchen` | La sube `agent-kitchen` |
28
+ | `issue-flow` | Flujo de issues ([`@ramon-sg/issue-flow`](https://github.com/ramon-sg/issue-flow)): rama, worktree y PR en draft por issue, y sus 5 skills | `package.json` | npm | `issue-flow apply` (`init` antes de 0.3.0), y sus skills a la misma tag | `issue-flow apply` en la versión nueva (`upgrade` antes de 0.3.0), y sus skills a la misma tag. Desde 0.3.0, si su doctor ve hooks o el `$schema` atrás, `apply` en la versión del repo. Desde 0.3.0, si `github-rest` es una versión exacta bajo el mínimo que pide esa versión del flujo, la sube. Desde 0.2.6, si a GitHub le faltan labels del flujo (`spike`, `P1`–`P4`…), `issue-flow labels sync` los crea |
29
29
  | `claude-cloud` | Sesiones en la nube ([`@ramon-sg/claude-cloud`](https://github.com/ramon-sg/claude-cloud)): herramientas, Docker y el clone completo | `.claude/cloud.json` | npm | `claude-cloud init` | Cambia la versión que fija `.claude/cloud.json` |
30
30
  | `claude-repo-guard` | Barandas ([`claude-repo-guard`](https://github.com/ramon-sg/claude-repo-guard)): modo auto con sandbox, escribe solo en su worktree | `.claude/repo-guard.manifest.json` | Tags `vX.Y.Z` de GitHub | `apply` en esa tag (`install` antes de 0.5.0) | `apply` en la tag nueva |
31
31
  | `skills` | Skills con [`skills-manager`](https://github.com/ramon-sg/skills-manager): las del flujo y las de [`agent-skills`](https://github.com/ramon-sg/agent-skills) (planes, auditoría de código y de tests) | `.agents/skills.lock.json` | Tags del catálogo | `skills-manager init --select` con las elegidas | `skills-manager update` |
@@ -39,6 +39,7 @@ Si npm o GitHub no responden, la pieza se muestra pero no se ofrece actualizarla
39
39
  - **Qué skills:** la primera vez vienen todas las de los catálogos. Con "Elegir" aparece un menú agrupado por catálogo, con la descripción de cada una. Si un catálogo trae skills que el repo no tiene, se ofrecen como un paso sin marcar.
40
40
  - **Skills propias del repo:** si el repo ya tiene una carpeta con el nombre de una skill del catálogo, pregunta una vez si reemplazarlas. Al reemplazar se conserva su `project.md`. Con `--yes` no se reemplazan: se dejan fuera y lo dice.
41
41
  - **Seguridad y flujo:** si el repo tiene el flujo, antes de instalar `claude-repo-guard` se escribe en sus opciones que abrir el PR en draft, asignar la issue y agregar el "Slug de rama" son parte del trabajo normal. También se escribe la rama base del flujo como `integrationBranch`. En macOS se agrega `weakerNetworkIsolation`, que `gh` necesita dentro del sandbox. Si `claude-repo-guard` ya estaba al día pero le faltan esas opciones, se ofrecen como un paso.
42
+ - **Qué piezas usa el repo:** `.agents/kitchen.json` guarda por pieza si el repo la quiere (`{ "pieces": { "claude-repo-guard": false } }`). Se crea en la primera corrida de `apply` en una terminal: lo instalado cuenta como sí y lo que ofreció y no marcaste, como no. `false` deja la pieza fuera del menú y de `--yes`, sin desinstalarla; la tabla lo dice y `doctor` no corre sus revisiones. Una pieza que el archivo no menciona se ofrece una vez, marcada "(nueva)"; con `--yes` o sin terminal se deja fuera, si no está instalada, y lo dice (una ya instalada se actualiza como siempre). `--yes`, `--dry-run`, `--skip` y las corridas sin terminal nunca escriben el archivo. Se cambia con `apply --pieces` o a mano. `skills-manager` no tiene decisión: sus skills siguen en `.agents/skills.json`.
42
43
  - **La nube no molesta en local:** el hook de sesión solo actúa en la nube. Un repo que nunca se abre ahí no pierde nada.
43
44
 
44
45
  ## Uso
@@ -47,8 +48,10 @@ Si npm o GitHub no responden, la pieza se muestra pero no se ofrece actualizarla
47
48
  npx agent-kitchen apply # pregunta, en una terminal (Enter: todo lo sugerido)
48
49
  npx agent-kitchen apply --dry-run # solo muestra qué haría
49
50
  npx agent-kitchen apply --yes # hace lo sugerido sin preguntar
51
+ npx agent-kitchen apply --commit # al final, pregunta si commitear, en una rama o con PR
50
52
  npx agent-kitchen apply --verbose # muestra la salida de cada herramienta
51
53
  npx agent-kitchen apply --skip claude-cloud # deja fuera una pieza (o varias, con comas)
54
+ npx agent-kitchen apply --pieces # cambia qué piezas usa el repo (solo en una terminal)
52
55
  npx agent-kitchen doctor # revisa cada pieza con su propio doctor
53
56
  ```
54
57
 
@@ -56,7 +59,14 @@ npx agent-kitchen doctor # revisa cada pieza con su propio
56
59
 
57
60
  - **Sin terminal y sin `--yes`** (por ejemplo, desde un agente), solo muestra lo que ofrecería.
58
61
  - **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`.
59
- - **Al terminar,** corre el `install` de tu gestor de paquetes, revisa el diff y haz commit.
62
+ - **Al terminar,** corre el `install` de tu gestor de paquetes, revisa el diff y haz commit. O usa `--commit`.
63
+
64
+ **`--commit`** deja los cambios commiteados sin que lo haga un agente:
65
+
66
+ - **Antes de cualquier paso,** exige el repo sin cambios (también sin seguimiento; los worktrees de `.claude/worktrees/` no cuentan) y en la rama base: `baseBranch` de `.agents/flow.json`, si no la rama por defecto de `origin`, si no `main`. Con `--dry-run` hace solo esta revisión y lista los pasos.
67
+ - **Al terminar,** si cambió `package.json`, corre el `install` de tu gestor para que el lockfile entre en el commit. Muestra los archivos cambiados y pregunta: dejarlos sin commit (por defecto), commit en la base, commit en una rama nueva `chore/agent-kitchen-apply-<fecha>`, o rama nueva con push y PR (con `@ramon-sg/github-rest`).
68
+ - **Con rama,** la copia principal pasa por la rama solo mientras corre el comando: commitea ahí (los hooks de git corren, y uno que prohíbe commitear en la base no la frena) y vuelve a la base sin los cambios. Si el push o el PR fallan, la rama queda y la salida dice qué correr.
69
+ - **Con `--yes`,** elige la rama con PR. Si algún paso falló, no commitea.
60
70
 
61
71
  **`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 (`✗`).
62
72
 
package/dist/cli.js CHANGED
@@ -21,14 +21,19 @@ 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] [--verbose] [--skip <pieza>,...]
24
+ agent-kitchen apply [--yes | --dry-run] [--commit] [--verbose] [--skip <pieza>,...]
25
+ agent-kitchen apply --pieces [--commit]
25
26
  agent-kitchen doctor
26
27
 
27
28
  agent-kitchen revisa cada pieza de las herramientas de agentes del repo (${pieceNames().join(", ")}),
28
29
  muestra qué tiene el repo y la última versión de cada una, y apply ofrece instalar lo que falta
29
30
  y actualizar lo que está atrasado. En una terminal pregunta una vez: hacer todo lo sugerido,
30
31
  o elegir los pasos y las skills. --yes hace lo sugerido sin preguntar; sin terminal solo lo
31
- muestra. --verbose muestra la salida de cada herramienta. Córrelo en la copia principal del
32
+ muestra. --verbose muestra la salida de cada herramienta. --commit exige el repo sin cambios
33
+ y en la rama base; al final instala si cambió package.json y pregunta si dejar los cambios,
34
+ commitearlos en la base, en una rama nueva, o en una rama nueva con push y PR (con --yes,
35
+ esto último). apply --pieces elige qué piezas usa el repo y lo guarda en .agents/kitchen.json
36
+ (en una terminal, sin --yes ni --dry-run). Córrelo en la copia principal del
32
37
  repo, desde tu terminal. Sin subcomando hace lo mismo que apply, por ahora.
33
38
 
34
39
  doctor corre las revisiones de cada pieza en la versión del repo, sin cambiar nada.`;
@@ -77,7 +82,7 @@ async function main(argv) {
77
82
  const { values, positionals } = parseArgs({
78
83
  args: argv,
79
84
  allowPositionals: true,
80
- options: { yes: { type: "boolean" }, "dry-run": { type: "boolean" }, verbose: { type: "boolean", short: "v" }, skip: { type: "string" }, help: { type: "boolean", short: "h" }, version: { type: "boolean" } },
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" } },
81
86
  });
82
87
  if (values.help === true) {
83
88
  out.stdout(`${USAGE}\n`);
@@ -95,14 +100,14 @@ async function main(argv) {
95
100
  if (rest.length > 0 || (command !== undefined && command !== "doctor" && command !== "apply"))
96
101
  throw new UsageError(`no existe el comando ${positionals.join(" ")}.`);
97
102
  if (command === "doctor") {
98
- if (values.yes !== undefined || values["dry-run"] !== undefined || values.skip !== undefined || values.verbose !== undefined)
103
+ if (values.yes !== undefined || values["dry-run"] !== undefined || values.skip !== undefined || values.verbose !== undefined || values.commit !== undefined || values.pieces !== undefined)
99
104
  throw new UsageError("doctor no lleva opciones.");
100
105
  return doctor(process.cwd(), out);
101
106
  }
102
107
  if (command === undefined)
103
108
  out.stderr("agent-kitchen: usa 'agent-kitchen apply'; sin subcomando dejará de instalar en una versión mayor.\n");
104
109
  const skip = (values.skip ?? "").split(",").map((name) => name.trim()).filter((name) => name !== "");
105
- return sync({ cwd: process.cwd(), yes: values.yes ?? false, dryRun: values["dry-run"] ?? false, verbose: values.verbose ?? false, skip }, out);
110
+ return sync({ cwd: process.cwd(), yes: values.yes ?? false, dryRun: values["dry-run"] ?? false, verbose: values.verbose ?? false, skip, commit: values.commit ?? false, pieces: values.pieces ?? false }, out);
106
111
  }
107
112
  try {
108
113
  process.exitCode = await main(process.argv.slice(2));
@@ -1,12 +1,4 @@
1
- /**
2
- * @file `agent-kitchen doctor`: whether each piece the repo has works, with the doctor of each
3
- * tool at the repo's own version: the version of skills-manager agent-kitchen needs, the
4
- * issue flow (and its skills at its tag),
5
- * `skills-manager check`, the version `.claude/cloud.json` pins, and claude-repo-guard. It
6
- * changes nothing. A piece the repo does not have, or a newer release, is only noted, and so is
7
- * a missing rule in AGENTS.md.
8
- */
9
- import type { Output } from "../output.js";
1
+ import { type Output } from "../output.js";
10
2
  /**
11
3
  * Checks the repo; see the file's header.
12
4
  * @param cwd - Where it runs.
@@ -1,8 +1,19 @@
1
- import { agentsMdChecks } from "../pieces/agents-md.js";
2
- import { cloudChecks } from "../pieces/cloud.js";
3
- import { guardChecks } from "../pieces/guard.js";
4
- import { issueFlowChecks } from "../pieces/issue-flow.js";
5
- import { skillsChecks } from "../pieces/skills.js";
1
+ /**
2
+ * @file `agent-kitchen doctor`: whether each piece the repo has works, with the doctor of each
3
+ * tool at the repo's own version: the version of skills-manager agent-kitchen needs, the
4
+ * issue flow (and its skills at its tag),
5
+ * `skills-manager check`, the version `.claude/cloud.json` pins, and claude-repo-guard. It
6
+ * changes nothing. A piece the repo does not have, or a newer release, is only noted, and so is
7
+ * a missing rule in AGENTS.md. A piece the repo decided against (`.agents/kitchen.json`) is
8
+ * only noted, and its tool is not asked.
9
+ */
10
+ import { readDecisions } from "../decisions.js";
11
+ import { warn } from "../output.js";
12
+ import { agentsMdChecks, NAME as AGENTS_MD } from "../pieces/agents-md.js";
13
+ import { cloudChecks, NAME as CLOUD } from "../pieces/cloud.js";
14
+ import { guardChecks, NAME as GUARD } from "../pieces/guard.js";
15
+ import { issueFlowChecks, NAME as FLOW } from "../pieces/issue-flow.js";
16
+ import { NAME as SKILLS, skillsChecks } from "../pieces/skills.js";
6
17
  import { skillsManagerChecks } from "../pieces/skills-manager.js";
7
18
  import { mainCheckout } from "../repo.js";
8
19
  /** The mark of each check's status. */
@@ -17,7 +28,12 @@ const MARKS = { ok: "✓", fail: "✗", info: "·" };
17
28
  */
18
29
  export async function doctor(cwd, out) {
19
30
  const root = mainCheckout(cwd, "agent-kitchen doctor");
20
- const checks = (await Promise.all([skillsManagerChecks(root), issueFlowChecks(root), skillsChecks(root), cloudChecks(root), guardChecks(root), agentsMdChecks(root)])).flat();
31
+ const saved = readDecisions(root);
32
+ for (const warning of saved?.warnings ?? [])
33
+ warn(out, warning);
34
+ /** The checks of a piece, or a note if the repo left it out. */
35
+ const unless = (name, run) => saved?.pieces[name] === false ? Promise.resolve([{ name, status: "info", detail: "fuera por decisión del repo" }]) : run();
36
+ const checks = (await Promise.all([skillsManagerChecks(root), unless(FLOW, () => issueFlowChecks(root)), unless(SKILLS, () => skillsChecks(root)), unless(CLOUD, () => cloudChecks(root)), unless(GUARD, () => guardChecks(root)), unless(AGENTS_MD, () => agentsMdChecks(root))])).flat();
21
37
  for (const check of checks)
22
38
  out.stdout(`${MARKS[check.status]} ${check.name}: ${check.detail}\n`);
23
39
  return checks.some((check) => check.status === "fail") ? 1 : 0;
@@ -6,6 +6,9 @@ export interface SyncOptions {
6
6
  dryRun: boolean;
7
7
  verbose: boolean;
8
8
  skip: string[];
9
+ commit: boolean;
10
+ /** Ask which pieces the repo uses, save the answer, then go on as usual. */
11
+ pieces: boolean;
9
12
  }
10
13
  /**
11
14
  * The names `--skip` takes.
@@ -13,12 +16,29 @@ export interface SyncOptions {
13
16
  * @example pieceNames() // ["skills-manager", "issue-flow", "claude-cloud", "claude-repo-guard", "skills", "agents-md"]
14
17
  */
15
18
  export declare function pieceNames(): string[];
19
+ /**
20
+ * The closing line when no step ran, which must not contradict a file written in this run.
21
+ * @param why - "leave" if the person left without changing anything, "none" if they picked no steps.
22
+ * @param wrote - Whether the decisions file was written in this run.
23
+ * @returns The line, with its newline.
24
+ * @example noStepsMessage("none", true) // "No elegiste pasos; solo guardé qué piezas usa el repo.\n"
25
+ */
26
+ export declare function noStepsMessage(why: "leave" | "none", wrote: boolean): string;
27
+ /**
28
+ * What the commit of `--commit` lists: the steps that worked, and the decisions file if this
29
+ * run wrote it.
30
+ * @param done - The labels of the steps that worked.
31
+ * @param wrote - Whether the decisions file was written in this run.
32
+ * @returns The labels.
33
+ * @example commitLabels([], true) // ["Guardar qué piezas usa el repo en .agents/kitchen.json"]
34
+ */
35
+ export declare function commitLabels(done: string[], wrote: boolean): string[];
16
36
  /**
17
37
  * Runs the command; see the file's header.
18
38
  * @param options - The options.
19
39
  * @param out - Where to write and ask.
20
40
  * @returns 0, or 1 if a step failed or the person cancelled.
21
41
  * @throws KitError outside the main checkout or with a broken file; UsageError for an unknown piece.
22
- * @example await sync({ cwd, yes: false, dryRun: true, verbose: false, skip: [] }, out)
42
+ * @example await sync({ cwd, yes: false, dryRun: true, verbose: false, skip: [], commit: false, pieces: false }, out)
23
43
  */
24
44
  export declare function sync(options: SyncOptions, out: Output): Promise<number>;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @file `agent-kitchen apply [--yes] [--dry-run] [--verbose] [--skip <piece>,...]`: the one command
2
+ * @file `agent-kitchen apply [--yes] [--dry-run] [--commit] [--verbose] [--skip <piece>,...]`: the one command
3
3
  * that changes the repo (and, for now, also the bare `agent-kitchen`).
4
4
  * It looks at each piece (the version of skills-manager, the issue flow, claude-cloud,
5
5
  * claude-repo-guard, the skills, the rule in AGENTS.md) with
@@ -11,10 +11,22 @@
11
11
  * flow's version), one line each, with the
12
12
  * tools' output only when one fails; one that fails does not stop the rest. With `--yes`
13
13
  * it does what is suggested without asking (and keeps the repo's own skills); without a
14
- * terminal it only shows the steps.
14
+ * terminal it only shows the steps. With `--commit` it first checks the repo is clean and on
15
+ * its base branch, and at the end installs if package.json changed and asks what to do with
16
+ * the changes (see git-finish.ts), also when the only change is the decisions file below;
17
+ * the steps then show as plain lines, since a spinner before a question leaves it deaf.
18
+ * What the repo decided of each piece, in `.agents/kitchen.json`, is respected: a piece set to
19
+ * false is left out as with `--skip`, and one with no decision is marked as new. Without
20
+ * asking (`--yes` or no terminal), a new piece that is not installed is left out for the run.
21
+ * In a terminal, once the person has answered, the pieces without a decision get one and the
22
+ * file is written; `--yes`, `--dry-run` and `--skip` never write it.
23
+ * `--pieces` (terminal only, not with `--yes` or `--dry-run`) first asks which pieces the repo
24
+ * uses, writes all of them to the file, and then goes on as usual.
15
25
  */
16
26
  import { spinner } from "@clack/prompts";
17
- import { askPath, askReplace, askSkills, askSteps } from "../menu.js";
27
+ import { DECIDABLE, decide, DECISIONS, off, readDecisions, undecided, writeDecisions } from "../decisions.js";
28
+ import { baseBranch, branchName, changedPaths, checkReady, finish } from "../git-finish.js";
29
+ import { askFinish, askPath, askPieces, askReplace, askSkills, askSteps } from "../menu.js";
18
30
  import { KitError, UsageError, warn } from "../output.js";
19
31
  import { cloudStatus, NAME as CLOUD } from "../pieces/cloud.js";
20
32
  import { guardStatus, NAME as GUARD } from "../pieces/guard.js";
@@ -23,7 +35,7 @@ import { keepOwn, NAME as SKILLS, ownInPick, skillsStatus } from "../pieces/skil
23
35
  import { agentsMdStatus, NAME as AGENTS_MD } from "../pieces/agents-md.js";
24
36
  import { NAME as SKILLS_MANAGER, skillsManagerStatus } from "../pieces/skills-manager.js";
25
37
  import { mainCheckout, packageManager } from "../repo.js";
26
- import { download, failedOutput, setVerbose } from "../tools.js";
38
+ import { download, failedOutput, runInstall, setVerbose } from "../tools.js";
27
39
  /** Every piece, in the order its steps run. */
28
40
  const PIECES = [SKILLS_MANAGER, FLOW, CLOUD, GUARD, SKILLS, AGENTS_MD];
29
41
  /**
@@ -35,37 +47,119 @@ export function pieceNames() {
35
47
  return [...PIECES];
36
48
  }
37
49
  /**
38
- * Each piece's status, with every lookup at once. The guard's step and the rule in AGENTS.md
39
- * depend on whether the flow is about to be installed (it writes package.json), so they wait
40
- * for the flow's status.
50
+ * Each piece's status, with every lookup at once. skills-manager, the guard's step and the
51
+ * rule in AGENTS.md depend on whether the flow is about to be installed (it writes
52
+ * package.json), so they wait for the flow's status.
41
53
  * @param root - The repo.
42
54
  * @param skip - The pieces left out.
43
55
  * @returns The statuses, in the pieces' order.
44
56
  */
45
57
  async function statusesOf(root, skip) {
46
- const skillsManager = skip.includes(SKILLS_MANAGER) ? null : skillsManagerStatus(root, !skip.includes(SKILLS));
47
58
  const flow = skip.includes(FLOW) ? null : issueFlowStatus(root);
59
+ const flowComing = (flow ?? Promise.resolve(null)).then((status) => status !== null && !status.installed && status.steps.length > 0);
60
+ const skillsManager = skip.includes(SKILLS_MANAGER) ? null : flowComing.then((coming) => skillsManagerStatus(root, !skip.includes(SKILLS), coming));
48
61
  const cloud = skip.includes(CLOUD) ? null : cloudStatus(root);
49
62
  const skills = skip.includes(SKILLS) ? null : skillsStatus(root, !skip.includes(FLOW));
50
- const flowComing = (flow ?? Promise.resolve(null)).then((status) => status !== null && !status.installed && status.steps.length > 0);
51
63
  const guard = skip.includes(GUARD) ? null : flowComing.then((coming) => guardStatus(root, coming));
52
64
  const agentsMd = skip.includes(AGENTS_MD) ? null : flowComing.then((coming) => agentsMdStatus(root, coming));
53
65
  const all = await Promise.all([skillsManager, flow, cloud, guard, skills, agentsMd]);
54
66
  return all.flatMap((status) => status ?? []);
55
67
  }
56
68
  /**
57
- * Prints what the repo has of each piece.
69
+ * Prints what the repo has of each piece, in the pieces' order, and the pieces the repo
70
+ * decided not to have, with how to change that.
58
71
  * @param statuses - The pieces' statuses.
72
+ * @param decidedOff - The pieces left out by the repo's decision.
59
73
  * @param out - Where to write.
60
74
  */
61
- function printStatus(statuses, out) {
62
- const width = Math.max(...statuses.map((status) => status.name.length));
75
+ function printStatus(statuses, decidedOff, out) {
76
+ const width = Math.max(...statuses.map((status) => status.name.length), ...decidedOff.map((name) => name.length));
63
77
  const stateWidth = Math.max(...statuses.map((status) => status.state.length));
64
78
  out.stdout("\nEste repo tiene:\n");
65
- for (const status of statuses)
66
- out.stdout(` ${status.name.padEnd(width)} ${status.state.padEnd(stateWidth)} · ${status.summary}\n`);
79
+ for (const name of PIECES) {
80
+ for (const status of statuses.filter((each) => each.name === name))
81
+ out.stdout(` ${status.name.padEnd(width)} ${status.state.padEnd(stateWidth)} · ${status.summary}\n`);
82
+ if (decidedOff.includes(name))
83
+ out.stdout(` ${name.padEnd(width)} fuera por decisión del repo\n`);
84
+ }
85
+ if (decidedOff.length > 0)
86
+ out.stdout(`\nPara cambiar qué piezas usa el repo: agent-kitchen apply --pieces (o edita ${DECISIONS}).\n`);
67
87
  out.stdout("\n");
68
88
  }
89
+ /**
90
+ * Settles the pieces without a decision and writes the file, unless nothing changed or
91
+ * nothing was decided.
92
+ * @param root - The repo.
93
+ * @param saved - What was read, or null if there is no file.
94
+ * @param statuses - What each piece found.
95
+ * @param chosen - The steps the person picked.
96
+ * @param commits - Whether `--commit` asks about the changes at the end.
97
+ * @param out - Where to write.
98
+ * @returns True if the file was written.
99
+ */
100
+ function saveDecisions(root, saved, statuses, chosen, commits, out) {
101
+ const previous = saved?.pieces ?? {};
102
+ const pieces = decide(statuses, chosen, previous);
103
+ const same = Object.keys(pieces).length === Object.keys(previous).length && Object.entries(pieces).every(([name, value]) => previous[name] === value);
104
+ if (Object.keys(pieces).length === 0 || (saved !== null && same))
105
+ return false;
106
+ writeDecisions(root, pieces, saved?.raw ?? {});
107
+ out.stdout(savedMessage(commits));
108
+ return true;
109
+ }
110
+ /**
111
+ * The closing line when no step ran, which must not contradict a file written in this run.
112
+ * @param why - "leave" if the person left without changing anything, "none" if they picked no steps.
113
+ * @param wrote - Whether the decisions file was written in this run.
114
+ * @returns The line, with its newline.
115
+ * @example noStepsMessage("none", true) // "No elegiste pasos; solo guardé qué piezas usa el repo.\n"
116
+ */
117
+ export function noStepsMessage(why, wrote) {
118
+ if (why === "leave")
119
+ return wrote ? "No corrí ningún paso; solo guardé qué piezas usa el repo.\n" : "No se cambió nada.\n";
120
+ return wrote ? "No elegiste pasos; solo guardé qué piezas usa el repo.\n" : "No elegiste nada; no se cambió nada.\n";
121
+ }
122
+ /**
123
+ * The line that says the decisions were saved.
124
+ * @param commits - Whether `--commit` asks about the changes at the end, so the file need not be committed by hand.
125
+ * @returns The line.
126
+ */
127
+ function savedMessage(commits) {
128
+ return `Guardé qué piezas usa el repo en ${DECISIONS}${commits ? "" : "; commitéalo"}.\n`;
129
+ }
130
+ /** The line the commit of `--commit` adds for the decisions file. */
131
+ const DECISIONS_STEP = `Guardar qué piezas usa el repo en ${DECISIONS}`;
132
+ /**
133
+ * What the commit of `--commit` lists: the steps that worked, and the decisions file if this
134
+ * run wrote it.
135
+ * @param done - The labels of the steps that worked.
136
+ * @param wrote - Whether the decisions file was written in this run.
137
+ * @returns The labels.
138
+ * @example commitLabels([], true) // ["Guardar qué piezas usa el repo en .agents/kitchen.json"]
139
+ */
140
+ export function commitLabels(done, wrote) {
141
+ return wrote ? [...done, DECISIONS_STEP] : done;
142
+ }
143
+ /**
144
+ * Asks which pieces the repo uses and writes the answer for every decidable piece.
145
+ * @param root - The repo.
146
+ * @param saved - What was read, or null if there is no file.
147
+ * @param commits - Whether `--commit` asks about the changes at the end.
148
+ * @param out - Where to write.
149
+ * @returns False if the person cancelled.
150
+ */
151
+ async function choosePieces(root, saved, commits, out) {
152
+ out.stdout("Revisando el repo y las últimas versiones…\n");
153
+ // No skip here: an installed, undecided piece passed in --skip still starts ticked.
154
+ const statuses = await statusesOf(root, []);
155
+ const ticked = DECIDABLE.filter((name) => saved?.pieces[name] === true || (saved?.pieces[name] === undefined && statuses.some((status) => status.name === name && status.installed)));
156
+ const picked = await askPieces(DECIDABLE, ticked);
157
+ if (picked === null)
158
+ return false;
159
+ writeDecisions(root, Object.fromEntries(DECIDABLE.map((name) => [name, picked.includes(name)])), saved?.raw ?? {});
160
+ out.stdout(savedMessage(commits));
161
+ return true;
162
+ }
69
163
  /**
70
164
  * Leaves the repo's own skills out of a step's pick, saying so.
71
165
  * @param root - The repo.
@@ -206,7 +300,7 @@ async function runStep(root, step, progress, out) {
206
300
  * @param out - Where to write and ask.
207
301
  * @returns 0, or 1 if a step failed or the person cancelled.
208
302
  * @throws KitError outside the main checkout or with a broken file; UsageError for an unknown piece.
209
- * @example await sync({ cwd, yes: false, dryRun: true, verbose: false, skip: [] }, out)
303
+ * @example await sync({ cwd, yes: false, dryRun: true, verbose: false, skip: [], commit: false, pieces: false }, out)
210
304
  */
211
305
  export async function sync(options, out) {
212
306
  const unknown = options.skip.find((name) => !pieceNames().includes(name));
@@ -214,39 +308,133 @@ export async function sync(options, out) {
214
308
  throw new UsageError(`no existe la pieza ${unknown}; son ${pieceNames().join(", ")}.`);
215
309
  if (options.yes && options.dryRun)
216
310
  throw new UsageError("usa --yes o --dry-run, no los dos.");
311
+ if (options.pieces && (options.yes || options.dryRun))
312
+ throw new UsageError("usa --pieces solo, sin --yes ni --dry-run.");
313
+ if (options.pieces && !out.interactive)
314
+ throw new UsageError("--pieces pregunta qué piezas usa el repo; córrelo en una terminal.");
217
315
  const root = mainCheckout(options.cwd, "agent-kitchen apply");
316
+ const base = options.commit ? baseBranch(root) : null;
317
+ if (base !== null)
318
+ checkReady(root, base);
218
319
  setVerbose(options.verbose);
320
+ let saved = readDecisions(root);
321
+ let wrote = false;
322
+ for (const warning of saved?.warnings ?? [])
323
+ warn(out, warning);
324
+ if (options.pieces) {
325
+ if (!(await choosePieces(root, saved, base !== null, out))) {
326
+ out.stdout("No se cambió nada.\n");
327
+ return 0;
328
+ }
329
+ saved = readDecisions(root);
330
+ wrote = true;
331
+ }
332
+ const skip = [...options.skip, ...off(saved)];
333
+ const asks = out.interactive && !options.yes && !options.dryRun;
334
+ const saves = asks && options.skip.length === 0;
219
335
  if (out.interactive)
220
336
  out.stdout("Revisando el repo y las últimas versiones…\n");
221
- const statuses = await statusesOf(root, options.skip);
222
- printStatus(statuses, out);
337
+ let statuses = await statusesOf(root, skip);
338
+ if (saved !== null && !asks && !options.dryRun) {
339
+ const left = undecided(saved).filter((name) => statuses.some((status) => status.name === name && !status.installed));
340
+ if (left.length > 0) {
341
+ skip.push(...left);
342
+ statuses = await statusesOf(root, skip);
343
+ out.stdout(`Pieza nueva sin decisión: ${left.join(", ")}. Corre agent-kitchen apply en una terminal para elegir.\n`);
344
+ }
345
+ }
346
+ if (saved !== null) {
347
+ const fresh = undecided(saved);
348
+ for (const status of statuses.filter((each) => fresh.includes(each.name))) {
349
+ status.state += " (nueva)";
350
+ for (const step of status.steps)
351
+ step.label += " (nueva)";
352
+ }
353
+ }
354
+ printStatus(statuses, off(saved).filter((name) => !options.skip.includes(name)), out);
223
355
  const steps = statuses.flatMap((status) => status.steps);
356
+ // With --commit, a run that only wrote the decisions file still ends with the question.
357
+ const endWithoutSteps = async () => (base !== null && wrote ? commitChanges(root, base, commitLabels([], true), [], options, out) : 0);
224
358
  if (steps.length === 0) {
359
+ if (saves)
360
+ wrote = saveDecisions(root, saved, statuses, [], base !== null, out) || wrote;
225
361
  out.stdout("Todo al día: no hay nada que instalar ni actualizar.\n");
226
- return 0;
362
+ return endWithoutSteps();
227
363
  }
228
364
  const chosen = await chooseSteps(root, steps, options, out);
365
+ if (saves && Array.isArray(chosen))
366
+ wrote = saveDecisions(root, saved, statuses, chosen, base !== null, out) || wrote;
229
367
  if (chosen === "leave") {
230
- out.stdout("No se cambió nada.\n");
231
- return 0;
368
+ out.stdout(noStepsMessage("leave", wrote));
369
+ return endWithoutSteps();
232
370
  }
233
371
  if (chosen === null) {
372
+ if (wrote) {
373
+ out.stdout(noStepsMessage("leave", wrote));
374
+ return endWithoutSteps();
375
+ }
234
376
  if (!options.dryRun && out.interactive)
235
377
  warn(out, "no se cambió nada.");
236
378
  return options.dryRun || !out.interactive ? 0 : 1;
237
379
  }
238
380
  if (chosen.length === 0) {
239
381
  if (!options.yes)
240
- out.stdout("No elegiste nada; no se cambió nada.\n");
241
- return 0;
382
+ out.stdout(noStepsMessage("none", wrote));
383
+ return endWithoutSteps();
242
384
  }
243
- const progress = progressFor(out, options.verbose);
385
+ const progress = progressFor(out, options.verbose || base !== null);
244
386
  const failed = [];
245
387
  for (const step of chosen) {
246
388
  if (!(await runStep(root, step, progress, out)))
247
389
  failed.push(step.label);
248
390
  }
391
+ if (base !== null)
392
+ return commitChanges(root, base, commitLabels(chosen.filter((step) => !failed.includes(step.label)).map((step) => step.label), wrote), failed, options, out);
249
393
  const next = `Falta: corre '${packageManager(root)} install', revisa el diff y haz commit.`;
250
394
  out.stdout(`\n${failed.length === 0 ? "Listo." : `Terminó, pero fallaron: ${failed.join("; ")}.`} ${next}\n`);
251
395
  return failed.length === 0 ? 0 : 1;
252
396
  }
397
+ /**
398
+ * The end of `--commit`: the install if package.json changed, the changes, the question (or
399
+ * the branch with a PR under --yes) and what git did.
400
+ * @param root - The repo.
401
+ * @param base - The base branch.
402
+ * @param done - What the commit lists: the labels of the steps that worked, and the decisions file if written.
403
+ * @param failed - The labels of the steps that failed.
404
+ * @param options - The options.
405
+ * @param out - Where to write and ask.
406
+ * @returns 0, or 1 if a step, the install or git failed.
407
+ */
408
+ async function commitChanges(root, base, done, failed, options, out) {
409
+ const summary = failed.length === 0 ? "Listo." : `Terminó, pero fallaron: ${failed.join("; ")}.`;
410
+ const changed = changedPaths(root);
411
+ if (changed.length === 0) {
412
+ out.stdout(`\n${summary} No cambió ningún archivo: no hay nada que commitear.\n`);
413
+ return failed.length === 0 ? 0 : 1;
414
+ }
415
+ const manager = packageManager(root);
416
+ const packageChanged = changed.includes("package.json");
417
+ if (packageChanged) {
418
+ const label = `Instalar las dependencias con ${manager}`;
419
+ out.stdout(`→ ${label}\n`);
420
+ const installed = await runInstall(root, manager);
421
+ out.stdout(`${installed.ok ? "✓" : "✗"} ${label}\n`);
422
+ if (!installed.ok) {
423
+ out.stderr(failedOutput(installed.output));
424
+ out.stdout(`\n${summary} No commiteé: el install falló y los cambios quedan sin commit.\n`);
425
+ return 1;
426
+ }
427
+ }
428
+ const paths = changedPaths(root);
429
+ out.stdout(`\n${summary} Cambios:\n${paths.map((path) => ` ${path}`).join("\n")}\n`);
430
+ if (failed.length > 0 && options.yes) {
431
+ out.stdout("No commiteé porque algún paso falló; los cambios quedan sin commit.\n");
432
+ return 1;
433
+ }
434
+ const branch = branchName(root, new Date());
435
+ const choice = options.yes ? "pr" : await askFinish(base, branch, failed.length > 0);
436
+ const result = await finish(root, choice, { base, branch, steps: done, paths, manager, packageChanged });
437
+ for (const line of result.lines)
438
+ out.stdout(`${line}\n`);
439
+ return result.ok && failed.length === 0 ? 0 : 1;
440
+ }
@@ -0,0 +1,54 @@
1
+ import type { PieceStatus, Step } from "./piece.js";
2
+ /** The file with the decisions, relative to the repo. */
3
+ export declare const DECISIONS = ".agents/kitchen.json";
4
+ /** The pieces a repo can decide on, in the order sync runs them. */
5
+ export declare const DECIDABLE: string[];
6
+ /** What was read from the decisions file. */
7
+ export interface Saved {
8
+ /** The valid decisions of the decidable pieces. */
9
+ pieces: Record<string, boolean>;
10
+ /** The whole file, to write back without losing what is not ours. */
11
+ raw: Record<string, unknown>;
12
+ /** What was wrong in the file, in Spanish, for the user. */
13
+ warnings: string[];
14
+ }
15
+ /**
16
+ * Reads the decisions of a repo.
17
+ * @param root - The repo.
18
+ * @returns The decisions, or null if the file does not exist.
19
+ * @throws KitError if the file is not valid JSON.
20
+ * @example readDecisions(root)?.pieces // { "claude-repo-guard": false }
21
+ */
22
+ export declare function readDecisions(root: string): Saved | null;
23
+ /**
24
+ * The decidable pieces with no decision yet.
25
+ * @param saved - What was read, or null if there is no file.
26
+ * @returns Their names; all of them without a file.
27
+ * @example undecided(null) // ["issue-flow", "claude-cloud", ...]
28
+ */
29
+ export declare function undecided(saved: Saved | null): string[];
30
+ /**
31
+ * The pieces the repo decided not to have.
32
+ * @param saved - What was read, or null if there is no file.
33
+ * @returns Their names.
34
+ * @example off({ pieces: { skills: false }, raw: {}, warnings: [] }) // ["skills"]
35
+ */
36
+ export declare function off(saved: Saved | null): string[];
37
+ /**
38
+ * Settles the pieces that have no decision from what the repo has and what was chosen.
39
+ * @param statuses - What each piece found.
40
+ * @param chosen - The steps the user picked.
41
+ * @param previous - The decisions already made; they never change here.
42
+ * @returns `previous` plus a decision for each decidable piece without one: installed or
43
+ * with a chosen step is true, offered steps all unchosen is false, no steps leaves it open.
44
+ * @example decide([{ name: "skills", installed: true, summary: "", state: "", steps: [] }], [], {}) // { skills: true }
45
+ */
46
+ export declare function decide(statuses: PieceStatus[], chosen: Step[], previous: Record<string, boolean>): Record<string, boolean>;
47
+ /**
48
+ * Writes the decisions, keeping every other key of the file.
49
+ * @param root - The repo.
50
+ * @param pieces - The decisions to write.
51
+ * @param raw - The whole file as it was read (empty if there was none).
52
+ * @example writeDecisions(root, { "issue-flow": true }, {})
53
+ */
54
+ export declare function writeDecisions(root: string, pieces: Record<string, boolean>, raw: Record<string, unknown>): void;