agent-cloud-kit 1.0.0-rc.2 → 1.0.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
- await askTerminal(text, "");
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,130 @@
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
+ /** Config file claude-repo-guard installs; the free mode is used only when it is there. */
63
+ const GUARD_CONFIG = ".claude/repo-guard.config.json";
64
+ /**
65
+ * Tells whether the repo uses claude-repo-guard and has its `claude:free` script. Both are
66
+ * required: the free mode drops the sandbox and the permission prompts, so a script with that
67
+ * name alone is not enough.
68
+ *
69
+ * @param repo - Repo root.
70
+ * @returns True when the guard config exists and `package.json` has the script.
71
+ * @example
72
+ * hasFreeScript("/home/u/app"); // true
73
+ */
74
+ export function hasFreeScript(repo) {
75
+ const file = join(repo, "package.json");
76
+ if (!existsSync(file) || !existsSync(join(repo, GUARD_CONFIG)))
77
+ return false;
78
+ try {
79
+ const scripts = JSON.parse(readFileSync(file, "utf8")).scripts;
80
+ return typeof scripts?.[FREE_SCRIPT] === "string";
81
+ }
82
+ catch {
83
+ return false;
84
+ }
85
+ }
86
+ /**
87
+ * The command that opens Claude with a prompt: the repo's `claude:free` script through its package
88
+ * manager, or `claude` itself.
89
+ *
90
+ * @param repo - Repo root.
91
+ * @param prompt - First prompt.
92
+ * @returns Command and arguments.
93
+ * @example
94
+ * claudeCommand("/home/u/app", "/cloud-ready configure"); // { cmd: "npm", args: ["run", "claude:free", "--", "/cloud-ready configure"] }
95
+ */
96
+ export function claudeCommand(repo, prompt) {
97
+ if (!hasFreeScript(repo))
98
+ return { cmd: "claude", args: [prompt] };
99
+ const manager = packageManager(repo);
100
+ if (manager === "npm")
101
+ return { cmd: "npm", args: ["run", FREE_SCRIPT, "--", prompt] };
102
+ return { cmd: manager, args: ["run", FREE_SCRIPT, prompt] };
103
+ }
104
+ /** Lockfiles that tell the package manager, checked in order. */
105
+ const LOCKFILES = [
106
+ ["pnpm-lock.yaml", "pnpm"],
107
+ ["bun.lock", "bun"],
108
+ ["bun.lockb", "bun"],
109
+ ["yarn.lock", "yarn"],
110
+ ];
111
+ /**
112
+ * The repo's package manager: `packageManager` in `package.json`, else the lockfile, else npm.
113
+ *
114
+ * @param repo - Repo root.
115
+ * @returns `npm`, `pnpm`, `yarn` or `bun`.
116
+ * @example
117
+ * packageManager("/home/u/app"); // "pnpm"
118
+ */
119
+ function packageManager(repo) {
120
+ try {
121
+ const field = JSON.parse(readFileSync(join(repo, "package.json"), "utf8")).packageManager;
122
+ const name = typeof field === "string" ? field.split("@")[0] : undefined;
123
+ if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun")
124
+ return name;
125
+ }
126
+ catch {
127
+ // NOTE: no readable package.json; fall back to the lockfile.
128
+ }
129
+ return LOCKFILES.find(([file]) => existsSync(join(repo, file)))?.[1] ?? "npm";
130
+ }
@@ -10,6 +10,7 @@ import { CONFIG_FILE, OUTPUT_PREFIX, 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, enterLine, hasFreeScript, renderBanner } from "./banner.js";
13
14
  import { preflight } from "./detect.js";
14
15
  import { configurePrompt, verifyPrompt } from "./prompts.js";
15
16
  /** Prefix of the branch `prepare` creates; the day follows it. */
@@ -35,17 +36,13 @@ const INCLUDE_QUESTION = "¿Incluir estos cambios en el commit? [s/N]";
35
36
  /** Answers to `INCLUDE_QUESTION` that mean yes. */
36
37
  const YES_ANSWER = /^(s|si|sí|y|yes)$/i;
37
38
  /** Text shown while the user configures the environment in claude.ai. */
38
- const WAIT_TEXT = "Configura el entorno en claude.ai y luego presiona Enter";
39
+ const WAIT_TEXT = "Configura el entorno en claude.ai y luego presiona Enter ";
39
40
  /** Matches a GitHub remote URL (https or ssh) and captures `owner/repo`. */
40
41
  const GITHUB_REMOTE = /github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/;
41
42
  /** Matches a pull request URL and captures its number. */
42
43
  const PR_URL = /\/pull\/(\d+)/;
43
44
  /** Command that continues a prepare run; `npx` because a first-time user does not have the binary on PATH. */
44
45
  const RESUME_COMMAND = `npx ${PACKAGE} prepare --resume`;
45
- /** 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. */
@@ -285,11 +282,12 @@ async function createAndConfigure(repo, branch, worktree, deps, progress) {
285
282
  deps.print(stopped);
286
283
  return false;
287
284
  }
288
- deps.print(EXIT_CLAUDE_HINT);
289
285
  try {
290
- await runInteractive(deps, progress, [configurePrompt()], worktree);
286
+ await runConfigure(deps, progress, "pr", worktree);
291
287
  }
292
- catch {
288
+ catch (err) {
289
+ if (err instanceof InterruptedError)
290
+ throw err;
293
291
  deps.print(stopped);
294
292
  return false;
295
293
  }
@@ -414,22 +412,42 @@ export async function prepare(repo, deps, options = {}) {
414
412
  *
415
413
  * @param deps - Dependencies.
416
414
  * @param progress - Marks the child as owning the terminal.
417
- * @param args - Arguments of `claude`.
415
+ * @param command - Command and arguments that open Claude.
418
416
  * @param cwd - Worktree.
419
417
  * @returns Resolves when `claude` exits 0.
420
418
  * @throws When `claude` exits non-zero.
421
419
  * @example
422
- * await runInteractive(deps, progress, ["--cloud", prompt], "/home/u/app-cloud-ready");
420
+ * await runInteractive(deps, progress, { cmd: "claude", args: ["--cloud", prompt] }, "/home/u/app-cloud-ready");
423
421
  */
424
- async function runInteractive(deps, progress, args, cwd) {
422
+ async function runInteractive(deps, progress, command, cwd) {
425
423
  progress.childOwnsTerminal = true;
426
424
  try {
427
- await deps.run("claude", args, { cwd });
425
+ await deps.run(command.cmd, command.args, { cwd });
428
426
  }
429
427
  finally {
430
428
  progress.childOwnsTerminal = false;
431
429
  }
432
430
  }
431
+ /**
432
+ * Shows the screen before Claude Code opens, waits for Enter, and runs the local agent with
433
+ * `cloud-ready configure` (through the repo's `claude:free` script when it has one).
434
+ *
435
+ * @param deps - Dependencies.
436
+ * @param progress - Marks the child as owning the terminal.
437
+ * @param place - Where `prepare` works.
438
+ * @param cwd - Folder where Claude runs.
439
+ * @returns Resolves when Claude exits 0.
440
+ * @throws InterruptedError on Ctrl+C at the Enter line; an error when Claude exits non-zero.
441
+ * @example
442
+ * await runConfigure(deps, progress, "pr", "/home/u/app-cloud-ready");
443
+ */
444
+ async function runConfigure(deps, progress, place, cwd) {
445
+ const color = deps.color ?? false;
446
+ for (const line of renderBanner({ place, free: hasFreeScript(cwd), color }))
447
+ deps.print(line);
448
+ await deps.waitEnter(enterLine(color));
449
+ await runInteractive(deps, progress, claudeCommand(cwd, configurePrompt()), cwd);
450
+ }
433
451
  /**
434
452
  * The line printed on Ctrl+C: where the worktree is, when it exists.
435
453
  *
@@ -493,7 +511,7 @@ async function prepareSteps(gitRoot, deps, options, progress) {
493
511
  deps.print(renderEnvironmentSection(config, deps.packageVersion));
494
512
  await deps.waitEnter(WAIT_TEXT);
495
513
  try {
496
- await runInteractive(deps, progress, ["--cloud", verifyPrompt({ branch, pr: pushed.pr })], worktree);
514
+ await runInteractive(deps, progress, { cmd: "claude", args: ["--cloud", verifyPrompt({ branch, pr: pushed.pr })] }, worktree);
497
515
  }
498
516
  catch (err) {
499
517
  deps.print(`${OUTPUT_PREFIX}falló claude --cloud: ${message(err)}; corre ${RESUME_COMMAND} en ${worktree} para reintentar`);
@@ -545,11 +563,12 @@ async function prepareHere(gitRoot, deps, progress) {
545
563
  deps.print(stopped);
546
564
  return EXIT_FAIL;
547
565
  }
548
- deps.print(EXIT_CLAUDE_HINT_HERE);
549
566
  try {
550
- await runInteractive(deps, progress, [configurePrompt()], gitRoot);
567
+ await runConfigure(deps, progress, "here", gitRoot);
551
568
  }
552
- catch {
569
+ catch (err) {
570
+ if (err instanceof InterruptedError)
571
+ throw err;
553
572
  deps.print(stopped);
554
573
  return EXIT_FAIL;
555
574
  }
@@ -620,7 +639,7 @@ export async function verifyBranch(repo, deps) {
620
639
  const progress = {};
621
640
  const stop = deps.onInterrupt?.(() => !progress.childOwnsTerminal);
622
641
  try {
623
- await runInteractive(deps, progress, ["--cloud", verifyPrompt({ branch, pr })], gitRoot);
642
+ await runInteractive(deps, progress, { cmd: "claude", args: ["--cloud", verifyPrompt({ branch, pr })] }, gitRoot);
624
643
  }
625
644
  catch (err) {
626
645
  deps.print(`${OUTPUT_PREFIX}falló claude --cloud: ${message(err)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-cloud-kit",
3
- "version": "1.0.0-rc.2",
3
+ "version": "1.0.0-rc.3",
4
4
  "description": "Zero-dependency CLI that prepares Claude Code cloud sessions.",
5
5
  "type": "module",
6
6
  "license": "MIT",