agent-cloud-kit 1.0.0-rc.0 → 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 +23 -13
- package/dist/cli.js +73 -33
- package/dist/config.js +11 -11
- package/dist/constants.js +15 -0
- 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 +21 -9
- package/dist/prepare/detect.js +11 -40
- package/dist/prepare/index.js +184 -42
- package/dist/prepare/prompts.js +6 -21
- package/dist/prepare/questions.js +3 -68
- package/dist/run.js +2 -2
- package/dist/select.js +112 -0
- package/dist/session.js +14 -13
- package/dist/setup.js +6 -6
- package/dist/skill.js +6 -6
- package/package.json +1 -1
- package/skills/cloud-ready/SKILL.md +34 -10
package/dist/installers.js
CHANGED
|
@@ -123,7 +123,7 @@ async function nodeExpectedHash(run, env, dir, version, file) {
|
|
|
123
123
|
await run("curl", ["-fsSL", "-o", path, "--", url], { env, timeoutMs: TOOL_INSTALL_TIMEOUT_MS });
|
|
124
124
|
const hash = parseShasums(readFileSync(path, "utf8"), file);
|
|
125
125
|
if (!hash)
|
|
126
|
-
throw new Error(`${file}
|
|
126
|
+
throw new Error(`${file} no aparece en ${NODE_SHASUMS_FILE}`);
|
|
127
127
|
return hash;
|
|
128
128
|
}
|
|
129
129
|
/**
|
|
@@ -138,7 +138,7 @@ async function nodeExpectedHash(run, env, dir, version, file) {
|
|
|
138
138
|
export function nodeArch(arch) {
|
|
139
139
|
const mapped = NODE_ARCHS[arch];
|
|
140
140
|
if (!mapped)
|
|
141
|
-
throw new Error(`
|
|
141
|
+
throw new Error(`arquitectura no soportada para node: ${arch}`);
|
|
142
142
|
return mapped;
|
|
143
143
|
}
|
|
144
144
|
/**
|
|
@@ -153,7 +153,7 @@ export function nodeArch(arch) {
|
|
|
153
153
|
*/
|
|
154
154
|
export function assertVersion(tool, version) {
|
|
155
155
|
if (!VERSION_PATTERN.test(version))
|
|
156
|
-
throw new Error(`
|
|
156
|
+
throw new Error(`versión inválida para ${tool}: ${version}`);
|
|
157
157
|
}
|
|
158
158
|
/**
|
|
159
159
|
* Tells whether a version is safe to use.
|
|
@@ -254,7 +254,7 @@ export function curlNodeIndex(run, env) {
|
|
|
254
254
|
await run("curl", ["-fsSL", "--max-time", NODE_INDEX_TIMEOUT_SECONDS, "-o", file, "--", NODE_INDEX_URL], { env });
|
|
255
255
|
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
256
256
|
if (!Array.isArray(parsed))
|
|
257
|
-
throw new Error("
|
|
257
|
+
throw new Error("el índice de versiones de node no es una lista");
|
|
258
258
|
return parsed.filter((r) => typeof r === "object" && r !== null && typeof r.version === "string" && (r.lts === false || typeof r.lts === "string"));
|
|
259
259
|
}
|
|
260
260
|
finally {
|
|
@@ -283,7 +283,7 @@ export async function resolveVersions(versions, fetchIndex) {
|
|
|
283
283
|
const result = await step(`node ${requested}`, async () => {
|
|
284
284
|
exact = resolveNodeVersion(requested, await fetchIndex());
|
|
285
285
|
if (!exact)
|
|
286
|
-
throw new Error(`
|
|
286
|
+
throw new Error(`ninguna versión de node coincide con ${requested}`);
|
|
287
287
|
});
|
|
288
288
|
if (!exact)
|
|
289
289
|
return { resolved, results: [result] };
|
|
@@ -403,7 +403,7 @@ export async function installTools(versions, toolsDir, run, env) {
|
|
|
403
403
|
}
|
|
404
404
|
if (tool === "node" && !NODE_EXACT.test(version)) {
|
|
405
405
|
results.push(await step(`${tool} ${version}`, async () => {
|
|
406
|
-
throw new Error(`node ${version}
|
|
406
|
+
throw new Error(`node ${version} debe resolverse primero a una versión exacta`);
|
|
407
407
|
}));
|
|
408
408
|
continue;
|
|
409
409
|
}
|
package/dist/launcher.js
CHANGED
|
@@ -58,7 +58,7 @@ export function planSetup(repos, current, read) {
|
|
|
58
58
|
return repos.map((repo) => {
|
|
59
59
|
const { config, errors } = read(repo);
|
|
60
60
|
if (!config)
|
|
61
|
-
return { repo, action: "skip", reason: errors.length ? errors.join("; ") : `no ${CONFIG_FILE}` };
|
|
61
|
+
return { repo, action: "skip", reason: errors.length ? errors.join("; ") : `no hay ${CONFIG_FILE}` };
|
|
62
62
|
if (config.version === current)
|
|
63
63
|
return { repo, action: "run", version: current };
|
|
64
64
|
return { repo, action: "relaunch", version: config.version };
|
package/dist/manual.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { readConfig } from "./config.js";
|
|
8
|
-
import { CONFIG_FILE, OUTPUT_PREFIX, PACKAGE } from "./constants.js";
|
|
8
|
+
import { CONFIG_FILE, OUTPUT_PREFIX, PACKAGE, packageSpec } from "./constants.js";
|
|
9
9
|
/** Path of the manual, relative to the repo. */
|
|
10
10
|
export const MANUAL_FILE = "docs/agent-cloud.md";
|
|
11
11
|
/** Marker that opens the generated block. */
|
|
@@ -16,17 +16,28 @@ export const END = "<!-- agent-cloud-kit:end -->";
|
|
|
16
16
|
const FINDINGS = "## Hallazgos\n\n_Lo que el agente descubra de la nube va aquí._\n";
|
|
17
17
|
/** Prefix of an https URL. */
|
|
18
18
|
const HTTPS_PREFIX = "https://";
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Setup script the environment must run, for a `bash` block.
|
|
21
|
+
*
|
|
22
|
+
* @param packageVersion - Version of the running package.
|
|
23
|
+
* @returns The script.
|
|
24
|
+
* @example
|
|
25
|
+
* setupScript("1.2.0"); // "#!/bin/bash\nnpx -y agent-cloud-kit@1 setup"
|
|
26
|
+
*/
|
|
27
|
+
function setupScript(packageVersion) {
|
|
28
|
+
return `#!/bin/bash\nnpx -y ${packageSpec(packageVersion)} setup`;
|
|
29
|
+
}
|
|
21
30
|
/** The "how it works" section, five lines. */
|
|
22
31
|
const HOW_IT_WORKS = [
|
|
23
32
|
"## Cómo funciona",
|
|
24
33
|
"",
|
|
25
34
|
"1. agent-kitchen instala las herramientas y los ganchos en el repo.",
|
|
26
|
-
`2. \`${PACKAGE} prepare\` configura el repo con la skill y
|
|
35
|
+
`2. \`${PACKAGE} prepare\` configura el repo con la skill y genera este documento.`,
|
|
27
36
|
"3. Tú configuras el entorno en claude.ai con lo que indica la siguiente sección.",
|
|
28
37
|
"4. Una sesión en la nube verifica que el entorno quedó bien.",
|
|
29
38
|
"5. El PR guarda el resultado.",
|
|
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 vuelve a verificar con `npx agent-cloud-kit verify` desde la rama.",
|
|
30
41
|
].join("\n");
|
|
31
42
|
/**
|
|
32
43
|
* Extracts the host of an apt repo URL.
|
|
@@ -62,11 +73,12 @@ function allowedHosts(config) {
|
|
|
62
73
|
* Renders the "Configurar el entorno en claude.ai" section.
|
|
63
74
|
*
|
|
64
75
|
* @param config - Repo config.
|
|
76
|
+
* @param packageVersion - Version of the running package, for the setup script.
|
|
65
77
|
* @returns The section, without a trailing newline.
|
|
66
78
|
* @example
|
|
67
|
-
* renderEnvironmentSection(config).startsWith("## Configurar el entorno en claude.ai"); // true
|
|
79
|
+
* renderEnvironmentSection(config, "1.0.0").startsWith("## Configurar el entorno en claude.ai"); // true
|
|
68
80
|
*/
|
|
69
|
-
export function renderEnvironmentSection(config) {
|
|
81
|
+
export function renderEnvironmentSection(config, packageVersion) {
|
|
70
82
|
const hosts = allowedHosts(config);
|
|
71
83
|
const lines = [
|
|
72
84
|
"## Configurar el entorno en claude.ai",
|
|
@@ -82,7 +94,7 @@ export function renderEnvironmentSection(config) {
|
|
|
82
94
|
"### Setup script",
|
|
83
95
|
"",
|
|
84
96
|
"```bash",
|
|
85
|
-
|
|
97
|
+
setupScript(packageVersion),
|
|
86
98
|
"```",
|
|
87
99
|
"",
|
|
88
100
|
"### Variables de entorno",
|
|
@@ -154,7 +166,7 @@ export function renderManual(input) {
|
|
|
154
166
|
`<!-- Generado por ${PACKAGE} ${input.packageVersion}. No edites entre las marcas. -->`,
|
|
155
167
|
"# Nube de Claude Code",
|
|
156
168
|
HOW_IT_WORKS,
|
|
157
|
-
renderEnvironmentSection(input.config),
|
|
169
|
+
renderEnvironmentSection(input.config, input.packageVersion),
|
|
158
170
|
VERIFY_SECTION,
|
|
159
171
|
`## Qué corre en la nube\n\n${notes?.trim() ? notes.trim() : "Nada anotado."}`,
|
|
160
172
|
END,
|
|
@@ -193,7 +205,7 @@ export function mergeManual(existing, block) {
|
|
|
193
205
|
export function writeManual(repo, packageVersion) {
|
|
194
206
|
const { config, errors } = readConfig(repo);
|
|
195
207
|
if (!config)
|
|
196
|
-
throw new Error(errors[0] ??
|
|
208
|
+
throw new Error(errors[0] ?? `no existe ${CONFIG_FILE}; corre init primero`);
|
|
197
209
|
const file = join(repo, MANUAL_FILE);
|
|
198
210
|
const existing = existsSync(file) ? readFileSync(file, "utf8") : undefined;
|
|
199
211
|
const block = renderManual({ config, packageVersion, notes: resolveNotes(repo, config.notes) });
|
package/dist/prepare/detect.js
CHANGED
|
@@ -1,57 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file Pure parts of `prepare`:
|
|
2
|
+
* @file Pure parts of `prepare`: preflight checks.
|
|
3
3
|
*/
|
|
4
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
/** File, in the repo root, whose keys are suggested as secret names. */
|
|
7
|
-
const ENV_EXAMPLE_FILE = ".env.example";
|
|
8
|
-
/** Matches a `KEY=value` line (optionally prefixed by `export`) and captures the key. */
|
|
9
|
-
const ENV_LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
|
|
10
4
|
/** Flag `claude` must advertise in its help to launch cloud sessions. */
|
|
11
5
|
const CLOUD_FLAG = "--cloud";
|
|
12
6
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* @param repo Repo root.
|
|
16
|
-
* @returns Keys in file order, without duplicates; empty when the file does not exist.
|
|
17
|
-
* @example
|
|
18
|
-
* suggestSecrets("/repo"); // ["API_TOKEN", "DB_URL"]
|
|
19
|
-
*/
|
|
20
|
-
export function suggestSecrets(repo) {
|
|
21
|
-
const file = join(repo, ENV_EXAMPLE_FILE);
|
|
22
|
-
if (!existsSync(file))
|
|
23
|
-
return [];
|
|
24
|
-
const keys = [];
|
|
25
|
-
for (const line of readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
26
|
-
if (line.trimStart().startsWith("#"))
|
|
27
|
-
continue;
|
|
28
|
-
const key = ENV_LINE.exec(line)?.[1];
|
|
29
|
-
if (key !== undefined && !keys.includes(key))
|
|
30
|
-
keys.push(key);
|
|
31
|
-
}
|
|
32
|
-
return keys;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Checks that `prepare` can run.
|
|
7
|
+
* Checks that `prepare` (or `verify`) can run.
|
|
36
8
|
*
|
|
37
9
|
* @param probe What was observed about the environment.
|
|
38
|
-
* @
|
|
10
|
+
* @param command Command named in the messages.
|
|
11
|
+
* @returns Problems in Spanish, in a fixed order; empty when everything is fine.
|
|
39
12
|
* @example
|
|
40
|
-
* preflight({ isTty: false, hasOrigin: true
|
|
13
|
+
* preflight({ isTty: false, hasOrigin: true }); // ["prepare necesita una terminal interactiva", ...]
|
|
41
14
|
*/
|
|
42
|
-
export function preflight(probe) {
|
|
15
|
+
export function preflight(probe, command = "prepare") {
|
|
43
16
|
const problems = [];
|
|
44
17
|
if (!probe.isTty)
|
|
45
|
-
problems.push(
|
|
18
|
+
problems.push(`${command} necesita una terminal interactiva (corre el comando directamente en tu terminal)`);
|
|
46
19
|
if (probe.gitRoot === undefined)
|
|
47
|
-
problems.push(
|
|
20
|
+
problems.push(`esto no es un repositorio git; corre ${command} dentro del repo`);
|
|
48
21
|
if (!probe.hasOrigin)
|
|
49
|
-
problems.push(
|
|
50
|
-
if (!probe.clean)
|
|
51
|
-
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`);
|
|
52
23
|
if (probe.claudeHelp === undefined)
|
|
53
|
-
problems.push(
|
|
24
|
+
problems.push(`claude no está instalado o no está en el PATH; instala Claude Code y vuelve a correr ${command}`);
|
|
54
25
|
else if (!probe.claudeHelp.includes(CLOUD_FLAG))
|
|
55
|
-
problems.push(
|
|
26
|
+
problems.push(`tu versión de claude no soporta --cloud; actualiza Claude Code y vuelve a correr ${command}`);
|
|
56
27
|
return problems;
|
|
57
28
|
}
|
package/dist/prepare/index.js
CHANGED
|
@@ -9,10 +9,9 @@ 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";
|
|
13
|
-
import { preflight
|
|
12
|
+
import { MANUAL_FILE, renderEnvironmentSection, writeManual } from "../manual.js";
|
|
13
|
+
import { preflight } from "./detect.js";
|
|
14
14
|
import { configurePrompt, verifyPrompt } from "./prompts.js";
|
|
15
|
-
import { askAll } from "./questions.js";
|
|
16
15
|
/** Prefix of the branch `prepare` creates; the day follows it. */
|
|
17
16
|
const BRANCH_PREFIX = "chore/cloud-ready-";
|
|
18
17
|
/** Suffix added to the repo folder name to name the worktree folder, next to the repo. */
|
|
@@ -30,13 +29,13 @@ const KNOWN_PATHS = [
|
|
|
30
29
|
/** Project file of the test-audit skill, committed too when it exists. */
|
|
31
30
|
const TEST_AUDIT_PROJECT_FILE = ".agents/skills/test-audit/project.md";
|
|
32
31
|
/** Message of the commit `prepare` makes. */
|
|
33
|
-
const COMMIT_MESSAGE = "chore:
|
|
32
|
+
const COMMIT_MESSAGE = "chore: prepara el repo para la nube de Claude Code";
|
|
34
33
|
/** Question asked before committing changes outside the known files. */
|
|
35
|
-
const INCLUDE_QUESTION = "
|
|
34
|
+
const INCLUDE_QUESTION = "¿Incluir estos cambios en el commit? [s/N]";
|
|
36
35
|
/** Answers to `INCLUDE_QUESTION` that mean yes. */
|
|
37
|
-
const YES_ANSWER = /^y
|
|
36
|
+
const YES_ANSWER = /^(s|si|sí|y|yes)$/i;
|
|
38
37
|
/** Text shown while the user configures the environment in claude.ai. */
|
|
39
|
-
const WAIT_TEXT = "
|
|
38
|
+
const WAIT_TEXT = "Configura el entorno en claude.ai y luego presiona Enter";
|
|
40
39
|
/** Matches a GitHub remote URL (https or ssh) and captures `owner/repo`. */
|
|
41
40
|
const GITHUB_REMOTE = /github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/;
|
|
42
41
|
/** Matches a pull request URL and captures its number. */
|
|
@@ -44,7 +43,13 @@ const PR_URL = /\/pull\/(\d+)/;
|
|
|
44
43
|
/** Command that continues a prepare run; `npx` because a first-time user does not have the binary on PATH. */
|
|
45
44
|
const RESUME_COMMAND = `npx ${PACKAGE} prepare --resume`;
|
|
46
45
|
/** Printed just before the local agent starts: `prepare` continues only when `claude` exits. */
|
|
47
|
-
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`;
|
|
48
53
|
/** Exit code for success. */
|
|
49
54
|
const EXIT_OK = 0;
|
|
50
55
|
/** Exit code for failure. */
|
|
@@ -57,7 +62,7 @@ const PR_NUMBER = /^\d+$/;
|
|
|
57
62
|
export class InterruptedError extends Error {
|
|
58
63
|
/** Builds the error. */
|
|
59
64
|
constructor() {
|
|
60
|
-
super("
|
|
65
|
+
super("interrumpido");
|
|
61
66
|
this.name = "InterruptedError";
|
|
62
67
|
}
|
|
63
68
|
}
|
|
@@ -80,10 +85,10 @@ export function branchDay(date) {
|
|
|
80
85
|
* @param worktree - Worktree folder.
|
|
81
86
|
* @returns The line to print.
|
|
82
87
|
* @example
|
|
83
|
-
* resumeLine("
|
|
88
|
+
* resumeLine("interrumpido", "/home/u/app-cloud-ready"); // "interrumpido; el worktree está en ...; corre ... --resume ahí para continuar"
|
|
84
89
|
*/
|
|
85
90
|
function resumeLine(reason, worktree) {
|
|
86
|
-
return `${reason};
|
|
91
|
+
return `${reason}; el worktree está en ${worktree}; corre ${RESUME_COMMAND} ahí para continuar`;
|
|
87
92
|
}
|
|
88
93
|
/**
|
|
89
94
|
* Runs a capture and trims its output; never throws.
|
|
@@ -154,7 +159,7 @@ function worktreePath(repo) {
|
|
|
154
159
|
function configureProblem(worktree) {
|
|
155
160
|
const { config, errors } = readConfig(worktree);
|
|
156
161
|
if (!config || errors.length)
|
|
157
|
-
return errors[0] ??
|
|
162
|
+
return errors[0] ?? `no existe ${CONFIG_FILE}`;
|
|
158
163
|
const notes = config.notes ?? DEFAULT_NOTES_FILE;
|
|
159
164
|
const rel = relative(worktree, join(worktree, notes));
|
|
160
165
|
const inside = !isAbsolute(notes) && rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`);
|
|
@@ -165,7 +170,7 @@ function configureProblem(worktree) {
|
|
|
165
170
|
catch {
|
|
166
171
|
// NOTE: a missing file is reported below.
|
|
167
172
|
}
|
|
168
|
-
return `
|
|
173
|
+
return `no existe el archivo de notas ${notes}`;
|
|
169
174
|
}
|
|
170
175
|
/**
|
|
171
176
|
* Lists the paths of `git status --porcelain=v1 -z` output.
|
|
@@ -235,7 +240,7 @@ async function findResume(repo, deps) {
|
|
|
235
240
|
return undefined;
|
|
236
241
|
}
|
|
237
242
|
/**
|
|
238
|
-
* Steps 2–5:
|
|
243
|
+
* Steps 2–5: creates the worktree, installs the kit and runs the local agent.
|
|
239
244
|
*
|
|
240
245
|
* @param repo - Git root of the repo.
|
|
241
246
|
* @param branch - Branch to create.
|
|
@@ -243,20 +248,18 @@ async function findResume(repo, deps) {
|
|
|
243
248
|
* @param deps - Dependencies.
|
|
244
249
|
* @param progress - Gets the worktree once it exists.
|
|
245
250
|
* @returns True when the worktree is ready to commit.
|
|
246
|
-
* @throws InterruptedError when the user presses Ctrl+C during the questions.
|
|
247
251
|
* @example
|
|
248
252
|
* await createAndConfigure("/home/u/app", "chore/cloud-ready-20261102", "/home/u/app-cloud-ready", deps, {});
|
|
249
253
|
*/
|
|
250
254
|
async function createAndConfigure(repo, branch, worktree, deps, progress) {
|
|
251
255
|
if (existsSync(worktree)) {
|
|
252
|
-
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}`);
|
|
253
257
|
return false;
|
|
254
258
|
}
|
|
255
259
|
if ((await tryCapture(deps, "git", ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], repo)) !== undefined) {
|
|
256
|
-
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`);
|
|
257
261
|
return false;
|
|
258
262
|
}
|
|
259
|
-
const answers = await askAll(deps.ask, { secrets: suggestSecrets(repo) }, deps.print);
|
|
260
263
|
const base = `origin/${defaultBranch(repo)}`;
|
|
261
264
|
try {
|
|
262
265
|
await deps.run("git", ["fetch", "-q", "origin"], { cwd: repo });
|
|
@@ -264,10 +267,10 @@ async function createAndConfigure(repo, branch, worktree, deps, progress) {
|
|
|
264
267
|
progress.worktree = worktree;
|
|
265
268
|
}
|
|
266
269
|
catch (err) {
|
|
267
|
-
deps.print(`${OUTPUT_PREFIX}
|
|
270
|
+
deps.print(`${OUTPUT_PREFIX}no se pudo crear el worktree: ${message(err)}`);
|
|
268
271
|
return false;
|
|
269
272
|
}
|
|
270
|
-
const stopped = resumeLine("
|
|
273
|
+
const stopped = resumeLine("el agente local se detuvo", worktree);
|
|
271
274
|
try {
|
|
272
275
|
const result = init(worktree, deps.packageVersion, deps.packageRoot);
|
|
273
276
|
for (const line of result.lines)
|
|
@@ -284,7 +287,7 @@ async function createAndConfigure(repo, branch, worktree, deps, progress) {
|
|
|
284
287
|
}
|
|
285
288
|
deps.print(EXIT_CLAUDE_HINT);
|
|
286
289
|
try {
|
|
287
|
-
await runInteractive(deps, progress, [configurePrompt(
|
|
290
|
+
await runInteractive(deps, progress, [configurePrompt()], worktree);
|
|
288
291
|
}
|
|
289
292
|
catch {
|
|
290
293
|
deps.print(stopped);
|
|
@@ -292,8 +295,8 @@ async function createAndConfigure(repo, branch, worktree, deps, progress) {
|
|
|
292
295
|
}
|
|
293
296
|
const problem = configureProblem(worktree);
|
|
294
297
|
if (problem !== undefined) {
|
|
295
|
-
deps.print(`${OUTPUT_PREFIX}configure
|
|
296
|
-
deps.print(resumeLine("prepare
|
|
298
|
+
deps.print(`${OUTPUT_PREFIX}configure no terminó: ${problem}`);
|
|
299
|
+
deps.print(resumeLine("prepare se detuvo", worktree));
|
|
297
300
|
return false;
|
|
298
301
|
}
|
|
299
302
|
return true;
|
|
@@ -320,7 +323,7 @@ async function commitAndPush(worktree, branch, deps) {
|
|
|
320
323
|
const status = await deps.capture("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: worktree });
|
|
321
324
|
const others = statusPaths(status).filter((path) => !covered(path, known));
|
|
322
325
|
if (others.length) {
|
|
323
|
-
deps.print(`${OUTPUT_PREFIX}
|
|
326
|
+
deps.print(`${OUTPUT_PREFIX}el agente local también cambió:`);
|
|
324
327
|
for (const path of others)
|
|
325
328
|
deps.print(` ${path}`);
|
|
326
329
|
if (YES_ANSWER.test((await deps.ask(INCLUDE_QUESTION, "")).trim())) {
|
|
@@ -328,48 +331,49 @@ async function commitAndPush(worktree, branch, deps) {
|
|
|
328
331
|
}
|
|
329
332
|
else {
|
|
330
333
|
await deps.run("git", ["reset", "-q", "--", ...others], { cwd: worktree });
|
|
331
|
-
deps.print(`${OUTPUT_PREFIX}
|
|
334
|
+
deps.print(`${OUTPUT_PREFIX}quedó sin commitear en ${worktree}`);
|
|
332
335
|
}
|
|
333
336
|
}
|
|
334
337
|
const staged = await deps.capture("git", ["diff", "--cached", "--name-only"], { cwd: worktree });
|
|
335
338
|
if (staged.trim())
|
|
336
339
|
await deps.run("git", ["commit", "-m", COMMIT_MESSAGE], { cwd: worktree });
|
|
337
340
|
else
|
|
338
|
-
deps.print(`${OUTPUT_PREFIX}
|
|
341
|
+
deps.print(`${OUTPUT_PREFIX}no hay nada nuevo que commitear`);
|
|
339
342
|
await deps.run("git", ["push", "-u", "origin", branch], { cwd: worktree });
|
|
340
343
|
}
|
|
341
344
|
catch (err) {
|
|
342
345
|
if (err instanceof InterruptedError)
|
|
343
346
|
throw err;
|
|
344
|
-
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í`);
|
|
345
348
|
return false;
|
|
346
349
|
}
|
|
347
350
|
const existing = await tryCapture(deps, "gh", ["pr", "view", branch, "--json", "number", "--jq", ".number"], worktree);
|
|
348
351
|
if (existing !== undefined && PR_NUMBER.test(existing)) {
|
|
349
|
-
deps.print(`${OUTPUT_PREFIX}
|
|
352
|
+
deps.print(`${OUTPUT_PREFIX}se reutiliza el PR abierto #${existing}`);
|
|
350
353
|
return { pr: existing };
|
|
351
354
|
}
|
|
352
355
|
try {
|
|
353
356
|
const url = await deps.capture("gh", ["pr", "create", "--draft", "--fill"], { cwd: worktree });
|
|
354
|
-
deps.print(`${OUTPUT_PREFIX}
|
|
357
|
+
deps.print(`${OUTPUT_PREFIX}PR en borrador: ${url.trim()}`);
|
|
355
358
|
return { pr: PR_URL.exec(url)?.[1] };
|
|
356
359
|
}
|
|
357
360
|
catch {
|
|
358
361
|
const origin = await tryCapture(deps, "git", ["remote", "get-url", "origin"], worktree);
|
|
359
362
|
const link = compareLink(origin, branch);
|
|
360
|
-
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}`}`);
|
|
361
364
|
return {};
|
|
362
365
|
}
|
|
363
366
|
}
|
|
364
367
|
/**
|
|
365
|
-
* Prepares a repo for Claude Code cloud sessions: checks, asks
|
|
366
|
-
* 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,
|
|
367
370
|
* waits while the user configures the environment and starts a cloud session that verifies it.
|
|
368
371
|
*
|
|
369
372
|
* @param repo - Folder where the user runs `prepare` (inside the repo, or the worktree with resume).
|
|
370
373
|
* @param deps - Dependencies.
|
|
371
374
|
* @param options - `resume` continues in an existing prepare worktree.
|
|
372
|
-
* @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.
|
|
373
377
|
* @example
|
|
374
378
|
* process.exitCode = await prepare(process.cwd(), deps);
|
|
375
379
|
*/
|
|
@@ -377,9 +381,8 @@ export async function prepare(repo, deps, options = {}) {
|
|
|
377
381
|
const gitRoot = await tryCapture(deps, "git", ["rev-parse", "--show-toplevel"], repo);
|
|
378
382
|
const cwd = gitRoot ?? repo;
|
|
379
383
|
const hasOrigin = (await tryCapture(deps, "git", ["remote", "get-url", "origin"], cwd)) !== undefined;
|
|
380
|
-
const clean = options.resume ? true : (await tryCapture(deps, "git", ["status", "--porcelain"], cwd)) === "";
|
|
381
384
|
const claudeHelp = await tryCapture(deps, "claude", ["--help"], cwd);
|
|
382
|
-
const problems = preflight({ isTty: deps.isTty, gitRoot, hasOrigin,
|
|
385
|
+
const problems = preflight({ isTty: deps.isTty, gitRoot, hasOrigin, claudeHelp });
|
|
383
386
|
if (problems.length || gitRoot === undefined) {
|
|
384
387
|
for (const problem of problems)
|
|
385
388
|
deps.print(`${OUTPUT_PREFIX}${problem}`);
|
|
@@ -433,10 +436,12 @@ async function runInteractive(deps, progress, args, cwd) {
|
|
|
433
436
|
* @param progress - What exists so far.
|
|
434
437
|
* @returns The line.
|
|
435
438
|
* @example
|
|
436
|
-
* interruptedLine({}); // "agent-cloud-kit:
|
|
439
|
+
* interruptedLine({}); // "agent-cloud-kit: interrumpido; no se creó nada"
|
|
437
440
|
*/
|
|
438
441
|
function interruptedLine(progress) {
|
|
439
|
-
|
|
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);
|
|
440
445
|
}
|
|
441
446
|
/**
|
|
442
447
|
* Everything after the preflight: resume or create the worktree, commit, wait, launch.
|
|
@@ -456,18 +461,25 @@ async function prepareSteps(gitRoot, deps, options, progress) {
|
|
|
456
461
|
if (options.resume) {
|
|
457
462
|
const found = await findResume(gitRoot, deps);
|
|
458
463
|
if (!found) {
|
|
459
|
-
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}*`);
|
|
460
465
|
return EXIT_FAIL;
|
|
461
466
|
}
|
|
462
467
|
({ worktree, branch } = found);
|
|
463
468
|
progress.worktree = worktree;
|
|
464
469
|
const problem = configureProblem(worktree);
|
|
465
470
|
if (problem) {
|
|
466
|
-
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}`);
|
|
467
472
|
return EXIT_FAIL;
|
|
468
473
|
}
|
|
469
474
|
}
|
|
470
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);
|
|
471
483
|
branch = `${BRANCH_PREFIX}${deps.today}`;
|
|
472
484
|
worktree = worktreePath(gitRoot);
|
|
473
485
|
if (!(await createAndConfigure(gitRoot, branch, worktree, deps, progress)))
|
|
@@ -478,15 +490,145 @@ async function prepareSteps(gitRoot, deps, options, progress) {
|
|
|
478
490
|
return EXIT_FAIL;
|
|
479
491
|
const config = readConfig(worktree).config;
|
|
480
492
|
if (config)
|
|
481
|
-
deps.print(renderEnvironmentSection(config));
|
|
493
|
+
deps.print(renderEnvironmentSection(config, deps.packageVersion));
|
|
482
494
|
await deps.waitEnter(WAIT_TEXT);
|
|
483
495
|
try {
|
|
484
496
|
await runInteractive(deps, progress, ["--cloud", verifyPrompt({ branch, pr: pushed.pr })], worktree);
|
|
485
497
|
}
|
|
486
498
|
catch (err) {
|
|
487
|
-
deps.print(`${OUTPUT_PREFIX}claude --cloud
|
|
499
|
+
deps.print(`${OUTPUT_PREFIX}falló claude --cloud: ${message(err)}; corre ${RESUME_COMMAND} en ${worktree} para reintentar`);
|
|
488
500
|
return EXIT_FAIL;
|
|
489
501
|
}
|
|
490
|
-
deps.print(`${OUTPUT_PREFIX}
|
|
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)}`);
|
|
566
|
+
return EXIT_FAIL;
|
|
567
|
+
}
|
|
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" : ""}`);
|
|
491
633
|
return EXIT_OK;
|
|
492
634
|
}
|