agent-cloud-kit 1.0.0-rc.6 → 1.0.0-rc.8

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/dist/init.js CHANGED
@@ -5,6 +5,7 @@
5
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import { dirname, join } from "node:path";
7
7
  import { CONFIG_FILE, OUTPUT_PREFIX, PACKAGE } from "./constants.js";
8
+ import { t } from "./messages/index.js";
8
9
  import { installSkill, linkSkill } from "./skill.js";
9
10
  /** Path of the Claude Code settings file, relative to the repo root. */
10
11
  const SETTINGS_FILE = ".claude/settings.json";
@@ -35,8 +36,8 @@ function isRecord(value) {
35
36
  * @returns The failed outcome.
36
37
  */
37
38
  function addByHand(reason) {
38
- const entry = JSON.stringify({ hooks: [HOOK] });
39
- return { line: `${SETTINGS_FILE} ${reason}; no se tocó. Agrega esta entrada a hooks.SessionStart a mano: ${entry}`, ok: false };
39
+ const hook = JSON.stringify({ hooks: [HOOK] });
40
+ return { entry: { kind: "warn", text: t().init.byHand(SETTINGS_FILE, reason, hook) }, ok: false };
40
41
  }
41
42
  /**
42
43
  * Writes a JSON file with a trailing newline, creating its folder.
@@ -70,7 +71,7 @@ function hasHook(entries) {
70
71
  function scaffoldConfig(repo, packageVersion) {
71
72
  const file = join(repo, CONFIG_FILE);
72
73
  if (existsSync(file))
73
- return `${CONFIG_FILE} ya existe; se dejó como estaba`;
74
+ return { kind: "same", text: t().init.configExists(CONFIG_FILE) };
74
75
  writeJson(file, {
75
76
  $schema: `https://unpkg.com/${PACKAGE}@${packageVersion}/schema.json`,
76
77
  version: packageVersion,
@@ -82,7 +83,7 @@ function scaffoldConfig(repo, packageVersion) {
82
83
  requiredEnv: [],
83
84
  domains: [],
84
85
  });
85
- return `se creó ${CONFIG_FILE}`;
86
+ return { kind: "ok", text: t().init.configCreated(CONFIG_FILE) };
86
87
  }
87
88
  /**
88
89
  * Adds the SessionStart hook to `.claude/settings.json`, keeping everything else.
@@ -98,21 +99,21 @@ function registerHook(repo) {
98
99
  settings = JSON.parse(readFileSync(file, "utf8"));
99
100
  }
100
101
  catch {
101
- return addByHand("no es JSON válido");
102
+ return addByHand(t().init.notJson);
102
103
  }
103
104
  }
104
105
  if (!isRecord(settings))
105
- return addByHand("no es un objeto JSON");
106
+ return addByHand(t().init.notObject);
106
107
  const hooks = settings.hooks ?? {};
107
108
  if (!isRecord(hooks))
108
- return addByHand('tiene un "hooks" que no es un objeto');
109
+ return addByHand(t().init.hooksNotObject);
109
110
  const entries = hooks.SessionStart ?? [];
110
111
  if (!Array.isArray(entries))
111
- return addByHand('tiene un "hooks.SessionStart" que no es un arreglo');
112
+ return addByHand(t().init.sessionStartNotArray);
112
113
  if (hasHook(entries))
113
- return { line: `${SETTINGS_FILE} ya tiene el hook SessionStart`, ok: true };
114
+ return { entry: { kind: "same", text: t().init.hookExists(SETTINGS_FILE) }, ok: true };
114
115
  writeJson(file, { ...settings, hooks: { ...hooks, SessionStart: [...entries, { hooks: [HOOK] }] } });
115
- return { line: `se agregó el hook SessionStart a ${SETTINGS_FILE}`, ok: true };
116
+ return { entry: { kind: "ok", text: t().init.hookAdded(SETTINGS_FILE) }, ok: true };
116
117
  }
117
118
  /**
118
119
  * Sets a repo up for agent-cloud-kit; safe to run again. It is the only place that copies the
@@ -131,6 +132,6 @@ export function init(repo, packageVersion, packageRoot) {
131
132
  const hook = registerHook(repo);
132
133
  const skill = installSkill(repo, packageRoot, packageVersion);
133
134
  const link = linkSkill(repo);
134
- const lines = [config, hook.line, skill, ...(link ? [link] : [])];
135
- return { lines: lines.map((l) => OUTPUT_PREFIX + l), ok: hook.ok };
135
+ const entries = [config, hook.entry, skill, ...(link ? [{ kind: "ok", text: link }] : [])];
136
+ return { lines: entries.map((e) => OUTPUT_PREFIX + e.text), entries, ok: hook.ok };
136
137
  }
@@ -7,6 +7,7 @@ import { tmpdir } from "node:os";
7
7
  import { join } from "node:path";
8
8
  import { TOOL_INSTALL_TIMEOUT_MS } from "./constants.js";
9
9
  import { TOOLS } from "./detect.js";
10
+ import { t } from "./messages/index.js";
10
11
  import { step } from "./steps.js";
11
12
  /** Base URL of the Node.js distribution. */
12
13
  const NODE_DIST_URL = "https://nodejs.org/dist";
@@ -123,7 +124,7 @@ async function nodeExpectedHash(run, env, dir, version, file) {
123
124
  await run("curl", ["-fsSL", "-o", path, "--", url], { env, timeoutMs: TOOL_INSTALL_TIMEOUT_MS });
124
125
  const hash = parseShasums(readFileSync(path, "utf8"), file);
125
126
  if (!hash)
126
- throw new Error(`${file} no aparece en ${NODE_SHASUMS_FILE}`);
127
+ throw new Error(t().installers.notInShasums(file, NODE_SHASUMS_FILE));
127
128
  return hash;
128
129
  }
129
130
  /**
@@ -138,7 +139,7 @@ async function nodeExpectedHash(run, env, dir, version, file) {
138
139
  export function nodeArch(arch) {
139
140
  const mapped = NODE_ARCHS[arch];
140
141
  if (!mapped)
141
- throw new Error(`arquitectura no soportada para node: ${arch}`);
142
+ throw new Error(t().installers.unsupportedArch(arch));
142
143
  return mapped;
143
144
  }
144
145
  /**
@@ -153,7 +154,7 @@ export function nodeArch(arch) {
153
154
  */
154
155
  export function assertVersion(tool, version) {
155
156
  if (!VERSION_PATTERN.test(version))
156
- throw new Error(`versión inválida para ${tool}: ${version}`);
157
+ throw new Error(t().installers.invalidVersion(tool, version));
157
158
  }
158
159
  /**
159
160
  * Tells whether a version is safe to use.
@@ -254,7 +255,7 @@ export function curlNodeIndex(run, env) {
254
255
  await run("curl", ["-fsSL", "--max-time", NODE_INDEX_TIMEOUT_SECONDS, "-o", file, "--", NODE_INDEX_URL], { env });
255
256
  const parsed = JSON.parse(readFileSync(file, "utf8"));
256
257
  if (!Array.isArray(parsed))
257
- throw new Error("el índice de versiones de node no es una lista");
258
+ throw new Error(t().installers.indexNotList);
258
259
  return parsed.filter((r) => typeof r === "object" && r !== null && typeof r.version === "string" && (r.lts === false || typeof r.lts === "string"));
259
260
  }
260
261
  finally {
@@ -283,7 +284,7 @@ export async function resolveVersions(versions, fetchIndex) {
283
284
  const result = await step(`node ${requested}`, async () => {
284
285
  exact = resolveNodeVersion(requested, await fetchIndex());
285
286
  if (!exact)
286
- throw new Error(`ninguna versión de node coincide con ${requested}`);
287
+ throw new Error(t().installers.noNodeMatch(requested));
287
288
  });
288
289
  if (!exact)
289
290
  return { resolved, results: [result] };
@@ -403,7 +404,7 @@ export async function installTools(versions, toolsDir, run, env) {
403
404
  }
404
405
  if (tool === "node" && !NODE_EXACT.test(version)) {
405
406
  results.push(await step(`${tool} ${version}`, async () => {
406
- throw new Error(`node ${version} debe resolverse primero a una versión exacta`);
407
+ throw new Error(t().installers.nodeMustResolve(version));
407
408
  }));
408
409
  continue;
409
410
  }
package/dist/lang.js ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @file The language of the terminal: `--lang`, then `AGENT_CLOUD_KIT_LANG`, then Spanish.
3
+ * `LANG` and `LC_ALL` are not read: on macOS they are often `en_US.UTF-8` for Spanish speakers.
4
+ */
5
+ /** Environment variable that fixes the language. */
6
+ export const LANG_ENV = "AGENT_CLOUD_KIT_LANG";
7
+ /** Option that picks the language for one run. */
8
+ export const LANG_OPTION = "--lang";
9
+ /** A `--lang` or `AGENT_CLOUD_KIT_LANG` value that is neither `es` nor `en`. */
10
+ export class LangError extends Error {
11
+ /** Where the value came from: `--lang` or `AGENT_CLOUD_KIT_LANG`. */
12
+ source;
13
+ /** The value. */
14
+ value;
15
+ /**
16
+ * Builds the error.
17
+ *
18
+ * @param source - Where the value came from.
19
+ * @param value - The value.
20
+ */
21
+ constructor(source, value) {
22
+ super(`${source}: ${value}`);
23
+ this.name = "LangError";
24
+ this.source = source;
25
+ this.value = value;
26
+ }
27
+ }
28
+ /**
29
+ * Tells whether a value is a language.
30
+ *
31
+ * @param value - The value.
32
+ * @returns True for `es` and `en`.
33
+ */
34
+ function isLang(value) {
35
+ return value === "es" || value === "en";
36
+ }
37
+ /**
38
+ * The language of the terminal.
39
+ *
40
+ * @param flag - The `--lang` value, or undefined.
41
+ * @param env - The environment.
42
+ * @returns The flag, else `AGENT_CLOUD_KIT_LANG` (empty counts as unset), else `es`.
43
+ * @throws LangError when the value is neither `es` nor `en`.
44
+ * @example
45
+ * resolveLang(undefined, { AGENT_CLOUD_KIT_LANG: "en" }); // "en"
46
+ */
47
+ export function resolveLang(flag, env) {
48
+ const fromEnv = env[LANG_ENV]?.trim() ? env[LANG_ENV] : undefined;
49
+ const value = flag ?? fromEnv;
50
+ if (value === undefined)
51
+ return "es";
52
+ const lang = value.trim().toLowerCase();
53
+ if (!isLang(lang))
54
+ throw new LangError(flag !== undefined ? LANG_OPTION : LANG_ENV, value);
55
+ return lang;
56
+ }
57
+ /**
58
+ * Takes `--lang <x>` or `--lang=<x>` out of the arguments, wherever it is.
59
+ *
60
+ * @param argv - Arguments after the executable name.
61
+ * @returns The other arguments and the value (empty string when `--lang` has no value).
62
+ * @example
63
+ * splitLang(["doctor", "--lang", "en"]); // { args: ["doctor"], flag: "en" }
64
+ */
65
+ export function splitLang(argv) {
66
+ const args = [];
67
+ let flag;
68
+ for (let i = 0; i < argv.length; i++) {
69
+ const arg = argv[i];
70
+ if (arg === LANG_OPTION) {
71
+ const next = argv[i + 1];
72
+ flag = next !== undefined && !next.startsWith("-") ? next : "";
73
+ if (flag)
74
+ i++;
75
+ }
76
+ else if (arg.startsWith(`${LANG_OPTION}=`)) {
77
+ flag = arg.slice(LANG_OPTION.length + 1);
78
+ }
79
+ else {
80
+ args.push(arg);
81
+ }
82
+ }
83
+ return flag === undefined ? { args } : { args, flag };
84
+ }
package/dist/launcher.js CHANGED
@@ -5,6 +5,7 @@
5
5
  import { existsSync, readdirSync, statSync } from "node:fs";
6
6
  import { join } from "node:path";
7
7
  import { CONFIG_FILE, PACKAGE } from "./constants.js";
8
+ import { t } from "./messages/index.js";
8
9
  /** Directory, inside the tools dir, that holds one installed copy of this package per version. */
9
10
  const PKG_DIR = "pkg";
10
11
  /** Name of the CLI executable. */
@@ -58,7 +59,7 @@ export function planSetup(repos, current, read) {
58
59
  return repos.map((repo) => {
59
60
  const { config, errors } = read(repo);
60
61
  if (!config)
61
- return { repo, action: "skip", reason: errors.length ? errors.join("; ") : `no hay ${CONFIG_FILE}` };
62
+ return { repo, action: "skip", reason: errors.length ? errors.join("; ") : t().config.missing(CONFIG_FILE) };
62
63
  if (config.version === current)
63
64
  return { repo, action: "run", version: current };
64
65
  return { repo, action: "relaunch", version: config.version };
package/dist/manual.js CHANGED
@@ -6,6 +6,7 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync,
6
6
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
7
  import { readConfig } from "./config.js";
8
8
  import { CONFIG_FILE, PACKAGE, packageSpec } from "./constants.js";
9
+ import { es, t } from "./messages/index.js";
9
10
  /** Path of the manual, relative to the repo. */
10
11
  export const MANUAL_FILE = "docs/agent-cloud.md";
11
12
  /** Marker that opens the generated block. */
@@ -60,35 +61,43 @@ function allowedHosts(config) {
60
61
  return [...new Set([...config.domains, ...hosts])].sort();
61
62
  }
62
63
  /**
63
- * Renders the "Configurar el entorno en claude.ai" section.
64
+ * Renders the "Configurar el entorno en claude.ai" section: in Spanish for the manual, in the
65
+ * language of the terminal when `prepare` shows it.
64
66
  *
65
67
  * @param config - Repo config.
66
68
  * @param packageVersion - Version of the running package, for the setup script.
69
+ * @param text - Texts of the section; Spanish when omitted.
67
70
  * @returns The section, without a trailing newline.
68
71
  * @example
69
72
  * renderEnvironmentSection(config, "1.0.0").startsWith("## Configurar el entorno en claude.ai"); // true
70
73
  */
71
- export function renderEnvironmentSection(config, packageVersion) {
74
+ export function renderEnvironmentSection(config, packageVersion, text = es.environment) {
72
75
  const hosts = allowedHosts(config);
73
76
  const code = (items) => items.map((item) => `\`${item}\``).join(", ");
74
77
  const lines = [
75
- "## Configurar el entorno en claude.ai",
78
+ text.title,
76
79
  "",
77
- "Elige el entorno con `/remote-env` y edítalo en claude.ai/code:",
80
+ text.intro,
78
81
  "",
79
- `- **Network access:** Custom y marca "Also include default list of common package managers". Dominios extra: ${hosts.length ? code(hosts) : "ninguno"}.`,
80
- `- **Variables:** ${config.requiredEnv.length ? `${code(config.requiredEnv)} (solo los nombres; los valores los pones tú)` : "ninguna"}.`,
81
- "- **Claude GitHub App:** instalado en el repo, o la sesión no puede pushear.",
82
- "- **Setup script:**",
82
+ text.network(hosts.length ? code(hosts) : text.none),
83
+ text.variables(config.requiredEnv.length ? text.namesOnly(code(config.requiredEnv)) : text.noneVariables),
84
+ text.githubApp,
85
+ text.setupScript,
83
86
  "",
84
87
  "```bash",
85
88
  setupScript(packageVersion),
86
89
  "```",
87
90
  ];
88
91
  if (config.requiredEnv.length)
89
- lines.push("", "> ⚠️ Quien use el entorno puede leer las variables, y no llegan al setup script (solo a la sesión).");
92
+ lines.push("", text.variablesWarning);
90
93
  return lines.join("\n");
91
94
  }
95
+ /** What to do when the tests change, one line. */
96
+ const TESTS_CHANGE_SECTION = [
97
+ "## Si cambian los tests",
98
+ "",
99
+ `Si agregas tests (p. ej. de Electron) o cambias cómo se corren, corre \`npx ${PACKAGE} prepare\`: la skill \`cloud-ready\` los adapta a la nube sin cambiar lo que comprueban y actualiza este documento.`,
100
+ ].join("\n");
92
101
  /** The "how to verify" section, one line. */
93
102
  const VERIFY_SECTION = [
94
103
  "## Verificar",
@@ -143,6 +152,7 @@ export function renderManual(input) {
143
152
  renderEnvironmentSection(input.config, input.packageVersion),
144
153
  VERIFY_SECTION,
145
154
  `## Qué corre en la nube\n\n${notes?.trim() ? notes.trim() : "Nada anotado."}`,
155
+ TESTS_CHANGE_SECTION,
146
156
  END,
147
157
  ];
148
158
  return parts.join("\n\n");
@@ -179,7 +189,7 @@ export function mergeManual(existing, block) {
179
189
  export function writeManual(repo, packageVersion) {
180
190
  const { config, errors } = readConfig(repo);
181
191
  if (!config)
182
- throw new Error(errors[0] ?? `no existe ${CONFIG_FILE}; corre init primero`);
192
+ throw new Error(errors[0] ?? t().manual.noConfig(CONFIG_FILE));
183
193
  const file = join(repo, MANUAL_FILE);
184
194
  const existing = existsSync(file) ? readFileSync(file, "utf8") : undefined;
185
195
  const block = renderManual({ config, packageVersion, notes: resolveNotes(repo, config.notes) });
@@ -0,0 +1,226 @@
1
+ /** Command that continues a prepare run. */
2
+ const RESUME = "npx agent-cloud-kit prepare --resume";
3
+ /** Command that launches the cloud verification. */
4
+ const VERIFY = "npx agent-cloud-kit verify";
5
+ export const en = {
6
+ help: `agent-cloud-kit: prepares repos for Claude Code cloud sessions.
7
+
8
+ Usage: agent-cloud-kit <command> [--lang es|en]
9
+
10
+ Commands:
11
+ setup [--repo <path>] prepares the machine for every repo in the home dir (or only <path>)
12
+ session reports the machine state and wires the tools into the session
13
+ docker starts the Docker daemon and waits until it answers
14
+ doctor checks the allowed domains, the tools, Docker and the machine state
15
+ init creates .claude/cloud.json and registers the SessionStart hook
16
+ manual writes docs/agent-cloud.md from .claude/cloud.json
17
+ prepare [--pr|--here|--resume]
18
+ runs locally: gets the repo ready for the cloud, in a new PR (--pr) or on the
19
+ current branch without committing (--here); with no option, it asks where
20
+ verify runs locally: starts a cloud session that verifies the current branch (already pushed)
21
+
22
+ Options:
23
+ --lang es|en language of the messages (Spanish by default; AGENT_CLOUD_KIT_LANG=es|en sets it)
24
+ --help shows this help
25
+ --version shows the package version
26
+ `,
27
+ lang: {
28
+ invalid: (source, value) => `${source} expects es or en: ${value}`,
29
+ },
30
+ cli: {
31
+ versionMismatch: (running, file, pinned) => `running version ${running}, although ${file} asks for ${pinned}`,
32
+ setupSummary: (repo, steps, failed, names) => `setup ${repo}: ${steps} steps, ${failed} failed${names.length ? ` (${names.join(", ")})` : ""}`,
33
+ skip: (repo, reason) => `skipping ${repo}: ${reason}`,
34
+ relaunching: (version, repo) => `relaunching with version ${version} for ${repo}`,
35
+ relaunchFailed: (version, repo, error) => `relaunching with version ${version} for ${repo} failed: ${error}`,
36
+ repoNeedsPath: (option) => `${option} needs a path`,
37
+ noRepos: (home) => `no repos found in ${home}`,
38
+ setupErrorFor: (repo, error) => `setup error in ${repo}: ${error}`,
39
+ setupError: (error) => `setup error: ${error}`,
40
+ sessionDelegateFailed: (version, error) => `session with version ${version} failed: ${error}`,
41
+ manualWritten: (file) => `wrote ${file}`,
42
+ unknownOption: (command, options) => `unknown option for ${command}: ${options}`,
43
+ onlyOne: (options) => `use only one of ${options}`,
44
+ },
45
+ config: {
46
+ dockerNotObject: '"docker" must be an object',
47
+ unknownField: (key) => `unknown field "${key}"`,
48
+ badPull: '"docker.pull" must be "auto", false or an array of strings',
49
+ notStringList: (key) => `"${key}" must be an array of strings`,
50
+ badHost: (host) => `"domains" has an invalid host: ${host}`,
51
+ badVersion: '"version" must be an exact semver, like "1.0.0"',
52
+ notesNotString: '"notes" must be a string',
53
+ unreadable: (reason) => `could not read the file: ${reason}`,
54
+ notObject: "must contain a JSON object",
55
+ missing: (file) => `no ${file}`,
56
+ },
57
+ run: {
58
+ timedOut: (cmd, ms) => `${cmd} went over the ${ms}ms limit`,
59
+ exitCode: (cmd, code) => `${cmd} exited with code ${code}`,
60
+ exitSignal: (cmd, signal) => `${cmd} was ended by signal ${signal}`,
61
+ },
62
+ docker: {
63
+ missing: "dockerd is not installed",
64
+ didNotStart: (log) => `dockerd did not start; check ${log}`,
65
+ },
66
+ doctor: {
67
+ domain: (host, ok) => `network ${host}: ${ok ? "ok" : "blocked"}`,
68
+ toolInvalid: (tool, version) => `tool ${tool} ${version}: invalid version`,
69
+ toolUnresolved: (tool, version) => `tool ${tool} ${version}: unresolved (setup did not install it)`,
70
+ tool: (tool, version, present) => `tool ${tool} ${version}: ${present ? "ok" : "missing"}`,
71
+ fileMissing: (file) => `${file}: missing`,
72
+ stateMissing: "state: missing (setup did not run)",
73
+ fingerprint: (same) => `fingerprint: ${same ? "ok" : "different"}`,
74
+ step: (name, ok, seconds) => `step ${name}: ${ok ? "ok" : "failed"} (${seconds}s)`,
75
+ docker: (ok) => `docker: ${ok ? "ok" : "missing"}`,
76
+ plugin: (name) => `warning: the plugin ${name} is enabled but is not in the repo; it will not reach the cloud (copy its skills with skills-manager)`,
77
+ skill: (dir, installed, version) => `warning: ${dir} is at ${installed} and the package at ${version}; run npx agent-cloud-kit@${version} init`,
78
+ groupError: (group, error) => `${group}: error: ${error}`,
79
+ groups: { network: "network", tools: "tools", docker: "docker", state: "state", plugins: "plugins", skill: "skill" },
80
+ allOk: "All good",
81
+ someFailed: (failed, total) => (failed === 1 ? `1 of ${total} checks failed` : `${failed} of ${total} checks failed`),
82
+ warnings: (count) => (count === 1 ? "1 warning" : `${count} warnings`),
83
+ failedHint: "look at the lines with missing, blocked or failed",
84
+ },
85
+ session: {
86
+ genericNote: "you are in a Claude Code cloud session. There are no open ports or previews: test against localhost. gh pr and gh issue fail (the GitHub proxy blocks GraphQL): use gh api (REST).",
87
+ setupNotRun: (repo, spec) => `the environment setup did not run for ${repo}. Set this as the environment setup script in claude.ai: npx -y ${spec} setup`,
88
+ setupFailed: (names) => `setup failed: ${names.join(", ")}. Run agent-cloud-kit doctor.`,
89
+ otherConfig: "the machine was prepared with another configuration. Edit the setup script field in claude.ai (any change) so it is rebuilt.",
90
+ slow: (time) => `setup took ${time}, close to the ~5 minute limit.`,
91
+ envFileFailed: (variable, file, error) => `could not write ${variable} (${file}): ${error}`,
92
+ stepTimedOut: (command) => `the session step went over the time limit: ${command}`,
93
+ stepFailed: (command) => `the session step failed: ${command}`,
94
+ missingEnv: (names) => `missing environment variables: ${names.join(", ")}`,
95
+ dockerFailed: (problem) => `could not start dockerd: ${problem}`,
96
+ error: (error) => `session error: ${error}`,
97
+ },
98
+ setup: {
99
+ aptRepoHttps: (prefix) => `the apt repo must be an ${prefix} URL`,
100
+ badDebianName: "invalid Debian package name",
101
+ badImage: "invalid image reference",
102
+ packageMismatch: (source, found, version) => `the package in ${source} is ${found ?? "unknown"}, not ${version}`,
103
+ noConfig: "no configuration",
104
+ },
105
+ steps: {
106
+ aptRepo: (url) => `apt repo ${url}`,
107
+ aptPackage: (name) => `apt package ${name}`,
108
+ selfInstall: (version) => `self install ${version}`,
109
+ writeState: "write state",
110
+ lane: (index) => `lane ${index}`,
111
+ config: "config",
112
+ },
113
+ installers: {
114
+ notInShasums: (file, shasums) => `${file} is not in ${shasums}`,
115
+ unsupportedArch: (arch) => `unsupported architecture for node: ${arch}`,
116
+ invalidVersion: (tool, version) => `invalid version for ${tool}: ${version}`,
117
+ indexNotList: "the node version index is not a list",
118
+ noNodeMatch: (requested) => `no node version matches ${requested}`,
119
+ nodeMustResolve: (version) => `node ${version} must first resolve to an exact version`,
120
+ },
121
+ init: {
122
+ byHand: (file, reason, entry) => `${file} ${reason}; it was left untouched. Add this entry to hooks.SessionStart by hand: ${entry}`,
123
+ notJson: "is not valid JSON",
124
+ notObject: "is not a JSON object",
125
+ hooksNotObject: 'has a "hooks" that is not an object',
126
+ sessionStartNotArray: 'has a "hooks.SessionStart" that is not an array',
127
+ configExists: (file) => `${file} already exists; left as it was`,
128
+ configCreated: (file) => `created ${file}`,
129
+ hookExists: (file) => `${file} already has the SessionStart hook`,
130
+ hookAdded: (file) => `added the SessionStart hook to ${file}`,
131
+ },
132
+ skill: {
133
+ current: (dir, version) => `${dir} is already at ${version}`,
134
+ missing: (path, root) => `${path}/SKILL.md not found in ${root}`,
135
+ installed: (dir, version) => `installed ${dir} ${version}`,
136
+ updated: (dir, from, to) => `updated ${dir} from ${from} to ${to}`,
137
+ linked: (link, dir) => `linked ${link} to ${dir}`,
138
+ },
139
+ manual: {
140
+ noConfig: (file) => `${file} does not exist; run init first`,
141
+ },
142
+ environment: {
143
+ title: "## Configure the environment in claude.ai",
144
+ intro: "Pick the environment with `/remote-env` and edit it in claude.ai/code:",
145
+ network: (hosts) => `- **Network access:** Limited (Custom) with "Also include default list of common package managers" turned on. Extra domains: ${hosts}.`,
146
+ variables: (names) => `- **Variables:** ${names}.`,
147
+ namesOnly: (names) => `${names} (only the names; you fill in the values)`,
148
+ none: "none",
149
+ noneVariables: "none",
150
+ githubApp: "- **Claude GitHub App:** installed on the repo, or the session cannot push.",
151
+ setupScript: "- **Setup script:**",
152
+ variablesWarning: "> ⚠️ Anyone who uses the environment can read the variables, and they do not reach the setup script (only the session).",
153
+ },
154
+ menu: {
155
+ cancelled: "Cancelled",
156
+ },
157
+ preflight: {
158
+ noTty: (command) => `${command} needs an interactive terminal (run the command directly in your terminal)`,
159
+ noGit: (command) => `this is not a git repository; run ${command} inside the repo`,
160
+ noOrigin: (command) => `the repository has no 'origin' remote; ${command} needs to push a branch to GitHub`,
161
+ noClaude: (command) => `claude is not installed or not on the PATH; install Claude Code and run ${command} again`,
162
+ noCloud: (command) => `your claude version does not support --cloud; update Claude Code and run ${command} again`,
163
+ },
164
+ banner: {
165
+ title: "Claude Code opens now",
166
+ subtitle: "with the cloud-ready skill",
167
+ step1: "Claude reviews the repo and proposes how to get it ready for the cloud",
168
+ step2: "Answer its questions",
169
+ step3: (done, exit, after) => `When it says ${done}, leave with ${exit} ${after}`,
170
+ configureDone: "configure finished",
171
+ afterPr: "→ prepare commits, pushes and opens a PR",
172
+ afterHere: "→ the changes stay uncommitted",
173
+ free: (script) => `It opens with ${script} (no sandbox nor permission prompts): the repo uses claude-repo-guard`,
174
+ enter: "opens Claude Code",
175
+ cancel: "· Ctrl+C cancels",
176
+ continue: "once you have configured the environment in claude.ai",
177
+ continueHint: "· opens the cloud session that verifies it",
178
+ },
179
+ prepare: {
180
+ interrupted: "interrupted",
181
+ resume: (reason, worktree) => `${reason}; the worktree is at ${worktree}; run ${RESUME} there to continue`,
182
+ noConfig: (file) => `${file} does not exist`,
183
+ noNotes: (file) => `the notes file ${file} does not exist`,
184
+ worktreeExists: (worktree) => `${worktree} already exists; remove it (git worktree remove ${worktree}) or continue there with ${RESUME}`,
185
+ branchExists: (branch) => `the branch ${branch} already exists; delete it (git branch -D ${branch}) or run prepare another day`,
186
+ worktreeCreated: (worktree, branch) => `created the worktree ${worktree} on the branch ${branch}`,
187
+ worktreeFailed: (error) => `could not create the worktree: ${error}`,
188
+ agentStopped: "the local agent stopped",
189
+ prepareStopped: "prepare stopped",
190
+ configureUnfinished: (problem) => `configure did not finish: ${problem}`,
191
+ otherChanges: "the local agent also changed:",
192
+ includeQuestion: "Include these changes in the commit? [y/N]",
193
+ leftUncommitted: (worktree) => `left uncommitted in ${worktree}`,
194
+ nothingToCommit: "nothing new to commit",
195
+ pushed: (branch) => `pushed the branch ${branch}`,
196
+ pushFailed: (error, worktree) => `could not commit and push: ${error}; fix it in ${worktree} and run ${RESUME} there`,
197
+ prReused: (pr) => `reusing the open PR #${pr}`,
198
+ prDraft: (url) => `draft PR: ${url}`,
199
+ prByHand: (link, branch) => `could not open the PR with gh; open it by hand${link ? `: ${link}` : ` from the branch ${branch}`}`,
200
+ interruptedHere: "interrupted; whatever changed is left uncommitted on your branch",
201
+ interruptedNothing: "interrupted; nothing was created",
202
+ nothingToResume: (suffix, prefix) => `nothing to resume: run prepare --resume in the *${suffix} worktree, on a ${prefix}* branch`,
203
+ resumeUnfinished: (worktree, problem) => `configure did not finish in ${worktree}: ${problem}; run claude "/cloud-ready configure" there first and then ${RESUME}`,
204
+ nothingChanged: "nothing was changed",
205
+ cloudFailedResume: (error, worktree) => `claude --cloud failed: ${error}; run ${RESUME} in ${worktree} to try again`,
206
+ placeQuestion: "Where do I prepare the repo?",
207
+ placePr: "In a new PR",
208
+ placePrHint: (base) => `recommended · starts from origin/${base}, does not touch your branch or your files`,
209
+ placeHere: "On the current branch, without committing",
210
+ placeHereHint: `you review them with git diff, commit and push, and then run ${VERIFY}`,
211
+ stoppedHere: "prepare stopped; whatever changed is left uncommitted on your branch",
212
+ hereUnfinished: (problem) => `configure did not finish: ${problem}; run claude "/cloud-ready configure" and then prepare again`,
213
+ manualFailed: (file, error) => `could not write ${file}: ${error}`,
214
+ hereTitle: "Done, without committing",
215
+ hereSteps: (manual) => ["Review the changes with git diff", "Commit and push", `Configure the environment in claude.ai (${manual})`, `Run ${VERIFY}`],
216
+ noBranch: "you are not on a branch; switch to the branch you want to verify",
217
+ notPrepared: (problem) => `the repo is not prepared (${problem}); run npx agent-cloud-kit prepare first`,
218
+ dirty: "the preparation has uncommitted changes; commit and push: the cloud only sees what is on origin",
219
+ notPushed: (branch) => `the branch ${branch} is not pushed or has unpushed commits; run git push and run verify again`,
220
+ cloudFailed: (error) => `claude --cloud failed: ${error}`,
221
+ cloudStarted: "The cloud session is running",
222
+ cloudWillReport: (branch, pr) => `the cloud session will report with a push to ${branch}${pr ? " and a comment on the PR" : ""}`,
223
+ rowBranch: "Branch",
224
+ rowPr: "PR",
225
+ },
226
+ };