agent-cloud-kit 1.0.0-rc.2 → 1.0.0-rc.4
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 +4 -0
- package/dist/cli.js +15 -4
- package/dist/prepare/banner.js +141 -0
- package/dist/prepare/index.js +103 -66
- package/dist/prepare/ui.js +128 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,10 @@ platform does not tell you:
|
|
|
43
43
|
commits, pushes and opens a draft PR.
|
|
44
44
|
- **En la rama actual, sin commit:** does the same in your checkout and stops there; you review
|
|
45
45
|
with `git diff`, commit, push, and then run `npx agent-cloud-kit verify`.
|
|
46
|
+
|
|
47
|
+
Before Claude opens, a short screen says what will happen and waits for Enter. If the repo uses
|
|
48
|
+
[claude-repo-guard](https://github.com/ramon-sg/claude-repo-guard) and has its `claude:free`
|
|
49
|
+
script, Claude opens through it, so the skill can write the files the guard blocks.
|
|
46
50
|
3. **Configure the environment in claude.ai** following `docs/agent-cloud.md`, which `prepare`
|
|
47
51
|
generated from your `.claude/cloud.json`. The document keeps growing: when the skill, locally
|
|
48
52
|
or in the cloud, finds that a domain or an environment variable is missing, it adds it to
|
package/dist/cli.js
CHANGED
|
@@ -323,14 +323,24 @@ function onProcessInterrupt(handler) {
|
|
|
323
323
|
return () => process.off("SIGINT", listener);
|
|
324
324
|
}
|
|
325
325
|
/**
|
|
326
|
-
* Shows a text on the terminal and waits for Enter.
|
|
326
|
+
* Shows a text on the terminal and waits for Enter. Ctrl+C rejects with `InterruptedError`.
|
|
327
327
|
*
|
|
328
|
-
* @param text - Text to show.
|
|
328
|
+
* @param text - Text to show, printed as is.
|
|
329
|
+
* @throws InterruptedError on Ctrl+C.
|
|
329
330
|
* @example
|
|
330
|
-
* await waitTerminal("Presiona Enter");
|
|
331
|
+
* await waitTerminal("Presiona Enter ");
|
|
331
332
|
*/
|
|
332
333
|
async function waitTerminal(text) {
|
|
333
|
-
|
|
334
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
335
|
+
try {
|
|
336
|
+
await new Promise((resolveAnswer, reject) => {
|
|
337
|
+
rl.on("SIGINT", () => reject(new InterruptedError()));
|
|
338
|
+
rl.question(text).then(resolveAnswer, reject);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
finally {
|
|
342
|
+
rl.close();
|
|
343
|
+
}
|
|
334
344
|
}
|
|
335
345
|
/**
|
|
336
346
|
* Runs `prepare` in the working directory.
|
|
@@ -385,6 +395,7 @@ function prepareDeps(deps) {
|
|
|
385
395
|
packageRoot: deps.packageRoot ?? RUNNING_PACKAGE_ROOT,
|
|
386
396
|
today: deps.today ?? branchDay(new Date()),
|
|
387
397
|
isTty: deps.isTty ?? (process.stdin.isTTY === true && process.stdout.isTTY === true),
|
|
398
|
+
color: deps.isTty === undefined && process.stdout.isTTY === true && !deps.env.NO_COLOR,
|
|
388
399
|
onInterrupt: deps.onInterrupt ?? onProcessInterrupt,
|
|
389
400
|
};
|
|
390
401
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The screen `prepare` shows before opening Claude Code: a small cloud, what will happen and
|
|
3
|
+
* what the user has to do, in a few colored lines.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
/** Script of claude-repo-guard that opens Claude without sandbox nor permission prompts. */
|
|
8
|
+
export const FREE_SCRIPT = "claude:free";
|
|
9
|
+
/**
|
|
10
|
+
* Wraps a text in an ANSI code when color is on.
|
|
11
|
+
*
|
|
12
|
+
* @param on - Whether color is on.
|
|
13
|
+
* @param code - SGR code, like `36` for cyan.
|
|
14
|
+
* @param text - Text.
|
|
15
|
+
* @returns The text, colored or not.
|
|
16
|
+
* @example
|
|
17
|
+
* paint(false, "36", "a"); // "a"
|
|
18
|
+
*/
|
|
19
|
+
function paint(on, code, text) {
|
|
20
|
+
return on ? `\x1b[${code}m${text}\x1b[0m` : text;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Renders the screen shown before opening Claude Code (without the Enter line, which the caller
|
|
24
|
+
* asks).
|
|
25
|
+
*
|
|
26
|
+
* @param input - Place, mode and color.
|
|
27
|
+
* @returns The lines.
|
|
28
|
+
* @example
|
|
29
|
+
* renderBanner({ place: "pr", free: false, color: false })[2]; // " .-( ). Ahora se abre Claude Code"
|
|
30
|
+
*/
|
|
31
|
+
export function renderBanner(input) {
|
|
32
|
+
const c = (code, text) => paint(input.color, code, text);
|
|
33
|
+
const cloud = (text) => c("36", text);
|
|
34
|
+
const step = (n, text) => ` ${c("1;35", String(n))} ${text}`;
|
|
35
|
+
const after = input.place === "pr" ? "prepare hace commit, push y PR" : "quedan los cambios sin commit";
|
|
36
|
+
const lines = [
|
|
37
|
+
"",
|
|
38
|
+
cloud(" .--."),
|
|
39
|
+
`${cloud(" .-( ).")} ${c("1", "Ahora se abre Claude Code")}`,
|
|
40
|
+
`${cloud(" (___.__)__)")} ${c("2", "con la skill cloud-ready")}`,
|
|
41
|
+
"",
|
|
42
|
+
step(1, "Claude revisa el repo y te propone cómo prepararlo para la nube"),
|
|
43
|
+
step(2, "Responde sus preguntas"),
|
|
44
|
+
step(3, `Cuando diga ${c("1", "configure terminó")}, sal con ${c("1;33", "/exit")} ${c("2", `→ ${after}`)}`),
|
|
45
|
+
];
|
|
46
|
+
if (input.free)
|
|
47
|
+
lines.push("", c("2", ` Se abre con ${FREE_SCRIPT} (sin sandbox ni preguntas de permisos): el repo usa claude-repo-guard`));
|
|
48
|
+
lines.push("");
|
|
49
|
+
return lines;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The line asked after the banner.
|
|
53
|
+
*
|
|
54
|
+
* @param color - Whether to use ANSI colors.
|
|
55
|
+
* @returns The text.
|
|
56
|
+
* @example
|
|
57
|
+
* enterLine(false); // " ⏎ Enter abre Claude Code · Ctrl+C cancela "
|
|
58
|
+
*/
|
|
59
|
+
export function enterLine(color) {
|
|
60
|
+
return ` ${paint(color, "1;32", "⏎ Enter")} abre Claude Code ${paint(color, "2", "· Ctrl+C cancela")} `;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The line asked while the user configures the environment in claude.ai.
|
|
64
|
+
*
|
|
65
|
+
* @param color - Whether to use ANSI colors.
|
|
66
|
+
* @returns The text.
|
|
67
|
+
* @example
|
|
68
|
+
* continueLine(false); // "\n ⏎ Enter cuando hayas configurado el entorno en claude.ai · abre la sesión cloud que lo verifica "
|
|
69
|
+
*/
|
|
70
|
+
export function continueLine(color) {
|
|
71
|
+
return `\n ${paint(color, "1;32", "⏎ Enter")} cuando hayas configurado el entorno en claude.ai ${paint(color, "2", "· abre la sesión cloud que lo verifica")} `;
|
|
72
|
+
}
|
|
73
|
+
/** Config file claude-repo-guard installs; the free mode is used only when it is there. */
|
|
74
|
+
const GUARD_CONFIG = ".claude/repo-guard.config.json";
|
|
75
|
+
/**
|
|
76
|
+
* Tells whether the repo uses claude-repo-guard and has its `claude:free` script. Both are
|
|
77
|
+
* required: the free mode drops the sandbox and the permission prompts, so a script with that
|
|
78
|
+
* name alone is not enough.
|
|
79
|
+
*
|
|
80
|
+
* @param repo - Repo root.
|
|
81
|
+
* @returns True when the guard config exists and `package.json` has the script.
|
|
82
|
+
* @example
|
|
83
|
+
* hasFreeScript("/home/u/app"); // true
|
|
84
|
+
*/
|
|
85
|
+
export function hasFreeScript(repo) {
|
|
86
|
+
const file = join(repo, "package.json");
|
|
87
|
+
if (!existsSync(file) || !existsSync(join(repo, GUARD_CONFIG)))
|
|
88
|
+
return false;
|
|
89
|
+
try {
|
|
90
|
+
const scripts = JSON.parse(readFileSync(file, "utf8")).scripts;
|
|
91
|
+
return typeof scripts?.[FREE_SCRIPT] === "string";
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The command that opens Claude with a prompt: the repo's `claude:free` script through its package
|
|
99
|
+
* manager, or `claude` itself.
|
|
100
|
+
*
|
|
101
|
+
* @param repo - Repo root.
|
|
102
|
+
* @param prompt - First prompt.
|
|
103
|
+
* @returns Command and arguments.
|
|
104
|
+
* @example
|
|
105
|
+
* claudeCommand("/home/u/app", "/cloud-ready configure"); // { cmd: "npm", args: ["run", "claude:free", "--", "/cloud-ready configure"] }
|
|
106
|
+
*/
|
|
107
|
+
export function claudeCommand(repo, prompt) {
|
|
108
|
+
if (!hasFreeScript(repo))
|
|
109
|
+
return { cmd: "claude", args: [prompt] };
|
|
110
|
+
const manager = packageManager(repo);
|
|
111
|
+
if (manager === "npm")
|
|
112
|
+
return { cmd: "npm", args: ["run", FREE_SCRIPT, "--", prompt] };
|
|
113
|
+
return { cmd: manager, args: ["run", FREE_SCRIPT, prompt] };
|
|
114
|
+
}
|
|
115
|
+
/** Lockfiles that tell the package manager, checked in order. */
|
|
116
|
+
const LOCKFILES = [
|
|
117
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
118
|
+
["bun.lock", "bun"],
|
|
119
|
+
["bun.lockb", "bun"],
|
|
120
|
+
["yarn.lock", "yarn"],
|
|
121
|
+
];
|
|
122
|
+
/**
|
|
123
|
+
* The repo's package manager: `packageManager` in `package.json`, else the lockfile, else npm.
|
|
124
|
+
*
|
|
125
|
+
* @param repo - Repo root.
|
|
126
|
+
* @returns `npm`, `pnpm`, `yarn` or `bun`.
|
|
127
|
+
* @example
|
|
128
|
+
* packageManager("/home/u/app"); // "pnpm"
|
|
129
|
+
*/
|
|
130
|
+
function packageManager(repo) {
|
|
131
|
+
try {
|
|
132
|
+
const field = JSON.parse(readFileSync(join(repo, "package.json"), "utf8")).packageManager;
|
|
133
|
+
const name = typeof field === "string" ? field.split("@")[0] : undefined;
|
|
134
|
+
if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun")
|
|
135
|
+
return name;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// NOTE: no readable package.json; fall back to the lockfile.
|
|
139
|
+
}
|
|
140
|
+
return LOCKFILES.find(([file]) => existsSync(join(repo, file)))?.[1] ?? "npm";
|
|
141
|
+
}
|
package/dist/prepare/index.js
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
import { existsSync, lstatSync, statSync } from "node:fs";
|
|
7
7
|
import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
8
8
|
import { readConfig } from "../config.js";
|
|
9
|
-
import { CONFIG_FILE,
|
|
9
|
+
import { CONFIG_FILE, PACKAGE } from "../constants.js";
|
|
10
10
|
import { defaultBranch } from "../detect.js";
|
|
11
11
|
import { init } from "../init.js";
|
|
12
12
|
import { MANUAL_FILE, renderEnvironmentSection, writeManual } from "../manual.js";
|
|
13
|
+
import { claudeCommand, continueLine, enterLine, hasFreeScript, renderBanner } from "./banner.js";
|
|
13
14
|
import { preflight } from "./detect.js";
|
|
15
|
+
import { formatLine, initKind, nextSteps, styleMarkdown, unprefixed } from "./ui.js";
|
|
14
16
|
import { configurePrompt, verifyPrompt } from "./prompts.js";
|
|
15
17
|
/** Prefix of the branch `prepare` creates; the day follows it. */
|
|
16
18
|
const BRANCH_PREFIX = "chore/cloud-ready-";
|
|
@@ -35,17 +37,12 @@ const INCLUDE_QUESTION = "¿Incluir estos cambios en el commit? [s/N]";
|
|
|
35
37
|
/** Answers to `INCLUDE_QUESTION` that mean yes. */
|
|
36
38
|
const YES_ANSWER = /^(s|si|sí|y|yes)$/i;
|
|
37
39
|
/** Text shown while the user configures the environment in claude.ai. */
|
|
38
|
-
const WAIT_TEXT = "Configura el entorno en claude.ai y luego presiona Enter";
|
|
39
40
|
/** Matches a GitHub remote URL (https or ssh) and captures `owner/repo`. */
|
|
40
41
|
const GITHUB_REMOTE = /github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/;
|
|
41
42
|
/** Matches a pull request URL and captures its number. */
|
|
42
43
|
const PR_URL = /\/pull\/(\d+)/;
|
|
43
44
|
/** Command that continues a prepare run; `npx` because a first-time user does not have the binary on PATH. */
|
|
44
45
|
const RESUME_COMMAND = `npx ${PACKAGE} prepare --resume`;
|
|
45
|
-
/** Printed just before the local agent starts: `prepare` continues only when `claude` exits. */
|
|
46
|
-
const EXIT_CLAUDE_HINT = `${OUTPUT_PREFIX}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
46
|
/** Question of the place menu. */
|
|
50
47
|
const PLACE_QUESTION = "¿Dónde preparo el repo?";
|
|
51
48
|
/** Command that launches the cloud verification of the current branch. */
|
|
@@ -78,6 +75,18 @@ export function branchDay(date) {
|
|
|
78
75
|
const pad = (n) => String(n).padStart(2, "0");
|
|
79
76
|
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}`;
|
|
80
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Prints one line with the icon and color of its kind (or the plain prefix without color).
|
|
80
|
+
*
|
|
81
|
+
* @param deps - Dependencies (`print`, `color`).
|
|
82
|
+
* @param kind - Kind of line.
|
|
83
|
+
* @param text - Text, without prefix.
|
|
84
|
+
* @example
|
|
85
|
+
* say(deps, "ok", "PR en borrador: https://github.com/acme/app/pull/7");
|
|
86
|
+
*/
|
|
87
|
+
function say(deps, kind, text) {
|
|
88
|
+
deps.print(formatLine(kind, text, deps.color ?? false));
|
|
89
|
+
}
|
|
81
90
|
/**
|
|
82
91
|
* Says where the worktree is and how to continue there.
|
|
83
92
|
*
|
|
@@ -253,11 +262,11 @@ async function findResume(repo, deps) {
|
|
|
253
262
|
*/
|
|
254
263
|
async function createAndConfigure(repo, branch, worktree, deps, progress) {
|
|
255
264
|
if (existsSync(worktree)) {
|
|
256
|
-
deps
|
|
265
|
+
say(deps, "fail", `${worktree} ya existe; bórralo (git worktree remove ${worktree}) o continúa ahí con ${RESUME_COMMAND}`);
|
|
257
266
|
return false;
|
|
258
267
|
}
|
|
259
268
|
if ((await tryCapture(deps, "git", ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], repo)) !== undefined) {
|
|
260
|
-
deps
|
|
269
|
+
say(deps, "fail", `la rama ${branch} ya existe; bórrala (git branch -D ${branch}) o corre prepare otro día`);
|
|
261
270
|
return false;
|
|
262
271
|
}
|
|
263
272
|
const base = `origin/${defaultBranch(repo)}`;
|
|
@@ -265,38 +274,40 @@ async function createAndConfigure(repo, branch, worktree, deps, progress) {
|
|
|
265
274
|
await deps.run("git", ["fetch", "-q", "origin"], { cwd: repo });
|
|
266
275
|
await deps.run("git", ["worktree", "add", "-b", branch, worktree, base], { cwd: repo });
|
|
267
276
|
progress.worktree = worktree;
|
|
277
|
+
say(deps, "ok", `se creó el worktree ${worktree} en la rama ${branch}`);
|
|
268
278
|
}
|
|
269
279
|
catch (err) {
|
|
270
|
-
deps
|
|
280
|
+
say(deps, "fail", `no se pudo crear el worktree: ${message(err)}`);
|
|
271
281
|
return false;
|
|
272
282
|
}
|
|
273
283
|
const stopped = resumeLine("el agente local se detuvo", worktree);
|
|
274
284
|
try {
|
|
275
285
|
const result = init(worktree, deps.packageVersion, deps.packageRoot);
|
|
276
286
|
for (const line of result.lines)
|
|
277
|
-
deps
|
|
287
|
+
say(deps, initKind(line), unprefixed(line));
|
|
278
288
|
if (!result.ok) {
|
|
279
|
-
deps
|
|
289
|
+
say(deps, "warn", stopped);
|
|
280
290
|
return false;
|
|
281
291
|
}
|
|
282
292
|
}
|
|
283
293
|
catch (err) {
|
|
284
|
-
deps
|
|
285
|
-
deps
|
|
294
|
+
say(deps, "fail", `${message(err)}`);
|
|
295
|
+
say(deps, "warn", stopped);
|
|
286
296
|
return false;
|
|
287
297
|
}
|
|
288
|
-
deps.print(EXIT_CLAUDE_HINT);
|
|
289
298
|
try {
|
|
290
|
-
await
|
|
299
|
+
await runConfigure(deps, progress, "pr", worktree);
|
|
291
300
|
}
|
|
292
|
-
catch {
|
|
293
|
-
|
|
301
|
+
catch (err) {
|
|
302
|
+
if (err instanceof InterruptedError)
|
|
303
|
+
throw err;
|
|
304
|
+
say(deps, "warn", stopped);
|
|
294
305
|
return false;
|
|
295
306
|
}
|
|
296
307
|
const problem = configureProblem(worktree);
|
|
297
308
|
if (problem !== undefined) {
|
|
298
|
-
deps
|
|
299
|
-
deps
|
|
309
|
+
say(deps, "fail", `configure no terminó: ${problem}`);
|
|
310
|
+
say(deps, "warn", resumeLine("prepare se detuvo", worktree));
|
|
300
311
|
return false;
|
|
301
312
|
}
|
|
302
313
|
return true;
|
|
@@ -323,44 +334,45 @@ async function commitAndPush(worktree, branch, deps) {
|
|
|
323
334
|
const status = await deps.capture("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: worktree });
|
|
324
335
|
const others = statusPaths(status).filter((path) => !covered(path, known));
|
|
325
336
|
if (others.length) {
|
|
326
|
-
deps
|
|
337
|
+
say(deps, "warn", `el agente local también cambió:`);
|
|
327
338
|
for (const path of others)
|
|
328
|
-
deps.print(`
|
|
339
|
+
deps.print(` ${path}`);
|
|
329
340
|
if (YES_ANSWER.test((await deps.ask(INCLUDE_QUESTION, "")).trim())) {
|
|
330
341
|
await deps.run("git", ["add", "--", ...others], { cwd: worktree });
|
|
331
342
|
}
|
|
332
343
|
else {
|
|
333
344
|
await deps.run("git", ["reset", "-q", "--", ...others], { cwd: worktree });
|
|
334
|
-
deps
|
|
345
|
+
say(deps, "warn", `quedó sin commitear en ${worktree}`);
|
|
335
346
|
}
|
|
336
347
|
}
|
|
337
348
|
const staged = await deps.capture("git", ["diff", "--cached", "--name-only"], { cwd: worktree });
|
|
338
349
|
if (staged.trim())
|
|
339
350
|
await deps.run("git", ["commit", "-m", COMMIT_MESSAGE], { cwd: worktree });
|
|
340
351
|
else
|
|
341
|
-
deps
|
|
352
|
+
say(deps, "same", `no hay nada nuevo que commitear`);
|
|
342
353
|
await deps.run("git", ["push", "-u", "origin", branch], { cwd: worktree });
|
|
354
|
+
say(deps, "ok", `se subió la rama ${branch}`);
|
|
343
355
|
}
|
|
344
356
|
catch (err) {
|
|
345
357
|
if (err instanceof InterruptedError)
|
|
346
358
|
throw err;
|
|
347
|
-
deps
|
|
359
|
+
say(deps, "fail", `no se pudo hacer commit y push: ${message(err)}; corrígelo en ${worktree} y corre ${RESUME_COMMAND} ahí`);
|
|
348
360
|
return false;
|
|
349
361
|
}
|
|
350
362
|
const existing = await tryCapture(deps, "gh", ["pr", "view", branch, "--json", "number", "--jq", ".number"], worktree);
|
|
351
363
|
if (existing !== undefined && PR_NUMBER.test(existing)) {
|
|
352
|
-
deps
|
|
364
|
+
say(deps, "ok", `se reutiliza el PR abierto #${existing}`);
|
|
353
365
|
return { pr: existing };
|
|
354
366
|
}
|
|
355
367
|
try {
|
|
356
368
|
const url = await deps.capture("gh", ["pr", "create", "--draft", "--fill"], { cwd: worktree });
|
|
357
|
-
deps
|
|
369
|
+
say(deps, "ok", `PR en borrador: ${url.trim()}`);
|
|
358
370
|
return { pr: PR_URL.exec(url)?.[1] };
|
|
359
371
|
}
|
|
360
372
|
catch {
|
|
361
373
|
const origin = await tryCapture(deps, "git", ["remote", "get-url", "origin"], worktree);
|
|
362
374
|
const link = compareLink(origin, branch);
|
|
363
|
-
deps
|
|
375
|
+
say(deps, "warn", `no se pudo abrir el PR con gh; ábrelo a mano${link ? `: ${link}` : ` desde la rama ${branch}`}`);
|
|
364
376
|
return {};
|
|
365
377
|
}
|
|
366
378
|
}
|
|
@@ -385,14 +397,14 @@ export async function prepare(repo, deps, options = {}) {
|
|
|
385
397
|
const problems = preflight({ isTty: deps.isTty, gitRoot, hasOrigin, claudeHelp });
|
|
386
398
|
if (problems.length || gitRoot === undefined) {
|
|
387
399
|
for (const problem of problems)
|
|
388
|
-
deps
|
|
400
|
+
say(deps, "fail", `${problem}`);
|
|
389
401
|
return EXIT_FAIL;
|
|
390
402
|
}
|
|
391
403
|
const progress = {};
|
|
392
404
|
const stop = deps.onInterrupt?.(() => {
|
|
393
405
|
if (progress.childOwnsTerminal)
|
|
394
406
|
return false;
|
|
395
|
-
deps
|
|
407
|
+
say(deps, "warn", interruptedLine(progress));
|
|
396
408
|
return true;
|
|
397
409
|
});
|
|
398
410
|
try {
|
|
@@ -401,7 +413,7 @@ export async function prepare(repo, deps, options = {}) {
|
|
|
401
413
|
catch (err) {
|
|
402
414
|
if (!(err instanceof InterruptedError))
|
|
403
415
|
throw err;
|
|
404
|
-
deps
|
|
416
|
+
say(deps, "warn", interruptedLine(progress));
|
|
405
417
|
return EXIT_INTERRUPTED;
|
|
406
418
|
}
|
|
407
419
|
finally {
|
|
@@ -414,34 +426,54 @@ export async function prepare(repo, deps, options = {}) {
|
|
|
414
426
|
*
|
|
415
427
|
* @param deps - Dependencies.
|
|
416
428
|
* @param progress - Marks the child as owning the terminal.
|
|
417
|
-
* @param
|
|
429
|
+
* @param command - Command and arguments that open Claude.
|
|
418
430
|
* @param cwd - Worktree.
|
|
419
431
|
* @returns Resolves when `claude` exits 0.
|
|
420
432
|
* @throws When `claude` exits non-zero.
|
|
421
433
|
* @example
|
|
422
|
-
* await runInteractive(deps, progress, ["--cloud", prompt], "/home/u/app-cloud-ready");
|
|
434
|
+
* await runInteractive(deps, progress, { cmd: "claude", args: ["--cloud", prompt] }, "/home/u/app-cloud-ready");
|
|
423
435
|
*/
|
|
424
|
-
async function runInteractive(deps, progress,
|
|
436
|
+
async function runInteractive(deps, progress, command, cwd) {
|
|
425
437
|
progress.childOwnsTerminal = true;
|
|
426
438
|
try {
|
|
427
|
-
await deps.run(
|
|
439
|
+
await deps.run(command.cmd, command.args, { cwd });
|
|
428
440
|
}
|
|
429
441
|
finally {
|
|
430
442
|
progress.childOwnsTerminal = false;
|
|
431
443
|
}
|
|
432
444
|
}
|
|
445
|
+
/**
|
|
446
|
+
* Shows the screen before Claude Code opens, waits for Enter, and runs the local agent with
|
|
447
|
+
* `cloud-ready configure` (through the repo's `claude:free` script when it has one).
|
|
448
|
+
*
|
|
449
|
+
* @param deps - Dependencies.
|
|
450
|
+
* @param progress - Marks the child as owning the terminal.
|
|
451
|
+
* @param place - Where `prepare` works.
|
|
452
|
+
* @param cwd - Folder where Claude runs.
|
|
453
|
+
* @returns Resolves when Claude exits 0.
|
|
454
|
+
* @throws InterruptedError on Ctrl+C at the Enter line; an error when Claude exits non-zero.
|
|
455
|
+
* @example
|
|
456
|
+
* await runConfigure(deps, progress, "pr", "/home/u/app-cloud-ready");
|
|
457
|
+
*/
|
|
458
|
+
async function runConfigure(deps, progress, place, cwd) {
|
|
459
|
+
const color = deps.color ?? false;
|
|
460
|
+
for (const line of renderBanner({ place, free: hasFreeScript(cwd), color }))
|
|
461
|
+
deps.print(line);
|
|
462
|
+
await deps.waitEnter(enterLine(color));
|
|
463
|
+
await runInteractive(deps, progress, claudeCommand(cwd, configurePrompt()), cwd);
|
|
464
|
+
}
|
|
433
465
|
/**
|
|
434
466
|
* The line printed on Ctrl+C: where the worktree is, when it exists.
|
|
435
467
|
*
|
|
436
468
|
* @param progress - What exists so far.
|
|
437
469
|
* @returns The line.
|
|
438
470
|
* @example
|
|
439
|
-
* interruptedLine({}); // "
|
|
471
|
+
* interruptedLine({}); // "interrumpido; no se creó nada"
|
|
440
472
|
*/
|
|
441
473
|
function interruptedLine(progress) {
|
|
442
474
|
if (progress.here)
|
|
443
|
-
return
|
|
444
|
-
return progress.worktree === undefined ?
|
|
475
|
+
return "interrumpido; lo que alcanzó a cambiar quedó sin commit en tu rama";
|
|
476
|
+
return progress.worktree === undefined ? "interrumpido; no se creó nada" : resumeLine("interrumpido", progress.worktree);
|
|
445
477
|
}
|
|
446
478
|
/**
|
|
447
479
|
* Everything after the preflight: resume or create the worktree, commit, wait, launch.
|
|
@@ -461,21 +493,21 @@ async function prepareSteps(gitRoot, deps, options, progress) {
|
|
|
461
493
|
if (options.resume) {
|
|
462
494
|
const found = await findResume(gitRoot, deps);
|
|
463
495
|
if (!found) {
|
|
464
|
-
deps
|
|
496
|
+
say(deps, "fail", `no hay nada que reanudar: corre prepare --resume en el worktree *${WORKTREE_SUFFIX}, sobre una rama ${BRANCH_PREFIX}*`);
|
|
465
497
|
return EXIT_FAIL;
|
|
466
498
|
}
|
|
467
499
|
({ worktree, branch } = found);
|
|
468
500
|
progress.worktree = worktree;
|
|
469
501
|
const problem = configureProblem(worktree);
|
|
470
502
|
if (problem) {
|
|
471
|
-
deps
|
|
503
|
+
say(deps, "fail", `configure no terminó en ${worktree}: ${problem}; corre claude "/cloud-ready configure" ahí primero y luego ${RESUME_COMMAND}`);
|
|
472
504
|
return EXIT_FAIL;
|
|
473
505
|
}
|
|
474
506
|
}
|
|
475
507
|
else {
|
|
476
508
|
const place = options.place ?? (await askPlace(gitRoot, deps));
|
|
477
509
|
if (place === null) {
|
|
478
|
-
deps
|
|
510
|
+
say(deps, "same", `no se cambió nada`);
|
|
479
511
|
return EXIT_FAIL;
|
|
480
512
|
}
|
|
481
513
|
if (place === "here")
|
|
@@ -490,16 +522,17 @@ async function prepareSteps(gitRoot, deps, options, progress) {
|
|
|
490
522
|
return EXIT_FAIL;
|
|
491
523
|
const config = readConfig(worktree).config;
|
|
492
524
|
if (config)
|
|
493
|
-
|
|
494
|
-
|
|
525
|
+
for (const line of styleMarkdown(renderEnvironmentSection(config, deps.packageVersion), deps.color ?? false))
|
|
526
|
+
deps.print(line);
|
|
527
|
+
await deps.waitEnter(continueLine(deps.color ?? false));
|
|
495
528
|
try {
|
|
496
|
-
await runInteractive(deps, progress, ["--cloud", verifyPrompt({ branch, pr: pushed.pr })], worktree);
|
|
529
|
+
await runInteractive(deps, progress, { cmd: "claude", args: ["--cloud", verifyPrompt({ branch, pr: pushed.pr })] }, worktree);
|
|
497
530
|
}
|
|
498
531
|
catch (err) {
|
|
499
|
-
deps
|
|
532
|
+
say(deps, "fail", `falló claude --cloud: ${message(err)}; corre ${RESUME_COMMAND} en ${worktree} para reintentar`);
|
|
500
533
|
return EXIT_FAIL;
|
|
501
534
|
}
|
|
502
|
-
deps
|
|
535
|
+
say(deps, "ok", `la sesión cloud avisará con un push a ${branch} y un comentario en el PR`);
|
|
503
536
|
return EXIT_OK;
|
|
504
537
|
}
|
|
505
538
|
/**
|
|
@@ -530,45 +563,49 @@ async function askPlace(gitRoot, deps) {
|
|
|
530
563
|
*/
|
|
531
564
|
async function prepareHere(gitRoot, deps, progress) {
|
|
532
565
|
progress.here = true;
|
|
533
|
-
const stopped =
|
|
566
|
+
const stopped = `prepare se detuvo; lo que alcanzó a cambiar quedó sin commit en tu rama`;
|
|
534
567
|
try {
|
|
535
568
|
const result = init(gitRoot, deps.packageVersion, deps.packageRoot);
|
|
536
569
|
for (const line of result.lines)
|
|
537
|
-
deps
|
|
570
|
+
say(deps, initKind(line), unprefixed(line));
|
|
538
571
|
if (!result.ok) {
|
|
539
|
-
deps
|
|
572
|
+
say(deps, "warn", stopped);
|
|
540
573
|
return EXIT_FAIL;
|
|
541
574
|
}
|
|
542
575
|
}
|
|
543
576
|
catch (err) {
|
|
544
|
-
deps
|
|
545
|
-
deps
|
|
577
|
+
say(deps, "fail", `${message(err)}`);
|
|
578
|
+
say(deps, "warn", stopped);
|
|
546
579
|
return EXIT_FAIL;
|
|
547
580
|
}
|
|
548
|
-
deps.print(EXIT_CLAUDE_HINT_HERE);
|
|
549
581
|
try {
|
|
550
|
-
await
|
|
582
|
+
await runConfigure(deps, progress, "here", gitRoot);
|
|
551
583
|
}
|
|
552
|
-
catch {
|
|
553
|
-
|
|
584
|
+
catch (err) {
|
|
585
|
+
if (err instanceof InterruptedError)
|
|
586
|
+
throw err;
|
|
587
|
+
say(deps, "warn", stopped);
|
|
554
588
|
return EXIT_FAIL;
|
|
555
589
|
}
|
|
556
590
|
const problem = configureProblem(gitRoot);
|
|
557
591
|
if (problem !== undefined) {
|
|
558
|
-
deps
|
|
592
|
+
say(deps, "fail", `configure no terminó: ${problem}; corre claude "/cloud-ready configure" y luego prepare otra vez`);
|
|
559
593
|
return EXIT_FAIL;
|
|
560
594
|
}
|
|
561
595
|
try {
|
|
562
596
|
writeManual(gitRoot, deps.packageVersion);
|
|
563
597
|
}
|
|
564
598
|
catch (err) {
|
|
565
|
-
deps
|
|
599
|
+
say(deps, "fail", `no se pudo escribir ${MANUAL_FILE}: ${message(err)}`);
|
|
566
600
|
return EXIT_FAIL;
|
|
567
601
|
}
|
|
568
602
|
const config = readConfig(gitRoot).config;
|
|
569
603
|
if (config)
|
|
570
|
-
|
|
571
|
-
|
|
604
|
+
for (const line of styleMarkdown(renderEnvironmentSection(config, deps.packageVersion), deps.color ?? false))
|
|
605
|
+
deps.print(line);
|
|
606
|
+
const steps = ["Revisa los cambios con git diff", "Haz commit y push", `Configura el entorno en claude.ai (${MANUAL_FILE})`, `Corre ${VERIFY_COMMAND}`];
|
|
607
|
+
for (const line of nextSteps("Listo, sin commit", steps, deps.color ?? false))
|
|
608
|
+
deps.print(line);
|
|
572
609
|
return EXIT_OK;
|
|
573
610
|
}
|
|
574
611
|
/**
|
|
@@ -589,30 +626,30 @@ export async function verifyBranch(repo, deps) {
|
|
|
589
626
|
const problems = preflight({ isTty: deps.isTty, gitRoot, hasOrigin, claudeHelp }, "verify");
|
|
590
627
|
if (problems.length || gitRoot === undefined) {
|
|
591
628
|
for (const problem of problems)
|
|
592
|
-
deps
|
|
629
|
+
say(deps, "fail", `${problem}`);
|
|
593
630
|
return EXIT_FAIL;
|
|
594
631
|
}
|
|
595
632
|
const branch = await tryCapture(deps, "git", ["rev-parse", "--abbrev-ref", "HEAD"], gitRoot);
|
|
596
633
|
if (branch === undefined || branch === "HEAD") {
|
|
597
|
-
deps
|
|
634
|
+
say(deps, "fail", `no estás en una rama; cámbiate a la rama que quieres verificar`);
|
|
598
635
|
return EXIT_FAIL;
|
|
599
636
|
}
|
|
600
637
|
const problem = configureProblem(gitRoot);
|
|
601
638
|
if (problem !== undefined) {
|
|
602
|
-
deps
|
|
639
|
+
say(deps, "fail", `el repo no está preparado (${problem}); corre npx ${PACKAGE} prepare primero`);
|
|
603
640
|
return EXIT_FAIL;
|
|
604
641
|
}
|
|
605
642
|
const notes = readConfig(gitRoot).config?.notes ?? DEFAULT_NOTES_FILE;
|
|
606
643
|
const files = [CONFIG_FILE, MANUAL_FILE, notes].filter((path) => entryExists(join(gitRoot, path)));
|
|
607
644
|
const dirty = await tryCapture(deps, "git", ["status", "--porcelain", "--", ...files], gitRoot);
|
|
608
645
|
if (dirty) {
|
|
609
|
-
deps
|
|
646
|
+
say(deps, "fail", `hay cambios sin commit en la preparación; haz commit y push: la nube solo ve lo que está en origin`);
|
|
610
647
|
return EXIT_FAIL;
|
|
611
648
|
}
|
|
612
649
|
const head = await tryCapture(deps, "git", ["rev-parse", "HEAD"], gitRoot);
|
|
613
650
|
const remote = (await tryCapture(deps, "git", ["ls-remote", "origin", `refs/heads/${branch}`], gitRoot))?.split(/\s/)[0];
|
|
614
651
|
if (!head || remote !== head) {
|
|
615
|
-
deps
|
|
652
|
+
say(deps, "fail", `la rama ${branch} no está subida o tiene commits sin subir; haz git push y vuelve a correr verify`);
|
|
616
653
|
return EXIT_FAIL;
|
|
617
654
|
}
|
|
618
655
|
const found = await tryCapture(deps, "gh", ["pr", "view", branch, "--json", "number", "--jq", ".number"], gitRoot);
|
|
@@ -620,15 +657,15 @@ export async function verifyBranch(repo, deps) {
|
|
|
620
657
|
const progress = {};
|
|
621
658
|
const stop = deps.onInterrupt?.(() => !progress.childOwnsTerminal);
|
|
622
659
|
try {
|
|
623
|
-
await runInteractive(deps, progress, ["--cloud", verifyPrompt({ branch, pr })], gitRoot);
|
|
660
|
+
await runInteractive(deps, progress, { cmd: "claude", args: ["--cloud", verifyPrompt({ branch, pr })] }, gitRoot);
|
|
624
661
|
}
|
|
625
662
|
catch (err) {
|
|
626
|
-
deps
|
|
663
|
+
say(deps, "fail", `falló claude --cloud: ${message(err)}`);
|
|
627
664
|
return EXIT_FAIL;
|
|
628
665
|
}
|
|
629
666
|
finally {
|
|
630
667
|
stop?.();
|
|
631
668
|
}
|
|
632
|
-
deps
|
|
669
|
+
say(deps, "ok", `la sesión cloud avisará con un push a ${branch}${pr ? " y un comentario en el PR" : ""}`);
|
|
633
670
|
return EXIT_OK;
|
|
634
671
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file How `prepare` and `verify` show their lines: an icon and a color per kind in a terminal,
|
|
3
|
+
* or the plain `agent-cloud-kit: ` prefix otherwise (logs, pipes, tests).
|
|
4
|
+
*/
|
|
5
|
+
import { OUTPUT_PREFIX } from "../constants.js";
|
|
6
|
+
/** Icon and SGR color code of each kind. */
|
|
7
|
+
const STYLE = {
|
|
8
|
+
ok: { icon: "✔", code: "32" },
|
|
9
|
+
same: { icon: "●", code: "2" },
|
|
10
|
+
info: { icon: "›", code: "36" },
|
|
11
|
+
warn: { icon: "▲", code: "33" },
|
|
12
|
+
fail: { icon: "✖", code: "31" },
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Wraps a text in an SGR code when color is on.
|
|
16
|
+
*
|
|
17
|
+
* @param on - Whether color is on.
|
|
18
|
+
* @param code - SGR code.
|
|
19
|
+
* @param text - Text.
|
|
20
|
+
* @returns The text, colored or not.
|
|
21
|
+
* @example
|
|
22
|
+
* paint(true, "1", "a"); // "\x1b[1ma\x1b[0m"
|
|
23
|
+
*/
|
|
24
|
+
export function paint(on, code, text) {
|
|
25
|
+
return on ? `\x1b[${code}m${text}\x1b[0m` : text;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Formats one line.
|
|
29
|
+
*
|
|
30
|
+
* @param kind - Kind of line.
|
|
31
|
+
* @param text - Text, without prefix.
|
|
32
|
+
* @param color - Whether the output is a color terminal.
|
|
33
|
+
* @returns ` ✔ text` (icon colored; the text dimmed for `same`) or `agent-cloud-kit: text`.
|
|
34
|
+
* @example
|
|
35
|
+
* formatLine("ok", "se creó .claude/cloud.json", false); // "agent-cloud-kit: se creó .claude/cloud.json"
|
|
36
|
+
*/
|
|
37
|
+
export function formatLine(kind, text, color) {
|
|
38
|
+
if (!color)
|
|
39
|
+
return `${OUTPUT_PREFIX}${text}`;
|
|
40
|
+
const { icon, code } = STYLE[kind];
|
|
41
|
+
return ` ${paint(true, code, icon)} ${kind === "same" ? paint(true, "2", text) : text}`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Guesses the kind of a line `init` printed, from its wording.
|
|
45
|
+
*
|
|
46
|
+
* @param text - Line, with or without the prefix.
|
|
47
|
+
* @returns `ok` for something done, `same` for something already there, `warn` otherwise.
|
|
48
|
+
* @example
|
|
49
|
+
* initKind("agent-cloud-kit: se creó .claude/cloud.json"); // "ok"
|
|
50
|
+
*/
|
|
51
|
+
export function initKind(text) {
|
|
52
|
+
const plain = text.startsWith(OUTPUT_PREFIX) ? text.slice(OUTPUT_PREFIX.length) : text;
|
|
53
|
+
if (/^se (creó|agregó|instaló|enlazó|actualizó|escribió)(\s|$)/.test(plain))
|
|
54
|
+
return "ok";
|
|
55
|
+
if (/\bya (existe|tiene|está)\b/.test(plain))
|
|
56
|
+
return "same";
|
|
57
|
+
return "warn";
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Removes the `agent-cloud-kit: ` prefix of a line, if it has it.
|
|
61
|
+
*
|
|
62
|
+
* @param text - Line.
|
|
63
|
+
* @returns The line without prefix.
|
|
64
|
+
* @example
|
|
65
|
+
* unprefixed("agent-cloud-kit: hola"); // "hola"
|
|
66
|
+
*/
|
|
67
|
+
export function unprefixed(text) {
|
|
68
|
+
return text.startsWith(OUTPUT_PREFIX) ? text.slice(OUTPUT_PREFIX.length) : text;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Styles a Markdown section for the terminal: headings in bold (and cyan for `##`), code fences
|
|
72
|
+
* dropped and their lines indented, list items as bullets. Without color, the text is unchanged.
|
|
73
|
+
*
|
|
74
|
+
* @param markdown - Section text.
|
|
75
|
+
* @param color - Whether the output is a color terminal.
|
|
76
|
+
* @returns The lines.
|
|
77
|
+
* @example
|
|
78
|
+
* styleMarkdown("## A\n- b", true); // ["", " \x1b[1;36mA\x1b[0m", " • b"]
|
|
79
|
+
*/
|
|
80
|
+
export function styleMarkdown(markdown, color) {
|
|
81
|
+
if (!color)
|
|
82
|
+
return markdown.split("\n");
|
|
83
|
+
const out = [];
|
|
84
|
+
let inCode = false;
|
|
85
|
+
for (const line of markdown.split("\n")) {
|
|
86
|
+
if (line.startsWith("```")) {
|
|
87
|
+
inCode = !inCode;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (inCode)
|
|
91
|
+
out.push(` ${paint(true, "33", line)}`);
|
|
92
|
+
else if (line.startsWith("### "))
|
|
93
|
+
out.push(` ${paint(true, "1", line.slice(4))}`);
|
|
94
|
+
else if (line.startsWith("## "))
|
|
95
|
+
out.push("", ` ${paint(true, "1;36", line.slice(3))}`);
|
|
96
|
+
else if (line.startsWith("- "))
|
|
97
|
+
out.push(` ${paint(true, "36", "•")} ${inline(line.slice(2))}`);
|
|
98
|
+
else
|
|
99
|
+
out.push(line === "" ? "" : ` ${inline(line)}`);
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Styles inline Markdown: `**bold**` in bold and `` `code` `` in yellow, without the marks.
|
|
105
|
+
*
|
|
106
|
+
* @param text - One line.
|
|
107
|
+
* @returns The styled line.
|
|
108
|
+
* @example
|
|
109
|
+
* inline("Elige **Custom**"); // "Elige \x1b[1mCustom\x1b[0m"
|
|
110
|
+
*/
|
|
111
|
+
function inline(text) {
|
|
112
|
+
return text.replace(/\*\*([^*]+)\*\*/g, (_m, bold) => paint(true, "1", bold)).replace(/`([^`]+)`/g, (_m, code) => paint(true, "33", code));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Renders a "what now" block: a title and numbered steps.
|
|
116
|
+
*
|
|
117
|
+
* @param title - Title.
|
|
118
|
+
* @param steps - Steps, in order.
|
|
119
|
+
* @param color - Whether the output is a color terminal.
|
|
120
|
+
* @returns The lines (without color: one prefixed line with the steps joined).
|
|
121
|
+
* @example
|
|
122
|
+
* nextSteps("Listo", ["haz commit"], false); // ["agent-cloud-kit: Listo. 1) haz commit"]
|
|
123
|
+
*/
|
|
124
|
+
export function nextSteps(title, steps, color) {
|
|
125
|
+
if (!color)
|
|
126
|
+
return [`${OUTPUT_PREFIX}${title}. ${steps.map((s, i) => `${i + 1}) ${s}`).join("; ")}`];
|
|
127
|
+
return ["", ` ${paint(true, "1;32", "✔")} ${paint(true, "1", title)}`, "", ...steps.map((s, i) => ` ${paint(true, "1;35", String(i + 1))} ${s}`), ""];
|
|
128
|
+
}
|