agent-kitchen 0.2.0 → 0.3.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
@@ -11,8 +11,10 @@ Al arrancar revisa si npm tiene una versión más nueva de sí mismo. Si la hay,
11
11
 
12
12
  ## En simple
13
13
 
14
- - Muestra cada pieza con la versión que tiene el repo y la última que existe.
15
- - Ofrece instalar lo que falta y actualizar lo que está atrasado, en un menú de casillas. Todo viene marcado; desmarcas lo que no quieres.
14
+ - Muestra cada pieza con la versión que tiene el repo y si está al día.
15
+ - 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.
16
+ - Cada paso es una línea con ✓ o ✗. La salida de cada herramienta solo aparece si falla, o con `--verbose`.
17
+ - Es rápido: las versiones se consultan todas a la vez y las herramientas se descargan mientras respondes.
16
18
  - 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í.
17
19
  - Correrlo de nuevo no rompe nada: si todo está al día, lo dice y termina.
18
20
 
@@ -23,23 +25,25 @@ Al arrancar revisa si npm tiene una versión más nueva de sí mismo. Si la hay,
23
25
  | `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 |
24
26
  | `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` |
25
27
  | `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 | `install` en esa tag | `install` en la tag nueva |
26
- | `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 | El menú de `skills-manager` | `skills-manager update` |
28
+ | `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` |
27
29
 
28
30
  Si npm o GitHub no responden, la pieza se muestra pero no se ofrece actualizarla. Si un paso falla, los demás siguen y el comando dice cómo reintentarlo.
29
31
 
30
32
  ### Cómo se conectan
31
33
 
32
34
  - **Las skills del flujo van con su CLI,** porque la llaman: se instalan en la tag de la versión del flujo y se mueven con él. Las generales llevan cada una su propia versión.
33
- - **Skills nuevas:** si un catálogo trae skills que el repo no tiene, aparecen en el menú sin marcar. Al marcarlas se abre el menú de `skills-manager` para elegir.
35
+ - **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.
36
+ - **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.
34
37
  - **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.
35
38
  - **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.
36
39
 
37
40
  ## Uso
38
41
 
39
42
  ```sh
40
- npx agent-kitchen # menú, en una terminal
41
- npx agent-kitchen --dry-run # solo muestra qué ofrecería
43
+ npx agent-kitchen # pregunta, en una terminal (Enter: todo lo sugerido)
44
+ npx agent-kitchen --dry-run # solo muestra qué haría
42
45
  npx agent-kitchen --yes # hace lo sugerido sin preguntar
46
+ npx agent-kitchen --verbose # muestra la salida de cada herramienta
43
47
  npx agent-kitchen --skip claude-cloud # deja fuera una pieza (o varias, con comas)
44
48
  npx agent-kitchen doctor # revisa cada pieza con su propio doctor
45
49
  ```
@@ -52,7 +56,7 @@ npx agent-kitchen doctor # revisa cada pieza con su propio docto
52
56
 
53
57
  ## Sumar una pieza
54
58
 
55
- Una pieza es un archivo en `src/pieces/` con dos funciones: `status(root)` dice qué tiene el repo y qué pasos ofrece, y `checks(root)` da sus revisiones para `doctor`. Se registra en `PIECES` de `src/commands/sync.ts` y en `src/commands/doctor.ts`. Cada paso corre la herramienta de la pieza, que es dueña de sus archivos. Un catálogo de skills nuevo es una línea en `GENERAL_CATALOGS` de `src/piece.ts`.
59
+ Una pieza es un archivo en `src/pieces/` con dos funciones asíncronas: `status(root)` dice qué tiene el repo y qué pasos ofrece, y `checks(root)` da sus revisiones para `doctor`. Un paso dice qué paquetes descargar antes (`downloads`) y nunca pregunta nada: las preguntas van antes, en `src/menu.ts`. Se registra en `PIECES` de `src/commands/sync.ts` y en `src/commands/doctor.ts`. Cada paso corre la herramienta de la pieza, que es dueña de sus archivos. Un catálogo de skills nuevo es una línea en `GENERAL_CATALOGS` de `src/piece.ts`.
56
60
 
57
61
  ## Desarrollo
58
62
 
@@ -61,7 +65,7 @@ npm install
61
65
  npm test # compila y corre los tests
62
66
  ```
63
67
 
64
- Los tests corren el CLI compilado sobre repos reales. `npx` y `npm` son falsos (`test/fake-tools.mjs`) y registran cada llamada. Las tags de GitHub salen de repos locales a los que git llega reescribiendo `https://github.com/`, así que `git ls-remote` es el real. El menú no se prueba renderizado: se prueba `menuOptions` y se corrió a mano en una terminal.
68
+ Los tests corren el CLI compilado sobre repos reales. `npx` y `npm` son falsos (`test/fake-tools.mjs`) y registran cada llamada. Las tags de GitHub salen de repos locales a los que git llega reescribiendo `https://github.com/`, así que `git ls-remote` es el real. Los menús no se prueban renderizados: se prueban `menuOptions` y `skillOptions`, y se corrieron en una terminal con `expect`. Con `@clack/prompts` 1.8.1, una ruedita mostrada antes de una pregunta deja a la segunda pregunta sin teclado; por eso la revisión inicial es una línea de texto.
65
69
 
66
70
  ## Publicar una versión
67
71
 
package/dist/cli.js CHANGED
@@ -11,23 +11,27 @@ import { parseArgs } from "node:util";
11
11
  import { doctor } from "./commands/doctor.js";
12
12
  import { pieceNames, sync } from "./commands/sync.js";
13
13
  import { KitError, processOutput, UsageError } from "./output.js";
14
- import { compareVersions, isRelease, npmLatest } from "./tools.js";
14
+ import { FLOW_PACKAGE, GENERAL_CATALOGS } from "./piece.js";
15
+ import { PACKAGE as CLOUD_PACKAGE } from "./pieces/cloud.js";
16
+ import { REPO as GUARD_REPO } from "./pieces/guard.js";
17
+ import { compareVersions, isRelease, latestTag, npmLatest } from "./tools.js";
15
18
  /** This package on npm. */
16
19
  const PACKAGE = "agent-kitchen";
17
20
  /** Set in the run that a newer agent-kitchen was handed to, so it never hands over again. */
18
21
  const HANDED_OVER = "AGENT_KITCHEN_HANDED_OVER";
19
22
  /** Usage text. */
20
- const USAGE = `Usage:
21
- agent-kitchen [--yes | --dry-run] [--skip <piece>,...]
23
+ const USAGE = `Uso:
24
+ agent-kitchen [--yes | --dry-run] [--verbose] [--skip <pieza>,...]
22
25
  agent-kitchen doctor
23
26
 
24
- agent-kitchen looks at each piece of the repo's agent tooling (${pieceNames().join(", ")}),
25
- shows what the repo has and the latest of each, and offers to install what is missing and
26
- update what is behind: in a terminal, a menu where you untick what you do not want. --yes
27
- runs what it suggests without asking; without a terminal it only shows it. Run it in the
28
- main checkout, from your terminal.
27
+ agent-kitchen revisa cada pieza de las herramientas de agentes del repo (${pieceNames().join(", ")}),
28
+ muestra qué tiene el repo y la última versión de cada una, y ofrece instalar lo que falta y
29
+ actualizar lo que está atrasado. En una terminal pregunta una vez: hacer todo lo sugerido,
30
+ 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
+ repo, desde tu terminal.
29
33
 
30
- doctor runs each piece's own checks at the repo's version, and changes nothing.`;
34
+ doctor corre las revisiones de cada pieza en la versión del repo, sin cambiar nada.`;
31
35
  /**
32
36
  * This package's version.
33
37
  * @returns The version in its package.json.
@@ -36,19 +40,29 @@ function ownVersion() {
36
40
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
37
41
  return pkg.version ?? "unknown";
38
42
  }
43
+ /**
44
+ * Starts the lookups of the latest versions, so they run while this one checks its own.
45
+ */
46
+ function startLookups() {
47
+ void npmLatest(FLOW_PACKAGE);
48
+ void npmLatest(CLOUD_PACKAGE);
49
+ void latestTag(GUARD_REPO);
50
+ for (const catalog of GENERAL_CATALOGS)
51
+ void latestTag(catalog);
52
+ }
39
53
  /**
40
54
  * Runs the latest agent-kitchen with the same arguments, if npm has one newer than this.
41
55
  * @param argv - The arguments.
42
56
  * @returns Its exit code, or null if this one is the latest or npm cannot tell.
43
57
  */
44
- function handOver(argv) {
58
+ async function handOver(argv) {
45
59
  const own = ownVersion();
46
60
  if (process.env[HANDED_OVER] !== undefined || !isRelease(own))
47
61
  return null;
48
- const latest = npmLatest(PACKAGE);
62
+ const latest = await npmLatest(PACKAGE);
49
63
  if (latest === null || compareVersions(latest, own) <= 0)
50
64
  return null;
51
- process.stderr.write(`agent-kitchen: running ${latest}, the latest (this is ${own}).\n`);
65
+ process.stderr.write(`agent-kitchen: uso ${latest}, la última (esta es ${own}).\n`);
52
66
  const result = spawnSync("npx", ["--yes", `${PACKAGE}@${latest}`, ...argv], { stdio: "inherit", env: { ...process.env, [HANDED_OVER]: own } });
53
67
  return result.status ?? 1;
54
68
  }
@@ -63,7 +77,7 @@ async function main(argv) {
63
77
  const { values, positionals } = parseArgs({
64
78
  args: argv,
65
79
  allowPositionals: true,
66
- options: { yes: { type: "boolean" }, "dry-run": { type: "boolean" }, skip: { type: "string" }, help: { type: "boolean", short: "h" }, version: { type: "boolean" } },
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" } },
67
81
  });
68
82
  if (values.help === true) {
69
83
  out.stdout(`${USAGE}\n`);
@@ -73,19 +87,20 @@ async function main(argv) {
73
87
  out.stdout(`${ownVersion()}\n`);
74
88
  return 0;
75
89
  }
76
- const handed = handOver(argv);
90
+ startLookups();
91
+ const handed = await handOver(argv);
77
92
  if (handed !== null)
78
93
  return handed;
79
94
  const [command, ...rest] = positionals;
80
95
  if (rest.length > 0 || (command !== undefined && command !== "doctor"))
81
- throw new UsageError(`unknown command ${positionals.join(" ")}.`);
96
+ throw new UsageError(`no existe el comando ${positionals.join(" ")}.`);
82
97
  if (command === "doctor") {
83
- if (values.yes !== undefined || values["dry-run"] !== undefined || values.skip !== undefined)
84
- throw new UsageError("doctor takes no flags.");
98
+ if (values.yes !== undefined || values["dry-run"] !== undefined || values.skip !== undefined || values.verbose !== undefined)
99
+ throw new UsageError("doctor no lleva opciones.");
85
100
  return doctor(process.cwd(), out);
86
101
  }
87
102
  const skip = (values.skip ?? "").split(",").map((name) => name.trim()).filter((name) => name !== "");
88
- return sync({ cwd: process.cwd(), yes: values.yes ?? false, dryRun: values["dry-run"] ?? false, skip }, out);
103
+ return sync({ cwd: process.cwd(), yes: values.yes ?? false, dryRun: values["dry-run"] ?? false, verbose: values.verbose ?? false, skip }, out);
89
104
  }
90
105
  try {
91
106
  process.exitCode = await main(process.argv.slice(2));
@@ -11,6 +11,6 @@ import type { Output } from "../output.js";
11
11
  * @param out - Where to write.
12
12
  * @returns 0 if nothing failed, 1 if something did.
13
13
  * @throws KitError outside a checkout or with a broken file.
14
- * @example doctor(process.cwd(), out)
14
+ * @example await doctor(process.cwd(), out)
15
15
  */
16
- export declare function doctor(cwd: string, out: Output): number;
16
+ export declare function doctor(cwd: string, out: Output): Promise<number>;
@@ -11,11 +11,11 @@ const MARKS = { ok: "✓", fail: "✗", info: "·" };
11
11
  * @param out - Where to write.
12
12
  * @returns 0 if nothing failed, 1 if something did.
13
13
  * @throws KitError outside a checkout or with a broken file.
14
- * @example doctor(process.cwd(), out)
14
+ * @example await doctor(process.cwd(), out)
15
15
  */
16
- export function doctor(cwd, out) {
16
+ export async function doctor(cwd, out) {
17
17
  const root = mainCheckout(cwd, "agent-kitchen doctor");
18
- const checks = [...issueFlowChecks(root), ...skillsChecks(root), ...cloudChecks(root), ...guardChecks(root)];
18
+ const checks = (await Promise.all([issueFlowChecks(root), skillsChecks(root), cloudChecks(root), guardChecks(root)])).flat();
19
19
  for (const check of checks)
20
20
  out.stdout(`${MARKS[check.status]} ${check.name}: ${check.detail}\n`);
21
21
  return checks.some((check) => check.status === "fail") ? 1 : 0;
@@ -4,6 +4,7 @@ export interface SyncOptions {
4
4
  cwd: string;
5
5
  yes: boolean;
6
6
  dryRun: boolean;
7
+ verbose: boolean;
7
8
  skip: string[];
8
9
  }
9
10
  /**
@@ -18,6 +19,6 @@ export declare function pieceNames(): string[];
18
19
  * @param out - Where to write and ask.
19
20
  * @returns 0, or 1 if a step failed or the person cancelled.
20
21
  * @throws KitError outside the main checkout or with a broken file; UsageError for an unknown piece.
21
- * @example await sync({ cwd, yes: false, dryRun: true, skip: [] }, out)
22
+ * @example await sync({ cwd, yes: false, dryRun: true, verbose: false, skip: [] }, out)
22
23
  */
23
24
  export declare function sync(options: SyncOptions, out: Output): Promise<number>;
@@ -1,33 +1,48 @@
1
1
  /**
2
- * @file `agent-kitchen [--yes] [--dry-run] [--skip <piece>,...]`: the one command. It looks at
3
- * each piece (the issue flow, claude-cloud, claude-repo-guard, the skills), shows what the
4
- * repo has and the latest of each, and offers the steps that install what is missing or
5
- * update what is behind. In a terminal the person unticks what they do not want; with
6
- * `--yes` it runs the ticked ones; without a terminal it only shows them. The steps run in
7
- * order (the flow first, the skills last, since they follow the flow's version) and one that
8
- * fails does not stop the rest.
2
+ * @file `agent-kitchen [--yes] [--dry-run] [--verbose] [--skip <piece>,...]`: the one command.
3
+ * It looks at each piece (the issue flow, claude-cloud, claude-repo-guard, the skills) with
4
+ * every lookup at once, shows what the repo has, and offers the steps that install what is
5
+ * missing or update what is behind. In a terminal it asks first, all in one go: everything
6
+ * suggested, or pick the steps and the skills; whether to replace the repo's own skills.
7
+ * While the person answers, the tools download. Then the steps run in order (the flow
8
+ * first, the skills last, since they follow the flow's version), one line each, with the
9
+ * tools' output only when one fails; one that fails does not stop the rest. With `--yes`
10
+ * it does what is suggested without asking (and keeps the repo's own skills); without a
11
+ * terminal it only shows the steps.
9
12
  */
10
- import { askSteps } from "../menu.js";
13
+ import { spinner } from "@clack/prompts";
14
+ import { askPath, askReplace, askSkills, askSteps } from "../menu.js";
11
15
  import { KitError, UsageError, warn } from "../output.js";
12
16
  import { cloudStatus, NAME as CLOUD } from "../pieces/cloud.js";
13
17
  import { guardStatus, NAME as GUARD } from "../pieces/guard.js";
14
18
  import { issueFlowStatus, NAME as FLOW } from "../pieces/issue-flow.js";
15
- import { NAME as SKILLS, skillsStatus } from "../pieces/skills.js";
19
+ import { keepOwn, NAME as SKILLS, ownInPick, skillsStatus } from "../pieces/skills.js";
16
20
  import { mainCheckout, packageManager } from "../repo.js";
21
+ import { download, failedOutput, setVerbose } from "../tools.js";
17
22
  /** Every piece, in the order its steps run. */
18
- const PIECES = [
19
- { name: FLOW, status: issueFlowStatus },
20
- { name: CLOUD, status: cloudStatus },
21
- { name: GUARD, status: (root, before) => guardStatus(root, before.some((status) => status.name === FLOW && !status.installed && status.steps.length > 0)) },
22
- { name: SKILLS, status: skillsStatus },
23
- ];
23
+ const PIECES = [FLOW, CLOUD, GUARD, SKILLS];
24
24
  /**
25
25
  * The names `--skip` takes.
26
26
  * @returns The pieces' names.
27
27
  * @example pieceNames() // ["issue-flow", "claude-cloud", "claude-repo-guard", "skills"]
28
28
  */
29
29
  export function pieceNames() {
30
- return PIECES.map((piece) => piece.name);
30
+ return [...PIECES];
31
+ }
32
+ /**
33
+ * Each piece's status, with every lookup at once. The guard's step depends on whether the
34
+ * flow is about to be installed, so it waits for the flow's status.
35
+ * @param root - The repo.
36
+ * @param skip - The pieces left out.
37
+ * @returns The statuses, in the pieces' order.
38
+ */
39
+ async function statusesOf(root, skip) {
40
+ const flow = skip.includes(FLOW) ? null : issueFlowStatus(root);
41
+ const cloud = skip.includes(CLOUD) ? null : cloudStatus(root);
42
+ const skills = skip.includes(SKILLS) ? null : skillsStatus(root, !skip.includes(FLOW));
43
+ const guard = skip.includes(GUARD) ? null : (flow ?? Promise.resolve(null)).then((status) => guardStatus(root, status !== null && !status.installed && status.steps.length > 0));
44
+ const all = await Promise.all([flow, cloud, guard, skills]);
45
+ return all.flatMap((status) => status ?? []);
31
46
  }
32
47
  /**
33
48
  * Prints what the repo has of each piece.
@@ -36,28 +51,145 @@ export function pieceNames() {
36
51
  */
37
52
  function printStatus(statuses, out) {
38
53
  const width = Math.max(...statuses.map((status) => status.name.length));
39
- out.stdout("This repo has:\n");
54
+ const stateWidth = Math.max(...statuses.map((status) => status.state.length));
55
+ out.stdout("\nEste repo tiene:\n");
40
56
  for (const status of statuses)
41
- out.stdout(` ${status.name.padEnd(width)} ${status.state} · ${status.summary}\n`);
57
+ out.stdout(` ${status.name.padEnd(width)} ${status.state.padEnd(stateWidth)} · ${status.summary}\n`);
58
+ out.stdout("\n");
59
+ }
60
+ /**
61
+ * Leaves the repo's own skills out of a step's pick, saying so.
62
+ * @param root - The repo.
63
+ * @param step - A step that installs skills.
64
+ * @param out - Where to write.
65
+ */
66
+ function keepOwnSkills(root, step, out) {
67
+ if (step.skills === undefined)
68
+ return;
69
+ const own = ownInPick(root, step.skills);
70
+ if (own.length === 0)
71
+ return;
72
+ Object.assign(step.skills, keepOwn(step.skills, own));
73
+ out.stdout(`Se dejan fuera las skills que el repo ya tiene como propias: ${own.join(", ")}. Para reemplazarlas, corre agent-kitchen en una terminal.\n`);
74
+ }
75
+ /**
76
+ * Asks, for a step that installs skills, which ones, and whether to replace the repo's own.
77
+ * @param root - The repo.
78
+ * @param step - The step.
79
+ * @param chooseSkills - Whether to show the skills menu, or keep the step's pick.
80
+ * @returns False if the person cancelled.
81
+ */
82
+ async function askAboutSkills(root, step, chooseSkills) {
83
+ const pick = step.skills;
84
+ if (pick === undefined)
85
+ return true;
86
+ if (chooseSkills) {
87
+ const picked = await askSkills(pick.candidates, pick.picked.length > 0 ? pick.picked : pick.candidates.map((candidate) => candidate.id));
88
+ if (picked === null)
89
+ return false;
90
+ pick.picked = picked;
91
+ }
92
+ const own = ownInPick(root, pick);
93
+ if (own.length === 0)
94
+ return true;
95
+ const replace = await askReplace(own);
96
+ if (replace === null)
97
+ return false;
98
+ if (replace)
99
+ pick.replace = own;
100
+ else
101
+ Object.assign(pick, keepOwn(pick, own));
102
+ return true;
42
103
  }
43
104
  /**
44
- * The steps to run: the ticked ones with `--yes`, the picked ones in a terminal, or none.
105
+ * The steps to run: the suggested ones with `--yes`, what the person picks in a terminal,
106
+ * or none (only shown) with `--dry-run` or without a terminal.
107
+ * @param root - The repo.
45
108
  * @param steps - The steps offered.
46
109
  * @param options - The options.
47
110
  * @param out - Where to write.
48
- * @returns The steps, or null if nothing should run.
111
+ * @returns The steps, "leave" if the person chose to leave, or null if nothing should run (only shown, or cancelled).
49
112
  */
50
- async function chooseSteps(steps, options, out) {
51
- if (options.yes)
52
- return steps.filter((step) => step.checked);
113
+ async function chooseSteps(root, steps, options, out) {
114
+ const suggested = steps.filter((step) => step.checked);
115
+ if (options.yes) {
116
+ if (suggested.length === 0)
117
+ out.stdout(`Todo al día. Opcional, para elegir en una terminal: ${steps.map((step) => step.label).join("; ")}.\n`);
118
+ for (const step of suggested)
119
+ keepOwnSkills(root, step, out);
120
+ return suggested;
121
+ }
53
122
  if (options.dryRun || !out.interactive) {
54
- out.stdout(`It would offer${options.dryRun ? "" : " (run it in a terminal to pick, or with --yes)"}:\n${steps.map((step) => ` [${step.checked ? "x" : " "}] ${step.label}\n`).join("")}`);
123
+ const hint = options.dryRun ? "" : " (para hacerlos, corre agent-kitchen en una terminal, o con --yes)";
124
+ out.stdout(`Pasos sugeridos${hint}:\n${steps.map((step) => ` [${step.checked ? "x" : " "}] ${step.label}\n`).join("")}`);
55
125
  return null;
56
126
  }
57
- const picked = await askSteps(steps);
58
- if (picked === null)
59
- warn(out, "cancelled; nothing changed.");
60
- return picked;
127
+ for (const step of suggested)
128
+ for (const spec of step.downloads)
129
+ void download(root, spec);
130
+ const path = await askPath(suggested.length);
131
+ if (path === "leave")
132
+ return "leave";
133
+ const chosen = path === "all" ? suggested : await askSteps(steps);
134
+ if (chosen === null)
135
+ return null;
136
+ for (const step of chosen) {
137
+ if (!(await askAboutSkills(root, step, path === "pick")))
138
+ return null;
139
+ }
140
+ return chosen;
141
+ }
142
+ /**
143
+ * How progress is shown: a spinner in a terminal (unless the tools print with
144
+ * `--verbose`), plain lines otherwise. Only for the steps, after every question: a spinner
145
+ * shown before a question leaves the second question deaf to the keyboard (@clack/prompts
146
+ * 1.8.1).
147
+ * @param out - Where to write.
148
+ * @param verbose - Whether the tools print on the terminal.
149
+ * @returns The progress.
150
+ */
151
+ function progressFor(out, verbose) {
152
+ if (out.interactive && !verbose) {
153
+ const spin = spinner();
154
+ return { start: (text) => spin.start(text), stop: (text, ok) => (ok ? spin.stop(text) : spin.error(text)) };
155
+ }
156
+ return { start: (text) => out.stdout(`→ ${text}\n`), stop: (text, ok) => out.stdout(`${ok ? "✓" : "✗"} ${text}\n`) };
157
+ }
158
+ /**
159
+ * Runs one step and shows how it went: its notes, and what to do if it failed.
160
+ * @param root - The repo.
161
+ * @param step - The step.
162
+ * @param progress - How progress is shown.
163
+ * @param out - Where to write.
164
+ * @returns Whether it worked.
165
+ * @throws What the step throws that is not a KitError.
166
+ */
167
+ async function runStep(root, step, progress, out) {
168
+ const notes = [];
169
+ const failures = [];
170
+ const context = {
171
+ root,
172
+ note: (text) => notes.push(text),
173
+ fail: (text, output) => failures.push(` ${text}\n${output === undefined ? "" : failedOutput(output)}`),
174
+ };
175
+ progress.start(step.label);
176
+ await Promise.all(step.downloads.map((spec) => download(root, spec)));
177
+ let ok;
178
+ try {
179
+ ok = await step.run(context);
180
+ }
181
+ catch (error) {
182
+ if (!(error instanceof KitError))
183
+ throw error;
184
+ failures.push(` ${error.message}\n`);
185
+ ok = false;
186
+ }
187
+ progress.stop(step.label, ok);
188
+ for (const note of notes)
189
+ out.stdout(` · ${note}\n`);
190
+ for (const failure of failures)
191
+ out.stderr(failure);
192
+ return ok;
61
193
  }
62
194
  /**
63
195
  * Runs the command; see the file's header.
@@ -65,47 +197,47 @@ async function chooseSteps(steps, options, out) {
65
197
  * @param out - Where to write and ask.
66
198
  * @returns 0, or 1 if a step failed or the person cancelled.
67
199
  * @throws KitError outside the main checkout or with a broken file; UsageError for an unknown piece.
68
- * @example await sync({ cwd, yes: false, dryRun: true, skip: [] }, out)
200
+ * @example await sync({ cwd, yes: false, dryRun: true, verbose: false, skip: [] }, out)
69
201
  */
70
202
  export async function sync(options, out) {
71
203
  const unknown = options.skip.find((name) => !pieceNames().includes(name));
72
204
  if (unknown !== undefined)
73
- throw new UsageError(`unknown piece ${unknown}; there are ${pieceNames().join(", ")}.`);
205
+ throw new UsageError(`no existe la pieza ${unknown}; son ${pieceNames().join(", ")}.`);
74
206
  if (options.yes && options.dryRun)
75
- throw new UsageError("use --yes or --dry-run, not both.");
207
+ throw new UsageError("usa --yes o --dry-run, no los dos.");
76
208
  const root = mainCheckout(options.cwd, "agent-kitchen");
77
- const statuses = [];
78
- for (const piece of PIECES.filter((item) => !options.skip.includes(item.name)))
79
- statuses.push(piece.status(root, statuses));
209
+ setVerbose(options.verbose);
210
+ if (out.interactive)
211
+ out.stdout("Revisando el repo y las últimas versiones…\n");
212
+ const statuses = await statusesOf(root, options.skip);
80
213
  printStatus(statuses, out);
81
214
  const steps = statuses.flatMap((status) => status.steps);
82
215
  if (steps.length === 0) {
83
- out.stdout("Nothing to install or update.\n");
216
+ out.stdout("Todo al día: no hay nada que instalar ni actualizar.\n");
84
217
  return 0;
85
218
  }
86
- const chosen = await chooseSteps(steps, options, out);
87
- if (chosen === null)
219
+ const chosen = await chooseSteps(root, steps, options, out);
220
+ if (chosen === "leave") {
221
+ out.stdout("No se cambió nada.\n");
222
+ return 0;
223
+ }
224
+ if (chosen === null) {
225
+ if (!options.dryRun && out.interactive)
226
+ warn(out, "no se cambió nada.");
88
227
  return options.dryRun || !out.interactive ? 0 : 1;
228
+ }
89
229
  if (chosen.length === 0) {
90
- out.stdout("Nothing picked; nothing changed.\n");
230
+ if (!options.yes)
231
+ out.stdout("No elegiste nada; no se cambió nada.\n");
91
232
  return 0;
92
233
  }
234
+ const progress = progressFor(out, options.verbose);
93
235
  const failed = [];
94
236
  for (const step of chosen) {
95
- out.stdout(`\n→ ${step.label}\n`);
96
- let ok;
97
- try {
98
- ok = step.run({ root, yes: options.yes || !out.interactive, out });
99
- }
100
- catch (error) {
101
- if (!(error instanceof KitError))
102
- throw error;
103
- warn(out, error.message);
104
- ok = false;
105
- }
106
- if (!ok)
237
+ if (!(await runStep(root, step, progress, out)))
107
238
  failed.push(step.label);
108
239
  }
109
- out.stdout(`\n${failed.length === 0 ? "Done." : `Done, but these failed: ${failed.join("; ")}.`} Next: run '${packageManager(root)} install', review the diff and commit it.\n`);
240
+ const next = `Falta: corre '${packageManager(root)} install', revisa el diff y haz commit.`;
241
+ out.stdout(`\n${failed.length === 0 ? "Listo." : `Terminó, pero fallaron: ${failed.join("; ")}.`} ${next}\n`);
110
242
  return failed.length === 0 ? 0 : 1;
111
243
  }
package/dist/menu.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- import type { Step } from "./piece.js";
1
+ import type { Candidate, Step } from "./piece.js";
2
+ /** What the person wants after seeing the table. */
3
+ export type Path = "all" | "pick" | "leave";
2
4
  /**
3
5
  * The menu's options: each step's index and label, which names its piece.
4
6
  * @param steps - The steps.
@@ -12,6 +14,24 @@ export declare function menuOptions(steps: Step[]): {
12
14
  }[];
13
15
  initialValues: number[];
14
16
  };
17
+ /**
18
+ * The skills menu's options: grouped by catalog, each with its description cut short.
19
+ * @param candidates - What the catalogs offer.
20
+ * @returns The groups.
21
+ * @example Object.keys(skillOptions(candidates)) // ["ramon-sg/issue-flow", "ramon-sg/agent-skills"]
22
+ */
23
+ export declare function skillOptions(candidates: Candidate[]): Record<string, {
24
+ value: string;
25
+ label: string;
26
+ hint?: string;
27
+ }[]>;
28
+ /**
29
+ * Asks what to do: everything suggested, pick step by step, or leave.
30
+ * @param count - How many steps are suggested; with none, the option to do them is not shown.
31
+ * @returns The answer; "leave" if the person cancelled.
32
+ * @example await askPath(4) // "all"
33
+ */
34
+ export declare function askPath(count: number): Promise<Path>;
15
35
  /**
16
36
  * Asks which steps to run.
17
37
  * @param steps - The steps.
@@ -19,3 +39,18 @@ export declare function menuOptions(steps: Step[]): {
19
39
  * @example await askSteps(steps)
20
40
  */
21
41
  export declare function askSteps(steps: Step[]): Promise<Step[] | null>;
42
+ /**
43
+ * Asks which skills to install.
44
+ * @param candidates - What the catalogs offer.
45
+ * @param ticked - The ids ticked at the start.
46
+ * @returns The picked ids, or null if the person cancelled.
47
+ * @example await askSkills(candidates, ["catalog:ramon-sg/agent-skills/planning"])
48
+ */
49
+ export declare function askSkills(candidates: Candidate[], ticked: string[]): Promise<string[] | null>;
50
+ /**
51
+ * Asks whether the catalog's skills replace the repo's own ones with the same name.
52
+ * @param own - Their names.
53
+ * @returns True to replace them, false to keep them, or null if the person cancelled.
54
+ * @example await askReplace(["planning"])
55
+ */
56
+ export declare function askReplace(own: string[]): Promise<boolean | null>;