agent-cloud-kit 1.0.0-rc.7 → 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/README.md CHANGED
@@ -161,6 +161,20 @@ A Node version that is not exact (`22`, `22.1`, `lts/iron`, `lts/*` or `node`) i
161
161
  Environment variables: `AGENT_CLOUD_KIT_DIR` overrides `/opt/agent-cloud-kit` and `AGENT_CLOUD_KIT_HOME`
162
162
  overrides `/home/user`; both exist mainly for tests.
163
163
 
164
+ ### Language and Kuro
165
+
166
+ The terminal speaks Spanish by default. `--lang en` (with any command) switches it to English for
167
+ one run, and `AGENT_CLOUD_KIT_LANG=en|es` fixes it; `--lang` wins over the variable. `LANG` and
168
+ `LC_ALL` are not read. Only the terminal changes: what is written in the repo (`docs/agent-cloud.md`,
169
+ `.claude/cloud-notes.md`, the `cloud-ready` skill, the commit) and the prompts for Claude stay in
170
+ Spanish. `setup` passes the language on to the version it relaunches, and `session` to the one it
171
+ delegates to.
172
+
173
+ In a terminal, the final messages (the end of `doctor`, `prepare` and `verify`, and the screen
174
+ before Claude opens) come in a block with Kuro, the mascot of issue-flow and agent-kitchen.
175
+ `NO_COLOR` removes the color and keeps Kuro. Outside a terminal (logs, pipes, the `SessionStart`
176
+ hook) the lines keep the plain `agent-cloud-kit: ` prefix.
177
+
164
178
  ### The `SessionStart` hook
165
179
 
166
180
  `init` adds this hook. Outside a cloud session it does nothing, and it calls the copy of the
package/dist/block.js ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @file The block of the final messages, like issue-flow's: a title with a status mark, a few
3
+ * aligned rows and free lines, with Kuro (the mascot) beside it. It is drawn only in a terminal;
4
+ * logs, pipes and Claude's context keep the plain `agent-cloud-kit: ` lines.
5
+ */
6
+ /** Kuro, the drawing of issue-flow, without trailing spaces. */
7
+ export const KURO = [" ▄█████▄", " ≋≋██▀▀▀▀▀██", " █ ▄ ▄ █ ╱", " ▀█▄▄▄▄█▀ ╱"];
8
+ /** Column where the text begins, after Kuro. */
9
+ const TEXT_COLUMN = 26;
10
+ /** Indent of the text without Kuro. */
11
+ const INDENT = 2;
12
+ /** ASCII mark of each mood. */
13
+ const MARKS = { ok: "[ok]", error: "[!]", question: "[?]", next: "[>]" };
14
+ /** SGR color of each mood. */
15
+ const MOOD_COLOR = { ok: "32", error: "31", question: "33", next: "36" };
16
+ /**
17
+ * Wraps a text in an SGR code when color is on.
18
+ *
19
+ * @param on - Whether color is on.
20
+ * @param code - SGR code.
21
+ * @param text - Text.
22
+ * @returns The text, colored or not.
23
+ * @example
24
+ * paint(false, "1", "x"); // "x"
25
+ */
26
+ function paint(on, code, text) {
27
+ return on ? `\x1b[${code}m${text}\x1b[0m` : text;
28
+ }
29
+ /**
30
+ * Visible width of a text: characters, without ANSI codes.
31
+ *
32
+ * @param text - Text.
33
+ * @returns The width.
34
+ * @example
35
+ * width("\x1b[1mab\x1b[0m"); // 2
36
+ */
37
+ function width(text) {
38
+ return [...text.replace(/\x1b\[[0-9;]*m/g, "")].length;
39
+ }
40
+ /**
41
+ * Renders a block. Values are never cut or wrapped: a command to copy must stay whole.
42
+ *
43
+ * @param block - What to say.
44
+ * @param opts - Whether to draw Kuro and whether to color.
45
+ * @returns The lines, with a blank line before and after.
46
+ * @example
47
+ * renderBlock({ mood: "ok", title: "Listo" }, { art: false, color: false }); // ["", " [ok] Listo", ""]
48
+ */
49
+ export function renderBlock(block, opts) {
50
+ const code = MOOD_COLOR[block.mood];
51
+ const text = [`${paint(opts.color, code, MARKS[block.mood])} ${paint(opts.color, `1;${code}`, block.title)}`];
52
+ if (block.rows?.length) {
53
+ const column = Math.max(...block.rows.map(([label]) => width(label))) + 2;
54
+ text.push("");
55
+ for (const [label, value] of block.rows)
56
+ text.push(`${paint(opts.color, "2", label)}${" ".repeat(column - width(label))}${value}`);
57
+ }
58
+ if (block.lines?.length)
59
+ text.push("", ...block.lines);
60
+ const out = [];
61
+ if (opts.art) {
62
+ // NOTE: Kuro takes rows 0-3 and the text starts on row 1, next to the head.
63
+ const total = Math.max(KURO.length, text.length + 1);
64
+ for (let row = 0; row < total; row++) {
65
+ const left = KURO[row] ?? "";
66
+ const drawn = left ? paint(opts.color, "35", left) : "";
67
+ const right = text[row - 1];
68
+ out.push(right === undefined || right === "" ? drawn : `${drawn}${" ".repeat(TEXT_COLUMN - width(left))}${right}`);
69
+ }
70
+ }
71
+ else {
72
+ for (const line of text)
73
+ out.push(line === "" ? "" : `${" ".repeat(INDENT)}${line}`);
74
+ }
75
+ return ["", ...out.map((line) => line.trimEnd()), ""];
76
+ }
package/dist/cli.js CHANGED
@@ -2,46 +2,28 @@
2
2
  /**
3
3
  * @file Command line entry point of agent-cloud-kit: wires `setup`, `session`, `docker`, `doctor`,
4
4
  * `init`, `manual`, `prepare` and `verify` to their modules, relaunching or delegating to the version each
5
- * repo pins.
5
+ * repo pins. It picks the language of the terminal once (`--lang`, then `AGENT_CLOUD_KIT_LANG`).
6
6
  */
7
7
  import { execFileSync } from "node:child_process";
8
8
  import { existsSync, readFileSync, realpathSync } from "node:fs";
9
9
  import { resolve } from "node:path";
10
10
  import { createInterface } from "node:readline/promises";
11
11
  import { pathToFileURL } from "node:url";
12
+ import { renderBlock } from "./block.js";
12
13
  import { readConfig } from "./config.js";
13
14
  import { CONFIG_FILE, DOCKERD_LOG, OUTPUT_PREFIX, RUNNING_PACKAGE_ROOT, homeDir, toolsDir } from "./constants.js";
14
- import { doctor } from "./doctor.js";
15
+ import { doctor, doctorBlock } from "./doctor.js";
15
16
  import { dockerResponds, realDocker } from "./docker.js";
16
17
  import { init } from "./init.js";
18
+ import { LANG_ENV, LangError, resolveLang, splitLang } from "./lang.js";
17
19
  import { findRepos, packageBin, planSetup, relaunch } from "./launcher.js";
18
20
  import { MANUAL_FILE, writeManual } from "./manual.js";
21
+ import { stepLabel, t, useLang } from "./messages/index.js";
19
22
  import { EXIT_INTERRUPTED, InterruptedError, branchDay, prepare, verifyBranch } from "./prepare/index.js";
20
23
  import { captureProcess, runProcess } from "./run.js";
21
24
  import { chooseTerminal } from "./select.js";
22
25
  import { session } from "./session.js";
23
26
  import { setupRepo } from "./setup.js";
24
- /** Help text shown by `--help`. */
25
- const HELP = `${OUTPUT_PREFIX}prepara repos para las sesiones cloud de Claude Code.
26
-
27
- Uso: agent-cloud-kit <comando>
28
-
29
- Comandos:
30
- setup [--repo <path>] prepara la máquina para cada repo del directorio home (o solo <path>)
31
- session informa el estado de la máquina y conecta las herramientas a la sesión
32
- docker inicia el demonio de Docker y espera a que responda
33
- doctor revisa los dominios permitidos, las herramientas, Docker y el estado de la máquina
34
- init crea .claude/cloud.json y registra el hook SessionStart
35
- manual escribe docs/agent-cloud.md a partir de .claude/cloud.json
36
- prepare [--pr|--here|--resume]
37
- corre en local: deja el repo listo para la nube, en un PR nuevo (--pr) o en la
38
- rama actual sin commit (--here); sin opción, pregunta dónde
39
- verify corre en local: lanza una sesión cloud que verifica la rama actual (ya subida)
40
-
41
- Opciones:
42
- --help muestra esta ayuda
43
- --version muestra la versión del paquete
44
- `;
45
27
  /** Environment variable with the project dir that Claude Code passes to hooks. */
46
28
  const PROJECT_DIR_VAR = "CLAUDE_PROJECT_DIR";
47
29
  /** Environment marker set on a delegated `session`, so it never delegates again. */
@@ -93,7 +75,7 @@ async function runOne(repo, deps) {
93
75
  for (const error of errors)
94
76
  deps.print(`${OUTPUT_PREFIX}${repo}: ${error}`);
95
77
  if (config && config.version !== deps.packageVersion) {
96
- deps.print(`${OUTPUT_PREFIX}corriendo la versión ${deps.packageVersion}, aunque ${CONFIG_FILE} pide la ${config.version}`);
78
+ deps.print(OUTPUT_PREFIX + t().cli.versionMismatch(deps.packageVersion, CONFIG_FILE, config.version));
97
79
  }
98
80
  const state = await setupRepo(repo, {
99
81
  toolsDir: toolsDir(deps.env),
@@ -103,9 +85,8 @@ async function runOne(repo, deps) {
103
85
  docker: deps.docker,
104
86
  packageRoot: deps.packageRoot,
105
87
  });
106
- const failed = state.steps.filter((s) => !s.ok).map((s) => s.name);
107
- const detail = failed.length ? ` (${failed.join(", ")})` : "";
108
- deps.print(`${OUTPUT_PREFIX}setup ${repo}: ${state.steps.length} pasos, ${failed.length} fallaron${detail}`);
88
+ const failed = state.steps.filter((s) => !s.ok).map((s) => stepLabel(s.name));
89
+ deps.print(OUTPUT_PREFIX + t().cli.setupSummary(repo, state.steps.length, failed.length, failed));
109
90
  }
110
91
  /**
111
92
  * Carries out one item of the setup plan; may throw, the caller reports it and moves on.
@@ -115,19 +96,19 @@ async function runOne(repo, deps) {
115
96
  */
116
97
  async function runPlanItem(item, deps) {
117
98
  if (item.action === "skip") {
118
- deps.print(`${OUTPUT_PREFIX}se omite ${item.repo}: ${item.reason ?? ""}`);
99
+ deps.print(OUTPUT_PREFIX + t().cli.skip(item.repo, item.reason ?? ""));
119
100
  return;
120
101
  }
121
102
  if (item.action === "run" || !item.version) {
122
103
  await runOne(item.repo, deps);
123
104
  return;
124
105
  }
125
- deps.print(`${OUTPUT_PREFIX}relanzando con la versión ${item.version} para ${item.repo}`);
106
+ deps.print(OUTPUT_PREFIX + t().cli.relaunching(item.version, item.repo));
126
107
  try {
127
- await relaunch(item.repo, item.version, deps.run, deps.env);
108
+ await relaunch(item.repo, item.version, deps.run, langEnv(deps.env));
128
109
  }
129
110
  catch (err) {
130
- deps.print(`${OUTPUT_PREFIX}falló el relanzamiento con la versión ${item.version} para ${item.repo}: ${message(err)}`);
111
+ deps.print(OUTPUT_PREFIX + t().cli.relaunchFailed(item.version, item.repo, message(err)));
131
112
  }
132
113
  }
133
114
  /**
@@ -144,25 +125,25 @@ async function setupCommand(args, deps) {
144
125
  if (at >= 0) {
145
126
  const value = args[at + 1];
146
127
  if (!value || value.startsWith("-"))
147
- deps.print(`${OUTPUT_PREFIX}${REPO_OPTION} necesita una ruta`);
128
+ deps.print(OUTPUT_PREFIX + t().cli.repoNeedsPath(REPO_OPTION));
148
129
  else
149
130
  await runOne(resolve(deps.cwd, value), deps);
150
131
  return EXIT_OK;
151
132
  }
152
133
  const repos = findRepos(homeDir(deps.env));
153
134
  if (!repos.length)
154
- deps.print(`${OUTPUT_PREFIX}no se encontraron repos en ${homeDir(deps.env)}`);
135
+ deps.print(OUTPUT_PREFIX + t().cli.noRepos(homeDir(deps.env)));
155
136
  for (const item of planSetup(repos, deps.packageVersion, readConfig)) {
156
137
  try {
157
138
  await runPlanItem(item, deps);
158
139
  }
159
140
  catch (err) {
160
- deps.print(`${OUTPUT_PREFIX}error en setup de ${item.repo}: ${message(err)}`);
141
+ deps.print(OUTPUT_PREFIX + t().cli.setupErrorFor(item.repo, message(err)));
161
142
  }
162
143
  }
163
144
  }
164
145
  catch (err) {
165
- deps.print(`${OUTPUT_PREFIX}error en setup: ${message(err)}`);
146
+ deps.print(OUTPUT_PREFIX + t().cli.setupError(message(err)));
166
147
  }
167
148
  return EXIT_OK;
168
149
  }
@@ -203,10 +184,10 @@ async function sessionCommand(deps) {
203
184
  const delegated = deps.env[DELEGATED_ENV] === DELEGATED_VALUE;
204
185
  if (!delegated && wanted && wanted !== deps.packageVersion && existsSync(packageBin(tools, wanted))) {
205
186
  try {
206
- await deps.run(packageBin(tools, wanted), ["session"], { env: { ...deps.env, [DELEGATED_ENV]: DELEGATED_VALUE } });
187
+ await deps.run(packageBin(tools, wanted), ["session"], { env: { ...langEnv(deps.env), [DELEGATED_ENV]: DELEGATED_VALUE } });
207
188
  }
208
189
  catch (err) {
209
- deps.print(`${OUTPUT_PREFIX}falló session con la versión ${wanted}: ${message(err)}`);
190
+ deps.print(OUTPUT_PREFIX + t().cli.sessionDelegateFailed(wanted, message(err)));
210
191
  }
211
192
  return EXIT_OK;
212
193
  }
@@ -224,7 +205,7 @@ async function sessionCommand(deps) {
224
205
  async function dockerCommand(deps) {
225
206
  if (await deps.docker.ensure().catch(() => false))
226
207
  return EXIT_OK;
227
- deps.print(`${OUTPUT_PREFIX}dockerd no arrancó; revisa ${DOCKERD_LOG}`);
208
+ deps.print(OUTPUT_PREFIX + t().docker.didNotStart(DOCKERD_LOG));
228
209
  return EXIT_FAIL;
229
210
  }
230
211
  /**
@@ -242,6 +223,10 @@ async function doctorCommand(deps) {
242
223
  });
243
224
  for (const line of result.lines)
244
225
  deps.print(line);
226
+ const art = deps.stdoutTty ?? process.stdout.isTTY === true;
227
+ if (art)
228
+ for (const line of renderBlock(doctorBlock(result.counts), { art, color: !deps.env.NO_COLOR }))
229
+ deps.print(line);
245
230
  return result.ok ? EXIT_OK : EXIT_FAIL;
246
231
  }
247
232
  /**
@@ -277,7 +262,7 @@ function manualCommand(deps) {
277
262
  deps.print(`${OUTPUT_PREFIX}${message(err)}`);
278
263
  return EXIT_FAIL;
279
264
  }
280
- deps.print(`${OUTPUT_PREFIX}se escribió ${MANUAL_FILE}`);
265
+ deps.print(OUTPUT_PREFIX + t().cli.manualWritten(MANUAL_FILE));
281
266
  return EXIT_OK;
282
267
  }
283
268
  /**
@@ -353,11 +338,11 @@ async function prepareCommand(args, deps) {
353
338
  const known = [RESUME_OPTION, PR_OPTION, HERE_OPTION];
354
339
  const unknown = args.filter((arg) => !known.includes(arg));
355
340
  if (unknown.length) {
356
- deps.print(`${OUTPUT_PREFIX}opción desconocida para prepare: ${unknown.join(" ")}`);
341
+ deps.print(OUTPUT_PREFIX + t().cli.unknownOption("prepare", unknown.join(" ")));
357
342
  return EXIT_FAIL;
358
343
  }
359
344
  if (args.filter((arg) => known.includes(arg)).length > 1) {
360
- deps.print(`${OUTPUT_PREFIX}usa solo una de ${known.join(", ")}`);
345
+ deps.print(OUTPUT_PREFIX + t().cli.onlyOne(known.join(", ")));
361
346
  return EXIT_FAIL;
362
347
  }
363
348
  const place = args.includes(PR_OPTION) ? "pr" : args.includes(HERE_OPTION) ? "here" : undefined;
@@ -372,7 +357,7 @@ async function prepareCommand(args, deps) {
372
357
  */
373
358
  async function verifyCommand(args, deps) {
374
359
  if (args.length) {
375
- deps.print(`${OUTPUT_PREFIX}opción desconocida para verify: ${args.join(" ")}`);
360
+ deps.print(OUTPUT_PREFIX + t().cli.unknownOption("verify", args.join(" ")));
376
361
  return EXIT_FAIL;
377
362
  }
378
363
  return verifyBranch(deps.cwd, prepareDeps(deps));
@@ -395,10 +380,34 @@ function prepareDeps(deps) {
395
380
  packageRoot: deps.packageRoot ?? RUNNING_PACKAGE_ROOT,
396
381
  today: deps.today ?? branchDay(new Date()),
397
382
  isTty: deps.isTty ?? (process.stdin.isTTY === true && process.stdout.isTTY === true),
398
- color: deps.isTty === undefined && process.stdout.isTTY === true && !deps.env.NO_COLOR,
383
+ color: prepareArt(deps) && !deps.env.NO_COLOR,
384
+ art: prepareArt(deps),
399
385
  onInterrupt: deps.onInterrupt ?? onProcessInterrupt,
400
386
  };
401
387
  }
388
+ /** Language of this run, set by `main`. */
389
+ let currentLang = "es";
390
+ /**
391
+ * The environment for another version of this package: the same, with the language of this run,
392
+ * so a `--lang` given here reaches the relaunched setup or the delegated session.
393
+ *
394
+ * @param env - Process environment.
395
+ * @returns A new environment.
396
+ * @example
397
+ * langEnv({}); // { AGENT_CLOUD_KIT_LANG: "es" }
398
+ */
399
+ function langEnv(env) {
400
+ return { ...env, [LANG_ENV]: currentLang };
401
+ }
402
+ /**
403
+ * Whether `prepare` and `verify` draw Kuro: stdout is a terminal (and no test said otherwise).
404
+ *
405
+ * @param deps - CLI dependencies.
406
+ * @returns True in a terminal.
407
+ */
408
+ function prepareArt(deps) {
409
+ return deps.stdoutTty ?? (deps.isTty === undefined && process.stdout.isTTY === true);
410
+ }
402
411
  /**
403
412
  * Runs the CLI.
404
413
  *
@@ -409,7 +418,20 @@ function prepareDeps(deps) {
409
418
  * process.exitCode = await main(["setup"], realDeps());
410
419
  */
411
420
  export async function main(argv, deps) {
412
- const [command, ...rest] = argv;
421
+ const { args, flag } = splitLang(argv);
422
+ try {
423
+ currentLang = resolveLang(flag, deps.env);
424
+ }
425
+ catch (err) {
426
+ if (!(err instanceof LangError))
427
+ throw err;
428
+ currentLang = "es";
429
+ useLang(currentLang);
430
+ deps.print(OUTPUT_PREFIX + t().lang.invalid(err.source, err.value));
431
+ return EXIT_FAIL;
432
+ }
433
+ useLang(currentLang);
434
+ const [command, ...rest] = args;
413
435
  if (command === "setup")
414
436
  return setupCommand(rest, deps);
415
437
  if (command === "session")
@@ -430,7 +452,7 @@ export async function main(argv, deps) {
430
452
  deps.print(deps.packageVersion);
431
453
  return EXIT_OK;
432
454
  }
433
- deps.print(HELP);
455
+ deps.print(t().help);
434
456
  return command === undefined || command === "--help" ? EXIT_OK : EXIT_FAIL;
435
457
  }
436
458
  /**
package/dist/config.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
  import { CONFIG_FILE } from "./constants.js";
7
+ import { t } from "./messages/index.js";
7
8
  /** Top-level key that editors use to point at the JSON schema; allowed but not part of the config. */
8
9
  const SCHEMA_KEY = "$schema";
9
10
  /** Exact semver: no ranges, no prefixes. */
@@ -78,12 +79,12 @@ function parseDocker(value, errors) {
78
79
  if (value === undefined)
79
80
  return { pull: PULL_AUTO };
80
81
  if (!isRecord(value)) {
81
- errors.push(fail('"docker" debe ser un objeto'));
82
+ errors.push(fail(t().config.dockerNotObject));
82
83
  return { pull: PULL_AUTO };
83
84
  }
84
85
  for (const key of Object.keys(value)) {
85
86
  if (!DOCKER_KEYS.includes(key)) {
86
- errors.push(fail(`campo desconocido "docker.${key}"`));
87
+ errors.push(fail(t().config.unknownField(`docker.${key}`)));
87
88
  }
88
89
  }
89
90
  const pull = value.pull;
@@ -91,7 +92,7 @@ function parseDocker(value, errors) {
91
92
  return { pull: PULL_AUTO };
92
93
  if (pull === false || isStringList(pull))
93
94
  return { pull };
94
- errors.push(fail('"docker.pull" debe ser "auto", false o un arreglo de strings'));
95
+ errors.push(fail(t().config.badPull));
95
96
  return { pull: PULL_AUTO };
96
97
  }
97
98
  /**
@@ -108,7 +109,7 @@ function parseList(raw, key, errors) {
108
109
  return [];
109
110
  if (isStringList(value))
110
111
  return value;
111
- errors.push(fail(`"${key}" debe ser un arreglo de strings`));
112
+ errors.push(fail(t().config.notStringList(key)));
112
113
  return [];
113
114
  }
114
115
  /**
@@ -124,13 +125,13 @@ function parseDomains(value, errors) {
124
125
  if (value === undefined)
125
126
  return [];
126
127
  if (!isStringList(value)) {
127
- errors.push(fail('"domains" debe ser un arreglo de strings'));
128
+ errors.push(fail(t().config.notStringList("domains")));
128
129
  return [];
129
130
  }
130
131
  return value.filter((host) => {
131
132
  const valid = isHostName(host);
132
133
  if (!valid)
133
- errors.push(fail(`"domains" tiene un host inválido: ${JSON.stringify(host)}`));
134
+ errors.push(fail(t().config.badHost(JSON.stringify(host))));
134
135
  return valid;
135
136
  });
136
137
  }
@@ -145,11 +146,11 @@ function parseConfig(raw, errors) {
145
146
  const known = [...CONFIG_KEYS, SCHEMA_KEY];
146
147
  for (const key of Object.keys(raw)) {
147
148
  if (!known.includes(key))
148
- errors.push(fail(`campo desconocido "${key}"`));
149
+ errors.push(fail(t().config.unknownField(key)));
149
150
  }
150
151
  const version = raw.version;
151
152
  if (typeof version !== "string" || !EXACT_SEMVER.test(version)) {
152
- errors.push(fail('"version" debe ser un semver exacto, como "1.0.0"'));
153
+ errors.push(fail(t().config.badVersion));
153
154
  return undefined;
154
155
  }
155
156
  const lists = Object.fromEntries(LIST_KEYS.map((key) => [key, parseList(raw, key, errors)]));
@@ -166,7 +167,7 @@ function parseConfig(raw, errors) {
166
167
  if (typeof raw.notes === "string")
167
168
  config.notes = raw.notes;
168
169
  else if (raw.notes !== undefined)
169
- errors.push(fail('"notes" debe ser un string'));
170
+ errors.push(fail(t().config.notesNotString));
170
171
  return config;
171
172
  }
172
173
  /**
@@ -187,10 +188,10 @@ export function readConfig(repo) {
187
188
  }
188
189
  catch (error) {
189
190
  const reason = error instanceof Error ? error.message : String(error);
190
- return { errors: [fail(`no se pudo leer el archivo: ${reason}`)] };
191
+ return { errors: [fail(t().config.unreadable(reason))] };
191
192
  }
192
193
  if (!isRecord(raw))
193
- return { errors: [fail("debe contener un objeto JSON")] };
194
+ return { errors: [fail(t().config.notObject)] };
194
195
  const errors = [];
195
196
  const config = parseConfig(raw, errors);
196
197
  return config ? { config, errors } : { errors };
package/dist/docker.js CHANGED
@@ -4,14 +4,13 @@
4
4
  import { execFileSync, spawn } from "node:child_process";
5
5
  import { closeSync, openSync } from "node:fs";
6
6
  import { DOCKERD_LOG, DOCKER_WAIT_SECONDS } from "./constants.js";
7
+ import { t } from "./messages/index.js";
7
8
  /** Timeout of each probe command, in milliseconds. */
8
9
  const PROBE_TIMEOUT_MS = 10_000;
9
10
  /** Delay between readiness checks, in milliseconds. */
10
11
  const POLL_MS = 1000;
11
12
  /** Milliseconds in a second. */
12
13
  const MS_PER_SECOND = 1000;
13
- /** Message returned when there is no `dockerd` executable on PATH. */
14
- export const DOCKERD_MISSING = "dockerd no está instalado";
15
14
  /**
16
15
  * Tells whether a command exits with code 0.
17
16
  *
@@ -33,7 +32,7 @@ function succeeds(cmd, args) {
33
32
  *
34
33
  * @returns True when `docker info` succeeds.
35
34
  * @example
36
- * if (!dockerResponds()) console.log("docker is down");
35
+ * if (!dockerResponds()) await ensureDocker();
37
36
  */
38
37
  export function dockerResponds() {
39
38
  return succeeds("docker", ["info"]);
@@ -43,7 +42,7 @@ export function dockerResponds() {
43
42
  * answers or when one is already running. Never throws.
44
43
  *
45
44
  * @param probe - Command probe; the real one when omitted (tests inject a fake).
46
- * @returns `DOCKERD_MISSING` when there is no `dockerd` on PATH, another error message when the
45
+ * @returns The "dockerd is not installed" message when there is no `dockerd` on PATH, another error message when the
47
46
  * daemon could not be started, undefined otherwise.
48
47
  * @example
49
48
  * const problem = startDockerDetached();
@@ -52,7 +51,7 @@ export function startDockerDetached(probe = succeeds) {
52
51
  if (probe("docker", ["info"]))
53
52
  return undefined;
54
53
  if (!probe("sh", ["-c", "command -v dockerd"]))
55
- return DOCKERD_MISSING;
54
+ return t().docker.missing;
56
55
  if (probe("pgrep", ["-x", "dockerd"]))
57
56
  return undefined;
58
57
  try {
@@ -78,7 +77,7 @@ export function startDockerDetached(probe = succeeds) {
78
77
  * @param probe - Command probe; the real one when omitted (tests inject a fake).
79
78
  * @returns True when Docker answers in time; false when it never does or cannot start.
80
79
  * @example
81
- * if (await ensureDocker()) console.log("docker is up");
80
+ * const up = await ensureDocker();
82
81
  */
83
82
  export async function ensureDocker(probe = succeeds) {
84
83
  if (startDockerDetached(probe))
package/dist/doctor.js CHANGED
@@ -8,6 +8,7 @@ import { readConfig } from "./config.js";
8
8
  import { CONFIG_FILE, OUTPUT_PREFIX } from "./constants.js";
9
9
  import { TOOLS, readToolVersions } from "./detect.js";
10
10
  import { isValidRequest, toolExecutable } from "./installers.js";
11
+ import { stepLabel, t } from "./messages/index.js";
11
12
  import { expectedFingerprint } from "./session.js";
12
13
  import { SKILL_DIR, skillVersion } from "./skill.js";
13
14
  import { readState } from "./state.js";
@@ -49,10 +50,10 @@ async function checkDomain(host, capture) {
49
50
  try {
50
51
  const status = (await capture("curl", args)).trim();
51
52
  const ok = status !== BLOCKED_STATUS && status !== NO_ANSWER_STATUS;
52
- return { text: `red ${host}: ${ok ? "ok" : "bloqueado"}`, level: verdict(ok) };
53
+ return { text: t().doctor.domain(host, ok), level: verdict(ok) };
53
54
  }
54
55
  catch {
55
- return { text: `red ${host}: bloqueado`, level: "fail" };
56
+ return { text: t().doctor.domain(host, false), level: "fail" };
56
57
  }
57
58
  }
58
59
  /**
@@ -82,12 +83,12 @@ function checkTools(repo, toolsDir, state) {
82
83
  if (!version)
83
84
  return [];
84
85
  if (!isValidRequest(tool, version))
85
- return [{ text: `herramienta ${tool} ${version}: versión inválida`, level: "fail" }];
86
+ return [{ text: t().doctor.toolInvalid(tool, version), level: "fail" }];
86
87
  const executable = toolExecutable(tool, version, toolsDir, state?.resolved ?? {});
87
88
  if (!executable)
88
- return [{ text: `herramienta ${tool} ${version}: sin resolver (setup no la instaló)`, level: "fail" }];
89
+ return [{ text: t().doctor.toolUnresolved(tool, version), level: "fail" }];
89
90
  const present = existsSync(executable);
90
- return [{ text: `herramienta ${tool} ${version}: ${present ? "ok" : "falta"}`, level: verdict(present) }];
91
+ return [{ text: t().doctor.tool(tool, version, present), level: verdict(present) }];
91
92
  });
92
93
  }
93
94
  /**
@@ -99,19 +100,19 @@ function checkTools(repo, toolsDir, state) {
99
100
  */
100
101
  function checkState(repo, ctx) {
101
102
  if (!existsSync(join(repo, CONFIG_FILE)))
102
- return [{ text: `${CONFIG_FILE}: falta`, level: "fail" }];
103
+ return [{ text: t().doctor.fileMissing(CONFIG_FILE), level: "fail" }];
103
104
  const { config, errors } = readConfig(repo);
104
105
  const out = errors.map((text) => ({ text: `${CONFIG_FILE}: ${text}`, level: "fail" }));
105
106
  if (!config)
106
107
  return out;
107
108
  const state = readState(ctx.toolsDir, repo);
108
109
  if (!state)
109
- return [...out, { text: "estado: falta (setup no corrió)", level: "fail" }];
110
+ return [...out, { text: t().doctor.stateMissing, level: "fail" }];
110
111
  const same = expectedFingerprint(repo, config, state) === state.fingerprint;
111
- out.push({ text: `huella: ${same ? "ok" : "distinta"}`, level: verdict(same) });
112
+ out.push({ text: t().doctor.fingerprint(same), level: verdict(same) });
112
113
  for (const s of state.steps) {
113
114
  const seconds = (s.ms / MS_PER_SECOND).toFixed(DURATION_DECIMALS);
114
- out.push({ text: `paso ${s.name}: ${s.ok ? "ok" : "falló"} (${seconds}s)`, level: verdict(s.ok) });
115
+ out.push({ text: t().doctor.step(stepLabel(s.name), s.ok, seconds), level: verdict(s.ok) });
115
116
  }
116
117
  return out;
117
118
  }
@@ -132,10 +133,7 @@ function checkPlugins(repo) {
132
133
  return [];
133
134
  return Object.entries(plugins)
134
135
  .filter(([, enabled]) => enabled === true)
135
- .map(([name]) => ({
136
- text: `aviso: el plugin ${name} está activado pero no está en el repo; no llegará a la nube (copia sus skills con skills-manager)`,
137
- level: "warn",
138
- }));
136
+ .map(([name]) => ({ text: t().doctor.plugin(name), level: "warn" }));
139
137
  }
140
138
  catch {
141
139
  return [];
@@ -154,12 +152,7 @@ function checkSkill(repo, packageVersion) {
154
152
  const installed = skillVersion(repo);
155
153
  if (installed === undefined || installed === packageVersion)
156
154
  return [];
157
- return [
158
- {
159
- text: `aviso: ${SKILL_DIR} está en ${installed} y el paquete en ${packageVersion}; corre npx agent-cloud-kit@${packageVersion} init`,
160
- level: "warn",
161
- },
162
- ];
155
+ return [{ text: t().doctor.skill(SKILL_DIR, installed, packageVersion), level: "warn" }];
163
156
  }
164
157
  /**
165
158
  * Runs one group of checks, turning anything it throws into a failed check.
@@ -173,7 +166,7 @@ async function guarded(name, fn) {
173
166
  return await fn();
174
167
  }
175
168
  catch (err) {
176
- return [{ text: `${name}: error: ${err instanceof Error ? err.message : String(err)}`, level: "fail" }];
169
+ return [{ text: t().doctor.groupError(name, err instanceof Error ? err.message : String(err)), level: "fail" }];
177
170
  }
178
171
  }
179
172
  /**
@@ -187,14 +180,32 @@ async function guarded(name, fn) {
187
180
  */
188
181
  export async function doctor(repo, ctx) {
189
182
  const checks = [];
190
- checks.push(...(await guarded("red", async () => Promise.all(domainsToProbe(repo).map((host) => checkDomain(host, ctx.capture))))));
191
- checks.push(...(await guarded("herramientas", () => checkTools(repo, ctx.toolsDir, readState(ctx.toolsDir, repo)))));
192
- checks.push(...(await guarded("docker", () => {
183
+ const groups = t().doctor.groups;
184
+ checks.push(...(await guarded(groups.network, async () => Promise.all(domainsToProbe(repo).map((host) => checkDomain(host, ctx.capture))))));
185
+ checks.push(...(await guarded(groups.tools, () => checkTools(repo, ctx.toolsDir, readState(ctx.toolsDir, repo)))));
186
+ checks.push(...(await guarded(groups.docker, () => {
193
187
  const docker = ctx.dockerInfo();
194
- return [{ text: `docker: ${docker ? "ok" : "falta"}`, level: verdict(docker) }];
188
+ return [{ text: t().doctor.docker(docker), level: verdict(docker) }];
195
189
  })));
196
- checks.push(...(await guarded("estado", () => checkState(repo, ctx))));
197
- checks.push(...(await guarded("plugins", () => checkPlugins(repo))));
198
- checks.push(...(await guarded("skill", () => checkSkill(repo, ctx.packageVersion))));
199
- return { lines: checks.map((c) => OUTPUT_PREFIX + c.text), ok: checks.every((c) => c.level !== "fail") };
190
+ checks.push(...(await guarded(groups.state, () => checkState(repo, ctx))));
191
+ checks.push(...(await guarded(groups.plugins, () => checkPlugins(repo))));
192
+ checks.push(...(await guarded(groups.skill, () => checkSkill(repo, ctx.packageVersion))));
193
+ const failed = checks.filter((c) => c.level === "fail").length;
194
+ const warnings = checks.filter((c) => c.level === "warn").length;
195
+ return { lines: checks.map((c) => OUTPUT_PREFIX + c.text), ok: failed === 0, counts: { total: checks.length, failed, warnings } };
196
+ }
197
+ /**
198
+ * The final block of `doctor`, shown with Kuro in a terminal.
199
+ *
200
+ * @param counts - Checks that ran, failed and warned.
201
+ * @returns What the block says.
202
+ * @example
203
+ * doctorBlock({ total: 9, failed: 0, warnings: 0 }); // { mood: "ok", title: "Todo en orden" }
204
+ */
205
+ export function doctorBlock(counts) {
206
+ const m = t().doctor;
207
+ const lines = counts.warnings ? [m.warnings(counts.warnings)] : [];
208
+ if (!counts.failed)
209
+ return { mood: "ok", title: m.allOk, lines };
210
+ return { mood: "error", title: m.someFailed(counts.failed, counts.total), lines: [...lines, m.failedHint] };
200
211
  }