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/README.md +17 -10
- package/dist/cli.js +73 -33
- 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/detect.js +10 -11
- package/dist/prepare/index.js +180 -35
- 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/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
|
|
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] ??
|
|
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) });
|
package/dist/prepare/detect.js
CHANGED
|
@@ -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
|
-
* @
|
|
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
|
|
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(
|
|
18
|
+
problems.push(`${command} necesita una terminal interactiva (corre el comando directamente en tu terminal)`);
|
|
18
19
|
if (probe.gitRoot === undefined)
|
|
19
|
-
problems.push(
|
|
20
|
+
problems.push(`esto no es un repositorio git; corre ${command} dentro del repo`);
|
|
20
21
|
if (!probe.hasOrigin)
|
|
21
|
-
problems.push(
|
|
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(
|
|
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(
|
|
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
|
}
|
package/dist/prepare/index.js
CHANGED
|
@@ -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:
|
|
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 = "
|
|
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
|
|
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 = "
|
|
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}
|
|
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("
|
|
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("
|
|
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};
|
|
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] ??
|
|
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 `
|
|
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}
|
|
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}
|
|
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}
|
|
270
|
+
deps.print(`${OUTPUT_PREFIX}no se pudo crear el worktree: ${message(err)}`);
|
|
265
271
|
return false;
|
|
266
272
|
}
|
|
267
|
-
const stopped = resumeLine("
|
|
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
|
|
293
|
-
deps.print(resumeLine("prepare
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
|
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,
|
|
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,
|
|
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:
|
|
439
|
+
* interruptedLine({}); // "agent-cloud-kit: interrumpido; no se creó nada"
|
|
434
440
|
*/
|
|
435
441
|
function interruptedLine(progress) {
|
|
436
|
-
|
|
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}
|
|
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
|
|
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
|
|
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
|
-
|
|
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}
|
|
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
|
}
|