agent-kitchen 0.6.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.
@@ -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.
@@ -23,7 +23,6 @@
23
23
  * `--pieces` (terminal only, not with `--yes` or `--dry-run`) first asks which pieces the repo
24
24
  * uses, writes all of them to the file, and then goes on as usual.
25
25
  */
26
- import { spinner } from "@clack/prompts";
27
26
  import { DECIDABLE, decide, DECISIONS, off, readDecisions, undecided, writeDecisions } from "../decisions.js";
28
27
  import { baseBranch, branchName, changedPaths, checkReady, finish } from "../git-finish.js";
29
28
  import { askFinish, askPath, askPieces, askReplace, askSkills, askSteps } from "../menu.js";
@@ -36,6 +35,9 @@ import { agentsMdStatus, NAME as AGENTS_MD } from "../pieces/agents-md.js";
36
35
  import { NAME as SKILLS_MANAGER, skillsManagerStatus } from "../pieces/skills-manager.js";
37
36
  import { mainCheckout, packageManager } from "../repo.js";
38
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";
39
41
  /** Every piece, in the order its steps run. */
40
42
  const PIECES = [SKILLS_MANAGER, FLOW, CLOUD, GUARD, SKILLS, AGENTS_MD];
41
43
  /**
@@ -115,9 +117,7 @@ function saveDecisions(root, saved, statuses, chosen, commits, out) {
115
117
  * @example noStepsMessage("none", true) // "No elegiste pasos; solo guardé qué piezas usa el repo.\n"
116
118
  */
117
119
  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";
120
+ return `${ui.noSteps(why, wrote)}\n`;
121
121
  }
122
122
  /**
123
123
  * The line that says the decisions were saved.
@@ -125,20 +125,19 @@ export function noStepsMessage(why, wrote) {
125
125
  * @returns The line.
126
126
  */
127
127
  function savedMessage(commits) {
128
- return `Guardé qué piezas usa el repo en ${DECISIONS}${commits ? "" : "; commitéalo"}.\n`;
128
+ return `${ui.saved(DECISIONS, commits)}\n`;
129
129
  }
130
- /** The line the commit of `--commit` adds for the decisions file. */
131
- const DECISIONS_STEP = `Guardar qué piezas usa el repo en ${DECISIONS}`;
132
130
  /**
133
131
  * 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.
132
+ * run wrote it, in the commit's texts (not the interface's).
133
+ * @param done - The steps that worked.
136
134
  * @param wrote - Whether the decisions file was written in this run.
137
- * @returns The labels.
135
+ * @returns The lines.
138
136
  * @example commitLabels([], true) // ["Guardar qué piezas usa el repo en .agents/kitchen.json"]
139
137
  */
140
138
  export function commitLabels(done, wrote) {
141
- return wrote ? [...done, DECISIONS_STEP] : done;
139
+ const lines = done.map((step) => commitLine(step.text));
140
+ return wrote ? [...lines, DECISIONS_LINE] : lines;
142
141
  }
143
142
  /**
144
143
  * Asks which pieces the repo uses and writes the answer for every decidable piece.
@@ -149,7 +148,7 @@ export function commitLabels(done, wrote) {
149
148
  * @returns False if the person cancelled.
150
149
  */
151
150
  async function choosePieces(root, saved, commits, out) {
152
- out.stdout("Revisando el repo y las últimas versiones…\n");
151
+ out.stdout(`${ui.checking}\n`);
153
152
  // No skip here: an installed, undecided piece passed in --skip still starts ticked.
154
153
  const statuses = await statusesOf(root, []);
155
154
  const ticked = DECIDABLE.filter((name) => saved?.pieces[name] === true || (saved?.pieces[name] === undefined && statuses.some((status) => status.name === name && status.installed)));
@@ -173,7 +172,7 @@ function keepOwnSkills(root, step, out) {
173
172
  if (own.length === 0)
174
173
  return;
175
174
  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`);
175
+ out.stdout(`${ui.ownSkillsKept(own)}\n`);
177
176
  }
178
177
  /**
179
178
  * Asks, for a step that installs skills, which ones, and whether to replace the repo's own.
@@ -217,14 +216,13 @@ async function chooseSteps(root, steps, options, out) {
217
216
  const suggested = steps.filter((step) => step.checked);
218
217
  if (options.yes) {
219
218
  if (suggested.length === 0)
220
- 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`);
221
220
  for (const step of suggested)
222
221
  keepOwnSkills(root, step, out);
223
222
  return suggested;
224
223
  }
225
224
  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("")}`);
225
+ out.stdout(`${ui.suggested(options.dryRun)}\n${steps.map((step) => ` [${step.checked ? "x" : " "}] ${stepLabel(step)}\n`).join("")}`);
228
226
  return null;
229
227
  }
230
228
  for (const step of suggested)
@@ -242,22 +240,6 @@ async function chooseSteps(root, steps, options, out) {
242
240
  }
243
241
  return chosen;
244
242
  }
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
243
  /**
262
244
  * Runs one step and shows how it went: its notes, and what to do if it failed.
263
245
  * @param root - The repo.
@@ -275,19 +257,21 @@ async function runStep(root, step, progress, out) {
275
257
  note: (text) => notes.push(text),
276
258
  fail: (text, output) => failures.push(` ${text}\n${output === undefined ? "" : failedOutput(output)}`),
277
259
  };
278
- progress.start(step.label);
279
- await Promise.all(step.downloads.map((spec) => download(root, spec)));
260
+ progress.start(step.text);
280
261
  let ok;
281
262
  try {
263
+ await Promise.all(step.downloads.map((spec) => download(root, spec)));
282
264
  ok = await step.run(context);
283
265
  }
284
266
  catch (error) {
285
- if (!(error instanceof KitError))
267
+ if (!(error instanceof KitError)) {
268
+ progress.stop(step.text, false);
286
269
  throw error;
270
+ }
287
271
  failures.push(` ${error.message}\n`);
288
272
  ok = false;
289
273
  }
290
- progress.stop(step.label, ok);
274
+ progress.stop(step.text, ok);
291
275
  for (const note of notes)
292
276
  out.stdout(` · ${note}\n`);
293
277
  for (const failure of failures)
@@ -333,32 +317,33 @@ export async function sync(options, out) {
333
317
  const asks = out.interactive && !options.yes && !options.dryRun;
334
318
  const saves = asks && options.skip.length === 0;
335
319
  if (out.interactive)
336
- out.stdout("Revisando el repo y las últimas versiones…\n");
320
+ out.stdout(`${ui.checking}\n`);
337
321
  let statuses = await statusesOf(root, skip);
338
322
  if (saved !== null && !asks && !options.dryRun) {
339
323
  const left = undecided(saved).filter((name) => statuses.some((status) => status.name === name && !status.installed));
340
324
  if (left.length > 0) {
341
325
  skip.push(...left);
342
326
  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`);
327
+ out.stdout(`${ui.undecided(left)}\n`);
344
328
  }
345
329
  }
346
330
  if (saved !== null) {
347
331
  const fresh = undecided(saved);
348
332
  for (const status of statuses.filter((each) => fresh.includes(each.name))) {
349
- status.state += " (nueva)";
333
+ status.state += ui.fresh;
350
334
  for (const step of status.steps)
351
- step.label += " (nueva)";
335
+ step.fresh = true;
352
336
  }
353
337
  }
354
338
  printStatus(statuses, off(saved).filter((name) => !options.skip.includes(name)), out);
355
339
  const steps = statuses.flatMap((status) => status.steps);
356
340
  // 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);
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);
358
343
  if (steps.length === 0) {
359
344
  if (saves)
360
345
  wrote = saveDecisions(root, saved, statuses, [], base !== null, out) || wrote;
361
- out.stdout("Todo al día: no hay nada que instalar ni actualizar.\n");
346
+ out.stdout(`${ui.upToDate}\n`);
362
347
  return endWithoutSteps();
363
348
  }
364
349
  const chosen = await chooseSteps(root, steps, options, out);
@@ -382,16 +367,15 @@ export async function sync(options, out) {
382
367
  out.stdout(noStepsMessage("none", wrote));
383
368
  return endWithoutSteps();
384
369
  }
385
- const progress = progressFor(out, options.verbose || base !== null);
386
- const failed = [];
370
+ const failedSteps = [];
387
371
  for (const step of chosen) {
388
372
  if (!(await runStep(root, step, progress, out)))
389
- failed.push(step.label);
373
+ failedSteps.push(step);
390
374
  }
375
+ const failed = failedSteps.map(stepLabel);
391
376
  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`);
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`);
395
379
  return failed.length === 0 ? 0 : 1;
396
380
  }
397
381
  /**
@@ -399,14 +383,15 @@ export async function sync(options, out) {
399
383
  * the branch with a PR under --yes) and what git did.
400
384
  * @param root - The repo.
401
385
  * @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.
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.
404
389
  * @param options - The options.
405
390
  * @param out - Where to write and ask.
406
391
  * @returns 0, or 1 if a step, the install or git failed.
407
392
  */
408
- async function commitChanges(root, base, done, failed, options, out) {
409
- const summary = failed.length === 0 ? "Listo." : `Terminó, pero fallaron: ${failed.join("; ")}.`;
393
+ async function commitChanges(root, base, done, failed, progress, options, out) {
394
+ const summary = ui.summary(failed);
410
395
  const changed = changedPaths(root);
411
396
  if (changed.length === 0) {
412
397
  out.stdout(`\n${summary} No cambió ningún archivo: no hay nada que commitear.\n`);
@@ -415,10 +400,10 @@ async function commitChanges(root, base, done, failed, options, out) {
415
400
  const manager = packageManager(root);
416
401
  const packageChanged = changed.includes("package.json");
417
402
  if (packageChanged) {
418
- const label = `Instalar las dependencias con ${manager}`;
419
- out.stdout(`→ ${label}\n`);
403
+ const text = { kind: "install-deps", manager };
404
+ progress.start(text);
420
405
  const installed = await runInstall(root, manager);
421
- out.stdout(`${installed.ok ? "✓" : "✗"} ${label}\n`);
406
+ progress.stop(text, installed.ok);
422
407
  if (!installed.ok) {
423
408
  out.stderr(failedOutput(installed.output));
424
409
  out.stdout(`\n${summary} No commiteé: el install falló y los cambios quedan sin commit.\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,29 @@
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, a line now and then while it runs, and the end.
10
+ * @param out - Where to write.
11
+ * @param options - Whether to print while it runs, how often, and the clock.
12
+ * @returns The progress.
13
+ * @example plainProgress(out, { ticks: true }).start({ kind: "guard-adjust" })
14
+ */
15
+ export declare function plainProgress(out: Output, options: {
16
+ ticks: boolean;
17
+ tickMs?: number;
18
+ now?: () => number;
19
+ }): Progress;
20
+ /**
21
+ * How progress is shown: a spinner in a terminal, plain lines otherwise or when `plain` asks
22
+ * for them. Only for the steps, after every question: a spinner shown before a question
23
+ * leaves the second question deaf to the keyboard (@clack/prompts 1.8.1).
24
+ * @param out - Where to write.
25
+ * @param verbose - Whether the tools print on the terminal.
26
+ * @param plain - Whether a question may come after the steps (`--commit`).
27
+ * @returns The progress.
28
+ */
29
+ export declare function progressFor(out: Output, verbose: boolean, plain: boolean): Progress;
@@ -0,0 +1,75 @@
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, a line now and then while it runs, and the end.
24
+ * @param out - Where to write.
25
+ * @param options - Whether to print while it runs, how often, and the clock.
26
+ * @returns The progress.
27
+ * @example plainProgress(out, { ticks: true }).start({ kind: "guard-adjust" })
28
+ */
29
+ export function plainProgress(out, options) {
30
+ const now = options.now ?? Date.now;
31
+ let started = 0;
32
+ let timer;
33
+ return {
34
+ start: (text) => {
35
+ started = now();
36
+ out.stdout(`${phrases(text).doing}\n`);
37
+ if (options.ticks)
38
+ timer = setInterval(() => out.stdout(`${ui.still(ui.duration(now() - started))}\n`), options.tickMs ?? TICK_MS);
39
+ },
40
+ stop: (text, ok) => {
41
+ clearInterval(timer);
42
+ timer = undefined;
43
+ out.stdout(`${ok ? "✓" : "✗"} ${endLine(text, ok, now() - started)}\n`);
44
+ },
45
+ };
46
+ }
47
+ /**
48
+ * How progress is shown: a spinner in a terminal, plain lines otherwise or when `plain` asks
49
+ * for them. Only for the steps, after every question: a spinner shown before a question
50
+ * leaves the second question deaf to the keyboard (@clack/prompts 1.8.1).
51
+ * @param out - Where to write.
52
+ * @param verbose - Whether the tools print on the terminal.
53
+ * @param plain - Whether a question may come after the steps (`--commit`).
54
+ * @returns The progress.
55
+ */
56
+ export function progressFor(out, verbose, plain) {
57
+ if (out.interactive && !verbose && !plain) {
58
+ const spin = spinner();
59
+ let started = 0;
60
+ return {
61
+ start: (text) => {
62
+ started = Date.now();
63
+ spin.start(phrases(text).doing);
64
+ },
65
+ stop: (text, ok) => {
66
+ const line = endLine(text, ok, Date.now() - started);
67
+ if (ok)
68
+ spin.stop(line);
69
+ else
70
+ spin.error(line);
71
+ },
72
+ };
73
+ }
74
+ return plainProgress(out, { ticks: !verbose });
75
+ }
@@ -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. */
41
+ next: (manager: string) => 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: (manager) => `Falta: corre '${manager} 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.7.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",