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.
- package/README.md +21 -10
- package/dist/cli.js +87 -36
- package/dist/config.js +11 -11
- package/dist/docker.js +1 -1
- package/dist/doctor.js +17 -17
- package/dist/init.js +10 -10
- package/dist/installers.js +6 -6
- package/dist/launcher.js +1 -1
- package/dist/manual.js +2 -2
- package/dist/prepare/banner.js +130 -0
- package/dist/prepare/detect.js +10 -11
- package/dist/prepare/index.js +208 -44
- package/dist/run.js +2 -2
- package/dist/select.js +112 -0
- package/dist/session.js +10 -10
- package/dist/setup.js +6 -6
- package/dist/skill.js +6 -6
- package/package.json +1 -1
- package/skills/cloud-ready/SKILL.md +5 -4
package/dist/prepare/index.js
CHANGED
|
@@ -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:
|
|
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 = "
|
|
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
|
|
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 = "
|
|
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
|
-
/**
|
|
46
|
-
const
|
|
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("
|
|
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("
|
|
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};
|
|
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] ??
|
|
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 `
|
|
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}
|
|
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}
|
|
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}
|
|
267
|
+
deps.print(`${OUTPUT_PREFIX}no se pudo crear el worktree: ${message(err)}`);
|
|
265
268
|
return false;
|
|
266
269
|
}
|
|
267
|
-
const stopped = resumeLine("
|
|
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
|
|
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
|
|
293
|
-
deps.print(resumeLine("prepare
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
422
|
+
async function runInteractive(deps, progress, command, cwd) {
|
|
419
423
|
progress.childOwnsTerminal = true;
|
|
420
424
|
try {
|
|
421
|
-
await deps.run(
|
|
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:
|
|
457
|
+
* interruptedLine({}); // "agent-cloud-kit: interrumpido; no se creó nada"
|
|
434
458
|
*/
|
|
435
459
|
function interruptedLine(progress) {
|
|
436
|
-
|
|
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}
|
|
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
|
|
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}
|
|
562
|
+
deps.print(`${OUTPUT_PREFIX}${message(err)}`);
|
|
563
|
+
deps.print(stopped);
|
|
485
564
|
return EXIT_FAIL;
|
|
486
565
|
}
|
|
487
|
-
|
|
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}
|
|
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}
|
|
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 = "
|
|
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
|
-
`
|
|
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
|
|
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("
|
|
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
|
|
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 `
|
|
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 ? "
|
|
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(`
|
|
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(`
|
|
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
|
|
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
|
}
|