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

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.
@@ -9,7 +9,8 @@ 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
+ import { claudeCommand, enterLine, hasFreeScript, renderBanner } from "./banner.js";
13
14
  import { preflight } from "./detect.js";
14
15
  import { configurePrompt, verifyPrompt } from "./prompts.js";
15
16
  /** Prefix of the branch `prepare` creates; the day follows it. */
@@ -29,21 +30,23 @@ const KNOWN_PATHS = [
29
30
  /** Project file of the test-audit skill, committed too when it exists. */
30
31
  const TEST_AUDIT_PROJECT_FILE = ".agents/skills/test-audit/project.md";
31
32
  /** Message of the commit `prepare` makes. */
32
- const COMMIT_MESSAGE = "chore: prepare the repo for the Claude Code cloud";
33
+ const COMMIT_MESSAGE = "chore: prepara el repo para la nube de Claude Code";
33
34
  /** Question asked before committing changes outside the known files. */
34
- const INCLUDE_QUESTION = "Include these changes in the commit? [y/N]";
35
+ const INCLUDE_QUESTION = "¿Incluir estos cambios en el commit? [s/N]";
35
36
  /** Answers to `INCLUDE_QUESTION` that mean yes. */
36
- const YES_ANSWER = /^y(es)?$/i;
37
+ const YES_ANSWER = /^(s|si|sí|y|yes)$/i;
37
38
  /** Text shown while the user configures the environment in claude.ai. */
38
- const WAIT_TEXT = "Configure the environment in claude.ai, then press Enter";
39
+ const WAIT_TEXT = "Configura el entorno en claude.ai y luego presiona Enter ";
39
40
  /** Matches a GitHub remote URL (https or ssh) and captures `owner/repo`. */
40
41
  const GITHUB_REMOTE = /github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/;
41
42
  /** Matches a pull request URL and captures its number. */
42
43
  const PR_URL = /\/pull\/(\d+)/;
43
44
  /** Command that continues a prepare run; `npx` because a first-time user does not have the binary on PATH. */
44
45
  const RESUME_COMMAND = `npx ${PACKAGE} prepare --resume`;
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
+ /** 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`;
47
50
  /** Exit code for success. */
48
51
  const EXIT_OK = 0;
49
52
  /** Exit code for failure. */
@@ -56,7 +59,7 @@ const PR_NUMBER = /^\d+$/;
56
59
  export class InterruptedError extends Error {
57
60
  /** Builds the error. */
58
61
  constructor() {
59
- super("interrupted");
62
+ super("interrumpido");
60
63
  this.name = "InterruptedError";
61
64
  }
62
65
  }
@@ -79,10 +82,10 @@ export function branchDay(date) {
79
82
  * @param worktree - Worktree folder.
80
83
  * @returns The line to print.
81
84
  * @example
82
- * resumeLine("interrupted", "/home/u/app-cloud-ready"); // "interrupted; the worktree is at ...; run ... --resume there"
85
+ * resumeLine("interrumpido", "/home/u/app-cloud-ready"); // "interrumpido; el worktree está en ...; corre ... --resume ahí para continuar"
83
86
  */
84
87
  function resumeLine(reason, worktree) {
85
- return `${reason}; the worktree is at ${worktree}; run ${RESUME_COMMAND} there`;
88
+ return `${reason}; el worktree está en ${worktree}; corre ${RESUME_COMMAND} ahí para continuar`;
86
89
  }
87
90
  /**
88
91
  * Runs a capture and trims its output; never throws.
@@ -153,7 +156,7 @@ function worktreePath(repo) {
153
156
  function configureProblem(worktree) {
154
157
  const { config, errors } = readConfig(worktree);
155
158
  if (!config || errors.length)
156
- return errors[0] ?? `${CONFIG_FILE} not found`;
159
+ return errors[0] ?? `no existe ${CONFIG_FILE}`;
157
160
  const notes = config.notes ?? DEFAULT_NOTES_FILE;
158
161
  const rel = relative(worktree, join(worktree, notes));
159
162
  const inside = !isAbsolute(notes) && rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`);
@@ -164,7 +167,7 @@ function configureProblem(worktree) {
164
167
  catch {
165
168
  // NOTE: a missing file is reported below.
166
169
  }
167
- return `the notes file ${notes} does not exist`;
170
+ return `no existe el archivo de notas ${notes}`;
168
171
  }
169
172
  /**
170
173
  * Lists the paths of `git status --porcelain=v1 -z` output.
@@ -247,11 +250,11 @@ async function findResume(repo, deps) {
247
250
  */
248
251
  async function createAndConfigure(repo, branch, worktree, deps, progress) {
249
252
  if (existsSync(worktree)) {
250
- deps.print(`${OUTPUT_PREFIX}${worktree} already exists; remove it (git worktree remove ${worktree}) or continue there with ${RESUME_COMMAND}`);
253
+ deps.print(`${OUTPUT_PREFIX}${worktree} ya existe; bórralo (git worktree remove ${worktree}) o continúa ahí con ${RESUME_COMMAND}`);
251
254
  return false;
252
255
  }
253
256
  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`);
257
+ deps.print(`${OUTPUT_PREFIX}la rama ${branch} ya existe; bórrala (git branch -D ${branch}) o corre prepare otro día`);
255
258
  return false;
256
259
  }
257
260
  const base = `origin/${defaultBranch(repo)}`;
@@ -261,10 +264,10 @@ async function createAndConfigure(repo, branch, worktree, deps, progress) {
261
264
  progress.worktree = worktree;
262
265
  }
263
266
  catch (err) {
264
- deps.print(`${OUTPUT_PREFIX}could not create the worktree: ${message(err)}`);
267
+ deps.print(`${OUTPUT_PREFIX}no se pudo crear el worktree: ${message(err)}`);
265
268
  return false;
266
269
  }
267
- const stopped = resumeLine("the local agent stopped", worktree);
270
+ const stopped = resumeLine("el agente local se detuvo", worktree);
268
271
  try {
269
272
  const result = init(worktree, deps.packageVersion, deps.packageRoot);
270
273
  for (const line of result.lines)
@@ -279,18 +282,19 @@ async function createAndConfigure(repo, branch, worktree, deps, progress) {
279
282
  deps.print(stopped);
280
283
  return false;
281
284
  }
282
- deps.print(EXIT_CLAUDE_HINT);
283
285
  try {
284
- await runInteractive(deps, progress, [configurePrompt()], worktree);
286
+ await runConfigure(deps, progress, "pr", worktree);
285
287
  }
286
- catch {
288
+ catch (err) {
289
+ if (err instanceof InterruptedError)
290
+ throw err;
287
291
  deps.print(stopped);
288
292
  return false;
289
293
  }
290
294
  const problem = configureProblem(worktree);
291
295
  if (problem !== undefined) {
292
- deps.print(`${OUTPUT_PREFIX}configure did not finish: ${problem}`);
293
- deps.print(resumeLine("prepare stopped", worktree));
296
+ deps.print(`${OUTPUT_PREFIX}configure no terminó: ${problem}`);
297
+ deps.print(resumeLine("prepare se detuvo", worktree));
294
298
  return false;
295
299
  }
296
300
  return true;
@@ -317,7 +321,7 @@ async function commitAndPush(worktree, branch, deps) {
317
321
  const status = await deps.capture("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: worktree });
318
322
  const others = statusPaths(status).filter((path) => !covered(path, known));
319
323
  if (others.length) {
320
- deps.print(`${OUTPUT_PREFIX}the local agent also changed:`);
324
+ deps.print(`${OUTPUT_PREFIX}el agente local también cambió:`);
321
325
  for (const path of others)
322
326
  deps.print(` ${path}`);
323
327
  if (YES_ANSWER.test((await deps.ask(INCLUDE_QUESTION, "")).trim())) {
@@ -325,48 +329,49 @@ async function commitAndPush(worktree, branch, deps) {
325
329
  }
326
330
  else {
327
331
  await deps.run("git", ["reset", "-q", "--", ...others], { cwd: worktree });
328
- deps.print(`${OUTPUT_PREFIX}left uncommitted in ${worktree}`);
332
+ deps.print(`${OUTPUT_PREFIX}quedó sin commitear en ${worktree}`);
329
333
  }
330
334
  }
331
335
  const staged = await deps.capture("git", ["diff", "--cached", "--name-only"], { cwd: worktree });
332
336
  if (staged.trim())
333
337
  await deps.run("git", ["commit", "-m", COMMIT_MESSAGE], { cwd: worktree });
334
338
  else
335
- deps.print(`${OUTPUT_PREFIX}nothing new to commit`);
339
+ deps.print(`${OUTPUT_PREFIX}no hay nada nuevo que commitear`);
336
340
  await deps.run("git", ["push", "-u", "origin", branch], { cwd: worktree });
337
341
  }
338
342
  catch (err) {
339
343
  if (err instanceof InterruptedError)
340
344
  throw err;
341
- deps.print(`${OUTPUT_PREFIX}could not commit and push: ${message(err)}; fix it in ${worktree} and run ${RESUME_COMMAND} there`);
345
+ deps.print(`${OUTPUT_PREFIX}no se pudo hacer commit y push: ${message(err)}; corrígelo en ${worktree} y corre ${RESUME_COMMAND} ahí`);
342
346
  return false;
343
347
  }
344
348
  const existing = await tryCapture(deps, "gh", ["pr", "view", branch, "--json", "number", "--jq", ".number"], worktree);
345
349
  if (existing !== undefined && PR_NUMBER.test(existing)) {
346
- deps.print(`${OUTPUT_PREFIX}reusing the open PR #${existing}`);
350
+ deps.print(`${OUTPUT_PREFIX}se reutiliza el PR abierto #${existing}`);
347
351
  return { pr: existing };
348
352
  }
349
353
  try {
350
354
  const url = await deps.capture("gh", ["pr", "create", "--draft", "--fill"], { cwd: worktree });
351
- deps.print(`${OUTPUT_PREFIX}draft PR: ${url.trim()}`);
355
+ deps.print(`${OUTPUT_PREFIX}PR en borrador: ${url.trim()}`);
352
356
  return { pr: PR_URL.exec(url)?.[1] };
353
357
  }
354
358
  catch {
355
359
  const origin = await tryCapture(deps, "git", ["remote", "get-url", "origin"], worktree);
356
360
  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}`}`);
361
+ deps.print(`${OUTPUT_PREFIX}no se pudo abrir el PR con gh; ábrelo a mano${link ? `: ${link}` : ` desde la rama ${branch}`}`);
358
362
  return {};
359
363
  }
360
364
  }
361
365
  /**
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,
366
+ * Prepares a repo for Claude Code cloud sessions: checks, asks where to work (a new PR or the
367
+ * 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
368
  * waits while the user configures the environment and starts a cloud session that verifies it.
365
369
  *
366
370
  * @param repo - Folder where the user runs `prepare` (inside the repo, or the worktree with resume).
367
371
  * @param deps - Dependencies.
368
372
  * @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.
373
+ * @returns 0 when the cloud session was started (or, in the current branch, when it is prepared),
374
+ * 130 after Ctrl+C at a question, 1 otherwise.
370
375
  * @example
371
376
  * process.exitCode = await prepare(process.cwd(), deps);
372
377
  */
@@ -374,9 +379,8 @@ export async function prepare(repo, deps, options = {}) {
374
379
  const gitRoot = await tryCapture(deps, "git", ["rev-parse", "--show-toplevel"], repo);
375
380
  const cwd = gitRoot ?? repo;
376
381
  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
382
  const claudeHelp = await tryCapture(deps, "claude", ["--help"], cwd);
379
- const problems = preflight({ isTty: deps.isTty, gitRoot, hasOrigin, clean, claudeHelp });
383
+ const problems = preflight({ isTty: deps.isTty, gitRoot, hasOrigin, claudeHelp });
380
384
  if (problems.length || gitRoot === undefined) {
381
385
  for (const problem of problems)
382
386
  deps.print(`${OUTPUT_PREFIX}${problem}`);
@@ -408,32 +412,54 @@ export async function prepare(repo, deps, options = {}) {
408
412
  *
409
413
  * @param deps - Dependencies.
410
414
  * @param progress - Marks the child as owning the terminal.
411
- * @param args - Arguments of `claude`.
415
+ * @param command - Command and arguments that open Claude.
412
416
  * @param cwd - Worktree.
413
417
  * @returns Resolves when `claude` exits 0.
414
418
  * @throws When `claude` exits non-zero.
415
419
  * @example
416
- * await runInteractive(deps, progress, ["--cloud", prompt], "/home/u/app-cloud-ready");
420
+ * await runInteractive(deps, progress, { cmd: "claude", args: ["--cloud", prompt] }, "/home/u/app-cloud-ready");
417
421
  */
418
- async function runInteractive(deps, progress, args, cwd) {
422
+ async function runInteractive(deps, progress, command, cwd) {
419
423
  progress.childOwnsTerminal = true;
420
424
  try {
421
- await deps.run("claude", args, { cwd });
425
+ await deps.run(command.cmd, command.args, { cwd });
422
426
  }
423
427
  finally {
424
428
  progress.childOwnsTerminal = false;
425
429
  }
426
430
  }
431
+ /**
432
+ * Shows the screen before Claude Code opens, waits for Enter, and runs the local agent with
433
+ * `cloud-ready configure` (through the repo's `claude:free` script when it has one).
434
+ *
435
+ * @param deps - Dependencies.
436
+ * @param progress - Marks the child as owning the terminal.
437
+ * @param place - Where `prepare` works.
438
+ * @param cwd - Folder where Claude runs.
439
+ * @returns Resolves when Claude exits 0.
440
+ * @throws InterruptedError on Ctrl+C at the Enter line; an error when Claude exits non-zero.
441
+ * @example
442
+ * await runConfigure(deps, progress, "pr", "/home/u/app-cloud-ready");
443
+ */
444
+ async function runConfigure(deps, progress, place, cwd) {
445
+ const color = deps.color ?? false;
446
+ for (const line of renderBanner({ place, free: hasFreeScript(cwd), color }))
447
+ deps.print(line);
448
+ await deps.waitEnter(enterLine(color));
449
+ await runInteractive(deps, progress, claudeCommand(cwd, configurePrompt()), cwd);
450
+ }
427
451
  /**
428
452
  * The line printed on Ctrl+C: where the worktree is, when it exists.
429
453
  *
430
454
  * @param progress - What exists so far.
431
455
  * @returns The line.
432
456
  * @example
433
- * interruptedLine({}); // "agent-cloud-kit: interrupted; nothing was created"
457
+ * interruptedLine({}); // "agent-cloud-kit: interrumpido; no se creó nada"
434
458
  */
435
459
  function interruptedLine(progress) {
436
- return progress.worktree === undefined ? `${OUTPUT_PREFIX}interrupted; nothing was created` : resumeLine("interrupted", progress.worktree);
460
+ if (progress.here)
461
+ return `${OUTPUT_PREFIX}interrumpido; lo que alcanzó a cambiar quedó sin commit en tu rama`;
462
+ return progress.worktree === undefined ? `${OUTPUT_PREFIX}interrumpido; no se creó nada` : resumeLine("interrumpido", progress.worktree);
437
463
  }
438
464
  /**
439
465
  * Everything after the preflight: resume or create the worktree, commit, wait, launch.
@@ -453,18 +479,25 @@ async function prepareSteps(gitRoot, deps, options, progress) {
453
479
  if (options.resume) {
454
480
  const found = await findResume(gitRoot, deps);
455
481
  if (!found) {
456
- deps.print(`${OUTPUT_PREFIX}nothing to resume: run it in the ${WORKTREE_SUFFIX} worktree on a ${BRANCH_PREFIX}* branch`);
482
+ deps.print(`${OUTPUT_PREFIX}no hay nada que reanudar: corre prepare --resume en el worktree *${WORKTREE_SUFFIX}, sobre una rama ${BRANCH_PREFIX}*`);
457
483
  return EXIT_FAIL;
458
484
  }
459
485
  ({ worktree, branch } = found);
460
486
  progress.worktree = worktree;
461
487
  const problem = configureProblem(worktree);
462
488
  if (problem) {
463
- deps.print(`${OUTPUT_PREFIX}configure is not finished in ${worktree}: ${problem}; run claude "/cloud-ready configure" there first, then ${RESUME_COMMAND}`);
489
+ deps.print(`${OUTPUT_PREFIX}configure no terminó en ${worktree}: ${problem}; corre claude "/cloud-ready configure" ahí primero y luego ${RESUME_COMMAND}`);
464
490
  return EXIT_FAIL;
465
491
  }
466
492
  }
467
493
  else {
494
+ const place = options.place ?? (await askPlace(gitRoot, deps));
495
+ if (place === null) {
496
+ deps.print(`${OUTPUT_PREFIX}no se cambió nada`);
497
+ return EXIT_FAIL;
498
+ }
499
+ if (place === "here")
500
+ return prepareHere(gitRoot, deps, progress);
468
501
  branch = `${BRANCH_PREFIX}${deps.today}`;
469
502
  worktree = worktreePath(gitRoot);
470
503
  if (!(await createAndConfigure(gitRoot, branch, worktree, deps, progress)))
@@ -478,12 +511,143 @@ async function prepareSteps(gitRoot, deps, options, progress) {
478
511
  deps.print(renderEnvironmentSection(config, deps.packageVersion));
479
512
  await deps.waitEnter(WAIT_TEXT);
480
513
  try {
481
- await runInteractive(deps, progress, ["--cloud", verifyPrompt({ branch, pr: pushed.pr })], worktree);
514
+ await runInteractive(deps, progress, { cmd: "claude", args: ["--cloud", verifyPrompt({ branch, pr: pushed.pr })] }, worktree);
515
+ }
516
+ catch (err) {
517
+ deps.print(`${OUTPUT_PREFIX}falló claude --cloud: ${message(err)}; corre ${RESUME_COMMAND} en ${worktree} para reintentar`);
518
+ return EXIT_FAIL;
519
+ }
520
+ deps.print(`${OUTPUT_PREFIX}la sesión cloud avisará con un push a ${branch} y un comentario en el PR`);
521
+ return EXIT_OK;
522
+ }
523
+ /**
524
+ * Asks where to work, with the same options as agent-kitchen.
525
+ *
526
+ * @param gitRoot - Git root.
527
+ * @param deps - Dependencies.
528
+ * @returns The place, or null when cancelled.
529
+ * @example
530
+ * await askPlace("/home/u/app", deps); // "pr"
531
+ */
532
+ async function askPlace(gitRoot, deps) {
533
+ return deps.choose(PLACE_QUESTION, [
534
+ { value: "pr", label: "En un PR nuevo", hint: `recomendado · parte de origin/${defaultBranch(gitRoot)}, no toca tu rama ni tus archivos` },
535
+ { value: "here", label: "En la rama actual, sin commit", hint: `los revisas con git diff, haces commit y push, y luego corres ${VERIFY_COMMAND}` },
536
+ ], "pr");
537
+ }
538
+ /**
539
+ * The "current branch" place: installs the kit, runs the local agent and writes the manual in the
540
+ * checkout, without committing. The cloud verification comes later, with `verify`, once pushed.
541
+ *
542
+ * @param gitRoot - Git root (the current checkout).
543
+ * @param deps - Dependencies.
544
+ * @param progress - Marks the checkout as changed.
545
+ * @returns The exit code.
546
+ * @example
547
+ * await prepareHere("/home/u/app", deps, {}); // 0
548
+ */
549
+ async function prepareHere(gitRoot, deps, progress) {
550
+ progress.here = true;
551
+ const stopped = `${OUTPUT_PREFIX}prepare se detuvo; lo que alcanzó a cambiar quedó sin commit en tu rama`;
552
+ try {
553
+ const result = init(gitRoot, deps.packageVersion, deps.packageRoot);
554
+ for (const line of result.lines)
555
+ deps.print(line);
556
+ if (!result.ok) {
557
+ deps.print(stopped);
558
+ return EXIT_FAIL;
559
+ }
482
560
  }
483
561
  catch (err) {
484
- deps.print(`${OUTPUT_PREFIX}claude --cloud failed: ${message(err)}; run ${RESUME_COMMAND} in ${worktree} to try again`);
562
+ deps.print(`${OUTPUT_PREFIX}${message(err)}`);
563
+ deps.print(stopped);
485
564
  return EXIT_FAIL;
486
565
  }
487
- deps.print(`${OUTPUT_PREFIX}the cloud session will report back as a push to ${branch} and a comment on the PR`);
566
+ try {
567
+ await runConfigure(deps, progress, "here", gitRoot);
568
+ }
569
+ catch (err) {
570
+ if (err instanceof InterruptedError)
571
+ throw err;
572
+ deps.print(stopped);
573
+ return EXIT_FAIL;
574
+ }
575
+ const problem = configureProblem(gitRoot);
576
+ if (problem !== undefined) {
577
+ deps.print(`${OUTPUT_PREFIX}configure no terminó: ${problem}; corre claude "/cloud-ready configure" y luego prepare otra vez`);
578
+ return EXIT_FAIL;
579
+ }
580
+ try {
581
+ writeManual(gitRoot, deps.packageVersion);
582
+ }
583
+ catch (err) {
584
+ deps.print(`${OUTPUT_PREFIX}no se pudo escribir ${MANUAL_FILE}: ${message(err)}`);
585
+ return EXIT_FAIL;
586
+ }
587
+ const config = readConfig(gitRoot).config;
588
+ if (config)
589
+ deps.print(renderEnvironmentSection(config, deps.packageVersion));
590
+ 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}`);
591
+ return EXIT_OK;
592
+ }
593
+ /**
594
+ * Launches the cloud verification of the current branch: checks that the preparation is there
595
+ * and pushed (the cloud only sees origin), then starts `claude --cloud` with `cloud-ready verify`.
596
+ *
597
+ * @param repo - Folder where the user runs `verify`.
598
+ * @param deps - Dependencies (`ask`, `choose`, `waitEnter` and `today` are not used).
599
+ * @returns 0 when the cloud session was started, 1 otherwise.
600
+ * @example
601
+ * process.exitCode = await verifyBranch(process.cwd(), deps);
602
+ */
603
+ export async function verifyBranch(repo, deps) {
604
+ const gitRoot = await tryCapture(deps, "git", ["rev-parse", "--show-toplevel"], repo);
605
+ const cwd = gitRoot ?? repo;
606
+ const hasOrigin = (await tryCapture(deps, "git", ["remote", "get-url", "origin"], cwd)) !== undefined;
607
+ const claudeHelp = await tryCapture(deps, "claude", ["--help"], cwd);
608
+ const problems = preflight({ isTty: deps.isTty, gitRoot, hasOrigin, claudeHelp }, "verify");
609
+ if (problems.length || gitRoot === undefined) {
610
+ for (const problem of problems)
611
+ deps.print(`${OUTPUT_PREFIX}${problem}`);
612
+ return EXIT_FAIL;
613
+ }
614
+ const branch = await tryCapture(deps, "git", ["rev-parse", "--abbrev-ref", "HEAD"], gitRoot);
615
+ if (branch === undefined || branch === "HEAD") {
616
+ deps.print(`${OUTPUT_PREFIX}no estás en una rama; cámbiate a la rama que quieres verificar`);
617
+ return EXIT_FAIL;
618
+ }
619
+ const problem = configureProblem(gitRoot);
620
+ if (problem !== undefined) {
621
+ deps.print(`${OUTPUT_PREFIX}el repo no está preparado (${problem}); corre npx ${PACKAGE} prepare primero`);
622
+ return EXIT_FAIL;
623
+ }
624
+ const notes = readConfig(gitRoot).config?.notes ?? DEFAULT_NOTES_FILE;
625
+ const files = [CONFIG_FILE, MANUAL_FILE, notes].filter((path) => entryExists(join(gitRoot, path)));
626
+ const dirty = await tryCapture(deps, "git", ["status", "--porcelain", "--", ...files], gitRoot);
627
+ if (dirty) {
628
+ 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`);
629
+ return EXIT_FAIL;
630
+ }
631
+ const head = await tryCapture(deps, "git", ["rev-parse", "HEAD"], gitRoot);
632
+ const remote = (await tryCapture(deps, "git", ["ls-remote", "origin", `refs/heads/${branch}`], gitRoot))?.split(/\s/)[0];
633
+ if (!head || remote !== head) {
634
+ deps.print(`${OUTPUT_PREFIX}la rama ${branch} no está subida o tiene commits sin subir; haz git push y vuelve a correr verify`);
635
+ return EXIT_FAIL;
636
+ }
637
+ const found = await tryCapture(deps, "gh", ["pr", "view", branch, "--json", "number", "--jq", ".number"], gitRoot);
638
+ const pr = found !== undefined && PR_NUMBER.test(found) ? found : undefined;
639
+ const progress = {};
640
+ const stop = deps.onInterrupt?.(() => !progress.childOwnsTerminal);
641
+ try {
642
+ await runInteractive(deps, progress, { cmd: "claude", args: ["--cloud", verifyPrompt({ branch, pr })] }, gitRoot);
643
+ }
644
+ catch (err) {
645
+ deps.print(`${OUTPUT_PREFIX}falló claude --cloud: ${message(err)}`);
646
+ return EXIT_FAIL;
647
+ }
648
+ finally {
649
+ stop?.();
650
+ }
651
+ deps.print(`${OUTPUT_PREFIX}la sesión cloud avisará con un push a ${branch}${pr ? " y un comentario en el PR" : ""}`);
488
652
  return EXIT_OK;
489
653
  }
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
  }