agent-kitchen 0.5.0 → 0.7.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;
@@ -1,4 +1,5 @@
1
1
  import { type Output } from "../output.js";
2
+ import type { Step } from "../piece.js";
2
3
  /** Options of the command. */
3
4
  export interface SyncOptions {
4
5
  cwd: string;
@@ -6,6 +7,9 @@ export interface SyncOptions {
6
7
  dryRun: boolean;
7
8
  verbose: boolean;
8
9
  skip: string[];
10
+ commit: boolean;
11
+ /** Ask which pieces the repo uses, save the answer, then go on as usual. */
12
+ pieces: boolean;
9
13
  }
10
14
  /**
11
15
  * The names `--skip` takes.
@@ -13,12 +17,29 @@ export interface SyncOptions {
13
17
  * @example pieceNames() // ["skills-manager", "issue-flow", "claude-cloud", "claude-repo-guard", "skills", "agents-md"]
14
18
  */
15
19
  export declare function pieceNames(): string[];
20
+ /**
21
+ * The closing line when no step ran, which must not contradict a file written in this run.
22
+ * @param why - "leave" if the person left without changing anything, "none" if they picked no steps.
23
+ * @param wrote - Whether the decisions file was written in this run.
24
+ * @returns The line, with its newline.
25
+ * @example noStepsMessage("none", true) // "No elegiste pasos; solo guardé qué piezas usa el repo.\n"
26
+ */
27
+ export declare function noStepsMessage(why: "leave" | "none", wrote: boolean): string;
28
+ /**
29
+ * What the commit of `--commit` lists: the steps that worked, and the decisions file if this
30
+ * run wrote it, in the commit's texts (not the interface's).
31
+ * @param done - The steps that worked.
32
+ * @param wrote - Whether the decisions file was written in this run.
33
+ * @returns The lines.
34
+ * @example commitLabels([], true) // ["Guardar qué piezas usa el repo en .agents/kitchen.json"]
35
+ */
36
+ export declare function commitLabels(done: Pick<Step, "text">[], wrote: boolean): string[];
16
37
  /**
17
38
  * Runs the command; see the file's header.
18
39
  * @param options - The options.
19
40
  * @param out - Where to write and ask.
20
41
  * @returns 0, or 1 if a step failed or the person cancelled.
21
42
  * @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)
43
+ * @example await sync({ cwd, yes: false, dryRun: true, verbose: false, skip: [], commit: false, pieces: false }, out)
23
44
  */
24
45
  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,21 @@
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
- import { spinner } from "@clack/prompts";
17
- import { askPath, askReplace, askSkills, askSteps } from "../menu.js";
26
+ import { DECIDABLE, decide, DECISIONS, off, readDecisions, undecided, writeDecisions } from "../decisions.js";
27
+ import { baseBranch, branchName, changedPaths, checkReady, finish } from "../git-finish.js";
28
+ import { askFinish, askPath, askPieces, askReplace, askSkills, askSteps } from "../menu.js";
18
29
  import { KitError, UsageError, warn } from "../output.js";
19
30
  import { cloudStatus, NAME as CLOUD } from "../pieces/cloud.js";
20
31
  import { guardStatus, NAME as GUARD } from "../pieces/guard.js";
@@ -23,7 +34,10 @@ import { keepOwn, NAME as SKILLS, ownInPick, skillsStatus } from "../pieces/skil
23
34
  import { agentsMdStatus, NAME as AGENTS_MD } from "../pieces/agents-md.js";
24
35
  import { NAME as SKILLS_MANAGER, skillsManagerStatus } from "../pieces/skills-manager.js";
25
36
  import { mainCheckout, packageManager } from "../repo.js";
26
- import { download, failedOutput, setVerbose } from "../tools.js";
37
+ import { download, failedOutput, runInstall, setVerbose } from "../tools.js";
38
+ import { commitLine, DECISIONS_LINE } from "../commit-texts.js";
39
+ import { progressFor } from "../progress.js";
40
+ import { stepLabel, ui } from "../texts/index.js";
27
41
  /** Every piece, in the order its steps run. */
28
42
  const PIECES = [SKILLS_MANAGER, FLOW, CLOUD, GUARD, SKILLS, AGENTS_MD];
29
43
  /**
@@ -35,37 +49,116 @@ export function pieceNames() {
35
49
  return [...PIECES];
36
50
  }
37
51
  /**
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.
52
+ * Each piece's status, with every lookup at once. skills-manager, the guard's step and the
53
+ * rule in AGENTS.md depend on whether the flow is about to be installed (it writes
54
+ * package.json), so they wait for the flow's status.
41
55
  * @param root - The repo.
42
56
  * @param skip - The pieces left out.
43
57
  * @returns The statuses, in the pieces' order.
44
58
  */
45
59
  async function statusesOf(root, skip) {
46
- const skillsManager = skip.includes(SKILLS_MANAGER) ? null : skillsManagerStatus(root, !skip.includes(SKILLS));
47
60
  const flow = skip.includes(FLOW) ? null : issueFlowStatus(root);
61
+ const flowComing = (flow ?? Promise.resolve(null)).then((status) => status !== null && !status.installed && status.steps.length > 0);
62
+ const skillsManager = skip.includes(SKILLS_MANAGER) ? null : flowComing.then((coming) => skillsManagerStatus(root, !skip.includes(SKILLS), coming));
48
63
  const cloud = skip.includes(CLOUD) ? null : cloudStatus(root);
49
64
  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
65
  const guard = skip.includes(GUARD) ? null : flowComing.then((coming) => guardStatus(root, coming));
52
66
  const agentsMd = skip.includes(AGENTS_MD) ? null : flowComing.then((coming) => agentsMdStatus(root, coming));
53
67
  const all = await Promise.all([skillsManager, flow, cloud, guard, skills, agentsMd]);
54
68
  return all.flatMap((status) => status ?? []);
55
69
  }
56
70
  /**
57
- * Prints what the repo has of each piece.
71
+ * Prints what the repo has of each piece, in the pieces' order, and the pieces the repo
72
+ * decided not to have, with how to change that.
58
73
  * @param statuses - The pieces' statuses.
74
+ * @param decidedOff - The pieces left out by the repo's decision.
59
75
  * @param out - Where to write.
60
76
  */
61
- function printStatus(statuses, out) {
62
- const width = Math.max(...statuses.map((status) => status.name.length));
77
+ function printStatus(statuses, decidedOff, out) {
78
+ const width = Math.max(...statuses.map((status) => status.name.length), ...decidedOff.map((name) => name.length));
63
79
  const stateWidth = Math.max(...statuses.map((status) => status.state.length));
64
80
  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`);
81
+ for (const name of PIECES) {
82
+ for (const status of statuses.filter((each) => each.name === name))
83
+ out.stdout(` ${status.name.padEnd(width)} ${status.state.padEnd(stateWidth)} · ${status.summary}\n`);
84
+ if (decidedOff.includes(name))
85
+ out.stdout(` ${name.padEnd(width)} fuera por decisión del repo\n`);
86
+ }
87
+ if (decidedOff.length > 0)
88
+ out.stdout(`\nPara cambiar qué piezas usa el repo: agent-kitchen apply --pieces (o edita ${DECISIONS}).\n`);
67
89
  out.stdout("\n");
68
90
  }
91
+ /**
92
+ * Settles the pieces without a decision and writes the file, unless nothing changed or
93
+ * nothing was decided.
94
+ * @param root - The repo.
95
+ * @param saved - What was read, or null if there is no file.
96
+ * @param statuses - What each piece found.
97
+ * @param chosen - The steps the person picked.
98
+ * @param commits - Whether `--commit` asks about the changes at the end.
99
+ * @param out - Where to write.
100
+ * @returns True if the file was written.
101
+ */
102
+ function saveDecisions(root, saved, statuses, chosen, commits, out) {
103
+ const previous = saved?.pieces ?? {};
104
+ const pieces = decide(statuses, chosen, previous);
105
+ const same = Object.keys(pieces).length === Object.keys(previous).length && Object.entries(pieces).every(([name, value]) => previous[name] === value);
106
+ if (Object.keys(pieces).length === 0 || (saved !== null && same))
107
+ return false;
108
+ writeDecisions(root, pieces, saved?.raw ?? {});
109
+ out.stdout(savedMessage(commits));
110
+ return true;
111
+ }
112
+ /**
113
+ * The closing line when no step ran, which must not contradict a file written in this run.
114
+ * @param why - "leave" if the person left without changing anything, "none" if they picked no steps.
115
+ * @param wrote - Whether the decisions file was written in this run.
116
+ * @returns The line, with its newline.
117
+ * @example noStepsMessage("none", true) // "No elegiste pasos; solo guardé qué piezas usa el repo.\n"
118
+ */
119
+ export function noStepsMessage(why, wrote) {
120
+ return `${ui.noSteps(why, wrote)}\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 `${ui.saved(DECISIONS, commits)}\n`;
129
+ }
130
+ /**
131
+ * What the commit of `--commit` lists: the steps that worked, and the decisions file if this
132
+ * run wrote it, in the commit's texts (not the interface's).
133
+ * @param done - The steps that worked.
134
+ * @param wrote - Whether the decisions file was written in this run.
135
+ * @returns The lines.
136
+ * @example commitLabels([], true) // ["Guardar qué piezas usa el repo en .agents/kitchen.json"]
137
+ */
138
+ export function commitLabels(done, wrote) {
139
+ const lines = done.map((step) => commitLine(step.text));
140
+ return wrote ? [...lines, DECISIONS_LINE] : lines;
141
+ }
142
+ /**
143
+ * Asks which pieces the repo uses and writes the answer for every decidable piece.
144
+ * @param root - The repo.
145
+ * @param saved - What was read, or null if there is no file.
146
+ * @param commits - Whether `--commit` asks about the changes at the end.
147
+ * @param out - Where to write.
148
+ * @returns False if the person cancelled.
149
+ */
150
+ async function choosePieces(root, saved, commits, out) {
151
+ out.stdout(`${ui.checking}\n`);
152
+ // No skip here: an installed, undecided piece passed in --skip still starts ticked.
153
+ const statuses = await statusesOf(root, []);
154
+ const ticked = DECIDABLE.filter((name) => saved?.pieces[name] === true || (saved?.pieces[name] === undefined && statuses.some((status) => status.name === name && status.installed)));
155
+ const picked = await askPieces(DECIDABLE, ticked);
156
+ if (picked === null)
157
+ return false;
158
+ writeDecisions(root, Object.fromEntries(DECIDABLE.map((name) => [name, picked.includes(name)])), saved?.raw ?? {});
159
+ out.stdout(savedMessage(commits));
160
+ return true;
161
+ }
69
162
  /**
70
163
  * Leaves the repo's own skills out of a step's pick, saying so.
71
164
  * @param root - The repo.
@@ -79,7 +172,7 @@ function keepOwnSkills(root, step, out) {
79
172
  if (own.length === 0)
80
173
  return;
81
174
  Object.assign(step.skills, keepOwn(step.skills, own));
82
- out.stdout(`Se dejan fuera las skills que el repo ya tiene como propias: ${own.join(", ")}. Para reemplazarlas, corre agent-kitchen apply en una terminal.\n`);
175
+ out.stdout(`${ui.ownSkillsKept(own)}\n`);
83
176
  }
84
177
  /**
85
178
  * Asks, for a step that installs skills, which ones, and whether to replace the repo's own.
@@ -123,14 +216,13 @@ async function chooseSteps(root, steps, options, out) {
123
216
  const suggested = steps.filter((step) => step.checked);
124
217
  if (options.yes) {
125
218
  if (suggested.length === 0)
126
- out.stdout(`Todo al día. Opcional, para elegir en una terminal: ${steps.map((step) => step.label).join("; ")}.\n`);
219
+ out.stdout(`${ui.onlyOptional(steps.map(stepLabel))}\n`);
127
220
  for (const step of suggested)
128
221
  keepOwnSkills(root, step, out);
129
222
  return suggested;
130
223
  }
131
224
  if (options.dryRun || !out.interactive) {
132
- const hint = options.dryRun ? "" : " (para hacerlos, corre agent-kitchen apply en una terminal, o con --yes)";
133
- out.stdout(`Pasos sugeridos${hint}:\n${steps.map((step) => ` [${step.checked ? "x" : " "}] ${step.label}\n`).join("")}`);
225
+ out.stdout(`${ui.suggested(options.dryRun)}\n${steps.map((step) => ` [${step.checked ? "x" : " "}] ${stepLabel(step)}\n`).join("")}`);
134
226
  return null;
135
227
  }
136
228
  for (const step of suggested)
@@ -148,22 +240,6 @@ async function chooseSteps(root, steps, options, out) {
148
240
  }
149
241
  return chosen;
150
242
  }
151
- /**
152
- * How progress is shown: a spinner in a terminal (unless the tools print with
153
- * `--verbose`), plain lines otherwise. Only for the steps, after every question: a spinner
154
- * shown before a question leaves the second question deaf to the keyboard (@clack/prompts
155
- * 1.8.1).
156
- * @param out - Where to write.
157
- * @param verbose - Whether the tools print on the terminal.
158
- * @returns The progress.
159
- */
160
- function progressFor(out, verbose) {
161
- if (out.interactive && !verbose) {
162
- const spin = spinner();
163
- return { start: (text) => spin.start(text), stop: (text, ok) => (ok ? spin.stop(text) : spin.error(text)) };
164
- }
165
- return { start: (text) => out.stdout(`→ ${text}\n`), stop: (text, ok) => out.stdout(`${ok ? "✓" : "✗"} ${text}\n`) };
166
- }
167
243
  /**
168
244
  * Runs one step and shows how it went: its notes, and what to do if it failed.
169
245
  * @param root - The repo.
@@ -181,19 +257,21 @@ async function runStep(root, step, progress, out) {
181
257
  note: (text) => notes.push(text),
182
258
  fail: (text, output) => failures.push(` ${text}\n${output === undefined ? "" : failedOutput(output)}`),
183
259
  };
184
- progress.start(step.label);
185
- await Promise.all(step.downloads.map((spec) => download(root, spec)));
260
+ progress.start(step.text);
186
261
  let ok;
187
262
  try {
263
+ await Promise.all(step.downloads.map((spec) => download(root, spec)));
188
264
  ok = await step.run(context);
189
265
  }
190
266
  catch (error) {
191
- if (!(error instanceof KitError))
267
+ if (!(error instanceof KitError)) {
268
+ progress.stop(step.text, false);
192
269
  throw error;
270
+ }
193
271
  failures.push(` ${error.message}\n`);
194
272
  ok = false;
195
273
  }
196
- progress.stop(step.label, ok);
274
+ progress.stop(step.text, ok);
197
275
  for (const note of notes)
198
276
  out.stdout(` · ${note}\n`);
199
277
  for (const failure of failures)
@@ -206,7 +284,7 @@ async function runStep(root, step, progress, out) {
206
284
  * @param out - Where to write and ask.
207
285
  * @returns 0, or 1 if a step failed or the person cancelled.
208
286
  * @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)
287
+ * @example await sync({ cwd, yes: false, dryRun: true, verbose: false, skip: [], commit: false, pieces: false }, out)
210
288
  */
211
289
  export async function sync(options, out) {
212
290
  const unknown = options.skip.find((name) => !pieceNames().includes(name));
@@ -214,39 +292,134 @@ export async function sync(options, out) {
214
292
  throw new UsageError(`no existe la pieza ${unknown}; son ${pieceNames().join(", ")}.`);
215
293
  if (options.yes && options.dryRun)
216
294
  throw new UsageError("usa --yes o --dry-run, no los dos.");
295
+ if (options.pieces && (options.yes || options.dryRun))
296
+ throw new UsageError("usa --pieces solo, sin --yes ni --dry-run.");
297
+ if (options.pieces && !out.interactive)
298
+ throw new UsageError("--pieces pregunta qué piezas usa el repo; córrelo en una terminal.");
217
299
  const root = mainCheckout(options.cwd, "agent-kitchen apply");
300
+ const base = options.commit ? baseBranch(root) : null;
301
+ if (base !== null)
302
+ checkReady(root, base);
218
303
  setVerbose(options.verbose);
304
+ let saved = readDecisions(root);
305
+ let wrote = false;
306
+ for (const warning of saved?.warnings ?? [])
307
+ warn(out, warning);
308
+ if (options.pieces) {
309
+ if (!(await choosePieces(root, saved, base !== null, out))) {
310
+ out.stdout("No se cambió nada.\n");
311
+ return 0;
312
+ }
313
+ saved = readDecisions(root);
314
+ wrote = true;
315
+ }
316
+ const skip = [...options.skip, ...off(saved)];
317
+ const asks = out.interactive && !options.yes && !options.dryRun;
318
+ const saves = asks && options.skip.length === 0;
219
319
  if (out.interactive)
220
- out.stdout("Revisando el repo y las últimas versiones…\n");
221
- const statuses = await statusesOf(root, options.skip);
222
- printStatus(statuses, out);
320
+ out.stdout(`${ui.checking}\n`);
321
+ let statuses = await statusesOf(root, skip);
322
+ if (saved !== null && !asks && !options.dryRun) {
323
+ const left = undecided(saved).filter((name) => statuses.some((status) => status.name === name && !status.installed));
324
+ if (left.length > 0) {
325
+ skip.push(...left);
326
+ statuses = await statusesOf(root, skip);
327
+ out.stdout(`${ui.undecided(left)}\n`);
328
+ }
329
+ }
330
+ if (saved !== null) {
331
+ const fresh = undecided(saved);
332
+ for (const status of statuses.filter((each) => fresh.includes(each.name))) {
333
+ status.state += ui.fresh;
334
+ for (const step of status.steps)
335
+ step.fresh = true;
336
+ }
337
+ }
338
+ printStatus(statuses, off(saved).filter((name) => !options.skip.includes(name)), out);
223
339
  const steps = statuses.flatMap((status) => status.steps);
340
+ // With --commit, a run that only wrote the decisions file still ends with the question.
341
+ const progress = progressFor(out, options.verbose, base !== null);
342
+ const endWithoutSteps = async () => (base !== null && wrote ? commitChanges(root, base, commitLabels([], true), [], progress, options, out) : 0);
224
343
  if (steps.length === 0) {
225
- out.stdout("Todo al día: no hay nada que instalar ni actualizar.\n");
226
- return 0;
344
+ if (saves)
345
+ wrote = saveDecisions(root, saved, statuses, [], base !== null, out) || wrote;
346
+ out.stdout(`${ui.upToDate}\n`);
347
+ return endWithoutSteps();
227
348
  }
228
349
  const chosen = await chooseSteps(root, steps, options, out);
350
+ if (saves && Array.isArray(chosen))
351
+ wrote = saveDecisions(root, saved, statuses, chosen, base !== null, out) || wrote;
229
352
  if (chosen === "leave") {
230
- out.stdout("No se cambió nada.\n");
231
- return 0;
353
+ out.stdout(noStepsMessage("leave", wrote));
354
+ return endWithoutSteps();
232
355
  }
233
356
  if (chosen === null) {
357
+ if (wrote) {
358
+ out.stdout(noStepsMessage("leave", wrote));
359
+ return endWithoutSteps();
360
+ }
234
361
  if (!options.dryRun && out.interactive)
235
362
  warn(out, "no se cambió nada.");
236
363
  return options.dryRun || !out.interactive ? 0 : 1;
237
364
  }
238
365
  if (chosen.length === 0) {
239
366
  if (!options.yes)
240
- out.stdout("No elegiste nada; no se cambió nada.\n");
241
- return 0;
367
+ out.stdout(noStepsMessage("none", wrote));
368
+ return endWithoutSteps();
242
369
  }
243
- const progress = progressFor(out, options.verbose);
244
- const failed = [];
370
+ const failedSteps = [];
245
371
  for (const step of chosen) {
246
372
  if (!(await runStep(root, step, progress, out)))
247
- failed.push(step.label);
373
+ failedSteps.push(step);
248
374
  }
249
- const next = `Falta: corre '${packageManager(root)} install', revisa el diff y haz commit.`;
250
- out.stdout(`\n${failed.length === 0 ? "Listo." : `Terminó, pero fallaron: ${failed.join("; ")}.`} ${next}\n`);
375
+ const failed = failedSteps.map(stepLabel);
376
+ if (base !== null)
377
+ return commitChanges(root, base, commitLabels(chosen.filter((step) => !failedSteps.includes(step)), wrote), failed, progress, options, out);
378
+ out.stdout(`\n${ui.summary(failed)} ${ui.next(packageManager(root))}\n`);
251
379
  return failed.length === 0 ? 0 : 1;
252
380
  }
381
+ /**
382
+ * The end of `--commit`: the install if package.json changed, the changes, the question (or
383
+ * the branch with a PR under --yes) and what git did.
384
+ * @param root - The repo.
385
+ * @param base - The base branch.
386
+ * @param done - What the commit lists: the steps that worked, and the decisions file if written.
387
+ * @param failed - The steps that failed, as the interface names them.
388
+ * @param progress - How progress is shown, for the install.
389
+ * @param options - The options.
390
+ * @param out - Where to write and ask.
391
+ * @returns 0, or 1 if a step, the install or git failed.
392
+ */
393
+ async function commitChanges(root, base, done, failed, progress, options, out) {
394
+ const summary = ui.summary(failed);
395
+ const changed = changedPaths(root);
396
+ if (changed.length === 0) {
397
+ out.stdout(`\n${summary} No cambió ningún archivo: no hay nada que commitear.\n`);
398
+ return failed.length === 0 ? 0 : 1;
399
+ }
400
+ const manager = packageManager(root);
401
+ const packageChanged = changed.includes("package.json");
402
+ if (packageChanged) {
403
+ const text = { kind: "install-deps", manager };
404
+ progress.start(text);
405
+ const installed = await runInstall(root, manager);
406
+ progress.stop(text, installed.ok);
407
+ if (!installed.ok) {
408
+ out.stderr(failedOutput(installed.output));
409
+ out.stdout(`\n${summary} No commiteé: el install falló y los cambios quedan sin commit.\n`);
410
+ return 1;
411
+ }
412
+ }
413
+ const paths = changedPaths(root);
414
+ out.stdout(`\n${summary} Cambios:\n${paths.map((path) => ` ${path}`).join("\n")}\n`);
415
+ if (failed.length > 0 && options.yes) {
416
+ out.stdout("No commiteé porque algún paso falló; los cambios quedan sin commit.\n");
417
+ return 1;
418
+ }
419
+ const branch = branchName(root, new Date());
420
+ const choice = options.yes ? "pr" : await askFinish(base, branch, failed.length > 0);
421
+ const result = await finish(root, choice, { base, branch, steps: done, paths, manager, packageChanged });
422
+ for (const line of result.lines)
423
+ out.stdout(`${line}\n`);
424
+ return result.ok && failed.length === 0 ? 0 : 1;
425
+ }
@@ -0,0 +1,18 @@
1
+ import type { StepText } from "./texts/step-text.js";
2
+ /** The subject of the commit and the title of the PR. */
3
+ export declare const COMMIT_TITLE = "chore: agent-kitchen apply";
4
+ /** The line of the decisions file. */
5
+ export declare const DECISIONS_LINE = "Guardar qu\u00E9 piezas usa el repo en .agents/kitchen.json";
6
+ /**
7
+ * A step's line in the commit and the PR.
8
+ * @param text - What the step does.
9
+ * @returns The line, without the dash.
10
+ * @example commitLine({ kind: "flow-install", version: "0.2.0" }) // "Instalar issue-flow 0.2.0: config del flujo, hooks y scripts"
11
+ */
12
+ export declare function commitLine(text: StepText): string;
13
+ /**
14
+ * The body of the PR.
15
+ * @param lines - What the commit lists.
16
+ * @returns The body.
17
+ */
18
+ export declare function prBody(lines: string[]): string;