agent-cloud-kit 1.0.0-rc.1 → 1.0.0-rc.2

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/dist/manual.js CHANGED
@@ -37,7 +37,7 @@ const HOW_IT_WORKS = [
37
37
  "4. Una sesión en la nube verifica que el entorno quedó bien.",
38
38
  "5. El PR guarda el resultado.",
39
39
  "",
40
- "Este documento se va actualizando. Cuando la skill `cloud-ready`, en local o en la nube, detecta que hace falta un dominio o una variable de entorno nueva, la agrega a `.claude/cloud.json`, regenera este archivo y te lo dice en la sesión. Cuando cambie la sección \"Configurar el entorno en claude.ai\", aplica lo nuevo en claude.ai y abre una sesión nueva.",
40
+ "Este documento se va actualizando. Cuando la skill `cloud-ready`, en local o en la nube, detecta que hace falta un dominio o una variable de entorno nueva, la agrega a `.claude/cloud.json`, regenera este archivo y te lo dice en la sesión. Cuando cambie la sección \"Configurar el entorno en claude.ai\", aplica lo nuevo en claude.ai y vuelve a verificar con `npx agent-cloud-kit verify` desde la rama.",
41
41
  ].join("\n");
42
42
  /**
43
43
  * Extracts the host of an apt repo URL.
@@ -205,7 +205,7 @@ export function mergeManual(existing, block) {
205
205
  export function writeManual(repo, packageVersion) {
206
206
  const { config, errors } = readConfig(repo);
207
207
  if (!config)
208
- throw new Error(errors[0] ?? `${CONFIG_FILE} not found; run init first`);
208
+ throw new Error(errors[0] ?? `no existe ${CONFIG_FILE}; corre init primero`);
209
209
  const file = join(repo, MANUAL_FILE);
210
210
  const existing = existsSync(file) ? readFileSync(file, "utf8") : undefined;
211
211
  const block = renderManual({ config, packageVersion, notes: resolveNotes(repo, config.notes) });
@@ -4,26 +4,25 @@
4
4
  /** Flag `claude` must advertise in its help to launch cloud sessions. */
5
5
  const CLOUD_FLAG = "--cloud";
6
6
  /**
7
- * Checks that `prepare` can run.
7
+ * Checks that `prepare` (or `verify`) can run.
8
8
  *
9
9
  * @param probe What was observed about the environment.
10
- * @returns Problems in English, in a fixed order; empty when everything is fine.
10
+ * @param command Command named in the messages.
11
+ * @returns Problems in Spanish, in a fixed order; empty when everything is fine.
11
12
  * @example
12
- * preflight({ isTty: false, hasOrigin: true, clean: true }); // ["prepare needs an interactive terminal", ...]
13
+ * preflight({ isTty: false, hasOrigin: true }); // ["prepare necesita una terminal interactiva", ...]
13
14
  */
14
- export function preflight(probe) {
15
+ export function preflight(probe, command = "prepare") {
15
16
  const problems = [];
16
17
  if (!probe.isTty)
17
- problems.push("prepare needs an interactive terminal");
18
+ problems.push(`${command} necesita una terminal interactiva (corre el comando directamente en tu terminal)`);
18
19
  if (probe.gitRoot === undefined)
19
- problems.push("not inside a git repository");
20
+ problems.push(`esto no es un repositorio git; corre ${command} dentro del repo`);
20
21
  if (!probe.hasOrigin)
21
- problems.push("the repository has no 'origin' remote");
22
- if (!probe.clean)
23
- problems.push("the working tree has uncommitted changes; commit or stash them first");
22
+ problems.push(`el repositorio no tiene el remoto 'origin'; ${command} necesita subir una rama a GitHub`);
24
23
  if (probe.claudeHelp === undefined)
25
- problems.push("claude is not installed or not on the PATH");
24
+ problems.push(`claude no está instalado o no está en el PATH; instala Claude Code y vuelve a correr ${command}`);
26
25
  else if (!probe.claudeHelp.includes(CLOUD_FLAG))
27
- problems.push("claude does not support --cloud; update Claude Code");
26
+ problems.push(`tu versión de claude no soporta --cloud; actualiza Claude Code y vuelve a correr ${command}`);
28
27
  return problems;
29
28
  }
@@ -9,7 +9,7 @@ import { readConfig } from "../config.js";
9
9
  import { CONFIG_FILE, OUTPUT_PREFIX, PACKAGE } from "../constants.js";
10
10
  import { defaultBranch } from "../detect.js";
11
11
  import { init } from "../init.js";
12
- import { renderEnvironmentSection, writeManual } from "../manual.js";
12
+ import { MANUAL_FILE, renderEnvironmentSection, writeManual } from "../manual.js";
13
13
  import { preflight } from "./detect.js";
14
14
  import { configurePrompt, verifyPrompt } from "./prompts.js";
15
15
  /** Prefix of the branch `prepare` creates; the day follows it. */
@@ -29,13 +29,13 @@ const KNOWN_PATHS = [
29
29
  /** Project file of the test-audit skill, committed too when it exists. */
30
30
  const TEST_AUDIT_PROJECT_FILE = ".agents/skills/test-audit/project.md";
31
31
  /** Message of the commit `prepare` makes. */
32
- const COMMIT_MESSAGE = "chore: prepare the repo for the Claude Code cloud";
32
+ const COMMIT_MESSAGE = "chore: prepara el repo para la nube de Claude Code";
33
33
  /** Question asked before committing changes outside the known files. */
34
- const INCLUDE_QUESTION = "Include these changes in the commit? [y/N]";
34
+ const INCLUDE_QUESTION = "¿Incluir estos cambios en el commit? [s/N]";
35
35
  /** Answers to `INCLUDE_QUESTION` that mean yes. */
36
- const YES_ANSWER = /^y(es)?$/i;
36
+ const YES_ANSWER = /^(s|si|sí|y|yes)$/i;
37
37
  /** Text shown while the user configures the environment in claude.ai. */
38
- const WAIT_TEXT = "Configure the environment in claude.ai, then press Enter";
38
+ const WAIT_TEXT = "Configura el entorno en claude.ai y luego presiona Enter";
39
39
  /** Matches a GitHub remote URL (https or ssh) and captures `owner/repo`. */
40
40
  const GITHUB_REMOTE = /github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/;
41
41
  /** Matches a pull request URL and captures its number. */
@@ -43,7 +43,13 @@ const PR_URL = /\/pull\/(\d+)/;
43
43
  /** Command that continues a prepare run; `npx` because a first-time user does not have the binary on PATH. */
44
44
  const RESUME_COMMAND = `npx ${PACKAGE} prepare --resume`;
45
45
  /** Printed just before the local agent starts: `prepare` continues only when `claude` exits. */
46
- const EXIT_CLAUDE_HINT = `${OUTPUT_PREFIX}when the skill says configure is done, exit claude (/exit) and prepare will commit, push and open the PR`;
46
+ const EXIT_CLAUDE_HINT = `${OUTPUT_PREFIX}cuando la skill diga que configure terminó, sal de claude (/exit) y prepare hará el commit, el push y abrirá el PR`;
47
+ /** Printed before the local agent starts in the current branch: nothing is committed. */
48
+ const EXIT_CLAUDE_HINT_HERE = `${OUTPUT_PREFIX}cuando la skill diga que configure terminó, sal de claude (/exit)`;
49
+ /** Question of the place menu. */
50
+ const PLACE_QUESTION = "¿Dónde preparo el repo?";
51
+ /** Command that launches the cloud verification of the current branch. */
52
+ const VERIFY_COMMAND = `npx ${PACKAGE} verify`;
47
53
  /** Exit code for success. */
48
54
  const EXIT_OK = 0;
49
55
  /** Exit code for failure. */
@@ -56,7 +62,7 @@ const PR_NUMBER = /^\d+$/;
56
62
  export class InterruptedError extends Error {
57
63
  /** Builds the error. */
58
64
  constructor() {
59
- super("interrupted");
65
+ super("interrumpido");
60
66
  this.name = "InterruptedError";
61
67
  }
62
68
  }
@@ -79,10 +85,10 @@ export function branchDay(date) {
79
85
  * @param worktree - Worktree folder.
80
86
  * @returns The line to print.
81
87
  * @example
82
- * resumeLine("interrupted", "/home/u/app-cloud-ready"); // "interrupted; the worktree is at ...; run ... --resume there"
88
+ * resumeLine("interrumpido", "/home/u/app-cloud-ready"); // "interrumpido; el worktree está en ...; corre ... --resume ahí para continuar"
83
89
  */
84
90
  function resumeLine(reason, worktree) {
85
- return `${reason}; the worktree is at ${worktree}; run ${RESUME_COMMAND} there`;
91
+ return `${reason}; el worktree está en ${worktree}; corre ${RESUME_COMMAND} ahí para continuar`;
86
92
  }
87
93
  /**
88
94
  * Runs a capture and trims its output; never throws.
@@ -153,7 +159,7 @@ function worktreePath(repo) {
153
159
  function configureProblem(worktree) {
154
160
  const { config, errors } = readConfig(worktree);
155
161
  if (!config || errors.length)
156
- return errors[0] ?? `${CONFIG_FILE} not found`;
162
+ return errors[0] ?? `no existe ${CONFIG_FILE}`;
157
163
  const notes = config.notes ?? DEFAULT_NOTES_FILE;
158
164
  const rel = relative(worktree, join(worktree, notes));
159
165
  const inside = !isAbsolute(notes) && rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`);
@@ -164,7 +170,7 @@ function configureProblem(worktree) {
164
170
  catch {
165
171
  // NOTE: a missing file is reported below.
166
172
  }
167
- return `the notes file ${notes} does not exist`;
173
+ return `no existe el archivo de notas ${notes}`;
168
174
  }
169
175
  /**
170
176
  * Lists the paths of `git status --porcelain=v1 -z` output.
@@ -247,11 +253,11 @@ async function findResume(repo, deps) {
247
253
  */
248
254
  async function createAndConfigure(repo, branch, worktree, deps, progress) {
249
255
  if (existsSync(worktree)) {
250
- deps.print(`${OUTPUT_PREFIX}${worktree} already exists; remove it (git worktree remove ${worktree}) or continue there with ${RESUME_COMMAND}`);
256
+ deps.print(`${OUTPUT_PREFIX}${worktree} ya existe; bórralo (git worktree remove ${worktree}) o continúa ahí con ${RESUME_COMMAND}`);
251
257
  return false;
252
258
  }
253
259
  if ((await tryCapture(deps, "git", ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], repo)) !== undefined) {
254
- deps.print(`${OUTPUT_PREFIX}branch ${branch} already exists; delete it (git branch -D ${branch}) or run prepare another day`);
260
+ deps.print(`${OUTPUT_PREFIX}la rama ${branch} ya existe; bórrala (git branch -D ${branch}) o corre prepare otro día`);
255
261
  return false;
256
262
  }
257
263
  const base = `origin/${defaultBranch(repo)}`;
@@ -261,10 +267,10 @@ async function createAndConfigure(repo, branch, worktree, deps, progress) {
261
267
  progress.worktree = worktree;
262
268
  }
263
269
  catch (err) {
264
- deps.print(`${OUTPUT_PREFIX}could not create the worktree: ${message(err)}`);
270
+ deps.print(`${OUTPUT_PREFIX}no se pudo crear el worktree: ${message(err)}`);
265
271
  return false;
266
272
  }
267
- const stopped = resumeLine("the local agent stopped", worktree);
273
+ const stopped = resumeLine("el agente local se detuvo", worktree);
268
274
  try {
269
275
  const result = init(worktree, deps.packageVersion, deps.packageRoot);
270
276
  for (const line of result.lines)
@@ -289,8 +295,8 @@ async function createAndConfigure(repo, branch, worktree, deps, progress) {
289
295
  }
290
296
  const problem = configureProblem(worktree);
291
297
  if (problem !== undefined) {
292
- deps.print(`${OUTPUT_PREFIX}configure did not finish: ${problem}`);
293
- deps.print(resumeLine("prepare stopped", worktree));
298
+ deps.print(`${OUTPUT_PREFIX}configure no terminó: ${problem}`);
299
+ deps.print(resumeLine("prepare se detuvo", worktree));
294
300
  return false;
295
301
  }
296
302
  return true;
@@ -317,7 +323,7 @@ async function commitAndPush(worktree, branch, deps) {
317
323
  const status = await deps.capture("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: worktree });
318
324
  const others = statusPaths(status).filter((path) => !covered(path, known));
319
325
  if (others.length) {
320
- deps.print(`${OUTPUT_PREFIX}the local agent also changed:`);
326
+ deps.print(`${OUTPUT_PREFIX}el agente local también cambió:`);
321
327
  for (const path of others)
322
328
  deps.print(` ${path}`);
323
329
  if (YES_ANSWER.test((await deps.ask(INCLUDE_QUESTION, "")).trim())) {
@@ -325,48 +331,49 @@ async function commitAndPush(worktree, branch, deps) {
325
331
  }
326
332
  else {
327
333
  await deps.run("git", ["reset", "-q", "--", ...others], { cwd: worktree });
328
- deps.print(`${OUTPUT_PREFIX}left uncommitted in ${worktree}`);
334
+ deps.print(`${OUTPUT_PREFIX}quedó sin commitear en ${worktree}`);
329
335
  }
330
336
  }
331
337
  const staged = await deps.capture("git", ["diff", "--cached", "--name-only"], { cwd: worktree });
332
338
  if (staged.trim())
333
339
  await deps.run("git", ["commit", "-m", COMMIT_MESSAGE], { cwd: worktree });
334
340
  else
335
- deps.print(`${OUTPUT_PREFIX}nothing new to commit`);
341
+ deps.print(`${OUTPUT_PREFIX}no hay nada nuevo que commitear`);
336
342
  await deps.run("git", ["push", "-u", "origin", branch], { cwd: worktree });
337
343
  }
338
344
  catch (err) {
339
345
  if (err instanceof InterruptedError)
340
346
  throw err;
341
- deps.print(`${OUTPUT_PREFIX}could not commit and push: ${message(err)}; fix it in ${worktree} and run ${RESUME_COMMAND} there`);
347
+ deps.print(`${OUTPUT_PREFIX}no se pudo hacer commit y push: ${message(err)}; corrígelo en ${worktree} y corre ${RESUME_COMMAND} ahí`);
342
348
  return false;
343
349
  }
344
350
  const existing = await tryCapture(deps, "gh", ["pr", "view", branch, "--json", "number", "--jq", ".number"], worktree);
345
351
  if (existing !== undefined && PR_NUMBER.test(existing)) {
346
- deps.print(`${OUTPUT_PREFIX}reusing the open PR #${existing}`);
352
+ deps.print(`${OUTPUT_PREFIX}se reutiliza el PR abierto #${existing}`);
347
353
  return { pr: existing };
348
354
  }
349
355
  try {
350
356
  const url = await deps.capture("gh", ["pr", "create", "--draft", "--fill"], { cwd: worktree });
351
- deps.print(`${OUTPUT_PREFIX}draft PR: ${url.trim()}`);
357
+ deps.print(`${OUTPUT_PREFIX}PR en borrador: ${url.trim()}`);
352
358
  return { pr: PR_URL.exec(url)?.[1] };
353
359
  }
354
360
  catch {
355
361
  const origin = await tryCapture(deps, "git", ["remote", "get-url", "origin"], worktree);
356
362
  const link = compareLink(origin, branch);
357
- deps.print(`${OUTPUT_PREFIX}could not open the PR with gh; open it by hand${link ? `: ${link}` : ` from branch ${branch}`}`);
363
+ deps.print(`${OUTPUT_PREFIX}no se pudo abrir el PR con gh; ábrelo a mano${link ? `: ${link}` : ` desde la rama ${branch}`}`);
358
364
  return {};
359
365
  }
360
366
  }
361
367
  /**
362
- * Prepares a repo for Claude Code cloud sessions: checks, asks, creates a worktree on a new
363
- * branch, runs the local agent with the `cloud-ready` skill, commits, pushes, opens a draft PR,
368
+ * Prepares a repo for Claude Code cloud sessions: checks, asks where to work (a new PR or the
369
+ * current branch, see `prepareHere`), creates a worktree on a new branch, runs the local agent with the `cloud-ready` skill, commits, pushes, opens a draft PR,
364
370
  * waits while the user configures the environment and starts a cloud session that verifies it.
365
371
  *
366
372
  * @param repo - Folder where the user runs `prepare` (inside the repo, or the worktree with resume).
367
373
  * @param deps - Dependencies.
368
374
  * @param options - `resume` continues in an existing prepare worktree.
369
- * @returns 0 when the cloud session was started, 130 after Ctrl+C at a question, 1 otherwise.
375
+ * @returns 0 when the cloud session was started (or, in the current branch, when it is prepared),
376
+ * 130 after Ctrl+C at a question, 1 otherwise.
370
377
  * @example
371
378
  * process.exitCode = await prepare(process.cwd(), deps);
372
379
  */
@@ -374,9 +381,8 @@ export async function prepare(repo, deps, options = {}) {
374
381
  const gitRoot = await tryCapture(deps, "git", ["rev-parse", "--show-toplevel"], repo);
375
382
  const cwd = gitRoot ?? repo;
376
383
  const hasOrigin = (await tryCapture(deps, "git", ["remote", "get-url", "origin"], cwd)) !== undefined;
377
- const clean = options.resume ? true : (await tryCapture(deps, "git", ["status", "--porcelain"], cwd)) === "";
378
384
  const claudeHelp = await tryCapture(deps, "claude", ["--help"], cwd);
379
- const problems = preflight({ isTty: deps.isTty, gitRoot, hasOrigin, clean, claudeHelp });
385
+ const problems = preflight({ isTty: deps.isTty, gitRoot, hasOrigin, claudeHelp });
380
386
  if (problems.length || gitRoot === undefined) {
381
387
  for (const problem of problems)
382
388
  deps.print(`${OUTPUT_PREFIX}${problem}`);
@@ -430,10 +436,12 @@ async function runInteractive(deps, progress, args, cwd) {
430
436
  * @param progress - What exists so far.
431
437
  * @returns The line.
432
438
  * @example
433
- * interruptedLine({}); // "agent-cloud-kit: interrupted; nothing was created"
439
+ * interruptedLine({}); // "agent-cloud-kit: interrumpido; no se creó nada"
434
440
  */
435
441
  function interruptedLine(progress) {
436
- return progress.worktree === undefined ? `${OUTPUT_PREFIX}interrupted; nothing was created` : resumeLine("interrupted", progress.worktree);
442
+ if (progress.here)
443
+ return `${OUTPUT_PREFIX}interrumpido; lo que alcanzó a cambiar quedó sin commit en tu rama`;
444
+ return progress.worktree === undefined ? `${OUTPUT_PREFIX}interrumpido; no se creó nada` : resumeLine("interrumpido", progress.worktree);
437
445
  }
438
446
  /**
439
447
  * Everything after the preflight: resume or create the worktree, commit, wait, launch.
@@ -453,18 +461,25 @@ async function prepareSteps(gitRoot, deps, options, progress) {
453
461
  if (options.resume) {
454
462
  const found = await findResume(gitRoot, deps);
455
463
  if (!found) {
456
- deps.print(`${OUTPUT_PREFIX}nothing to resume: run it in the ${WORKTREE_SUFFIX} worktree on a ${BRANCH_PREFIX}* branch`);
464
+ deps.print(`${OUTPUT_PREFIX}no hay nada que reanudar: corre prepare --resume en el worktree *${WORKTREE_SUFFIX}, sobre una rama ${BRANCH_PREFIX}*`);
457
465
  return EXIT_FAIL;
458
466
  }
459
467
  ({ worktree, branch } = found);
460
468
  progress.worktree = worktree;
461
469
  const problem = configureProblem(worktree);
462
470
  if (problem) {
463
- deps.print(`${OUTPUT_PREFIX}configure is not finished in ${worktree}: ${problem}; run claude "/cloud-ready configure" there first, then ${RESUME_COMMAND}`);
471
+ deps.print(`${OUTPUT_PREFIX}configure no terminó en ${worktree}: ${problem}; corre claude "/cloud-ready configure" ahí primero y luego ${RESUME_COMMAND}`);
464
472
  return EXIT_FAIL;
465
473
  }
466
474
  }
467
475
  else {
476
+ const place = options.place ?? (await askPlace(gitRoot, deps));
477
+ if (place === null) {
478
+ deps.print(`${OUTPUT_PREFIX}no se cambió nada`);
479
+ return EXIT_FAIL;
480
+ }
481
+ if (place === "here")
482
+ return prepareHere(gitRoot, deps, progress);
468
483
  branch = `${BRANCH_PREFIX}${deps.today}`;
469
484
  worktree = worktreePath(gitRoot);
470
485
  if (!(await createAndConfigure(gitRoot, branch, worktree, deps, progress)))
@@ -481,9 +496,139 @@ async function prepareSteps(gitRoot, deps, options, progress) {
481
496
  await runInteractive(deps, progress, ["--cloud", verifyPrompt({ branch, pr: pushed.pr })], worktree);
482
497
  }
483
498
  catch (err) {
484
- deps.print(`${OUTPUT_PREFIX}claude --cloud failed: ${message(err)}; run ${RESUME_COMMAND} in ${worktree} to try again`);
499
+ deps.print(`${OUTPUT_PREFIX}falló claude --cloud: ${message(err)}; corre ${RESUME_COMMAND} en ${worktree} para reintentar`);
500
+ return EXIT_FAIL;
501
+ }
502
+ deps.print(`${OUTPUT_PREFIX}la sesión cloud avisará con un push a ${branch} y un comentario en el PR`);
503
+ return EXIT_OK;
504
+ }
505
+ /**
506
+ * Asks where to work, with the same options as agent-kitchen.
507
+ *
508
+ * @param gitRoot - Git root.
509
+ * @param deps - Dependencies.
510
+ * @returns The place, or null when cancelled.
511
+ * @example
512
+ * await askPlace("/home/u/app", deps); // "pr"
513
+ */
514
+ async function askPlace(gitRoot, deps) {
515
+ return deps.choose(PLACE_QUESTION, [
516
+ { value: "pr", label: "En un PR nuevo", hint: `recomendado · parte de origin/${defaultBranch(gitRoot)}, no toca tu rama ni tus archivos` },
517
+ { value: "here", label: "En la rama actual, sin commit", hint: `los revisas con git diff, haces commit y push, y luego corres ${VERIFY_COMMAND}` },
518
+ ], "pr");
519
+ }
520
+ /**
521
+ * The "current branch" place: installs the kit, runs the local agent and writes the manual in the
522
+ * checkout, without committing. The cloud verification comes later, with `verify`, once pushed.
523
+ *
524
+ * @param gitRoot - Git root (the current checkout).
525
+ * @param deps - Dependencies.
526
+ * @param progress - Marks the checkout as changed.
527
+ * @returns The exit code.
528
+ * @example
529
+ * await prepareHere("/home/u/app", deps, {}); // 0
530
+ */
531
+ async function prepareHere(gitRoot, deps, progress) {
532
+ progress.here = true;
533
+ const stopped = `${OUTPUT_PREFIX}prepare se detuvo; lo que alcanzó a cambiar quedó sin commit en tu rama`;
534
+ try {
535
+ const result = init(gitRoot, deps.packageVersion, deps.packageRoot);
536
+ for (const line of result.lines)
537
+ deps.print(line);
538
+ if (!result.ok) {
539
+ deps.print(stopped);
540
+ return EXIT_FAIL;
541
+ }
542
+ }
543
+ catch (err) {
544
+ deps.print(`${OUTPUT_PREFIX}${message(err)}`);
545
+ deps.print(stopped);
546
+ return EXIT_FAIL;
547
+ }
548
+ deps.print(EXIT_CLAUDE_HINT_HERE);
549
+ try {
550
+ await runInteractive(deps, progress, [configurePrompt()], gitRoot);
551
+ }
552
+ catch {
553
+ deps.print(stopped);
554
+ return EXIT_FAIL;
555
+ }
556
+ const problem = configureProblem(gitRoot);
557
+ if (problem !== undefined) {
558
+ deps.print(`${OUTPUT_PREFIX}configure no terminó: ${problem}; corre claude "/cloud-ready configure" y luego prepare otra vez`);
559
+ return EXIT_FAIL;
560
+ }
561
+ try {
562
+ writeManual(gitRoot, deps.packageVersion);
563
+ }
564
+ catch (err) {
565
+ deps.print(`${OUTPUT_PREFIX}no se pudo escribir ${MANUAL_FILE}: ${message(err)}`);
485
566
  return EXIT_FAIL;
486
567
  }
487
- deps.print(`${OUTPUT_PREFIX}the cloud session will report back as a push to ${branch} and a comment on the PR`);
568
+ const config = readConfig(gitRoot).config;
569
+ if (config)
570
+ deps.print(renderEnvironmentSection(config, deps.packageVersion));
571
+ deps.print(`${OUTPUT_PREFIX}listo, sin commit. Revisa los cambios con git diff, haz commit y push, configura el entorno en claude.ai (${MANUAL_FILE}) y corre ${VERIFY_COMMAND}`);
572
+ return EXIT_OK;
573
+ }
574
+ /**
575
+ * Launches the cloud verification of the current branch: checks that the preparation is there
576
+ * and pushed (the cloud only sees origin), then starts `claude --cloud` with `cloud-ready verify`.
577
+ *
578
+ * @param repo - Folder where the user runs `verify`.
579
+ * @param deps - Dependencies (`ask`, `choose`, `waitEnter` and `today` are not used).
580
+ * @returns 0 when the cloud session was started, 1 otherwise.
581
+ * @example
582
+ * process.exitCode = await verifyBranch(process.cwd(), deps);
583
+ */
584
+ export async function verifyBranch(repo, deps) {
585
+ const gitRoot = await tryCapture(deps, "git", ["rev-parse", "--show-toplevel"], repo);
586
+ const cwd = gitRoot ?? repo;
587
+ const hasOrigin = (await tryCapture(deps, "git", ["remote", "get-url", "origin"], cwd)) !== undefined;
588
+ const claudeHelp = await tryCapture(deps, "claude", ["--help"], cwd);
589
+ const problems = preflight({ isTty: deps.isTty, gitRoot, hasOrigin, claudeHelp }, "verify");
590
+ if (problems.length || gitRoot === undefined) {
591
+ for (const problem of problems)
592
+ deps.print(`${OUTPUT_PREFIX}${problem}`);
593
+ return EXIT_FAIL;
594
+ }
595
+ const branch = await tryCapture(deps, "git", ["rev-parse", "--abbrev-ref", "HEAD"], gitRoot);
596
+ if (branch === undefined || branch === "HEAD") {
597
+ deps.print(`${OUTPUT_PREFIX}no estás en una rama; cámbiate a la rama que quieres verificar`);
598
+ return EXIT_FAIL;
599
+ }
600
+ const problem = configureProblem(gitRoot);
601
+ if (problem !== undefined) {
602
+ deps.print(`${OUTPUT_PREFIX}el repo no está preparado (${problem}); corre npx ${PACKAGE} prepare primero`);
603
+ return EXIT_FAIL;
604
+ }
605
+ const notes = readConfig(gitRoot).config?.notes ?? DEFAULT_NOTES_FILE;
606
+ const files = [CONFIG_FILE, MANUAL_FILE, notes].filter((path) => entryExists(join(gitRoot, path)));
607
+ const dirty = await tryCapture(deps, "git", ["status", "--porcelain", "--", ...files], gitRoot);
608
+ if (dirty) {
609
+ deps.print(`${OUTPUT_PREFIX}hay cambios sin commit en la preparación; haz commit y push: la nube solo ve lo que está en origin`);
610
+ return EXIT_FAIL;
611
+ }
612
+ const head = await tryCapture(deps, "git", ["rev-parse", "HEAD"], gitRoot);
613
+ const remote = (await tryCapture(deps, "git", ["ls-remote", "origin", `refs/heads/${branch}`], gitRoot))?.split(/\s/)[0];
614
+ if (!head || remote !== head) {
615
+ deps.print(`${OUTPUT_PREFIX}la rama ${branch} no está subida o tiene commits sin subir; haz git push y vuelve a correr verify`);
616
+ return EXIT_FAIL;
617
+ }
618
+ const found = await tryCapture(deps, "gh", ["pr", "view", branch, "--json", "number", "--jq", ".number"], gitRoot);
619
+ const pr = found !== undefined && PR_NUMBER.test(found) ? found : undefined;
620
+ const progress = {};
621
+ const stop = deps.onInterrupt?.(() => !progress.childOwnsTerminal);
622
+ try {
623
+ await runInteractive(deps, progress, ["--cloud", verifyPrompt({ branch, pr })], gitRoot);
624
+ }
625
+ catch (err) {
626
+ deps.print(`${OUTPUT_PREFIX}falló claude --cloud: ${message(err)}`);
627
+ return EXIT_FAIL;
628
+ }
629
+ finally {
630
+ stop?.();
631
+ }
632
+ deps.print(`${OUTPUT_PREFIX}la sesión cloud avisará con un push a ${branch}${pr ? " y un comentario en el PR" : ""}`);
488
633
  return EXIT_OK;
489
634
  }
package/dist/run.js CHANGED
@@ -16,7 +16,7 @@ export class RunTimeoutError extends Error {
16
16
  * @param ms - Time limit, in milliseconds.
17
17
  */
18
18
  constructor(cmd, ms) {
19
- super(`${cmd} timed out after ${ms}ms`);
19
+ super(`${cmd} superó el límite de ${ms}ms`);
20
20
  this.name = "RunTimeoutError";
21
21
  }
22
22
  }
@@ -82,7 +82,7 @@ function spawnAndWait(cmd, args, opts, stdio, onStdout) {
82
82
  clearTimeout(timer);
83
83
  if (code === 0)
84
84
  return resolve();
85
- reject(new Error(`${cmd} exited with ${code === null ? `signal ${signal}` : `code ${code}`}`));
85
+ reject(new Error(`${cmd} terminó con ${code === null ? `la señal ${signal}` : `el código ${code}`}`));
86
86
  });
87
87
  });
88
88
  }
package/dist/select.js ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @file A small arrow-key menu for the terminal, with the look of agent-kitchen's (`@clack/prompts`)
3
+ * and no dependencies: ↑/↓ move, Enter picks, Esc or Ctrl+C cancels.
4
+ */
5
+ import { emitKeypressEvents } from "node:readline";
6
+ /** ANSI codes used by the menu. */
7
+ const ANSI = {
8
+ dim: (text) => `\x1b[2m${text}\x1b[22m`,
9
+ cyan: (text) => `\x1b[36m${text}\x1b[39m`,
10
+ green: (text) => `\x1b[32m${text}\x1b[39m`,
11
+ hideCursor: "\x1b[?25l",
12
+ showCursor: "\x1b[?25h",
13
+ up: (lines) => (lines > 0 ? `\x1b[${lines}A` : ""),
14
+ clearDown: "\r\x1b[J",
15
+ };
16
+ /**
17
+ * Applies one key to the menu.
18
+ *
19
+ * @param state - Current state.
20
+ * @param key - Key pressed.
21
+ * @param count - Number of options.
22
+ * @returns The new state.
23
+ * @example
24
+ * menuKey({ active: 0 }, { name: "down" }, 2); // { active: 1 }
25
+ */
26
+ export function menuKey(state, key, count) {
27
+ if (key.ctrl && key.name === "c")
28
+ return { ...state, done: "cancelled" };
29
+ if (key.name === "escape")
30
+ return { ...state, done: "cancelled" };
31
+ if (key.name === "return" || key.name === "enter")
32
+ return { ...state, done: "picked" };
33
+ if (key.name === "up" || key.name === "k")
34
+ return { active: (state.active - 1 + count) % count };
35
+ if (key.name === "down" || key.name === "j")
36
+ return { active: (state.active + 1) % count };
37
+ return state;
38
+ }
39
+ /**
40
+ * Draws the menu: the question, the options (hint only on the active one) and, once done, the
41
+ * picked option or "Cancelado".
42
+ *
43
+ * @param message - Question.
44
+ * @param choices - Options.
45
+ * @param state - Current state.
46
+ * @returns The lines to print.
47
+ * @example
48
+ * renderMenu("¿Dónde?", [{ value: "a", label: "A" }], { active: 0 })[0]; // "◆ ¿Dónde?" (with color)
49
+ */
50
+ export function renderMenu(message, choices, state) {
51
+ const bar = ANSI.dim("│");
52
+ if (state.done === "picked")
53
+ return [`${ANSI.green("◇")} ${message}`, `${bar} ${ANSI.dim(choices[state.active]?.label ?? "")}`, bar];
54
+ if (state.done === "cancelled")
55
+ return [`${ANSI.dim("■")} ${message}`, `${bar} ${ANSI.dim("Cancelado")}`, bar];
56
+ const lines = [`${ANSI.cyan("◆")} ${message}`];
57
+ choices.forEach((choice, index) => {
58
+ if (index === state.active) {
59
+ const hint = choice.hint ? ` ${ANSI.dim(`(${choice.hint})`)}` : "";
60
+ lines.push(`${ANSI.cyan("│")} ${ANSI.green("●")} ${choice.label}${hint}`);
61
+ }
62
+ else {
63
+ lines.push(`${ANSI.cyan("│")} ${ANSI.dim(`○ ${choice.label}`)}`);
64
+ }
65
+ });
66
+ lines.push(ANSI.cyan("└"));
67
+ return lines;
68
+ }
69
+ /**
70
+ * Shows the menu on the process terminal. Raw mode is on only while it is open, so a later
71
+ * `readline` question or a child process gets a normal terminal.
72
+ *
73
+ * @param message - Question.
74
+ * @param choices - Options.
75
+ * @param initial - Value active at the start.
76
+ * @returns The picked value, or null on Esc or Ctrl+C.
77
+ * @example
78
+ * await chooseTerminal("¿Dónde preparo el repo?", [{ value: "pr", label: "En un PR nuevo" }], "pr"); // "pr"
79
+ */
80
+ export const chooseTerminal = async (message, choices, initial) => {
81
+ const { stdin, stdout } = process;
82
+ let state = { active: Math.max(0, choices.findIndex((c) => c.value === initial)) };
83
+ let drawn = 0;
84
+ const draw = () => {
85
+ const lines = renderMenu(message, choices, state);
86
+ stdout.write(`${ANSI.up(drawn)}${ANSI.clearDown}${lines.join("\n")}\n`);
87
+ drawn = lines.length;
88
+ };
89
+ emitKeypressEvents(stdin);
90
+ stdin.setRawMode(true);
91
+ stdin.resume();
92
+ stdout.write(ANSI.hideCursor);
93
+ draw();
94
+ try {
95
+ return await new Promise((resolvePick) => {
96
+ const onKey = (_text, key = {}) => {
97
+ state = menuKey(state, key, choices.length);
98
+ draw();
99
+ if (state.done === undefined)
100
+ return;
101
+ stdin.off("keypress", onKey);
102
+ resolvePick(state.done === "picked" ? (choices[state.active]?.value ?? null) : null);
103
+ };
104
+ stdin.on("keypress", onKey);
105
+ });
106
+ }
107
+ finally {
108
+ stdin.setRawMode(false);
109
+ stdin.pause();
110
+ stdout.write(ANSI.showCursor);
111
+ }
112
+ };
package/dist/session.js CHANGED
@@ -15,7 +15,7 @@ import { resolveNotes } from "./manual.js";
15
15
  import { RunTimeoutError } from "./run.js";
16
16
  import { fingerprint, readState } from "./state.js";
17
17
  /** Generic note shown in every cloud session. */
18
- const GENERIC_NOTE = "you are in a Claude Code cloud session. There are no open ports or previews: test against localhost. gh pr and gh issue fail (the GitHub proxy blocks GraphQL): use gh api (REST).";
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
19
  /** Milliseconds in a second. */
20
20
  const MS_PER_SECOND = 1000;
21
21
  /** Seconds in a minute. */
@@ -97,19 +97,19 @@ export function expectedFingerprint(repo, config, state) {
97
97
  function machineWarnings(repo, config, state, packageVersion) {
98
98
  if (!state) {
99
99
  return [
100
- `the environment setup did not run for ${basename(repo)}. Set the setup script of the environment in claude.ai to: npx -y ${packageSpec(packageVersion)} setup`,
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
101
  ];
102
102
  }
103
103
  const out = [];
104
104
  const failed = state.steps.filter((s) => !s.ok).map((s) => s.name);
105
105
  if (failed.length)
106
- out.push(`setup failed: ${failed.join(", ")}. Run agent-cloud-kit doctor.`);
106
+ out.push(`falló el setup: ${failed.join(", ")}. Corre agent-cloud-kit doctor.`);
107
107
  if (expectedFingerprint(repo, config, state) !== state.fingerprint) {
108
- out.push("the machine was prepared with another configuration. Edit the setup script field in claude.ai (any change) so it is rebuilt.");
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.");
109
109
  }
110
110
  const total = state.steps.reduce((sum, s) => sum + s.ms, 0);
111
111
  if (total > SETUP_SLOW_MS)
112
- out.push(`setup took ${minutesSeconds(total)}, close to the ~5 minute limit.`);
112
+ out.push(`el setup tardó ${minutesSeconds(total)}, cerca del límite de ~5 minutos.`);
113
113
  return out;
114
114
  }
115
115
  /**
@@ -129,7 +129,7 @@ function writeEnvFile(env, lines) {
129
129
  return undefined;
130
130
  }
131
131
  catch (err) {
132
- return `could not write ${ENV_FILE_VAR} (${file}): ${err instanceof Error ? err.message : String(err)}`;
132
+ return `no se pudo escribir ${ENV_FILE_VAR} (${file}): ${err instanceof Error ? err.message : String(err)}`;
133
133
  }
134
134
  }
135
135
  /**
@@ -170,7 +170,7 @@ async function runSessionSteps(repo, commands, run, env) {
170
170
  await run("bash", ["-c", command], { cwd: repo, env, stdio: "stderr", timeoutMs: SESSION_STEP_TIMEOUT_MS });
171
171
  }
172
172
  catch (err) {
173
- problems.push(`${err instanceof RunTimeoutError ? "session step timed out" : "session step failed"}: ${command}`);
173
+ problems.push(`${err instanceof RunTimeoutError ? "el paso de sesión superó el tiempo límite" : "falló el paso de sesión"}: ${command}`);
174
174
  }
175
175
  }
176
176
  return problems;
@@ -213,13 +213,13 @@ async function sessionBody(repo, ctx, out) {
213
213
  out.push(...machineWarnings(repo, config, state, ctx.packageVersion));
214
214
  const missing = (config?.requiredEnv ?? []).filter((name) => !ctx.env[name]);
215
215
  if (missing.length)
216
- out.push(`missing environment variables: ${missing.join(", ")}`);
216
+ out.push(`faltan variables de entorno: ${missing.join(", ")}`);
217
217
  const notes = resolveNotes(repo, config?.notes)?.trim();
218
218
  if (notes)
219
219
  out.push(notes);
220
220
  const dockerProblem = startDocker(ctx.docker ?? realDocker);
221
221
  if (dockerProblem)
222
- out.push(`could not start dockerd: ${dockerProblem}`);
222
+ out.push(`no se pudo iniciar dockerd: ${dockerProblem}`);
223
223
  const env = toolsEnv(dirs, ctx.env, ca);
224
224
  await syncGit(repo, ctx.run, env);
225
225
  out.push(...(await runSessionSteps(repo, config?.session ?? [], ctx.run, env)));
@@ -240,7 +240,7 @@ export async function session(repo, ctx) {
240
240
  await sessionBody(repo, ctx, out);
241
241
  }
242
242
  catch (err) {
243
- out.push(`session error: ${err instanceof Error ? err.message : String(err)}`);
243
+ out.push(`error en session: ${err instanceof Error ? err.message : String(err)}`);
244
244
  }
245
245
  return out.map((line) => OUTPUT_PREFIX + line);
246
246
  }