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/prepare/prompts.js
CHANGED
|
@@ -1,31 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* @param items Items.
|
|
5
|
-
* @param none Text when there are no items.
|
|
6
|
-
* @returns The items joined by commas, or `none`.
|
|
7
|
-
* @example
|
|
8
|
-
* list(["A", "B"], "ninguna"); // "A, B"
|
|
2
|
+
* @file Prompts `prepare` hands to Claude, in Spanish: they invoke the `cloud-ready` skill.
|
|
9
3
|
*/
|
|
10
|
-
function list(items, none) {
|
|
11
|
-
return items.length > 0 ? items.join(", ") : none;
|
|
12
|
-
}
|
|
13
4
|
/**
|
|
14
|
-
* Prompt for the local session: `configure` mode
|
|
5
|
+
* Prompt for the local session: `configure` mode. `prepare` asks nothing first; the skill detects
|
|
6
|
+
* secrets and domains itself.
|
|
15
7
|
*
|
|
16
|
-
* @param answers What the user answered.
|
|
17
8
|
* @returns The prompt.
|
|
18
9
|
* @example
|
|
19
|
-
* configurePrompt(
|
|
10
|
+
* configurePrompt(); // "/cloud-ready configure"
|
|
20
11
|
*/
|
|
21
|
-
export function configurePrompt(
|
|
22
|
-
return
|
|
23
|
-
"/cloud-ready configure",
|
|
24
|
-
"",
|
|
25
|
-
"Respuestas del usuario (no las vuelvas a preguntar):",
|
|
26
|
-
`- Variables secretas: ${list(answers.secrets, "ninguna")}`,
|
|
27
|
-
`- Dominios extra: ${list(answers.domains, "ninguno")}`,
|
|
28
|
-
].join("\n");
|
|
12
|
+
export function configurePrompt() {
|
|
13
|
+
return "/cloud-ready configure";
|
|
29
14
|
}
|
|
30
15
|
/**
|
|
31
16
|
* Prompt for the cloud session: `verify` mode with the prepare branch and, when known, the PR.
|
|
@@ -1,70 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file
|
|
3
|
-
* the skill
|
|
2
|
+
* @file How `prepare` asks the user a question. It asks nothing up front: secrets and domains are
|
|
3
|
+
* detected by the `cloud-ready` skill, locally or in the cloud, when they are needed.
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
import { OUTPUT_PREFIX } from "../constants.js";
|
|
7
|
-
/** Answer that leaves a list empty. */
|
|
8
|
-
const EMPTY_LIST_ANSWER = "-";
|
|
9
|
-
/**
|
|
10
|
-
* Turns a raw answer into a list.
|
|
11
|
-
*
|
|
12
|
-
* @param answer Raw answer: comma-separated, empty to accept, `-` for none.
|
|
13
|
-
* @param suggestion Suggested items, used when the answer is empty.
|
|
14
|
-
* @returns The items.
|
|
15
|
-
* @example
|
|
16
|
-
* parseList("a, b", []); // ["a", "b"]
|
|
17
|
-
*/
|
|
18
|
-
function parseList(answer, suggestion) {
|
|
19
|
-
const text = answer.trim();
|
|
20
|
-
if (text === "")
|
|
21
|
-
return suggestion;
|
|
22
|
-
if (text === EMPTY_LIST_ANSWER)
|
|
23
|
-
return [];
|
|
24
|
-
return text.split(",").map((item) => item.trim()).filter((item) => item !== "");
|
|
25
|
-
}
|
|
26
|
-
/** Example host shown when a typed domain is not a host name. */
|
|
27
|
-
const HOST_EXAMPLE = "api.example.com";
|
|
28
|
-
/**
|
|
29
|
-
* Keeps the typed domains that are host names (the rule of `domains` in the config). Each invalid
|
|
30
|
-
* entry is asked again once; if it is still not a host name it is dropped with a printed note.
|
|
31
|
-
*
|
|
32
|
-
* @param ask Function that asks one question.
|
|
33
|
-
* @param typed Domains as typed.
|
|
34
|
-
* @param print Prints one line.
|
|
35
|
-
* @returns The valid domains.
|
|
36
|
-
* @example
|
|
37
|
-
* await validDomains(ask, ["api.example.com"], console.log); // ["api.example.com"]
|
|
38
|
-
*/
|
|
39
|
-
async function validDomains(ask, typed, print) {
|
|
40
|
-
const domains = [];
|
|
41
|
-
for (const entry of typed) {
|
|
42
|
-
if (isHostName(entry)) {
|
|
43
|
-
domains.push(entry);
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
const retry = (await ask(`${JSON.stringify(entry)} is not a host name (like ${HOST_EXAMPLE}); type it again, or ${EMPTY_LIST_ANSWER} to drop it`, "")).trim();
|
|
47
|
-
if (isHostName(retry)) {
|
|
48
|
-
domains.push(retry);
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
const dropped = retry === "" || retry === EMPTY_LIST_ANSWER ? entry : retry;
|
|
52
|
-
print(`${OUTPUT_PREFIX}dropped ${JSON.stringify(dropped)}: not a host name like ${HOST_EXAMPLE}`);
|
|
53
|
-
}
|
|
54
|
-
return domains;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Asks every question.
|
|
58
|
-
*
|
|
59
|
-
* @param ask Function that asks one question.
|
|
60
|
-
* @param suggestions Suggested items.
|
|
61
|
-
* @param print Prints one line (notes about dropped domains).
|
|
62
|
-
* @returns The answers.
|
|
63
|
-
* @example
|
|
64
|
-
* await askAll(ask, { secrets: ["API_TOKEN"] }, console.log); // { secrets: ["API_TOKEN"], domains: [] }
|
|
65
|
-
*/
|
|
66
|
-
export async function askAll(ask, suggestions, print) {
|
|
67
|
-
const secrets = parseList(await ask("Secret environment variables needed in the cloud (comma-separated, - for none)", suggestions.secrets.join(", ")), suggestions.secrets);
|
|
68
|
-
const typed = parseList(await ask("Extra domains the setup or tests need (comma-separated, - for none)", ""), []);
|
|
69
|
-
return { secrets, domains: await validDomains(ask, typed, print) };
|
|
70
|
-
}
|
|
5
|
+
export {};
|
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
|
@@ -6,7 +6,7 @@ import { appendFileSync, existsSync } from "node:fs";
|
|
|
6
6
|
import { basename, join } from "node:path";
|
|
7
7
|
import { findProxyCa, shellQuote } from "./ca.js";
|
|
8
8
|
import { readConfig } from "./config.js";
|
|
9
|
-
import { CONFIG_FILE, GIT_FETCH_TIMEOUT_MS, OUTPUT_PREFIX,
|
|
9
|
+
import { CONFIG_FILE, GIT_FETCH_TIMEOUT_MS, OUTPUT_PREFIX, packageSpec, SETUP_SLOW_MS } from "./constants.js";
|
|
10
10
|
import { configImages, defaultBranch, readToolVersions } from "./detect.js";
|
|
11
11
|
import { realDocker } from "./docker.js";
|
|
12
12
|
import { pathLine, toolBinDirs } from "./installers.js";
|
|
@@ -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. */
|
|
@@ -91,24 +91,25 @@ export function expectedFingerprint(repo, config, state) {
|
|
|
91
91
|
* @param repo - Repo root.
|
|
92
92
|
* @param config - Repo config.
|
|
93
93
|
* @param state - Recorded state, if any.
|
|
94
|
+
* @param packageVersion - Version of the running package, for the setup command.
|
|
94
95
|
* @returns Warning messages, without prefix.
|
|
95
96
|
*/
|
|
96
|
-
function machineWarnings(repo, config, state) {
|
|
97
|
+
function machineWarnings(repo, config, state, packageVersion) {
|
|
97
98
|
if (!state) {
|
|
98
99
|
return [
|
|
99
|
-
`
|
|
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`,
|
|
100
101
|
];
|
|
101
102
|
}
|
|
102
103
|
const out = [];
|
|
103
104
|
const failed = state.steps.filter((s) => !s.ok).map((s) => s.name);
|
|
104
105
|
if (failed.length)
|
|
105
|
-
out.push(`setup
|
|
106
|
+
out.push(`falló el setup: ${failed.join(", ")}. Corre agent-cloud-kit doctor.`);
|
|
106
107
|
if (expectedFingerprint(repo, config, state) !== state.fingerprint) {
|
|
107
|
-
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.");
|
|
108
109
|
}
|
|
109
110
|
const total = state.steps.reduce((sum, s) => sum + s.ms, 0);
|
|
110
111
|
if (total > SETUP_SLOW_MS)
|
|
111
|
-
out.push(`setup
|
|
112
|
+
out.push(`el setup tardó ${minutesSeconds(total)}, cerca del límite de ~5 minutos.`);
|
|
112
113
|
return out;
|
|
113
114
|
}
|
|
114
115
|
/**
|
|
@@ -128,7 +129,7 @@ function writeEnvFile(env, lines) {
|
|
|
128
129
|
return undefined;
|
|
129
130
|
}
|
|
130
131
|
catch (err) {
|
|
131
|
-
return `
|
|
132
|
+
return `no se pudo escribir ${ENV_FILE_VAR} (${file}): ${err instanceof Error ? err.message : String(err)}`;
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
/**
|
|
@@ -169,7 +170,7 @@ async function runSessionSteps(repo, commands, run, env) {
|
|
|
169
170
|
await run("bash", ["-c", command], { cwd: repo, env, stdio: "stderr", timeoutMs: SESSION_STEP_TIMEOUT_MS });
|
|
170
171
|
}
|
|
171
172
|
catch (err) {
|
|
172
|
-
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}`);
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
176
|
return problems;
|
|
@@ -209,16 +210,16 @@ async function sessionBody(repo, ctx, out) {
|
|
|
209
210
|
if (envFileProblem)
|
|
210
211
|
out.push(envFileProblem);
|
|
211
212
|
if (config)
|
|
212
|
-
out.push(...machineWarnings(repo, config, state));
|
|
213
|
+
out.push(...machineWarnings(repo, config, state, ctx.packageVersion));
|
|
213
214
|
const missing = (config?.requiredEnv ?? []).filter((name) => !ctx.env[name]);
|
|
214
215
|
if (missing.length)
|
|
215
|
-
out.push(`
|
|
216
|
+
out.push(`faltan variables de entorno: ${missing.join(", ")}`);
|
|
216
217
|
const notes = resolveNotes(repo, config?.notes)?.trim();
|
|
217
218
|
if (notes)
|
|
218
219
|
out.push(notes);
|
|
219
220
|
const dockerProblem = startDocker(ctx.docker ?? realDocker);
|
|
220
221
|
if (dockerProblem)
|
|
221
|
-
out.push(`
|
|
222
|
+
out.push(`no se pudo iniciar dockerd: ${dockerProblem}`);
|
|
222
223
|
const env = toolsEnv(dirs, ctx.env, ca);
|
|
223
224
|
await syncGit(repo, ctx.run, env);
|
|
224
225
|
out.push(...(await runSessionSteps(repo, config?.session ?? [], ctx.run, env)));
|
|
@@ -239,7 +240,7 @@ export async function session(repo, ctx) {
|
|
|
239
240
|
await sessionBody(repo, ctx, out);
|
|
240
241
|
}
|
|
241
242
|
catch (err) {
|
|
242
|
-
out.push(`session
|
|
243
|
+
out.push(`error en session: ${err instanceof Error ? err.message : String(err)}`);
|
|
243
244
|
}
|
|
244
245
|
return out.map((line) => OUTPUT_PREFIX + line);
|
|
245
246
|
}
|
package/dist/setup.js
CHANGED
|
@@ -66,14 +66,14 @@ async function aptLane(config, deps) {
|
|
|
66
66
|
for (const url of config.aptRepos) {
|
|
67
67
|
const name = `apt repo ${url}`;
|
|
68
68
|
if (!url.startsWith(HTTPS_PREFIX)) {
|
|
69
|
-
results.push(await refused(name, `
|
|
69
|
+
results.push(await refused(name, `el repo de apt debe ser una URL ${HTTPS_PREFIX}`));
|
|
70
70
|
continue;
|
|
71
71
|
}
|
|
72
72
|
results.push(await step(name, () => deps.run("bash", ["-c", APT_REPO_SCRIPT, SCRIPT_NAME, url], { env, timeoutMs: APT_STEP_TIMEOUT_MS })));
|
|
73
73
|
}
|
|
74
74
|
const valid = config.apt.filter((pkg) => DEBIAN_PACKAGE_NAME.test(pkg));
|
|
75
75
|
for (const pkg of config.apt.filter((p) => !valid.includes(p))) {
|
|
76
|
-
results.push(await refused(`apt package ${pkg}`, "
|
|
76
|
+
results.push(await refused(`apt package ${pkg}`, "nombre de paquete Debian inválido"));
|
|
77
77
|
}
|
|
78
78
|
if (!valid.length && !config.aptRepos.length)
|
|
79
79
|
return results;
|
|
@@ -119,11 +119,11 @@ async function dockerLane(config, images, docker, deps) {
|
|
|
119
119
|
const results = [];
|
|
120
120
|
const valid = images.filter((image) => !WHITESPACE.test(image) && !image.startsWith("-") && image !== "");
|
|
121
121
|
for (const image of images.filter((i) => !valid.includes(i))) {
|
|
122
|
-
results.push(await refused(`docker pull ${image}`, "
|
|
122
|
+
results.push(await refused(`docker pull ${image}`, "referencia de imagen inválida"));
|
|
123
123
|
}
|
|
124
124
|
const up = await docker.ensure().catch(() => false);
|
|
125
125
|
if (!up)
|
|
126
|
-
return [...results, await refused("docker", `dockerd
|
|
126
|
+
return [...results, await refused("docker", `dockerd no arrancó; revisa ${DOCKERD_LOG}`)];
|
|
127
127
|
return [...results, ...(await pullAll(valid, deps))];
|
|
128
128
|
}
|
|
129
129
|
/**
|
|
@@ -181,7 +181,7 @@ function manifestVersion(root) {
|
|
|
181
181
|
function copyPackage(source, target, version) {
|
|
182
182
|
const found = manifestVersion(source);
|
|
183
183
|
if (found !== version)
|
|
184
|
-
throw new Error(`
|
|
184
|
+
throw new Error(`el paquete en ${source} es ${found ?? "desconocido"}, no ${version}`);
|
|
185
185
|
const temp = `${target}.${process.pid}.tmp`;
|
|
186
186
|
rmSync(temp, { recursive: true, force: true });
|
|
187
187
|
mkdirSync(temp, { recursive: true });
|
|
@@ -267,7 +267,7 @@ export async function setupRepo(repo, ctx) {
|
|
|
267
267
|
});
|
|
268
268
|
const { config, errors } = readConfig(repo);
|
|
269
269
|
if (!config)
|
|
270
|
-
return failed("config", errors.join("; ") || "
|
|
270
|
+
return failed("config", errors.join("; ") || "sin configuración");
|
|
271
271
|
try {
|
|
272
272
|
return await runPhases(repo, config, ctx);
|
|
273
273
|
}
|
package/dist/skill.js
CHANGED
|
@@ -78,15 +78,15 @@ function withVersion(text, version) {
|
|
|
78
78
|
* @throws When the package has no skill or the copy fails (the installed skill is kept).
|
|
79
79
|
* @example
|
|
80
80
|
* installSkill("/home/user/app", "/usr/lib/node_modules/agent-cloud-kit", "1.0.0");
|
|
81
|
-
* // "
|
|
81
|
+
* // "se instaló .agents/skills/cloud-ready 1.0.0"
|
|
82
82
|
*/
|
|
83
83
|
export function installSkill(repo, packageRoot, packageVersion) {
|
|
84
84
|
const current = skillVersion(repo);
|
|
85
85
|
if (current === packageVersion)
|
|
86
|
-
return `${SKILL_DIR}
|
|
86
|
+
return `${SKILL_DIR} ya está en ${packageVersion}`;
|
|
87
87
|
const source = join(packageRoot, PACKAGE_SKILL_DIR);
|
|
88
88
|
if (!existsSync(join(source, "SKILL.md")))
|
|
89
|
-
throw new Error(
|
|
89
|
+
throw new Error(`no se encontró ${PACKAGE_SKILL_DIR}/SKILL.md en ${packageRoot}`);
|
|
90
90
|
const target = join(repo, SKILL_DIR);
|
|
91
91
|
const temp = `${target}.${process.pid}.tmp`;
|
|
92
92
|
const old = `${target}.${process.pid}.old`;
|
|
@@ -107,7 +107,7 @@ export function installSkill(repo, packageRoot, packageVersion) {
|
|
|
107
107
|
throw err;
|
|
108
108
|
}
|
|
109
109
|
rmSync(old, { recursive: true, force: true });
|
|
110
|
-
return current === undefined ? `
|
|
110
|
+
return current === undefined ? `se instaló ${SKILL_DIR} ${packageVersion}` : `se actualizó ${SKILL_DIR} de ${current} a ${packageVersion}`;
|
|
111
111
|
}
|
|
112
112
|
/**
|
|
113
113
|
* Tells whether a path exists, counting a broken symlink as existing.
|
|
@@ -132,7 +132,7 @@ function present(path) {
|
|
|
132
132
|
* @param repo - Repo root.
|
|
133
133
|
* @returns A line describing what was done, or undefined when nothing was needed.
|
|
134
134
|
* @example
|
|
135
|
-
* linkSkill("/home/user/app"); // "
|
|
135
|
+
* linkSkill("/home/user/app"); // "se enlazó .claude/skills/cloud-ready a .agents/skills/cloud-ready"
|
|
136
136
|
*/
|
|
137
137
|
export function linkSkill(repo) {
|
|
138
138
|
const claudeSkills = join(repo, CLAUDE_SKILLS_DIR);
|
|
@@ -144,5 +144,5 @@ export function linkSkill(repo) {
|
|
|
144
144
|
return undefined;
|
|
145
145
|
mkdirSync(claudeSkills, { recursive: true });
|
|
146
146
|
symlinkSync(SKILL_LINK_TARGET, link);
|
|
147
|
-
return `
|
|
147
|
+
return `se enlazó ${SKILL_LINK} a ${SKILL_DIR}`;
|
|
148
148
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cloud-ready
|
|
3
3
|
description: Prepara o verifica un repo Node/TypeScript para las sesiones en la nube de Claude Code (claude.ai/code). Usa `configure` en local, con el usuario, para escribir `.claude/cloud.json` y decidir qué corre en la nube; usa `verify` dentro de una sesión en la nube para comprobar la preparación, arreglarla y reportar en el PR.
|
|
4
|
-
argument-hint: "configure
|
|
4
|
+
argument-hint: "configure | verify"
|
|
5
5
|
metadata:
|
|
6
6
|
version: "1.0.0"
|
|
7
7
|
---
|
|
@@ -33,8 +33,9 @@ Son los únicos que `verify` puede cambiar:
|
|
|
33
33
|
## `configure` (local, con el usuario)
|
|
34
34
|
|
|
35
35
|
Si `CLAUDE_CODE_REMOTE=true`, detente: este modo necesita al usuario en una terminal local.
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
No le pidas al usuario una lista de variables secretas ni de dominios: anota en `requiredEnv` y
|
|
37
|
+
`domains` solo lo que veas que el repo necesita en la nube; lo que falte lo detecta `verify` (ver
|
|
38
|
+
"Dominios y variables nuevos").
|
|
38
39
|
|
|
39
40
|
1. **Lee qué usa el repo:** `package.json` (scripts, `packageManager`, `engines`), el lockfile,
|
|
40
41
|
`.tool-versions`, `mise.toml`, `.nvmrc`, archivos de Docker compose, `Dockerfile`,
|
|
@@ -81,11 +82,13 @@ partida: no las vuelvas a preguntar.
|
|
|
81
82
|
8. **Si existe `.agents/skills/test-audit/project.md`:** escribe o actualiza su sección "Nube"
|
|
82
83
|
con el mismo resumen; no toques el resto del archivo.
|
|
83
84
|
9. **Corre `agent-cloud-kit manual`** y muéstrale al usuario la sección "Configurar el entorno en
|
|
84
|
-
claude.ai" de `docs/agent-cloud.md` (setup script, dominios, variables).
|
|
85
|
+
claude.ai" de `docs/agent-cloud.md` (setup script, dominios, variables). Dile que ese
|
|
86
|
+
documento se irá actualizando: si la skill, en local o en la nube, detecta que hace falta un
|
|
87
|
+
dominio o una variable nueva, lo agrega ahí y se lo dice.
|
|
85
88
|
10. **No hagas commit:** `agent-cloud-kit prepare` lo hace al salir. Si te invocaron fuera de
|
|
86
89
|
`prepare`, pregunta al usuario.
|
|
87
90
|
11. **Termina diciéndole al usuario que salga:** dile al usuario, con estas palabras o parecidas,
|
|
88
|
-
"configure terminó: sal de claude con `/exit` y `prepare` sigue
|
|
91
|
+
"configure terminó: sal de claude con `/exit` y `prepare` sigue". Si te
|
|
89
92
|
invocaron fuera de `prepare`, omite este paso.
|
|
90
93
|
|
|
91
94
|
## `verify` (en la nube)
|
|
@@ -95,8 +98,8 @@ Solo con `CLAUDE_CODE_REMOTE=true`; si no, detente y dilo. El prompt trae la ram
|
|
|
95
98
|
`claude/…`: no la cambies; al final empujas a la rama de `prepare`.
|
|
96
99
|
|
|
97
100
|
1. **Contexto:** lee `.claude/cloud.json`, el archivo de `notes` y `docs/agent-cloud.md`. Anota
|
|
98
|
-
las líneas `agent-cloud-kit:` de falla que mostró el inicio de la sesión ("
|
|
99
|
-
|
|
101
|
+
las líneas `agent-cloud-kit:` de falla que mostró el inicio de la sesión ("el setup del entorno no corrió",
|
|
102
|
+
"falló el setup: …", "la máquina se preparó con otra configuración", "faltan variables de entorno").
|
|
100
103
|
2. **Corre `agent-cloud-kit doctor`** y guarda la salida. Si `agent-cloud-kit` no está en el
|
|
101
104
|
`PATH`, el setup no corrió: usa `npx -y agent-cloud-kit doctor` y repórtalo.
|
|
102
105
|
3. **Comprueba la preparación:** dependencias instaladas (p. ej. `node_modules` existe), las
|
|
@@ -112,9 +115,11 @@ Solo con `CLAUDE_CODE_REMOTE=true`; si no, detente y dilo. El prompt trae la ram
|
|
|
112
115
|
- **Se arregla en la preparación:** cambia solo los archivos de la preparación y vuelve a
|
|
113
116
|
comprobar. Un cambio de `setup` no se re-ejecuta en esta sesión (el setup ya corrió): corre
|
|
114
117
|
el comando a mano para probarlo y anota que la máquina guardada se rehará en una sesión nueva.
|
|
115
|
-
- **
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
- **Falta un dominio o una variable:** sigue "Dominios y variables nuevos" y deja de intentar
|
|
119
|
+
arreglos que dependan de eso.
|
|
120
|
+
- **Solo se cambia en claude.ai** (entorno por defecto con `/remote-env`, rehacer la máquina
|
|
121
|
+
editando el setup script, el Claude GitHub App): no lo intentes; deja la instrucción exacta
|
|
122
|
+
para el reporte y deja de intentar arreglos.
|
|
118
123
|
- **Requiere cambiar código, tests o scripts:** no lo cambies; deja el cambio propuesto
|
|
119
124
|
(archivo y diff) para el comentario del PR.
|
|
120
125
|
|
|
@@ -157,6 +162,25 @@ Solo con `CLAUDE_CODE_REMOTE=true`; si no, detente y dilo. El prompt trae la ram
|
|
|
157
162
|
**Hallazgos:** los nuevos, o "ninguno".
|
|
158
163
|
```
|
|
159
164
|
|
|
165
|
+
## Dominios y variables nuevos
|
|
166
|
+
|
|
167
|
+
En ambos modos, cuando algo falla porque falta un dominio en la red del entorno (el proxy
|
|
168
|
+
rechaza la conexión, `403` o `CONNECT` fallido hacia un host) o una variable de entorno (el
|
|
169
|
+
inicio de la sesión la marca como faltante, o el error nombra una variable vacía):
|
|
170
|
+
|
|
171
|
+
1. Agrega el host a `domains` o el nombre a `requiredEnv` en `.claude/cloud.json` (solo el
|
|
172
|
+
nombre; nunca un valor).
|
|
173
|
+
2. Corre `agent-cloud-kit manual`: `docs/agent-cloud.md` queda con lo nuevo.
|
|
174
|
+
3. **Díselo al usuario en la sesión, en ese momento**, sin esperar al final: qué falta, por qué
|
|
175
|
+
(el comando y el error) y qué hacer, con estas palabras o parecidas: "Falta `<X>` en el entorno
|
|
176
|
+
de claude.ai. Agrégalo en <Network access / Variables de entorno> (está en
|
|
177
|
+
`docs/agent-cloud.md`) y vuelve a verificar: en tu terminal, en la rama `<rama>` actualizada
|
|
178
|
+
(`git pull`), corre `npx agent-cloud-kit verify`".
|
|
179
|
+
4. En `verify`, ponlo también en "Cambiar en claude.ai" del comentario del PR.
|
|
180
|
+
|
|
181
|
+
Un dominio que ya está en `domains`, o una variable que ya está en `requiredEnv`, no se agrega
|
|
182
|
+
de nuevo: si sigue faltando, el usuario aún no actualizó el entorno; recuérdaselo.
|
|
183
|
+
|
|
160
184
|
## Hallazgos
|
|
161
185
|
|
|
162
186
|
En ambos modos: todo hecho nuevo sobre la nube (algo que no está en `reference/knowledge.md`,
|