agent-kitchen 0.6.0 → 0.8.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
@@ -59,7 +59,7 @@ npx agent-kitchen doctor # revisa cada pieza con su propio
59
59
 
60
60
  - **Sin terminal y sin `--yes`** (por ejemplo, desde un agente), solo muestra lo que ofrecería.
61
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`.
62
- - **Al terminar,** corre el `install` de tu gestor de paquetes, revisa el diff y haz commit. O usa `--commit`.
62
+ - **Al terminar,** si los pasos cambiaron `package.json`, corre el `install` de tu gestor de paquetes. Te queda revisar el diff y hacer commit. O usa `--commit`.
63
63
 
64
64
  **`--commit`** deja los cambios commiteados sin que lo haga un agente:
65
65
 
@@ -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;
@@ -26,13 +27,13 @@ export declare function pieceNames(): string[];
26
27
  export declare function noStepsMessage(why: "leave" | "none", wrote: boolean): string;
27
28
  /**
28
29
  * 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.
30
+ * run wrote it, in the commit's texts (not the interface's).
31
+ * @param done - The steps that worked.
31
32
  * @param wrote - Whether the decisions file was written in this run.
32
- * @returns The labels.
33
+ * @returns The lines.
33
34
  * @example commitLabels([], true) // ["Guardar qué piezas usa el repo en .agents/kitchen.json"]
34
35
  */
35
- export declare function commitLabels(done: string[], wrote: boolean): string[];
36
+ export declare function commitLabels(done: Pick<Step, "text">[], wrote: boolean): string[];
36
37
  /**
37
38
  * Runs the command; see the file's header.
38
39
  * @param options - The options.
@@ -11,7 +11,8 @@
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. With `--commit` it first checks the repo is clean and on
14
+ * terminal it only shows the steps. Without `--commit`, it installs at the end if the steps
15
+ * changed package.json. With `--commit` it first checks the repo is clean and on
15
16
  * its base branch, and at the end installs if package.json changed and asks what to do with
16
17
  * the changes (see git-finish.ts), also when the only change is the decisions file below;
17
18
  * the steps then show as plain lines, since a spinner before a question leaves it deaf.
@@ -23,7 +24,6 @@
23
24
  * `--pieces` (terminal only, not with `--yes` or `--dry-run`) first asks which pieces the repo
24
25
  * uses, writes all of them to the file, and then goes on as usual.
25
26
  */
26
- import { spinner } from "@clack/prompts";
27
27
  import { DECIDABLE, decide, DECISIONS, off, readDecisions, undecided, writeDecisions } from "../decisions.js";
28
28
  import { baseBranch, branchName, changedPaths, checkReady, finish } from "../git-finish.js";
29
29
  import { askFinish, askPath, askPieces, askReplace, askSkills, askSteps } from "../menu.js";
@@ -34,8 +34,11 @@ import { issueFlowStatus, NAME as FLOW } from "../pieces/issue-flow.js";
34
34
  import { keepOwn, NAME as SKILLS, ownInPick, skillsStatus } from "../pieces/skills.js";
35
35
  import { agentsMdStatus, NAME as AGENTS_MD } from "../pieces/agents-md.js";
36
36
  import { NAME as SKILLS_MANAGER, skillsManagerStatus } from "../pieces/skills-manager.js";
37
- import { mainCheckout, packageManager } from "../repo.js";
37
+ import { mainCheckout, packageManager, packageText } from "../repo.js";
38
38
  import { download, failedOutput, runInstall, setVerbose } from "../tools.js";
39
+ import { commitLine, DECISIONS_LINE } from "../commit-texts.js";
40
+ import { progressFor } from "../progress.js";
41
+ import { phrases, stepLabel, ui } from "../texts/index.js";
39
42
  /** Every piece, in the order its steps run. */
40
43
  const PIECES = [SKILLS_MANAGER, FLOW, CLOUD, GUARD, SKILLS, AGENTS_MD];
41
44
  /**
@@ -115,9 +118,7 @@ function saveDecisions(root, saved, statuses, chosen, commits, out) {
115
118
  * @example noStepsMessage("none", true) // "No elegiste pasos; solo guardé qué piezas usa el repo.\n"
116
119
  */
117
120
  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
+ return `${ui.noSteps(why, wrote)}\n`;
121
122
  }
122
123
  /**
123
124
  * The line that says the decisions were saved.
@@ -125,20 +126,19 @@ export function noStepsMessage(why, wrote) {
125
126
  * @returns The line.
126
127
  */
127
128
  function savedMessage(commits) {
128
- return `Guardé qué piezas usa el repo en ${DECISIONS}${commits ? "" : "; commitéalo"}.\n`;
129
+ return `${ui.saved(DECISIONS, commits)}\n`;
129
130
  }
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
131
  /**
133
132
  * 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.
133
+ * run wrote it, in the commit's texts (not the interface's).
134
+ * @param done - The steps that worked.
136
135
  * @param wrote - Whether the decisions file was written in this run.
137
- * @returns The labels.
136
+ * @returns The lines.
138
137
  * @example commitLabels([], true) // ["Guardar qué piezas usa el repo en .agents/kitchen.json"]
139
138
  */
140
139
  export function commitLabels(done, wrote) {
141
- return wrote ? [...done, DECISIONS_STEP] : done;
140
+ const lines = done.map((step) => commitLine(step.text));
141
+ return wrote ? [...lines, DECISIONS_LINE] : lines;
142
142
  }
143
143
  /**
144
144
  * Asks which pieces the repo uses and writes the answer for every decidable piece.
@@ -149,7 +149,7 @@ export function commitLabels(done, wrote) {
149
149
  * @returns False if the person cancelled.
150
150
  */
151
151
  async function choosePieces(root, saved, commits, out) {
152
- out.stdout("Revisando el repo y las últimas versiones…\n");
152
+ out.stdout(`${ui.checking}\n`);
153
153
  // No skip here: an installed, undecided piece passed in --skip still starts ticked.
154
154
  const statuses = await statusesOf(root, []);
155
155
  const ticked = DECIDABLE.filter((name) => saved?.pieces[name] === true || (saved?.pieces[name] === undefined && statuses.some((status) => status.name === name && status.installed)));
@@ -173,7 +173,7 @@ function keepOwnSkills(root, step, out) {
173
173
  if (own.length === 0)
174
174
  return;
175
175
  Object.assign(step.skills, keepOwn(step.skills, own));
176
- 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`);
176
+ out.stdout(`${ui.ownSkillsKept(own)}\n`);
177
177
  }
178
178
  /**
179
179
  * Asks, for a step that installs skills, which ones, and whether to replace the repo's own.
@@ -217,14 +217,13 @@ async function chooseSteps(root, steps, options, out) {
217
217
  const suggested = steps.filter((step) => step.checked);
218
218
  if (options.yes) {
219
219
  if (suggested.length === 0)
220
- out.stdout(`Todo al día. Opcional, para elegir en una terminal: ${steps.map((step) => step.label).join("; ")}.\n`);
220
+ out.stdout(`${ui.onlyOptional(steps.map(stepLabel))}\n`);
221
221
  for (const step of suggested)
222
222
  keepOwnSkills(root, step, out);
223
223
  return suggested;
224
224
  }
225
225
  if (options.dryRun || !out.interactive) {
226
- const hint = options.dryRun ? "" : " (para hacerlos, corre agent-kitchen apply en una terminal, o con --yes)";
227
- out.stdout(`Pasos sugeridos${hint}:\n${steps.map((step) => ` [${step.checked ? "x" : " "}] ${step.label}\n`).join("")}`);
226
+ out.stdout(`${ui.suggested(options.dryRun)}\n${steps.map((step) => ` [${step.checked ? "x" : " "}] ${stepLabel(step)}\n`).join("")}`);
228
227
  return null;
229
228
  }
230
229
  for (const step of suggested)
@@ -242,22 +241,6 @@ async function chooseSteps(root, steps, options, out) {
242
241
  }
243
242
  return chosen;
244
243
  }
245
- /**
246
- * How progress is shown: a spinner in a terminal (unless the tools print with
247
- * `--verbose`), plain lines otherwise. Only for the steps, after every question: a spinner
248
- * shown before a question leaves the second question deaf to the keyboard (@clack/prompts
249
- * 1.8.1).
250
- * @param out - Where to write.
251
- * @param verbose - Whether the tools print on the terminal.
252
- * @returns The progress.
253
- */
254
- function progressFor(out, verbose) {
255
- if (out.interactive && !verbose) {
256
- const spin = spinner();
257
- return { start: (text) => spin.start(text), stop: (text, ok) => (ok ? spin.stop(text) : spin.error(text)) };
258
- }
259
- return { start: (text) => out.stdout(`→ ${text}\n`), stop: (text, ok) => out.stdout(`${ok ? "✓" : "✗"} ${text}\n`) };
260
- }
261
244
  /**
262
245
  * Runs one step and shows how it went: its notes, and what to do if it failed.
263
246
  * @param root - The repo.
@@ -275,19 +258,21 @@ async function runStep(root, step, progress, out) {
275
258
  note: (text) => notes.push(text),
276
259
  fail: (text, output) => failures.push(` ${text}\n${output === undefined ? "" : failedOutput(output)}`),
277
260
  };
278
- progress.start(step.label);
279
- await Promise.all(step.downloads.map((spec) => download(root, spec)));
261
+ progress.start(step.text);
280
262
  let ok;
281
263
  try {
264
+ await Promise.all(step.downloads.map((spec) => download(root, spec)));
282
265
  ok = await step.run(context);
283
266
  }
284
267
  catch (error) {
285
- if (!(error instanceof KitError))
268
+ if (!(error instanceof KitError)) {
269
+ progress.stop(step.text, false);
286
270
  throw error;
271
+ }
287
272
  failures.push(` ${error.message}\n`);
288
273
  ok = false;
289
274
  }
290
- progress.stop(step.label, ok);
275
+ progress.stop(step.text, ok);
291
276
  for (const note of notes)
292
277
  out.stdout(` · ${note}\n`);
293
278
  for (const failure of failures)
@@ -333,32 +318,33 @@ export async function sync(options, out) {
333
318
  const asks = out.interactive && !options.yes && !options.dryRun;
334
319
  const saves = asks && options.skip.length === 0;
335
320
  if (out.interactive)
336
- out.stdout("Revisando el repo y las últimas versiones…\n");
321
+ out.stdout(`${ui.checking}\n`);
337
322
  let statuses = await statusesOf(root, skip);
338
323
  if (saved !== null && !asks && !options.dryRun) {
339
324
  const left = undecided(saved).filter((name) => statuses.some((status) => status.name === name && !status.installed));
340
325
  if (left.length > 0) {
341
326
  skip.push(...left);
342
327
  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`);
328
+ out.stdout(`${ui.undecided(left)}\n`);
344
329
  }
345
330
  }
346
331
  if (saved !== null) {
347
332
  const fresh = undecided(saved);
348
333
  for (const status of statuses.filter((each) => fresh.includes(each.name))) {
349
- status.state += " (nueva)";
334
+ status.state += ui.fresh;
350
335
  for (const step of status.steps)
351
- step.label += " (nueva)";
336
+ step.fresh = true;
352
337
  }
353
338
  }
354
339
  printStatus(statuses, off(saved).filter((name) => !options.skip.includes(name)), out);
355
340
  const steps = statuses.flatMap((status) => status.steps);
356
341
  // 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);
342
+ const progress = progressFor(out, options.verbose, base !== null);
343
+ const endWithoutSteps = async () => (base !== null && wrote ? commitChanges(root, base, commitLabels([], true), [], progress, options, out) : 0);
358
344
  if (steps.length === 0) {
359
345
  if (saves)
360
346
  wrote = saveDecisions(root, saved, statuses, [], base !== null, out) || wrote;
361
- out.stdout("Todo al día: no hay nada que instalar ni actualizar.\n");
347
+ out.stdout(`${ui.upToDate}\n`);
362
348
  return endWithoutSteps();
363
349
  }
364
350
  const chosen = await chooseSteps(root, steps, options, out);
@@ -382,31 +368,51 @@ export async function sync(options, out) {
382
368
  out.stdout(noStepsMessage("none", wrote));
383
369
  return endWithoutSteps();
384
370
  }
385
- const progress = progressFor(out, options.verbose || base !== null);
386
- const failed = [];
371
+ const pkgBefore = packageText(root);
372
+ const failedSteps = [];
387
373
  for (const step of chosen) {
388
374
  if (!(await runStep(root, step, progress, out)))
389
- failed.push(step.label);
375
+ failedSteps.push(step);
390
376
  }
377
+ const failed = failedSteps.map(stepLabel);
391
378
  if (base !== null)
392
- return commitChanges(root, base, commitLabels(chosen.filter((step) => !failed.includes(step.label)).map((step) => step.label), wrote), failed, options, out);
393
- const next = `Falta: corre '${packageManager(root)} install', revisa el diff y haz commit.`;
394
- out.stdout(`\n${failed.length === 0 ? "Listo." : `Terminó, pero fallaron: ${failed.join("; ")}.`} ${next}\n`);
379
+ return commitChanges(root, base, commitLabels(chosen.filter((step) => !failedSteps.includes(step)), wrote), failed, progress, options, out);
380
+ const installFailed = packageText(root) !== pkgBefore ? !(await install(root, progress, out)) : false;
381
+ if (installFailed)
382
+ failed.push(phrases({ kind: "install-deps", manager: packageManager(root) }).menu);
383
+ out.stdout(`\n${ui.summary(failed)} ${ui.next(installFailed ? packageManager(root) : null)}\n`);
395
384
  return failed.length === 0 ? 0 : 1;
396
385
  }
386
+ /**
387
+ * Runs the repo's install with the same progress as a step, and shows its output if it fails.
388
+ * @param root - The repo.
389
+ * @param progress - How progress is shown.
390
+ * @param out - Where to write.
391
+ * @returns Whether it worked.
392
+ */
393
+ async function install(root, progress, out) {
394
+ const text = { kind: "install-deps", manager: packageManager(root) };
395
+ progress.start(text);
396
+ const installed = await runInstall(root, text.manager);
397
+ progress.stop(text, installed.ok);
398
+ if (!installed.ok)
399
+ out.stderr(failedOutput(installed.output));
400
+ return installed.ok;
401
+ }
397
402
  /**
398
403
  * The end of `--commit`: the install if package.json changed, the changes, the question (or
399
404
  * the branch with a PR under --yes) and what git did.
400
405
  * @param root - The repo.
401
406
  * @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.
407
+ * @param done - What the commit lists: the steps that worked, and the decisions file if written.
408
+ * @param failed - The steps that failed, as the interface names them.
409
+ * @param progress - How progress is shown, for the install.
404
410
  * @param options - The options.
405
411
  * @param out - Where to write and ask.
406
412
  * @returns 0, or 1 if a step, the install or git failed.
407
413
  */
408
- async function commitChanges(root, base, done, failed, options, out) {
409
- const summary = failed.length === 0 ? "Listo." : `Terminó, pero fallaron: ${failed.join("; ")}.`;
414
+ async function commitChanges(root, base, done, failed, progress, options, out) {
415
+ const summary = ui.summary(failed);
410
416
  const changed = changedPaths(root);
411
417
  if (changed.length === 0) {
412
418
  out.stdout(`\n${summary} No cambió ningún archivo: no hay nada que commitear.\n`);
@@ -414,16 +420,9 @@ async function commitChanges(root, base, done, failed, options, out) {
414
420
  }
415
421
  const manager = packageManager(root);
416
422
  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
- }
423
+ if (packageChanged && !(await install(root, progress, out))) {
424
+ out.stdout(`\n${summary} No commiteé: el install falló y los cambios quedan sin commit.\n`);
425
+ return 1;
427
426
  }
428
427
  const paths = changedPaths(root);
429
428
  out.stdout(`\n${summary} Cambios:\n${paths.map((path) => ` ${path}`).join("\n")}\n`);
@@ -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;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @file The texts of the commit and the PR of `apply --commit`. They are always in Spanish,
3
+ * whatever language the interface shows: a repo's history should not change language with the
4
+ * terminal of whoever ran the command. A step reads as its Spanish menu order.
5
+ */
6
+ import { DECISIONS } from "./decisions.js";
7
+ import { es } from "./texts/es.js";
8
+ import { phrasesIn } from "./texts/index.js";
9
+ /** The subject of the commit and the title of the PR. */
10
+ export const COMMIT_TITLE = "chore: agent-kitchen apply";
11
+ /** The line of the decisions file. */
12
+ export const DECISIONS_LINE = `Guardar qué piezas usa el repo en ${DECISIONS}`;
13
+ /**
14
+ * A step's line in the commit and the PR.
15
+ * @param text - What the step does.
16
+ * @returns The line, without the dash.
17
+ * @example commitLine({ kind: "flow-install", version: "0.2.0" }) // "Instalar issue-flow 0.2.0: config del flujo, hooks y scripts"
18
+ */
19
+ export function commitLine(text) {
20
+ return phrasesIn(es, text).menu;
21
+ }
22
+ /**
23
+ * The body of the PR.
24
+ * @param lines - What the commit lists.
25
+ * @returns The body.
26
+ */
27
+ export function prBody(lines) {
28
+ return `Cambios de \`agent-kitchen apply\`:\n\n${lines.map((line) => `- ${line}`).join("\n")}\n`;
29
+ }
@@ -10,12 +10,11 @@ import { spawnSync } from "node:child_process";
10
10
  import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
11
11
  import { tmpdir } from "node:os";
12
12
  import { join, relative } from "node:path";
13
+ import { COMMIT_TITLE as TITLE, prBody } from "./commit-texts.js";
13
14
  import { KitError } from "./output.js";
14
15
  import { FLOW_CONFIG } from "./piece.js";
15
16
  import { dependencyVersion, readJson } from "./repo.js";
16
17
  import { captureNpxWithout, isRelease, npmLatest } from "./tools.js";
17
- /** The subject of the commit and the title of the PR. */
18
- const TITLE = "chore: agent-kitchen apply";
19
18
  /** github-rest on npm, which opens the PR (REST, never gh's GraphQL). */
20
19
  const GITHUB_REST = "@ramon-sg/github-rest";
21
20
  /**
@@ -153,7 +152,7 @@ async function openPr(root, branch, base, steps) {
153
152
  return { error: `no sé qué versión de ${GITHUB_REST} usar.` };
154
153
  const dir = mkdtempSync(join(tmpdir(), "agent-kitchen-pr-"));
155
154
  const bodyFile = join(dir, "body.md");
156
- writeFileSync(bodyFile, `Cambios de \`agent-kitchen apply\`:\n\n${steps.map((step) => `- ${step}`).join("\n")}\n`);
155
+ writeFileSync(bodyFile, prBody(steps));
157
156
  try {
158
157
  const result = await captureNpxWithout(root, [`${GITHUB_REST}@${version}`, "pr", "create", "--head", branch, "--base", base, "--title", TITLE, "--body-file", bodyFile], ["GH_REPO"]);
159
158
  const url = result.out.trim().split("\n").at(-1) ?? "";
package/dist/menu.js CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { styleText } from "node:util";
8
8
  import { confirm, groupMultiselect, isCancel, MULTISELECT_INSTRUCTIONS, multiselect, select, SELECT_INSTRUCTIONS } from "@clack/prompts";
9
+ import { stepLabel } from "./texts/index.js";
9
10
  /** Width a skill's description is cut to in the menu. */
10
11
  const DESCRIPTION_WIDTH = 70;
11
12
  /**
@@ -24,7 +25,7 @@ function spanishKeys() {
24
25
  */
25
26
  export function menuOptions(steps) {
26
27
  return {
27
- options: steps.map((step, index) => ({ value: index, label: step.label })),
28
+ options: steps.map((step, index) => ({ value: index, label: stepLabel(step) })),
28
29
  initialValues: steps.flatMap((step, index) => (step.checked ? [index] : [])),
29
30
  };
30
31
  }
package/dist/piece.d.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  * writes the few lines that join two pieces, the one line of AGENTS.md, which has no tool,
8
8
  * and the version of skills-manager in package.json, which agent-kitchen is the one to run.
9
9
  */
10
+ import type { StepText } from "./texts/step-text.js";
10
11
  /** The issue flow on npm. */
11
12
  export declare const FLOW_PACKAGE = "@ramon-sg/issue-flow";
12
13
  /** The GitHub repo of the issue flow, which is also the catalog of its skills. */
@@ -36,10 +37,15 @@ export interface SkillPick {
36
37
  picked: string[];
37
38
  replace: string[];
38
39
  }
39
- /** One step a piece offers: what it does, what it downloads, and how it runs. */
40
+ /**
41
+ * One step a piece offers: what it does (as data; texts/ turns it into words for each place
42
+ * it shows), what it downloads, and how it runs. `fresh` marks a step of a piece the repo has
43
+ * not decided on yet.
44
+ */
40
45
  export interface Step {
41
46
  piece: string;
42
- label: string;
47
+ text: StepText;
48
+ fresh?: boolean;
43
49
  checked: boolean;
44
50
  downloads: string[];
45
51
  skills?: SkillPick;
@@ -100,10 +100,8 @@ export async function agentsMdStatus(root, flowComing) {
100
100
  const summary = "regla de versiones instaladas en AGENTS.md";
101
101
  if (!lack.rule && !lack.import)
102
102
  return { name: NAME, summary, installed: true, state: "al día", steps: [] };
103
- const files = [...(lack.rule ? ["AGENTS.md"] : []), ...(lack.import ? ["CLAUDE.md"] : [])];
104
- const label = lack.rule ? `Agregar a AGENTS.md la regla de versiones instaladas${lack.import ? " (y @AGENTS.md en CLAUDE.md)" : ""}` : "Agregar @AGENTS.md a CLAUDE.md, para que Claude Code lea AGENTS.md";
105
103
  const state = lack.rule ? "falta la regla" : "CLAUDE.md no importa AGENTS.md";
106
- return { name: NAME, summary, installed: !lack.rule, state, steps: [{ piece: NAME, checked: true, downloads: [], label: `${label}: ${files.join(", ")}`, run: async (context) => write(context) }] };
104
+ return { name: NAME, summary, installed: !lack.rule, state, steps: [{ piece: NAME, checked: true, downloads: [], text: { kind: "agents-md", rule: lack.rule, claudeImport: lack.import }, run: async (context) => write(context) }] };
107
105
  }
108
106
  /**
109
107
  * Whether AGENTS.md has the rule and CLAUDE.md, if there is one, imports it. Only noted.
@@ -28,7 +28,7 @@ function stepFor(config, latest) {
28
28
  piece: NAME,
29
29
  checked: true,
30
30
  downloads: [spec],
31
- label: `Instalar claude-cloud ${latest ?? "1.x"}: prepara las sesiones en la nube de Claude`,
31
+ text: { kind: "cloud-install", version: latest },
32
32
  run: async (context) => {
33
33
  const run = await runNpx(context.root, [spec, "init"]);
34
34
  if (run.ok)
@@ -44,7 +44,7 @@ function stepFor(config, latest) {
44
44
  piece: NAME,
45
45
  checked: true,
46
46
  downloads: [],
47
- label: `Actualizar claude-cloud ${version ?? "?"} → ${latest} (la versión que fija ${CONFIG})`,
47
+ text: { kind: "cloud-update", from: version, to: latest, config: CONFIG },
48
48
  run: async (context) => {
49
49
  const current = readJson(context.root, CONFIG) ?? {};
50
50
  const schema = typeof current.$schema === "string" ? current.$schema.replace(/@ramon-sg\/claude-cloud@[^/]+\//, `${PACKAGE}@${latest}/`) : undefined;
@@ -141,11 +141,11 @@ function installedVersion(root) {
141
141
  */
142
142
  function stepFor(root, installed, latest, flowComing) {
143
143
  if (installed === undefined)
144
- return { piece: NAME, checked: true, downloads: [guardSpec(latest)], label: `Instalar claude-repo-guard ${latest ?? "(main)"}: modo auto con sandbox, y un documento de qué protege`, run: (context) => install(context, latest) };
144
+ return { piece: NAME, checked: true, downloads: [guardSpec(latest)], text: { kind: "guard-install", version: latest }, run: (context) => install(context, latest) };
145
145
  if (latest !== null && (installed === null || compareVersions(latest, installed) > 0))
146
- return { piece: NAME, checked: true, downloads: [guardSpec(latest)], label: `Actualizar claude-repo-guard ${installed ?? "?"} → ${latest}`, run: (context) => install(context, latest) };
146
+ return { piece: NAME, checked: true, downloads: [guardSpec(latest)], text: { kind: "guard-update", from: installed, to: latest }, run: (context) => install(context, latest) };
147
147
  if (flowComing || guardOptions(root, process.platform).changes.length > 0)
148
- return { piece: NAME, checked: true, downloads: [guardSpec(installed)], label: "Ajustar claude-repo-guard a lo que necesita el flujo de issues", run: (context) => addOptions(context, installed) };
148
+ return { piece: NAME, checked: true, downloads: [guardSpec(installed)], text: { kind: "guard-adjust" }, run: (context) => addOptions(context, installed) };
149
149
  return null;
150
150
  }
151
151
  /**
@@ -197,7 +197,7 @@ async function missingLabels(root, version) {
197
197
  * @returns The step.
198
198
  */
199
199
  function labelsStep(version, missing) {
200
- return { piece: NAME, checked: true, downloads: [`${FLOW_PACKAGE}@${version}`], label: `Crear en GitHub los labels que faltan: ${missing.join(", ")}`, run: async (context) => {
200
+ return { piece: NAME, checked: true, downloads: [`${FLOW_PACKAGE}@${version}`], text: { kind: "flow-labels", missing }, run: async (context) => {
201
201
  if (flowVersion(context.root) !== version) {
202
202
  context.note(`no se crearon: el repo no tiene issue-flow ${version}`);
203
203
  return true;
@@ -233,7 +233,7 @@ function restStep(root, version, minimum) {
233
233
  const current = dependencyVersion(readJson(root, "package.json"), GITHUB_REST);
234
234
  if (!below(current))
235
235
  return null;
236
- return { piece: NAME, checked: true, downloads: [], label: `Subir github-rest ${current} → ${minimum}, el mínimo de issue-flow ${version}`, run: async (context) => {
236
+ return { piece: NAME, checked: true, downloads: [], text: { kind: "github-rest-bump", from: current, to: minimum, flow: version }, run: async (context) => {
237
237
  if (flowVersion(context.root) !== version)
238
238
  context.note(`no se subió: el repo no tiene issue-flow ${version}`);
239
239
  else if (!below(dependencyVersion(readJson(context.root, "package.json"), GITHUB_REST)))
@@ -256,16 +256,16 @@ function restStep(root, version, minimum) {
256
256
  function stepFor(installed, latest, root, repair) {
257
257
  if (installed === null && latest !== null) {
258
258
  const run = usesApply(latest) ? async (context) => (await applyFlow(context, latest)) && (await moveSkills(context, latest)) : (context) => runFlow(context, latest, ["init", "--yes"]);
259
- return { piece: NAME, checked: true, downloads: [`${FLOW_PACKAGE}@${latest}`], label: `Instalar issue-flow ${latest}: config del flujo, hooks y scripts`, run };
259
+ return { piece: NAME, checked: true, downloads: [`${FLOW_PACKAGE}@${latest}`], text: { kind: "flow-install", version: latest }, run };
260
260
  }
261
261
  if (installed !== null && latest !== null && compareVersions(latest, installed) > 0) {
262
262
  const update = (context) => (usesApply(latest) ? applyFlow(context, latest) : runFlow(context, latest, ["upgrade"]));
263
- return { piece: NAME, checked: true, downloads: [`${FLOW_PACKAGE}@${latest}`], label: `Actualizar issue-flow ${installed} → ${latest}, con sus hooks y sus skills`, run: async (context) => (await update(context)) && (await moveSkills(context, latest)) };
263
+ return { piece: NAME, checked: true, downloads: [`${FLOW_PACKAGE}@${latest}`], text: { kind: "flow-update", from: installed, to: latest }, run: async (context) => (await update(context)) && (await moveSkills(context, latest)) };
264
264
  }
265
265
  if (installed !== null && repair)
266
- return { piece: NAME, checked: true, downloads: [`${FLOW_PACKAGE}@${installed}`], label: `Reparar issue-flow ${installed}: hooks y $schema`, run: async (context) => (await applyFlow(context, installed)) && (skillsOff(context.root, installed).length === 0 || (await moveSkills(context, installed))) };
266
+ return { piece: NAME, checked: true, downloads: [`${FLOW_PACKAGE}@${installed}`], text: { kind: "flow-repair", version: installed }, run: async (context) => (await applyFlow(context, installed)) && (skillsOff(context.root, installed).length === 0 || (await moveSkills(context, installed))) };
267
267
  if (installed !== null && skillsOff(root, installed).length > 0)
268
- return { piece: NAME, checked: true, downloads: [], label: `Poner las skills del flujo en v${installed}, la versión de issue-flow`, run: (context) => moveSkills(context, installed) };
268
+ return { piece: NAME, checked: true, downloads: [], text: { kind: "flow-skills", version: installed }, run: (context) => moveSkills(context, installed) };
269
269
  return null;
270
270
  }
271
271
  /**
@@ -66,7 +66,7 @@ export function skillsManagerStatus(root, withSkills, flowComing) {
66
66
  piece: NAME,
67
67
  checked: true,
68
68
  downloads: [`${SKILLS_MANAGER}@${MIN_VERSION}`],
69
- label: `Crear package.json con skills-manager ${MIN_VERSION}`,
69
+ text: { kind: "skills-manager-create", version: MIN_VERSION },
70
70
  run: async (context) => {
71
71
  if (readJson(context.root, "package.json") === null)
72
72
  writeJson(context.root, "package.json", { private: true, devDependencies: { [SKILLS_MANAGER]: MIN_VERSION } });
@@ -85,13 +85,13 @@ export function skillsManagerStatus(root, withSkills, flowComing) {
85
85
  }
86
86
  if (now.kind === "exact" && !below(now.version))
87
87
  return { name: NAME, summary, installed: true, state: `${now.version}, al día`, steps: [] };
88
- const label = now.kind === "missing" ? `Agregar skills-manager ${MIN_VERSION} a package.json` : `Subir skills-manager ${now.version} → ${MIN_VERSION}, el mínimo de agent-kitchen`;
88
+ const text = now.kind === "missing" ? { kind: "skills-manager-add", version: MIN_VERSION } : { kind: "skills-manager-bump", from: now.version, to: MIN_VERSION };
89
89
  const state = now.kind === "missing" ? "no está en package.json" : `${now.version}, bajo ${MIN_VERSION}`;
90
90
  const step = {
91
91
  piece: NAME,
92
92
  checked: true,
93
93
  downloads: [`${SKILLS_MANAGER}@${MIN_VERSION}`],
94
- label,
94
+ text,
95
95
  run: async (context) => {
96
96
  const current = pinned(context.root);
97
97
  if (current.kind === "missing" || (current.kind === "exact" && below(current.version))) {
@@ -174,7 +174,7 @@ async function updateStep(root, catalog, ref) {
174
174
  piece: NAME,
175
175
  checked: true,
176
176
  downloads: [skillsManagerSpec(root)],
177
- label: `Actualizar skills de ${catalog} ${from} → v${latest}: ${behind.map((skill) => skill.name).join(", ")}`,
177
+ text: { kind: "skills-update", catalog, from, to: latest, names: behind.map((skill) => skill.name) },
178
178
  run: async (context) => {
179
179
  if (ref !== null)
180
180
  pinCatalog(context.root, catalog, `v${latest}`);
@@ -215,10 +215,9 @@ export async function skillsStatus(root, withFlow) {
215
215
  });
216
216
  const offered = candidates ?? fallback;
217
217
  const catalogs = [...new Set(offered.map((candidate) => candidate.catalog))];
218
- const label = `Instalar ${offered.length} skills de ${catalogs.join(" y ")}`;
219
218
  const skills = { candidates: offered, picked: offered.map((candidate) => candidate.id), replace: [] };
220
219
  const state = candidates === null ? "no instaladas (no se pudieron leer los catálogos)" : "no instaladas";
221
- return { name: NAME, summary, installed: false, state, steps: [{ piece: NAME, checked: true, downloads: [], label, skills, run: (context) => install(context, skills) }] };
220
+ return { name: NAME, summary, installed: false, state, steps: [{ piece: NAME, checked: true, downloads: [], text: { kind: "skills-install", count: offered.length, catalogs }, skills, run: (context) => install(context, skills) }] };
222
221
  }
223
222
  const general = manifestCatalogs(root).filter((source) => source.catalog !== FLOW_CATALOG);
224
223
  const steps = (await Promise.all(general.map((source) => updateStep(root, source.catalog, source.ref)))).flatMap((step) => step ?? []);
@@ -227,7 +226,7 @@ export async function skillsStatus(root, withFlow) {
227
226
  const fresh = (candidates ?? []).filter((candidate) => locked[candidate.name] === undefined);
228
227
  if (fresh.length > 0) {
229
228
  const skills = { candidates: fresh, picked: [], replace: [] };
230
- steps.push({ piece: NAME, checked: false, downloads: [skillsManagerSpec(root)], label: `Agregar skills nuevas (${fresh.length}): ${fresh.map((candidate) => candidate.name).join(", ")}`, skills, run: (context) => install(context, skills) });
229
+ steps.push({ piece: NAME, checked: false, downloads: [skillsManagerSpec(root)], text: { kind: "skills-add", names: fresh.map((candidate) => candidate.name) }, skills, run: (context) => install(context, skills) });
231
230
  }
232
231
  const count = manifestCatalogs(root).reduce((total, source) => total + catalogSkills(root, source.catalog).length, 0);
233
232
  const behind = steps.filter((step) => step.checked).length;
@@ -0,0 +1,30 @@
1
+ import type { Output } from "./output.js";
2
+ import type { StepText } from "./texts/step-text.js";
3
+ /** How a step's progress is shown. */
4
+ export interface Progress {
5
+ start: (text: StepText) => void;
6
+ stop: (text: StepText, ok: boolean) => void;
7
+ }
8
+ /**
9
+ * Progress as plain lines: the start (marked ◒, like the spinner), a line now and then while it
10
+ * runs, and the end (marked ✓ or ✗).
11
+ * @param out - Where to write.
12
+ * @param options - Whether to print while it runs, how often, and the clock.
13
+ * @returns The progress.
14
+ * @example plainProgress(out, { ticks: true }).start({ kind: "guard-adjust" })
15
+ */
16
+ export declare function plainProgress(out: Output, options: {
17
+ ticks: boolean;
18
+ tickMs?: number;
19
+ now?: () => number;
20
+ }): Progress;
21
+ /**
22
+ * How progress is shown: a spinner in a terminal, plain lines otherwise or when `plain` asks
23
+ * for them. Only for the steps, after every question: a spinner shown before a question
24
+ * leaves the second question deaf to the keyboard (@clack/prompts 1.8.1).
25
+ * @param out - Where to write.
26
+ * @param verbose - Whether the tools print on the terminal.
27
+ * @param plain - Whether a question may come after the steps (`--commit`).
28
+ * @returns The progress.
29
+ */
30
+ export declare function progressFor(out: Output, verbose: boolean, plain: boolean): Progress;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @file How a step's progress is shown. Its start says what is happening ("Actualizando…") and
3
+ * its end what is left, with how long it took. In a terminal, a spinner; otherwise plain
4
+ * lines, and a step that takes a while prints how long it has been running, so it does not
5
+ * look stuck (unless the tools print their own output, with `--verbose`).
6
+ */
7
+ import { spinner } from "@clack/prompts";
8
+ import { phrases, ui } from "./texts/index.js";
9
+ /** How often a plain step says it is still running. */
10
+ const TICK_MS = 10_000;
11
+ /**
12
+ * The end line of a step.
13
+ * @param text - What the step does.
14
+ * @param ok - Whether it worked.
15
+ * @param ms - How long it took.
16
+ * @returns The line, without the mark.
17
+ */
18
+ function endLine(text, ok, ms) {
19
+ const said = phrases(text);
20
+ return `${ok ? said.done : ui.failed(said.menu)} (${ui.duration(ms)})`;
21
+ }
22
+ /**
23
+ * Progress as plain lines: the start (marked ◒, like the spinner), a line now and then while it
24
+ * runs, and the end (marked ✓ or ✗).
25
+ * @param out - Where to write.
26
+ * @param options - Whether to print while it runs, how often, and the clock.
27
+ * @returns The progress.
28
+ * @example plainProgress(out, { ticks: true }).start({ kind: "guard-adjust" })
29
+ */
30
+ export function plainProgress(out, options) {
31
+ const now = options.now ?? Date.now;
32
+ let started = 0;
33
+ let timer;
34
+ return {
35
+ start: (text) => {
36
+ started = now();
37
+ out.stdout(`◒ ${phrases(text).doing}\n`);
38
+ if (options.ticks)
39
+ timer = setInterval(() => out.stdout(`${ui.still(ui.duration(now() - started))}\n`), options.tickMs ?? TICK_MS);
40
+ },
41
+ stop: (text, ok) => {
42
+ clearInterval(timer);
43
+ timer = undefined;
44
+ out.stdout(`${ok ? "✓" : "✗"} ${endLine(text, ok, now() - started)}\n`);
45
+ },
46
+ };
47
+ }
48
+ /**
49
+ * How progress is shown: a spinner in a terminal, plain lines otherwise or when `plain` asks
50
+ * for them. Only for the steps, after every question: a spinner shown before a question
51
+ * leaves the second question deaf to the keyboard (@clack/prompts 1.8.1).
52
+ * @param out - Where to write.
53
+ * @param verbose - Whether the tools print on the terminal.
54
+ * @param plain - Whether a question may come after the steps (`--commit`).
55
+ * @returns The progress.
56
+ */
57
+ export function progressFor(out, verbose, plain) {
58
+ if (out.interactive && !verbose && !plain) {
59
+ const spin = spinner();
60
+ let started = 0;
61
+ return {
62
+ start: (text) => {
63
+ started = Date.now();
64
+ spin.start(phrases(text).doing);
65
+ },
66
+ stop: (text, ok) => {
67
+ const line = endLine(text, ok, Date.now() - started);
68
+ if (ok)
69
+ spin.stop(line);
70
+ else
71
+ spin.error(line);
72
+ },
73
+ };
74
+ }
75
+ return plainProgress(out, { ticks: !verbose });
76
+ }
package/dist/repo.d.ts CHANGED
@@ -46,6 +46,13 @@ export declare function dependencyVersion(pkg: Record<string, unknown> | null, n
46
46
  * @example packageManager(root) // "pnpm"
47
47
  */
48
48
  export declare function packageManager(root: string): string;
49
+ /**
50
+ * The text of the repo's package.json, to tell later whether a run changed it.
51
+ * @param root - The repo.
52
+ * @returns Its text, or null if there is none.
53
+ * @example packageText(root) // '{ "devDependencies": … }'
54
+ */
55
+ export declare function packageText(root: string): string | null;
49
56
  /**
50
57
  * The version of a package installed in the repo's `node_modules`.
51
58
  * @param root - The repo.
package/dist/repo.js CHANGED
@@ -115,6 +115,16 @@ export function packageManager(root) {
115
115
  return field;
116
116
  return LOCKFILES.find(([file]) => existsSync(join(root, file)))?.[1] ?? "npm";
117
117
  }
118
+ /**
119
+ * The text of the repo's package.json, to tell later whether a run changed it.
120
+ * @param root - The repo.
121
+ * @returns Its text, or null if there is none.
122
+ * @example packageText(root) // '{ "devDependencies": … }'
123
+ */
124
+ export function packageText(root) {
125
+ const file = join(root, "package.json");
126
+ return existsSync(file) ? readFileSync(file, "utf8") : null;
127
+ }
118
128
  /**
119
129
  * The version of a package installed in the repo's `node_modules`.
120
130
  * @param root - The repo.
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @file The shape of a catalog of the texts the person sees while `apply` runs its steps.
3
+ * Every language implements `Catalog`; the compiler then asks for every kind of step and every
4
+ * message. The interface uses `ui`; the commit and the PR do not (see commit-texts.ts).
5
+ */
6
+ import type { StepKind, StepOf } from "./step-text.js";
7
+ /** One step's texts, one per place it shows. */
8
+ export interface StepPhrases {
9
+ /** In the menu and the lists of steps, as an order: "Actualizar issue-flow 0.2.7 → 0.3.0". */
10
+ menu: string;
11
+ /** While it runs: "Actualizando issue-flow de 0.2.7 a 0.3.0…". */
12
+ doing: string;
13
+ /** When it worked, saying what is left: "issue-flow actualizado de 0.2.7 a 0.3.0". */
14
+ done: string;
15
+ }
16
+ /** Every text of `apply` around its steps. */
17
+ export interface Catalog {
18
+ /** Each kind of step's texts. */
19
+ steps: {
20
+ [K in StepKind]: (step: StepOf<K>) => StepPhrases;
21
+ };
22
+ /** Added to a step of a piece the repo has not decided on yet. */
23
+ fresh: string;
24
+ /** The end line of a step that failed, from its menu text. */
25
+ failed: (menu: string) => string;
26
+ /** A duration, short: "0,4 s", "12 s", "1 min 5 s". */
27
+ duration: (ms: number) => string;
28
+ /** The line a long step prints while it runs, so it does not look stuck. */
29
+ still: (elapsed: string) => string;
30
+ /** The line before looking at the pieces. */
31
+ checking: string;
32
+ /** Nothing to install or update. */
33
+ upToDate: string;
34
+ /** With --yes and nothing suggested: the optional steps. */
35
+ onlyOptional: (menus: string[]) => string;
36
+ /** The heading of the steps listed without running them. */
37
+ suggested: (dryRun: boolean) => string;
38
+ /** The last line when every step worked or some failed. */
39
+ summary: (failed: string[]) => string;
40
+ /** What is left to do by hand after the steps, without --commit; with the manager if its install failed. */
41
+ next: (failedInstall: string | null) => string;
42
+ /** A new piece left out because no one was asked about it. */
43
+ undecided: (names: string[]) => string;
44
+ /** The repo's own skills left out of a pick. */
45
+ ownSkillsKept: (names: string[]) => string;
46
+ /** The decisions were saved. */
47
+ saved: (file: string, commits: boolean) => string;
48
+ /** The closing line when no step ran: the person left, or picked none. */
49
+ noSteps: (why: "leave" | "none", wrote: boolean) => string;
50
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @file The texts in Spanish. The menu orders ("Actualizar"), progress says what is happening
3
+ * ("Actualizando") and the end line says what is left ("actualizado").
4
+ */
5
+ import type { Catalog } from "./catalog.js";
6
+ /** The texts in Spanish. */
7
+ export declare const es: Catalog;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * A list of names, joined with commas.
3
+ * @param names - The names.
4
+ * @returns The list.
5
+ */
6
+ const list = (names) => names.join(", ");
7
+ /** The texts in Spanish. */
8
+ export const es = {
9
+ steps: {
10
+ "skills-manager-create": ({ version }) => ({
11
+ menu: `Crear package.json con skills-manager ${version}`,
12
+ doing: `Creando package.json con skills-manager ${version}…`,
13
+ done: `package.json creado con skills-manager ${version}`,
14
+ }),
15
+ "skills-manager-add": ({ version }) => ({
16
+ menu: `Agregar skills-manager ${version} a package.json`,
17
+ doing: `Agregando skills-manager ${version} a package.json…`,
18
+ done: `skills-manager ${version} agregado a package.json`,
19
+ }),
20
+ "skills-manager-bump": ({ from, to }) => ({
21
+ menu: `Subir skills-manager ${from} → ${to}, el mínimo de agent-kitchen`,
22
+ doing: `Subiendo skills-manager de ${from} a ${to}…`,
23
+ done: `skills-manager subido de ${from} a ${to}`,
24
+ }),
25
+ "flow-install": ({ version }) => ({
26
+ menu: `Instalar issue-flow ${version}: config del flujo, hooks y scripts`,
27
+ doing: `Instalando issue-flow ${version}…`,
28
+ done: `issue-flow ${version} instalado: config del flujo, hooks y scripts`,
29
+ }),
30
+ "flow-update": ({ from, to }) => ({
31
+ menu: `Actualizar issue-flow ${from} → ${to}, con sus hooks y sus skills`,
32
+ doing: `Actualizando issue-flow de ${from} a ${to}…`,
33
+ done: `issue-flow actualizado de ${from} a ${to}, con sus hooks y sus skills`,
34
+ }),
35
+ "flow-repair": ({ version }) => ({
36
+ menu: `Reparar issue-flow ${version}: hooks y $schema`,
37
+ doing: `Reparando issue-flow ${version}…`,
38
+ done: `issue-flow ${version} reparado: hooks y $schema`,
39
+ }),
40
+ "flow-skills": ({ version }) => ({
41
+ menu: `Poner las skills del flujo en v${version}, la versión de issue-flow`,
42
+ doing: `Poniendo las skills del flujo en v${version}…`,
43
+ done: `Skills del flujo en v${version}, la versión de issue-flow`,
44
+ }),
45
+ "flow-labels": ({ missing }) => ({
46
+ menu: `Crear en GitHub los labels que faltan: ${list(missing)}`,
47
+ doing: `Creando en GitHub los labels que faltan: ${list(missing)}…`,
48
+ done: `Labels creados en GitHub: ${list(missing)}`,
49
+ }),
50
+ "github-rest-bump": ({ from, to, flow }) => ({
51
+ menu: `Subir github-rest ${from} → ${to}, el mínimo de issue-flow ${flow}`,
52
+ doing: `Subiendo github-rest de ${from} a ${to}…`,
53
+ done: `github-rest subido de ${from} a ${to}, el mínimo de issue-flow ${flow}`,
54
+ }),
55
+ "cloud-install": ({ version }) => ({
56
+ menu: `Instalar claude-cloud ${version ?? "1.x"}: prepara las sesiones en la nube de Claude`,
57
+ doing: `Instalando claude-cloud ${version ?? "1.x"}…`,
58
+ done: `claude-cloud ${version ?? "1.x"} instalado: prepara las sesiones en la nube de Claude`,
59
+ }),
60
+ "cloud-update": ({ from, to, config }) => ({
61
+ menu: `Actualizar claude-cloud ${from ?? "?"} → ${to} (la versión que fija ${config})`,
62
+ doing: `Actualizando claude-cloud de ${from ?? "?"} a ${to}…`,
63
+ done: `claude-cloud actualizado de ${from ?? "?"} a ${to} en ${config}`,
64
+ }),
65
+ "guard-install": ({ version }) => ({
66
+ menu: `Instalar claude-repo-guard ${version ?? "(main)"}: modo auto con sandbox, y un documento de qué protege`,
67
+ doing: `Instalando claude-repo-guard ${version ?? "(main)"}…`,
68
+ done: `claude-repo-guard ${version ?? "(main)"} instalado: modo auto con sandbox, y un documento de qué protege`,
69
+ }),
70
+ "guard-update": ({ from, to }) => ({
71
+ menu: `Actualizar claude-repo-guard ${from ?? "?"} → ${to}`,
72
+ doing: `Actualizando claude-repo-guard de ${from ?? "?"} a ${to}…`,
73
+ done: `claude-repo-guard actualizado de ${from ?? "?"} a ${to}`,
74
+ }),
75
+ "guard-adjust": () => ({
76
+ menu: "Ajustar claude-repo-guard a lo que necesita el flujo de issues",
77
+ doing: "Ajustando claude-repo-guard al flujo de issues…",
78
+ done: "claude-repo-guard ajustado a lo que necesita el flujo de issues",
79
+ }),
80
+ "skills-install": ({ count, catalogs }) => ({
81
+ menu: `Instalar ${count} skills de ${catalogs.join(" y ")}`,
82
+ doing: `Instalando ${count} skills de ${catalogs.join(" y ")}…`,
83
+ done: `${count} skills instaladas de ${catalogs.join(" y ")}`,
84
+ }),
85
+ "skills-update": ({ catalog, from, to, names }) => ({
86
+ menu: `Actualizar skills de ${catalog} ${from} → v${to}: ${list(names)}`,
87
+ doing: `Actualizando skills de ${catalog} de ${from} a v${to}…`,
88
+ done: `Skills de ${catalog} actualizadas a v${to}: ${list(names)}`,
89
+ }),
90
+ "skills-add": ({ names }) => ({
91
+ menu: `Agregar skills nuevas (${names.length}): ${list(names)}`,
92
+ doing: `Agregando skills nuevas: ${list(names)}…`,
93
+ done: `Skills nuevas agregadas: ${list(names)}`,
94
+ }),
95
+ "agents-md": ({ rule, claudeImport }) => {
96
+ const files = [...(rule ? ["AGENTS.md"] : []), ...(claudeImport ? ["CLAUDE.md"] : [])].join(", ");
97
+ if (!rule)
98
+ return {
99
+ menu: `Agregar @AGENTS.md a CLAUDE.md, para que Claude Code lea AGENTS.md: ${files}`,
100
+ doing: "Agregando @AGENTS.md a CLAUDE.md…",
101
+ done: "@AGENTS.md agregado a CLAUDE.md: Claude Code lee AGENTS.md",
102
+ };
103
+ const also = claudeImport ? " (y @AGENTS.md en CLAUDE.md)" : "";
104
+ return {
105
+ menu: `Agregar a AGENTS.md la regla de versiones instaladas${also}: ${files}`,
106
+ doing: `Agregando a AGENTS.md la regla de versiones instaladas${also}…`,
107
+ done: `Regla de versiones instaladas agregada a AGENTS.md${also}`,
108
+ };
109
+ },
110
+ "install-deps": ({ manager }) => ({
111
+ menu: `Instalar las dependencias con ${manager}`,
112
+ doing: `Instalando las dependencias con ${manager}…`,
113
+ done: `Dependencias instaladas con ${manager}`,
114
+ }),
115
+ },
116
+ fresh: " (nueva)",
117
+ failed: (menu) => `Falló: ${menu}`,
118
+ duration: (ms) => {
119
+ if (ms < 10_000)
120
+ return `${(ms / 1000).toFixed(1).replace(".", ",")} s`;
121
+ const seconds = Math.round(ms / 1000);
122
+ return seconds < 60 ? `${seconds} s` : `${Math.floor(seconds / 60)} min${seconds % 60 === 0 ? "" : ` ${seconds % 60} s`}`;
123
+ },
124
+ still: (elapsed) => ` … sigue (${elapsed})`,
125
+ checking: "Revisando el repo y las últimas versiones…",
126
+ upToDate: "Todo al día: no hay nada que instalar ni actualizar.",
127
+ onlyOptional: (menus) => `Todo al día. Opcional, para elegir en una terminal: ${menus.join("; ")}.`,
128
+ suggested: (dryRun) => `Pasos sugeridos${dryRun ? "" : " (para hacerlos, corre agent-kitchen apply en una terminal, o con --yes)"}:`,
129
+ summary: (failed) => (failed.length === 0 ? "Listo." : `Terminó, pero fallaron: ${failed.join("; ")}.`),
130
+ next: (failedInstall) => `Falta: ${failedInstall === null ? "" : `corre '${failedInstall} install', `}revisa el diff y haz commit.`,
131
+ undecided: (names) => `Pieza nueva sin decisión: ${list(names)}. Corre agent-kitchen apply en una terminal para elegir.`,
132
+ ownSkillsKept: (names) => `Se dejan fuera las skills que el repo ya tiene como propias: ${list(names)}. Para reemplazarlas, corre agent-kitchen apply en una terminal.`,
133
+ saved: (file, commits) => `Guardé qué piezas usa el repo en ${file}${commits ? "" : "; commitéalo"}.`,
134
+ noSteps: (why, wrote) => {
135
+ if (why === "leave")
136
+ return wrote ? "No corrí ningún paso; solo guardé qué piezas usa el repo." : "No se cambió nada.";
137
+ return wrote ? "No elegiste pasos; solo guardé qué piezas usa el repo." : "No elegiste nada; no se cambió nada.";
138
+ },
139
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @file The interface's texts, and how a step reads in a list. Another language is a file
3
+ * like es.ts that implements `Catalog`, chosen here.
4
+ */
5
+ import type { Step } from "../piece.js";
6
+ import type { Catalog, StepPhrases } from "./catalog.js";
7
+ import type { StepText } from "./step-text.js";
8
+ /** The catalog the interface uses. */
9
+ export declare const ui: Catalog;
10
+ /**
11
+ * A step's texts in a catalog.
12
+ * @param catalog - The catalog.
13
+ * @param text - What the step does.
14
+ * @returns Its texts.
15
+ * @example phrasesIn(es, { kind: "guard-adjust" }).doing // "Ajustando claude-repo-guard al flujo de issues…"
16
+ */
17
+ export declare function phrasesIn(catalog: Catalog, text: StepText): StepPhrases;
18
+ /**
19
+ * A step's texts in the interface.
20
+ * @param text - What the step does.
21
+ * @returns Its texts.
22
+ */
23
+ export declare function phrases(text: StepText): StepPhrases;
24
+ /**
25
+ * How a step reads in the menu and in lists: its order, marked if its piece is new.
26
+ * @param step - The step.
27
+ * @returns The text.
28
+ * @example stepLabel(step) // "Actualizar issue-flow 0.2.7 → 0.3.0, con sus hooks y sus skills"
29
+ */
30
+ export declare function stepLabel(step: Pick<Step, "text" | "fresh">): string;
@@ -0,0 +1,31 @@
1
+ import { es } from "./es.js";
2
+ /** The catalog the interface uses. */
3
+ export const ui = es;
4
+ /**
5
+ * A step's texts in a catalog.
6
+ * @param catalog - The catalog.
7
+ * @param text - What the step does.
8
+ * @returns Its texts.
9
+ * @example phrasesIn(es, { kind: "guard-adjust" }).doing // "Ajustando claude-repo-guard al flujo de issues…"
10
+ */
11
+ export function phrasesIn(catalog, text) {
12
+ const render = catalog.steps[text.kind];
13
+ return render(text);
14
+ }
15
+ /**
16
+ * A step's texts in the interface.
17
+ * @param text - What the step does.
18
+ * @returns Its texts.
19
+ */
20
+ export function phrases(text) {
21
+ return phrasesIn(ui, text);
22
+ }
23
+ /**
24
+ * How a step reads in the menu and in lists: its order, marked if its piece is new.
25
+ * @param step - The step.
26
+ * @returns The text.
27
+ * @example stepLabel(step) // "Actualizar issue-flow 0.2.7 → 0.3.0, con sus hooks y sus skills"
28
+ */
29
+ export function stepLabel(step) {
30
+ return `${phrases(step.text).menu}${step.fresh === true ? ui.fresh : ""}`;
31
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @file What a step does, as data: its kind, the piece's versions and what it brings. No
3
+ * words here; each place that shows a step (the menu, the start and end of its progress, the
4
+ * commit) turns this into its own text, so the interface can change language without the
5
+ * commits changing too.
6
+ */
7
+ /** A version that may be unknown (`null`), shown as `?` or as the place it comes from. */
8
+ type MaybeVersion = string | null;
9
+ /** What a step does, one kind per thing a piece can offer. */
10
+ export type StepText = {
11
+ kind: "skills-manager-create";
12
+ version: string;
13
+ } | {
14
+ kind: "skills-manager-add";
15
+ version: string;
16
+ } | {
17
+ kind: "skills-manager-bump";
18
+ from: string;
19
+ to: string;
20
+ } | {
21
+ kind: "flow-install";
22
+ version: string;
23
+ } | {
24
+ kind: "flow-update";
25
+ from: string;
26
+ to: string;
27
+ } | {
28
+ kind: "flow-repair";
29
+ version: string;
30
+ } | {
31
+ kind: "flow-skills";
32
+ version: string;
33
+ } | {
34
+ kind: "flow-labels";
35
+ missing: string[];
36
+ } | {
37
+ kind: "github-rest-bump";
38
+ from: string;
39
+ to: string;
40
+ flow: string;
41
+ } | {
42
+ kind: "cloud-install";
43
+ version: MaybeVersion;
44
+ } | {
45
+ kind: "cloud-update";
46
+ from: MaybeVersion;
47
+ to: string;
48
+ config: string;
49
+ } | {
50
+ kind: "guard-install";
51
+ version: MaybeVersion;
52
+ } | {
53
+ kind: "guard-update";
54
+ from: MaybeVersion;
55
+ to: string;
56
+ } | {
57
+ kind: "guard-adjust";
58
+ } | {
59
+ kind: "skills-install";
60
+ count: number;
61
+ catalogs: string[];
62
+ } | {
63
+ kind: "skills-update";
64
+ catalog: string;
65
+ from: string;
66
+ to: string;
67
+ names: string[];
68
+ } | {
69
+ kind: "skills-add";
70
+ names: string[];
71
+ } | {
72
+ kind: "agents-md";
73
+ rule: boolean;
74
+ claudeImport: boolean;
75
+ } | {
76
+ kind: "install-deps";
77
+ manager: string;
78
+ };
79
+ /** The kinds of step. */
80
+ export type StepKind = StepText["kind"];
81
+ /** The data of one kind of step. */
82
+ export type StepOf<K extends StepKind> = Extract<StepText, {
83
+ kind: K;
84
+ }>;
85
+ export {};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @file What a step does, as data: its kind, the piece's versions and what it brings. No
3
+ * words here; each place that shows a step (the menu, the start and end of its progress, the
4
+ * commit) turns this into its own text, so the interface can change language without the
5
+ * commits changing too.
6
+ */
7
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-kitchen",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "One command to install, update and check a repo's agent tooling: the issue flow and its skills, the general skills, Claude cloud sessions, claude-repo-guard and the installed-versions rule in AGENTS.md.",
5
5
  "license": "MIT",
6
6
  "type": "module",