agent-cloud-kit 1.0.0-rc.7 → 1.0.0-rc.8

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.
@@ -6,13 +6,15 @@
6
6
  import { existsSync, lstatSync, statSync } from "node:fs";
7
7
  import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
8
8
  import { readConfig } from "../config.js";
9
- import { CONFIG_FILE, PACKAGE } from "../constants.js";
9
+ import { CONFIG_FILE } from "../constants.js";
10
10
  import { defaultBranch } from "../detect.js";
11
11
  import { init } from "../init.js";
12
+ import { renderBlock } from "../block.js";
12
13
  import { MANUAL_FILE, renderEnvironmentSection, writeManual } from "../manual.js";
14
+ import { t } from "../messages/index.js";
13
15
  import { claudeCommand, continueLine, enterLine, hasFreeScript, renderBanner } from "./banner.js";
14
16
  import { preflight } from "./detect.js";
15
- import { formatLine, initKind, nextSteps, styleMarkdown, unprefixed } from "./ui.js";
17
+ import { formatLine, nextSteps, styleMarkdown } from "./ui.js";
16
18
  import { configurePrompt, verifyPrompt } from "./prompts.js";
17
19
  /** Prefix of the branch `prepare` creates; the day follows it. */
18
20
  const BRANCH_PREFIX = "chore/cloud-ready-";
@@ -30,23 +32,14 @@ const KNOWN_PATHS = [
30
32
  ];
31
33
  /** Project file of the test-audit skill, committed too when it exists. */
32
34
  const TEST_AUDIT_PROJECT_FILE = ".agents/skills/test-audit/project.md";
33
- /** Message of the commit `prepare` makes. */
35
+ /** Message of the commit `prepare` makes: it lives in the repo, so it is always in Spanish. */
34
36
  const COMMIT_MESSAGE = "chore: prepara el repo para la nube de Claude Code";
35
- /** Question asked before committing changes outside the known files. */
36
- const INCLUDE_QUESTION = "¿Incluir estos cambios en el commit? [s/N]";
37
- /** Answers to `INCLUDE_QUESTION` that mean yes. */
37
+ /** Answers to the "include these changes?" question that mean yes, in either language. */
38
38
  const YES_ANSWER = /^(s|si|sí|y|yes)$/i;
39
- /** Text shown while the user configures the environment in claude.ai. */
40
39
  /** Matches a GitHub remote URL (https or ssh) and captures `owner/repo`. */
41
40
  const GITHUB_REMOTE = /github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/;
42
41
  /** Matches a pull request URL and captures its number. */
43
42
  const PR_URL = /\/pull\/(\d+)/;
44
- /** Command that continues a prepare run; `npx` because a first-time user does not have the binary on PATH. */
45
- const RESUME_COMMAND = `npx ${PACKAGE} prepare --resume`;
46
- /** Question of the place menu. */
47
- const PLACE_QUESTION = "¿Dónde preparo el repo?";
48
- /** Command that launches the cloud verification of the current branch. */
49
- const VERIFY_COMMAND = `npx ${PACKAGE} verify`;
50
43
  /** Exit code for success. */
51
44
  const EXIT_OK = 0;
52
45
  /** Exit code for failure. */
@@ -59,7 +52,7 @@ const PR_NUMBER = /^\d+$/;
59
52
  export class InterruptedError extends Error {
60
53
  /** Builds the error. */
61
54
  constructor() {
62
- super("interrumpido");
55
+ super(t().prepare.interrupted);
63
56
  this.name = "InterruptedError";
64
57
  }
65
58
  }
@@ -87,6 +80,23 @@ export function branchDay(date) {
87
80
  function say(deps, kind, text) {
88
81
  deps.print(formatLine(kind, text, deps.color ?? false));
89
82
  }
83
+ /**
84
+ * Prints a final message: a block with Kuro in a terminal, else the plain prefixed lines.
85
+ *
86
+ * @param deps - Dependencies (`print`, `art`, `color`).
87
+ * @param block - What to say.
88
+ * @param plain - Lines to print without a terminal, without prefix.
89
+ * @example
90
+ * finish(deps, { mood: "ok", title: "Listo" }, ["listo"]);
91
+ */
92
+ function finish(deps, block, plain) {
93
+ if (deps.art)
94
+ for (const line of renderBlock(block, { art: true, color: deps.color ?? false }))
95
+ deps.print(line);
96
+ else
97
+ for (const line of plain)
98
+ say(deps, block.mood === "error" ? "fail" : "ok", line);
99
+ }
90
100
  /**
91
101
  * Says where the worktree is and how to continue there.
92
102
  *
@@ -97,7 +107,7 @@ function say(deps, kind, text) {
97
107
  * resumeLine("interrumpido", "/home/u/app-cloud-ready"); // "interrumpido; el worktree está en ...; corre ... --resume ahí para continuar"
98
108
  */
99
109
  function resumeLine(reason, worktree) {
100
- return `${reason}; el worktree está en ${worktree}; corre ${RESUME_COMMAND} ahí para continuar`;
110
+ return t().prepare.resume(reason, worktree);
101
111
  }
102
112
  /**
103
113
  * Runs a capture and trims its output; never throws.
@@ -168,7 +178,7 @@ function worktreePath(repo) {
168
178
  function configureProblem(worktree) {
169
179
  const { config, errors } = readConfig(worktree);
170
180
  if (!config || errors.length)
171
- return errors[0] ?? `no existe ${CONFIG_FILE}`;
181
+ return errors[0] ?? t().prepare.noConfig(CONFIG_FILE);
172
182
  const notes = config.notes ?? DEFAULT_NOTES_FILE;
173
183
  const rel = relative(worktree, join(worktree, notes));
174
184
  const inside = !isAbsolute(notes) && rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`);
@@ -179,7 +189,7 @@ function configureProblem(worktree) {
179
189
  catch {
180
190
  // NOTE: a missing file is reported below.
181
191
  }
182
- return `no existe el archivo de notas ${notes}`;
192
+ return t().prepare.noNotes(notes);
183
193
  }
184
194
  /**
185
195
  * Lists the paths of `git status --porcelain=v1 -z` output.
@@ -262,11 +272,11 @@ async function findResume(repo, deps) {
262
272
  */
263
273
  async function createAndConfigure(repo, branch, worktree, deps, progress) {
264
274
  if (existsSync(worktree)) {
265
- say(deps, "fail", `${worktree} ya existe; bórralo (git worktree remove ${worktree}) o continúa ahí con ${RESUME_COMMAND}`);
275
+ say(deps, "fail", t().prepare.worktreeExists(worktree));
266
276
  return false;
267
277
  }
268
278
  if ((await tryCapture(deps, "git", ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], repo)) !== undefined) {
269
- say(deps, "fail", `la rama ${branch} ya existe; bórrala (git branch -D ${branch}) o corre prepare otro día`);
279
+ say(deps, "fail", t().prepare.branchExists(branch));
270
280
  return false;
271
281
  }
272
282
  const base = `origin/${defaultBranch(repo)}`;
@@ -274,17 +284,17 @@ async function createAndConfigure(repo, branch, worktree, deps, progress) {
274
284
  await deps.run("git", ["fetch", "-q", "origin"], { cwd: repo });
275
285
  await deps.run("git", ["worktree", "add", "-b", branch, worktree, base], { cwd: repo });
276
286
  progress.worktree = worktree;
277
- say(deps, "ok", `se creó el worktree ${worktree} en la rama ${branch}`);
287
+ say(deps, "ok", t().prepare.worktreeCreated(worktree, branch));
278
288
  }
279
289
  catch (err) {
280
- say(deps, "fail", `no se pudo crear el worktree: ${message(err)}`);
290
+ say(deps, "fail", t().prepare.worktreeFailed(message(err)));
281
291
  return false;
282
292
  }
283
- const stopped = resumeLine("el agente local se detuvo", worktree);
293
+ const stopped = resumeLine(t().prepare.agentStopped, worktree);
284
294
  try {
285
295
  const result = init(worktree, deps.packageVersion, deps.packageRoot);
286
- for (const line of result.lines)
287
- say(deps, initKind(line), unprefixed(line));
296
+ for (const entry of result.entries)
297
+ say(deps, entry.kind, entry.text);
288
298
  if (!result.ok) {
289
299
  say(deps, "warn", stopped);
290
300
  return false;
@@ -306,8 +316,8 @@ async function createAndConfigure(repo, branch, worktree, deps, progress) {
306
316
  }
307
317
  const problem = configureProblem(worktree);
308
318
  if (problem !== undefined) {
309
- say(deps, "fail", `configure no terminó: ${problem}`);
310
- say(deps, "warn", resumeLine("prepare se detuvo", worktree));
319
+ say(deps, "fail", t().prepare.configureUnfinished(problem));
320
+ say(deps, "warn", resumeLine(t().prepare.prepareStopped, worktree));
311
321
  return false;
312
322
  }
313
323
  return true;
@@ -334,45 +344,45 @@ async function commitAndPush(worktree, branch, deps) {
334
344
  const status = await deps.capture("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: worktree });
335
345
  const others = statusPaths(status).filter((path) => !covered(path, known));
336
346
  if (others.length) {
337
- say(deps, "warn", `el agente local también cambió:`);
347
+ say(deps, "warn", t().prepare.otherChanges);
338
348
  for (const path of others)
339
349
  deps.print(` ${path}`);
340
- if (YES_ANSWER.test((await deps.ask(INCLUDE_QUESTION, "")).trim())) {
350
+ if (YES_ANSWER.test((await deps.ask(t().prepare.includeQuestion, "")).trim())) {
341
351
  await deps.run("git", ["add", "--", ...others], { cwd: worktree });
342
352
  }
343
353
  else {
344
354
  await deps.run("git", ["reset", "-q", "--", ...others], { cwd: worktree });
345
- say(deps, "warn", `quedó sin commitear en ${worktree}`);
355
+ say(deps, "warn", t().prepare.leftUncommitted(worktree));
346
356
  }
347
357
  }
348
358
  const staged = await deps.capture("git", ["diff", "--cached", "--name-only"], { cwd: worktree });
349
359
  if (staged.trim())
350
360
  await deps.run("git", ["commit", "-m", COMMIT_MESSAGE], { cwd: worktree });
351
361
  else
352
- say(deps, "same", `no hay nada nuevo que commitear`);
362
+ say(deps, "same", t().prepare.nothingToCommit);
353
363
  await deps.run("git", ["push", "-u", "origin", branch], { cwd: worktree });
354
- say(deps, "ok", `se subió la rama ${branch}`);
364
+ say(deps, "ok", t().prepare.pushed(branch));
355
365
  }
356
366
  catch (err) {
357
367
  if (err instanceof InterruptedError)
358
368
  throw err;
359
- say(deps, "fail", `no se pudo hacer commit y push: ${message(err)}; corrígelo en ${worktree} y corre ${RESUME_COMMAND} ahí`);
369
+ say(deps, "fail", t().prepare.pushFailed(message(err), worktree));
360
370
  return false;
361
371
  }
362
372
  const existing = await tryCapture(deps, "gh", ["pr", "view", branch, "--json", "number", "--jq", ".number"], worktree);
363
373
  if (existing !== undefined && PR_NUMBER.test(existing)) {
364
- say(deps, "ok", `se reutiliza el PR abierto #${existing}`);
374
+ say(deps, "ok", t().prepare.prReused(existing));
365
375
  return { pr: existing };
366
376
  }
367
377
  try {
368
378
  const url = await deps.capture("gh", ["pr", "create", "--draft", "--fill"], { cwd: worktree });
369
- say(deps, "ok", `PR en borrador: ${url.trim()}`);
379
+ say(deps, "ok", t().prepare.prDraft(url.trim()));
370
380
  return { pr: PR_URL.exec(url)?.[1] };
371
381
  }
372
382
  catch {
373
383
  const origin = await tryCapture(deps, "git", ["remote", "get-url", "origin"], worktree);
374
384
  const link = compareLink(origin, branch);
375
- say(deps, "warn", `no se pudo abrir el PR con gh; ábrelo a mano${link ? `: ${link}` : ` desde la rama ${branch}`}`);
385
+ say(deps, "warn", t().prepare.prByHand(link, branch));
376
386
  return {};
377
387
  }
378
388
  }
@@ -472,8 +482,8 @@ async function runConfigure(deps, progress, place, cwd) {
472
482
  */
473
483
  function interruptedLine(progress) {
474
484
  if (progress.here)
475
- return "interrumpido; lo que alcanzó a cambiar quedó sin commit en tu rama";
476
- return progress.worktree === undefined ? "interrumpido; no se creó nada" : resumeLine("interrumpido", progress.worktree);
485
+ return t().prepare.interruptedHere;
486
+ return progress.worktree === undefined ? t().prepare.interruptedNothing : resumeLine(t().prepare.interrupted, progress.worktree);
477
487
  }
478
488
  /**
479
489
  * Everything after the preflight: resume or create the worktree, commit, wait, launch.
@@ -493,21 +503,21 @@ async function prepareSteps(gitRoot, deps, options, progress) {
493
503
  if (options.resume) {
494
504
  const found = await findResume(gitRoot, deps);
495
505
  if (!found) {
496
- say(deps, "fail", `no hay nada que reanudar: corre prepare --resume en el worktree *${WORKTREE_SUFFIX}, sobre una rama ${BRANCH_PREFIX}*`);
506
+ say(deps, "fail", t().prepare.nothingToResume(WORKTREE_SUFFIX, BRANCH_PREFIX));
497
507
  return EXIT_FAIL;
498
508
  }
499
509
  ({ worktree, branch } = found);
500
510
  progress.worktree = worktree;
501
511
  const problem = configureProblem(worktree);
502
512
  if (problem) {
503
- say(deps, "fail", `configure no terminó en ${worktree}: ${problem}; corre claude "/cloud-ready configure" ahí primero y luego ${RESUME_COMMAND}`);
513
+ say(deps, "fail", t().prepare.resumeUnfinished(worktree, problem));
504
514
  return EXIT_FAIL;
505
515
  }
506
516
  }
507
517
  else {
508
518
  const place = options.place ?? (await askPlace(gitRoot, deps));
509
519
  if (place === null) {
510
- say(deps, "same", `no se cambió nada`);
520
+ say(deps, "same", t().prepare.nothingChanged);
511
521
  return EXIT_FAIL;
512
522
  }
513
523
  if (place === "here")
@@ -522,17 +532,17 @@ async function prepareSteps(gitRoot, deps, options, progress) {
522
532
  return EXIT_FAIL;
523
533
  const config = readConfig(worktree).config;
524
534
  if (config)
525
- for (const line of styleMarkdown(renderEnvironmentSection(config, deps.packageVersion), deps.color ?? false))
535
+ for (const line of styleMarkdown(renderEnvironmentSection(config, deps.packageVersion, t().environment), deps.color ?? false))
526
536
  deps.print(line);
527
537
  await deps.waitEnter(continueLine(deps.color ?? false));
528
538
  try {
529
539
  await runInteractive(deps, progress, { cmd: "claude", args: ["--cloud", verifyPrompt({ branch, pr: pushed.pr })] }, worktree);
530
540
  }
531
541
  catch (err) {
532
- say(deps, "fail", `falló claude --cloud: ${message(err)}; corre ${RESUME_COMMAND} en ${worktree} para reintentar`);
542
+ say(deps, "fail", t().prepare.cloudFailedResume(message(err), worktree));
533
543
  return EXIT_FAIL;
534
544
  }
535
- say(deps, "ok", `la sesión cloud avisará con un push a ${branch} y un comentario en el PR`);
545
+ cloudStarted(deps, branch, pushed.pr, true);
536
546
  return EXIT_OK;
537
547
  }
538
548
  /**
@@ -545,9 +555,9 @@ async function prepareSteps(gitRoot, deps, options, progress) {
545
555
  * await askPlace("/home/u/app", deps); // "pr"
546
556
  */
547
557
  async function askPlace(gitRoot, deps) {
548
- return deps.choose(PLACE_QUESTION, [
549
- { value: "pr", label: "En un PR nuevo", hint: `recomendado · parte de origin/${defaultBranch(gitRoot)}, no toca tu rama ni tus archivos` },
550
- { value: "here", label: "En la rama actual, sin commit", hint: `los revisas con git diff, haces commit y push, y luego corres ${VERIFY_COMMAND}` },
558
+ return deps.choose(t().prepare.placeQuestion, [
559
+ { value: "pr", label: t().prepare.placePr, hint: t().prepare.placePrHint(defaultBranch(gitRoot)) },
560
+ { value: "here", label: t().prepare.placeHere, hint: t().prepare.placeHereHint },
551
561
  ], "pr");
552
562
  }
553
563
  /**
@@ -563,11 +573,11 @@ async function askPlace(gitRoot, deps) {
563
573
  */
564
574
  async function prepareHere(gitRoot, deps, progress) {
565
575
  progress.here = true;
566
- const stopped = `prepare se detuvo; lo que alcanzó a cambiar quedó sin commit en tu rama`;
576
+ const stopped = t().prepare.stoppedHere;
567
577
  try {
568
578
  const result = init(gitRoot, deps.packageVersion, deps.packageRoot);
569
- for (const line of result.lines)
570
- say(deps, initKind(line), unprefixed(line));
579
+ for (const entry of result.entries)
580
+ say(deps, entry.kind, entry.text);
571
581
  if (!result.ok) {
572
582
  say(deps, "warn", stopped);
573
583
  return EXIT_FAIL;
@@ -589,23 +599,26 @@ async function prepareHere(gitRoot, deps, progress) {
589
599
  }
590
600
  const problem = configureProblem(gitRoot);
591
601
  if (problem !== undefined) {
592
- say(deps, "fail", `configure no terminó: ${problem}; corre claude "/cloud-ready configure" y luego prepare otra vez`);
602
+ say(deps, "fail", t().prepare.hereUnfinished(problem));
593
603
  return EXIT_FAIL;
594
604
  }
595
605
  try {
596
606
  writeManual(gitRoot, deps.packageVersion);
597
607
  }
598
608
  catch (err) {
599
- say(deps, "fail", `no se pudo escribir ${MANUAL_FILE}: ${message(err)}`);
609
+ say(deps, "fail", t().prepare.manualFailed(MANUAL_FILE, message(err)));
600
610
  return EXIT_FAIL;
601
611
  }
602
612
  const config = readConfig(gitRoot).config;
603
613
  if (config)
604
- for (const line of styleMarkdown(renderEnvironmentSection(config, deps.packageVersion), deps.color ?? false))
614
+ for (const line of styleMarkdown(renderEnvironmentSection(config, deps.packageVersion, t().environment), deps.color ?? false))
615
+ deps.print(line);
616
+ const steps = t().prepare.hereSteps(MANUAL_FILE);
617
+ if (deps.art)
618
+ finish(deps, { mood: "ok", title: t().prepare.hereTitle, lines: numbered(steps, deps.color ?? false) }, []);
619
+ else
620
+ for (const line of nextSteps(t().prepare.hereTitle, steps, deps.color ?? false))
605
621
  deps.print(line);
606
- const steps = ["Revisa los cambios con git diff", "Haz commit y push", `Configura el entorno en claude.ai (${MANUAL_FILE})`, `Corre ${VERIFY_COMMAND}`];
607
- for (const line of nextSteps("Listo, sin commit", steps, deps.color ?? false))
608
- deps.print(line);
609
622
  return EXIT_OK;
610
623
  }
611
624
  /**
@@ -631,25 +644,25 @@ export async function verifyBranch(repo, deps) {
631
644
  }
632
645
  const branch = await tryCapture(deps, "git", ["rev-parse", "--abbrev-ref", "HEAD"], gitRoot);
633
646
  if (branch === undefined || branch === "HEAD") {
634
- say(deps, "fail", `no estás en una rama; cámbiate a la rama que quieres verificar`);
647
+ say(deps, "fail", t().prepare.noBranch);
635
648
  return EXIT_FAIL;
636
649
  }
637
650
  const problem = configureProblem(gitRoot);
638
651
  if (problem !== undefined) {
639
- say(deps, "fail", `el repo no está preparado (${problem}); corre npx ${PACKAGE} prepare primero`);
652
+ say(deps, "fail", t().prepare.notPrepared(problem));
640
653
  return EXIT_FAIL;
641
654
  }
642
655
  const notes = readConfig(gitRoot).config?.notes ?? DEFAULT_NOTES_FILE;
643
656
  const files = [CONFIG_FILE, MANUAL_FILE, notes].filter((path) => entryExists(join(gitRoot, path)));
644
657
  const dirty = await tryCapture(deps, "git", ["status", "--porcelain", "--", ...files], gitRoot);
645
658
  if (dirty) {
646
- say(deps, "fail", `hay cambios sin commit en la preparación; haz commit y push: la nube solo ve lo que está en origin`);
659
+ say(deps, "fail", t().prepare.dirty);
647
660
  return EXIT_FAIL;
648
661
  }
649
662
  const head = await tryCapture(deps, "git", ["rev-parse", "HEAD"], gitRoot);
650
663
  const remote = (await tryCapture(deps, "git", ["ls-remote", "origin", `refs/heads/${branch}`], gitRoot))?.split(/\s/)[0];
651
664
  if (!head || remote !== head) {
652
- say(deps, "fail", `la rama ${branch} no está subida o tiene commits sin subir; haz git push y vuelve a correr verify`);
665
+ say(deps, "fail", t().prepare.notPushed(branch));
653
666
  return EXIT_FAIL;
654
667
  }
655
668
  const found = await tryCapture(deps, "gh", ["pr", "view", branch, "--json", "number", "--jq", ".number"], gitRoot);
@@ -660,12 +673,39 @@ export async function verifyBranch(repo, deps) {
660
673
  await runInteractive(deps, progress, { cmd: "claude", args: ["--cloud", verifyPrompt({ branch, pr })] }, gitRoot);
661
674
  }
662
675
  catch (err) {
663
- say(deps, "fail", `falló claude --cloud: ${message(err)}`);
676
+ say(deps, "fail", t().prepare.cloudFailed(message(err)));
664
677
  return EXIT_FAIL;
665
678
  }
666
679
  finally {
667
680
  stop?.();
668
681
  }
669
- say(deps, "ok", `la sesión cloud avisará con un push a ${branch}${pr ? " y un comentario en el PR" : ""}`);
682
+ cloudStarted(deps, branch, pr, pr !== undefined);
670
683
  return EXIT_OK;
671
684
  }
685
+ /**
686
+ * Numbers some steps for a block, with the number in color.
687
+ *
688
+ * @param steps - Steps, in order.
689
+ * @param color - Whether to color.
690
+ * @returns The lines.
691
+ * @example
692
+ * numbered(["a"], false); // ["1 a"]
693
+ */
694
+ function numbered(steps, color) {
695
+ return steps.map((step, i) => `${color ? `\x1b[1;35m${i + 1}\x1b[0m` : i + 1} ${step}`);
696
+ }
697
+ /**
698
+ * The final message once the cloud session started: the branch, the PR and how it will report.
699
+ *
700
+ * @param deps - Dependencies.
701
+ * @param branch - Branch the session verifies.
702
+ * @param pr - PR number, when known.
703
+ * @param comment - Whether the session will comment on a PR.
704
+ * @example
705
+ * cloudStarted(deps, "main", "7", true);
706
+ */
707
+ function cloudStarted(deps, branch, pr, comment) {
708
+ const m = t().prepare;
709
+ const rows = [[m.rowBranch, branch], ...(pr ? [[m.rowPr, `#${pr}`]] : [])];
710
+ finish(deps, { mood: "ok", title: m.cloudStarted, rows, lines: [m.cloudWillReport(branch, comment)] }, [m.cloudWillReport(branch, comment)]);
711
+ }
@@ -40,33 +40,6 @@ export function formatLine(kind, text, color) {
40
40
  const { icon, code } = STYLE[kind];
41
41
  return ` ${paint(true, code, icon)} ${kind === "same" ? paint(true, "2", text) : text}`;
42
42
  }
43
- /**
44
- * Guesses the kind of a line `init` printed, from its wording.
45
- *
46
- * @param text - Line, with or without the prefix.
47
- * @returns `ok` for something done, `same` for something already there, `warn` otherwise.
48
- * @example
49
- * initKind("agent-cloud-kit: se creó .claude/cloud.json"); // "ok"
50
- */
51
- export function initKind(text) {
52
- const plain = text.startsWith(OUTPUT_PREFIX) ? text.slice(OUTPUT_PREFIX.length) : text;
53
- if (/^se (creó|agregó|instaló|enlazó|actualizó|escribió)(\s|$)/.test(plain))
54
- return "ok";
55
- if (/\bya (existe|tiene|está)\b/.test(plain))
56
- return "same";
57
- return "warn";
58
- }
59
- /**
60
- * Removes the `agent-cloud-kit: ` prefix of a line, if it has it.
61
- *
62
- * @param text - Line.
63
- * @returns The line without prefix.
64
- * @example
65
- * unprefixed("agent-cloud-kit: hola"); // "hola"
66
- */
67
- export function unprefixed(text) {
68
- return text.startsWith(OUTPUT_PREFIX) ? text.slice(OUTPUT_PREFIX.length) : text;
69
- }
70
43
  /**
71
44
  * Styles a Markdown section for the terminal: headings in bold (and cyan for `##`), code fences
72
45
  * dropped and their lines indented, list items as bullets. Without color, the text is unchanged.
package/dist/run.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * Modules that execute processes receive a `Run` (or a `Capture`) so tests can inject a fake.
4
4
  */
5
5
  import { spawn } from "node:child_process";
6
+ import { t } from "./messages/index.js";
6
7
  /** Signal sent to a process group that ran past its time limit. */
7
8
  const KILL_SIGNAL = "SIGKILL";
8
9
  /** File descriptor of the parent's stderr, reused as the child's stdout and stderr. */
@@ -16,7 +17,7 @@ export class RunTimeoutError extends Error {
16
17
  * @param ms - Time limit, in milliseconds.
17
18
  */
18
19
  constructor(cmd, ms) {
19
- super(`${cmd} superó el límite de ${ms}ms`);
20
+ super(t().run.timedOut(cmd, ms));
20
21
  this.name = "RunTimeoutError";
21
22
  }
22
23
  }
@@ -82,7 +83,7 @@ function spawnAndWait(cmd, args, opts, stdio, onStdout) {
82
83
  clearTimeout(timer);
83
84
  if (code === 0)
84
85
  return resolve();
85
- reject(new Error(`${cmd} terminó con ${code === null ? `la señal ${signal}` : `el código ${code}`}`));
86
+ reject(new Error(code === null ? t().run.exitSignal(cmd, String(signal)) : t().run.exitCode(cmd, code)));
86
87
  });
87
88
  });
88
89
  }
package/dist/select.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * and no dependencies: ↑/↓ move, Enter picks, Esc or Ctrl+C cancels.
4
4
  */
5
5
  import { emitKeypressEvents } from "node:readline";
6
+ import { t } from "./messages/index.js";
6
7
  /** ANSI codes used by the menu. */
7
8
  const ANSI = {
8
9
  dim: (text) => `\x1b[2m${text}\x1b[22m`,
@@ -38,7 +39,7 @@ export function menuKey(state, key, count) {
38
39
  }
39
40
  /**
40
41
  * Draws the menu: the question, the options (hint only on the active one) and, once done, the
41
- * picked option or "Cancelado".
42
+ * picked option or "Cancelado" (in the language of the terminal).
42
43
  *
43
44
  * @param message - Question.
44
45
  * @param choices - Options.
@@ -52,7 +53,7 @@ export function renderMenu(message, choices, state) {
52
53
  if (state.done === "picked")
53
54
  return [`${ANSI.green("◇")} ${message}`, `${bar} ${ANSI.dim(choices[state.active]?.label ?? "")}`, bar];
54
55
  if (state.done === "cancelled")
55
- return [`${ANSI.dim("■")} ${message}`, `${bar} ${ANSI.dim("Cancelado")}`, bar];
56
+ return [`${ANSI.dim("■")} ${message}`, `${bar} ${ANSI.dim(t().menu.cancelled)}`, bar];
56
57
  const lines = [`${ANSI.cyan("◆")} ${message}`];
57
58
  choices.forEach((choice, index) => {
58
59
  if (index === state.active) {
package/dist/session.js CHANGED
@@ -12,10 +12,9 @@ import { realDocker } from "./docker.js";
12
12
  import { pathLine, toolBinDirs } from "./installers.js";
13
13
  import { linkDir } from "./launcher.js";
14
14
  import { resolveNotes } from "./manual.js";
15
+ import { t } from "./messages/index.js";
15
16
  import { RunTimeoutError } from "./run.js";
16
17
  import { fingerprint, readState } from "./state.js";
17
- /** Generic note shown in every cloud session. */
18
- const GENERIC_NOTE = "estás en una sesión cloud de Claude Code. No hay puertos abiertos ni previews: prueba contra localhost. gh pr y gh issue fallan (el proxy de GitHub bloquea GraphQL): usa gh api (REST).";
19
18
  /** Milliseconds in a second. */
20
19
  const MS_PER_SECOND = 1000;
21
20
  /** Seconds in a minute. */
@@ -96,20 +95,18 @@ export function expectedFingerprint(repo, config, state) {
96
95
  */
97
96
  function machineWarnings(repo, config, state, packageVersion) {
98
97
  if (!state) {
99
- return [
100
- `el setup del entorno no corrió para ${basename(repo)}. Pon como script de setup del entorno en claude.ai: npx -y ${packageSpec(packageVersion)} setup`,
101
- ];
98
+ return [t().session.setupNotRun(basename(repo), packageSpec(packageVersion))];
102
99
  }
103
100
  const out = [];
104
101
  const failed = state.steps.filter((s) => !s.ok).map((s) => s.name);
105
102
  if (failed.length)
106
- out.push(`falló el setup: ${failed.join(", ")}. Corre agent-cloud-kit doctor.`);
103
+ out.push(t().session.setupFailed(failed));
107
104
  if (expectedFingerprint(repo, config, state) !== state.fingerprint) {
108
- out.push("la máquina se preparó con otra configuración. Edita el campo del script de setup en claude.ai (cualquier cambio) para que se reconstruya.");
105
+ out.push(t().session.otherConfig);
109
106
  }
110
107
  const total = state.steps.reduce((sum, s) => sum + s.ms, 0);
111
108
  if (total > SETUP_SLOW_MS)
112
- out.push(`el setup tardó ${minutesSeconds(total)}, cerca del límite de ~5 minutos.`);
109
+ out.push(t().session.slow(minutesSeconds(total)));
113
110
  return out;
114
111
  }
115
112
  /**
@@ -129,7 +126,7 @@ function writeEnvFile(env, lines) {
129
126
  return undefined;
130
127
  }
131
128
  catch (err) {
132
- return `no se pudo escribir ${ENV_FILE_VAR} (${file}): ${err instanceof Error ? err.message : String(err)}`;
129
+ return t().session.envFileFailed(ENV_FILE_VAR, file, err instanceof Error ? err.message : String(err));
133
130
  }
134
131
  }
135
132
  /**
@@ -170,7 +167,7 @@ async function runSessionSteps(repo, commands, run, env) {
170
167
  await run("bash", ["-c", command], { cwd: repo, env, stdio: "stderr", timeoutMs: SESSION_STEP_TIMEOUT_MS });
171
168
  }
172
169
  catch (err) {
173
- problems.push(`${err instanceof RunTimeoutError ? "el paso de sesión superó el tiempo límite" : "falló el paso de sesión"}: ${command}`);
170
+ problems.push(err instanceof RunTimeoutError ? t().session.stepTimedOut(command) : t().session.stepFailed(command));
174
171
  }
175
172
  }
176
173
  return problems;
@@ -197,7 +194,7 @@ function startDocker(docker) {
197
194
  * @param out - Output lines, filled in order.
198
195
  */
199
196
  async function sessionBody(repo, ctx, out) {
200
- out.push(GENERIC_NOTE);
197
+ out.push(t().session.genericNote);
201
198
  if (!existsSync(join(repo, CONFIG_FILE)))
202
199
  return;
203
200
  const { config, errors } = readConfig(repo);
@@ -213,13 +210,13 @@ async function sessionBody(repo, ctx, out) {
213
210
  out.push(...machineWarnings(repo, config, state, ctx.packageVersion));
214
211
  const missing = (config?.requiredEnv ?? []).filter((name) => !ctx.env[name]);
215
212
  if (missing.length)
216
- out.push(`faltan variables de entorno: ${missing.join(", ")}`);
213
+ out.push(t().session.missingEnv(missing));
217
214
  const notes = resolveNotes(repo, config?.notes)?.trim();
218
215
  if (notes)
219
216
  out.push(notes);
220
217
  const dockerProblem = startDocker(ctx.docker ?? realDocker);
221
218
  if (dockerProblem)
222
- out.push(`no se pudo iniciar dockerd: ${dockerProblem}`);
219
+ out.push(t().session.dockerFailed(dockerProblem));
223
220
  const env = toolsEnv(dirs, ctx.env, ca);
224
221
  await syncGit(repo, ctx.run, env);
225
222
  out.push(...(await runSessionSteps(repo, config?.session ?? [], ctx.run, env)));
@@ -240,7 +237,7 @@ export async function session(repo, ctx) {
240
237
  await sessionBody(repo, ctx, out);
241
238
  }
242
239
  catch (err) {
243
- out.push(`error en session: ${err instanceof Error ? err.message : String(err)}`);
240
+ out.push(t().session.error(err instanceof Error ? err.message : String(err)));
244
241
  }
245
242
  return out.map((line) => OUTPUT_PREFIX + line);
246
243
  }
package/dist/setup.js CHANGED
@@ -13,6 +13,7 @@ import { configImages, readToolVersions } from "./detect.js";
13
13
  import { realDocker } from "./docker.js";
14
14
  import { curlNodeIndex, installTools, resolveVersions } from "./installers.js";
15
15
  import { linkPath, packageBin, packageInstallDir } from "./launcher.js";
16
+ import { STEP, t } from "./messages/index.js";
16
17
  import { pathDirs, toolsEnv } from "./session.js";
17
18
  import { fingerprint, writeState } from "./state.js";
18
19
  import { lanes, step } from "./steps.js";
@@ -64,16 +65,16 @@ async function aptLane(config, deps) {
64
65
  const env = { ...deps.env, ...APT_ENV };
65
66
  const results = [];
66
67
  for (const url of config.aptRepos) {
67
- const name = `apt repo ${url}`;
68
+ const name = `${STEP.aptRepo}${url}`;
68
69
  if (!url.startsWith(HTTPS_PREFIX)) {
69
- results.push(await refused(name, `el repo de apt debe ser una URL ${HTTPS_PREFIX}`));
70
+ results.push(await refused(name, t().setup.aptRepoHttps(HTTPS_PREFIX)));
70
71
  continue;
71
72
  }
72
73
  results.push(await step(name, () => deps.run("bash", ["-c", APT_REPO_SCRIPT, SCRIPT_NAME, url], { env, timeoutMs: APT_STEP_TIMEOUT_MS })));
73
74
  }
74
75
  const valid = config.apt.filter((pkg) => DEBIAN_PACKAGE_NAME.test(pkg));
75
76
  for (const pkg of config.apt.filter((p) => !valid.includes(p))) {
76
- results.push(await refused(`apt package ${pkg}`, "nombre de paquete Debian inválido"));
77
+ results.push(await refused(`${STEP.aptPackage}${pkg}`, t().setup.badDebianName));
77
78
  }
78
79
  if (!valid.length && !config.aptRepos.length)
79
80
  return results;
@@ -119,11 +120,11 @@ async function dockerLane(config, images, docker, deps) {
119
120
  const results = [];
120
121
  const valid = images.filter((image) => !WHITESPACE.test(image) && !image.startsWith("-") && image !== "");
121
122
  for (const image of images.filter((i) => !valid.includes(i))) {
122
- results.push(await refused(`docker pull ${image}`, "referencia de imagen inválida"));
123
+ results.push(await refused(`docker pull ${image}`, t().setup.badImage));
123
124
  }
124
125
  const up = await docker.ensure().catch(() => false);
125
126
  if (!up)
126
- return [...results, await refused("docker", `dockerd no arrancó; revisa ${DOCKERD_LOG}`)];
127
+ return [...results, await refused("docker", t().docker.didNotStart(DOCKERD_LOG))];
127
128
  return [...results, ...(await pullAll(valid, deps))];
128
129
  }
129
130
  /**
@@ -181,7 +182,7 @@ function manifestVersion(root) {
181
182
  function copyPackage(source, target, version) {
182
183
  const found = manifestVersion(source);
183
184
  if (found !== version)
184
- throw new Error(`el paquete en ${source} es ${found ?? "desconocido"}, no ${version}`);
185
+ throw new Error(t().setup.packageMismatch(source, found, version));
185
186
  const temp = `${target}.${process.pid}.tmp`;
186
187
  rmSync(temp, { recursive: true, force: true });
187
188
  mkdirSync(temp, { recursive: true });
@@ -201,7 +202,7 @@ function copyPackage(source, target, version) {
201
202
  * @returns The self install result.
202
203
  */
203
204
  function selfInstall(version, toolsDir, source) {
204
- return step(`self install ${version}`, async () => {
205
+ return step(`${STEP.selfInstall}${version}`, async () => {
205
206
  const target = packageInstallDir(toolsDir, version);
206
207
  if (manifestVersion(target) !== version)
207
208
  copyPackage(source, target, version);
@@ -243,7 +244,7 @@ async function runPhases(repo, config, ctx) {
243
244
  steps,
244
245
  resolved,
245
246
  };
246
- const saved = await step("write state", async () => writeState(ctx.toolsDir, repo, state));
247
+ const saved = await step(STEP.writeState, async () => writeState(ctx.toolsDir, repo, state));
247
248
  return saved.ok ? state : { ...state, steps: [...steps, saved] };
248
249
  }
249
250
  /**
@@ -267,7 +268,7 @@ export async function setupRepo(repo, ctx) {
267
268
  });
268
269
  const { config, errors } = readConfig(repo);
269
270
  if (!config)
270
- return failed("config", errors.join("; ") || "sin configuración");
271
+ return failed(STEP.config, errors.join("; ") || t().setup.noConfig);
271
272
  try {
272
273
  return await runPhases(repo, config, ctx);
273
274
  }